fix(acp): 复用工具调用回放详情

This commit is contained in:
jiwangyihao
2026-05-17 05:13:49 +08:00
parent b53fc964ce
commit 359a7ab448
3 changed files with 115 additions and 12 deletions
@@ -66,7 +66,11 @@ import {
import { ACP_BUILTIN_SLASH_COMMANDS, executeAcpBuiltinSlashCommand } from "../../slash-commands/acp-builtins";
import { parseThinkingLevel } from "../../thinking";
import { createAcpClientBridge } from "./acp-client-bridge";
import { mapAgentSessionEventToAcpSessionUpdates, mapToolKind } from "./acp-event-mapper";
import {
buildToolCallStartUpdate,
mapAgentSessionEventToAcpSessionUpdates,
normalizeReplayToolArguments,
} from "./acp-event-mapper";
import { ACP_TERMINAL_AUTH_FLAG } from "./terminal-auth";
const ACP_DEFAULT_MODE_ID = "default";
@@ -1590,7 +1594,7 @@ export class AcpAgent implements Agent {
#messageToReplayNotifications(sessionId: string, message: ReplayableMessage, cwd: string): SessionNotification[] {
if (message.role === "assistant") {
return this.#replayAssistantMessage(sessionId, message);
return this.#replayAssistantMessage(sessionId, message, cwd);
}
if (
message.role === "user" ||
@@ -1631,7 +1635,7 @@ export class AcpAgent implements Agent {
return [];
}
#replayAssistantMessage(sessionId: string, message: ReplayableMessage): SessionNotification[] {
#replayAssistantMessage(sessionId: string, message: ReplayableMessage, cwd: string): SessionNotification[] {
const notifications: SessionNotification[] = [];
const messageId = crypto.randomUUID();
if (Array.isArray(message.content)) {
@@ -1673,16 +1677,14 @@ export class AcpAgent implements Agent {
"name" in item &&
typeof item.name === "string"
) {
const update: SessionUpdate = {
sessionUpdate: "tool_call",
const args = normalizeReplayToolArguments("arguments" in item ? item.arguments : undefined).args;
const update = buildToolCallStartUpdate({
toolCallId: item.id,
title: item.name,
kind: mapToolKind(item.name),
toolName: item.name,
args,
status: "completed",
};
if ("arguments" in item && typeof item.arguments === "string") {
update.rawInput = item.arguments;
}
cwd,
});
notifications.push({ sessionId, update });
}
}
@@ -335,6 +335,18 @@ export function buildToolCallStartUpdate(input: {
return update;
}
export function normalizeReplayToolArguments(value: unknown): { args: unknown } {
if (typeof value !== "string") {
return { args: value ?? {} };
}
try {
const parsed: unknown = JSON.parse(value);
return { args: parsed };
} catch {
return { args: value };
}
}
function buildToolStartContent(toolName: string, args: unknown): ToolCallContent[] {
if (!isCommandToolName(toolName)) {
return [];
@@ -2,7 +2,11 @@ import { describe, expect, it } from "bun:test";
import path from "node:path";
import type { SessionNotification } from "@agentclientprotocol/sdk";
import { zSessionNotification } from "@agentclientprotocol/sdk/dist/schema/zod.gen.js";
import { mapAgentSessionEventToAcpSessionUpdates } from "../src/modes/acp/acp-event-mapper";
import {
buildToolCallStartUpdate,
mapAgentSessionEventToAcpSessionUpdates,
normalizeReplayToolArguments,
} from "../src/modes/acp/acp-event-mapper";
import type { AgentSessionEvent } from "../src/session/agent-session";
import { expectAcpStructure, expectAcpStructureRejects } from "./helpers/acp-schema";
@@ -273,6 +277,91 @@ describe("ACP event mapper", () => {
expect(update.content).toEqual([{ type: "content", content: { type: "text", text: "$ npm run check" } }]);
});
it("builds replayed bash tool calls from JSON string arguments", () => {
const replayArgs = normalizeReplayToolArguments(JSON.stringify({ command: "npm test", cwd: "/repo" }));
const update = buildToolCallStartUpdate({
toolCallId: "toolu_replay_1",
toolName: "bash",
args: replayArgs.args,
status: "completed",
});
expectAcpStructure(zSessionNotification, { sessionId: "session-1", update });
expect(update).toMatchObject({
sessionUpdate: "tool_call",
toolCallId: "toolu_replay_1",
title: "bash: npm test",
kind: "execute",
status: "completed",
rawInput: { command: "npm test", cwd: "/repo" },
content: [{ type: "content", content: { type: "text", text: "$ npm test" } }],
});
});
it("builds replayed read tool-call locations against the replay cwd", () => {
const replayArgs = normalizeReplayToolArguments(JSON.stringify({ path: "src/foo.ts" }));
const update = buildToolCallStartUpdate({
toolCallId: "toolu_replay_read",
toolName: "read",
args: replayArgs.args,
cwd: path.resolve("/repo"),
status: "completed",
});
expectAcpStructure(zSessionNotification, { sessionId: "session-1", update });
expect(update).toMatchObject({
sessionUpdate: "tool_call",
toolCallId: "toolu_replay_read",
title: "read: src/foo.ts",
kind: "read",
status: "completed",
rawInput: { path: "src/foo.ts" },
locations: [{ path: path.resolve("/repo", "src/foo.ts") }],
});
expect("content" in update).toBe(false);
});
it("keeps malformed replay arguments as raw input without command content", () => {
const replayArgs = normalizeReplayToolArguments("{not json");
const update = buildToolCallStartUpdate({
toolCallId: "toolu_replay_bad",
toolName: "bash",
args: replayArgs.args,
status: "completed",
});
expectAcpStructure(zSessionNotification, { sessionId: "session-1", update });
expect(update).toMatchObject({
sessionUpdate: "tool_call",
toolCallId: "toolu_replay_bad",
title: "bash",
kind: "execute",
status: "completed",
rawInput: "{not json",
});
expect("content" in update).toBe(false);
});
it("keeps object replay arguments unchanged and builds command content", () => {
const rawArgs = { command: "bun test", cwd: "/repo" };
const replayArgs = normalizeReplayToolArguments(rawArgs);
const update = buildToolCallStartUpdate({
toolCallId: "toolu_replay_object",
toolName: "bash",
args: replayArgs.args,
status: "completed",
});
expect(replayArgs.args).toBe(rawArgs);
expectAcpStructure(zSessionNotification, { sessionId: "session-1", update });
expect(update).toMatchObject({
title: "bash: bun test",
status: "completed",
rawInput: rawArgs,
content: [{ type: "content", content: { type: "text", text: "$ bun test" } }],
});
});
it("does not add command text content to non-command tool starts", () => {
const updates = mapAgentSessionEventToAcpSessionUpdates(
{