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
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
107 changes: 105 additions & 2 deletions apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import {
type ProviderUserInputAnswers,
type ProviderThreadId,
type ThreadId,
type ToolActivitySource,
} from "@t3tools/contracts";

import * as Cause from "effect/Cause";
Expand Down Expand Up @@ -2031,6 +2032,66 @@ function claudeToolUseBlocksFromAssistantMessage(
return message.message.content.filter(isClaudeToolUseContentBlock);
}

interface ClaudeToolPresentation {
readonly title: string;
readonly toolSource?: ToolActivitySource;
}

function boundedMetaText(value: unknown, maxLength: number): string | undefined {
if (typeof value !== "string") return undefined;
const text = value.trim().replace(/\s+/gu, " ");
return text.length > 0 && text.length <= maxLength ? text : undefined;
}

function metaHttpUrl(value: unknown): string | undefined {
if (typeof value !== "string" || value.length > 4096) return undefined;
try {
const url = new URL(value);
return (url.protocol === "https:" || url.protocol === "http:") && url.href.length <= 4096
? url.href
: undefined;
} catch {
return undefined;
}
}

/**
* Claude Code's own label for each tool use (the MCP tool's title, or its
* humanized name) plus the server's name and icon. The CLI sends these as
* `tool_use_meta` on assistant frames; the SDK types do not declare it yet.
*/
function claudeToolPresentationsFromAssistantMessage(
message: SDKMessage,
): ReadonlyMap<string, ClaudeToolPresentation> {
const presentations = new Map<string, ClaudeToolPresentation>();
const meta = message.type === "assistant" ? Reflect.get(message, "tool_use_meta") : undefined;
if (!Array.isArray(meta)) return presentations;
for (const entry of meta) {
if (typeof entry !== "object" || entry === null) continue;
const id = boundedMetaText(Reflect.get(entry, "id"), 512);
const title = boundedMetaText(Reflect.get(entry, "display_name"), 160);
if (id === undefined || title === undefined) continue;
const serverName = boundedMetaText(Reflect.get(entry, "server_display_name"), 160);
const iconUrl = metaHttpUrl(Reflect.get(entry, "icon_url"));
presentations.set(id, {
title,
...(serverName === undefined
? {}
: {
toolSource: {
key: `mcp:${serverName.toLowerCase()}`,
name: serverName,
kind: "integration",
...(iconUrl === undefined
? {}
: { icon: { _tag: "themed-logo", logoUrl: iconUrl } as const }),
},
}),
});
}
return presentations;
}

