From 4744f1f20a6289cb8f4f865f64b3e5d99a91d806 Mon Sep 17 00:00:00 2001 From: Huarch Date: Fri, 25 Sep 2026 16:23:00 +0800 Subject: [PATCH] fix(agent): derive activity stages from tool actions --- src/routes/chatActivityTracker.ts | 95 ++++++++++++++++++++++-- tests/routes/chatActivityTracker.test.ts | 64 ++++++++++++++++ 2 files changed, 152 insertions(+), 7 deletions(-) diff --git a/src/routes/chatActivityTracker.ts b/src/routes/chatActivityTracker.ts index bf726ac..1f6ea7c 100644 --- a/src/routes/chatActivityTracker.ts +++ b/src/routes/chatActivityTracker.ts @@ -2,6 +2,7 @@ import type { Part } from "@opencode-ai/sdk/v2"; import { getToolLabel, + initialActivity, normalizeToolStatus, type ActivityActionPayload, type ActivityPayload, @@ -12,6 +13,51 @@ import { type ToolPart = Extract; type ActivityContext = Pick; +type AutomaticStageKind = "reference" | "data" | "analysis" | "visualization" | "other"; + +const automaticStages: Record> = { + reference: { + title: "查找所需资料", + reason: "正在检索完成本次请求所需的工作流和参考信息。", + }, + data: { + title: "获取业务数据", + reason: "正在查询本次分析所需的数据和业务能力。", + }, + analysis: { + title: "处理分析数据", + reason: "正在对已获取的信息进行计算和核查。", + }, + visualization: { + title: "更新地图与图表", + reason: "正在将分析内容呈现在地图或图表中。", + }, + other: { + title: "执行分析操作", + reason: "正在处理完成本次请求所需的操作。", + }, +}; + +const referenceTools = new Set(["skill", "read", "glob", "grep", "session_search"]); +const dataTools = new Set(["tjwater_cli", "web_search", "geocode"]); +const visualizationTools = new Set([ + "store_render_ref", + "locate_features", + "zoom_to_map", + "view_scada", + "show_chart", + "render_junctions", + "view_history", + "apply_layer_style", +]); + +const getAutomaticStageKind = (tool: string): AutomaticStageKind => { + if (referenceTools.has(tool)) return "reference"; + if (dataTools.has(tool)) return "data"; + if (tool === "bash") return "analysis"; + if (visualizationTools.has(tool)) return "visualization"; + return "other"; +}; const getActionTarget = (params: Record) => { for (const key of ["command", "file_path", "filePath", "path", "query", "keyword"]) { @@ -33,6 +79,9 @@ export const createActivityTracker = ({ const activities = new Map(); const actionActivityIds = new Map(); let currentActivityId: string | null = null; + let currentStageOrigin: "startup" | "explicit" | "automatic" = "startup"; + let currentAutomaticKind: AutomaticStageKind | null = null; + let automaticStageCount = 0; const emit = (activity: ActivityPayload, todos?: TodoItemPayload[]) => { const now = Date.now(); @@ -88,11 +137,13 @@ export const createActivityTracker = ({ emit(nextActivity); }; - const start = ( + const startStage = ( id: string, title: string, reason: string, - todos?: TodoItemPayload[], + todos: TodoItemPayload[] | undefined, + origin: typeof currentStageOrigin, + automaticKind: AutomaticStageKind | null = null, ) => { finalize("completed"); const activity: ActivityPayload = { @@ -106,17 +157,47 @@ export const createActivityTracker = ({ }; activities.set(id, activity); currentActivityId = id; + currentStageOrigin = origin; + currentAutomaticKind = automaticKind; emit(activity, todos); return activity; }; + const start = ( + id: string, + title: string, + reason: string, + todos?: TodoItemPayload[], + ) => startStage( + id, + title, + reason, + todos, + id === initialActivity.id ? "startup" : "explicit", + ); + const ensure = (tool: string) => { const activity = current(); - if (activity?.status === "running") return activity; - return start( - `activity-fallback-${Date.now().toString(36)}`, - "执行分析操作", - `为完成当前请求,需要使用${getToolLabel(tool)}处理相关信息。`, + if (activity?.status === "running" && currentStageOrigin === "explicit") { + return activity; + } + const kind = getAutomaticStageKind(tool); + if ( + activity?.status === "running" && + currentStageOrigin === "automatic" && + (currentAutomaticKind === kind || + activity.actions.some((action) => action.status === "running")) + ) { + return activity; + } + const stage = automaticStages[kind]; + return startStage( + `activity-auto-${++automaticStageCount}`, + stage.title, + stage.reason, + undefined, + "automatic", + kind, ); }; diff --git a/tests/routes/chatActivityTracker.test.ts b/tests/routes/chatActivityTracker.test.ts index b0c4248..ae47697 100644 --- a/tests/routes/chatActivityTracker.test.ts +++ b/tests/routes/chatActivityTracker.test.ts @@ -1,8 +1,72 @@ import { describe, expect, it } from "bun:test"; import { createActivityTracker } from "../../src/routes/chatActivityTracker.js"; +import { initialActivity } from "../../src/routes/chatStreamEvents.js"; describe("createActivityTracker", () => { + it("moves real tool work out of preparation when the model omits activity updates", () => { + const events: Array<{ event: string; data: Record }> = []; + const tracker = createActivityTracker({ + clientSessionId: "client-session-1", + write: (event, data) => events.push({ event, data }), + }); + const action = (id: string, tool: string, status: string) => + tracker.upsertAction( + { id, tool, state: { status, input: {} } } as never, + {}, + ); + + tracker.start(initialActivity.id, initialActivity.title, initialActivity.reason); + action("skill-1", "skill", "completed"); + action("read-1", "read", "completed"); + action("data-1", "tjwater_cli", "completed"); + action("data-1", "tjwater_cli", "completed"); + action("compute-1", "bash", "completed"); + action("map-1", "show_chart", "completed"); + tracker.finalize("completed"); + + const started = events + .filter(({ data }) => + (data.activity as { actions: unknown[] }).actions.length === 0 && + (data.activity as { status: string }).status === "running", + ) + .map(({ data }) => (data.activity as { title: string }).title); + expect(started).toEqual([ + initialActivity.title, + "查找所需资料", + "获取业务数据", + "处理分析数据", + "更新地图与图表", + ]); + expect(tracker.getActionContext("read-1")?.title).toBe("查找所需资料"); + expect(tracker.getActionContext("data-1")?.title).toBe("获取业务数据"); + expect(tracker.getActionContext("map-1")?.title).toBe("更新地图与图表"); + }); + + it("keeps a model-provided stage across tool categories", () => { + const events: Array<{ event: string; data: Record }> = []; + const tracker = createActivityTracker({ + clientSessionId: "client-session-1", + write: (event, data) => events.push({ event, data }), + }); + tracker.start(initialActivity.id, initialActivity.title, initialActivity.reason); + tracker.start("model-stage", "验证分析结果", "核对本次计算与数据。"); + for (const [id, tool] of [["tool-1", "tjwater_cli"], ["tool-2", "show_chart"]]) { + tracker.upsertAction( + { id, tool, state: { status: "completed", input: {} } } as never, + {}, + ); + } + expect(tracker.getActionContext("tool-1")?.title).toBe("验证分析结果"); + expect(tracker.getActionContext("tool-2")?.title).toBe("验证分析结果"); + expect( + events.filter(({ data }) => + (data.activity as { actions: unknown[] }).actions.length === 0 && + (data.activity as { status: string }).status === "running", + ), + ).toHaveLength(2); + }); + it("groups actions and closes running children with their activity", () => { const events: Array<{ event: string; data: Record }> = []; const tracker = createActivityTracker({