Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions apps/server/scripts/record-claude-agent-sdk-replay-fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import {
CLAUDE_BACKGROUND_SUBAGENT_LIFECYCLE_STOP_PROMPT,
} from "../src/orchestration-v2/testkit/fixtures/claude_background_subagent_lifecycle/input.ts";
import { CLAUDE_BACKGROUND_MONITOR_WAKE_PROMPT } from "../src/orchestration-v2/testkit/fixtures/claude_background_monitor_wake/input.ts";
import { CLAUDE_MCP_TOOL_PRESENTATION_PROMPT } from "../src/orchestration-v2/testkit/fixtures/claude_mcp_tool_presentation/input.ts";
import { CLAUDE_BACKGROUND_TASK_INTERRUPT_PROMPT } from "../src/orchestration-v2/testkit/fixtures/claude_background_task_interrupt/input.ts";
import { CLAUDE_BACKGROUND_WAKE_BEFORE_QUEUED_PROMPT_LAUNCH_PROMPT } from "../src/orchestration-v2/testkit/fixtures/claude_background_wake_before_queued_prompt/input.ts";
import {
Expand Down Expand Up @@ -237,6 +238,14 @@ const CLAUDE_RECORDINGS = {
interruptAfter: "tool_use",
interruptAfterToolUses: 2,
},
// Needs the claude.ai Firecrawl connector on the recording account. Claude
// Code describes MCP tool uses in an undeclared `tool_use_meta` field.
claude_mcp_tool_presentation: {
prompts: [CLAUDE_MCP_TOOL_PRESENTATION_PROMPT],
defaultTranscriptFile: "fixtures/claude_mcp_tool_presentation/claude_transcript.ndjson",
queryMode: "streaming",
enableTools: true,
},
subagent: {
prompts: [SUBAGENT_PROMPT],
defaultTranscriptFile: "fixtures/subagent/claude_transcript.ndjson",
Expand Down
57 changes: 56 additions & 1 deletion apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1075,6 +1075,39 @@ describe("AcpAdapterV2", () => {
"cognition.ai/subagent_context": { parentAgentId: "child-a" },
},
},
{
sessionUpdate: "tool_call",
toolCallId: "child-weather",
title: "Check weather",
status: "completed",
rawInput: { server: "weather", tool: "get_weather", city: "Berlin" },
rawOutput: {
result: {
_meta: {
source: { name: "Weather", logoUrl: "https://example.com/weather.png" },
},
content: [{ type: "text", text: "Sunny" }],
},
},
_meta: {
is_mcp_tool_call: true,
"cognition.ai/subagent_context": { parentAgentId: "child-a" },
},
},
{
sessionUpdate: "tool_call",
toolCallId: "parent-weather",
title: "Check weather",
status: "completed",
rawInput: { server: "weather", tool: "get_weather", city: "Berlin" },
rawOutput: {
_meta: {
source: { name: "Weather", logoUrl: "https://example.com/weather.png" },
},
content: [{ type: "text", text: "Sunny" }],
},
_meta: { is_mcp_tool_call: true },
},
{
sessionUpdate: "agent_message_chunk",
content: { type: "text", text: " → decider → event)." },
Expand Down Expand Up @@ -1192,6 +1225,28 @@ describe("AcpAdapterV2", () => {
);
assert.deepEqual([...childMessages.values()], ["Checking the code.", "ONE"]);
assert.equal(task?.prompt, "Run pwd, then reply ONE.");
const childMcp = items.find(
(item) =>
item.threadId === task?.childThreadId &&
item.type === "dynamic_tool" &&
item.toolName === "weather.get_weather",
);
const parentMcp = items.find(
(item) =>
item.threadId === threadId &&
item.type === "dynamic_tool" &&
item.toolName === "weather.get_weather",
);
for (const item of [parentMcp, childMcp]) {
assert.equal(item?.title, "get weather");
assert.deepEqual(item?.toolSource, {
key: "mcp:weather",
name: "Weather",
kind: "integration",
icon: { _tag: "themed-logo", logoUrl: "https://example.com/weather.png" },
});
assert.deepEqual(item?.toolIcon, item?.toolSource?.icon);
}
assert.isTrue(
items.some(
(item) =>
Expand All @@ -1216,7 +1271,7 @@ describe("AcpAdapterV2", () => {
const parentTools = items.filter(
(item) => item.threadId === threadId && item.type === "dynamic_tool",
);
assert.equal(parentTools.length, 1);
assert.equal(parentTools.length, 2);
assert.equal(parentTools[0]?.title, "Parent tool finished");
}).pipe(Effect.provide(testLayer), Effect.scoped),
);
Expand Down
28 changes: 25 additions & 3 deletions apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import {
makeAcpMcpOverAcpBridge,
type AcpMcpOverAcpBridge,
} from "../../mcp/AcpMcpOverAcpBridge.ts";
import { mcpToolPresentation } from "../../provider/McpToolPresentation.ts";
import * as McpProviderSession from "../../mcp/McpProviderSession.ts";
import {
applyAcpAgentTerminalUpdate,
Expand Down Expand Up @@ -3258,9 +3259,14 @@ export function makeAcpAdapterV2(options: AcpAdapterV2Options): ProviderAdapterV
} else if (mcpIdentity !== undefined) {
turnItem = {
...base,
// Identity lives in toolName, like native Codex MCP items; the
// agent's own title (e.g. "Ran command") would shadow it.
title: null,
...mcpToolPresentation({
serverName: mcpIdentity.server,
toolName: mcpIdentity.tool,
source: unknownRecord(
(unknownRecord(rawOutputRecord?.result) ?? rawOutputRecord)?._meta,
)?.source,
}),
type: "dynamic_tool",
toolName: `${mcpIdentity.server}.${mcpIdentity.tool}`,
input:
Expand Down Expand Up @@ -4285,6 +4291,7 @@ export function makeAcpAdapterV2(options: AcpAdapterV2Options): ProviderAdapterV
const key = `${nativeTaskId}:tool:${toolCall.toolCallId}`;
const merged = mergeToolCallState(context.tools.get(key), toolCall);
context.tools.set(key, merged);
const mcpIdentity = extractMcpToolCallIdentity(merged);
const now = yield* DateTime.now;
const status = toolStatus(merged.status);
const startedAt = context.toolStartedAt.get(key) ?? now;
Expand All @@ -4309,7 +4316,22 @@ export function makeAcpAdapterV2(options: AcpAdapterV2Options): ProviderAdapterV
completedAt: completedAtForStatus(status, now),
updatedAt: now,
type: "dynamic_tool",
toolName: merged.title ?? merged.kind ?? "Tool",
...(mcpIdentity === undefined
? {}
: mcpToolPresentation({
serverName: mcpIdentity.server,
toolName: mcpIdentity.tool,
source: unknownRecord(
(
unknownRecord(unknownRecord(merged.data.rawOutput)?.result) ??
unknownRecord(merged.data.rawOutput)
)?._meta,
)?.source,
})),
toolName:
mcpIdentity === undefined
? (merged.title ?? merged.kind ?? "Tool")
: `${mcpIdentity.server}.${mcpIdentity.tool}`,
input: merged.data.rawInput ?? null,
output: merged.data.rawOutput ?? merged.data.content ?? null,
},
Expand Down
105 changes: 105 additions & 0 deletions apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2132,6 +2132,111 @@ describe("ClaudeAdapterV2 background wake turns", () => {
});
const makeWakeHarness = makeWakeHarnessWithOptions();

it.effect.each([
{ isError: false, title: "Check weather" },
{ isError: true, title: "Check weather" },
{ isError: false, title: undefined },
])("keeps late MCP display metadata with %j", ({ isError, title }) =>
Effect.gen(function* () {
const harness = yield* makeWakeHarness;
yield* harness.runtime.startTurn(
makeClaudeTestTurnInput({
threadId: harness.threadId,
providerThread: harness.providerThread,
now: yield* DateTime.now,
attemptId: RunAttemptId.make("mcp-presentation"),
text: "Check the weather",
attachments: [],
}),
);
const toolName = "mcp__weather__get_weather";
const id = "weather-call";
yield* Effect.promise(() =>
harness.getOpenedOptions()!.canUseTool!(
toolName,
{ city: "Berlin" },
{
signal: new AbortController().signal,
toolUseID: id,
requestId: "weather-request",
},
),
);
yield* harness.offerAndWait(
claudeSdkFrame({
type: "assistant",
uuid: "weather-assistant",
session_id: WAKE_NATIVE_SESSION,
parent_tool_use_id: null,
message: {
id: "weather-message",
type: "message",
role: "assistant",
model: "claude-sonnet-4-6",
content: [{ type: "tool_use", id, name: toolName, input: { city: "Berlin" } }],
},
tool_use_meta: [
{
id,
display_name: title,
server_display_name: "Weather",
icon_url: "https://example.com/weather.png",
},
],
}),
);
yield* harness.offerAndWait(
claudeSdkFrame({
type: "user",
uuid: "weather-result",
session_id: WAKE_NATIVE_SESSION,
parent_tool_use_id: null,
message: {
role: "user",
content: [
{
type: "tool_result",
tool_use_id: id,
content: "Weather result",
is_error: isError,
},
],
},
}),
);
yield* Queue.offer(
harness.sdkMessages,
makeResultFrame({ uuid: "weather-terminal", result: "Weather checked" }),
);
yield* Queue.take(harness.terminalReceipts);
const items = harness.events.flatMap((event) =>
event.type === "turn_item.updated" && event.turnItem.type === "dynamic_tool"
? [event.turnItem]
: [],
);
assert.deepEqual(
items.map((item) => item.status),
["running", "running", isError ? "failed" : "completed"],
);
assert.isNull(items[0]?.title);
for (const item of items.slice(1)) {
assert.equal(item.title, title ?? "get weather");
assert.deepEqual(item.toolIcon, {
_tag: "themed-logo",
logoUrl: "https://example.com/weather.png",
});
assert.deepEqual(item.toolSource, {
key: "mcp:weather",
name: "Weather",
kind: "integration",
icon: { _tag: "themed-logo", logoUrl: "https://example.com/weather.png" },
});
assert.deepEqual(item.input, { city: "Berlin" });
}
assert.equal(new Set(items.map((item) => item.id)).size, 1);
}).pipe(Effect.scoped, Effect.provide(Layer.merge(idAllocatorLayer, NodeServices.layer))),
);

it.effect.each(["completed", "interrupted"] as const)(
"projects Claude thinking blocks when %s",
(status) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1120,6 +1120,11 @@ function sanitizeSdkMessageForReplay(input: {
session_id: message.session_id,
};
}
// Like init's slash_commands, the recording account's commands and skills
// are local configuration, not protocol.
if (message.type === "system" && message.subtype === "commands_changed") {
return { ...message, commands: [] };
}
if (message.type === "rate_limit_event") {
return {
...message,
Expand Down
Loading
Loading