function claudeToolResultBlocksFromAssistantMessage(
message: SDKMessage,
): ReadonlyArray<ClaudeToolResultContentBlock> {
Expand Down Expand Up @@ -2599,6 +2660,7 @@ interface ActiveClaudeToolCall {
readonly parentNodeId: OrchestrationV2ExecutionNode["id"];
readonly ordinal: number;
readonly startedAt: DateTime.Utc;
readonly presentation?: ClaudeToolPresentation;
}

const PENDING_CLAUDE_SUBAGENT_MODEL_CAP = 64;
Expand Down Expand Up @@ -3540,6 +3602,7 @@ export function makeClaudeAdapterV2(
>;
readonly startedAt: DateTime.Utc;
readonly updatedAt: DateTime.Utc;
readonly presentation: ClaudeToolPresentation | undefined;
}) => {
const completedAt = input.status === "running" ? null : input.updatedAt;
const nodeId = idAllocator.derive.nodeFromProviderItem({
Expand Down Expand Up @@ -3597,7 +3660,10 @@ export function makeClaudeAdapterV2(
parentItemId: null,
ordinal: input.ordinal,
status: input.status,
title: readPath !== undefined ? formatReadToolLabel(readPath) : (searchTitle ?? null),
title:
readPath !== undefined
? formatReadToolLabel(readPath)
: (searchTitle ?? input.presentation?.title ?? null),
startedAt: input.startedAt,
completedAt,
updatedAt: input.updatedAt,
Expand Down Expand Up @@ -3660,6 +3726,9 @@ export function makeClaudeAdapterV2(
: {
...itemBase,
type: "dynamic_tool",
...(input.presentation?.toolSource === undefined
? {}
: { toolSource: input.presentation.toolSource }),
toolName: input.toolName,
...(viewedImagePath === undefined ? {} : { viewedImagePath }),
input: claudeNativeToolInputValue(input.toolInput),
Expand Down Expand Up @@ -4324,10 +4393,38 @@ export function makeClaudeAdapterV2(
readonly toolName: string;
readonly toolInput: ClaudeNativeToolInput;
readonly parentToolUseId: string | null;
readonly presentation?: ClaudeToolPresentation | undefined;
}) {
const existing = input.context.toolCalls.get(input.nativeItemId);
if (existing !== undefined) {
return existing;
// The permission callback can start a call before its assistant
// frame arrives with the tool's display name and icon.
if (input.presentation === undefined || existing.presentation !== undefined) {
return existing;
}
const presented = { ...existing, presentation: input.presentation };
input.context.toolCalls.set(input.nativeItemId, presented);
const updatedAt = yield* DateTime.now;
yield* emitToolCallArtifacts(
buildToolCallArtifacts({
context: input.context,
nativeItemId: presented.nativeItemId,
toolName: presented.toolName,
classification: presented.classification,
toolInput: presented.input,
threadId: presented.threadId,
runId: presented.runId,
rootNodeId: presented.rootNodeId,
parentNodeId: presented.parentNodeId,
ordinal: presented.ordinal,
output: NO_CLAUDE_NATIVE_TOOL_OUTPUT,
status: "running",
startedAt: presented.startedAt,
updatedAt,
presentation: input.presentation,
}),
);
return presented;
}
const startedAt = yield* DateTime.now;
const classification = classifyClaudeNativeTool(input.toolName);
Expand All @@ -4354,6 +4451,7 @@ export function makeClaudeAdapterV2(
parentNodeId,
ordinal,
startedAt,
...(input.presentation === undefined ? {} : { presentation: input.presentation }),
};
input.context.toolCalls.set(input.nativeItemId, toolCall);
yield* emitToolCallArtifacts(
Expand All @@ -4372,6 +4470,7 @@ export function makeClaudeAdapterV2(
status: "running",
startedAt,
updatedAt: startedAt,
presentation: input.presentation,
}),
);
return toolCall;
Expand Down Expand Up @@ -4560,6 +4659,7 @@ export function makeClaudeAdapterV2(
status: "failed",
startedAt: toolCall.startedAt,
updatedAt: input.completedAt,
presentation: toolCall.presentation,
});
yield* emitToolCallArtifacts(artifacts);
}
Expand Down Expand Up @@ -5726,6 +5826,7 @@ export function makeClaudeAdapterV2(
}
}

const toolPresentations = claudeToolPresentationsFromAssistantMessage(message);
for (const toolUse of claudeToolUseBlocksFromAssistantMessage(message)) {
const nativeToolInput = claudeNativeToolInputFromUnknown(toolUse.input);
if (toolUse.name === "Agent") {
Expand All @@ -5746,6 +5847,7 @@ export function makeClaudeAdapterV2(
toolName: toolUse.name,
toolInput: nativeToolInput,
parentToolUseId: parentToolUseIdFromSdkMessage(message),
presentation: toolPresentations.get(toolUse.id),
});
const heldPlan = heldProposedPlansByToolUseId.get(toolUse.id);
if (heldPlan !== undefined) {
Expand Down Expand Up @@ -5809,6 +5911,7 @@ export function makeClaudeAdapterV2(
status: isClaudeToolResultError(toolResult) ? "failed" : "completed",
startedAt: toolCall.startedAt,
updatedAt: completedAt,
presentation: toolCall.presentation,
});
yield* emitToolCallArtifacts(artifacts);
context.toolCalls.delete(toolCall.nativeItemId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,8 @@ describe("Claude Agent SDK replay fixtures", () => {
for (const toolName of claudeToolUseNamesFromTranscript(transcript)) {
seenToolNames.add(toolName);
const classification = classifyClaudeNativeTool(toolName);
if (!classification.known) {
// MCP tools are open-ended and deliberately become dynamic tools.
if (!classification.known && !toolName.startsWith("mcp__")) {
unknownToolNames.add(`${fixture.name}:${toolName}`);
}
}
Expand Down
Loading
Loading