fix(acp): 复用工具调用回放详情
This commit is contained in:
@@ -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(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user