feat: reconcile tool result echoes

This commit is contained in:
2026-08-03 16:19:18 +08:00
parent 10d95c4a5e
commit b3c33f69e7
2 changed files with 29 additions and 2 deletions
+6
View File
@@ -74,6 +74,12 @@ M0-01M0-08 已完成。Mac 经 Linux Tailscale Web Host `http://100.121.118.1
Conversation Store v5 持久化任务快照,刷新或重新登录后仍只显示任务最新状态。尚未创建取消 outbox、尚未发送任何网络事件、尚未实现 result echo 或 Hermes Adapter 契约。`npm test` 当前为 4 个文件 / 22 个测试通过,`npm run build` 通过。 Conversation Store v5 持久化任务快照,刷新或重新登录后仍只显示任务最新状态。尚未创建取消 outbox、尚未发送任何网络事件、尚未实现 result echo 或 Hermes Adapter 契约。`npm test` 当前为 4 个文件 / 22 个测试通过,`npm run build` 通过。
### M1-05 `tool.result` 回声(已完成)
每张可信 Tool Call 卡片均以 `call_id` 建立展示索引。同步到达的合法 `tool.result``tool.cancel` 只会更新同一张 choice / confirm / input 卡片的最终状态,并禁用控件;它们不会再被渲染为独立系统消息或“不支持的 Agent 内容”。未关联到本地卡片的 result/cancel 保持无展示副作用。
本项没有接入 Adapter 的实际发送、Agent 侧幂等消费或审计;这些是 M1-06 的唯一范围。`npm test` 当前为 4 个文件 / 22 个测试通过,`npm run build` 通过。
### 当前结构债务(M0 的改造对象) ### 当前结构债务(M0 的改造对象)
`src/main.ts` 目前包含 App Shell、页面 DOM、会话 cursor、同步循环与 Runtime 编排;M0-05 已将登录、发送与同步 HTTP `fetch` 迁入 `src/runtime/transport-adapter.ts`M0-06 已将所有可信 DOM renderer 迁入 `src/runtime/trusted-dom-renderers.ts`。在 M0 准入前不新增 Surface 或 Capability 功能。 `src/main.ts` 目前包含 App Shell、页面 DOM、会话 cursor、同步循环与 Runtime 编排;M0-05 已将登录、发送与同步 HTTP `fetch` 迁入 `src/runtime/transport-adapter.ts`M0-06 已将所有可信 DOM renderer 迁入 `src/runtime/trusted-dom-renderers.ts`。在 M0 准入前不新增 Surface 或 Capability 功能。
+23 -2
View File
@@ -35,6 +35,7 @@ export type TaskInteractionHandlers = {
export class TrustedDOMRendererContext { export class TrustedDOMRendererContext {
private thinking: HTMLElement | undefined; private thinking: HTMLElement | undefined;
private readonly userRows = new Map<string, HTMLElement>(); private readonly userRows = new Map<string, HTMLElement>();
private readonly toolCards = new Map<string, { card: HTMLElement; status: HTMLElement; controls: readonly (HTMLButtonElement | HTMLInputElement | HTMLTextAreaElement)[] }>();
private readonly taskCards = new Map<string, { card: HTMLElement; title: HTMLElement; progress: HTMLProgressElement; status: HTMLElement; cancel: HTMLButtonElement }>(); private readonly taskCards = new Map<string, { card: HTMLElement; title: HTMLElement; progress: HTMLProgressElement; status: HTMLElement; cancel: HTMLButtonElement }>();
private readonly expirationTimers = new Set<number>(); private readonly expirationTimers = new Set<number>();
@@ -48,6 +49,7 @@ export class TrustedDOMRendererContext {
public reset(): void { public reset(): void {
this.thinking = undefined; this.thinking = undefined;
this.userRows.clear(); this.userRows.clear();
this.toolCards.clear();
this.taskCards.clear(); this.taskCards.clear();
for (const timer of this.expirationTimers) window.clearTimeout(timer); for (const timer of this.expirationTimers) window.clearTimeout(timer);
this.expirationTimers.clear(); this.expirationTimers.clear();
@@ -107,11 +109,15 @@ export class TrustedDOMRendererContext {
} }
public renderToolResult(item: Extract<ConversationItem, { kind: "tool-result" }>): void { public renderToolResult(item: Extract<ConversationItem, { kind: "tool-result" }>): void {
this.renderSystemMessage(`收到工具调用结果(${item.result.call_id} · ${item.result.status})。`); const entry = this.toolCards.get(item.result.call_id);
if (!entry) return;
this.updateToolCardFinal(entry, item.result.status, item.result.error);
} }
public renderToolCancel(item: Extract<ConversationItem, { kind: "tool-cancel" }>): void { public renderToolCancel(item: Extract<ConversationItem, { kind: "tool-cancel" }>): void {
this.renderSystemMessage(`工具调用已取消(${item.cancel.call_id})。`); const entry = this.toolCards.get(item.cancel.call_id);
if (!entry) return;
this.updateToolCardFinal(entry, "cancelled", item.cancel.reason);
} }
public renderSurface(item: Extract<ConversationItem, { kind: "surface" }>): void { public renderSurface(item: Extract<ConversationItem, { kind: "surface" }>): void {
@@ -286,6 +292,7 @@ export class TrustedDOMRendererContext {
card.append(heading, detail, actions); card.append(heading, detail, actions);
if (group.mode === "multi-choice") card.append(submitButton); if (group.mode === "multi-choice") card.append(submitButton);
card.append(status); card.append(status);
this.toolCards.set(call.call_id, { card, status, controls: buttons });
this.messages.append(card); this.messages.append(card);
this.scheduleExpiry(card, status, call, buttons); this.scheduleExpiry(card, status, call, buttons);
this.scrollLatest(); this.scrollLatest();
@@ -323,6 +330,7 @@ export class TrustedDOMRendererContext {
}); });
actions.append(approve, cancel); actions.append(approve, cancel);
card.append(heading, detail, actions, status); card.append(heading, detail, actions, status);
this.toolCards.set(call.call_id, { card, status, controls: buttons });
this.messages.append(card); this.messages.append(card);
this.scheduleExpiry(card, status, call, buttons); this.scheduleExpiry(card, status, call, buttons);
this.scrollLatest(); this.scrollLatest();
@@ -387,6 +395,7 @@ export class TrustedDOMRendererContext {
actions.append(submit, cancel); actions.append(submit, cancel);
nativeForm.append(errors, actions); nativeForm.append(errors, actions);
card.append(heading, detail, nativeForm, status); card.append(heading, detail, nativeForm, status);
this.toolCards.set(call.call_id, { card, status, controls });
this.messages.append(card); this.messages.append(card);
this.scheduleExpiry(card, status, call, controls); this.scheduleExpiry(card, status, call, controls);
this.scrollLatest(); this.scrollLatest();
@@ -432,6 +441,18 @@ export class TrustedDOMRendererContext {
status.textContent = "该交互已结束或已提交"; status.textContent = "该交互已结束或已提交";
} }
private updateToolCardFinal(entry: { card: HTMLElement; status: HTMLElement; controls: readonly (HTMLButtonElement | HTMLInputElement | HTMLTextAreaElement)[] }, finalStatus: "completed" | "failed" | "cancelled" | "expired", error?: string): void {
entry.card.dataset.status = finalStatus;
entry.status.textContent = finalStatus === "completed"
? "已完成"
: finalStatus === "cancelled"
? "已取消"
: finalStatus === "expired"
? "该交互已过期"
: `处理失败${error ? `${error}` : ""}`;
for (const control of entry.controls) control.disabled = true;
}
private showThinking(detail: string): void { private showThinking(detail: string): void {
this.thinking?.remove(); this.thinking?.remove();
this.thinking = this.appendBubble("agent", detail); this.thinking = this.appendBubble("agent", detail);