fix(chat): support Hermes clarify batches
Build / build (push) Canceled after 0s
Playwright / e2e (push) Canceled after 0s

This commit is contained in:
2026-11-09 09:14:42 +08:00
parent 96aac671b3
commit f935385356
10 changed files with 233 additions and 10 deletions
@@ -189,6 +189,11 @@ const queuedMessages = computed(() => {
const visibleApproval = computed(() => chatStore.activePendingApproval);
const visibleClarify = computed(() => chatStore.activePendingClarify);
const clarifyResponse = ref("");
const clarifySelections = ref<string[]>([]);
watch(() => visibleClarify.value?.clarifyId, () => {
clarifyResponse.value = "";
clarifySelections.value = [];
});
const hasFloatingPrompt = computed(() => !!visibleApproval.value || !!visibleClarify.value);
const virtualListPadding = computed(() => {
if (queuedMessages.value.length > 0 && hasFloatingPrompt.value) return "20px 20px 380px";
@@ -242,6 +247,13 @@ function handleClarify(response?: string) {
clarifyResponse.value = "";
}
function handleClarifyMulti() {
const responses = [...clarifySelections.value];
const custom = clarifyResponse.value.trim();
if (custom) responses.push(custom);
handleClarify(responses.length ? JSON.stringify(responses) : '');
}
function removeQueuedMessage(messageId: string) {
const sid = chatStore.activeSessionId;
if (!sid) return;
@@ -829,7 +841,10 @@ defineExpose({
</div>
<div class="approval-float-title">{{ t("chat.clarifyTitle") }}</div>
<div class="approval-float-desc">{{ visibleClarify.question }}</div>
<div v-if="visibleClarify.choices && visibleClarify.choices.length" class="approval-float-actions">
<a-checkbox-group v-if="visibleClarify.multiSelect && visibleClarify.choices?.length" v-model="clarifySelections" class="approval-float-actions">
<a-checkbox v-for="choice in visibleClarify.choices" :key="choice" :value="choice">{{ choice }}</a-checkbox>
</a-checkbox-group>
<div v-else-if="visibleClarify.choices && visibleClarify.choices.length" class="approval-float-actions">
<a-button
v-for="choice in visibleClarify.choices"
:key="choice"
@@ -839,19 +854,19 @@ defineExpose({
>
{{ choice }}
</a-button>
<a-button size="small" status="danger" type="secondary" @click="handleClarify('')">
{{ t("chat.clarifyDismiss") }}
</a-button>
</div>
<div v-else class="clarify-float-input-row">
<div class="clarify-float-input-row">
<a-input
v-model="clarifyResponse"
size="small"
:placeholder="t('chat.clarifyPlaceholder')"
/>
<a-button size="small" type="primary" @click="handleClarify()">
<a-button size="small" type="primary" @click="visibleClarify.multiSelect ? handleClarifyMulti() : handleClarify()">
{{ t("chat.clarifySubmit") }}
</a-button>
<a-button size="small" status="danger" type="secondary" @click="handleClarify('')">
{{ t("chat.clarifyDismiss") }}
</a-button>
</div>
</div>
</Transition>
@@ -88,6 +88,7 @@ export interface PendingClarify {
clarifyId: string
question: string
choices: string[] | null
multiSelect: boolean
timeoutMs: number
requestedAt: number
}
@@ -2113,6 +2114,7 @@ export const useChatStore = defineStore('chat', () => {
clarifyId,
question: String((evt as any).question || ''),
choices: Array.isArray((evt as any).choices) ? (evt as any).choices : null,
multiSelect: !!(evt as any).multi_select,
timeoutMs: Number((evt as any).timeout_ms) || 300000,
requestedAt: Date.now(),
})
@@ -0,0 +1,41 @@
"""Adapt Hermes batch clarify callbacks to the bridge's per-question UI."""
from __future__ import annotations
import queue
import uuid
def make_clarify_callback(pool, session_id, legacy_callback):
def callback(questions, choices=None):
# Older Hermes versions call callback(question, choices) and expect text.
if not isinstance(questions, list):
return legacy_callback(questions, choices)
answers = {}
for index, item in enumerate(questions):
if not isinstance(item, dict):
raise ValueError('invalid clarify question')
question_id = str(item.get('qid') or f'q{index}')
clarify_id = uuid.uuid4().hex
response_queue = queue.Queue(maxsize=1)
with pool._lock:
pool._clarify_requests[clarify_id] = response_queue
try:
pool._append_event(session_id, {
'event': 'clarify.requested',
'clarify_id': clarify_id,
'question': str(item.get('question') or ''),
'choices': item.get('choices') or None,
'multi_select': bool(item.get('multi_select')),
'timeout_ms': 300_000,
})
try:
response = response_queue.get(timeout=300)
except queue.Empty:
return {'answers': answers, 'outcome': 'timed_out'}
answers[question_id] = response or None
finally:
with pool._lock:
pool._clarify_requests.pop(clarify_id, None)
return {'answers': answers, 'outcome': 'submitted'}
return callback
@@ -0,0 +1,25 @@
#!/usr/bin/env python3
"""Launch the read-only vendored bridge with local compatibility adapters."""
from pathlib import Path
import sys
compat_dir = Path(__file__).resolve().parent
sys.path.insert(0, str(compat_dir))
sys.path.insert(0, str(compat_dir.parent / 'python'))
import hermes_bridge as bridge
from clarify_compat import make_clarify_callback
original_callback = bridge.AgentPool._clarify_callback
def clarify_callback(self, session_id):
return make_clarify_callback(self, session_id, original_callback(self, session_id))
bridge.AgentPool._clarify_callback = clarify_callback
# Workers must launch this same compatibility entrypoint, not the vendored one.
bridge._transport.__file__ = __file__
if __name__ == '__main__':
raise SystemExit(bridge.main())
@@ -332,11 +332,11 @@ export function buildAgentBridgeLaunchArgs(command: BridgeCommand, script: strin
function bridgeScriptPath(): string {
const candidates = [
// Built server: dist/server/index.js -> dist/server/agent-bridge/python/hermes_bridge.py
resolve(__dirname, 'agent-bridge', 'python', 'hermes_bridge.py'),
// Built server: dist/server/index.js -> dist/server/agent-bridge/compat/hermes_bridge.py
resolve(__dirname, 'agent-bridge', 'compat', 'hermes_bridge.py'),
// ts-node/dev source tree.
resolve(__dirname, 'services/hermes/agent-bridge/python/hermes_bridge.py'),
resolve(process.cwd(), 'packages/server/src/services/hermes/agent-bridge/python/hermes_bridge.py'),
resolve(__dirname, 'services/hermes/agent-bridge/compat/hermes_bridge.py'),
resolve(process.cwd(), 'packages/server/src/services/hermes/agent-bridge/compat/hermes_bridge.py'),
]
const found = candidates.find(candidate => existsSync(candidate))
if (!found) {
@@ -1202,6 +1202,7 @@ async function applyBridgeChunkAsync(
clarify_id: ev.clarify_id,
question: ev.question,
choices: Array.isArray(ev.choices) ? ev.choices : null,
multi_select: Boolean(ev.multi_select),
timeout_ms: ev.timeout_ms,
}
replaceState(sessionMap, sessionId, 'clarify.requested', payload)