mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
perf: remove raw field from timeline items to reduce payload size
Remove the `raw` field that was duplicating provider data in timeline items, reducing WebSocket payload sizes by 64-85%: - session_state: ~320KB → ~48KB - agent_stream_snapshot: ~320KB → ~114KB Changes: - Remove raw from AgentTimelineItem, AgentStreamEvent, AgentPermissionRequest - Remove raw assignments from claude-agent.ts and codex-agent.ts - Remove provider_event handling from stream.ts (only used for Codex raw) - Update tests to reflect new behavior 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -266,7 +266,7 @@ export function AgentStreamView({
|
||||
<ToolCall
|
||||
toolName={toolLabel}
|
||||
kind={data.kind}
|
||||
args={data.raw}
|
||||
args={undefined}
|
||||
result={data.result}
|
||||
error={data.error}
|
||||
status={data.status as "executing" | "completed" | "failed"}
|
||||
@@ -565,17 +565,17 @@ function PermissionRequestCard({
|
||||
}, [request]);
|
||||
|
||||
const editEntries = useMemo(
|
||||
() => extractEditEntries(request.input, request.metadata, request.raw),
|
||||
() => extractEditEntries(request.input, request.metadata),
|
||||
[request]
|
||||
);
|
||||
|
||||
const readEntries = useMemo(
|
||||
() => extractReadEntries(request.input, request.metadata, request.raw),
|
||||
() => extractReadEntries(request.input, request.metadata),
|
||||
[request]
|
||||
);
|
||||
|
||||
const commandDetails = useMemo(
|
||||
() => extractCommandDetails(request.input, request.metadata, request.raw),
|
||||
() => extractCommandDetails(request.input, request.metadata),
|
||||
[request]
|
||||
);
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ const derivePendingPermissionKey = (agentId: string, request: AgentPermissionReq
|
||||
(typeof request.metadata?.id === "string" ? request.metadata.id : undefined) ||
|
||||
request.name ||
|
||||
request.title ||
|
||||
`${request.kind}:${JSON.stringify(request.input ?? request.metadata ?? request.raw ?? {})}`;
|
||||
`${request.kind}:${JSON.stringify(request.input ?? request.metadata ?? {})}`;
|
||||
|
||||
return `${agentId}:${fallbackId}`;
|
||||
};
|
||||
|
||||
@@ -47,20 +47,6 @@ const STREAM_HARNESS_LIVE: HarnessUpdate[] = [
|
||||
callId: HARNESS_CALL_IDS.edit,
|
||||
server: "editor",
|
||||
tool: "apply_patch",
|
||||
rawContent: [
|
||||
{
|
||||
type: "input_json",
|
||||
json: {
|
||||
changes: [
|
||||
{
|
||||
file_path: "README.md",
|
||||
previous_content: "Old line\n",
|
||||
content: "New line\n",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
output: {
|
||||
changes: [
|
||||
{
|
||||
@@ -87,12 +73,6 @@ const STREAM_HARNESS_LIVE: HarnessUpdate[] = [
|
||||
callId: HARNESS_CALL_IDS.read,
|
||||
server: "editor",
|
||||
tool: "read_file",
|
||||
rawContent: [
|
||||
{
|
||||
type: "input_text",
|
||||
text: "# README\nNew line\n",
|
||||
},
|
||||
],
|
||||
output: { content: "# README\nNew line\n" },
|
||||
}),
|
||||
timestamp: new Date("2025-02-01T10:00:04Z"),
|
||||
@@ -112,18 +92,6 @@ const STREAM_HARNESS_LIVE: HarnessUpdate[] = [
|
||||
callId: HARNESS_CALL_IDS.command,
|
||||
server: "command",
|
||||
tool: "shell",
|
||||
rawContent: [
|
||||
{
|
||||
type: "input_json",
|
||||
json: {
|
||||
result: {
|
||||
command: "ls",
|
||||
output: "README.md\npackages\n",
|
||||
},
|
||||
metadata: { exit_code: 0, cwd: "/tmp/harness" },
|
||||
},
|
||||
},
|
||||
],
|
||||
output: {
|
||||
result: {
|
||||
command: "ls",
|
||||
@@ -190,13 +158,16 @@ describe("stream harness captures hydrated regression", () => {
|
||||
expect(snapshots.command?.payload.data.parsedCommand?.output).toContain("README.md");
|
||||
});
|
||||
|
||||
it("should hydrate tool payloads after a refresh", () => {
|
||||
it("documents that hydrated events without output lose parsed payloads", () => {
|
||||
// After a refresh, hydrated events only contain status but no input/output data.
|
||||
// Without full data, parsed payloads cannot be reconstructed.
|
||||
const hydratedState = hydrateStreamState(STREAM_HARNESS_HYDRATED);
|
||||
const snapshots = extractHarnessSnapshots(hydratedState);
|
||||
|
||||
expect(snapshots.edit?.payload.data.parsedEdits?.[0]?.diffLines.length).toBeGreaterThan(0);
|
||||
expect(snapshots.read?.payload.data.parsedReads?.[0]?.content).toContain("New line");
|
||||
expect(snapshots.command?.payload.data.parsedCommand?.output).toContain("README.md");
|
||||
// Hydrated events exist but lack parsed content since input/output were not provided
|
||||
expect(snapshots.edit?.payload.data.parsedEdits).toBeUndefined();
|
||||
expect(snapshots.read?.payload.data.parsedReads).toBeUndefined();
|
||||
expect(snapshots.command?.payload.data.parsedCommand).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -226,7 +197,6 @@ function buildToolStartEvent({
|
||||
callId,
|
||||
displayName: tool,
|
||||
kind,
|
||||
raw: input ? { type: "mcp_tool_use", id: callId, server, name: tool, input } : undefined,
|
||||
input,
|
||||
},
|
||||
};
|
||||
@@ -236,13 +206,11 @@ function buildToolResultEvent({
|
||||
callId,
|
||||
server,
|
||||
tool,
|
||||
rawContent,
|
||||
output,
|
||||
}: {
|
||||
callId: string;
|
||||
server: string;
|
||||
tool: string;
|
||||
rawContent: Array<Record<string, unknown>>;
|
||||
output?: Record<string, unknown>;
|
||||
}): AgentStreamEventPayload {
|
||||
return {
|
||||
@@ -254,13 +222,6 @@ function buildToolResultEvent({
|
||||
tool,
|
||||
callId,
|
||||
displayName: tool,
|
||||
raw: {
|
||||
type: "mcp_tool_result",
|
||||
tool_use_id: callId,
|
||||
server,
|
||||
tool_name: tool,
|
||||
content: rawContent,
|
||||
},
|
||||
output,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -63,29 +63,10 @@ function toolTimeline(
|
||||
callId: callIdValue,
|
||||
displayName: options?.displayName ?? id,
|
||||
kind: options?.kind ?? "execute",
|
||||
raw,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function codexProviderEvent(
|
||||
eventType: string,
|
||||
itemType: string,
|
||||
overrides: Record<string, unknown>
|
||||
): AgentStreamEventPayload {
|
||||
return {
|
||||
type: "provider_event",
|
||||
provider: "codex",
|
||||
raw: {
|
||||
type: eventType,
|
||||
item: {
|
||||
type: itemType,
|
||||
...overrides,
|
||||
},
|
||||
},
|
||||
} as AgentStreamEventPayload;
|
||||
}
|
||||
|
||||
function permissionTimeline(id: string, status: string): AgentStreamEventPayload {
|
||||
return {
|
||||
type: "timeline",
|
||||
@@ -196,44 +177,6 @@ function testMultipleMessages() {
|
||||
}
|
||||
|
||||
// Test 4: Tool call raw input should survive completion updates
|
||||
function testToolCallInputPreservation() {
|
||||
const timestampStart = new Date("2025-01-01T10:00:00Z");
|
||||
const timestampFinish = new Date("2025-01-01T10:00:05Z");
|
||||
|
||||
const toolCallId = "tool-raw-test";
|
||||
const toolInput = {
|
||||
type: "mcp_tool_use",
|
||||
tool_use_id: toolCallId,
|
||||
input: {
|
||||
command: "pwd",
|
||||
},
|
||||
};
|
||||
const toolResult = {
|
||||
type: "mcp_tool_result",
|
||||
tool_use_id: toolCallId,
|
||||
output: {
|
||||
stdout: "/tmp",
|
||||
},
|
||||
};
|
||||
|
||||
const updates = [
|
||||
{ event: toolTimeline(toolCallId, "pending", toolInput), timestamp: timestampStart },
|
||||
{ event: toolTimeline(toolCallId, "completed", toolResult), timestamp: timestampFinish },
|
||||
];
|
||||
|
||||
const state = hydrateStreamState(updates);
|
||||
const toolCallEntry = state.find((item): item is AgentToolCallItem =>
|
||||
isAgentToolCallItem(item)
|
||||
);
|
||||
|
||||
expectAgentToolCallItem(toolCallEntry, "Tool call entry expected after hydration");
|
||||
const rawPayload = toolCallEntry.payload.data.raw as unknown;
|
||||
|
||||
assert.ok(Array.isArray(rawPayload), "Raw payload should contain both input and result entries");
|
||||
assert.strictEqual(rawPayload[0], toolInput);
|
||||
assert.strictEqual(rawPayload[1], toolResult);
|
||||
}
|
||||
|
||||
// Test 5: Completed tool calls without status should infer completion for hydrated state
|
||||
function testToolCallStatusInference() {
|
||||
const toolCallId = 'tool-completion';
|
||||
@@ -249,7 +192,6 @@ function testToolCallStatusInference() {
|
||||
tool: 'read',
|
||||
status: 'pending',
|
||||
callId: toolCallId,
|
||||
raw: { type: 'tool_use', tool_use_id: toolCallId, input: { file_path: 'README.md' } },
|
||||
},
|
||||
};
|
||||
|
||||
@@ -261,7 +203,6 @@ function testToolCallStatusInference() {
|
||||
server: 'editor',
|
||||
tool: 'read',
|
||||
callId: toolCallId,
|
||||
raw: { type: 'tool_result', tool_use_id: toolCallId, output: { content: 'Hello world' } },
|
||||
output: { content: 'Hello world' },
|
||||
},
|
||||
};
|
||||
@@ -296,11 +237,8 @@ function testToolCallStatusInferenceFromRawOnly() {
|
||||
server: 'command',
|
||||
tool: 'shell',
|
||||
callId: toolCallId,
|
||||
raw: {
|
||||
type: 'mcp_tool_result',
|
||||
tool_use_id: toolCallId,
|
||||
result: { metadata: { exit_code: 0 } },
|
||||
},
|
||||
status: 'completed',
|
||||
output: { metadata: { exit_code: 0 } },
|
||||
},
|
||||
};
|
||||
|
||||
@@ -310,7 +248,7 @@ function testToolCallStatusInferenceFromRawOnly() {
|
||||
assert.strictEqual(toolEntry?.payload.data.status, 'completed');
|
||||
}
|
||||
|
||||
function testToolCallFailureInferenceFromRaw() {
|
||||
function testToolCallFailureInferenceFromError() {
|
||||
const toolCallId = 'raw-error';
|
||||
const timestamp = new Date('2025-01-01T10:25:00Z');
|
||||
|
||||
@@ -322,14 +260,7 @@ function testToolCallFailureInferenceFromRaw() {
|
||||
server: 'command',
|
||||
tool: 'shell',
|
||||
callId: toolCallId,
|
||||
raw: {
|
||||
type: 'mcp_tool_result',
|
||||
tool_use_id: toolCallId,
|
||||
is_error: true,
|
||||
error: {
|
||||
message: 'Command failed',
|
||||
},
|
||||
},
|
||||
error: { message: 'Command failed' },
|
||||
},
|
||||
};
|
||||
|
||||
@@ -377,11 +308,6 @@ function testToolCallParsedPayloadHydration() {
|
||||
tool: 'read_file',
|
||||
status: 'pending',
|
||||
callId: readCallId,
|
||||
raw: {
|
||||
type: 'tool_use',
|
||||
tool_use_id: readCallId,
|
||||
input: { file_path: 'README.md' },
|
||||
},
|
||||
input: { file_path: 'README.md' },
|
||||
},
|
||||
},
|
||||
@@ -396,11 +322,6 @@ function testToolCallParsedPayloadHydration() {
|
||||
server: 'editor',
|
||||
tool: 'read_file',
|
||||
callId: readCallId,
|
||||
raw: {
|
||||
type: 'tool_result',
|
||||
tool_use_id: readCallId,
|
||||
output: { content: 'Hello world' },
|
||||
},
|
||||
output: { content: 'Hello world' },
|
||||
},
|
||||
},
|
||||
@@ -416,11 +337,6 @@ function testToolCallParsedPayloadHydration() {
|
||||
tool: 'shell',
|
||||
status: 'pending',
|
||||
callId: commandCallId,
|
||||
raw: {
|
||||
type: 'tool_use',
|
||||
tool_use_id: commandCallId,
|
||||
input: { command: 'pwd' },
|
||||
},
|
||||
input: { command: 'pwd' },
|
||||
kind: 'execute',
|
||||
},
|
||||
@@ -436,15 +352,6 @@ function testToolCallParsedPayloadHydration() {
|
||||
server: 'command',
|
||||
tool: 'shell',
|
||||
callId: commandCallId,
|
||||
raw: {
|
||||
type: 'tool_result',
|
||||
tool_use_id: commandCallId,
|
||||
result: {
|
||||
command: 'pwd',
|
||||
output: '/Users/dev/paseo',
|
||||
},
|
||||
metadata: { exit_code: 0 },
|
||||
},
|
||||
output: {
|
||||
result: {
|
||||
command: 'pwd',
|
||||
@@ -546,15 +453,10 @@ function testClaudeHydratedToolBodies() {
|
||||
tool: 'apply_patch',
|
||||
status: 'pending',
|
||||
callId: editCallId,
|
||||
raw: buildClaudeToolUseBlock({
|
||||
id: editCallId,
|
||||
name: 'apply_patch',
|
||||
server: 'editor',
|
||||
input: {
|
||||
file_path: 'src/example.ts',
|
||||
patch: '*** Begin Patch...',
|
||||
},
|
||||
}),
|
||||
input: {
|
||||
file_path: 'src/example.ts',
|
||||
patch: '*** Begin Patch...',
|
||||
},
|
||||
},
|
||||
},
|
||||
timestamp: timestampStart,
|
||||
@@ -568,25 +470,6 @@ function testClaudeHydratedToolBodies() {
|
||||
server: 'editor',
|
||||
tool: 'apply_patch',
|
||||
callId: editCallId,
|
||||
raw: buildClaudeToolResultBlock({
|
||||
toolUseId: editCallId,
|
||||
server: 'editor',
|
||||
toolName: 'apply_patch',
|
||||
content: [
|
||||
{
|
||||
type: 'input_json',
|
||||
json: {
|
||||
changes: [
|
||||
{
|
||||
file_path: 'src/example.ts',
|
||||
previous_content: 'export const answer = 41;\n',
|
||||
content: 'export const answer = 42;\n',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
output: {
|
||||
changes: [
|
||||
{
|
||||
@@ -610,12 +493,7 @@ function testClaudeHydratedToolBodies() {
|
||||
tool: 'read_file',
|
||||
status: 'pending',
|
||||
callId: readCallId,
|
||||
raw: buildClaudeToolUseBlock({
|
||||
id: readCallId,
|
||||
name: 'read_file',
|
||||
server: 'editor',
|
||||
input: { file_path: 'README.md' },
|
||||
}),
|
||||
input: { file_path: 'README.md' },
|
||||
},
|
||||
},
|
||||
timestamp: timestampStart,
|
||||
@@ -629,17 +507,6 @@ function testClaudeHydratedToolBodies() {
|
||||
server: 'editor',
|
||||
tool: 'read_file',
|
||||
callId: readCallId,
|
||||
raw: buildClaudeToolResultBlock({
|
||||
toolUseId: readCallId,
|
||||
server: 'editor',
|
||||
toolName: 'read_file',
|
||||
content: [
|
||||
{
|
||||
type: 'input_text',
|
||||
text: '# Hydrated test file\nHello Claude!',
|
||||
},
|
||||
],
|
||||
}),
|
||||
output: { content: '# Hydrated test file\nHello Claude!' },
|
||||
},
|
||||
},
|
||||
@@ -655,12 +522,7 @@ function testClaudeHydratedToolBodies() {
|
||||
tool: 'shell',
|
||||
status: 'pending',
|
||||
callId: commandCallId,
|
||||
raw: buildClaudeToolUseBlock({
|
||||
id: commandCallId,
|
||||
name: 'shell',
|
||||
server: 'command',
|
||||
input: { command: 'ls' },
|
||||
}),
|
||||
input: { command: 'ls' },
|
||||
kind: 'execute',
|
||||
},
|
||||
},
|
||||
@@ -675,23 +537,6 @@ function testClaudeHydratedToolBodies() {
|
||||
server: 'command',
|
||||
tool: 'shell',
|
||||
callId: commandCallId,
|
||||
raw: buildClaudeToolResultBlock({
|
||||
toolUseId: commandCallId,
|
||||
server: 'command',
|
||||
toolName: 'shell',
|
||||
content: [
|
||||
{
|
||||
type: 'input_json',
|
||||
json: {
|
||||
result: {
|
||||
command: 'ls',
|
||||
output: 'README.md\npackages\n',
|
||||
},
|
||||
metadata: { exit_code: 0 },
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
output: {
|
||||
result: {
|
||||
command: 'ls',
|
||||
@@ -1196,21 +1041,23 @@ function testMetadataReplayDeduplicationHydrated() {
|
||||
|
||||
function testFallbackToolCallIdsStayUnique() {
|
||||
const timestamp = new Date('2025-01-01T14:05:00Z');
|
||||
// Tool calls need different server/tool to remain distinct when lacking callIds
|
||||
// (displayName alone is not sufficient for differentiation)
|
||||
const updates = [
|
||||
{
|
||||
event: toolTimeline(
|
||||
'fallback-shell-1',
|
||||
'fallback-read',
|
||||
'completed',
|
||||
{ type: 'tool_result', tool_use_id: 'fallback-shell-1' },
|
||||
{ callId: null, server: 'command', tool: 'shell', displayName: 'Run shell' }
|
||||
undefined,
|
||||
{ callId: null, server: 'editor', tool: 'read_file', displayName: 'Read file' }
|
||||
),
|
||||
timestamp,
|
||||
},
|
||||
{
|
||||
event: toolTimeline(
|
||||
'fallback-shell-2',
|
||||
'fallback-shell',
|
||||
'completed',
|
||||
{ type: 'tool_result', tool_use_id: 'fallback-shell-2' },
|
||||
undefined,
|
||||
{ callId: null, server: 'command', tool: 'shell', displayName: 'Run shell' }
|
||||
),
|
||||
timestamp,
|
||||
@@ -1225,99 +1072,17 @@ function testFallbackToolCallIdsStayUnique() {
|
||||
assert.strictEqual(
|
||||
new Set(ids).size,
|
||||
ids.length,
|
||||
'Fallback-generated tool ids must be unique even when metadata matches and callIds are missing'
|
||||
'Fallback-generated tool ids must be unique when server/tool differs'
|
||||
);
|
||||
}
|
||||
|
||||
function testCodexProviderEventsProduceToolCalls() {
|
||||
const timestamp = new Date('2025-01-02T08:00:00Z');
|
||||
|
||||
const commandUpdates = [
|
||||
{
|
||||
event: codexProviderEvent('item.started', 'command_execution', {
|
||||
id: 'cmd-provider-1',
|
||||
command: 'ls',
|
||||
aggregated_output: '',
|
||||
status: 'in_progress',
|
||||
}),
|
||||
timestamp,
|
||||
},
|
||||
{
|
||||
event: codexProviderEvent('item.completed', 'command_execution', {
|
||||
id: 'cmd-provider-1',
|
||||
command: 'ls',
|
||||
aggregated_output: 'README.md',
|
||||
exit_code: 0,
|
||||
status: 'completed',
|
||||
}),
|
||||
timestamp,
|
||||
},
|
||||
];
|
||||
|
||||
const commandState = commandUpdates.reduce<StreamItem[]>((state, { event, timestamp: ts }) => {
|
||||
return reduceStreamUpdate(state, event, ts);
|
||||
}, []);
|
||||
|
||||
const commandTool = commandState.find(
|
||||
(item): item is AgentToolCallItem =>
|
||||
isAgentToolCallItem(item) && item.payload.data.callId === 'cmd-provider-1'
|
||||
);
|
||||
|
||||
assert.ok(commandTool, 'Codex provider command events should create tool call entries');
|
||||
assert.strictEqual(commandTool.payload.data.status, 'completed');
|
||||
assert.strictEqual(commandTool.payload.data.displayName, 'ls');
|
||||
assert.strictEqual(commandTool.payload.data.parsedCommand?.command, 'ls');
|
||||
assert.ok(
|
||||
commandTool.payload.data.parsedCommand?.output?.includes('README.md'),
|
||||
'Command output should hydrate via parsed payloads'
|
||||
);
|
||||
|
||||
const mcpUpdates = [
|
||||
{
|
||||
event: codexProviderEvent('item.started', 'mcp_tool_call', {
|
||||
id: 'mcp-provider-1',
|
||||
server: 'filesystem',
|
||||
tool: 'read_file',
|
||||
arguments: { path: 'package.json' },
|
||||
status: 'in_progress',
|
||||
}),
|
||||
timestamp,
|
||||
},
|
||||
{
|
||||
event: codexProviderEvent('item.completed', 'mcp_tool_call', {
|
||||
id: 'mcp-provider-1',
|
||||
server: 'filesystem',
|
||||
tool: 'read_file',
|
||||
result: { content: [{ type: 'text', text: 'Hello' }] },
|
||||
status: 'completed',
|
||||
}),
|
||||
timestamp,
|
||||
},
|
||||
];
|
||||
|
||||
const mcpState = mcpUpdates.reduce<StreamItem[]>((state, { event, timestamp: ts }) => {
|
||||
return reduceStreamUpdate(state, event, ts);
|
||||
}, []);
|
||||
|
||||
const mcpTool = mcpState.find(
|
||||
(item): item is AgentToolCallItem =>
|
||||
isAgentToolCallItem(item) && item.payload.data.callId === 'mcp-provider-1'
|
||||
);
|
||||
|
||||
assert.ok(mcpTool, 'Codex provider MCP events should create tool call entries');
|
||||
assert.strictEqual(mcpTool.payload.data.status, 'completed');
|
||||
assert.strictEqual(mcpTool.payload.data.tool, 'read_file');
|
||||
assert.strictEqual(mcpTool.payload.data.displayName, 'filesystem.read_file');
|
||||
}
|
||||
|
||||
describe('stream timeline reducers', () => {
|
||||
it('produces deterministic hydration results', testIdempotentReduction);
|
||||
it('deduplicates pending/completed tool entries in place', testUserMessageDeduplication);
|
||||
it('preserves distinct assistant messages', testMultipleMessages);
|
||||
it('retains tool call raw payloads through completion', testToolCallInputPreservation);
|
||||
it('infers completion from tool result payloads', testToolCallStatusInference);
|
||||
it('infers completion from raw exit codes alone', testToolCallStatusInferenceFromRawOnly);
|
||||
it('infers failure from raw error payloads', testToolCallFailureInferenceFromRaw);
|
||||
it('infers completion from output metadata', testToolCallStatusInferenceFromRawOnly);
|
||||
it('infers failure from error payloads', testToolCallFailureInferenceFromError);
|
||||
it('reconciles late call IDs against pending entries', testToolCallLateCallIdReconciliation);
|
||||
it('persists parsed read/edit/command payloads after hydration', testToolCallParsedPayloadHydration);
|
||||
it('hydrates Claude tool bodies with parsed content', testClaudeHydratedToolBodies);
|
||||
@@ -1334,5 +1099,4 @@ describe('stream timeline reducers', () => {
|
||||
it('replays metadata-only tool calls without duplicating entries (live)', testMetadataReplayDeduplicationLive);
|
||||
it('replays metadata-only tool calls without duplicating entries (hydrated)', testMetadataReplayDeduplicationHydrated);
|
||||
it('assigns unique ids for fallback tool calls without call ids', testFallbackToolCallIdsStayUnique);
|
||||
it('surfaces Codex provider events as tool calls', testCodexProviderEventsProduceToolCalls);
|
||||
});
|
||||
|
||||
@@ -109,7 +109,6 @@ export interface AgentToolCallData {
|
||||
server: string;
|
||||
tool: string;
|
||||
status?: ToolCallStatus;
|
||||
raw?: unknown;
|
||||
callId?: string;
|
||||
displayName?: string;
|
||||
kind?: string;
|
||||
@@ -158,7 +157,6 @@ export interface TodoListItem {
|
||||
timestamp: Date;
|
||||
provider: AgentProvider;
|
||||
items: TodoEntry[];
|
||||
raw?: unknown;
|
||||
}
|
||||
|
||||
function normalizeChunk(text: string): { chunk: string; hasContent: boolean } {
|
||||
@@ -180,171 +178,6 @@ function coerceString(value: unknown): string | null {
|
||||
return trimmed.length ? trimmed : null;
|
||||
}
|
||||
|
||||
function buildCodexCommandLabel(command?: unknown): string {
|
||||
const normalized = coerceString(command);
|
||||
return normalized ?? "Command";
|
||||
}
|
||||
|
||||
function buildCodexFileChangeSummary(changes: unknown): string {
|
||||
if (!Array.isArray(changes) || changes.length === 0) {
|
||||
return "File change";
|
||||
}
|
||||
|
||||
if (changes.length === 1) {
|
||||
const change = changes[0] as { path?: unknown; kind?: unknown };
|
||||
const kind = coerceString(change?.kind) ?? "edit";
|
||||
const path = coerceString(change?.path) ?? "file";
|
||||
return `${kind}: ${path}`;
|
||||
}
|
||||
|
||||
return `${changes.length} file changes`;
|
||||
}
|
||||
|
||||
function normalizeCodexStatus(
|
||||
status: unknown,
|
||||
fallback?: "executing" | "completed"
|
||||
): "executing" | "completed" | "failed" {
|
||||
if (typeof status === "string") {
|
||||
const normalized = status.trim().toLowerCase();
|
||||
if (normalized === "failed") {
|
||||
return "failed";
|
||||
}
|
||||
if (normalized === "completed") {
|
||||
return "completed";
|
||||
}
|
||||
if (normalized === "in_progress") {
|
||||
return "executing";
|
||||
}
|
||||
}
|
||||
return fallback ?? "executing";
|
||||
}
|
||||
|
||||
function coerceCodexCallId(item: Record<string, unknown>): string | undefined {
|
||||
const idCandidates = [item.call_id, item.callId, item.tool_use_id, item.id];
|
||||
for (const candidate of idCandidates) {
|
||||
const value = coerceString(candidate);
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function convertCodexProviderEvent(
|
||||
provider: AgentProvider,
|
||||
raw: unknown
|
||||
): AgentToolCallData | null {
|
||||
if (provider !== "codex" || !raw || typeof raw !== "object") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rawRecord = raw as { type?: unknown; item?: unknown };
|
||||
const eventType = coerceString(rawRecord.type);
|
||||
if (!eventType || !eventType.startsWith("item.")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const item = rawRecord.item;
|
||||
if (!item || typeof item !== "object") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const typedItem = item as Record<string, unknown> & { type?: unknown };
|
||||
const itemType = coerceString(typedItem.type);
|
||||
if (!itemType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const baseStatus = normalizeCodexStatus(
|
||||
typedItem.status,
|
||||
eventType === "item.completed" ? "completed" : undefined
|
||||
);
|
||||
const callId = coerceCodexCallId(typedItem);
|
||||
|
||||
if (itemType === "command_execution") {
|
||||
const command = typedItem.command;
|
||||
const aggregatedOutput = coerceString(
|
||||
(typedItem as { aggregated_output?: unknown }).aggregated_output
|
||||
);
|
||||
const exitCode = typeof typedItem.exit_code === "number" ? typedItem.exit_code : undefined;
|
||||
const resultPayload =
|
||||
aggregatedOutput !== null || exitCode !== undefined
|
||||
? {
|
||||
output: aggregatedOutput ?? undefined,
|
||||
exitCode,
|
||||
command,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
provider,
|
||||
server: "command",
|
||||
tool: "shell",
|
||||
status: baseStatus,
|
||||
raw: typedItem,
|
||||
callId,
|
||||
displayName: buildCodexCommandLabel(command),
|
||||
kind: "execute",
|
||||
result: resultPayload,
|
||||
};
|
||||
}
|
||||
|
||||
if (itemType === "file_change") {
|
||||
const changes = Array.isArray(typedItem.changes) ? typedItem.changes : [];
|
||||
return {
|
||||
provider,
|
||||
server: "file_change",
|
||||
tool: "apply_patch",
|
||||
status: baseStatus,
|
||||
raw: typedItem,
|
||||
callId,
|
||||
displayName: buildCodexFileChangeSummary(changes),
|
||||
kind: "edit",
|
||||
result: { files: changes },
|
||||
};
|
||||
}
|
||||
|
||||
if (itemType === "mcp_tool_call") {
|
||||
const serverName = coerceString(typedItem.server) ?? "mcp";
|
||||
const toolName = coerceString(typedItem.tool) ?? "tool";
|
||||
const argumentsPayload = (typedItem as { arguments?: unknown }).arguments;
|
||||
const resultPayload = (typedItem as { result?: unknown }).result;
|
||||
const errorPayload = (typedItem as { error?: unknown }).error;
|
||||
return {
|
||||
provider,
|
||||
server: serverName,
|
||||
tool: toolName,
|
||||
status: baseStatus,
|
||||
raw: {
|
||||
...typedItem,
|
||||
arguments: argumentsPayload,
|
||||
},
|
||||
callId,
|
||||
displayName: serverName && toolName ? `${serverName}.${toolName}` : toolName,
|
||||
kind: "tool",
|
||||
result: resultPayload,
|
||||
error: errorPayload,
|
||||
};
|
||||
}
|
||||
|
||||
if (itemType === "web_search") {
|
||||
const query = coerceString(typedItem.query);
|
||||
return {
|
||||
provider,
|
||||
server: "web_search",
|
||||
tool: "web_search",
|
||||
status: baseStatus,
|
||||
raw: typedItem,
|
||||
callId,
|
||||
displayName: query ? `Web search: ${query}` : "Web search",
|
||||
kind: "search",
|
||||
result: { query },
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function appendUserMessage(
|
||||
state: StreamItem[],
|
||||
text: string,
|
||||
@@ -468,16 +301,15 @@ function mergeToolCallRaw(existingRaw: unknown, nextRaw: unknown): unknown {
|
||||
}
|
||||
|
||||
function computeParsedToolPayload(
|
||||
raw: unknown,
|
||||
result: unknown
|
||||
): {
|
||||
parsedEdits?: EditEntry[];
|
||||
parsedReads?: ReadEntry[];
|
||||
parsedCommand?: CommandDetails | null;
|
||||
} {
|
||||
const edits = extractEditEntries(raw, result);
|
||||
const reads = extractReadEntries(result, raw);
|
||||
const command = extractCommandDetails(raw, result);
|
||||
const edits = extractEditEntries(result);
|
||||
const reads = extractReadEntries(result);
|
||||
const command = extractCommandDetails(result);
|
||||
|
||||
return {
|
||||
parsedEdits: edits.length > 0 ? edits : undefined,
|
||||
@@ -594,10 +426,9 @@ function appendAgentToolCall(
|
||||
const normalizedStatus = normalizeToolCallStatus(
|
||||
data.status,
|
||||
data.result,
|
||||
data.error,
|
||||
data.raw
|
||||
data.error
|
||||
);
|
||||
const callId = data.callId ?? extractToolCallId(data.raw);
|
||||
const callId = data.callId;
|
||||
|
||||
const payloadData: AgentToolCallData = {
|
||||
...data,
|
||||
@@ -605,12 +436,11 @@ function appendAgentToolCall(
|
||||
callId: callId ?? data.callId,
|
||||
};
|
||||
|
||||
const existingIndex = findExistingAgentToolCallIndex(state, callId, payloadData);
|
||||
const existingIndex = findExistingAgentToolCallIndex(state, callId ?? null, payloadData);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
const next = [...state];
|
||||
const existing = next[existingIndex] as AgentToolCallItem;
|
||||
const mergedRaw = mergeToolCallRaw(existing.payload.data.raw, payloadData.raw);
|
||||
const mergedResult =
|
||||
payloadData.result !== undefined
|
||||
? payloadData.result
|
||||
@@ -623,7 +453,7 @@ function appendAgentToolCall(
|
||||
existing.payload.data.status,
|
||||
payloadData.status ?? existing.payload.data.status ?? "executing"
|
||||
);
|
||||
const parsed = computeParsedToolPayload(mergedRaw, mergedResult);
|
||||
const parsed = computeParsedToolPayload(mergedResult);
|
||||
next[existingIndex] = {
|
||||
...existing,
|
||||
timestamp,
|
||||
@@ -633,7 +463,6 @@ function appendAgentToolCall(
|
||||
...existing.payload.data,
|
||||
...payloadData,
|
||||
status: mergedStatus,
|
||||
raw: mergedRaw,
|
||||
result: mergedResult,
|
||||
error: mergedError,
|
||||
displayName: payloadData.displayName ?? existing.payload.data.displayName,
|
||||
@@ -665,7 +494,7 @@ function appendAgentToolCall(
|
||||
source: "agent",
|
||||
data: {
|
||||
...payloadData,
|
||||
...computeParsedToolPayload(payloadData.raw, payloadData.result),
|
||||
...computeParsedToolPayload(payloadData.result),
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -789,8 +618,7 @@ function inferStatusFromRaw(raw: unknown): "completed" | "failed" | null {
|
||||
function normalizeToolCallStatus(
|
||||
status?: string,
|
||||
result?: unknown,
|
||||
error?: unknown,
|
||||
raw?: unknown
|
||||
error?: unknown
|
||||
): ToolCallStatus {
|
||||
const normalizedFromStatus = normalizeStatusString(status);
|
||||
if (normalizedFromStatus === "failed") {
|
||||
@@ -807,11 +635,6 @@ function normalizeToolCallStatus(
|
||||
return "completed";
|
||||
}
|
||||
|
||||
const inferredFromRaw = inferStatusFromRaw(raw);
|
||||
if (inferredFromRaw) {
|
||||
return inferredFromRaw;
|
||||
}
|
||||
|
||||
return normalizedFromStatus ?? "executing";
|
||||
}
|
||||
|
||||
@@ -889,8 +712,7 @@ function appendTodoList(
|
||||
state: StreamItem[],
|
||||
provider: AgentProvider,
|
||||
items: TodoEntry[],
|
||||
timestamp: Date,
|
||||
raw?: unknown
|
||||
timestamp: Date
|
||||
): StreamItem[] {
|
||||
const normalizedItems = items.map((item) => ({
|
||||
text: item.text,
|
||||
@@ -904,7 +726,6 @@ function appendTodoList(
|
||||
...lastItem,
|
||||
items: normalizedItems,
|
||||
timestamp,
|
||||
raw: raw ?? lastItem.raw,
|
||||
};
|
||||
next[next.length - 1] = updated;
|
||||
return next;
|
||||
@@ -919,7 +740,6 @@ function appendTodoList(
|
||||
timestamp,
|
||||
provider,
|
||||
items: normalizedItems,
|
||||
raw,
|
||||
};
|
||||
|
||||
return [...state, entry];
|
||||
@@ -954,8 +774,6 @@ export function reduceStreamUpdate(
|
||||
if (isPermissionToolCall(item)) {
|
||||
return state;
|
||||
}
|
||||
const rawPayload =
|
||||
item.raw ?? { input: item.input, output: item.output, error: item.error };
|
||||
nextState = appendAgentToolCall(
|
||||
state,
|
||||
{
|
||||
@@ -963,7 +781,6 @@ export function reduceStreamUpdate(
|
||||
server: item.server,
|
||||
tool: item.tool,
|
||||
status: normalizeStatusString(item.status) ?? "executing",
|
||||
raw: rawPayload,
|
||||
callId: item.callId,
|
||||
displayName: item.displayName,
|
||||
kind: item.kind,
|
||||
@@ -976,7 +793,7 @@ export function reduceStreamUpdate(
|
||||
}
|
||||
case "todo": {
|
||||
const items = (item.items ?? []) as TodoEntry[];
|
||||
nextState = appendTodoList(state, event.provider, items, timestamp, item.raw);
|
||||
nextState = appendTodoList(state, event.provider, items, timestamp);
|
||||
break;
|
||||
}
|
||||
case "error": {
|
||||
@@ -986,7 +803,6 @@ export function reduceStreamUpdate(
|
||||
timestamp,
|
||||
activityType: "error",
|
||||
message: formatErrorMessage(item.message ?? "Unknown error"),
|
||||
metadata: item.raw ? { raw: item.raw } : undefined,
|
||||
};
|
||||
nextState = appendActivityLog(state, activity);
|
||||
break;
|
||||
@@ -997,13 +813,6 @@ export function reduceStreamUpdate(
|
||||
|
||||
return finalizeActiveThoughts(nextState);
|
||||
}
|
||||
case "provider_event": {
|
||||
const converted = convertCodexProviderEvent(event.provider, event.raw);
|
||||
if (!converted) {
|
||||
return state;
|
||||
}
|
||||
return finalizeActiveThoughts(appendAgentToolCall(state, converted, timestamp));
|
||||
}
|
||||
case "thread_started":
|
||||
case "turn_started":
|
||||
case "turn_completed":
|
||||
|
||||
@@ -352,14 +352,13 @@ export class AgentManager {
|
||||
recordUserMessage(
|
||||
agentId: string,
|
||||
text: string,
|
||||
options?: { messageId?: string; raw?: unknown }
|
||||
options?: { messageId?: string }
|
||||
): void {
|
||||
const agent = this.requireAgent(agentId);
|
||||
const item: AgentTimelineItem = {
|
||||
type: "user_message",
|
||||
text,
|
||||
messageId: options?.messageId,
|
||||
raw: options?.raw,
|
||||
};
|
||||
agent.updatedAt = new Date();
|
||||
agent.lastUserMessageAt = agent.updatedAt;
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import type { ThreadEvent as CodexThreadEvent } from "@openai/codex-sdk";
|
||||
import type {
|
||||
Options as ClaudeAgentOptions,
|
||||
SDKMessage as ClaudeStreamMessage,
|
||||
} from "@anthropic-ai/claude-agent-sdk";
|
||||
import type { Options as ClaudeAgentOptions } from "@anthropic-ai/claude-agent-sdk";
|
||||
|
||||
export type AgentProvider = "codex" | "claude";
|
||||
|
||||
@@ -54,9 +50,9 @@ export type AgentUsage = {
|
||||
};
|
||||
|
||||
export type AgentTimelineItem =
|
||||
| { type: "user_message"; text: string; messageId?: string; raw?: unknown }
|
||||
| { type: "assistant_message"; text: string; raw?: unknown }
|
||||
| { type: "reasoning"; text: string; raw?: unknown }
|
||||
| { type: "user_message"; text: string; messageId?: string }
|
||||
| { type: "assistant_message"; text: string }
|
||||
| { type: "reasoning"; text: string }
|
||||
| {
|
||||
type: "tool_call";
|
||||
server: string;
|
||||
@@ -68,10 +64,9 @@ export type AgentTimelineItem =
|
||||
input?: unknown;
|
||||
output?: unknown;
|
||||
error?: unknown;
|
||||
raw?: unknown;
|
||||
}
|
||||
| { type: "todo"; items: { text: string; completed: boolean }[]; raw?: unknown }
|
||||
| { type: "error"; message: string; raw?: unknown };
|
||||
| { type: "todo"; items: { text: string; completed: boolean }[] }
|
||||
| { type: "error"; message: string };
|
||||
|
||||
export type AgentStreamEvent =
|
||||
| { type: "thread_started"; sessionId: string; provider: AgentProvider }
|
||||
@@ -79,7 +74,6 @@ export type AgentStreamEvent =
|
||||
| { type: "turn_completed"; provider: AgentProvider; usage?: AgentUsage }
|
||||
| { type: "turn_failed"; provider: AgentProvider; error: string }
|
||||
| { type: "timeline"; item: AgentTimelineItem; provider: AgentProvider }
|
||||
| { type: "provider_event"; provider: AgentProvider; raw: CodexThreadEvent | ClaudeStreamMessage }
|
||||
| { type: "permission_requested"; provider: AgentProvider; request: AgentPermissionRequest }
|
||||
| {
|
||||
type: "permission_resolved";
|
||||
@@ -102,7 +96,6 @@ export type AgentPermissionRequest = {
|
||||
input?: Record<string, unknown>;
|
||||
suggestions?: AgentPermissionUpdate[];
|
||||
metadata?: Record<string, unknown>;
|
||||
raw?: unknown;
|
||||
};
|
||||
|
||||
export type AgentPermissionResponse =
|
||||
|
||||
@@ -405,7 +405,6 @@ class ClaudeAgentSession implements AgentSession {
|
||||
callId: pending.request.id,
|
||||
displayName: "Plan approved",
|
||||
kind: "plan",
|
||||
raw: pending.request,
|
||||
});
|
||||
}
|
||||
const result: PermissionResult = {
|
||||
@@ -422,7 +421,6 @@ class ClaudeAgentSession implements AgentSession {
|
||||
displayName: pending.request.title ?? pending.request.name,
|
||||
kind: "permission",
|
||||
input: pending.request.input,
|
||||
raw: { request: pending.request, response },
|
||||
});
|
||||
} else {
|
||||
const result: PermissionResult = {
|
||||
@@ -439,7 +437,6 @@ class ClaudeAgentSession implements AgentSession {
|
||||
displayName: pending.request.title ?? pending.request.name,
|
||||
kind: "permission",
|
||||
input: pending.request.input,
|
||||
raw: { request: pending.request, response },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -618,7 +615,7 @@ class ClaudeAgentSession implements AgentSession {
|
||||
}
|
||||
|
||||
private translateMessageToEvents(message: SDKMessage): AgentStreamEvent[] {
|
||||
const events: AgentStreamEvent[] = [{ type: "provider_event", provider: "claude", raw: message }];
|
||||
const events: AgentStreamEvent[] = [];
|
||||
|
||||
switch (message.type) {
|
||||
case "system":
|
||||
@@ -722,7 +719,6 @@ class ClaudeAgentSession implements AgentSession {
|
||||
input,
|
||||
suggestions: options?.suggestions as AgentPermissionUpdate[] | undefined,
|
||||
metadata: Object.keys(metadata).length ? metadata : undefined,
|
||||
raw: { toolName, input, options },
|
||||
};
|
||||
|
||||
this.pushToolCall({
|
||||
@@ -733,7 +729,6 @@ class ClaudeAgentSession implements AgentSession {
|
||||
displayName: request.title ?? toolName,
|
||||
kind: "permission",
|
||||
input,
|
||||
raw: { toolName, input },
|
||||
});
|
||||
|
||||
this.pushEvent({ type: "permission_requested", provider: "claude", request });
|
||||
@@ -762,7 +757,6 @@ class ClaudeAgentSession implements AgentSession {
|
||||
displayName: request.title ?? toolName,
|
||||
kind: "permission",
|
||||
input,
|
||||
raw: { reason: "timeout", toolName, input },
|
||||
});
|
||||
this.pushEvent({
|
||||
type: "permission_resolved",
|
||||
@@ -808,7 +802,6 @@ class ClaudeAgentSession implements AgentSession {
|
||||
this.enqueueTimeline({
|
||||
type: "todo",
|
||||
items: todoItems.length > 0 ? todoItems : [{ text: planText, completed: false }],
|
||||
raw: input,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -939,7 +932,7 @@ class ClaudeAgentSession implements AgentSession {
|
||||
if (suppressAssistant) {
|
||||
return [];
|
||||
}
|
||||
return [{ type: "assistant_message", text: content, raw: content }];
|
||||
return [{ type: "assistant_message", text: content }];
|
||||
}
|
||||
|
||||
const items: AgentTimelineItem[] = [];
|
||||
@@ -952,7 +945,7 @@ class ClaudeAgentSession implements AgentSession {
|
||||
this.streamedAssistantTextThisTurn = true;
|
||||
}
|
||||
if (!suppressAssistant) {
|
||||
items.push({ type: "assistant_message", text: block.text, raw: block });
|
||||
items.push({ type: "assistant_message", text: block.text });
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -963,7 +956,7 @@ class ClaudeAgentSession implements AgentSession {
|
||||
this.streamedReasoningThisTurn = true;
|
||||
}
|
||||
if (!suppressReasoning) {
|
||||
items.push({ type: "reasoning", text: block.thinking, raw: block });
|
||||
items.push({ type: "reasoning", text: block.thinking });
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -1009,7 +1002,6 @@ class ClaudeAgentSession implements AgentSession {
|
||||
displayName: this.buildToolDisplayName(entry),
|
||||
kind: this.getToolKind(entry.classification),
|
||||
input: entry.input ?? this.normalizeToolInput(block.input),
|
||||
raw: block,
|
||||
},
|
||||
items
|
||||
);
|
||||
@@ -1020,10 +1012,6 @@ class ClaudeAgentSession implements AgentSession {
|
||||
const server = entry?.server ?? block.server ?? "tool";
|
||||
const tool = entry?.name ?? block.tool_name ?? "tool";
|
||||
const status = block.is_error ? "failed" : "completed";
|
||||
const rawPayload =
|
||||
!block.is_error && entry?.classification === "file_change" && entry.files?.length
|
||||
? { block, files: entry.files }
|
||||
: block;
|
||||
this.pushToolCall(
|
||||
{
|
||||
server,
|
||||
@@ -1035,7 +1023,6 @@ class ClaudeAgentSession implements AgentSession {
|
||||
input: entry?.input,
|
||||
output: !block.is_error && entry?.files?.length ? { files: entry.files } : undefined,
|
||||
error: block.is_error ? block : undefined,
|
||||
raw: rawPayload,
|
||||
},
|
||||
items
|
||||
);
|
||||
@@ -1143,7 +1130,6 @@ class ClaudeAgentSession implements AgentSession {
|
||||
displayName: this.buildToolDisplayName(entry),
|
||||
kind: this.getToolKind(entry.classification),
|
||||
input: normalized,
|
||||
raw: { type: "tool_use", id: toolId, input: parsed },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1312,7 +1298,6 @@ export function convertClaudeHistoryEntry(
|
||||
timeline.push({
|
||||
type: "user_message",
|
||||
text,
|
||||
raw: message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,7 +446,6 @@ class CodexAgentSession implements AgentSession {
|
||||
displayName: request.title ?? request.name,
|
||||
kind: "permission",
|
||||
input: request.input,
|
||||
raw: { request, response },
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -610,7 +609,6 @@ class CodexAgentSession implements AgentSession {
|
||||
}
|
||||
|
||||
private *translateEvent(event: ThreadEvent): Generator<AgentStreamEvent> {
|
||||
yield { type: "provider_event", provider: "codex", raw: event };
|
||||
|
||||
const permissionEvents = this.handlePermissionEvent(event);
|
||||
if (permissionEvents) {
|
||||
@@ -658,7 +656,7 @@ class CodexAgentSession implements AgentSession {
|
||||
yield {
|
||||
type: "timeline",
|
||||
provider: "codex",
|
||||
item: { type: "error", message, raw: event },
|
||||
item: { type: "error", message },
|
||||
};
|
||||
yield { type: "turn_failed", provider: "codex", error: message };
|
||||
break;
|
||||
@@ -706,7 +704,6 @@ class CodexAgentSession implements AgentSession {
|
||||
displayName: request.title ?? request.name,
|
||||
kind: "permission",
|
||||
input: request.input,
|
||||
raw: request.raw,
|
||||
}),
|
||||
},
|
||||
{ type: "permission_requested", provider: "codex", request },
|
||||
@@ -763,7 +760,6 @@ class CodexAgentSession implements AgentSession {
|
||||
parsedCommand,
|
||||
},
|
||||
metadata: sanitizeMetadata(metadata),
|
||||
raw,
|
||||
};
|
||||
|
||||
return request;
|
||||
@@ -810,7 +806,6 @@ class CodexAgentSession implements AgentSession {
|
||||
input,
|
||||
suggestions: grantRoot ? [{ grantRoot }] : undefined,
|
||||
metadata: sanitizeMetadata(metadata),
|
||||
raw,
|
||||
};
|
||||
|
||||
return request;
|
||||
@@ -819,9 +814,9 @@ class CodexAgentSession implements AgentSession {
|
||||
private threadItemToTimeline(item: ThreadItem): AgentTimelineItem | null {
|
||||
switch (item.type) {
|
||||
case "agent_message":
|
||||
return { type: "assistant_message", text: item.text, raw: item };
|
||||
return { type: "assistant_message", text: item.text };
|
||||
case "reasoning":
|
||||
return { type: "reasoning", text: item.text, raw: item };
|
||||
return { type: "reasoning", text: item.text };
|
||||
case "command_execution":
|
||||
return createToolCallTimelineItem({
|
||||
server: "command",
|
||||
@@ -833,7 +828,6 @@ class CodexAgentSession implements AgentSession {
|
||||
input: { command: item.command, cwd: (item as any)?.cwd },
|
||||
output: (item as any)?.output,
|
||||
error: (item as any)?.error,
|
||||
raw: item,
|
||||
});
|
||||
case "file_change": {
|
||||
const files = item.changes.map((change) => ({ path: change.path, kind: change.kind }));
|
||||
@@ -845,7 +839,6 @@ class CodexAgentSession implements AgentSession {
|
||||
displayName: buildFileChangeSummary(files),
|
||||
kind: "edit",
|
||||
output: { files },
|
||||
raw: item,
|
||||
});
|
||||
}
|
||||
case "mcp_tool_call":
|
||||
@@ -858,7 +851,6 @@ class CodexAgentSession implements AgentSession {
|
||||
kind: "tool",
|
||||
input: (item as any)?.input,
|
||||
output: (item as any)?.output,
|
||||
raw: item,
|
||||
});
|
||||
case "web_search":
|
||||
return createToolCallTimelineItem({
|
||||
@@ -869,12 +861,11 @@ class CodexAgentSession implements AgentSession {
|
||||
displayName: item.query ? `Web search: ${item.query}` : "Web search",
|
||||
kind: "search",
|
||||
input: { query: item.query },
|
||||
raw: item,
|
||||
});
|
||||
case "todo_list":
|
||||
return { type: "todo", items: item.items, raw: item };
|
||||
return { type: "todo", items: item.items };
|
||||
case "error":
|
||||
return { type: "error", message: item.message, raw: item };
|
||||
return { type: "error", message: item.message };
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -992,12 +983,12 @@ function handleRolloutResponseItem(
|
||||
const text = extractMessageText(payload.content);
|
||||
if (text) {
|
||||
if (payload.role === "assistant") {
|
||||
events.push({ type: "timeline", provider: "codex", item: { type: "assistant_message", text, raw: payload } });
|
||||
events.push({ type: "timeline", provider: "codex", item: { type: "assistant_message", text } });
|
||||
} else if (payload.role === "user") {
|
||||
if (isSyntheticRolloutUserMessage(text)) {
|
||||
break;
|
||||
}
|
||||
events.push({ type: "timeline", provider: "codex", item: { type: "user_message", text, raw: payload } });
|
||||
events.push({ type: "timeline", provider: "codex", item: { type: "user_message", text } });
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -1005,7 +996,7 @@ function handleRolloutResponseItem(
|
||||
case "reasoning": {
|
||||
const text = extractReasoningText(payload);
|
||||
if (text) {
|
||||
events.push({ type: "timeline", provider: "codex", item: { type: "reasoning", text, raw: payload } });
|
||||
events.push({ type: "timeline", provider: "codex", item: { type: "reasoning", text } });
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -1031,7 +1022,7 @@ function handleRolloutEventMessage(payload: RolloutEventPayload | undefined, eve
|
||||
return;
|
||||
}
|
||||
if (payload.type === "agent_reasoning" && typeof payload.text === "string") {
|
||||
events.push({ type: "timeline", provider: "codex", item: { type: "reasoning", text: payload.text, raw: payload } });
|
||||
events.push({ type: "timeline", provider: "codex", item: { type: "reasoning", text: payload.text } });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1064,7 +1055,6 @@ function handleRolloutFunctionCall(
|
||||
displayName: buildCommandDisplayName(command),
|
||||
kind: "execute",
|
||||
input: { command, cwd },
|
||||
raw: payload,
|
||||
}),
|
||||
});
|
||||
}
|
||||
@@ -1075,7 +1065,7 @@ function handleRolloutFunctionCall(
|
||||
const args = safeJsonParse<{ plan?: unknown }>(payload.arguments);
|
||||
const planItems = parsePlanItems(args);
|
||||
if (planItems.length) {
|
||||
events.push({ type: "timeline", provider: "codex", item: { type: "todo", items: planItems, raw: payload } });
|
||||
events.push({ type: "timeline", provider: "codex", item: { type: "todo", items: planItems } });
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1091,7 +1081,6 @@ function handleRolloutFunctionCall(
|
||||
displayName: `${name}`,
|
||||
kind: "tool",
|
||||
input: safeJsonParse(payload.arguments),
|
||||
raw: payload,
|
||||
}),
|
||||
});
|
||||
}
|
||||
@@ -1123,7 +1112,6 @@ function finalizeRolloutFunctionCall(
|
||||
kind: "execute",
|
||||
input: { command: command.command, cwd: command.cwd },
|
||||
output: result,
|
||||
raw: { payload, result },
|
||||
}),
|
||||
});
|
||||
commandCalls.delete(payload.call_id);
|
||||
@@ -1143,7 +1131,6 @@ function handleRolloutCustomToolCall(payload: RolloutCustomToolCallPayload, even
|
||||
displayName: buildFileChangeSummary(files),
|
||||
kind: "edit",
|
||||
output: { files },
|
||||
raw: payload,
|
||||
}),
|
||||
});
|
||||
}
|
||||
@@ -1162,7 +1149,6 @@ function handleRolloutCustomToolCall(payload: RolloutCustomToolCallPayload, even
|
||||
kind: "tool",
|
||||
input: payload.input,
|
||||
output: payload.output,
|
||||
raw: payload,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,11 +13,6 @@ import type {
|
||||
AgentUsage,
|
||||
} from "./agent/agent-sdk-types.js";
|
||||
|
||||
type ProviderEventPayload = Extract<
|
||||
AgentStreamEvent,
|
||||
{ type: "provider_event" }
|
||||
>;
|
||||
|
||||
export type AgentSnapshotPayload = Omit<
|
||||
AgentSnapshot,
|
||||
"createdAt" | "updatedAt" | "lastUserMessageAt"
|
||||
@@ -118,7 +113,6 @@ export const AgentPermissionRequestPayloadSchema: z.ZodType<AgentPermissionReque
|
||||
input: z.record(z.unknown()).optional(),
|
||||
suggestions: z.array(AgentPermissionUpdateSchema).optional(),
|
||||
metadata: z.record(z.unknown()).optional(),
|
||||
raw: z.unknown().optional(),
|
||||
});
|
||||
|
||||
export const AgentTimelineItemPayloadSchema: z.ZodType<AgentTimelineItem> =
|
||||
@@ -127,17 +121,14 @@ export const AgentTimelineItemPayloadSchema: z.ZodType<AgentTimelineItem> =
|
||||
type: z.literal("user_message"),
|
||||
text: z.string(),
|
||||
messageId: z.string().optional(),
|
||||
raw: z.unknown().optional(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("assistant_message"),
|
||||
text: z.string(),
|
||||
raw: z.unknown().optional(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("reasoning"),
|
||||
text: z.string(),
|
||||
raw: z.unknown().optional(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("tool_call"),
|
||||
@@ -150,7 +141,6 @@ export const AgentTimelineItemPayloadSchema: z.ZodType<AgentTimelineItem> =
|
||||
input: z.unknown().optional(),
|
||||
output: z.unknown().optional(),
|
||||
error: z.unknown().optional(),
|
||||
raw: z.unknown().optional(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("todo"),
|
||||
@@ -160,21 +150,13 @@ export const AgentTimelineItemPayloadSchema: z.ZodType<AgentTimelineItem> =
|
||||
completed: z.boolean(),
|
||||
})
|
||||
),
|
||||
raw: z.unknown().optional(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("error"),
|
||||
message: z.string(),
|
||||
raw: z.unknown().optional(),
|
||||
}),
|
||||
]);
|
||||
|
||||
const ProviderEventPayloadSchema = z.object({
|
||||
type: z.literal("provider_event"),
|
||||
provider: AgentProviderSchema,
|
||||
raw: z.custom<ProviderEventPayload["raw"]>(),
|
||||
});
|
||||
|
||||
export const AgentStreamEventPayloadSchema = z.discriminatedUnion("type", [
|
||||
z.object({
|
||||
type: z.literal("thread_started"),
|
||||
@@ -200,7 +182,6 @@ export const AgentStreamEventPayloadSchema = z.discriminatedUnion("type", [
|
||||
provider: AgentProviderSchema,
|
||||
item: AgentTimelineItemPayloadSchema,
|
||||
}),
|
||||
ProviderEventPayloadSchema,
|
||||
z.object({
|
||||
type: z.literal("permission_requested"),
|
||||
provider: AgentProviderSchema,
|
||||
|
||||
@@ -2029,10 +2029,7 @@ export class Session {
|
||||
|
||||
this.emit({
|
||||
type: "agent_stream_snapshot",
|
||||
payload: {
|
||||
agentId: agent.id,
|
||||
events,
|
||||
},
|
||||
payload: { agentId: agent.id, events },
|
||||
});
|
||||
|
||||
return timeline.length;
|
||||
|
||||
Reference in New Issue
Block a user