From 7f458f7897be227ccd45e7cfe89639eafd62b989 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Mon, 25 May 2026 18:08:23 +0800 Subject: [PATCH 1/6] feat: flatten tool call records --- .changeset/flatten-toolcall.md | 8 ++ apps/kimi-code/src/cli/run-prompt.ts | 26 ++--- apps/kimi-code/src/tui/actions/replay-ops.ts | 4 +- apps/kimi-code/test/cli/run-prompt.test.ts | 2 +- apps/kimi-code/test/tui/replay-ops.test.ts | 20 +--- apps/vis/server/src/lib/context-builder.ts | 6 +- apps/vis/server/src/lib/types.ts | 2 +- apps/vis/server/src/lib/wire-replay.ts | 21 +++++ apps/vis/server/test/context-builder.test.ts | 2 +- .../src/components/context/MessageBubble.tsx | 4 +- apps/vis/web/src/types.ts | 2 +- packages/agent-core/package.json | 4 + .../src/agent/compaction/render-messages.ts | 4 +- .../agent-core/src/agent/context/index.ts | 6 +- .../agent-core/src/agent/permission/index.ts | 4 +- .../permission/policies/ask-user-question.ts | 2 +- .../policies/default-git-cwd-write.ts | 2 +- .../src/agent/permission/policies/plan.ts | 6 +- .../policies/yolo-workspace-access.ts | 2 +- .../src/agent/records/migration/index.ts | 15 ++- .../src/agent/records/migration/v1.1.ts | 74 +++++++++++++++ packages/agent-core/src/agent/turn/index.ts | 12 +-- .../agent-core/src/agent/turn/kosong-llm.ts | 6 +- packages/agent-core/src/loop/tool-call.ts | 4 +- packages/agent-core/src/utils/tokens.ts | 4 +- packages/agent-core/test/agent/basic.test.ts | 6 +- .../agent-core/test/agent/compaction.test.ts | 6 +- packages/agent-core/test/agent/config.test.ts | 6 +- .../agent-core/test/agent/context.test.ts | 3 +- .../test/agent/harness/snapshots.ts | 2 +- .../agent-core/test/agent/kosong-llm.test.ts | 17 ++-- .../agent-core/test/agent/permission.test.ts | 16 +--- packages/agent-core/test/agent/plan.test.ts | 62 ++++-------- .../test/agent/records/index.test.ts | 46 +++++++++ .../test/agent/records/migration.test.ts | 36 +++++++ packages/agent-core/test/agent/resume.test.ts | 45 +++++++++ packages/agent-core/test/agent/tool.test.ts | 24 ++--- packages/agent-core/test/agent/turn.test.ts | 24 ++--- .../test/loop/api-shape.e2e.test.ts | 8 +- .../agent-core/test/loop/fixtures/fake-llm.ts | 4 +- .../test/loop/tool-call.e2e.test.ts | 4 +- .../test/session/subagent-host.test.ts | 4 +- .../test/tools/plan-mode-hard-block.test.ts | 4 +- .../planning/exit-plan-mode-telemetry.test.ts | 4 +- packages/kosong/src/generate.ts | 9 +- packages/kosong/src/index.ts | 1 - packages/kosong/src/message.ts | 14 +-- packages/kosong/src/providers/anthropic.ts | 18 ++-- .../src/providers/chat-completions-stream.ts | 12 +-- packages/kosong/src/providers/google-genai.ts | 14 ++- packages/kosong/src/providers/kimi.ts | 8 +- .../kosong/src/providers/openai-legacy.ts | 8 +- .../kosong/src/providers/openai-responses.ts | 16 ++-- packages/kosong/test/anthropic.test.ts | 36 +++---- .../kosong/test/e2e/abort-cleanup.test.ts | 4 +- packages/kosong/test/e2e/abort-signal.test.ts | 2 +- .../kosong/test/e2e/anthropic-adapter.test.ts | 2 +- .../kosong/test/e2e/error-recovery.test.ts | 16 ++-- .../kosong/test/e2e/extreme-streams.test.ts | 14 +-- .../test/e2e/google-genai-adapter.test.ts | 6 +- .../kosong/test/e2e/kimi-adapter-e2e.test.ts | 6 +- .../test/e2e/multi-step-agent-loop.test.ts | 22 ++--- .../e2e/openai-legacy-adapter-e2e.test.ts | 6 +- .../e2e/openai-responses-adapter-e2e.test.ts | 4 +- .../test/e2e/parallel-tool-calls.test.ts | 50 +++++----- .../test/e2e/provider-error-handling.test.ts | 6 +- .../test/e2e/streaming-fidelity.test.ts | 32 +++---- .../test/e2e/streaming-providers.test.ts | 26 ++--- .../kosong/test/e2e/toolchain-bridges.test.ts | 8 +- .../kosong/test/e2e/toolset-advanced.test.ts | 26 ++--- packages/kosong/test/echo-provider.test.ts | 6 +- .../kosong/test/fixtures/echo-provider.ts | 2 +- .../kosong/test/fixtures/simple-toolset.ts | 6 +- packages/kosong/test/fixtures/step.ts | 2 +- packages/kosong/test/generate.test.ts | 94 +++++++++---------- packages/kosong/test/google-genai.test.ts | 42 ++++----- packages/kosong/test/kimi.test.ts | 26 +++-- packages/kosong/test/message.test.ts | 28 +++--- packages/kosong/test/openai-legacy.test.ts | 50 ++++++---- packages/kosong/test/openai-responses.test.ts | 18 ++-- packages/kosong/test/regression.test.ts | 4 +- .../test/scripted-echo-provider.test.ts | 6 +- packages/kosong/test/simple-toolset.test.ts | 2 +- packages/kosong/test/step.test.ts | 28 +++--- packages/kosong/test/stress.test.ts | 28 +++--- packages/kosong/test/type-safety-negative.ts | 2 +- packages/kosong/test/type-safety.test.ts | 8 +- packages/kosong/test/typed-tool.test.ts | 2 +- .../src/sessions/translator.ts | 9 +- .../fixtures.snapshot.test.ts.snap | 50 +++++----- .../test/sessions/close-tool-calls.test.ts | 2 +- .../test/sessions/translator.test.ts | 2 +- packages/node-sdk/test/export-session.test.ts | 7 +- 93 files changed, 763 insertions(+), 590 deletions(-) create mode 100644 .changeset/flatten-toolcall.md create mode 100644 packages/agent-core/src/agent/records/migration/v1.1.ts diff --git a/.changeset/flatten-toolcall.md b/.changeset/flatten-toolcall.md new file mode 100644 index 0000000..9a09dd9 --- /dev/null +++ b/.changeset/flatten-toolcall.md @@ -0,0 +1,8 @@ +--- +"@moonshot-ai/kosong": minor +"@moonshot-ai/agent-core": minor +"@moonshot-ai/kimi-code": minor +"@moonshot-ai/migration-legacy": minor +--- + +Flatten `ToolCall` by inlining `name` and `arguments` directly, removing the nested `function` object and the `ToolCallFunction` interface. diff --git a/apps/kimi-code/src/cli/run-prompt.ts b/apps/kimi-code/src/cli/run-prompt.ts index 806d60a..c48b51e 100644 --- a/apps/kimi-code/src/cli/run-prompt.ts +++ b/apps/kimi-code/src/cli/run-prompt.ts @@ -456,10 +456,8 @@ class PromptTranscriptWriter implements PromptTurnWriter { interface PromptJsonToolCall { type: 'function'; id: string; - function: { - name: string; - arguments: string; - }; + name: string; + arguments: string; } interface PromptJsonAssistantMessage { @@ -497,17 +495,15 @@ class PromptJsonWriter implements PromptTurnWriter { writeToolCall(toolCallId: string, name: string, args: unknown): void { const existing = this.toolCalls.find((toolCall) => toolCall.id === toolCallId); if (existing !== undefined) { - existing.function.name = name; - existing.function.arguments = stringifyJsonValue(args); + existing.name = name; + existing.arguments = stringifyJsonValue(args); return; } this.toolCalls.push({ type: 'function', id: toolCallId, - function: { - name, - arguments: stringifyJsonValue(args), - }, + name, + arguments: stringifyJsonValue(args), }); } @@ -518,10 +514,10 @@ class PromptJsonWriter implements PromptTurnWriter { ): void { const toolCall = this.findOrCreateToolCall(toolCallId, name ?? ''); if (name !== undefined) { - toolCall.function.name = name; + toolCall.name = name; } if (argumentsPart !== undefined) { - toolCall.function.arguments += argumentsPart; + toolCall.arguments += argumentsPart; } } @@ -560,10 +556,8 @@ class PromptJsonWriter implements PromptTurnWriter { const toolCall: PromptJsonToolCall = { type: 'function', id: toolCallId, - function: { - name, - arguments: '', - }, + name, + arguments: '', }; this.toolCalls.push(toolCall); return toolCall; diff --git a/apps/kimi-code/src/tui/actions/replay-ops.ts b/apps/kimi-code/src/tui/actions/replay-ops.ts index 8b11a2c..9272816 100644 --- a/apps/kimi-code/src/tui/actions/replay-ops.ts +++ b/apps/kimi-code/src/tui/actions/replay-ops.ts @@ -520,12 +520,12 @@ function collectMessageContent(target: OpenAssistant, content: readonly ContentP function toolCallFromMessage(rawToolCall: ToolCall): ToolCallBlockData | undefined { const id = rawToolCall.id; - const name = rawToolCall.function.name; + const name = rawToolCall.name; if (id.length === 0 || name.length === 0) return undefined; return { id, name, - args: parseToolArguments(rawToolCall.function.arguments), + args: parseToolArguments(rawToolCall.arguments), }; } diff --git a/apps/kimi-code/test/cli/run-prompt.test.ts b/apps/kimi-code/test/cli/run-prompt.test.ts index 0169acb..c34cbde 100644 --- a/apps/kimi-code/test/cli/run-prompt.test.ts +++ b/apps/kimi-code/test/cli/run-prompt.test.ts @@ -432,7 +432,7 @@ describe('runPrompt', () => { expect(stdout.text()).toBe( [ - '{"role":"assistant","content":"checking","tool_calls":[{"type":"function","id":"tc_1","function":{"name":"Shell","arguments":"{\\"command\\":\\"ls\\"}"}}]}', + '{"role":"assistant","content":"checking","tool_calls":[{"type":"function","id":"tc_1","name":"Shell","arguments":"{\\"command\\":\\"ls\\"}"}]}', '{"role":"tool","tool_call_id":"tc_1","content":"file1.py\\nfile2.py"}', '{"role":"assistant","content":"done"}', '', diff --git a/apps/kimi-code/test/tui/replay-ops.test.ts b/apps/kimi-code/test/tui/replay-ops.test.ts index 2a366e6..a94c350 100644 --- a/apps/kimi-code/test/tui/replay-ops.test.ts +++ b/apps/kimi-code/test/tui/replay-ops.test.ts @@ -279,14 +279,12 @@ describe('projectReplayRecords', () => { { type: 'function', id: 'call_agent', - function: { - name: 'Agent', + name: 'Agent', arguments: JSON.stringify({ description: 'Optimize summary', subagent_type: 'coder', run_in_background: true, }), - }, }, ], }), @@ -400,10 +398,8 @@ describe('projectReplayRecords', () => { { type: 'function', id: 'tc_1', - function: { - name: 'Bash', + name: 'Bash', arguments: '{"command":"pwd"}', - }, }, ], }), @@ -427,10 +423,8 @@ describe('projectReplayRecords', () => { { type: 'function', id: 'tc_1', - function: { - name: 'Bash', + name: 'Bash', arguments: '{"command":"false"}', - }, }, ], }), @@ -461,10 +455,8 @@ describe('projectReplayRecords', () => { { type: 'function', id: 'call_resume_bash', - function: { - name: 'Bash', + name: 'Bash', arguments: '{"command":"echo ok"}', - }, }, ], }, @@ -500,10 +492,8 @@ describe('projectReplayRecords', () => { { type: 'function', id: 'tc_media', - function: { - name: 'ReadMediaFile', + name: 'ReadMediaFile', arguments: '{"path":"/tmp/a.png"}', - }, }, ], }), diff --git a/apps/vis/server/src/lib/context-builder.ts b/apps/vis/server/src/lib/context-builder.ts index b31df20..9cfdbe8 100644 --- a/apps/vis/server/src/lib/context-builder.ts +++ b/apps/vis/server/src/lib/context-builder.ts @@ -190,10 +190,8 @@ export function buildAnnotatedMessages( currentStep.tool_calls.push({ type: 'function', id: r.data.tool_call_id, - function: { - name: r.data.tool_name, - arguments: r.data.args === undefined ? null : JSON.stringify(r.data.args), - }, + name: r.data.tool_name, + arguments: r.data.args === undefined ? null : JSON.stringify(r.data.args), }); break; } diff --git a/apps/vis/server/src/lib/types.ts b/apps/vis/server/src/lib/types.ts index 90a0149..de98d1a 100644 --- a/apps/vis/server/src/lib/types.ts +++ b/apps/vis/server/src/lib/types.ts @@ -607,7 +607,7 @@ export interface ContentPart { export interface ToolCallEntry { type: 'function'; id: string; - function: { name: string; arguments: string | null }; + name: string; arguments: string | null; } export type MessageOrigin = diff --git a/apps/vis/server/src/lib/wire-replay.ts b/apps/vis/server/src/lib/wire-replay.ts index 3060eac..d338816 100644 --- a/apps/vis/server/src/lib/wire-replay.ts +++ b/apps/vis/server/src/lib/wire-replay.ts @@ -1,5 +1,10 @@ import { readFile } from 'node:fs/promises'; +import { + migrateWireRecords, + type WireMigrationRecord, +} from '@moonshot-ai/agent-core/agent/records/migration'; + import type { ContentPartRecord, NotificationRecord, @@ -54,6 +59,22 @@ export async function replayWire(wirePath: string): Promise { rawRecords.push({ seq, raw: parsed }); } + // Extract source version from metadata (if any) and apply wire migrations. + let sourceVersion: string | undefined; + for (const record of rawRecords) { + if (record.raw['type'] === 'metadata') { + const meta = record.raw as unknown as WireFileMetadata; + sourceVersion ??= meta.protocol_version; + } + } + const migrated = migrateWireRecords( + rawRecords.map((r) => r.raw as WireMigrationRecord), + sourceVersion, + ); + for (const [i, rawRecord] of rawRecords.entries()) { + rawRecord.raw = migrated[i]!; + } + for (const [recordIndex, record] of rawRecords.entries()) { const parsed = record.raw; const seq = record.seq; diff --git a/apps/vis/server/test/context-builder.test.ts b/apps/vis/server/test/context-builder.test.ts index 9823a2c..130fb8d 100644 --- a/apps/vis/server/test/context-builder.test.ts +++ b/apps/vis/server/test/context-builder.test.ts @@ -215,7 +215,7 @@ describe('context-builder', () => { | undefined; expect(text?.text).toBe('hello!'); expect(m?.message.tool_calls).toHaveLength(1); - expect(m?.message.tool_calls[0]?.function.name).toBe('Bash'); + expect(m?.message.tool_calls[0]?.name).toBe('Bash'); }); it('builds correct origin tagging on a synthetic session with system_reminder', async () => { diff --git a/apps/vis/web/src/components/context/MessageBubble.tsx b/apps/vis/web/src/components/context/MessageBubble.tsx index 953ad30..f818bcc 100644 --- a/apps/vis/web/src/components/context/MessageBubble.tsx +++ b/apps/vis/web/src/components/context/MessageBubble.tsx @@ -126,7 +126,7 @@ function ThinkBlock({ text }: { text: string }) { function ToolCallCard({ call }: { call: ToolCallEntry }) { const [open, setOpen] = useState(false); - const argsStr = call.function.arguments ?? ''; + const argsStr = call.arguments ?? ''; return (
diff --git a/apps/vis/web/src/types.ts b/apps/vis/web/src/types.ts index 1731b20..34dfd82 100644 --- a/apps/vis/web/src/types.ts +++ b/apps/vis/web/src/types.ts @@ -159,7 +159,7 @@ export interface ContentPart { export interface ToolCallEntry { type: 'function'; id: string; - function: { name: string; arguments: string | null }; + name: string; arguments: string | null; } export interface AnnotatedMessage { diff --git a/packages/agent-core/package.json b/packages/agent-core/package.json index 8763186..bb5753e 100644 --- a/packages/agent-core/package.json +++ b/packages/agent-core/package.json @@ -37,6 +37,10 @@ "types": "./src/index.ts", "default": "./src/index.ts" }, + "./agent/records/migration": { + "types": "./src/agent/records/migration/index.ts", + "default": "./src/agent/records/migration/index.ts" + }, "./session/store": { "types": "./src/session/store/index.ts", "default": "./src/session/store/index.ts" diff --git a/packages/agent-core/src/agent/compaction/render-messages.ts b/packages/agent-core/src/agent/compaction/render-messages.ts index 81a298a..0b2285f 100644 --- a/packages/agent-core/src/agent/compaction/render-messages.ts +++ b/packages/agent-core/src/agent/compaction/render-messages.ts @@ -52,8 +52,8 @@ function renderContentPartToText(part: Message['content'][number]): string { function renderToolCallToText(toolCall: Message['toolCalls'][number]): string { const lines = [ - `- ${toolCall.id}: ${toolCall.function.name}`, - renderBlock('arguments', renderToolCallArguments(toolCall.function.arguments)), + `- ${toolCall.id}: ${toolCall.name}`, + renderBlock('arguments', renderToolCallArguments(toolCall.arguments)), ]; if (toolCall.extras !== undefined) { diff --git a/packages/agent-core/src/agent/context/index.ts b/packages/agent-core/src/agent/context/index.ts index fd9141e..a9d65cb 100644 --- a/packages/agent-core/src/agent/context/index.ts +++ b/packages/agent-core/src/agent/context/index.ts @@ -178,10 +178,8 @@ export class ContextMemory { openStep.toolCalls.push({ type: 'function', id: event.toolCallId, - function: { - name: event.name, - arguments: event.args === undefined ? null : JSON.stringify(event.args), - }, + name: event.name, + arguments: event.args === undefined ? null : JSON.stringify(event.args), }); this.pendingToolResultIds.add(event.toolCallId); return; diff --git a/packages/agent-core/src/agent/permission/index.ts b/packages/agent-core/src/agent/permission/index.ts index 7628d84..421eb49 100644 --- a/packages/agent-core/src/agent/permission/index.ts +++ b/packages/agent-core/src/agent/permission/index.ts @@ -101,7 +101,7 @@ export class PermissionManager { async beforeToolCall( context: ToolExecutionHookContext, ): Promise { - const name = context.toolCall.function.name; + const name = context.toolCall.name; const args = context.args; const mode = this.mode; @@ -151,7 +151,7 @@ export class PermissionManager { ): Promise { const { signal } = context; const id = context.toolCall.id; - const name = context.toolCall.function.name; + const name = context.toolCall.name; const args = context.args; const display = options.display ?? ({ diff --git a/packages/agent-core/src/agent/permission/policies/ask-user-question.ts b/packages/agent-core/src/agent/permission/policies/ask-user-question.ts index a2edcc4..d1af924 100644 --- a/packages/agent-core/src/agent/permission/policies/ask-user-question.ts +++ b/packages/agent-core/src/agent/permission/policies/ask-user-question.ts @@ -4,7 +4,7 @@ export const AskUserQuestionAutoPermissionPolicy: PermissionPolicy = { name: 'auto.ask-user-question', evaluate({ mode, toolCallContext }) { if (mode !== 'auto') return undefined; - if (toolCallContext.toolCall.function.name !== 'AskUserQuestion') return undefined; + if (toolCallContext.toolCall.name !== 'AskUserQuestion') return undefined; return { kind: 'result', result: { diff --git a/packages/agent-core/src/agent/permission/policies/default-git-cwd-write.ts b/packages/agent-core/src/agent/permission/policies/default-git-cwd-write.ts index 656f208..b5bc7ba 100644 --- a/packages/agent-core/src/agent/permission/policies/default-git-cwd-write.ts +++ b/packages/agent-core/src/agent/permission/policies/default-git-cwd-write.ts @@ -31,7 +31,7 @@ export function createDefaultGitCwdWritePolicy(): PermissionPolicy { if (mode !== 'manual') return undefined; if (matchedRule !== undefined) return undefined; - const toolName = toolCallContext.toolCall.function.name; + const toolName = toolCallContext.toolCall.name; if (toolName !== 'Write' && toolName !== 'Edit') return undefined; const kaos = agent.runtime.kaos; diff --git a/packages/agent-core/src/agent/permission/policies/plan.ts b/packages/agent-core/src/agent/permission/policies/plan.ts index 7747992..bb2f7ac 100644 --- a/packages/agent-core/src/agent/permission/policies/plan.ts +++ b/packages/agent-core/src/agent/permission/policies/plan.ts @@ -16,7 +16,7 @@ interface ExitPlanModeExecutionMetadata { export const EnterPlanModePermissionPolicy: PermissionPolicy = { name: 'plan.enter-plan-mode', evaluate({ toolCallContext }) { - if (toolCallContext.toolCall.function.name !== 'EnterPlanMode') return undefined; + if (toolCallContext.toolCall.name !== 'EnterPlanMode') return undefined; return { kind: 'allow' }; }, }; @@ -24,7 +24,7 @@ export const EnterPlanModePermissionPolicy: PermissionPolicy = { export const ExitPlanModePermissionPolicy: PermissionPolicy = { name: 'plan.exit-plan-mode', async evaluate(context) { - if (context.toolCallContext.toolCall.function.name !== 'ExitPlanMode') return undefined; + if (context.toolCallContext.toolCall.name !== 'ExitPlanMode') return undefined; if (context.mode === 'auto') return { kind: 'allow' }; const review = await resolveExitPlanModeReview(context); @@ -82,7 +82,7 @@ export const PlanModeGuardPermissionPolicy: PermissionPolicy = { evaluate({ agent, toolCallContext }) { if (!agent.planMode.isActive) return undefined; - const name = toolCallContext.toolCall.function.name; + const name = toolCallContext.toolCall.name; const args = toolCallContext.args; if (name === 'Write' || name === 'Edit') { diff --git a/packages/agent-core/src/agent/permission/policies/yolo-workspace-access.ts b/packages/agent-core/src/agent/permission/policies/yolo-workspace-access.ts index b95b954..d9627b1 100644 --- a/packages/agent-core/src/agent/permission/policies/yolo-workspace-access.ts +++ b/packages/agent-core/src/agent/permission/policies/yolo-workspace-access.ts @@ -23,7 +23,7 @@ export const YoloOutsideWorkspacePermissionPolicy: PermissionPolicy = { evaluate({ agent, mode, toolCallContext }) { if (mode !== 'yolo') return undefined; - const toolName = toolCallContext.toolCall.function.name; + const toolName = toolCallContext.toolCall.name; const toolAccess = FILE_ACCESS_TOOLS[toolName]; if (toolAccess === undefined) return undefined; const [operation, displayOperation] = toolAccess; diff --git a/packages/agent-core/src/agent/records/migration/index.ts b/packages/agent-core/src/agent/records/migration/index.ts index cc2fe4d..0642299 100644 --- a/packages/agent-core/src/agent/records/migration/index.ts +++ b/packages/agent-core/src/agent/records/migration/index.ts @@ -1,5 +1,7 @@ +import { migrateV1_0ToV1_1 } from './v1.1'; + // Wire protocol versions currently support only the `number.number` format. -export const AGENT_WIRE_PROTOCOL_VERSION = '1.0'; +export const AGENT_WIRE_PROTOCOL_VERSION = '1.1'; export interface WireMigrationRecord { readonly type: string; @@ -12,7 +14,7 @@ export interface WireMigration { migrateRecord(record: WireMigrationRecord): WireMigrationRecord; } -const MIGRATIONS: readonly WireMigration[] = []; +const MIGRATIONS: readonly WireMigration[] = [migrateV1_0ToV1_1]; export function resolveWireMigrations(readVersion: string): readonly WireMigration[] { if (compareWireVersions(readVersion, AGENT_WIRE_PROTOCOL_VERSION) === 0) { @@ -48,6 +50,15 @@ export function migrateWireRecord( ); } +export function migrateWireRecords( + records: readonly WireMigrationRecord[], + readVersion: string | undefined, +): WireMigrationRecord[] { + const migrations = + readVersion === undefined ? MIGRATIONS : resolveWireMigrations(readVersion); + return records.map((record) => migrateWireRecord(record, migrations)); +} + function findMigration(sourceVersion: string): WireMigration | undefined { for (const migration of MIGRATIONS) { if (migration.sourceVersion === sourceVersion) return migration; diff --git a/packages/agent-core/src/agent/records/migration/v1.1.ts b/packages/agent-core/src/agent/records/migration/v1.1.ts new file mode 100644 index 0000000..4ca5cd3 --- /dev/null +++ b/packages/agent-core/src/agent/records/migration/v1.1.ts @@ -0,0 +1,74 @@ +import type { WireMigration, WireMigrationRecord } from './index'; + +/** + * Wire records before v1.1 used a nested `function` wrapper for each tool call: + * { function: { name: 'xxx', arguments: 'yyy' } } + * v1.1 flattens it to: + * { name: 'xxx', arguments: 'yyy' } + */ +interface LegacyToolCall { + function?: { + name?: string; + arguments?: string; + }; +} + +function isLegacyToolCall(v: unknown): v is LegacyToolCall { + return ( + typeof v === 'object' && + v !== null && + 'function' in v && + typeof (v as Record)['function'] === 'object' && + (v as Record)['function'] !== null + ); +} + +function migrateToolCall(v: LegacyToolCall): unknown { + const { function: fn, ...rest } = v; + return { + ...rest, + name: fn?.name, + arguments: fn?.arguments, + }; +} + +/** + * Recursively replace old-style ToolCall objects in a plain JSON value. + * Operates in-place (mutable) for performance. + */ +function migrateLegacyToolCalls(value: unknown): unknown { + if (Array.isArray(value)) { + for (let i = 0; i < value.length; i++) { + const item = value[i]; + if (isLegacyToolCall(item)) { + value[i] = migrateToolCall(item); + } else { + migrateLegacyToolCalls(item); + } + } + return value; + } + + if (typeof value === 'object' && value !== null) { + const obj = value as Record; + for (const key of Object.keys(obj)) { + const v = obj[key]; + if (isLegacyToolCall(v)) { + obj[key] = migrateToolCall(v); + } else { + migrateLegacyToolCalls(v); + } + } + return obj; + } + + return value; +} + +export const migrateV1_0ToV1_1: WireMigration = { + sourceVersion: '1.0', + targetVersion: '1.1', + migrateRecord(record: WireMigrationRecord): WireMigrationRecord { + return migrateLegacyToolCalls(structuredClone(record)) as WireMigrationRecord; + }, +}; diff --git a/packages/agent-core/src/agent/turn/index.ts b/packages/agent-core/src/agent/turn/index.ts index 1b14b2a..cf64acc 100644 --- a/packages/agent-core/src/agent/turn/index.ts +++ b/packages/agent-core/src/agent/turn/index.ts @@ -429,15 +429,15 @@ export class TurnFlow { prepareToolExecution: async (ctx) => { const cached = deduper.checkSameStep( ctx.toolCall.id, - ctx.toolCall.function.name, + ctx.toolCall.name, ctx.args, ); if (cached !== null) return { syntheticResult: cached }; const hookResult = await this.agent.hooks?.triggerBlock('PreToolUse', { - matcherValue: ctx.toolCall.function.name, + matcherValue: ctx.toolCall.name, signal: ctx.signal, inputData: { - toolName: ctx.toolCall.function.name, + toolName: ctx.toolCall.name, toolInput: toolInputRecord(ctx.args), toolCallId: ctx.toolCall.id, }, @@ -463,16 +463,16 @@ export class TurnFlow { // original's real outcome, not an empty success. const finalResult = await deduper.finalizeResult( ctx.toolCall.id, - ctx.toolCall.function.name, + ctx.toolCall.name, ctx.args, ctx.result, ); const { isError, output } = finalResult; const event = isError === true ? 'PostToolUseFailure' : 'PostToolUse'; void this.agent.hooks?.fireAndForgetTrigger(event, { - matcherValue: ctx.toolCall.function.name, + matcherValue: ctx.toolCall.name, inputData: { - toolName: ctx.toolCall.function.name, + toolName: ctx.toolCall.name, toolInput: toolInputRecord(ctx.args), toolCallId: ctx.toolCall.id, error: isError === true ? toKimiErrorPayload(toolOutputText(output)) : undefined, diff --git a/packages/agent-core/src/agent/turn/kosong-llm.ts b/packages/agent-core/src/agent/turn/kosong-llm.ts index 834fa47..9ca56af 100644 --- a/packages/agent-core/src/agent/turn/kosong-llm.ts +++ b/packages/agent-core/src/agent/turn/kosong-llm.ts @@ -191,15 +191,15 @@ function buildKosongCallbacks(params: LLMChatParams): GenerateCallbacks { return; } if (part.type === 'function') { - const identity = { toolCallId: part.id, name: part.function.name }; + const identity = { toolCallId: part.id, name: part.name }; lastToolCallIdentity = identity; if (part._streamIndex !== undefined) { toolCallIdentities.set(part._streamIndex, identity); } emitToolCallDelta({ toolCallId: part.id, - name: part.function.name, - ...(part.function.arguments !== null ? { argumentsPart: part.function.arguments } : {}), + name: part.name, + ...(part.arguments !== null ? { argumentsPart: part.arguments } : {}), }); if (part._streamIndex !== undefined) { const pendingDeltas = pendingIndexedToolCallDeltas.get(part._streamIndex); diff --git a/packages/agent-core/src/loop/tool-call.ts b/packages/agent-core/src/loop/tool-call.ts index 6981ebb..143fdda 100644 --- a/packages/agent-core/src/loop/tool-call.ts +++ b/packages/agent-core/src/loop/tool-call.ts @@ -156,8 +156,8 @@ function preflightToolCall( tools: readonly ExecutableTool[] | undefined, toolCall: ToolCall, ): PreflightedToolCall { - const toolName = toolCall.function.name; - const parsedArgs = parseToolCallArguments(toolCall.function.arguments); + const toolName = toolCall.name; + const parsedArgs = parseToolCallArguments(toolCall.arguments); const args = parsedArgs.success ? parsedArgs.data : {}; const tool = tools?.find((candidate) => candidate.name === toolName); if (tool === undefined) { diff --git a/packages/agent-core/src/utils/tokens.ts b/packages/agent-core/src/utils/tokens.ts index 5ccef7d..a6547e9 100644 --- a/packages/agent-core/src/utils/tokens.ts +++ b/packages/agent-core/src/utils/tokens.ts @@ -46,8 +46,8 @@ export function estimateTokensForMessage(message: Message): number { } if (message.toolCalls !== undefined) { for (const call of message.toolCalls) { - total += estimateTokens(call.function.name); - total += estimateTokens(JSON.stringify(call.function.arguments)); + total += estimateTokens(call.name); + total += estimateTokens(JSON.stringify(call.arguments)); } } return total; diff --git a/packages/agent-core/test/agent/basic.test.ts b/packages/agent-core/test/agent/basic.test.ts index 0a3e4da..5eade27 100644 --- a/packages/agent-core/test/agent/basic.test.ts +++ b/packages/agent-core/test/agent/basic.test.ts @@ -77,10 +77,8 @@ it('runs an agent turn through builtin tool approval and execution', async () => const bashCall: ToolCall = { type: 'function', id: 'call_bash', - function: { - name: 'Bash', - arguments: '{"command":"printf lookup-result","timeout":60}', - }, + name: 'Bash', + arguments: '{"command":"printf lookup-result","timeout":60}', }; const ctx = testAgent({ kaos: createCommandKaos('lookup-result') }); ctx.configure({ tools: ['Bash'] }); diff --git a/packages/agent-core/test/agent/compaction.test.ts b/packages/agent-core/test/agent/compaction.test.ts index 5fd8511..c0bf654 100644 --- a/packages/agent-core/test/agent/compaction.test.ts +++ b/packages/agent-core/test/agent/compaction.test.ts @@ -1478,10 +1478,8 @@ function missingToolCall(): ToolCall { return { type: 'function', id: 'call_missing', - function: { - name: 'MissingTool', - arguments: '{}', - }, + name: 'MissingTool', + arguments: '{}', }; } diff --git a/packages/agent-core/test/agent/config.test.ts b/packages/agent-core/test/agent/config.test.ts index d73a37a..de4a303 100644 --- a/packages/agent-core/test/agent/config.test.ts +++ b/packages/agent-core/test/agent/config.test.ts @@ -98,10 +98,8 @@ describe('Agent config', () => { const bashCall: ToolCall = { type: 'function', id: 'call_bash', - function: { - name: 'Bash', - arguments: '{"command":"printf original-result","timeout":60}', - }, + name: 'Bash', + arguments: '{"command":"printf original-result","timeout":60}', }; const ctx = testAgent({ kaos: createCommandKaos('original-result') }); ctx.configure({ tools: ['Bash'] }); diff --git a/packages/agent-core/test/agent/context.test.ts b/packages/agent-core/test/agent/context.test.ts index bd5b8cc..41731b6 100644 --- a/packages/agent-core/test/agent/context.test.ts +++ b/packages/agent-core/test/agent/context.test.ts @@ -736,7 +736,8 @@ function assistantToolCallMessage(ids: readonly string[]): Message { toolCalls: ids.map((id) => ({ type: 'function', id, - function: { name: 'Lookup', arguments: '{}' }, + name: 'Lookup', + arguments: '{}', })), }; } diff --git a/packages/agent-core/test/agent/harness/snapshots.ts b/packages/agent-core/test/agent/harness/snapshots.ts index e0ff133..bee99f9 100644 --- a/packages/agent-core/test/agent/harness/snapshots.ts +++ b/packages/agent-core/test/agent/harness/snapshots.ts @@ -229,7 +229,7 @@ function formatText(text: string): string { } function formatToolCall(call: Message['toolCalls'][number]): string { - return `${call.id}:${call.function.name} ${formatToolCallArguments(call.function.arguments)}`; + return `${call.id}:${call.name} ${formatToolCallArguments(call.arguments)}`; } function formatToolCallArguments(args: string | null): string { diff --git a/packages/agent-core/test/agent/kosong-llm.test.ts b/packages/agent-core/test/agent/kosong-llm.test.ts index 38130b9..4430101 100644 --- a/packages/agent-core/test/agent/kosong-llm.test.ts +++ b/packages/agent-core/test/agent/kosong-llm.test.ts @@ -27,7 +27,8 @@ describe('KosongLLM streaming tool-call deltas', () => { { type: 'function', id: 'call_bash', - function: { name: 'Bash', arguments: null }, + name: 'Bash', + arguments: null, _streamIndex: 0, }, { type: 'tool_call_part', argumentsPart: '{"command"', index: 0 }, @@ -47,7 +48,8 @@ describe('KosongLLM streaming tool-call deltas', () => { { type: 'function', id: 'call_bash', - function: { name: 'Bash', arguments: null }, + name: 'Bash', + arguments: null, _streamIndex: 0, }, { type: 'tool_call_part', argumentsPart: ':"pwd"}', index: 0 }, @@ -66,7 +68,8 @@ describe('KosongLLM streaming tool-call deltas', () => { { type: 'function', id: 'call_write', - function: { name: 'Write', arguments: null }, + name: 'Write', + arguments: null, }, { type: 'tool_call_part', argumentsPart: '{"path"' }, { type: 'tool_call_part', argumentsPart: ':"a.txt"}' }, @@ -124,10 +127,6 @@ function isToolCall(part: StreamedMessagePart): part is ToolCall { } function stripStreamIndex(toolCall: ToolCall): ToolCall { - return { - type: 'function', - id: toolCall.id, - function: { ...toolCall.function }, - ...(toolCall.extras !== undefined ? { extras: toolCall.extras } : {}), - }; + const { _streamIndex: _, ...rest } = toolCall; + return rest; } diff --git a/packages/agent-core/test/agent/permission.test.ts b/packages/agent-core/test/agent/permission.test.ts index 987e87b..73ef67d 100644 --- a/packages/agent-core/test/agent/permission.test.ts +++ b/packages/agent-core/test/agent/permission.test.ts @@ -190,10 +190,8 @@ describe('Agent permission', () => { const bashCall: ToolCall = { type: 'function', id: 'call_bash', - function: { - name: 'Bash', - arguments: '{"command":"printf should-not-run","timeout":60}', - }, + name: 'Bash', + arguments: '{"command":"printf should-not-run","timeout":60}', }; const ctx = testAgent({ kaos: createFakeKaos({ execWithEnv }), @@ -1795,10 +1793,8 @@ function bashCall(): ToolCall { return { type: 'function', id: 'call_bash', - function: { - name: 'Bash', - arguments: '{"command":"printf permission-output","timeout":60}', - }, + name: 'Bash', + arguments: '{"command":"printf permission-output","timeout":60}', }; } @@ -1906,10 +1902,8 @@ function hookContext(input: { const toolCall: ToolCall = { type: 'function', id: input.id, - function: { - name: toolName, + name: toolName, arguments: JSON.stringify(args), - }, }; return { turnId: '0', diff --git a/packages/agent-core/test/agent/plan.test.ts b/packages/agent-core/test/agent/plan.test.ts index a92ba61..fe34b69 100644 --- a/packages/agent-core/test/agent/plan.test.ts +++ b/packages/agent-core/test/agent/plan.test.ts @@ -69,10 +69,8 @@ describe('manual plan entry', () => { const enterPlanModeCall: ToolCall = { type: 'function', id: 'call_enter_plan', - function: { - name: 'EnterPlanMode', - arguments: '{}', - }, + name: 'EnterPlanMode', + arguments: '{}', }; const ctx = testAgent({ kaos: createPlanKaos({ @@ -147,10 +145,8 @@ describe('plan exit tool', () => { const exitPlanModeCall: ToolCall = { type: 'function', id: 'call_exit_plan', - function: { - name: 'ExitPlanMode', - arguments: '{}', - }, + name: 'ExitPlanMode', + arguments: '{}', }; ctx.mockNextResponse({ type: 'text', text: 'I will present the plan.' }, exitPlanModeCall); ctx.mockNextResponse({ type: 'text', text: 'I can execute after approval.' }); @@ -185,10 +181,8 @@ describe('plan exit tool', () => { const exitPlanModeCall: ToolCall = { type: 'function', id: 'call_exit_reject', - function: { - name: 'ExitPlanMode', - arguments: '{}', - }, + name: 'ExitPlanMode', + arguments: '{}', }; ctx.mockNextResponse({ type: 'text', text: 'I will present the plan.' }, exitPlanModeCall); ctx.mockNextResponse({ type: 'text', text: 'This response must not be requested.' }); @@ -225,18 +219,14 @@ describe('plan exit tool', () => { const exitPlanModeCall: ToolCall = { type: 'function', id: 'call_exit_reject_and_exit', - function: { - name: 'ExitPlanMode', - arguments: '{}', - }, + name: 'ExitPlanMode', + arguments: '{}', }; const bashCall: ToolCall = { type: 'function', id: 'call_bash_after_reject', - function: { - name: 'Bash', - arguments: '{"command":"touch should-not-run","timeout":60}', - }, + name: 'Bash', + arguments: '{"command":"touch should-not-run","timeout":60}', }; ctx.mockNextResponse( { type: 'text', text: 'I will present the plan and then run a command.' }, @@ -271,10 +261,8 @@ describe('plan exit tool', () => { const exitPlanModeCall: ToolCall = { type: 'function', id: 'call_exit_empty_plan', - function: { - name: 'ExitPlanMode', - arguments: '{}', - }, + name: 'ExitPlanMode', + arguments: '{}', }; ctx.mockNextResponse( { type: 'text', text: 'I will present the empty plan.' }, @@ -308,8 +296,7 @@ describe('plan exit tool options', () => { const exitPlanModeCall: ToolCall = { type: 'function', id: 'call_exit_options', - function: { - name: 'ExitPlanMode', + name: 'ExitPlanMode', // The second option omits `description` — valid input after the // schema relaxation. The approval policy must still surface both. arguments: JSON.stringify({ @@ -318,7 +305,6 @@ describe('plan exit tool options', () => { { label: 'Approach B' }, ], }), - }, }; ctx.mockNextResponse({ type: 'text', text: 'I will present the plan.' }, exitPlanModeCall); ctx.mockNextResponse({ type: 'text', text: 'I can execute after approval.' }); @@ -368,10 +354,8 @@ describe('plan allows safe tool flow', () => { const writePlanCall: ToolCall = { type: 'function', id: `call_${toolName.toLowerCase()}_plan`, - function: { - name: toolName, + name: toolName, arguments: JSON.stringify(args), - }, }; ctx.mockNextResponse({ type: 'text', text: 'I will update the plan file.' }, writePlanCall); @@ -413,10 +397,8 @@ describe('plan allows safe tool flow', () => { const writePlanCall: ToolCall = { type: 'function', id: 'call_write_plan_with_deny', - function: { - name: 'Write', - arguments: JSON.stringify({ path: planPath, content }), - }, + name: 'Write', + arguments: JSON.stringify({ path: planPath, content }), }; ctx.mockNextResponse({ type: 'text', text: 'I will update the plan file.' }, writePlanCall); @@ -439,10 +421,8 @@ describe('plan allows safe tool flow', () => { const bashCall: ToolCall = { type: 'function', id: 'call_bash', - function: { - name: 'Bash', - arguments: '{"command":"printf plan-safe","timeout":60}', - }, + name: 'Bash', + arguments: '{"command":"printf plan-safe","timeout":60}', }; const ctx = testAgent({ kaos: createCommandKaos('plan-safe') }); ctx.configure({ tools: ['Bash'] }); @@ -493,10 +473,8 @@ describe('plan mode Bash ordinary permission behavior', () => { const bashCall: ToolCall = { type: 'function', id: 'call_bash', - function: { - name: 'Bash', - arguments: '{"command":"rm forbidden.txt","timeout":60}', - }, + name: 'Bash', + arguments: '{"command":"rm forbidden.txt","timeout":60}', }; const ctx = testAgent({ kaos: createCommandKaos('removed') }); ctx.configure({ tools: ['Bash'] }); diff --git a/packages/agent-core/test/agent/records/index.test.ts b/packages/agent-core/test/agent/records/index.test.ts index 8fc3a2d..13513c3 100644 --- a/packages/agent-core/test/agent/records/index.test.ts +++ b/packages/agent-core/test/agent/records/index.test.ts @@ -111,6 +111,52 @@ describe('AgentRecords persistence metadata', () => { expect(persistence.rewrites).toEqual([]); }); + it('rewrites migrated records to the current wire version after replay', async () => { + const persistence = new RecordingInMemoryAgentRecordPersistence([ + { + type: 'metadata', + protocol_version: '1.0', + created_at: 1, + }, + { + type: 'context.append_message', + message: { + role: 'assistant', + content: [], + toolCalls: [ + { + type: 'function', + id: 'call_legacy_bash', + function: { + name: 'Bash', + arguments: '{"command":"pwd"}', + }, + }, + ], + }, + } as unknown as AgentRecord, + ]); + const records = new AgentRecords(() => {}, persistence); + + await records.replay(); + + expect(persistence.rewrites).toHaveLength(1); + expect(persistence.records[0]).toMatchObject({ + type: 'metadata', + protocol_version: AGENT_WIRE_PROTOCOL_VERSION, + }); + const migrated = persistence.records[1] as unknown as { + readonly message: { + readonly toolCalls: readonly Record[]; + }; + }; + expect(migrated.message.toolCalls[0]).toMatchObject({ + name: 'Bash', + arguments: '{"command":"pwd"}', + }); + expect(migrated.message.toolCalls[0]?.['function']).toBeUndefined(); + }); + it('rejects replaying records from a newer wire version', async () => { const persistence = new InMemoryAgentRecordPersistence([ { diff --git a/packages/agent-core/test/agent/records/migration.test.ts b/packages/agent-core/test/agent/records/migration.test.ts index 697a695..e15304c 100644 --- a/packages/agent-core/test/agent/records/migration.test.ts +++ b/packages/agent-core/test/agent/records/migration.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; import { + migrateWireRecords, migrateWireRecord, type WireMigration, } from '../../../src/agent/records/migration'; @@ -32,4 +33,39 @@ describe('wire record migrations', () => { second: true, }); }); + + it('flattens legacy tool calls when migrating v1.0 records', () => { + const migrated = migrateWireRecords( + [ + { + type: 'context.append_message', + message: { + role: 'assistant', + toolCalls: [ + { + type: 'function', + id: 'call_legacy_bash', + function: { + name: 'Bash', + arguments: '{"command":"pwd"}', + }, + }, + ], + }, + }, + ], + '1.0', + ); + + const toolCall = ( + migrated[0]?.['message'] as { + readonly toolCalls: readonly Record[]; + } + ).toolCalls[0]; + expect(toolCall).toMatchObject({ + name: 'Bash', + arguments: '{"command":"pwd"}', + }); + expect(toolCall?.['function']).toBeUndefined(); + }); }); diff --git a/packages/agent-core/test/agent/resume.test.ts b/packages/agent-core/test/agent/resume.test.ts index 5c355c4..f116b25 100644 --- a/packages/agent-core/test/agent/resume.test.ts +++ b/packages/agent-core/test/agent/resume.test.ts @@ -128,6 +128,45 @@ describe('Agent resume', () => { await ctx.expectResumeMatches(); }); + it('applies wire migrations while replaying persisted records', async () => { + const persistence = new RecordingAgentPersistence([ + { + type: 'metadata', + protocol_version: '1.0', + created_at: 1, + }, + { + type: 'context.append_message', + message: { + role: 'assistant', + content: [], + toolCalls: [ + { + type: 'function', + id: 'call_legacy_bash', + function: { + name: 'Bash', + arguments: '{"command":"pwd"}', + }, + }, + ], + }, + } as unknown as AgentRecord, + ]); + const ctx = testAgent({ persistence }); + + await ctx.agent.resume(); + + const toolCall = ctx.agent.context.messages[0]?.toolCalls[0] as + | { name?: string; arguments?: string | null; function?: unknown } + | undefined; + expect(toolCall).toMatchObject({ + name: 'Bash', + arguments: '{"command":"pwd"}', + }); + expect(toolCall?.function).toBeUndefined(); + }); + it('keeps delivered background notifications indexed after compaction replay', async () => { const origin = { kind: 'background_task', @@ -288,6 +327,7 @@ describe('Agent resume', () => { class RecordingAgentPersistence extends InMemoryAgentRecordPersistence { readonly appended: AgentRecord[] = []; + rewritten: readonly AgentRecord[] | undefined; constructor(events: readonly AgentRecord[]) { super(withMetadata(events)); @@ -297,6 +337,11 @@ class RecordingAgentPersistence extends InMemoryAgentRecordPersistence { this.appended.push(input); super.append(input); } + + override rewrite(records: readonly AgentRecord[]): void { + this.rewritten = records; + super.rewrite(records); + } } function withMetadata(events: readonly AgentRecord[]): readonly AgentRecord[] { diff --git a/packages/agent-core/test/agent/tool.test.ts b/packages/agent-core/test/agent/tool.test.ts index 542f666..b956bf3 100644 --- a/packages/agent-core/test/agent/tool.test.ts +++ b/packages/agent-core/test/agent/tool.test.ts @@ -155,10 +155,8 @@ describe('Agent tools', () => { const lookupCall: ToolCall = { type: 'function', id: 'call_lookup', - function: { - name: 'Lookup', - arguments: '{"query":"moon"}', - }, + name: 'Lookup', + arguments: '{"query":"moon"}', }; const resolved: Array<[string, string, string]> = []; const hookEngine = new HookEngine( @@ -255,10 +253,8 @@ describe('Agent tools', () => { const lookupCall: ToolCall = { type: 'function', id: 'call_lookup', - function: { - name: 'Lookup', - arguments: '{"query":"moon"}', - }, + name: 'Lookup', + arguments: '{"query":"moon"}', }; const ctx = testAgent(); ctx.configure(); @@ -367,10 +363,8 @@ function bashCall(): ToolCall { return { type: 'function', id: 'call_bash', - function: { - name: 'Bash', - arguments: '{"command":"printf hook-output","timeout":60}', - }, + name: 'Bash', + arguments: '{"command":"printf hook-output","timeout":60}', }; } @@ -378,14 +372,12 @@ function agentCall(): ToolCall { return { type: 'function', id: 'call_agent', - function: { - name: 'Agent', - arguments: JSON.stringify({ + name: 'Agent', + arguments: JSON.stringify({ prompt: 'Investigate deeply', description: 'Investigate deeply', subagent_type: 'coder', }), - }, }; } diff --git a/packages/agent-core/test/agent/turn.test.ts b/packages/agent-core/test/agent/turn.test.ts index f201b08..67c84be 100644 --- a/packages/agent-core/test/agent/turn.test.ts +++ b/packages/agent-core/test/agent/turn.test.ts @@ -206,10 +206,8 @@ describe('Agent turn flow', () => { ctx.mockNextResponse({ type: 'function', id: 'call_missing', - function: { - name: 'MissingTool', - arguments: '{}', - }, + name: 'MissingTool', + arguments: '{}', }); ctx.mockNextResponse({ type: 'text', text: 'done' }); @@ -860,10 +858,8 @@ describe('Agent turn flow', () => { const bashCall: ToolCall = { id: 'call_bash', type: 'function', - function: { - name: 'Bash', - arguments: '{"command":"printf loop-output","timeout":60}', - }, + name: 'Bash', + arguments: '{"command":"printf loop-output","timeout":60}', }; ctx.mockNextResponse(bashCall); @@ -1261,10 +1257,8 @@ describe('Agent turn flow', () => { const bashCall: ToolCall = { type: 'function', id: 'call_bash', - function: { - name: 'Bash', - arguments: '{"command":"printf approved","timeout":60}', - }, + name: 'Bash', + arguments: '{"command":"printf approved","timeout":60}', }; const ctx = testAgent({ kaos: createCommandKaos('approved'), @@ -1388,10 +1382,8 @@ function bashCallWithId(id: string, command: string): ToolCall { return { type: 'function', id, - function: { - name: 'Bash', - arguments: JSON.stringify({ command, timeout: 60 }), - }, + name: 'Bash', + arguments: JSON.stringify({ command, timeout: 60 }), }; } diff --git a/packages/agent-core/test/loop/api-shape.e2e.test.ts b/packages/agent-core/test/loop/api-shape.e2e.test.ts index 49b6c14..371c052 100644 --- a/packages/agent-core/test/loop/api-shape.e2e.test.ts +++ b/packages/agent-core/test/loop/api-shape.e2e.test.ts @@ -296,14 +296,14 @@ function _typeOnlyChecks(): void { const toolCallHookContext: ToolExecutionHookContext = { ...stepHookContext, - toolCall: { type: 'function', id: 'tc1', function: { name: 'echo', arguments: '{}' } }, + toolCall: { type: 'function', id: 'tc1', name: 'echo', arguments: '{}' }, args: {}, }; void toolCallHookContext; const _badToolExecutionHookContext: ToolExecutionHookContext = { ...stepHookContext, - toolCall: { type: 'function', id: 'tc1', function: { name: 'echo', arguments: '{}' } }, + toolCall: { type: 'function', id: 'tc1', name: 'echo', arguments: '{}' }, // @ts-expect-error — tool hooks receive `args`, not the old `input` field. input: {}, }; @@ -401,12 +401,12 @@ function _typeOnlyChecks(): void { const toolCall: ToolCall = { type: 'function', id: 'tc1', - function: { name: 'echo', arguments: '{"text":"hi"}' }, + name: 'echo', arguments: '{"text":"hi"}', }; void toolCall; const _badToolCall: ToolCall = { - // @ts-expect-error — the loop no longer owns a parsed `{ name, args }` tool-call shape. name: 'echo', + // @ts-expect-error — ToolCall has `name` but no `args` property. args: {}, }; void _badToolCall; diff --git a/packages/agent-core/test/loop/fixtures/fake-llm.ts b/packages/agent-core/test/loop/fixtures/fake-llm.ts index f346ff5..5729fc6 100644 --- a/packages/agent-core/test/loop/fixtures/fake-llm.ts +++ b/packages/agent-core/test/loop/fixtures/fake-llm.ts @@ -199,9 +199,7 @@ export function makeToolCall(name: string, args: unknown, id?: string): ToolCall return { type: 'function', id: id ?? `call_${Math.random().toString(36).slice(2, 10)}`, - function: { - name, + name, arguments: JSON.stringify(args), - }, }; } diff --git a/packages/agent-core/test/loop/tool-call.e2e.test.ts b/packages/agent-core/test/loop/tool-call.e2e.test.ts index 2a21de3..0934fca 100644 --- a/packages/agent-core/test/loop/tool-call.e2e.test.ts +++ b/packages/agent-core/test/loop/tool-call.e2e.test.ts @@ -162,10 +162,8 @@ describe('runTurn — tool-call behaviour', () => { { type: 'function', id: 'tc-1', - function: { - name: 'echo', + name: 'echo', arguments: '{', - }, }, ]), makeEndTurnResponse('done'), diff --git a/packages/agent-core/test/session/subagent-host.test.ts b/packages/agent-core/test/session/subagent-host.test.ts index fb23152..a1a6c06 100644 --- a/packages/agent-core/test/session/subagent-host.test.ts +++ b/packages/agent-core/test/session/subagent-host.test.ts @@ -1069,10 +1069,8 @@ function bashCall(): ToolCall { return { type: 'function', id: 'call_bash', - function: { - name: 'Bash', + name: 'Bash', arguments: '{"command":"printf should-not-run","timeout":60}', - }, }; } diff --git a/packages/agent-core/test/tools/plan-mode-hard-block.test.ts b/packages/agent-core/test/tools/plan-mode-hard-block.test.ts index 2cea3b4..6a549a9 100644 --- a/packages/agent-core/test/tools/plan-mode-hard-block.test.ts +++ b/packages/agent-core/test/tools/plan-mode-hard-block.test.ts @@ -38,10 +38,8 @@ function hookContext(toolName: string, args: unknown): ToolExecutionHookContext toolCall: { type: 'function', id: `call_${toolName}`, - function: { - name: toolName, + name: toolName, arguments: JSON.stringify(args), - }, } satisfies ToolCall, } as ToolExecutionHookContext; } diff --git a/packages/agent-core/test/tools/planning/exit-plan-mode-telemetry.test.ts b/packages/agent-core/test/tools/planning/exit-plan-mode-telemetry.test.ts index ddba6e4..f7c05cd 100644 --- a/packages/agent-core/test/tools/planning/exit-plan-mode-telemetry.test.ts +++ b/packages/agent-core/test/tools/planning/exit-plan-mode-telemetry.test.ts @@ -86,10 +86,8 @@ function permissionContext(args: ExitPlanModeInput): ToolExecutionHookContext { toolCall: { id: 'call_exit_plan', type: 'function', - function: { - name: 'ExitPlanMode', + name: 'ExitPlanMode', arguments: JSON.stringify(args), - }, }, args, }; diff --git a/packages/kosong/src/generate.ts b/packages/kosong/src/generate.ts index 3dd9f3e..5661870 100644 --- a/packages/kosong/src/generate.ts +++ b/packages/kosong/src/generate.ts @@ -133,10 +133,10 @@ export async function generate( if (arrayIdx !== undefined) { const target = message.toolCalls[arrayIdx]; if (target !== undefined && part.argumentsPart !== null) { - target.function.arguments = - target.function.arguments === null + target.arguments = + target.arguments === null ? part.argumentsPart - : target.function.arguments + part.argumentsPart; + : target.arguments + part.argumentsPart; } continue; } @@ -261,7 +261,8 @@ function flushPart( const stored: StoredToolCall = { type: 'function', id: part.id, - function: part.function, + name: part.name, + arguments: part.arguments, extras: part.extras, }; const ordinal = message.toolCalls.length; diff --git a/packages/kosong/src/index.ts b/packages/kosong/src/index.ts index 224a55c..713fd4e 100644 --- a/packages/kosong/src/index.ts +++ b/packages/kosong/src/index.ts @@ -19,7 +19,6 @@ export type { TextPart, ThinkPart, ToolCall, - ToolCallFunction, ToolCallPart, VideoURLPart, } from './message'; diff --git a/packages/kosong/src/message.ts b/packages/kosong/src/message.ts index 5bea821..4bd6db5 100644 --- a/packages/kosong/src/message.ts +++ b/packages/kosong/src/message.ts @@ -35,15 +35,11 @@ export interface VideoURLPart { */ export type ContentPart = TextPart | ThinkPart | ImageURLPart | AudioURLPart | VideoURLPart; -export interface ToolCallFunction { - name: string; - arguments: string | null; -} - export interface ToolCall { type: 'function'; id: string; - function: ToolCallFunction; + name: string; + arguments: string | null; extras?: Record; /** * Provider-specific streaming index used to route argument deltas to the @@ -164,10 +160,10 @@ export function mergeInPlace(target: StreamedMessagePart, source: StreamedMessag // ToolCall + ToolCallPart if (target.type === 'function' && source.type === 'tool_call_part') { if (source.argumentsPart !== null) { - target.function.arguments = - target.function.arguments === null + target.arguments = + target.arguments === null ? source.argumentsPart - : target.function.arguments + source.argumentsPart; + : target.arguments + source.argumentsPart; } return true; } diff --git a/packages/kosong/src/providers/anthropic.ts b/packages/kosong/src/providers/anthropic.ts index d70dff7..17cc71f 100644 --- a/packages/kosong/src/providers/anthropic.ts +++ b/packages/kosong/src/providers/anthropic.ts @@ -477,9 +477,9 @@ function convertMessage(message: Message): MessageParam { if (message.toolCalls.length > 0) { for (const tc of message.toolCalls) { let toolInput: Record = {}; - if (tc.function.arguments) { + if (tc.arguments) { try { - const parsed: unknown = JSON.parse(tc.function.arguments); + const parsed: unknown = JSON.parse(tc.arguments); if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) { toolInput = parsed as Record; } else { @@ -493,7 +493,7 @@ function convertMessage(message: Message): MessageParam { blocks.push({ type: 'tool_use', id: tc.id, - name: tc.function.name, + name: tc.name, input: toolInput, } satisfies ToolUseBlockParam); } @@ -648,10 +648,8 @@ class AnthropicStreamedMessage implements StreamedMessage { yield { type: 'function', id: block.id ?? crypto.randomUUID(), - function: { - name: block.name ?? '', - arguments: block.input !== undefined ? JSON.stringify(block.input) : null, - }, + name: block.name ?? '', + arguments: block.input !== undefined ? JSON.stringify(block.input) : null, } satisfies ToolCall; break; } @@ -703,10 +701,8 @@ class AnthropicStreamedMessage implements StreamedMessage { yield { type: 'function', id: block.id, - function: { - name: block.name, - arguments: '', - }, + name: block.name, + arguments: '', // Carry the Anthropic block index so parallel tool_use // blocks' interleaved input_json_delta chunks can be routed // to the correct ToolCall by the generate loop. diff --git a/packages/kosong/src/providers/chat-completions-stream.ts b/packages/kosong/src/providers/chat-completions-stream.ts index 8d1114f..ec09452 100644 --- a/packages/kosong/src/providers/chat-completions-stream.ts +++ b/packages/kosong/src/providers/chat-completions-stream.ts @@ -46,10 +46,8 @@ export function convertChatCompletionStreamToolCall( { type: 'function', id: toolCall.id ?? crypto.randomUUID(), - function: { - name: functionName, - arguments: functionArguments ?? null, - }, + name: functionName, + arguments: functionArguments ?? null, } satisfies ToolCall, ]; } @@ -88,10 +86,8 @@ export function convertChatCompletionStreamToolCall( const toolCallHeader: ToolCall = { type: 'function', id: buffered.id ?? toolCall.id ?? crypto.randomUUID(), - function: { - name: functionName, - arguments: initialArguments, - }, + name: functionName, + arguments: initialArguments, _streamIndex: streamIndex, }; return [toolCallHeader]; diff --git a/packages/kosong/src/providers/google-genai.ts b/packages/kosong/src/providers/google-genai.ts index bd98058..c1fef43 100644 --- a/packages/kosong/src/providers/google-genai.ts +++ b/packages/kosong/src/providers/google-genai.ts @@ -251,9 +251,9 @@ function messageToGoogleGenAI(message: Message): GoogleContent { // Handle tool calls for (const toolCall of message.toolCalls) { let args: Record = {}; - if (toolCall.function.arguments) { + if (toolCall.arguments) { try { - const parsed: unknown = JSON.parse(toolCall.function.arguments); + const parsed: unknown = JSON.parse(toolCall.arguments); if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) { args = parsed as Record; } else { @@ -267,7 +267,7 @@ function messageToGoogleGenAI(message: Message): GoogleContent { const functionCallPart: GooglePart = { function_call: { - name: toolCall.function.name, + name: toolCall.name, args, }, }; @@ -373,7 +373,7 @@ export function messagesToGoogleGenAIContents(messages: Message[]): GoogleConten contents.push(messageToGoogleGenAI(message)); const expectedToolCallIds: string[] = []; for (const toolCall of message.toolCalls) { - toolNameById.set(toolCall.id, toolCall.function.name); + toolNameById.set(toolCall.id, toolCall.name); expectedToolCallIds.push(toolCall.id); } @@ -542,10 +542,8 @@ export class GoogleGenAIStreamedMessage implements StreamedMessage { parts.push({ type: 'function', id: toolCallId, - function: { - name, - arguments: fc['args'] ? JSON.stringify(fc['args']) : '{}', - }, + name, + arguments: fc['args'] ? JSON.stringify(fc['args']) : '{}', ...(thoughtSigB64 ? { extras: { thought_signature_b64: thoughtSigB64 as string } } : {}), diff --git a/packages/kosong/src/providers/kimi.ts b/packages/kosong/src/providers/kimi.ts index 1aebb8a..77a7239 100644 --- a/packages/kosong/src/providers/kimi.ts +++ b/packages/kosong/src/providers/kimi.ts @@ -140,7 +140,7 @@ function convertMessage(message: Message): OpenAIMessage { const mapped: OpenAIToolCallOut = { type: tc.type, id: tc.id, - function: { name: tc.function.name, arguments: tc.function.arguments }, + function: { name: tc.name, arguments: tc.arguments }, }; if (tc.extras !== undefined) { mapped.extras = tc.extras; @@ -281,10 +281,8 @@ class KimiStreamedMessage implements StreamedMessage { yield { type: 'function', id: toolCall.id || crypto.randomUUID(), - function: { - name: toolCall.function.name, - arguments: toolCall.function.arguments, - }, + name: toolCall.function.name, + arguments: toolCall.function.arguments, } satisfies ToolCall; } } diff --git a/packages/kosong/src/providers/openai-legacy.ts b/packages/kosong/src/providers/openai-legacy.ts index 07de4b7..48bc19b 100644 --- a/packages/kosong/src/providers/openai-legacy.ts +++ b/packages/kosong/src/providers/openai-legacy.ts @@ -139,7 +139,7 @@ function convertMessage( result.tool_calls = message.toolCalls.map((tc) => ({ type: tc.type, id: tc.id, - function: { name: tc.function.name, arguments: tc.function.arguments }, + function: { name: tc.name, arguments: tc.arguments }, })); } @@ -236,10 +236,8 @@ export class OpenAILegacyStreamedMessage implements StreamedMessage { yield { type: 'function', id: toolCall.id || crypto.randomUUID(), - function: { - name: toolCall.function.name, - arguments: toolCall.function.arguments, - }, + name: toolCall.function.name, + arguments: toolCall.function.arguments, } satisfies ToolCall; } } diff --git a/packages/kosong/src/providers/openai-responses.ts b/packages/kosong/src/providers/openai-responses.ts index 1802a2f..7a9147e 100644 --- a/packages/kosong/src/providers/openai-responses.ts +++ b/packages/kosong/src/providers/openai-responses.ts @@ -451,9 +451,9 @@ function convertMessage( // Handle tool calls for (const toolCall of message.toolCalls) { result.push({ - arguments: toolCall.function.arguments ?? '{}', + arguments: toolCall.arguments ?? '{}', call_id: toolCall.id, - name: toolCall.function.name, + name: toolCall.name, type: 'function_call', }); } @@ -556,10 +556,8 @@ export class OpenAIResponsesStreamedMessage implements StreamedMessage { yield { type: 'function', id: functionCallId(outputItem.callId), - function: { - name: requireFunctionCallName(outputItem), - arguments: outputItem.arguments ?? null, - }, + name: requireFunctionCallName(outputItem), + arguments: outputItem.arguments ?? null, } satisfies ToolCall; } else if (outputItem.type === 'reasoning') { for (const summary of outputItem.summary) { @@ -701,10 +699,8 @@ export class OpenAIResponsesStreamedMessage implements StreamedMessage { const tc: ToolCall = { type: 'function', id: functionCallId(item.callId), - function: { - name: requireFunctionCallName(item), - arguments: item.arguments ?? null, - }, + name: requireFunctionCallName(item), + arguments: item.arguments ?? null, }; if (streamIndex !== undefined) { tc._streamIndex = streamIndex; diff --git a/packages/kosong/test/anthropic.test.ts b/packages/kosong/test/anthropic.test.ts index 5cfe429..5be4186 100644 --- a/packages/kosong/test/anthropic.test.ts +++ b/packages/kosong/test/anthropic.test.ts @@ -274,7 +274,7 @@ describe('AnthropicChatProvider', () => { const toolCall: ToolCall = { type: 'function', id: 'call_abc123', - function: { name: 'add', arguments: '{"a": 2, "b": 3}' }, + name: 'add', arguments: '{"a": 2, "b": 3}', }; const history: Message[] = [ { role: 'user', content: [{ type: 'text', text: 'Add 2 and 3' }], toolCalls: [] }, @@ -326,7 +326,7 @@ describe('AnthropicChatProvider', () => { const toolCall: ToolCall = { type: 'function', id: 'call_abc123', - function: { name: 'add', arguments: '{"a": 2, "b": 3}' }, + name: 'add', arguments: '{"a": 2, "b": 3}', }; const history: Message[] = [ { role: 'user', content: [{ type: 'text', text: 'Add 2 and 3' }], toolCalls: [] }, @@ -373,12 +373,12 @@ describe('AnthropicChatProvider', () => { const tcAdd: ToolCall = { type: 'function', id: 'call_add', - function: { name: 'add', arguments: '{"a": 2, "b": 3}' }, + name: 'add', arguments: '{"a": 2, "b": 3}', }; const tcMul: ToolCall = { type: 'function', id: 'call_mul', - function: { name: 'multiply', arguments: '{"a": 4, "b": 5}' }, + name: 'multiply', arguments: '{"a": 4, "b": 5}', }; const history: Message[] = [ { @@ -481,12 +481,12 @@ describe('AnthropicChatProvider', () => { const tcAdd: ToolCall = { type: 'function', id: 'call_add', - function: { name: 'add', arguments: '{"a": 2, "b": 3}' }, + name: 'add', arguments: '{"a": 2, "b": 3}', }; const tcMul: ToolCall = { type: 'function', id: 'call_mul', - function: { name: 'multiply', arguments: '{"a": 4, "b": 5}' }, + name: 'multiply', arguments: '{"a": 4, "b": 5}', }; const history: Message[] = [ { role: 'user', content: [{ type: 'text', text: 'Calculate 2+3 and 4*5' }], toolCalls: [] }, @@ -537,7 +537,7 @@ describe('AnthropicChatProvider', () => { const tcAdd: ToolCall = { type: 'function', id: 'call_add', - function: { name: 'add', arguments: '{"a": 2, "b": 3}' }, + name: 'add', arguments: '{"a": 2, "b": 3}', }; const history: Message[] = [ { role: 'user', content: [{ type: 'text', text: 'What is 2+3?' }], toolCalls: [] }, @@ -571,7 +571,7 @@ describe('AnthropicChatProvider', () => { const makeTc = (id: string, name: string): ToolCall => ({ type: 'function', id, - function: { name, arguments: '{"a": 1, "b": 1}' }, + name, arguments: '{"a": 1, "b": 1}', }); const history: Message[] = [ { role: 'user', content: [{ type: 'text', text: 'Do three things' }], toolCalls: [] }, @@ -605,12 +605,12 @@ describe('AnthropicChatProvider', () => { const tcAdd: ToolCall = { type: 'function', id: 'call_add', - function: { name: 'add', arguments: '{"a": 2, "b": 3}' }, + name: 'add', arguments: '{"a": 2, "b": 3}', }; const tcMul: ToolCall = { type: 'function', id: 'call_mul', - function: { name: 'multiply', arguments: '{"a": 4, "b": 5}' }, + name: 'multiply', arguments: '{"a": 4, "b": 5}', }; const history: Message[] = [ { role: 'user', content: [{ type: 'text', text: 'Do both' }], toolCalls: [] }, @@ -1386,7 +1386,7 @@ describe('AnthropicChatProvider', () => { { type: 'function', id: 'tool_1', - function: { name: 'add', arguments: '{"a":2,"b":3}' }, + name: 'add', arguments: '{"a":2,"b":3}', }, ]); expect(stream.usage).toEqual({ @@ -1552,7 +1552,7 @@ describe('AnthropicChatProvider', () => { { type: 'function', id: 'toolu_abc', - function: { name: 'add', arguments: '' }, + name: 'add', arguments: '', _streamIndex: 1, }, { type: 'tool_call_part', argumentsPart: '{"a":', index: 1 }, @@ -1626,13 +1626,13 @@ describe('AnthropicChatProvider', () => { { type: 'function', id: 'toolu_a', - function: { name: 'tool_a', arguments: '' }, + name: 'tool_a', arguments: '', _streamIndex: 0, }, { type: 'function', id: 'toolu_b', - function: { name: 'tool_b', arguments: '' }, + name: 'tool_b', arguments: '', _streamIndex: 1, }, { type: 'tool_call_part', argumentsPart: '{"x":', index: 0 }, @@ -1705,11 +1705,11 @@ describe('AnthropicChatProvider', () => { expect(message.toolCalls.length).toBe(2); expect(message.toolCalls[0]!.id).toBe('toolu_a'); - expect(message.toolCalls[0]!.function.name).toBe('tool_a'); - expect(message.toolCalls[0]!.function.arguments).toBe('{"x":1}'); + expect(message.toolCalls[0]!.name).toBe('tool_a'); + expect(message.toolCalls[0]!.arguments).toBe('{"x":1}'); expect(message.toolCalls[1]!.id).toBe('toolu_b'); - expect(message.toolCalls[1]!.function.name).toBe('tool_b'); - expect(message.toolCalls[1]!.function.arguments).toBe('{"y":2}'); + expect(message.toolCalls[1]!.name).toBe('tool_b'); + expect(message.toolCalls[1]!.arguments).toBe('{"y":2}'); // _streamIndex should be stripped from stored tool calls. expect( (message.toolCalls[0] as ToolCall & { _streamIndex?: number })._streamIndex, diff --git a/packages/kosong/test/e2e/abort-cleanup.test.ts b/packages/kosong/test/e2e/abort-cleanup.test.ts index 2ff5ea4..13daf1e 100644 --- a/packages/kosong/test/e2e/abort-cleanup.test.ts +++ b/packages/kosong/test/e2e/abort-cleanup.test.ts @@ -30,7 +30,7 @@ function clonePart(part: StreamedMessagePart): StreamedMessagePart { return { type: 'function', id: part.id, - function: { name: part.function.name, arguments: part.function.arguments }, + name: part.name, arguments: part.arguments, ...(part.extras !== undefined ? { extras: { ...part.extras } } : {}), _streamIndex: part._streamIndex, }; @@ -169,7 +169,7 @@ describe('e2e: abort cleanup', () => { { type: 'function', id: 'tc_slow', - function: { name: 'slow', arguments: '{}' }, + name: 'slow', arguments: '{}', }, ]; const provider = new TrackingProvider(parts, 0); diff --git a/packages/kosong/test/e2e/abort-signal.test.ts b/packages/kosong/test/e2e/abort-signal.test.ts index e7829ec..186759d 100644 --- a/packages/kosong/test/e2e/abort-signal.test.ts +++ b/packages/kosong/test/e2e/abort-signal.test.ts @@ -114,7 +114,7 @@ describe('e2e: abort signal', () => { { type: 'function', id: 'tool-1', - function: { name: 'slow-tool', arguments: '{}' }, + name: 'slow-tool', arguments: '{}', }, ]); diff --git a/packages/kosong/test/e2e/anthropic-adapter.test.ts b/packages/kosong/test/e2e/anthropic-adapter.test.ts index a64517a..cb19175 100644 --- a/packages/kosong/test/e2e/anthropic-adapter.test.ts +++ b/packages/kosong/test/e2e/anthropic-adapter.test.ts @@ -193,7 +193,7 @@ describe('e2e: Anthropic adapter bridge', () => { { type: 'function', id: 'toolu_1', - function: { name: 'add', arguments: '' }, + name: 'add', arguments: '', _streamIndex: 1, } satisfies ToolCall, { type: 'tool_call_part', argumentsPart: '{"a":2', index: 1 }, diff --git a/packages/kosong/test/e2e/error-recovery.test.ts b/packages/kosong/test/e2e/error-recovery.test.ts index 0af05c5..3636b8a 100644 --- a/packages/kosong/test/e2e/error-recovery.test.ts +++ b/packages/kosong/test/e2e/error-recovery.test.ts @@ -181,7 +181,7 @@ describe('e2e: error recovery', () => { const tc: ToolCall = { type: 'function', id: 'tc-1', - function: { name: 'slow_tool', arguments: '{}' }, + name: 'slow_tool', arguments: '{}', }; const provider = createStreamErrorProvider( @@ -219,17 +219,17 @@ describe('e2e: error recovery', () => { const tc1: ToolCall = { type: 'function', id: 'tc-ok', - function: { name: 'good_tool', arguments: '{}' }, + name: 'good_tool', arguments: '{}', }; const tc2: ToolCall = { type: 'function', id: 'tc-fail', - function: { name: 'bad_tool', arguments: '{}' }, + name: 'bad_tool', arguments: '{}', }; const tc3: ToolCall = { type: 'function', id: 'tc-ok-2', - function: { name: 'good_tool', arguments: '{}' }, + name: 'good_tool', arguments: '{}', }; const stream = createMockStream([tc1, tc2, tc3]); @@ -276,7 +276,7 @@ describe('e2e: error recovery', () => { const tc: ToolCall = { type: 'function', id: 'tc-missing', - function: { name: 'nonexistent_tool', arguments: '{}' }, + name: 'nonexistent_tool', arguments: '{}', }; const stream = createMockStream([{ type: 'text', text: 'calling' }, tc]); @@ -297,7 +297,7 @@ describe('e2e: error recovery', () => { const tc: ToolCall = { type: 'function', id: 'tc-bad-json', - function: { name: 'my_tool', arguments: '{invalid json' }, + name: 'my_tool', arguments: '{invalid json', }; const stream = createMockStream([{ type: 'text', text: 'calling' }, tc]); @@ -326,7 +326,7 @@ describe('e2e: error recovery', () => { const tc: ToolCall = { type: 'function', id: 'tc-str-throw', - function: { name: 'throws_string', arguments: '{}' }, + name: 'throws_string', arguments: '{}', }; const stream = createMockStream([tc]); @@ -357,7 +357,7 @@ describe('e2e: error recovery', () => { const tc: ToolCall = { type: 'function', id: 'tc-cleanup', - function: { name: 'cleanup_test', arguments: '{}' }, + name: 'cleanup_test', arguments: '{}', }; let handlerInvoked = false; diff --git a/packages/kosong/test/e2e/extreme-streams.test.ts b/packages/kosong/test/e2e/extreme-streams.test.ts index 379eb5f..6b1bdba 100644 --- a/packages/kosong/test/e2e/extreme-streams.test.ts +++ b/packages/kosong/test/e2e/extreme-streams.test.ts @@ -78,7 +78,7 @@ describe('e2e: extreme streaming scenarios', () => { parts.push({ type: 'function', id: `tc_${i}`, - function: { name: 'f', arguments: null }, + name: 'f', arguments: null, _streamIndex: i, }); } @@ -105,8 +105,8 @@ describe('e2e: extreme streaming scenarios', () => { for (let i = 0; i < n; i++) { const tc = result.message.toolCalls[i]!; expect(tc.id).toBe(`tc_${i}`); - expect(tc.function.name).toBe('f'); - expect(tc.function.arguments).toBe(`{"i":${i}}`); + expect(tc.name).toBe('f'); + expect(tc.arguments).toBe(`{"i":${i}}`); // _streamIndex must be stripped from the stored ToolCall. expect(tc).not.toHaveProperty('_streamIndex'); } @@ -119,7 +119,7 @@ describe('e2e: extreme streaming scenarios', () => { { type: 'function', id: 'tc_big', - function: { name: 'writeBlob', arguments: '{"blob":"' }, + name: 'writeBlob', arguments: '{"blob":"', _streamIndex: 0, }, ]; @@ -134,7 +134,7 @@ describe('e2e: extreme streaming scenarios', () => { const elapsed = Date.now() - t0; expect(result.message.toolCalls).toHaveLength(1); - const args = result.message.toolCalls[0]!.function.arguments; + const args = result.message.toolCalls[0]!.arguments; if (args === null) { throw new Error('Expected assembled tool-call arguments'); } @@ -159,7 +159,7 @@ describe('e2e: extreme streaming scenarios', () => { parts.push({ type: 'function', id: `tc_${i}`, - function: { name: 'f', arguments: null }, + name: 'f', arguments: null, _streamIndex: i, }); } @@ -175,7 +175,7 @@ describe('e2e: extreme streaming scenarios', () => { for (let i = 0; i < n; i++) { const tc = result.message.toolCalls[i]!; // Assembled args preserve the per-call routing contract. - expect(tc.function.arguments).toBe(`{"idx":${i}}`); + expect(tc.arguments).toBe(`{"idx":${i}}`); } }); }); diff --git a/packages/kosong/test/e2e/google-genai-adapter.test.ts b/packages/kosong/test/e2e/google-genai-adapter.test.ts index 7b548d7..21c9d50 100644 --- a/packages/kosong/test/e2e/google-genai-adapter.test.ts +++ b/packages/kosong/test/e2e/google-genai-adapter.test.ts @@ -145,12 +145,12 @@ describe('e2e: Google GenAI adapter bridge', () => { { type: 'function', id: 'call_add', - function: { name: 'add', arguments: '{"a":2,"b":3}' }, + name: 'add', arguments: '{"a":2,"b":3}', }, { type: 'function', id: 'call_mul', - function: { name: 'multiply', arguments: '{"a":4,"b":5}' }, + name: 'multiply', arguments: '{"a":4,"b":5}', }, ], }, @@ -180,7 +180,7 @@ describe('e2e: Google GenAI adapter bridge', () => { { type: 'function', id: 'notify_call-1', - function: { name: 'notify', arguments: '{"ok":true}' }, + name: 'notify', arguments: '{"ok":true}', extras: { thought_signature_b64: 'sig-1' }, } satisfies ToolCall, ]); diff --git a/packages/kosong/test/e2e/kimi-adapter-e2e.test.ts b/packages/kosong/test/e2e/kimi-adapter-e2e.test.ts index 796ace6..50055d6 100644 --- a/packages/kosong/test/e2e/kimi-adapter-e2e.test.ts +++ b/packages/kosong/test/e2e/kimi-adapter-e2e.test.ts @@ -131,7 +131,8 @@ describe('e2e: kimi adapter', () => { { type: 'function', id: 'call_weather', - function: { name: 'lookup_weather', arguments: '{"city":"Shanghai"}' }, + name: 'lookup_weather', + arguments: '{"city":"Shanghai"}', } satisfies ToolCall, ], }, @@ -185,7 +186,8 @@ describe('e2e: kimi adapter', () => { expect(result.parts[2]).toMatchObject({ type: 'function', id: 'call_weather', - function: { name: 'lookup_weather', arguments: '{"city":"' }, + name: 'lookup_weather', + arguments: '{"city":"', }); expect(result.parts[2]).toHaveProperty('_streamIndex', 0); expect(result.parts[3]).toMatchObject({ diff --git a/packages/kosong/test/e2e/multi-step-agent-loop.test.ts b/packages/kosong/test/e2e/multi-step-agent-loop.test.ts index dcf217d..dea7d85 100644 --- a/packages/kosong/test/e2e/multi-step-agent-loop.test.ts +++ b/packages/kosong/test/e2e/multi-step-agent-loop.test.ts @@ -112,7 +112,7 @@ describe('e2e: multi-step agent loop', () => { const toolCall: ToolCall = { type: 'function', id: 'tc-1', - function: { name: 'search', arguments: '{"query":"vitest"}' }, + name: 'search', arguments: '{"query":"vitest"}', }; const provider = new QueuedMockProvider([ @@ -147,12 +147,12 @@ describe('e2e: multi-step agent loop', () => { const tcSearch: ToolCall = { type: 'function', id: 'tc-search', - function: { name: 'search', arguments: '{"query":"vitest"}' }, + name: 'search', arguments: '{"query":"vitest"}', }; const tcRead: ToolCall = { type: 'function', id: 'tc-read', - function: { name: 'read_file', arguments: '{"path":"/docs/vitest.md"}' }, + name: 'read_file', arguments: '{"path":"/docs/vitest.md"}', }; const provider = new QueuedMockProvider([ @@ -183,17 +183,17 @@ describe('e2e: multi-step agent loop', () => { const tc1: ToolCall = { type: 'function', id: 'tc-a', - function: { name: 'fetch_url', arguments: '{"url":"https://a.com"}' }, + name: 'fetch_url', arguments: '{"url":"https://a.com"}', }; const tc2: ToolCall = { type: 'function', id: 'tc-b', - function: { name: 'fetch_url', arguments: '{"url":"https://b.com"}' }, + name: 'fetch_url', arguments: '{"url":"https://b.com"}', }; const tc3: ToolCall = { type: 'function', id: 'tc-c', - function: { name: 'fetch_url', arguments: '{"url":"https://c.com"}' }, + name: 'fetch_url', arguments: '{"url":"https://c.com"}', }; const provider = new QueuedMockProvider([ @@ -236,12 +236,12 @@ describe('e2e: multi-step agent loop', () => { const tcList: ToolCall = { type: 'function', id: 'tc-list', - function: { name: 'list_files', arguments: '{"dir":"/src"}' }, + name: 'list_files', arguments: '{"dir":"/src"}', }; const tcRead: ToolCall = { type: 'function', id: 'tc-read', - function: { name: 'read_file', arguments: '{"path":"/src/main.ts"}' }, + name: 'read_file', arguments: '{"path":"/src/main.ts"}', }; // The provider sees history growing—tool A result is in history when @@ -277,12 +277,12 @@ describe('e2e: multi-step agent loop', () => { const tc1: ToolCall = { type: 'function', id: 'tc-1', - function: { name: 'calc', arguments: '{"expr":"2+2"}' }, + name: 'calc', arguments: '{"expr":"2+2"}', }; const tc2: ToolCall = { type: 'function', id: 'tc-2', - function: { name: 'calc', arguments: '{"expr":"4*3"}' }, + name: 'calc', arguments: '{"expr":"4*3"}', }; const provider = new QueuedMockProvider([ @@ -338,7 +338,7 @@ describe('e2e: multi-step agent loop', () => { const tc: ToolCall = { type: 'function', id: 'tc-no-args', - function: { name: 'get_time', arguments: null }, + name: 'get_time', arguments: null, }; const provider = new QueuedMockProvider([ diff --git a/packages/kosong/test/e2e/openai-legacy-adapter-e2e.test.ts b/packages/kosong/test/e2e/openai-legacy-adapter-e2e.test.ts index 6afeb98..33657d5 100644 --- a/packages/kosong/test/e2e/openai-legacy-adapter-e2e.test.ts +++ b/packages/kosong/test/e2e/openai-legacy-adapter-e2e.test.ts @@ -115,7 +115,8 @@ describe('e2e: openai-legacy adapter', () => { { type: 'function', id: 'call_weather', - function: { name: 'lookup_weather', arguments: '{"city":"Shanghai"}' }, + name: 'lookup_weather', + arguments: '{"city":"Shanghai"}', } satisfies ToolCall, ], }, @@ -180,7 +181,8 @@ describe('e2e: openai-legacy adapter', () => { expect(result.parts[1]).toMatchObject({ type: 'function', id: 'call_weather', - function: { name: 'lookup_weather', arguments: '{"city":"' }, + name: 'lookup_weather', + arguments: '{"city":"', }); expect(result.parts[1]).toHaveProperty('_streamIndex', 0); expect(result.parts[2]).toMatchObject({ diff --git a/packages/kosong/test/e2e/openai-responses-adapter-e2e.test.ts b/packages/kosong/test/e2e/openai-responses-adapter-e2e.test.ts index ef47b31..e2d18ef 100644 --- a/packages/kosong/test/e2e/openai-responses-adapter-e2e.test.ts +++ b/packages/kosong/test/e2e/openai-responses-adapter-e2e.test.ts @@ -96,7 +96,7 @@ describe('e2e: openai-responses adapter', () => { { type: 'function', id: 'call_weather', - function: { name: 'lookup_weather', arguments: '{"city":"Shanghai"}' }, + name: 'lookup_weather', arguments: '{"city":"Shanghai"}', } satisfies ToolCall, ], }, @@ -168,7 +168,7 @@ describe('e2e: openai-responses adapter', () => { expect(result.parts[1]).toMatchObject({ type: 'function', id: 'call_weather', - function: { name: 'lookup_weather', arguments: '{"city":"Shanghai"}' }, + name: 'lookup_weather', arguments: '{"city":"Shanghai"}', }); }); }); diff --git a/packages/kosong/test/e2e/parallel-tool-calls.test.ts b/packages/kosong/test/e2e/parallel-tool-calls.test.ts index 05e6b52..5260ebe 100644 --- a/packages/kosong/test/e2e/parallel-tool-calls.test.ts +++ b/packages/kosong/test/e2e/parallel-tool-calls.test.ts @@ -56,14 +56,14 @@ describe('integration: parallel tool calls through SimpleToolset', () => { { type: 'function', id: 'tc_read_a', - function: { name: 'read_file', arguments: '' }, + name: 'read_file', arguments: '', _streamIndex: 0, }, { type: 'tool_call_part', argumentsPart: '{"path":"a.txt"}', index: 0 }, { type: 'function', id: 'tc_read_b', - function: { name: 'read_file', arguments: '' }, + name: 'read_file', arguments: '', _streamIndex: 1, }, { type: 'tool_call_part', argumentsPart: '{"path":"b.txt"}', index: 1 }, @@ -83,7 +83,7 @@ describe('integration: parallel tool calls through SimpleToolset', () => { } }, onToolCall(toolCall): void { - events.push(`ready:${toolCall.id}:${toolCall.function.arguments ?? ''}`); + events.push(`ready:${toolCall.id}:${toolCall.arguments ?? ''}`); }, }); @@ -105,19 +105,19 @@ describe('integration: parallel tool calls through SimpleToolset', () => { { type: 'function', id: 'tc_read', - function: { name: 'read_file', arguments: null }, + name: 'read_file', arguments: null, _streamIndex: 0, }, { type: 'function', id: 'tc_write', - function: { name: 'write_file', arguments: null }, + name: 'write_file', arguments: null, _streamIndex: 1, }, { type: 'function', id: 'tc_list', - function: { name: 'list_dir', arguments: null }, + name: 'list_dir', arguments: null, _streamIndex: 2, }, // Interleaved argument deltas. Arguments for each call, arriving @@ -194,14 +194,14 @@ describe('integration: parallel tool calls through SimpleToolset', () => { // Three tool calls, in stream order. expect(result.toolCalls).toHaveLength(3); - expect(result.toolCalls[0]!.function.name).toBe('read_file'); - expect(result.toolCalls[1]!.function.name).toBe('write_file'); - expect(result.toolCalls[2]!.function.name).toBe('list_dir'); + expect(result.toolCalls[0]!.name).toBe('read_file'); + expect(result.toolCalls[1]!.name).toBe('write_file'); + expect(result.toolCalls[2]!.name).toBe('list_dir'); // Fully-assembled arguments — no cross-contamination. - expect(result.toolCalls[0]!.function.arguments).toBe('{"path":"a.txt"}'); - expect(result.toolCalls[1]!.function.arguments).toBe('{"path":"b.txt","data":"X"}'); - expect(result.toolCalls[2]!.function.arguments).toBe('{"path":"/tmp"}'); + expect(result.toolCalls[0]!.arguments).toBe('{"path":"a.txt"}'); + expect(result.toolCalls[1]!.arguments).toBe('{"path":"b.txt","data":"X"}'); + expect(result.toolCalls[2]!.arguments).toBe('{"path":"/tmp"}'); // Each handler saw the correct parsed JSON arguments. const toolResults = await result.toolResults(); @@ -233,19 +233,19 @@ describe('integration: parallel tool calls through SimpleToolset', () => { { type: 'function', id: 'tc_a', - function: { name: 'slow', arguments: '{"id":"a"}' }, + name: 'slow', arguments: '{"id":"a"}', _streamIndex: 0, }, { type: 'function', id: 'tc_b', - function: { name: 'slow', arguments: '{"id":"b"}' }, + name: 'slow', arguments: '{"id":"b"}', _streamIndex: 1, }, { type: 'function', id: 'tc_c', - function: { name: 'slow', arguments: '{"id":"c"}' }, + name: 'slow', arguments: '{"id":"c"}', _streamIndex: 2, }, ]; @@ -298,19 +298,19 @@ describe('integration: parallel tool calls through SimpleToolset', () => { { type: 'function', id: 'tc_slow', - function: { name: 'sleep_tool', arguments: '{"ms":100,"tag":"slow"}' }, + name: 'sleep_tool', arguments: '{"ms":100,"tag":"slow"}', _streamIndex: 0, }, { type: 'function', id: 'tc_med', - function: { name: 'sleep_tool', arguments: '{"ms":50,"tag":"med"}' }, + name: 'sleep_tool', arguments: '{"ms":50,"tag":"med"}', _streamIndex: 1, }, { type: 'function', id: 'tc_fast', - function: { name: 'sleep_tool', arguments: '{"ms":1,"tag":"fast"}' }, + name: 'sleep_tool', arguments: '{"ms":1,"tag":"fast"}', _streamIndex: 2, }, ]; @@ -355,19 +355,19 @@ describe('integration: parallel tool calls through SimpleToolset', () => { { type: 'function', id: 'tc_ok_1', - function: { name: 'good', arguments: '{"i":1}' }, + name: 'good', arguments: '{"i":1}', _streamIndex: 0, }, { type: 'function', id: 'tc_bad', - function: { name: 'bad', arguments: '{}' }, + name: 'bad', arguments: '{}', _streamIndex: 1, }, { type: 'function', id: 'tc_ok_2', - function: { name: 'good', arguments: '{"i":2}' }, + name: 'good', arguments: '{"i":2}', _streamIndex: 2, }, ]; @@ -414,13 +414,13 @@ describe('integration: parallel tool calls through SimpleToolset', () => { { type: 'function', id: 'tc_alpha', - function: { name: 't', arguments: null }, + name: 't', arguments: null, _streamIndex: 'item_alpha', }, { type: 'function', id: 'tc_beta', - function: { name: 't', arguments: null }, + name: 't', arguments: null, _streamIndex: 'item_beta', }, { type: 'tool_call_part', argumentsPart: '{"k":', index: 'item_alpha' }, @@ -440,8 +440,8 @@ describe('integration: parallel tool calls through SimpleToolset', () => { }); expect(result.message.toolCalls).toHaveLength(2); - expect(result.message.toolCalls[0]!.function.arguments).toBe('{"k":"A"}'); - expect(result.message.toolCalls[1]!.function.arguments).toBe('{"k":"B"}'); + expect(result.message.toolCalls[0]!.arguments).toBe('{"k":"A"}'); + expect(result.message.toolCalls[1]!.arguments).toBe('{"k":"B"}'); // onToolCall fired once per fully-assembled call after stream drained. expect(seen).toHaveLength(2); expect(seen[0]!.id).toBe('tc_alpha'); diff --git a/packages/kosong/test/e2e/provider-error-handling.test.ts b/packages/kosong/test/e2e/provider-error-handling.test.ts index 107e83e..105b15d 100644 --- a/packages/kosong/test/e2e/provider-error-handling.test.ts +++ b/packages/kosong/test/e2e/provider-error-handling.test.ts @@ -216,7 +216,7 @@ describe('e2e: provider error handling (extended)', () => { const tc: ToolCall = { type: 'function', id: 'tc_sfx', - function: { name: 'side_effect', arguments: '{}' }, + name: 'side_effect', arguments: '{}', }; const provider = createMidStreamThrowingProvider( @@ -246,7 +246,7 @@ describe('e2e: provider error handling (extended)', () => { { type: 'function', id: 'tc_search', - function: { name: 'search', arguments: null }, + name: 'search', arguments: null, _streamIndex: 0, }, { type: 'tool_call_part', argumentsPart: '{"q":', index: 0 }, @@ -306,7 +306,7 @@ describe('e2e: provider error handling (extended)', () => { const tc: ToolCall = { type: 'function', id: 'tc_only', - function: { name: 'no_op', arguments: '{}' }, + name: 'no_op', arguments: '{}', }; const provider = createProvider(createStream([tc])); diff --git a/packages/kosong/test/e2e/streaming-fidelity.test.ts b/packages/kosong/test/e2e/streaming-fidelity.test.ts index 99ee0b3..ba7cc26 100644 --- a/packages/kosong/test/e2e/streaming-fidelity.test.ts +++ b/packages/kosong/test/e2e/streaming-fidelity.test.ts @@ -124,7 +124,7 @@ describe('e2e: streaming fidelity', () => { { type: 'function', id: 'tc-1', - function: { name: 'search', arguments: null }, + name: 'search', arguments: null, } satisfies ToolCall, ]; @@ -142,10 +142,10 @@ describe('e2e: streaming fidelity', () => { const result = await generate(provider, '', [], []); expect(result.message.toolCalls).toHaveLength(1); - expect(result.message.toolCalls[0]!.function.arguments).toBe(fullArgs); + expect(result.message.toolCalls[0]!.arguments).toBe(fullArgs); // Verify JSON is parseable and complete - const parsed = JSON.parse(result.message.toolCalls[0]!.function.arguments!) as Record< + const parsed = JSON.parse(result.message.toolCalls[0]!.arguments!) as Record< string, unknown >; @@ -162,7 +162,7 @@ describe('e2e: streaming fidelity', () => { { type: 'function', id: 'tc-a', - function: { name: 'read_file', arguments: null }, + name: 'read_file', arguments: null, } satisfies ToolCall, { type: 'tool_call_part', argumentsPart: args1.slice(0, 5) }, { type: 'tool_call_part', argumentsPart: args1.slice(5) }, @@ -170,7 +170,7 @@ describe('e2e: streaming fidelity', () => { { type: 'function', id: 'tc-b', - function: { name: 'read_file', arguments: null }, + name: 'read_file', arguments: null, } satisfies ToolCall, { type: 'tool_call_part', argumentsPart: args2.slice(0, 5) }, { type: 'tool_call_part', argumentsPart: args2.slice(5) }, @@ -182,8 +182,8 @@ describe('e2e: streaming fidelity', () => { const result = await generate(provider, '', [], []); expect(result.message.toolCalls).toHaveLength(2); - expect(result.message.toolCalls[0]!.function.arguments).toBe(args1); - expect(result.message.toolCalls[1]!.function.arguments).toBe(args2); + expect(result.message.toolCalls[0]!.arguments).toBe(args1); + expect(result.message.toolCalls[1]!.arguments).toBe(args2); }); it('ToolCallPart with null argumentsPart does not corrupt arguments', async () => { @@ -191,7 +191,7 @@ describe('e2e: streaming fidelity', () => { { type: 'function', id: 'tc-null', - function: { name: 'tool_x', arguments: null }, + name: 'tool_x', arguments: null, } satisfies ToolCall, { type: 'tool_call_part', argumentsPart: null }, { type: 'tool_call_part', argumentsPart: '{"key":' }, @@ -204,7 +204,7 @@ describe('e2e: streaming fidelity', () => { const result = await generate(provider, '', [], []); expect(result.message.toolCalls).toHaveLength(1); - expect(result.message.toolCalls[0]!.function.arguments).toBe('{"key":"val"}'); + expect(result.message.toolCalls[0]!.arguments).toBe('{"key":"val"}'); }); }); @@ -264,7 +264,7 @@ describe('e2e: streaming fidelity', () => { { type: 'function', id: 'tc-1', - function: { name: 'tool', arguments: null }, + name: 'tool', arguments: null, } satisfies ToolCall, { type: 'tool_call_part', argumentsPart: '{"a":' }, { type: 'tool_call_part', argumentsPart: '1}' }, @@ -295,7 +295,7 @@ describe('e2e: streaming fidelity', () => { { type: 'function', id: 'tc-1', - function: { name: 'search', arguments: null }, + name: 'search', arguments: null, } satisfies ToolCall, { type: 'tool_call_part', argumentsPart: '{"query' }, { type: 'tool_call_part', argumentsPart: '":"test' }, @@ -318,7 +318,7 @@ describe('e2e: streaming fidelity', () => { // Track when onMessagePart sees the ToolCall if (part.type === 'function') { const tc = part; - toolCallSnapshots.push(`onMessagePart:args=${tc.function.arguments}`); + toolCallSnapshots.push(`onMessagePart:args=${tc.arguments}`); } }, }); @@ -333,12 +333,12 @@ describe('e2e: streaming fidelity', () => { { type: 'function', id: 'tc-a', - function: { name: 'tool_a', arguments: '{"x":1}' }, + name: 'tool_a', arguments: '{"x":1}', } satisfies ToolCall, { type: 'function', id: 'tc-b', - function: { name: 'tool_b', arguments: '{"y":2}' }, + name: 'tool_b', arguments: '{"y":2}', } satisfies ToolCall, { type: 'text', text: 'done' }, ]; @@ -388,7 +388,7 @@ describe('e2e: streaming fidelity', () => { parts.push({ type: 'function', id: `tc-${i}`, - function: { name: 'task', arguments: null }, + name: 'task', arguments: null, } satisfies ToolCall); // Stream args in 3 chunks const chunk = Math.ceil(args.length / 3); @@ -409,7 +409,7 @@ describe('e2e: streaming fidelity', () => { for (let i = 0; i < 5; i++) { expect(result.message.toolCalls[i]!.id).toBe(`tc-${i}`); - const parsed = JSON.parse(result.message.toolCalls[i]!.function.arguments!) as { + const parsed = JSON.parse(result.message.toolCalls[i]!.arguments!) as { index: number; }; expect(parsed.index).toBe(i); diff --git a/packages/kosong/test/e2e/streaming-providers.test.ts b/packages/kosong/test/e2e/streaming-providers.test.ts index 8e12560..7527df0 100644 --- a/packages/kosong/test/e2e/streaming-providers.test.ts +++ b/packages/kosong/test/e2e/streaming-providers.test.ts @@ -77,7 +77,7 @@ describe('integration: streaming provider contracts', () => { { type: 'function', id: 'tc-kimi-1', - function: { name: 'search', arguments: null }, + name: 'search', arguments: null, } satisfies ToolCall, { type: 'tool_call_part', argumentsPart: '{"query":' }, { type: 'tool_call_part', argumentsPart: '"vitest"}' }, @@ -109,7 +109,7 @@ describe('integration: streaming provider contracts', () => { // ToolCall assembled correctly expect(result.message.toolCalls).toHaveLength(1); - expect(result.message.toolCalls[0]!.function.arguments).toBe('{"query":"vitest"}'); + expect(result.message.toolCalls[0]!.arguments).toBe('{"query":"vitest"}'); // Usage extracted from the stream metadata expect(result.usage).toEqual(usage); @@ -157,7 +157,7 @@ describe('integration: streaming provider contracts', () => { { type: 'function', id: 'toolu_01', - function: { name: 'read_file', arguments: '' }, + name: 'read_file', arguments: '', } satisfies ToolCall, // content_block_delta(input_json_delta) { type: 'tool_call_part', argumentsPart: '{"path":"/src/main.ts"}' }, @@ -189,8 +189,8 @@ describe('integration: streaming provider contracts', () => { // ToolCall assembled expect(result.message.toolCalls).toHaveLength(1); expect(result.message.toolCalls[0]!.id).toBe('toolu_01'); - expect(result.message.toolCalls[0]!.function.name).toBe('read_file'); - expect(result.message.toolCalls[0]!.function.arguments).toBe('{"path":"/src/main.ts"}'); + expect(result.message.toolCalls[0]!.name).toBe('read_file'); + expect(result.message.toolCalls[0]!.arguments).toBe('{"path":"/src/main.ts"}'); }); it('redacted_thinking yields ThinkPart with encrypted and empty think', async () => { @@ -256,7 +256,7 @@ describe('integration: streaming provider contracts', () => { { type: 'function', id: 'search_12345', - function: { name: 'search', arguments: '{"query":"vitest docs"}' }, + name: 'search', arguments: '{"query":"vitest docs"}', } satisfies ToolCall, ]; @@ -285,8 +285,8 @@ describe('integration: streaming provider contracts', () => { const tc = result.message.toolCalls[0]!; expect(tc.id).toBe('search_12345'); expect(tc.id).toMatch(/^search_\d+$/); - expect(tc.function.name).toBe('search'); - expect(tc.function.arguments).toBe('{"query":"vitest docs"}'); + expect(tc.name).toBe('search'); + expect(tc.arguments).toBe('{"query":"vitest docs"}'); }); it('Google function_call ID format: {name}_{id}', async () => { @@ -294,7 +294,7 @@ describe('integration: streaming provider contracts', () => { { type: 'function', id: 'read_file_9876', - function: { name: 'read_file', arguments: '{"path":"/tmp/test.ts"}' }, + name: 'read_file', arguments: '{"path":"/tmp/test.ts"}', } satisfies ToolCall, { type: 'text', text: 'Reading the file.' }, ]; @@ -306,7 +306,7 @@ describe('integration: streaming provider contracts', () => { expect(result.message.toolCalls).toHaveLength(1); const tc = result.message.toolCalls[0]!; // Verify the ID follows the {name}_{id} pattern - expect(tc.id.startsWith(`${tc.function.name}_`)).toBe(true); + expect(tc.id.startsWith(`${tc.name}_`)).toBe(true); }); it('Google multiple function calls in single response', async () => { @@ -315,17 +315,17 @@ describe('integration: streaming provider contracts', () => { { type: 'function', id: 'read_file_001', - function: { name: 'read_file', arguments: '{"path":"a.ts"}' }, + name: 'read_file', arguments: '{"path":"a.ts"}', } satisfies ToolCall, { type: 'function', id: 'read_file_002', - function: { name: 'read_file', arguments: '{"path":"b.ts"}' }, + name: 'read_file', arguments: '{"path":"b.ts"}', } satisfies ToolCall, { type: 'function', id: 'write_file_003', - function: { name: 'write_file', arguments: '{"path":"c.ts","content":"x"}' }, + name: 'write_file', arguments: '{"path":"c.ts","content":"x"}', } satisfies ToolCall, ]; diff --git a/packages/kosong/test/e2e/toolchain-bridges.test.ts b/packages/kosong/test/e2e/toolchain-bridges.test.ts index 35ac4dd..4c3d9ef 100644 --- a/packages/kosong/test/e2e/toolchain-bridges.test.ts +++ b/packages/kosong/test/e2e/toolchain-bridges.test.ts @@ -112,8 +112,7 @@ describe('e2e: kosong toolchain bridges', () => { { type: 'function', id: 'tc-route', - function: { - name: 'route_address', + name: 'route_address', arguments: JSON.stringify({ shipment: { id: 'pkg-42', @@ -121,7 +120,6 @@ describe('e2e: kosong toolchain bridges', () => { }, urgent: true, }), - }, }, ]), createStream([{ type: 'text', text: 'Shipment routed.' }]), @@ -199,13 +197,11 @@ describe('e2e: kosong toolchain bridges', () => { { type: 'function', id: 'tc-ship', - function: { - name: 'ship_package', + name: 'ship_package', arguments: JSON.stringify({ shipping: { city: 'Hangzhou', zip: '310000' }, billing: { city: 'Shenzhen', zip: '518000' }, }), - }, }, ]), createStream([{ type: 'text', text: 'Shipment booked.' }]), diff --git a/packages/kosong/test/e2e/toolset-advanced.test.ts b/packages/kosong/test/e2e/toolset-advanced.test.ts index 1b382d0..16b4449 100644 --- a/packages/kosong/test/e2e/toolset-advanced.test.ts +++ b/packages/kosong/test/e2e/toolset-advanced.test.ts @@ -66,7 +66,7 @@ describe('e2e: toolset advanced', () => { const tc: ToolCall = { type: 'function', id: 'tc-null', - function: { name: 'my_tool', arguments: null }, + name: 'my_tool', arguments: null, }; const provider = new QueuedProvider([ @@ -98,7 +98,7 @@ describe('e2e: toolset advanced', () => { const tc: ToolCall = { type: 'function', id: 'tc-empty', - function: { name: 'my_tool', arguments: '' }, + name: 'my_tool', arguments: '', }; const provider = new QueuedProvider([ @@ -125,7 +125,7 @@ describe('e2e: toolset advanced', () => { const tc: ToolCall = { type: 'function', id: 'tc-string-null', - function: { name: 'my_tool', arguments: 'null' }, + name: 'my_tool', arguments: 'null', }; const provider = new QueuedProvider([ @@ -156,7 +156,7 @@ describe('e2e: toolset advanced', () => { const tc: ToolCall = { type: 'function', id: 'tc-array', - function: { name: 'my_tool', arguments: '[]' }, + name: 'my_tool', arguments: '[]', }; const provider = new QueuedProvider([ @@ -188,7 +188,7 @@ describe('e2e: toolset advanced', () => { const tc: ToolCall = { type: 'function', id: 'tc-unicode', - function: { name: 'my_tool', arguments: args }, + name: 'my_tool', arguments: args, }; const provider = new QueuedProvider([ @@ -221,17 +221,17 @@ describe('e2e: toolset advanced', () => { const tc1: ToolCall = { type: 'function', id: 'tc-slow', - function: { name: 'delayed', arguments: '{"delay":100,"label":"slow"}' }, + name: 'delayed', arguments: '{"delay":100,"label":"slow"}', }; const tc2: ToolCall = { type: 'function', id: 'tc-fast', - function: { name: 'delayed', arguments: '{"delay":10,"label":"fast"}' }, + name: 'delayed', arguments: '{"delay":10,"label":"fast"}', }; const tc3: ToolCall = { type: 'function', id: 'tc-mid', - function: { name: 'delayed', arguments: '{"delay":50,"label":"mid"}' }, + name: 'delayed', arguments: '{"delay":50,"label":"mid"}', }; const provider = new QueuedProvider([ @@ -270,17 +270,17 @@ describe('e2e: toolset advanced', () => { const tc1: ToolCall = { type: 'function', id: 'tc-slow', - function: { name: 'delayed', arguments: '{"delay":80,"label":"slow"}' }, + name: 'delayed', arguments: '{"delay":80,"label":"slow"}', }; const tc2: ToolCall = { type: 'function', id: 'tc-fast', - function: { name: 'delayed', arguments: '{"delay":10,"label":"fast"}' }, + name: 'delayed', arguments: '{"delay":10,"label":"fast"}', }; const tc3: ToolCall = { type: 'function', id: 'tc-mid', - function: { name: 'delayed', arguments: '{"delay":40,"label":"mid"}' }, + name: 'delayed', arguments: '{"delay":40,"label":"mid"}', }; const provider = new QueuedProvider([ @@ -339,7 +339,7 @@ describe('e2e: toolset advanced', () => { yield { type: 'function', id: 'tc-doomed', - function: { name: 'slow_tool', arguments: '{}' }, + name: 'slow_tool', arguments: '{}', } satisfies ToolCall; yield { type: 'text', text: 'processing...' }; throw new Error('stream connection lost'); @@ -389,7 +389,7 @@ describe('e2e: toolset advanced', () => { const tc: ToolCall = { type: 'function', id: 'tc-error', - function: { name: 'bad_tool', arguments: '{}' }, + name: 'bad_tool', arguments: '{}', }; const provider = new QueuedProvider([ diff --git a/packages/kosong/test/echo-provider.test.ts b/packages/kosong/test/echo-provider.test.ts index 9a030f3..c95f578 100644 --- a/packages/kosong/test/echo-provider.test.ts +++ b/packages/kosong/test/echo-provider.test.ts @@ -69,7 +69,7 @@ describe('EchoChatProvider', () => { { type: 'function', id: 'call-1', - function: { name: 'search', arguments: '{"q":"python"' }, + name: 'search', arguments: '{"q":"python"', } satisfies ToolCall, { type: 'tool_call_part', argumentsPart: '}' } satisfies ToolCallPart, ]); @@ -347,7 +347,7 @@ describe('EchoChatProvider', () => { const tc = result.message.toolCalls[0]!; expect(tc.id).toBe('call-1'); - expect(tc.function.name).toBe('search'); - expect(tc.function.arguments).toBe('{"q":"python"}'); + expect(tc.name).toBe('search'); + expect(tc.arguments).toBe('{"q":"python"}'); }); }); diff --git a/packages/kosong/test/fixtures/echo-provider.ts b/packages/kosong/test/fixtures/echo-provider.ts index cb49f76..f62a689 100644 --- a/packages/kosong/test/fixtures/echo-provider.ts +++ b/packages/kosong/test/fixtures/echo-provider.ts @@ -223,7 +223,7 @@ function parseToolCall(payload: string, lineno: number, rawLine: string): ToolCa return { type: 'function', id: toolCallId, - function: { name, arguments: args ?? null }, + name, arguments: args ?? null, }; } diff --git a/packages/kosong/test/fixtures/simple-toolset.ts b/packages/kosong/test/fixtures/simple-toolset.ts index 9fad7bc..69cc7e8 100644 --- a/packages/kosong/test/fixtures/simple-toolset.ts +++ b/packages/kosong/test/fixtures/simple-toolset.ts @@ -149,17 +149,17 @@ export class SimpleToolset implements Toolset { } handle(toolCall: ToolCall, _options?: { signal?: AbortSignal }): Promise { - const entry = this.toolMap.get(toolCall.function.name); + const entry = this.toolMap.get(toolCall.name); if (entry === undefined) { return Promise.resolve({ toolCallId: toolCall.id, - returnValue: toolNotFoundError(toolCall.function.name), + returnValue: toolNotFoundError(toolCall.name), }); } let args: JsonValue; try { - args = JSON.parse(toolCall.function.arguments ?? '{}') as JsonValue; + args = JSON.parse(toolCall.arguments ?? '{}') as JsonValue; } catch (error: unknown) { const msg = error instanceof Error ? error.message : String(error); return Promise.resolve({ diff --git a/packages/kosong/test/fixtures/step.ts b/packages/kosong/test/fixtures/step.ts index 8d4b988..1994b82 100644 --- a/packages/kosong/test/fixtures/step.ts +++ b/packages/kosong/test/fixtures/step.ts @@ -17,7 +17,7 @@ export interface StepCallbacks { * * A step comprises one LLM generation followed by dispatch of any tool * calls the model requested. Tools are dispatched once the stream has - * fully drained — this guarantees `toolCall.function.arguments` is + * fully drained — this guarantees `toolCall.arguments` is * complete JSON even when a provider interleaves parallel tool call * argument deltas. Call {@link toolResults} to await all pending tool * executions in order. diff --git a/packages/kosong/test/generate.test.ts b/packages/kosong/test/generate.test.ts index f945b04..627a475 100644 --- a/packages/kosong/test/generate.test.ts +++ b/packages/kosong/test/generate.test.ts @@ -53,7 +53,7 @@ describe('generate()', () => { { type: 'function', id: 'get_weather#123', - function: { name: 'get_weather', arguments: null }, + name: 'get_weather', arguments: null, }, { type: 'tool_call_part', argumentsPart: '{' }, { type: 'tool_call_part', argumentsPart: '"city":' }, @@ -74,7 +74,7 @@ describe('generate()', () => { { type: 'function', id: 'get_weather#123', - function: { name: 'get_weather', arguments: '{"city":"Beijing"}' }, + name: 'get_weather', arguments: '{"city":"Beijing"}', }, ]); }); @@ -87,7 +87,7 @@ describe('generate()', () => { { type: 'function', id: 'get_weather#123', - function: { name: 'get_weather', arguments: null }, + name: 'get_weather', arguments: null, }, { type: 'tool_call_part', argumentsPart: '{' }, { type: 'tool_call_part', argumentsPart: '"city":' }, @@ -96,7 +96,7 @@ describe('generate()', () => { { type: 'function', id: 'get_time#123', - function: { name: 'get_time', arguments: '' }, + name: 'get_time', arguments: '', }, ]; const stream = createMockStream(inputParts); @@ -153,7 +153,7 @@ describe('generate()', () => { { type: 'function', id: 'tool-1', - function: { name: 'search', arguments: '{}' }, + name: 'search', arguments: '{}', extras: { metadata: { provider: 'kimi' }, tags: ['a', 'b'], @@ -176,7 +176,7 @@ describe('generate()', () => { { type: 'function', id: 'tool-1', - function: { name: 'search', arguments: '{}' }, + name: 'search', arguments: '{}', extras: { metadata: { provider: 'kimi' }, tags: ['a', 'b'], @@ -230,7 +230,7 @@ describe('generate()', () => { { type: 'function', id: 'tool#1', - function: { name: 'read_file', arguments: '{"path": "/tmp"}' }, + name: 'read_file', arguments: '{"path": "/tmp"}', }, ]); const provider = createMockProvider(stream); @@ -283,13 +283,13 @@ describe('generate()', () => { { type: 'function', id: 'tc-1', - function: { name: 'read_file', arguments: null }, + name: 'read_file', arguments: null, }, { type: 'tool_call_part', argumentsPart: '{"path":"/a"}' }, { type: 'function', id: 'tc-2', - function: { name: 'read_file', arguments: null }, + name: 'read_file', arguments: null, }, { type: 'tool_call_part', argumentsPart: '{"path":"/b"}' }, ]); @@ -301,12 +301,12 @@ describe('generate()', () => { { type: 'function', id: 'tc-1', - function: { name: 'read_file', arguments: '{"path":"/a"}' }, + name: 'read_file', arguments: '{"path":"/a"}', }, { type: 'function', id: 'tc-2', - function: { name: 'read_file', arguments: '{"path":"/b"}' }, + name: 'read_file', arguments: '{"path":"/b"}', }, ]); }); @@ -316,7 +316,7 @@ describe('generate()', () => { { type: 'function', id: 'tc-1', - function: { name: 'search', arguments: null }, + name: 'search', arguments: null, }, { type: 'tool_call_part', argumentsPart: '{"q' }, { type: 'tool_call_part', argumentsPart: '":"hello"}' }, @@ -329,7 +329,7 @@ describe('generate()', () => { { type: 'function', id: 'tc-1', - function: { name: 'search', arguments: '{"q":"hello"}' }, + name: 'search', arguments: '{"q":"hello"}', }, ]); }); @@ -346,13 +346,13 @@ describe('generate()', () => { { type: 'function', id: 'tc-a', - function: { name: 'read_file', arguments: null }, + name: 'read_file', arguments: null, _streamIndex: 0, }, { type: 'function', id: 'tc-b', - function: { name: 'read_file', arguments: null }, + name: 'read_file', arguments: null, _streamIndex: 1, }, // Interleaved argument deltas across the two tool calls. @@ -371,12 +371,12 @@ describe('generate()', () => { { type: 'function', id: 'tc-a', - function: { name: 'read_file', arguments: '{"path":"/a"}' }, + name: 'read_file', arguments: '{"path":"/a"}', }, { type: 'function', id: 'tc-b', - function: { name: 'read_file', arguments: '{"path":"/b"}' }, + name: 'read_file', arguments: '{"path":"/b"}', }, ]); // _streamIndex must NOT leak into the stored ToolCall. @@ -391,13 +391,13 @@ describe('generate()', () => { { type: 'function', id: 'call_a', - function: { name: 'read_file', arguments: null }, + name: 'read_file', arguments: null, _streamIndex: 'item_abc', }, { type: 'function', id: 'call_b', - function: { name: 'read_file', arguments: null }, + name: 'read_file', arguments: null, _streamIndex: 'item_xyz', }, { type: 'tool_call_part', argumentsPart: '{"p":"/x"}', index: 'item_xyz' }, @@ -411,12 +411,12 @@ describe('generate()', () => { { type: 'function', id: 'call_a', - function: { name: 'read_file', arguments: '{"p":"/a"}' }, + name: 'read_file', arguments: '{"p":"/a"}', }, { type: 'function', id: 'call_b', - function: { name: 'read_file', arguments: '{"p":"/x"}' }, + name: 'read_file', arguments: '{"p":"/x"}', }, ]); }); @@ -428,14 +428,14 @@ describe('generate()', () => { { type: 'function', id: 'tc-1', - function: { name: 'search', arguments: null }, + name: 'search', arguments: null, }, { type: 'tool_call_part', argumentsPart: '{"q":' }, { type: 'tool_call_part', argumentsPart: '"hi"}' }, { type: 'function', id: 'tc-2', - function: { name: 'search', arguments: null }, + name: 'search', arguments: null, }, { type: 'tool_call_part', argumentsPart: '{"q":"bye"}' }, ]); @@ -447,12 +447,12 @@ describe('generate()', () => { { type: 'function', id: 'tc-1', - function: { name: 'search', arguments: '{"q":"hi"}' }, + name: 'search', arguments: '{"q":"hi"}', }, { type: 'function', id: 'tc-2', - function: { name: 'search', arguments: '{"q":"bye"}' }, + name: 'search', arguments: '{"q":"bye"}', }, ]); }); @@ -465,7 +465,7 @@ describe('generate()', () => { { type: 'function', id: 'tc-0', - function: { name: 'write', arguments: null }, + name: 'write', arguments: null, _streamIndex: 0, }, { type: 'tool_call_part', argumentsPart: '{"a":', index: 0 }, @@ -473,7 +473,7 @@ describe('generate()', () => { { type: 'function', id: 'tc-1', - function: { name: 'write', arguments: null }, + name: 'write', arguments: null, _streamIndex: 1, }, { type: 'tool_call_part', argumentsPart: '{"b":2}', index: 1 }, @@ -488,12 +488,12 @@ describe('generate()', () => { { type: 'function', id: 'tc-0', - function: { name: 'write', arguments: '{"a":1}' }, + name: 'write', arguments: '{"a":1}', }, { type: 'function', id: 'tc-1', - function: { name: 'write', arguments: '{"b":2}' }, + name: 'write', arguments: '{"b":2}', }, ]); }); @@ -503,7 +503,7 @@ describe('generate()', () => { { type: 'function', id: 'tc-1', - function: { name: 'f', arguments: null }, + name: 'f', arguments: null, _streamIndex: 0, }, { type: 'tool_call_part', argumentsPart: '{}', index: 0 }, @@ -521,7 +521,7 @@ describe('generate()', () => { expect(received[0]).toEqual({ type: 'function', id: 'tc-1', - function: { name: 'f', arguments: '{}' }, + name: 'f', arguments: '{}', }); expect(received[0]).not.toHaveProperty('_streamIndex'); }); @@ -533,7 +533,7 @@ describe('generate()', () => { // Previously the generate loop fired onToolCall the moment a second // ToolCall header forced a flush of the first. At that point tc0's // arguments had not yet been received, so any consumer that parsed - // `tc.function.arguments` inside the callback (e.g. step() dispatching + // `tc.arguments` inside the callback (e.g. step() dispatching // the tool) would hit a JSON parse error on an empty/partial string. // // `onToolCall` must stay deferred until the stream has drained. @@ -544,13 +544,13 @@ describe('generate()', () => { { type: 'function', id: 'call_a', - function: { name: 'tool_a', arguments: null }, + name: 'tool_a', arguments: null, _streamIndex: 0, }, { type: 'function', id: 'call_b', - function: { name: 'tool_b', arguments: null }, + name: 'tool_b', arguments: null, _streamIndex: 1, }, { type: 'tool_call_part', argumentsPart: '{"x":', index: 0 }, @@ -566,7 +566,7 @@ describe('generate()', () => { const result = await generate(provider, '', [], [], { async onToolCall(tc: ToolCall): Promise { - callbackSnapshots.push({ id: tc.id, args: tc.function.arguments }); + callbackSnapshots.push({ id: tc.id, args: tc.arguments }); }, }); @@ -582,12 +582,12 @@ describe('generate()', () => { { type: 'function', id: 'call_a', - function: { name: 'tool_a', arguments: '{"x":1}' }, + name: 'tool_a', arguments: '{"x":1}', }, { type: 'function', id: 'call_b', - function: { name: 'tool_b', arguments: '{"y":2}' }, + name: 'tool_b', arguments: '{"y":2}', }, ]); }); @@ -601,19 +601,19 @@ describe('generate()', () => { { type: 'function', id: 'call_a', - function: { name: 'tool_a', arguments: null }, + name: 'tool_a', arguments: null, _streamIndex: 0, }, { type: 'function', id: 'call_b', - function: { name: 'tool_b', arguments: null }, + name: 'tool_b', arguments: null, _streamIndex: 1, }, { type: 'function', id: 'call_c', - function: { name: 'tool_c', arguments: null }, + name: 'tool_c', arguments: null, _streamIndex: 2, }, // Heavily interleaved argument deltas. @@ -636,12 +636,12 @@ describe('generate()', () => { if (msg === null) { // First callback: we don't have a message reference yet // because `generate` hasn't returned. But the invariant we - // care about is that `tc.function.arguments` is complete + // care about is that `tc.arguments` is complete // JSON — verify that directly. observations.push({ id: tc.id, totalToolCalls: -1, // unknown — message ref not yet bound - allComplete: tc.function.arguments !== null && tc.function.arguments.endsWith('}'), + allComplete: tc.arguments !== null && tc.arguments.endsWith('}'), }); return; } @@ -649,7 +649,7 @@ describe('generate()', () => { id: tc.id, totalToolCalls: msg.toolCalls.length, allComplete: msg.toolCalls.every( - (c) => c.function.arguments !== null && c.function.arguments.endsWith('}'), + (c) => c.arguments !== null && c.arguments.endsWith('}'), ), }); }, @@ -664,7 +664,7 @@ describe('generate()', () => { expect(observations.map((o) => o.id)).toEqual(['call_a', 'call_b', 'call_c']); // The final assembled arguments match the index-based routing. - expect(result.message.toolCalls.map((tc) => tc.function.arguments)).toEqual([ + expect(result.message.toolCalls.map((tc) => tc.arguments)).toEqual([ '{"k":1}', '{"k":2}', '{"k":3}', @@ -754,7 +754,7 @@ describe('generate()', () => { { type: 'function', id: 'call-1', - function: { name: 'plus', arguments: null }, + name: 'plus', arguments: null, }, { type: 'tool_call_part', argumentsPart: '{"a":1}' }, { type: 'text', text: 'done' }, @@ -828,13 +828,13 @@ describe('generate()', () => { { type: 'function', id: 'first', - function: { name: 'f', arguments: null }, + name: 'f', arguments: null, }, { type: 'tool_call_part', argumentsPart: '{"i":1}' }, { type: 'function', id: 'second', - function: { name: 'g', arguments: null }, + name: 'g', arguments: null, }, { type: 'tool_call_part', argumentsPart: '{"i":2}' }, ]); diff --git a/packages/kosong/test/google-genai.test.ts b/packages/kosong/test/google-genai.test.ts index e1ad47c..41a24a0 100644 --- a/packages/kosong/test/google-genai.test.ts +++ b/packages/kosong/test/google-genai.test.ts @@ -279,7 +279,7 @@ describe('GoogleGenAIChatProvider', () => { const toolCall: ToolCall = { type: 'function', id: 'call_abc123', - function: { name: 'add', arguments: '{"a": 2, "b": 3}' }, + name: 'add', arguments: '{"a": 2, "b": 3}', }; const history: Message[] = [ { role: 'user', content: [{ type: 'text', text: 'Add 2 and 3' }], toolCalls: [] }, @@ -337,7 +337,7 @@ describe('GoogleGenAIChatProvider', () => { { type: 'function', id: 'add_call_sig', - function: { name: 'add', arguments: '{"a": 2, "b": 3}' }, + name: 'add', arguments: '{"a": 2, "b": 3}', extras: { thought_signature_b64: 'dGhvdWdodF9zaWduYXR1cmVfZGF0YQ==' }, }, ], @@ -365,7 +365,7 @@ describe('GoogleGenAIChatProvider', () => { { type: 'function', id: 'tc_001', - function: { name: 'fetch_image', arguments: '{}' }, + name: 'fetch_image', arguments: '{}', }, ], }, @@ -418,7 +418,7 @@ describe('GoogleGenAIChatProvider', () => { { type: 'function', id: 'tc_002', - function: { name: 'fetch_media', arguments: '{}' }, + name: 'fetch_media', arguments: '{}', }, ], }, @@ -498,12 +498,12 @@ describe('GoogleGenAIChatProvider', () => { { type: 'function', id: 'call_add', - function: { name: 'add', arguments: '{"a": 2, "b": 3}' }, + name: 'add', arguments: '{"a": 2, "b": 3}', }, { type: 'function', id: 'call_mul', - function: { name: 'multiply', arguments: '{"a": 4, "b": 5}' }, + name: 'multiply', arguments: '{"a": 4, "b": 5}', }, ], }, @@ -685,7 +685,7 @@ describe('GoogleGenAIChatProvider', () => { { type: 'function', id: 'call_xyz', - function: { name: 'add', arguments: '{"a": 2, "b": 3}' }, + name: 'add', arguments: '{"a": 2, "b": 3}', }, ], }, @@ -973,7 +973,7 @@ describe('GoogleGenAIChatProvider', () => { { type: 'function', id: 'add_call_1', - function: { name: 'add', arguments: '{"a":2,"b":3}' }, + name: 'add', arguments: '{"a":2,"b":3}', }, ]); }); @@ -1003,7 +1003,7 @@ describe('GoogleGenAIChatProvider', () => { { type: 'function', id: 'search_fc_1', - function: { name: 'search', arguments: '{"q":"test"}' }, + name: 'search', arguments: '{"q":"test"}', extras: { thought_signature_b64: 'sig_abc123' }, }, ]); @@ -1074,7 +1074,7 @@ describe('GoogleGenAIChatProvider', () => { expect(parts[1]).toEqual({ type: 'text', text: 'answer' }); expect(parts[2]).toMatchObject({ type: 'function', - function: { name: 'calc' }, + name: 'calc', }); }); @@ -1192,7 +1192,7 @@ describe('GoogleGenAIChatProvider', () => { { type: 'function', id: 'call_known', - function: { name: 'add', arguments: '{"a":1,"b":2}' }, + name: 'add', arguments: '{"a":1,"b":2}', }, ], }, @@ -1226,12 +1226,12 @@ describe('GoogleGenAIChatProvider', () => { { type: 'function', id: 'call_a', - function: { name: 'tool_a', arguments: '{}' }, + name: 'tool_a', arguments: '{}', }, { type: 'function', id: 'call_b', - function: { name: 'tool_b', arguments: '{}' }, + name: 'tool_b', arguments: '{}', }, ], }, @@ -1261,7 +1261,7 @@ describe('GoogleGenAIChatProvider', () => { { type: 'function', id: 'call_a', - function: { name: 'tool_a', arguments: '{}' }, + name: 'tool_a', arguments: '{}', }, ], }, @@ -1483,7 +1483,7 @@ describe('messagesToGoogleGenAIContents - error branches', () => { { type: 'function', id: 'tc_arr', - function: { name: 'foo', arguments: '[1,2,3]' }, + name: 'foo', arguments: '[1,2,3]', }, ], }, @@ -1502,7 +1502,7 @@ describe('messagesToGoogleGenAIContents - error branches', () => { { type: 'function', id: 'tc_1', - function: { name: 'foo', arguments: '{}' }, + name: 'foo', arguments: '{}', }, ], }, @@ -1527,7 +1527,7 @@ describe('messagesToGoogleGenAIContents - error branches', () => { { type: 'function', id: 'tc_dup', - function: { name: 'foo', arguments: '{}' }, + name: 'foo', arguments: '{}', }, ], }, @@ -1556,12 +1556,12 @@ describe('messagesToGoogleGenAIContents - error branches', () => { { type: 'function', id: 'tc_expected', - function: { name: 'foo', arguments: '{}' }, + name: 'foo', arguments: '{}', }, { type: 'function', id: 'tc_missing', - function: { name: 'bar', arguments: '{}' }, + name: 'bar', arguments: '{}', }, ], }, @@ -1584,7 +1584,7 @@ describe('messagesToGoogleGenAIContents - error branches', () => { { type: 'function', id: 'tc_known', - function: { name: 'foo', arguments: '{}' }, + name: 'foo', arguments: '{}', }, ], }, @@ -1615,7 +1615,7 @@ describe('messagesToGoogleGenAIContents - extra branches', () => { { type: 'function', id: 'tc_bad', - function: { name: 'foo', arguments: 'not valid {json' }, + name: 'foo', arguments: 'not valid {json', }, ], }, diff --git a/packages/kosong/test/kimi.test.ts b/packages/kosong/test/kimi.test.ts index 9c5ea95..f72aff7 100644 --- a/packages/kosong/test/kimi.test.ts +++ b/packages/kosong/test/kimi.test.ts @@ -322,7 +322,8 @@ describe('KimiChatProvider', () => { const toolCall: ToolCall = { type: 'function', id: 'call_abc123', - function: { name: 'add', arguments: '{"a": 2, "b": 3}' }, + name: 'add', + arguments: '{"a": 2, "b": 3}', }; const history: Message[] = [ { role: 'user', content: [{ type: 'text', text: 'Add 2 and 3' }], toolCalls: [] }, @@ -362,7 +363,8 @@ describe('KimiChatProvider', () => { const toolCall: ToolCall = { type: 'function', id: 'call_abc123', - function: { name: 'add', arguments: '{"a": 2, "b": 3}' }, + name: 'add', + arguments: '{"a": 2, "b": 3}', }; const history: Message[] = [ { role: 'user', content: [{ type: 'text', text: 'Add 2 and 3' }], toolCalls: [] }, @@ -404,12 +406,14 @@ describe('KimiChatProvider', () => { { type: 'function', id: 'call_add', - function: { name: 'add', arguments: '{"a": 2, "b": 3}' }, + name: 'add', + arguments: '{"a": 2, "b": 3}', }, { type: 'function', id: 'call_mul', - function: { name: 'multiply', arguments: '{"a": 4, "b": 5}' }, + name: 'multiply', + arguments: '{"a": 4, "b": 5}', }, ], }, @@ -935,7 +939,8 @@ describe('KimiChatProvider', () => { { type: 'function', id: 'call_delayed', - function: { name: 'foo', arguments: '{"a":1}' }, + name: 'foo', + arguments: '{"a":1}', }, ]); }); @@ -968,7 +973,7 @@ describe('KimiChatProvider', () => { } }, onToolCall(toolCall): void { - events.push(`ready:${toolCall.id}:${toolCall.function.arguments ?? ''}`); + events.push(`ready:${toolCall.id}:${toolCall.arguments ?? ''}`); }, }, ); @@ -1023,7 +1028,8 @@ describe('KimiChatProvider', () => { { type: 'function', id: 'call_noidx', - function: { name: 'foo', arguments: '{"a":1}' }, + name: 'foo', + arguments: '{"a":1}', }, ]); }); @@ -1131,7 +1137,8 @@ describe('KimiChatProvider', () => { expect(toolCall).toMatchObject({ type: 'function', id: 'call_ns_a', - function: { name: 'lookup', arguments: '{"q":"hi"}' }, + name: 'lookup', + arguments: '{"q":"hi"}', }); }); @@ -1264,7 +1271,8 @@ describe('KimiChatProvider', () => { const toolCall: ToolCall = { type: 'function', id: 'call_xyz', - function: { name: 'add', arguments: '{}' }, + name: 'add', + arguments: '{}', }; it('omits content when assistant tool call content is empty', async () => { diff --git a/packages/kosong/test/message.test.ts b/packages/kosong/test/message.test.ts index 240a62a..ac2b64f 100644 --- a/packages/kosong/test/message.test.ts +++ b/packages/kosong/test/message.test.ts @@ -47,11 +47,11 @@ describe('createAssistantMessage', () => { const tc: ToolCall = { type: 'function', id: 'call-1', - function: { name: 'search', arguments: '{"q":"ts"}' }, + name: 'search', arguments: '{"q":"ts"}', }; const msg = createAssistantMessage([{ type: 'text', text: 'ok' }], [tc]); expect(msg.toolCalls).toHaveLength(1); - expect(msg.toolCalls[0]!.function.name).toBe('search'); + expect(msg.toolCalls[0]!.name).toBe('search'); }); it('defaults toolCalls to empty array when not provided', () => { @@ -250,7 +250,7 @@ describe('type guards', () => { const part: StreamedMessagePart = { type: 'function', id: 'c1', - function: { name: 'f', arguments: null }, + name: 'f', arguments: null, }; expect(isContentPart(part)).toBe(false); }); @@ -264,7 +264,7 @@ describe('type guards', () => { const part: StreamedMessagePart = { type: 'function', id: 'c1', - function: { name: 'f', arguments: null }, + name: 'f', arguments: null, }; expect(isToolCall(part)).toBe(true); }); @@ -318,33 +318,33 @@ describe('mergeInPlace', () => { const target: ToolCall = { type: 'function', id: 'c1', - function: { name: 'f', arguments: null }, + name: 'f', arguments: null, }; const source: ToolCallPart = { type: 'tool_call_part', argumentsPart: '{"a":' }; expect(mergeInPlace(target, source)).toBe(true); - expect(target.function.arguments).toBe('{"a":'); + expect(target.arguments).toBe('{"a":'); }); it('merges ToolCall + ToolCallPart (append)', () => { const target: ToolCall = { type: 'function', id: 'c1', - function: { name: 'f', arguments: '{"a":' }, + name: 'f', arguments: '{"a":', }; const source: ToolCallPart = { type: 'tool_call_part', argumentsPart: '1}' }; expect(mergeInPlace(target, source)).toBe(true); - expect(target.function.arguments).toBe('{"a":1}'); + expect(target.arguments).toBe('{"a":1}'); }); it('merges ToolCall + ToolCallPart with null argumentsPart (no-op)', () => { const target: ToolCall = { type: 'function', id: 'c1', - function: { name: 'f', arguments: '{"x":1}' }, + name: 'f', arguments: '{"x":1}', }; const source: ToolCallPart = { type: 'tool_call_part', argumentsPart: null }; expect(mergeInPlace(target, source)).toBe(true); - expect(target.function.arguments).toBe('{"x":1}'); + expect(target.arguments).toBe('{"x":1}'); }); it('returns false for TextPart + ThinkPart', () => { @@ -369,7 +369,7 @@ describe('mergeInPlace', () => { const target: ToolCall = { type: 'function', id: 'c1', - function: { name: 'f', arguments: null }, + name: 'f', arguments: null, }; const source: TextPart = { type: 'text', text: 'x' }; expect(mergeInPlace(target, source)).toBe(false); @@ -394,12 +394,12 @@ describe('Message optional fields', () => { { type: 'function', id: 'call-1', - function: { name: 'search', arguments: '{"q":"test"}' }, + name: 'search', arguments: '{"q":"test"}', }, ], }; expect(msg.toolCalls).toHaveLength(1); - expect(msg.toolCalls[0]!.function.name).toBe('search'); + expect(msg.toolCalls[0]!.name).toBe('search'); }); it('message can have partial flag', () => { @@ -416,7 +416,7 @@ describe('Message optional fields', () => { const tc: ToolCall = { type: 'function', id: 'call-1', - function: { name: 'search', arguments: '{}' }, + name: 'search', arguments: '{}', extras: { provider_id: 'anthropic-123' }, }; expect(tc.extras).toEqual({ provider_id: 'anthropic-123' }); diff --git a/packages/kosong/test/openai-legacy.test.ts b/packages/kosong/test/openai-legacy.test.ts index 1738c6a..258da82 100644 --- a/packages/kosong/test/openai-legacy.test.ts +++ b/packages/kosong/test/openai-legacy.test.ts @@ -209,7 +209,8 @@ describe('OpenAILegacyChatProvider', () => { const toolCall: ToolCall = { type: 'function', id: 'call_abc123', - function: { name: 'add', arguments: '{"a": 2, "b": 3}' }, + name: 'add', + arguments: '{"a": 2, "b": 3}', }; const history: Message[] = [ { role: 'user', content: [{ type: 'text', text: 'Add 2 and 3' }], toolCalls: [] }, @@ -254,7 +255,8 @@ describe('OpenAILegacyChatProvider', () => { const toolCall: ToolCall = { type: 'function', id: 'call_abc123', - function: { name: 'add', arguments: '{"a": 2, "b": 3}' }, + name: 'add', + arguments: '{"a": 2, "b": 3}', }; const history: Message[] = [ { role: 'user', content: [{ type: 'text', text: 'Add 2 and 3' }], toolCalls: [] }, @@ -297,12 +299,14 @@ describe('OpenAILegacyChatProvider', () => { { type: 'function', id: 'call_add', - function: { name: 'add', arguments: '{"a": 2, "b": 3}' }, + name: 'add', + arguments: '{"a": 2, "b": 3}', }, { type: 'function', id: 'call_mul', - function: { name: 'multiply', arguments: '{"a": 4, "b": 5}' }, + name: 'multiply', + arguments: '{"a": 4, "b": 5}', }, ], }, @@ -471,7 +475,8 @@ describe('OpenAILegacyChatProvider', () => { const toolCall: ToolCall = { type: 'function', id: 'call_abc123', - function: { name: 'add', arguments: '{"a": 2, "b": 3}' }, + name: 'add', + arguments: '{"a": 2, "b": 3}', }; const history: Message[] = [ { role: 'user', content: [{ type: 'text', text: 'Add 2 and 3' }], toolCalls: [] }, @@ -505,7 +510,8 @@ describe('OpenAILegacyChatProvider', () => { const toolCall: ToolCall = { type: 'function', id: 'call_audio', - function: { name: 'fetch_audio', arguments: '{}' }, + name: 'fetch_audio', + arguments: '{}', }; const history: Message[] = [ { role: 'user', content: [{ type: 'text', text: 'Play it' }], toolCalls: [] }, @@ -534,7 +540,8 @@ describe('OpenAILegacyChatProvider', () => { const toolCall: ToolCall = { type: 'function', id: 'call_video', - function: { name: 'fetch_video', arguments: '{}' }, + name: 'fetch_video', + arguments: '{}', }; const history: Message[] = [ { role: 'user', content: [{ type: 'text', text: 'Show it' }], toolCalls: [] }, @@ -563,7 +570,8 @@ describe('OpenAILegacyChatProvider', () => { const toolCall: ToolCall = { type: 'function', id: 'call_text', - function: { name: 'add', arguments: '{"a":1,"b":2}' }, + name: 'add', + arguments: '{"a":1,"b":2}', }; const history: Message[] = [ { role: 'user', content: [{ type: 'text', text: 'Add 1 2' }], toolCalls: [] }, @@ -590,7 +598,8 @@ describe('OpenAILegacyChatProvider', () => { const toolCall: ToolCall = { type: 'function', id: 'call_abc123', - function: { name: 'add', arguments: '{"a": 2, "b": 3}' }, + name: 'add', + arguments: '{"a": 2, "b": 3}', }; const history: Message[] = [ { role: 'user', content: [{ type: 'text', text: 'Add 2 and 3' }], toolCalls: [] }, @@ -772,12 +781,14 @@ describe('OpenAILegacyChatProvider', () => { { type: 'function', id: 'call_a', - function: { name: 'read_file', arguments: '{"path":"/a.txt"}' }, + name: 'read_file', + arguments: '{"path":"/a.txt"}', }, { type: 'function', id: 'call_b', - function: { name: 'write_file', arguments: '{"path":"/b.txt","content":"hi"}' }, + name: 'write_file', + arguments: '{"path":"/b.txt","content":"hi"}', }, ]); @@ -824,7 +835,7 @@ describe('OpenAILegacyChatProvider', () => { } }, onToolCall(toolCall: ToolCall): void { - events.push(`ready:${toolCall.id}:${toolCall.function.arguments ?? ''}`); + events.push(`ready:${toolCall.id}:${toolCall.arguments ?? ''}`); }, }, ); @@ -841,12 +852,14 @@ describe('OpenAILegacyChatProvider', () => { { type: 'function', id: 'call_a', - function: { name: 'read_file', arguments: '{"path":"a.txt"} ' }, + name: 'read_file', + arguments: '{"path":"a.txt"} ', }, { type: 'function', id: 'call_b', - function: { name: 'read_file', arguments: '{"path":"b.txt"}' }, + name: 'read_file', + arguments: '{"path":"b.txt"}', }, ]); }); @@ -926,7 +939,8 @@ describe('OpenAILegacyChatProvider', () => { { type: 'function', id: 'call_delayed', - function: { name: 'foo', arguments: '{"a":1}' }, + name: 'foo', + arguments: '{"a":1}', }, ]); }); @@ -1009,7 +1023,8 @@ describe('OpenAILegacyChatProvider — non-stream response parsing', () => { expect(toolCall).toMatchObject({ type: 'function', id: 'call_x', - function: { name: 'lookup', arguments: '{"q":"hi"}' }, + name: 'lookup', + arguments: '{"q":"hi"}', }); }); @@ -1114,7 +1129,8 @@ describe('OpenAILegacyChatProvider — non-indexed streaming tool_calls', () => { type: 'function', id: 'call_noidx', - function: { name: 'foo', arguments: '{"a":1}' }, + name: 'foo', + arguments: '{"a":1}', }, ]); }); diff --git a/packages/kosong/test/openai-responses.test.ts b/packages/kosong/test/openai-responses.test.ts index 1a533de..266f7c7 100644 --- a/packages/kosong/test/openai-responses.test.ts +++ b/packages/kosong/test/openai-responses.test.ts @@ -376,7 +376,7 @@ describe('OpenAIResponsesChatProvider', () => { { type: 'function', id: 'call_x', - function: { name: 'lookup', arguments: '{}' }, + name: 'lookup', arguments: '{}', }, ], }, @@ -417,12 +417,12 @@ describe('OpenAIResponsesChatProvider', () => { { type: 'function', id: 'call_add', - function: { name: 'add', arguments: '{"a": 2, "b": 3}' }, + name: 'add', arguments: '{"a": 2, "b": 3}', }, { type: 'function', id: 'call_mul', - function: { name: 'multiply', arguments: '{"a": 4, "b": 5}' }, + name: 'multiply', arguments: '{"a": 4, "b": 5}', }, ], }, @@ -552,7 +552,7 @@ describe('OpenAIResponsesChatProvider', () => { const toolCall: ToolCall = { type: 'function', id: 'call_abc123', - function: { name: 'add', arguments: '{"a": 2, "b": 3}' }, + name: 'add', arguments: '{"a": 2, "b": 3}', }; const history: Message[] = [ { role: 'user', content: [{ type: 'text', text: 'Add 2 and 3' }], toolCalls: [] }, @@ -652,7 +652,7 @@ describe('OpenAIResponsesChatProvider', () => { const toolCall: ToolCall = { type: 'function', id: 'call_audio', - function: { name: 'tts', arguments: '{"text":"hi"}' }, + name: 'tts', arguments: '{"text":"hi"}', }; const dataUrl = 'data:audio/mp3;base64,QUJD'; const httpsUrl = 'https://example.com/speech.wav'; @@ -695,7 +695,7 @@ describe('OpenAIResponsesChatProvider', () => { const toolCall: ToolCall = { type: 'function', id: 'call_abc123', - function: { name: 'add', arguments: '{"a": 2, "b": 3}' }, + name: 'add', arguments: '{"a": 2, "b": 3}', }; const history: Message[] = [ { role: 'user', content: [{ type: 'text', text: 'Add 2 and 3' }], toolCalls: [] }, @@ -920,7 +920,7 @@ describe('OpenAIResponsesChatProvider', () => { { type: 'function', id: 'call_xyz', - function: { name: 'lookup', arguments: '{"q":"hi"}' }, + name: 'lookup', arguments: '{"q":"hi"}', }, ]); }); @@ -1221,7 +1221,7 @@ describe('OpenAIResponsesChatProvider', () => { { type: 'function', id: 'call_123', - function: { name: 'add', arguments: '' }, + name: 'add', arguments: '', _streamIndex: 'item_123', }, { type: 'tool_call_part', argumentsPart: '{"a":', index: 'item_123' }, @@ -1277,7 +1277,7 @@ describe('OpenAIResponsesChatProvider', () => { { type: 'function', id: 'call_done_only', - function: { name: 'add', arguments: '{"a": 2, "b": 3}' }, + name: 'add', arguments: '{"a": 2, "b": 3}', extras: undefined, }, ]); diff --git a/packages/kosong/test/regression.test.ts b/packages/kosong/test/regression.test.ts index 558e4ec..0b1d571 100644 --- a/packages/kosong/test/regression.test.ts +++ b/packages/kosong/test/regression.test.ts @@ -82,7 +82,7 @@ describe('regression', () => { { type: 'function', id: 'call-1', - function: { name: 'search', arguments: '{}' }, + name: 'search', arguments: '{}', }, ], ); @@ -90,7 +90,7 @@ describe('regression', () => { { type: 'function', id: 'call-1', - function: { name: 'search', arguments: '{}' }, + name: 'search', arguments: '{}', }, ]); }); diff --git a/packages/kosong/test/scripted-echo-provider.test.ts b/packages/kosong/test/scripted-echo-provider.test.ts index b5d1d80..fcabfeb 100644 --- a/packages/kosong/test/scripted-echo-provider.test.ts +++ b/packages/kosong/test/scripted-echo-provider.test.ts @@ -62,7 +62,7 @@ describe('ScriptedEchoChatProvider', () => { { type: 'function', id: 'call-1', - function: { name: 'search', arguments: '{"q":"python"' }, + name: 'search', arguments: '{"q":"python"', } satisfies ToolCall, { type: 'tool_call_part', argumentsPart: '}' } satisfies ToolCallPart, ]); @@ -177,7 +177,7 @@ describe('ScriptedEchoChatProvider', () => { const tc = result.message.toolCalls[0]!; expect(tc.id).toBe('call-1'); - expect(tc.function.name).toBe('search'); - expect(tc.function.arguments).toBe('{"q":"python"}'); + expect(tc.name).toBe('search'); + expect(tc.arguments).toBe('{"q":"python"}'); }); }); diff --git a/packages/kosong/test/simple-toolset.test.ts b/packages/kosong/test/simple-toolset.test.ts index 9eca86e..5576de5 100644 --- a/packages/kosong/test/simple-toolset.test.ts +++ b/packages/kosong/test/simple-toolset.test.ts @@ -7,7 +7,7 @@ function makeToolCall(id: string, name: string, args: string | null): ToolCall { return { type: 'function', id, - function: { name, arguments: args }, + name, arguments: args, }; } describe('SimpleToolset', () => { diff --git a/packages/kosong/test/step.test.ts b/packages/kosong/test/step.test.ts index dac532f..0d8883a 100644 --- a/packages/kosong/test/step.test.ts +++ b/packages/kosong/test/step.test.ts @@ -75,7 +75,7 @@ describe('step()', () => { const plusToolCall: ToolCall = { type: 'function', id: 'plus#123', - function: { name: 'plus', arguments: '{"a": 1, "b": 2}' }, + name: 'plus', arguments: '{"a": 1, "b": 2}', }; const stream = createMockStream([{ type: 'text', text: 'Hello, world!' }, plusToolCall]); const provider = createMockProvider(stream); @@ -103,7 +103,7 @@ describe('step()', () => { const plusToolCall: ToolCall = { type: 'function', id: 'plus#123', - function: { name: 'plus', arguments: '{"a": 1, "b": 2}' }, + name: 'plus', arguments: '{"a": 1, "b": 2}', }; const stream = createMockStream([{ type: 'text', text: 'Hello, world!' }, plusToolCall]); const provider = createMockProvider(stream); @@ -175,12 +175,12 @@ describe('step()', () => { const tc1: ToolCall = { type: 'function', id: 'call-1', - function: { name: 'plus', arguments: '{"a":1,"b":2}' }, + name: 'plus', arguments: '{"a":1,"b":2}', }; const tc2: ToolCall = { type: 'function', id: 'call-2', - function: { name: 'plus', arguments: '{"a":3,"b":4}' }, + name: 'plus', arguments: '{"a":3,"b":4}', }; const stream = createMockStream([tc1, tc2]); const provider = createMockProvider(stream); @@ -214,12 +214,12 @@ describe('step()', () => { const tc1: ToolCall = { type: 'function', id: 'call-first', - function: { name: 'slow', arguments: '{}' }, + name: 'slow', arguments: '{}', }; const tc2: ToolCall = { type: 'function', id: 'call-second', - function: { name: 'slow', arguments: '{}' }, + name: 'slow', arguments: '{}', }; const throwingStream: StreamedMessage = { @@ -272,12 +272,12 @@ describe('step()', () => { const tc1: ToolCall = { type: 'function', id: 'call-rejected', - function: { name: 'boom', arguments: '{}' }, + name: 'boom', arguments: '{}', }; const tc2: ToolCall = { type: 'function', id: 'call-next', - function: { name: 'boom', arguments: '{}' }, + name: 'boom', arguments: '{}', }; const throwingStream: StreamedMessage = { @@ -343,7 +343,7 @@ describe('step()', () => { { type: 'function', id: 'call-rejected', - function: { name: 'plus', arguments: '{"a":1,"b":2}' }, + name: 'plus', arguments: '{"a":1,"b":2}', }, ]); const provider = createMockProvider(stream); @@ -384,13 +384,13 @@ describe('step()', () => { { type: 'function', id: 'tc-a', - function: { name: 'plus', arguments: null }, + name: 'plus', arguments: null, _streamIndex: 0, }, { type: 'function', id: 'tc-b', - function: { name: 'plus', arguments: null }, + name: 'plus', arguments: null, _streamIndex: 1, }, { type: 'tool_call_part', argumentsPart: '{"a":1,', index: 0 }, @@ -403,7 +403,7 @@ describe('step()', () => { const toolset: Toolset = { tools: [], handle(toolCall: ToolCall): Promise { - seenArgs.push({ id: toolCall.id, args: toolCall.function.arguments }); + seenArgs.push({ id: toolCall.id, args: toolCall.arguments }); return Promise.resolve({ toolCallId: toolCall.id, returnValue: toolOk({ output: `ok-${toolCall.id}` }), @@ -429,7 +429,7 @@ describe('step()', () => { const tc: ToolCall = { type: 'function', id: 'call-sync', - function: { name: 'plus', arguments: '{}' }, + name: 'plus', arguments: '{}', }; const stream = createMockStream([tc]); const provider = createMockProvider(stream); @@ -502,7 +502,7 @@ describe('step()', () => { const tc: ToolCall = { type: 'function', id: 'tc-propagate', - function: { name: 'plus', arguments: '{"a":1,"b":2}' }, + name: 'plus', arguments: '{"a":1,"b":2}', }; const stream = streamWithFinish([tc], 'tool_calls', 'tool_calls'); const provider = createMockProvider(stream); diff --git a/packages/kosong/test/stress.test.ts b/packages/kosong/test/stress.test.ts index 6401104..34b5abb 100644 --- a/packages/kosong/test/stress.test.ts +++ b/packages/kosong/test/stress.test.ts @@ -77,7 +77,7 @@ describe('stress: large ToolCall arguments (10KB)', () => { { type: 'function', id: 'large-tc-1', - function: { name: 'big_tool', arguments: null }, + name: 'big_tool', arguments: null, }, ...chunks.map( (chunk): StreamedMessagePart => ({ @@ -93,11 +93,11 @@ describe('stress: large ToolCall arguments (10KB)', () => { const result = await generate(provider, '', [], []); expect(result.message.toolCalls).toHaveLength(1); - expect(result.message.toolCalls[0]!.function.arguments).toBe(fullArgs); - expect(result.message.toolCalls[0]!.function.arguments!.length).toBe(fullArgs.length); + expect(result.message.toolCalls[0]!.arguments).toBe(fullArgs); + expect(result.message.toolCalls[0]!.arguments!.length).toBe(fullArgs.length); // Verify the JSON is parseable and correct - const parsed = JSON.parse(result.message.toolCalls[0]!.function.arguments!) as { + const parsed = JSON.parse(result.message.toolCalls[0]!.arguments!) as { data: string; }; expect(parsed.data).toBe(largeValue); @@ -109,17 +109,17 @@ describe('stress: concurrent tool dispatch', () => { const tc1: ToolCall = { type: 'function', id: 'call-1', - function: { name: 'slow_tool', arguments: '{"delay": 1}' }, + name: 'slow_tool', arguments: '{"delay": 1}', }; const tc2: ToolCall = { type: 'function', id: 'call-2', - function: { name: 'slow_tool', arguments: '{"delay": 2}' }, + name: 'slow_tool', arguments: '{"delay": 2}', }; const tc3: ToolCall = { type: 'function', id: 'call-3', - function: { name: 'slow_tool', arguments: '{"delay": 3}' }, + name: 'slow_tool', arguments: '{"delay": 3}', }; const stream = createMockStream([tc1, tc2, tc3]); @@ -169,7 +169,7 @@ describe('stress: tool handler throws exception', () => { const tc: ToolCall = { type: 'function', id: 'crash-call', - function: { name: 'crasher', arguments: '{}' }, + name: 'crasher', arguments: '{}', }; const stream = createMockStream([{ type: 'text', text: 'calling tool' }, tc]); @@ -203,12 +203,12 @@ describe('stress: tool handler throws exception', () => { const tc1: ToolCall = { type: 'function', id: 'ok-call', - function: { name: 'good_tool', arguments: '{}' }, + name: 'good_tool', arguments: '{}', }; const tc2: ToolCall = { type: 'function', id: 'bad-call', - function: { name: 'bad_tool', arguments: '{}' }, + name: 'bad_tool', arguments: '{}', }; const stream = createMockStream([tc1, tc2]); @@ -285,7 +285,7 @@ describe('stress: consecutive different type parts', () => { { type: 'function', id: 'tc-1', - function: { name: 'search', arguments: null }, + name: 'search', arguments: null, }, { type: 'tool_call_part', argumentsPart: '{"q":' }, // merges into ToolCall { type: 'tool_call_part', argumentsPart: '"test"}' }, // merges into ToolCall @@ -315,7 +315,7 @@ describe('stress: consecutive different type parts', () => { expect(result.message.toolCalls[0]).toEqual({ type: 'function', id: 'tc-1', - function: { name: 'search', arguments: '{"q":"test"}' }, + name: 'search', arguments: '{"q":"test"}', }); }); @@ -344,12 +344,12 @@ describe('stress: consecutive different type parts', () => { { type: 'function', id: 'tc-1', - function: { name: 'tool_a', arguments: '{"x":1}' }, + name: 'tool_a', arguments: '{"x":1}', }, { type: 'function', id: 'tc-2', - function: { name: 'tool_b', arguments: '{"y":2}' }, + name: 'tool_b', arguments: '{"y":2}', }, ]; diff --git a/packages/kosong/test/type-safety-negative.ts b/packages/kosong/test/type-safety-negative.ts index 9907efd..9f6e450 100644 --- a/packages/kosong/test/type-safety-negative.ts +++ b/packages/kosong/test/type-safety-negative.ts @@ -44,7 +44,7 @@ const msg4: Message = { const tc1: ToolCall = { type: 'function', id: 'call-1', - function: { name: 'test', arguments: null }, + name: 'test', arguments: null, extras: undefined, }; // Accessing a property from the wrong variant should fail. diff --git a/packages/kosong/test/type-safety.test.ts b/packages/kosong/test/type-safety.test.ts index c6a8215..5c3dec1 100644 --- a/packages/kosong/test/type-safety.test.ts +++ b/packages/kosong/test/type-safety.test.ts @@ -27,7 +27,7 @@ function processPartSafely(part: StreamedMessagePart): string { case 'video_url': return part.videoUrl.url; // VideoURLPart.videoUrl.url -> string case 'function': - return part.function.name; // ToolCall.function.name -> string + return part.name; // ToolCall.name -> string case 'tool_call_part': return part.argumentsPart ?? ''; // ToolCallPart.argumentsPart -> string | null default: { @@ -76,7 +76,7 @@ describe('StreamedMessagePart discriminated union narrowing', () => { const part: StreamedMessagePart = { type: 'function', id: 'call-1', - function: { name: 'search', arguments: '{"q":"test"}' }, + name: 'search', arguments: '{"q":"test"}', }; expect(processPartSafely(part)).toBe('search'); }); @@ -111,7 +111,7 @@ describe('exhaustiveness check', () => { { type: 'image_url', imageUrl: { url: 'c' } }, { type: 'audio_url', audioUrl: { url: 'd' } }, { type: 'video_url', videoUrl: { url: 'e' } }, - { type: 'function', id: 'f', function: { name: 'g', arguments: null } }, + { type: 'function', id: 'f', name: 'g', arguments: null }, { type: 'tool_call_part', argumentsPart: 'h' }, ]; for (const part of allParts) { @@ -151,7 +151,7 @@ describe('type assignability', () => { }); it('ToolCall is assignable to StreamedMessagePart', () => { - const tc: ToolCall = { type: 'function', id: 'c', function: { name: 'f', arguments: null } }; + const tc: ToolCall = { type: 'function', id: 'c', name: 'f', arguments: null }; const _part: StreamedMessagePart = tc; expect(_part.type).toBe('function'); }); diff --git a/packages/kosong/test/typed-tool.test.ts b/packages/kosong/test/typed-tool.test.ts index 15f054d..3e05ecf 100644 --- a/packages/kosong/test/typed-tool.test.ts +++ b/packages/kosong/test/typed-tool.test.ts @@ -100,7 +100,7 @@ describe('createTypedTool', () => { const result = await toolset.handle({ type: 'function', id: 'tc_001', - function: { name: 'echo', arguments: JSON.stringify({ text: 'hello' }) }, + name: 'echo', arguments: JSON.stringify({ text: 'hello' }), }); expect(result.returnValue.isError).toBe(false); diff --git a/packages/migration-legacy/src/sessions/translator.ts b/packages/migration-legacy/src/sessions/translator.ts index 499180b..3bc530c 100644 --- a/packages/migration-legacy/src/sessions/translator.ts +++ b/packages/migration-legacy/src/sessions/translator.ts @@ -6,7 +6,8 @@ export interface NormalizedMessage { readonly toolCalls: ReadonlyArray<{ readonly type: 'function'; readonly id: string; - readonly function: { readonly name: string; readonly arguments: string }; + readonly name: string; + readonly arguments: string; }>; readonly toolCallId?: string; } @@ -146,7 +147,8 @@ function buildTool(obj: Record): NormalizedMessage { interface RawToolCall { readonly type: 'function'; readonly id: string; - readonly function: { readonly name: string; readonly arguments: string }; + readonly name: string; + readonly arguments: string; } function parseToolCall(raw: unknown): RawToolCall | undefined { @@ -162,7 +164,8 @@ function parseToolCall(raw: unknown): RawToolCall | undefined { return { type: 'function', id: r['id'], - function: { name: f['name'], arguments: args }, + name: f['name'], + arguments: args, }; } diff --git a/packages/migration-legacy/test/sessions/__snapshots__/fixtures.snapshot.test.ts.snap b/packages/migration-legacy/test/sessions/__snapshots__/fixtures.snapshot.test.ts.snap index e037c8a..a67ee58 100644 --- a/packages/migration-legacy/test/sessions/__snapshots__/fixtures.snapshot.test.ts.snap +++ b/packages/migration-legacy/test/sessions/__snapshots__/fixtures.snapshot.test.ts.snap @@ -27,7 +27,7 @@ exports[`migration snapshot: archived > migration succeeds and matches snapshot "wire": "{"type":"metadata","protocol_version":"1.0","created_at":} {"type":"context.append_message","message":{"role":"user","content":[{"type":"text","text":"You are a code translation assistant.\\n\\nTask:\\n- Read the file \`sample.js\` in the current working directory.\\n- Translate it into idiomatic Python 3.\\n- Write the translated code to \`translated.py\` in the current working directory.\\n\\nRules:\\n- You must read the file from disk; do not guess its contents.\\n- Preserve behavior and output.\\n- Write only Python code in translated.py (no Markdown).\\n- Overwrite translated.py if it already exists.\\n- After writing, reply with a single short ASCII confirmation se... [truncated]"}],"toolCalls":[]}} {"type":"context.append_message","message":{"role":"user","content":[{"type":"text","text":"\\nYou are running in non-interactive mode. The user cannot answer questions or provide feedback during execution.\\n- Do NOT call AskUserQuestion. If you need to make a decision, make your best judgment and proceed.\\n- For EnterPlanMode / ExitPlanMode, they will be auto-approved. You can use them normally but expect no user feedback.\\n"}],"toolCalls":[]}} -{"type":"context.append_message","message":{"role":"assistant","content":[],"toolCalls":[{"type":"function","id":"ReadFile:0","function":{"name":"ReadFile","arguments":"{\\"path\\": \\"sample.js\\"}"}}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[],"toolCalls":[{"type":"function","id":"ReadFile:0","name":"ReadFile","arguments":"{\\"path\\": \\"sample.js\\"}"}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"10 lines read from file starting from line 1. End of file reached."},{"type":"text","text":" 1\\tfunction add(a, b) {\\n 2\\t return a + b;\\n 3\\t}\\n 4\\t\\n 5\\tfunction main() {\\n 6\\t const result = add(2, 3);\\n 7\\t console.log(\`2 + 3 = \${result}\`);\\n 8\\t}\\n 9\\t\\n 10\\tmain();\\n"}],"toolCalls":[],"toolCallId":"ReadFile:0"}} ", } @@ -59,63 +59,63 @@ exports[`migration snapshot: large-100msgs > migration succeeds and matches snap }", "wire": "{"type":"metadata","protocol_version":"1.0","created_at":} {"type":"context.append_message","message":{"role":"user","content":[{"type":"text","text":"请帮我探索一下当前这个仓库的整体结构,了解它的主要模块和功能。看完之后请写一份简短的小报告,列出你认为最重要的几个模块、它们的职责,以及彼此之间的依赖关系。报告控制在一页之内,便于团队成员快速了解项目。"}],"toolCalls":[]}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"用户想要我读取 example-task skill 的 SKILL.md 文件,但他们已经在 prompt 中提供了完整的 skill 内容。不过我需要了解这个 skill 的更多细节,特别是 references/prompt-patterns.md 文件。\\n\\n用户似乎在向我介绍这个 skill,让我熟悉它的用法。我应该先检查一下这个 skill 目录的结构,看看是否有其他有用的资源文件。\\n\\n让我先了解一下这个 skill 的完整结构,以便以后需要时可以使用它。"},{"type":"text","text":"我来熟悉一下 \`example-task\` skill 的完整结构和资源。"}],"toolCalls":[{"type":"function","id":"tool_3mtoDjRlqenM0GDSjv9xRzLS","function":{"name":"Glob","arguments":"{\\"pattern\\": \\".agents/skills/example-task/**/*\\"}"}}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"用户想要我读取 example-task skill 的 SKILL.md 文件,但他们已经在 prompt 中提供了完整的 skill 内容。不过我需要了解这个 skill 的更多细节,特别是 references/prompt-patterns.md 文件。\\n\\n用户似乎在向我介绍这个 skill,让我熟悉它的用法。我应该先检查一下这个 skill 目录的结构,看看是否有其他有用的资源文件。\\n\\n让我先了解一下这个 skill 的完整结构,以便以后需要时可以使用它。"},{"type":"text","text":"我来熟悉一下 \`example-task\` skill 的完整结构和资源。"}],"toolCalls":[{"type":"function","id":"tool_3mtoDjRlqenM0GDSjv9xRzLS","name":"Glob","arguments":"{\\"pattern\\": \\".agents/skills/example-task/**/*\\"}"}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Found 5 matches for pattern \`.agents/skills/example-task/**/*\`."},{"type":"text","text":".agents/skills/example-task/SKILL.md\\n.agents/skills/example-task/references\\n.agents/skills/example-task/references/prompt-patterns.md\\n.agents/skills/example-task/scripts\\n.agents/skills/example-task/scripts/inspect_session.py"}],"toolCalls":[],"toolCallId":"tool_3mtoDjRlqenM0GDSjv9xRzLS"}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"让我读取 references/prompt-patterns.md 和 scripts/inspect_session.py 来了解完整的 skill 资源。"}],"toolCalls":[{"type":"function","id":"tool_nqkbedqxXK1aX9zAWWNEhzmM","function":{"name":"ReadFile","arguments":"{\\"path\\": \\".agents/skills/example-task/references/prompt-patterns.md\\"}"}},{"type":"function","id":"tool_300OLXjS5RZxFz9LvWJP6QyJ","function":{"name":"ReadFile","arguments":"{\\"path\\": \\".agents/skills/example-task/scripts/inspect_session.py\\"}"}}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"让我读取 references/prompt-patterns.md 和 scripts/inspect_session.py 来了解完整的 skill 资源。"}],"toolCalls":[{"type":"function","id":"tool_nqkbedqxXK1aX9zAWWNEhzmM","name":"ReadFile","arguments":"{\\"path\\": \\".agents/skills/example-task/references/prompt-patterns.md\\"}"},{"type":"function","id":"tool_300OLXjS5RZxFz9LvWJP6QyJ","name":"ReadFile","arguments":"{\\"path\\": \\".agents/skills/example-task/scripts/inspect_session.py\\"}"}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"85 lines read from file starting from line 1. End of file reached."},{"type":"text","text":" 1\\t# Prompt 模板\\n 2\\t\\n 3\\t以下模板作为脚手架使用。运行前替换占位符。\\n 4\\t\\n 5\\t## 单轮还是多轮\\n 6\\t\\n 7\\t满足以下任一条件时使用多轮:\\n 8\\t\\n 9\\t- 功能有状态\\n 10\\t- 功能依赖时序或并发\\n 11\\t- 功能需要审批、清理或恢复\\n 12\\t- session 产物本身是证据的一部分\\n 13\\t- 工具接口可能近期发生过变化\\n 14\\t\\n 15\\t仅对无状态的窄范围检查使用单轮。\\n 16\\t\\n 17\\t## 变量\\n 18\\t\\n 19\\t起草 prompt 前填写以下字段:\\n 20\\t\\n 21\\t- \`\` — 被测功能名称\\n 22\\t- \`\` — 当前场景的目标\\n 23\\t- \`\` — 需要阅读的源码路径\\n 24\\t- \`\` — 执行约束\\n 25\\t- \`\` — 成功信号\\n 26\\t- \`321 lines read from file starting from line 1. End of file reached."},{"type":"text","text":" 1\\t#!/usr/bin/env python3\\n 2\\t\\"\\"\\"Locate and summarize a Kimi CLI session for smoke-test review.\\"\\"\\"\\n 3\\t\\n 4\\tfrom __future__ import annotations\\n 5\\t\\n 6\\timport argparse\\n 7\\timport json\\n 8\\timport sys\\n 9\\tfrom collections import Counter\\n 10\\tfrom pathlib import Path\\n 11\\tfrom typing import Any\\n 12\\t\\n 13\\t\\n 14\\tdef parse_args() -> argparse.Namespace:\\n 15\\t parser = argparse.ArgumentParser(\\n 16\\t description=\\"Locate and summarize a Kimi CLI session... [truncated]"}],"toolCalls":[],"toolCallId":"tool_300OLXjS5RZxFz9LvWJP6QyJ"}} {"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"好的,我现在已经完整了解了 \`example-task\` skill 的内容。这个 skill 提供了:\\n\\n1. **SKILL.md** - 主文档,描述了如何针对 the example CLI 的新增或变更功能进行端到端冒烟测试的完整流程\\n2. **references/prompt-patterns.md** - 可复用的 prompt 模板\\n3. **scripts/inspect_session.py** - 用于检查和分析 session 产物的脚本\\n\\n用户似乎是在测试我是否能正确读取和使用这个 skill。既然用户已经在 prompt 中提供了完整的 skill 内容,我应该:\\n1. 确认我已经理解了 skill 的内容\\n2. 询问用户是否有具体的功能需要我使用这个 skill 来进行冒烟测试\\n\\n或者,用户可能是想了解这个 skill 的用法。让我给出一个总结并询问用户的具体需求。"},{"type":"text","text":"我已完整阅读 \`example-task\` skill。这是一个用于对 the example CLI 进行端到端冒烟测试的系统化流程。\\n\\n## Skill 要点总结\\n\\n**核心流程:**\\n1. **确定测试范围** - 从 \`git diff\` 推断功能边界\\n2. **读取事实来源** - 文档、代码、agent prompt、测试\\n3. **制定最小测试计划** - 正常路径、边界条件、中断/清理\\n4. **多轮执行** - 探索→执行→观察→清理\\n5. **非交互模式运行** - 使用 \`--print --work-dir /tmp/...\`\\n6. **检查产物** - \`context.jsonl\`, \`wire.jsonl\`, 后台任务等\\n7. **问题探查** - 并行多路追踪根因\\n\\n**可用资源:**\\n- \`references/prompt-patterns.md\` - 探索/执行/观察/复盘 prompt 模板\\n- \`scripts/inspect_session.py\` - Session 产物分析工具\\n\\n---\\n\\n**你想让我使用这个 skill 来测试... [truncated]"}],"toolCalls":[]}} {"type":"context.append_message","message":{"role":"user","content":[{"type":"text","text":"请基于 README 和主要源代码目录,给出一份简短的探索清单"}],"toolCalls":[]}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"用户想让我直接使用 \`git diff main --name-only\` 来查看当前分支相对于 main 分支的变更,然后基于这些变更进行冒烟测试。\\n\\n让我先执行这个命令看看有哪些文件变更。"}],"toolCalls":[{"type":"function","id":"tool_jOGB6wicU1PmefwL15U57RUT","function":{"name":"Shell","arguments":"{\\"command\\": \\"git diff main --name-only && echo \\\\\\"---\\\\\\" && git diff main --stat\\"}"}}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"用户想让我直接使用 \`git diff main --name-only\` 来查看当前分支相对于 main 分支的变更,然后基于这些变更进行冒烟测试。\\n\\n让我先执行这个命令看看有哪些文件变更。"}],"toolCalls":[{"type":"function","id":"tool_jOGB6wicU1PmefwL15U57RUT","name":"Shell","arguments":"{\\"command\\": \\"git diff main --name-only && echo \\\\\\"---\\\\\\" && git diff main --stat\\"}"}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Command executed successfully."},{"type":"text","text":".agents/skills/example-task/SKILL.md\\n.agents/skills/example-task/references/prompt-patterns.md\\n.agents/skills/example-task/scripts/inspect_session.py\\nCHANGELOG.md\\nfeature-overview.md\\ndocs/en/configuration/data-locations.md\\ndocs/en/customization/wire-mode.md\\ndocs/en/guides/interaction.md\\ndocs/en/guides/sessions.md\\ndocs/en/reference/keyboard.md\\ndocs/en/reference/kimi-command.md\\ndocs/en/reference/kimi-info.md\\ndocs/en/reference/kimi-vis.md\\ndocs/en/release-notes/changelog.md\\ndoc... [truncated]"}],"toolCalls":[],"toolCallId":"tool_jOGB6wicU1PmefwL15U57RUT"}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"从 git diff 的输出来看,这是一个非常大的变更集,主要涉及以下几个核心功能:\\n\\n1. **后台任务系统 (background/)** - 全新模块\\n - \`src/example_pkg/background/\` - 后台任务管理核心\\n - \`src/example_pkg/tools/background/\` - 后台任务工具\\n\\n2. **通知系统 (notifications/)** - 全新模块\\n - \`src/example_pkg/notifications/\` - 通知管理\\n\\n3. **Shell UI 重构** - 大规模重构\\n - \`src/example_pkg/ui/shell/\` - Shell UI 组件重构\\n - 移除了大量旧代码(placeholders, echo 等)\\n\\n4. **Wire 协议更新**\\n - \`src/example_pkg/wire/types.py\` 和 \`protocol.py\`\\n\\n5. **动态注入 (dynamic_injection.py)**\\n - 更新\\n\\n6. **测试覆盖**\\n - 大量新增测试\\n\\n这是一个... [truncated]"},{"type":"text","text":"根据 git diff 输出,这是一个大规模功能变更,主要涉及:\\n\\n1. **后台任务系统** (\`src/example_pkg/background/\`) - 全新模块\\n2. **通知系统** (\`src/example_pkg/notifications/\`) - 全新模块 \\n3. **后台任务工具** (\`src/example_pkg/tools/background/\`) - 新工具\\n4. **Shell UI 重构** (\`src/example_pkg/ui/shell/\`) - 大量重构\\n5. **Wire 协议更新** (\`src/example_pkg/wire/types.py\`)\\n6. **动态注入更新** (\`src/example_pkg/soul/dynamic_injection.py\`)\\n\\n让我开始冒烟测试流程,首先读取事实来源:"}],"toolCalls":[{"type":"function","id":"tool_TB9CacXCzJ8B11tzzOneV7MK","function":{"name":"ReadFile","arguments":"{\\"path\\": \\"feature-overview.md\\"}"}},{"type":"function","id":"tool_7tKhkdZsBAFotRMTFs0BmgM6","function":{"name":"ReadFile","arguments":"{\\"path\\": \\"src/example_pkg/background/manager.py\\"}"}},{"type":"function","id":"tool_DR5VfRHgGe7WZ1dXKSA5Yowh","function":{"name":"ReadFile","arguments":"{\\"path\\": \\"src/example_pkg/background/models.py\\"}"}},{"type":"function","id":"tool_XZXYciY6FY2Tl4SHwuh7Elhl","function":{"name":"ReadFile","arguments":"{\\"path\\": \\"src/example_pkg/tools/background/__init__.py\\"}"}}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"从 git diff 的输出来看,这是一个非常大的变更集,主要涉及以下几个核心功能:\\n\\n1. **后台任务系统 (background/)** - 全新模块\\n - \`src/example_pkg/background/\` - 后台任务管理核心\\n - \`src/example_pkg/tools/background/\` - 后台任务工具\\n\\n2. **通知系统 (notifications/)** - 全新模块\\n - \`src/example_pkg/notifications/\` - 通知管理\\n\\n3. **Shell UI 重构** - 大规模重构\\n - \`src/example_pkg/ui/shell/\` - Shell UI 组件重构\\n - 移除了大量旧代码(placeholders, echo 等)\\n\\n4. **Wire 协议更新**\\n - \`src/example_pkg/wire/types.py\` 和 \`protocol.py\`\\n\\n5. **动态注入 (dynamic_injection.py)**\\n - 更新\\n\\n6. **测试覆盖**\\n - 大量新增测试\\n\\n这是一个... [truncated]"},{"type":"text","text":"根据 git diff 输出,这是一个大规模功能变更,主要涉及:\\n\\n1. **后台任务系统** (\`src/example_pkg/background/\`) - 全新模块\\n2. **通知系统** (\`src/example_pkg/notifications/\`) - 全新模块 \\n3. **后台任务工具** (\`src/example_pkg/tools/background/\`) - 新工具\\n4. **Shell UI 重构** (\`src/example_pkg/ui/shell/\`) - 大量重构\\n5. **Wire 协议更新** (\`src/example_pkg/wire/types.py\`)\\n6. **动态注入更新** (\`src/example_pkg/soul/dynamic_injection.py\`)\\n\\n让我开始冒烟测试流程,首先读取事实来源:"}],"toolCalls":[{"type":"function","id":"tool_TB9CacXCzJ8B11tzzOneV7MK","name":"ReadFile","arguments":"{\\"path\\": \\"feature-overview.md\\"}"},{"type":"function","id":"tool_7tKhkdZsBAFotRMTFs0BmgM6","name":"ReadFile","arguments":"{\\"path\\": \\"src/example_pkg/background/manager.py\\"}"},{"type":"function","id":"tool_DR5VfRHgGe7WZ1dXKSA5Yowh","name":"ReadFile","arguments":"{\\"path\\": \\"src/example_pkg/background/models.py\\"}"},{"type":"function","id":"tool_XZXYciY6FY2Tl4SHwuh7Elhl","name":"ReadFile","arguments":"{\\"path\\": \\"src/example_pkg/tools/background/__init__.py\\"}"}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"353 lines read from file starting from line 1. End of file reached."},{"type":"text","text":" 1\\t# 后台任务实现说明\\n 2\\t\\n 3\\t这份文档是给后续开发者快速理解当前 background task / notification 实现用的内部说明,不是用户文档。\\n 4\\t\\n 5\\t## 一句话结论\\n 6\\t\\n 7\\t当前实现已经不再把 background task completion 建模成 \`dynamic injection\` 的一个特例。\\n 8\\t\\n 9\\t现在的结构是:\\n 10\\t\\n 11\\t- \`Shell(run_in_background=true)\` 负责启动后台任务\\n 12\\t- \`TaskList\` / \`TaskOutput\` / \`TaskStop\` 负责最小控制面\\n 13\\t- \`BackgroundTaskManager\` 负责 task 生命周期\\n 14\\t- \`NotificationManager\` 负责通用 notification 基建\\n 15\\t- \`task notification\` 只是 notification 基建里的一个 type\\n ... [truncated]"}],"toolCalls":[],"toolCallId":"tool_TB9CacXCzJ8B11tzzOneV7MK"}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"358 lines read from file starting from line 1. End of file reached."},{"type":"text","text":" 1\\tfrom __future__ import annotations\\n 2\\t\\n 3\\timport asyncio\\n 4\\timport os\\n 5\\timport signal\\n 6\\timport subprocess\\n 7\\timport sys\\n 8\\timport time\\n 9\\tfrom pathlib import Path\\n 10\\t\\n 11\\tfrom kaos.local import local_kaos\\n 12\\t\\n 13\\tfrom example_pkg.config import BackgroundConfig\\n 14\\tfrom example_pkg.notifications import NotificationEvent, NotificationManager\\n 15\\tfrom example_pkg.session import Session\\n 16\\tfrom example_pkg.utils.logging import logger\\n 17\\t\\n 1... [truncated]"}],"toolCalls":[],"toolCallId":"tool_7tKhkdZsBAFotRMTFs0BmgM6"}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"89 lines read from file starting from line 1. End of file reached."},{"type":"text","text":" 1\\tfrom __future__ import annotations\\n 2\\t\\n 3\\timport time\\n 4\\tfrom typing import Literal\\n 5\\t\\n 6\\tfrom pydantic import BaseModel, ConfigDict, Field\\n 7\\t\\n 8\\ttype TaskKind = Literal[\\"bash\\", \\"agent\\"]\\n 9\\ttype TaskStatus = Literal[\\"created\\", \\"starting\\", \\"running\\", \\"completed\\", \\"failed\\", \\"killed\\", \\"lost\\"]\\n 10\\ttype TaskOwnerRole = Literal[\\"root\\", \\"fixed_subagent\\", \\"dynamic_subagent\\"]\\n 11\\t\\n 12\\tTERMINAL_TASK_STATUSES: tuple[TaskStatus, ...] = (\\"completed\\", \\"failed\\"... [truncated]"}],"toolCalls":[],"toolCallId":"tool_DR5VfRHgGe7WZ1dXKSA5Yowh"}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"235 lines read from file starting from line 1. End of file reached."},{"type":"text","text":" 1\\timport time\\n 2\\tfrom pathlib import Path\\n 3\\tfrom typing import override\\n 4\\t\\n 5\\tfrom kosong.tooling import CallableTool2, ToolError, ToolReturnValue\\n 6\\tfrom pydantic import BaseModel, Field\\n 7\\t\\n 8\\tfrom example_pkg.background import format_task, format_task_list, list_task_views\\n 9\\tfrom example_pkg.soul.agent import Runtime\\n 10\\tfrom example_pkg.soul.approval import Approval\\n 11\\tfrom example_pkg.tools.display import BackgroundTaskDisplayBlock\\n 12\\tfrom example_pkg.too... [truncated]"}],"toolCalls":[],"toolCallId":"tool_XZXYciY6FY2Tl4SHwuh7Elhl"}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"好的,我已经读取了关键的事实来源。现在让我继续读取其他重要文件来理解完整的功能边界:\\n\\n1. Shell 工具的后台模式支持\\n2. Notification 系统\\n3. Wire 协议更新\\n4. KimiSoul 中的 notification 集成\\n\\n让我继续读取这些文件。"}],"toolCalls":[{"type":"function","id":"tool_4vAGVbvQFNnyAFHtAkgnKwUu","function":{"name":"ReadFile","arguments":"{\\"path\\": \\"src/example_pkg/tools/shell/__init__.py\\"}"}},{"type":"function","id":"tool_YRsRNEO743zdY5vBEYPYv5QJ","function":{"name":"ReadFile","arguments":"{\\"path\\": \\"src/example_pkg/notifications/manager.py\\"}"}},{"type":"function","id":"tool_Zi0gi1gBQijIF8lEEx1RiSPz","function":{"name":"ReadFile","arguments":"{\\"path\\": \\"src/example_pkg/wire/types.py\\"}"}},{"type":"function","id":"tool_SorRMdJIzuUdPESVPYIbDqQU","function":{"name":"ReadFile","arguments":"{\\"path\\": \\"src/example_pkg/soul/kimisoul.py\\", \\"n_lines\\": 150}"}}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"好的,我已经读取了关键的事实来源。现在让我继续读取其他重要文件来理解完整的功能边界:\\n\\n1. Shell 工具的后台模式支持\\n2. Notification 系统\\n3. Wire 协议更新\\n4. KimiSoul 中的 notification 集成\\n\\n让我继续读取这些文件。"}],"toolCalls":[{"type":"function","id":"tool_4vAGVbvQFNnyAFHtAkgnKwUu","name":"ReadFile","arguments":"{\\"path\\": \\"src/example_pkg/tools/shell/__init__.py\\"}"},{"type":"function","id":"tool_YRsRNEO743zdY5vBEYPYv5QJ","name":"ReadFile","arguments":"{\\"path\\": \\"src/example_pkg/notifications/manager.py\\"}"},{"type":"function","id":"tool_Zi0gi1gBQijIF8lEEx1RiSPz","name":"ReadFile","arguments":"{\\"path\\": \\"src/example_pkg/wire/types.py\\"}"},{"type":"function","id":"tool_SorRMdJIzuUdPESVPYIbDqQU","name":"ReadFile","arguments":"{\\"path\\": \\"src/example_pkg/soul/kimisoul.py\\", \\"n_lines\\": 150}"}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"216 lines read from file starting from line 1. End of file reached."},{"type":"text","text":" 1\\timport asyncio\\n 2\\tfrom collections.abc import Callable\\n 3\\tfrom pathlib import Path\\n 4\\tfrom typing import Self, override\\n 5\\t\\n 6\\timport kaos\\n 7\\tfrom kaos import AsyncReadable\\n 8\\tfrom kosong.tooling import CallableTool2, ToolReturnValue\\n 9\\tfrom pydantic import BaseModel, Field, model_validator\\n 10\\t\\n 11\\tfrom example_pkg.background import TaskView, format_task\\n 12\\tfrom example_pkg.soul.agent import Runtime\\n 13\\tfrom example_pkg.soul.approval import Approval\\n ... [truncated]"}],"toolCalls":[],"toolCallId":"tool_4vAGVbvQFNnyAFHtAkgnKwUu"}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"105 lines read from file starting from line 1. End of file reached."},{"type":"text","text":" 1\\tfrom __future__ import annotations\\n 2\\t\\n 3\\timport time\\n 4\\timport uuid\\n 5\\tfrom pathlib import Path\\n 6\\t\\n 7\\tfrom example_pkg.config import NotificationConfig\\n 8\\t\\n 9\\tfrom .models import (\\n 10\\t NotificationDelivery,\\n 11\\t NotificationEvent,\\n 12\\t NotificationSink,\\n 13\\t NotificationSinkState,\\n 14\\t NotificationView,\\n 15\\t)\\n 16\\tfrom .store import NotificationStore\\n 17\\t\\n 18\\t\\n 19\\tclass NotificationManager:\\n 20\\t def __init__... [truncated]"}],"toolCalls":[],"toolCallId":"tool_YRsRNEO743zdY5vBEYPYv5QJ"}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"515 lines read from file starting from line 1. End of file reached."},{"type":"text","text":" 1\\tfrom __future__ import annotations\\n 2\\t\\n 3\\timport asyncio\\n 4\\tfrom typing import Any, Literal, TypeGuard, cast\\n 5\\t\\n 6\\tfrom kosong.chat_provider import TokenUsage\\n 7\\tfrom kosong.message import (\\n 8\\t AudioURLPart,\\n 9\\t ContentPart,\\n 10\\t ImageURLPart,\\n 11\\t TextPart,\\n 12\\t ThinkPart,\\n 13\\t ToolCall,\\n 14\\t ToolCallPart,\\n 15\\t VideoURLPart,\\n 16\\t)\\n 17\\tfrom kosong.tooling import (\\n 18\\t BriefDisplayBlock,\\n 19\\t Dis... [truncated]"}],"toolCalls":[],"toolCallId":"tool_Zi0gi1gBQijIF8lEEx1RiSPz"}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"150 lines read from file starting from line 1."},{"type":"text","text":" 1\\tfrom __future__ import annotations\\n 2\\t\\n 3\\timport asyncio\\n 4\\tfrom collections.abc import Awaitable, Callable, Sequence\\n 5\\tfrom contextlib import suppress\\n 6\\tfrom dataclasses import dataclass\\n 7\\tfrom functools import partial\\n 8\\tfrom pathlib import Path\\n 9\\tfrom typing import TYPE_CHECKING, Any, Literal\\n 10\\t\\n 11\\timport kosong\\n 12\\timport tenacity\\n 13\\tfrom kosong import StepResult\\n 14\\tfrom kosong.chat_provider import (\\n 15\\t APIConnectionError... [truncated]"}],"toolCalls":[],"toolCallId":"tool_SorRMdJIzuUdPESVPYIbDqQU"}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"我已经获取了足够的背景信息。让我继续读取 KimiSoul 中 notification 相关的代码,以及相关的测试来理解完整的功能边界。"}],"toolCalls":[{"type":"function","id":"tool_okFY9oLifSxqJb8egNPac0MS","function":{"name":"ReadFile","arguments":"{\\"path\\": \\"src/example_pkg/soul/kimisoul.py\\", \\"line_offset\\": 200, \\"n_lines\\": 200}"}},{"type":"function","id":"tool_CcxVl6nzWr4TfMaivaLiZxsz","function":{"name":"Grep","arguments":"{\\"pattern\\": \\"def _step|notification|claim|publish\\", \\"path\\": \\"src/example_pkg/soul/kimisoul.py\\", \\"output_mode\\": \\"content\\", \\"-n\\": true}"}}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"我已经获取了足够的背景信息。让我继续读取 KimiSoul 中 notification 相关的代码,以及相关的测试来理解完整的功能边界。"}],"toolCalls":[{"type":"function","id":"tool_okFY9oLifSxqJb8egNPac0MS","name":"ReadFile","arguments":"{\\"path\\": \\"src/example_pkg/soul/kimisoul.py\\", \\"line_offset\\": 200, \\"n_lines\\": 200}"},{"type":"function","id":"tool_CcxVl6nzWr4TfMaivaLiZxsz","name":"Grep","arguments":"{\\"pattern\\": \\"def _step|notification|claim|publish\\", \\"path\\": \\"src/example_pkg/soul/kimisoul.py\\", \\"output_mode\\": \\"content\\", \\"-n\\": true}"}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"200 lines read from file starting from line 200."},{"type":"text","text":" 200\\t\\n 201\\t def path_getter() -> Path | None:\\n 202\\t return self.get_plan_file_path()\\n 203\\t\\n 204\\t # WriteFile gets both checker and path_getter (for plan file auto-approve)\\n 205\\t from example_pkg.tools.file.write import WriteFile\\n 206\\t\\n 207\\t write_tool = self._agent.toolset.find(\\"WriteFile\\")\\n 208\\t if isinstance(write_tool, WriteFile):\\n 209\\t write_tool.bind_plan_mode(checker, path_getter)\\n 210\\t\\n 211\\t # ExitPlanMod... [truncated]"}],"toolCalls":[],"toolCallId":"tool_okFY9oLifSxqJb8egNPac0MS"}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"26:from example_pkg.notifications import build_notification_message, extract_notification_ids\\n147: self._runtime.notifications.ack_ids(\\"llm\\", extract_notification_ids(context.history))\\n626: async def _step(self) -> StepOutcome | None:\\n634: notifications = self._runtime.notifications.claim_for_sink(\\"llm\\", limit=4)\\n635: if notifications:\\n637: build_notification_message(view, self._runtime) for view in notifications\\n640: self._runti... [truncated]"}],"toolCalls":[],"toolCallId":"tool_CcxVl6nzWr4TfMaivaLiZxsz"}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"现在我对功能有了完整的理解。让我总结一下测试范围:\\n\\n**功能边界:**\\n1. **后台任务系统** - 创建、监控、停止、输出获取\\n2. **通知系统** - 发布、消费、去重、确认\\n3. **集成点** - Shell 后台模式、TaskList/TaskOutput/TaskStop 工具、Wire 通知事件、LLM 通知消息\\n\\n**用户可感知的行为变化:**\\n1. Shell 工具新增 \`run_in_background\` 和 \`description\` 参数\\n2. 新增 TaskList/TaskOutput/TaskStop 工具\\n3. 后台任务完成后自动发送通知到 LLM context 和 Wire\\n4. 通知通过独立的 \`\` 消息进入 context\\n5. Wire 协议新增 \`Notification\` 事件类型\\n\\n**测试计划:**\\n1. **正常路径** - 启动后台任务,等待完成,验证通知\\n2. **边界条件** - 超时处理、重复停止、无效 task_id\\n3. **中断/清理** - 主动停止任务、任务丢失恢复\\n\\n让我开始制定... [truncated]"},{"type":"text","text":"现在我对功能有了完整理解。让我制定测试计划:\\n\\n## 功能边界总结\\n\\n**主要变更:**\\n1. **后台任务系统** (\`background/\`) - 全新模块,支持后台 bash 任务\\n2. **通知系统** (\`notifications/\`) - 独立基建,支持多 sink (llm/wire/shell)\\n3. **新工具** - \`TaskList\` / \`TaskOutput\` / \`TaskStop\`\\n4. **Shell 工具更新** - 新增 \`run_in_background\` + \`description\` 参数\\n5. **Wire 协议** - 新增 \`Notification\` 事件类型\\n6. **KimiSoul 集成** - 自动 claim/ack notification,compaction 后恢复活跃任务快照\\n\\n**测试范围:**\\n- 后台任务生命周期(创建→运行→完成→通知)\\n- 任务控制工具(List/Output/Stop)\\n- 通知系统(去重、claim/ack、多 sink)\\n- 集成点(Shell 后台模式、Wire 事件、LLM ... [truncated]"}],"toolCalls":[{"type":"function","id":"tool_y3SXWWQIUysddnYoklaWhUeE","function":{"name":"SetTodoList","arguments":"{\\"todos\\": [{\\"title\\": \\"准备测试环境(创建隔离 work-dir)\\", \\"status\\": \\"in_progress\\"}, {\\"title\\": \\"探索轮:验证工具可用性和接口\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"执行轮1:正常路径 - 后台任务生命周期\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"执行轮2:边界条件 - 超时和停止\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"执行轮3:中断场景 - TaskStop 和恢复\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"观察轮:检查 session 产物\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"汇报结论\\", \\"status\\": \\"pending\\"}]}"}},{"type":"function","id":"tool_BH2zd6kVzHhV4pIjKLSmrUeB","function":{"name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"$(mktemp -d /tmp/kimi-smoke-XXXXXX)\\\\\\"\\\\necho \\\\\\"SMOKE_DIR=$SMOKE_DIR\\\\\\"\\\\n# 验证 CLI 可运行\\\\nuv run python -m example_pkg.cli --help 2>&1 | head -20\\"}"}}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"现在我对功能有了完整的理解。让我总结一下测试范围:\\n\\n**功能边界:**\\n1. **后台任务系统** - 创建、监控、停止、输出获取\\n2. **通知系统** - 发布、消费、去重、确认\\n3. **集成点** - Shell 后台模式、TaskList/TaskOutput/TaskStop 工具、Wire 通知事件、LLM 通知消息\\n\\n**用户可感知的行为变化:**\\n1. Shell 工具新增 \`run_in_background\` 和 \`description\` 参数\\n2. 新增 TaskList/TaskOutput/TaskStop 工具\\n3. 后台任务完成后自动发送通知到 LLM context 和 Wire\\n4. 通知通过独立的 \`\` 消息进入 context\\n5. Wire 协议新增 \`Notification\` 事件类型\\n\\n**测试计划:**\\n1. **正常路径** - 启动后台任务,等待完成,验证通知\\n2. **边界条件** - 超时处理、重复停止、无效 task_id\\n3. **中断/清理** - 主动停止任务、任务丢失恢复\\n\\n让我开始制定... [truncated]"},{"type":"text","text":"现在我对功能有了完整理解。让我制定测试计划:\\n\\n## 功能边界总结\\n\\n**主要变更:**\\n1. **后台任务系统** (\`background/\`) - 全新模块,支持后台 bash 任务\\n2. **通知系统** (\`notifications/\`) - 独立基建,支持多 sink (llm/wire/shell)\\n3. **新工具** - \`TaskList\` / \`TaskOutput\` / \`TaskStop\`\\n4. **Shell 工具更新** - 新增 \`run_in_background\` + \`description\` 参数\\n5. **Wire 协议** - 新增 \`Notification\` 事件类型\\n6. **KimiSoul 集成** - 自动 claim/ack notification,compaction 后恢复活跃任务快照\\n\\n**测试范围:**\\n- 后台任务生命周期(创建→运行→完成→通知)\\n- 任务控制工具(List/Output/Stop)\\n- 通知系统(去重、claim/ack、多 sink)\\n- 集成点(Shell 后台模式、Wire 事件、LLM ... [truncated]"}],"toolCalls":[{"type":"function","id":"tool_y3SXWWQIUysddnYoklaWhUeE","name":"SetTodoList","arguments":"{\\"todos\\": [{\\"title\\": \\"准备测试环境(创建隔离 work-dir)\\", \\"status\\": \\"in_progress\\"}, {\\"title\\": \\"探索轮:验证工具可用性和接口\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"执行轮1:正常路径 - 后台任务生命周期\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"执行轮2:边界条件 - 超时和停止\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"执行轮3:中断场景 - TaskStop 和恢复\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"观察轮:检查 session 产物\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"汇报结论\\", \\"status\\": \\"pending\\"}]}"},{"type":"function","id":"tool_BH2zd6kVzHhV4pIjKLSmrUeB","name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"$(mktemp -d /tmp/kimi-smoke-XXXXXX)\\\\\\"\\\\necho \\\\\\"SMOKE_DIR=$SMOKE_DIR\\\\\\"\\\\n# 验证 CLI 可运行\\\\nuv run python -m example_pkg.cli --help 2>&1 | head -20\\"}"}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Todo list updated"}],"toolCalls":[],"toolCallId":"tool_y3SXWWQIUysddnYoklaWhUeE"}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Command executed successfully."},{"type":"text","text":"SMOKE_DIR=/tmp/kimi-smoke-a2i1xj\\n \\n Usage: python -m example_pkg.cli [OPTIONS] COMMAND [ARGS]... \\n \\n The Starting Point for Next-Gen Agents \\n \\n╭─ Options ───────────────────────────────────────────────────... [truncated]"}],"toolCalls":[],"toolCallId":"tool_BH2zd6kVzHhV4pIjKLSmrUeB"}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"环境准备好了,现在让我开始第一轮探索测试,验证工具可用性。"}],"toolCalls":[{"type":"function","id":"tool_mj72EeaBIxBi3NOVwE3tjxBc","function":{"name":"SetTodoList","arguments":"{\\"todos\\": [{\\"title\\": \\"准备测试环境(创建隔离 work-dir)\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"探索轮:验证工具可用性和接口\\", \\"status\\": \\"in_progress\\"}, {\\"title\\": \\"执行轮1:正常路径 - 后台任务生命周期\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"执行轮2:边界条件 - 超时和停止\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"执行轮3:中断场景 - TaskStop 和恢复\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"观察轮:检查 session 产物\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"汇报结论\\", \\"status\\": \\"pending\\"}]}"}},{"type":"function","id":"tool_ocohNUErNzzNQnYsibl670ms","function":{"name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\n\\\\n# 探索轮:验证后台任务相关工具是否可用\\\\nuv run python -m example_pkg.cli \\\\\\\\\\\\n --print \\\\\\\\\\\\n --prompt \\\\\\"请列出你当前所有可用的工具,特别关注:\\\\n1. 是否有 TaskList、TaskOutput、TaskStop 这三个工具?\\\\n2. Shell 工具是否支持 run_in_background 参数?\\\\n3. 简单描述这些工具的功能。\\\\\\" \\\\\\\\\\\\n --work-dir \\\\\\"$SMOKE_DIR\\\\\\" 2>&1\\\\necho \\\\\\"exit_code=$?\\\\\\"\\", \\"timeout\\": 120}"}}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"环境准备好了,现在让我开始第一轮探索测试,验证工具可用性。"}],"toolCalls":[{"type":"function","id":"tool_mj72EeaBIxBi3NOVwE3tjxBc","name":"SetTodoList","arguments":"{\\"todos\\": [{\\"title\\": \\"准备测试环境(创建隔离 work-dir)\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"探索轮:验证工具可用性和接口\\", \\"status\\": \\"in_progress\\"}, {\\"title\\": \\"执行轮1:正常路径 - 后台任务生命周期\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"执行轮2:边界条件 - 超时和停止\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"执行轮3:中断场景 - TaskStop 和恢复\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"观察轮:检查 session 产物\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"汇报结论\\", \\"status\\": \\"pending\\"}]}"},{"type":"function","id":"tool_ocohNUErNzzNQnYsibl670ms","name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\n\\\\n# 探索轮:验证后台任务相关工具是否可用\\\\nuv run python -m example_pkg.cli \\\\\\\\\\\\n --print \\\\\\\\\\\\n --prompt \\\\\\"请列出你当前所有可用的工具,特别关注:\\\\n1. 是否有 TaskList、TaskOutput、TaskStop 这三个工具?\\\\n2. Shell 工具是否支持 run_in_background 参数?\\\\n3. 简单描述这些工具的功能。\\\\\\" \\\\\\\\\\\\n --work-dir \\\\\\"$SMOKE_DIR\\\\\\" 2>&1\\\\necho \\\\\\"exit_code=$?\\\\\\"\\", \\"timeout\\": 120}"}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Todo list updated"}],"toolCalls":[],"toolCallId":"tool_mj72EeaBIxBi3NOVwE3tjxBc"}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Command executed successfully."},{"type":"text","text":"请列出你当前所有可用的工具,特别关注:\\n1. 是否有 TaskList、TaskOutput、TaskStop 这三个工具?\\n2. Shell 工具是否支持 run_in_background 参数?\\n3. 简单描述这些工具的功能。\\nTurnBegin(\\n user_input='请列出你当前所有可用的工具,特别关注:\\\\n1. 是否有 \\nTaskList、TaskOutput、TaskStop 这三个工具?\\\\n2. Shell 工具是否支持 \\nrun_in_background 参数?\\\\n3. 简单描述这些工具的功能。'\\n)\\nStepBegin(n=1)\\nThinkPart(\\n type='think',\\n think='用户询问我当前可用的工具列表,特别关注三个问题:\\\\n1. 是否有 \\nTaskList、TaskOutput、TaskStop 这三个工具?\\\\n2. Shell 工具是否支持 \\nrun_in_background 参数?\\\\n3. \\n简单描述这些工具的功能。\\\\n\\\\n我需要查看我的工具列表。从 system \\n提示中,我可以看到我有以下工具:\\\\n- Tas... [truncated]"}],"toolCalls":[],"toolCallId":"tool_ocohNUErNzzNQnYsibl670ms"}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"探索轮成功,工具都已正确加载。现在进入执行轮1:测试正常路径 - 启动后台任务,等待完成,验证通知。"},{"type":"text","text":"探索轮成功!工具都已正确加载。现在执行第一轮测试:"}],"toolCalls":[{"type":"function","id":"tool_bpOwCgVz6wYah52SnaJB39ZW","function":{"name":"SetTodoList","arguments":"{\\"todos\\": [{\\"title\\": \\"准备测试环境(创建隔离 work-dir)\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"探索轮:验证工具可用性和接口\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮1:正常路径 - 后台任务生命周期\\", \\"status\\": \\"in_progress\\"}, {\\"title\\": \\"执行轮2:边界条件 - 超时和停止\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"执行轮3:中断场景 - TaskStop 和恢复\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"观察轮:检查 session 产物\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"汇报结论\\", \\"status\\": \\"pending\\"}]}"}},{"type":"function","id":"tool_HejoTo6Izmlets9y9hN9u1pZ","function":{"name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\n\\\\n# 执行轮1:正常路径 - 启动一个短生命周期的后台任务,验证生命周期完整\\\\nuv run python -m example_pkg.cli \\\\\\\\\\\\n --print \\\\\\\\\\\\n --prompt \\\\\\"请执行以下测试:\\\\n\\\\n1. 使用 Shell 工具启动一个后台任务:\\\\n - 命令: echo 'hello from background' && sleep 2 && echo 'task done'\\\\n - run_in_background: true\\\\n - description: '测试后台任务生命周期'\\\\n - timeout: 30\\\\n\\\\n2. 记录返回的 task_id\\\\n\\\\n3. 使用 TaskList 查看当前活跃任务\\\\n\\\\n4. 使用 TaskOutput 获取任务输出(block=true, timeout=10)\\\\n\\\\n请报告:task_id、任务状态、输出内容。\\\\\\" \\\\\\\\\\\\n --work-dir \\\\\\"$SMOKE_DIR\\\\\\" 2>&... [truncated]"}}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"探索轮成功,工具都已正确加载。现在进入执行轮1:测试正常路径 - 启动后台任务,等待完成,验证通知。"},{"type":"text","text":"探索轮成功!工具都已正确加载。现在执行第一轮测试:"}],"toolCalls":[{"type":"function","id":"tool_bpOwCgVz6wYah52SnaJB39ZW","name":"SetTodoList","arguments":"{\\"todos\\": [{\\"title\\": \\"准备测试环境(创建隔离 work-dir)\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"探索轮:验证工具可用性和接口\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮1:正常路径 - 后台任务生命周期\\", \\"status\\": \\"in_progress\\"}, {\\"title\\": \\"执行轮2:边界条件 - 超时和停止\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"执行轮3:中断场景 - TaskStop 和恢复\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"观察轮:检查 session 产物\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"汇报结论\\", \\"status\\": \\"pending\\"}]}"},{"type":"function","id":"tool_HejoTo6Izmlets9y9hN9u1pZ","name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\n\\\\n# 执行轮1:正常路径 - 启动一个短生命周期的后台任务,验证生命周期完整\\\\nuv run python -m example_pkg.cli \\\\\\\\\\\\n --print \\\\\\\\\\\\n --prompt \\\\\\"请执行以下测试:\\\\n\\\\n1. 使用 Shell 工具启动一个后台任务:\\\\n - 命令: echo 'hello from background' && sleep 2 && echo 'task done'\\\\n - run_in_background: true\\\\n - description: '测试后台任务生命周期'\\\\n - timeout: 30\\\\n\\\\n2. 记录返回的 task_id\\\\n\\\\n3. 使用 TaskList 查看当前活跃任务\\\\n\\\\n4. 使用 TaskOutput 获取任务输出(block=true, timeout=10)\\\\n\\\\n请报告:task_id、任务状态、输出内容。\\\\\\" \\\\\\\\\\\\n --work-dir \\\\\\"$SMOKE_DIR\\\\\\" 2>&... [truncated]"}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Todo list updated"}],"toolCalls":[],"toolCallId":"tool_bpOwCgVz6wYah52SnaJB39ZW"}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Command executed successfully."},{"type":"text","text":"请执行以下测试:\\n\\n1. 使用 Shell 工具启动一个后台任务:\\n - 命令: echo 'hello from background' && sleep 2 && echo 'task done'\\n - run_in_background: true\\n - description: '测试后台任务生命周期'\\n - timeout: 30\\n\\n2. 记录返回的 task_id\\n\\n3. 使用 TaskList 查看当前活跃任务\\n\\n4. 使用 TaskOutput 获取任务输出(block=true, timeout=10)\\n\\n请报告:task_id、任务状态、输出内容。\\nTurnBegin(\\n user_input=\\"请执行以下测试:\\\\n\\\\n1. 使用 Shell 工具启动一个后台任务:\\\\n - \\n命令: echo 'hello from background' && sleep 2 && echo 'task done'\\\\n - \\nrun_in_background: true\\\\n - description: '测试后台任务生命周期'\\\\n - t... [truncated]"}],"toolCalls":[],"toolCallId":"tool_HejoTo6Izmlets9y9hN9u1pZ"}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"第一轮测试成功!关键验证点:\\n1. ✅ Shell 后台任务启动成功,返回 task_id\\n2. ✅ TaskList 正确工作\\n3. ✅ TaskOutput 正确工作,block=true 等待完成\\n4. ✅ Notification 事件正确发送(Wire 中可见 Notification 事件)\\n\\n现在进行第二轮:边界条件 - 测试超时和停止功能。"},{"type":"text","text":"第一轮成功!所有核心功能正常工作。现在测试边界条件:"}],"toolCalls":[{"type":"function","id":"tool_3tueNNJt54oG2X47QK4PLnTj","function":{"name":"SetTodoList","arguments":"{\\"todos\\": [{\\"title\\": \\"准备测试环境(创建隔离 work-dir)\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"探索轮:验证工具可用性和接口\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮1:正常路径 - 后台任务生命周期\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮2:边界条件 - 超时和停止\\", \\"status\\": \\"in_progress\\"}, {\\"title\\": \\"执行轮3:中断场景 - TaskStop 和恢复\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"观察轮:检查 session 产物\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"汇报结论\\", \\"status\\": \\"pending\\"}]}"}},{"type":"function","id":"tool_NT90qPui3vOOBqIjmTgvYe9Q","function":{"name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\n\\\\n# 执行轮2:边界条件 - 超时处理和停止\\\\nuv run python -m example_pkg.cli \\\\\\\\\\\\n --print \\\\\\\\\\\\n --prompt \\\\\\"请执行以下边界条件测试:\\\\n\\\\n1. 启动一个长时间运行的后台任务(会被停止):\\\\n - 命令: for i in 1 2 3 4 5; do echo \\\\\\\\\\\\\\"line \\\\\\\\$i\\\\\\\\\\\\\\"; sleep 5; done\\\\n - run_in_background: true\\\\n - description: '测试停止功能'\\\\n - timeout: 60\\\\n\\\\n2. 使用 TaskList 确认任务在运行中\\\\n\\\\n3. 等待 3 秒后,使用 TaskStop 停止该任务\\\\n\\\\n4. 使用 TaskOutput 获取任务最终状态和输出\\\\n\\\\n请报告:task_id、停止前后的状态、输出内容、是否有正确通知。\\\\\\" \\\\\\\\\\\\n --work-dir \\\\\\"$SMOKE_DIR\\\\\\" 2>&1\\\\necho \\\\\\"... [truncated]"}}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"第一轮测试成功!关键验证点:\\n1. ✅ Shell 后台任务启动成功,返回 task_id\\n2. ✅ TaskList 正确工作\\n3. ✅ TaskOutput 正确工作,block=true 等待完成\\n4. ✅ Notification 事件正确发送(Wire 中可见 Notification 事件)\\n\\n现在进行第二轮:边界条件 - 测试超时和停止功能。"},{"type":"text","text":"第一轮成功!所有核心功能正常工作。现在测试边界条件:"}],"toolCalls":[{"type":"function","id":"tool_3tueNNJt54oG2X47QK4PLnTj","name":"SetTodoList","arguments":"{\\"todos\\": [{\\"title\\": \\"准备测试环境(创建隔离 work-dir)\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"探索轮:验证工具可用性和接口\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮1:正常路径 - 后台任务生命周期\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮2:边界条件 - 超时和停止\\", \\"status\\": \\"in_progress\\"}, {\\"title\\": \\"执行轮3:中断场景 - TaskStop 和恢复\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"观察轮:检查 session 产物\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"汇报结论\\", \\"status\\": \\"pending\\"}]}"},{"type":"function","id":"tool_NT90qPui3vOOBqIjmTgvYe9Q","name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\n\\\\n# 执行轮2:边界条件 - 超时处理和停止\\\\nuv run python -m example_pkg.cli \\\\\\\\\\\\n --print \\\\\\\\\\\\n --prompt \\\\\\"请执行以下边界条件测试:\\\\n\\\\n1. 启动一个长时间运行的后台任务(会被停止):\\\\n - 命令: for i in 1 2 3 4 5; do echo \\\\\\\\\\\\\\"line \\\\\\\\$i\\\\\\\\\\\\\\"; sleep 5; done\\\\n - run_in_background: true\\\\n - description: '测试停止功能'\\\\n - timeout: 60\\\\n\\\\n2. 使用 TaskList 确认任务在运行中\\\\n\\\\n3. 等待 3 秒后,使用 TaskStop 停止该任务\\\\n\\\\n4. 使用 TaskOutput 获取任务最终状态和输出\\\\n\\\\n请报告:task_id、停止前后的状态、输出内容、是否有正确通知。\\\\\\" \\\\\\\\\\\\n --work-dir \\\\\\"$SMOKE_DIR\\\\\\" 2>&1\\\\necho \\\\\\"... [truncated]"}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Todo list updated"}],"toolCalls":[],"toolCallId":"tool_3tueNNJt54oG2X47QK4PLnTj"}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Command executed successfully."},{"type":"text","text":"请执行以下边界条件测试:\\n\\n1. 启动一个长时间运行的后台任务(会被停止):\\n - 命令: for i in 1 2 3 4 5; do echo \\"line $i\\"; sleep 5; done\\n - run_in_background: true\\n - description: '测试停止功能'\\n - timeout: 60\\n\\n2. 使用 TaskList 确认任务在运行中\\n\\n3. 等待 3 秒后,使用 TaskStop 停止该任务\\n\\n4. 使用 TaskOutput 获取任务最终状态和输出\\n\\n请报告:task_id、停止前后的状态、输出内容、是否有正确通知。\\nTurnBegin(\\n user_input='请执行以下边界条件测试:\\\\n\\\\n1. \\n启动一个长时间运行的后台任务(会被停止):\\\\n - 命令: for i in 1 2 3 4 5; do \\necho \\"line $i\\"; sleep 5; done\\\\n - run_in_background: true\\\\n - description: \\n\\\\'测试停止功能\\\\'\\\\n - timeou... [truncated]"}],"toolCalls":[],"toolCallId":"tool_NT90qPui3vOOBqIjmTgvYe9Q"}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"第二轮也成功!测试了停止功能,验证了:\\n1. ✅ TaskStop 成功停止任务\\n2. ✅ 通知正确发送 (task.killed)\\n3. ✅ exit_code 正确 (-15 = SIGTERM)\\n4. ✅ 输出正确捕获\\n\\n现在进行第三轮测试:测试超时处理。"},{"type":"text","text":"第二轮成功!停止功能完全正常。现在测试超时场景:"}],"toolCalls":[{"type":"function","id":"tool_TS5JUSZELJoTVcos6rp90ZUT","function":{"name":"SetTodoList","arguments":"{\\"todos\\": [{\\"title\\": \\"准备测试环境(创建隔离 work-dir)\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"探索轮:验证工具可用性和接口\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮1:正常路径 - 后台任务生命周期\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮2:边界条件 - 超时和停止\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮3:超时场景测试\\", \\"status\\": \\"in_progress\\"}, {\\"title\\": \\"观察轮:检查 session 产物\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"汇报结论\\", \\"status\\": \\"pending\\"}]}"}},{"type":"function","id":"tool_J3cQ5m3qyTep0zNLWhZR3OGg","function":{"name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\n\\\\n# 执行轮3:测试超时处理\\\\nuv run python -m example_pkg.cli \\\\\\\\\\\\n --print \\\\\\\\\\\\n --prompt \\\\\\"请测试后台任务超时功能:\\\\n\\\\n1. 启动一个会超时的后台任务:\\\\n - 命令: sleep 300\\\\n - run_in_background: true\\\\n - description: '测试超时处理'\\\\n - timeout: 3 (设置为3秒,确保会超时)\\\\n\\\\n2. 使用 TaskOutput 阻塞等待任务结束(block=true, timeout=15)\\\\n\\\\n3. 检查任务最终状态是否为 timeout\\\\n\\\\n请报告:task_id、最终状态、exit_code、是否有 timeout 通知。\\\\\\" \\\\\\\\\\\\n --work-dir \\\\\\"$SMOKE_DIR\\\\\\" 2>&1\\\\necho \\\\\\"exit_code=$?\\\\\\"\\", \\"timeout\\": 180}"}}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"第二轮也成功!测试了停止功能,验证了:\\n1. ✅ TaskStop 成功停止任务\\n2. ✅ 通知正确发送 (task.killed)\\n3. ✅ exit_code 正确 (-15 = SIGTERM)\\n4. ✅ 输出正确捕获\\n\\n现在进行第三轮测试:测试超时处理。"},{"type":"text","text":"第二轮成功!停止功能完全正常。现在测试超时场景:"}],"toolCalls":[{"type":"function","id":"tool_TS5JUSZELJoTVcos6rp90ZUT","name":"SetTodoList","arguments":"{\\"todos\\": [{\\"title\\": \\"准备测试环境(创建隔离 work-dir)\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"探索轮:验证工具可用性和接口\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮1:正常路径 - 后台任务生命周期\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮2:边界条件 - 超时和停止\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮3:超时场景测试\\", \\"status\\": \\"in_progress\\"}, {\\"title\\": \\"观察轮:检查 session 产物\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"汇报结论\\", \\"status\\": \\"pending\\"}]}"},{"type":"function","id":"tool_J3cQ5m3qyTep0zNLWhZR3OGg","name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\n\\\\n# 执行轮3:测试超时处理\\\\nuv run python -m example_pkg.cli \\\\\\\\\\\\n --print \\\\\\\\\\\\n --prompt \\\\\\"请测试后台任务超时功能:\\\\n\\\\n1. 启动一个会超时的后台任务:\\\\n - 命令: sleep 300\\\\n - run_in_background: true\\\\n - description: '测试超时处理'\\\\n - timeout: 3 (设置为3秒,确保会超时)\\\\n\\\\n2. 使用 TaskOutput 阻塞等待任务结束(block=true, timeout=15)\\\\n\\\\n3. 检查任务最终状态是否为 timeout\\\\n\\\\n请报告:task_id、最终状态、exit_code、是否有 timeout 通知。\\\\\\" \\\\\\\\\\\\n --work-dir \\\\\\"$SMOKE_DIR\\\\\\" 2>&1\\\\necho \\\\\\"exit_code=$?\\\\\\"\\", \\"timeout\\": 180}"}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Todo list updated"}],"toolCalls":[],"toolCallId":"tool_TS5JUSZELJoTVcos6rp90ZUT"}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Command executed successfully."},{"type":"text","text":"请测试后台任务超时功能:\\n\\n1. 启动一个会超时的后台任务:\\n - 命令: sleep 300\\n - run_in_background: true\\n - description: '测试超时处理'\\n - timeout: 3 (设置为3秒,确保会超时)\\n\\n2. 使用 TaskOutput 阻塞等待任务结束(block=true, timeout=15)\\n\\n3. 检查任务最终状态是否为 timeout\\n\\n请报告:task_id、最终状态、exit_code、是否有 timeout 通知。\\nTurnBegin(\\n user_input=\\"请测试后台任务超时功能:\\\\n\\\\n1. 启动一个会超时的后台任务:\\\\n - \\n命令: sleep 300\\\\n - run_in_background: true\\\\n - description: \\n'测试超时处理'\\\\n - timeout: 3 (设置为3秒,确保会超时)\\\\n\\\\n2. 使用 TaskOutput\\n阻塞等待任务结束(block=true, timeout=15)\\\\n\\\\n3. 检查任务最终状态是否为 \\n... [truncated]"}],"toolCalls":[],"toolCallId":"tool_J3cQ5m3qyTep0zNLWhZR3OGg"}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"第三轮也成功!测试了超时处理:\\n1. ✅ 任务正确超时\\n2. ✅ 状态为 failed,timed_out=true\\n3. ✅ 通知正确发送 (task.timed_out)\\n4. ✅ exit_code 正确 (-15)\\n\\n现在进入观察轮,检查 session 产物,验证 notification 是否正确写入 context 等。"},{"type":"text","text":"第三轮成功!超时处理完全正常。现在进入观察轮,检查 session 产物:"}],"toolCalls":[{"type":"function","id":"tool_n7Bz3OSHPlOZnL3ghEe8Rciw","function":{"name":"SetTodoList","arguments":"{\\"todos\\": [{\\"title\\": \\"准备测试环境(创建隔离 work-dir)\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"探索轮:验证工具可用性和接口\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮1:正常路径 - 后台任务生命周期\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮2:边界条件 - 超时和停止\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮3:超时场景测试\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"观察轮:检查 session 产物\\", \\"status\\": \\"in_progress\\"}, {\\"title\\": \\"汇报结论\\", \\"status\\": \\"pending\\"}]}"}},{"type":"function","id":"tool_3s3qLBo5c7kA1pqbpX36YbVo","function":{"name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\n\\\\n# 查找 session 目录\\\\necho \\\\\\"=== Session 定位 ===\\\\\\"\\\\nSESSION_ID=$(echo -n \\\\\\"$SMOKE_DIR\\\\\\" | md5)\\\\necho \\\\\\"Work dir: $SMOKE_DIR\\\\\\"\\\\necho \\\\\\"Session ID (md5): $SESSION_ID\\\\\\"\\\\nSESSION_DIR=\\\\\\"$HOME/.kimi/sessions/$SESSION_ID\\\\\\"\\\\necho \\\\\\"Session dir: $SESSION_DIR\\\\\\"\\\\n\\\\n# 检查 session 目录是否存在\\\\nif [ -d \\\\\\"$SESSION_DIR\\\\\\" ]; then\\\\n echo \\\\\\"Session exists: YES\\\\\\"\\\\n ls -la \\\\\\"$SESSION_DIR\\\\\\"\\\\nelse\\\\n echo \\\\\\"Session exists: NO\\\\\\"\\\\n # 尝试找到最新的 session\\\\n echo \\\\\\"Finding... [truncated]"}}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"第三轮也成功!测试了超时处理:\\n1. ✅ 任务正确超时\\n2. ✅ 状态为 failed,timed_out=true\\n3. ✅ 通知正确发送 (task.timed_out)\\n4. ✅ exit_code 正确 (-15)\\n\\n现在进入观察轮,检查 session 产物,验证 notification 是否正确写入 context 等。"},{"type":"text","text":"第三轮成功!超时处理完全正常。现在进入观察轮,检查 session 产物:"}],"toolCalls":[{"type":"function","id":"tool_n7Bz3OSHPlOZnL3ghEe8Rciw","name":"SetTodoList","arguments":"{\\"todos\\": [{\\"title\\": \\"准备测试环境(创建隔离 work-dir)\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"探索轮:验证工具可用性和接口\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮1:正常路径 - 后台任务生命周期\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮2:边界条件 - 超时和停止\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮3:超时场景测试\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"观察轮:检查 session 产物\\", \\"status\\": \\"in_progress\\"}, {\\"title\\": \\"汇报结论\\", \\"status\\": \\"pending\\"}]}"},{"type":"function","id":"tool_3s3qLBo5c7kA1pqbpX36YbVo","name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\n\\\\n# 查找 session 目录\\\\necho \\\\\\"=== Session 定位 ===\\\\\\"\\\\nSESSION_ID=$(echo -n \\\\\\"$SMOKE_DIR\\\\\\" | md5)\\\\necho \\\\\\"Work dir: $SMOKE_DIR\\\\\\"\\\\necho \\\\\\"Session ID (md5): $SESSION_ID\\\\\\"\\\\nSESSION_DIR=\\\\\\"$HOME/.kimi/sessions/$SESSION_ID\\\\\\"\\\\necho \\\\\\"Session dir: $SESSION_DIR\\\\\\"\\\\n\\\\n# 检查 session 目录是否存在\\\\nif [ -d \\\\\\"$SESSION_DIR\\\\\\" ]; then\\\\n echo \\\\\\"Session exists: YES\\\\\\"\\\\n ls -la \\\\\\"$SESSION_DIR\\\\\\"\\\\nelse\\\\n echo \\\\\\"Session exists: NO\\\\\\"\\\\n # 尝试找到最新的 session\\\\n echo \\\\\\"Finding... [truncated]"}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Todo list updated"}],"toolCalls":[],"toolCallId":"tool_n7Bz3OSHPlOZnL3ghEe8Rciw"}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Command executed successfully."},{"type":"text","text":"=== Session 定位 ===\\nWork dir: /tmp/kimi-smoke-a2i1xj\\nSession ID (md5): d1c3750c0cc0757aeac04ad5a7d6b8cf\\nSession dir: /Users/example/proj/.kimi/sessions/d1c3750c0cc0757aeac04ad5a7d6b8cf\\nSession exists: YES\\ntotal 0\\ndrwxr-xr-x@ 6 user staff 192 3月 17 12:59 .\\ndrwxr-xr-x@ 1722 user staff 55104 3月 17 12:58 ..\\ndrwxr-xr-x@ 6 user staff 192 3月 17 12:58 0e1d3b94-dae2-4dfb-b36d-717397f4d2f6\\ndrwxr-xr-x@ 6 user staff 192 3月 17 12:59 228b54e2-5b5d-4675-94f2-ec56e42cdbae\\nd... [truncated]"}],"toolCalls":[],"toolCallId":"tool_3s3qLBo5c7kA1pqbpX36YbVo"}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"找到了 session 目录,下面有多个子目录(每次运行 CLI 会创建一个新的子 session)。让我使用 inspect_session.py 来检查最新的 session。"}],"toolCalls":[{"type":"function","id":"tool_4Zcuo5i4xl2NEz1Y1EZ1IvSn","function":{"name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\n\\\\n# 使用 inspect_session.py 检查最新的 session\\\\necho \\\\\\"=== 使用 inspect_session.py 检查 ===\\\\\\"\\\\nuv run python .agents/skills/example-task/scripts/inspect_session.py --share-dir ~/.kimi 2>&1\\"}"}}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"找到了 session 目录,下面有多个子目录(每次运行 CLI 会创建一个新的子 session)。让我使用 inspect_session.py 来检查最新的 session。"}],"toolCalls":[{"type":"function","id":"tool_4Zcuo5i4xl2NEz1Y1EZ1IvSn","name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\n\\\\n# 使用 inspect_session.py 检查最新的 session\\\\necho \\\\\\"=== 使用 inspect_session.py 检查 ===\\\\\\"\\\\nuv run python .agents/skills/example-task/scripts/inspect_session.py --share-dir ~/.kimi 2>&1\\"}"}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Command executed successfully."},{"type":"text","text":"=== 使用 inspect_session.py 检查 ===\\nSession dir: /Users/example/proj/.kimi/sessions/6a7967e2a97cd71a62b8375c5ae461ba/ef00f9f9-a470-4e50-b279-59429da67d27\\n\\n== Files ==\\ncontext.jsonl (209039 bytes)\\nstate.json (188 bytes)\\nwire.jsonl (197310 bytes)\\n\\n== Context ==\\n/Users/example/proj/.kimi/sessions/6a7967e2a97cd71a62b8375c5ae461ba/ef00f9f9-a470-4e50-b279-59429da67d27/context.jsonl\\nrecords: 85\\nroles: _checkpoint=16, _system_prompt=1, _usage=27, assistant=13, tool=26, user=2\\n[74] role=assistant | tools=SetTodoLis... [truncated]"}],"toolCalls":[],"toolCallId":"tool_4Zcuo5i4xl2NEz1Y1EZ1IvSn"}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"这个 session 不是我刚才测试的那个。让我直接检查正确的 session 目录(d1c3750c0cc0757aeac04ad5a7d6b8cf),里面有多个子目录,对应三次测试运行。"}],"toolCalls":[{"type":"function","id":"tool_DmwmxRwjKGDXt1PTAyVbYAdM","function":{"name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\nSESSION_ID=$(echo -n \\\\\\"$SMOKE_DIR\\\\\\" | md5)\\\\nSESSION_DIR=\\\\\\"$HOME/.kimi/sessions/$SESSION_ID\\\\\\"\\\\n\\\\necho \\\\\\"=== 检查正确的 session 目录 ===\\\\\\"\\\\necho \\\\\\"Session dir: $SESSION_DIR\\\\\\"\\\\necho \\\\\\"\\\\\\"\\\\necho \\\\\\"=== 子目录列表 ===\\\\\\"\\\\nls -la \\\\\\"$SESSION_DIR\\\\\\"\\\\n\\\\n# 检查每个子目录的 wire.jsonl 中的 Notification 事件\\\\necho \\\\\\"\\\\\\"\\\\necho \\\\\\"=== 检查 Notification 事件 ===\\\\\\"\\\\nfor subdir in \\\\\\"$SESSION_DIR\\\\\\"/*; do\\\\n if [ -d \\\\\\"$subdir\\\\\\" ] && [ -f \\\\\\"$subdir/wire.jsonl\\\\\\" ]; then\\\\n subname=$(basename... [truncated]"}}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"这个 session 不是我刚才测试的那个。让我直接检查正确的 session 目录(d1c3750c0cc0757aeac04ad5a7d6b8cf),里面有多个子目录,对应三次测试运行。"}],"toolCalls":[{"type":"function","id":"tool_DmwmxRwjKGDXt1PTAyVbYAdM","name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\nSESSION_ID=$(echo -n \\\\\\"$SMOKE_DIR\\\\\\" | md5)\\\\nSESSION_DIR=\\\\\\"$HOME/.kimi/sessions/$SESSION_ID\\\\\\"\\\\n\\\\necho \\\\\\"=== 检查正确的 session 目录 ===\\\\\\"\\\\necho \\\\\\"Session dir: $SESSION_DIR\\\\\\"\\\\necho \\\\\\"\\\\\\"\\\\necho \\\\\\"=== 子目录列表 ===\\\\\\"\\\\nls -la \\\\\\"$SESSION_DIR\\\\\\"\\\\n\\\\n# 检查每个子目录的 wire.jsonl 中的 Notification 事件\\\\necho \\\\\\"\\\\\\"\\\\necho \\\\\\"=== 检查 Notification 事件 ===\\\\\\"\\\\nfor subdir in \\\\\\"$SESSION_DIR\\\\\\"/*; do\\\\n if [ -d \\\\\\"$subdir\\\\\\" ] && [ -f \\\\\\"$subdir/wire.jsonl\\\\\\" ]; then\\\\n subname=$(basename... [truncated]"}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Command executed successfully."},{"type":"text","text":"=== 检查正确的 session 目录 ===\\nSession dir: /Users/example/proj/.kimi/sessions/d1c3750c0cc0757aeac04ad5a7d6b8cf\\n\\n=== 子目录列表 ===\\ntotal 0\\ndrwxr-xr-x@ 6 user staff 192 3月 17 12:59 .\\ndrwxr-xr-x@ 1722 user staff 55104 3月 17 12:58 ..\\ndrwxr-xr-x@ 6 user staff 192 3月 17 12:58 0e1d3b94-dae2-4dfb-b36d-717397f4d2f6\\ndrwxr-xr-x@ 6 user staff 192 3月 17 12:59 228b54e2-5b5d-4675-94f2-ec56e42cdbae\\ndrwxr-xr-x@ 6 user staff 192 3月 17 13:00 be8228d5-1c57-43dc-9f23-9e7dba2... [truncated]"}],"toolCalls":[],"toolCallId":"tool_DmwmxRwjKGDXt1PTAyVbYAdM"}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"奇怪,没有找到 Notification 事件。可能 Notification 事件格式不一样,让我直接查看 wire.jsonl 文件内容,搜索 notification。"}],"toolCalls":[{"type":"function","id":"tool_97R6Gz8LI01cAOsQDuv9nbwm","function":{"name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\nSESSION_ID=$(echo -n \\\\\\"$SMOKE_DIR\\\\\\" | md5)\\\\nSESSION_DIR=\\\\\\"$HOME/.kimi/sessions/$SESSION_ID\\\\\\"\\\\n\\\\n# 直接查看每个子目录的 wire.jsonl 内容\\\\necho \\\\\\"=== 查看 wire.jsonl 中的通知相关内容 ===\\\\\\"\\\\nfor subdir in \\\\\\"$SESSION_DIR\\\\\\"/*; do\\\\n if [ -d \\\\\\"$subdir\\\\\\" ] && [ -f \\\\\\"$subdir/wire.jsonl\\\\\\" ]; then\\\\n subname=$(basename \\\\\\"$subdir\\\\\\")\\\\n echo \\\\\\"\\\\\\"\\\\n echo \\\\\\"--- Sub-session: $subname ---\\\\\\"\\\\n # 查找包含 notification 的行\\\\n grep -i \\\\\\"notification\\\\\\\\|task\\\\\\\\.... [truncated]"}}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"奇怪,没有找到 Notification 事件。可能 Notification 事件格式不一样,让我直接查看 wire.jsonl 文件内容,搜索 notification。"}],"toolCalls":[{"type":"function","id":"tool_97R6Gz8LI01cAOsQDuv9nbwm","name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\nSESSION_ID=$(echo -n \\\\\\"$SMOKE_DIR\\\\\\" | md5)\\\\nSESSION_DIR=\\\\\\"$HOME/.kimi/sessions/$SESSION_ID\\\\\\"\\\\n\\\\n# 直接查看每个子目录的 wire.jsonl 内容\\\\necho \\\\\\"=== 查看 wire.jsonl 中的通知相关内容 ===\\\\\\"\\\\nfor subdir in \\\\\\"$SESSION_DIR\\\\\\"/*; do\\\\n if [ -d \\\\\\"$subdir\\\\\\" ] && [ -f \\\\\\"$subdir/wire.jsonl\\\\\\" ]; then\\\\n subname=$(basename \\\\\\"$subdir\\\\\\")\\\\n echo \\\\\\"\\\\\\"\\\\n echo \\\\\\"--- Sub-session: $subname ---\\\\\\"\\\\n # 查找包含 notification 的行\\\\n grep -i \\\\\\"notification\\\\\\\\|task\\\\\\\\.... [truncated]"}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Command executed successfully."},{"type":"text","text":"=== 查看 wire.jsonl 中的通知相关内容 ===\\n\\n--- Sub-session: 0e1d3b94-dae2-4dfb-b36d-717397f4d2f6 ---\\n{\\"timestamp\\": 1773723525.133148, \\"message\\": {\\"type\\": \\"ToolResult\\", \\"payload\\": {\\"tool_call_id\\": \\"tool_KviOIHcT5WgvrVhhHtEzsDNR\\", \\"return_value\\": {\\"is_error\\": false, \\"output\\": \\"task_id: b83rhswvs\\\\nkind: bash\\\\nstatus: starting\\\\ndescription: 测试后台任务生命周期\\\\ncommand: echo 'hello from background' && sleep 2 && echo 'task done'\\\\nautomatic_notification: true\\\\nnext_step: You will be automatically notified when it comple... [truncated]"}],"toolCalls":[],"toolCallId":"tool_97R6Gz8LI01cAOsQDuv9nbwm"}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"我找到了所有的 Notification 事件:\\n1. \`0e1d3b94-dae2-4dfb-b36d-717397f4d2f6\` - task.completed (b83rhswvs)\\n2. \`228b54e2-5b5d-4675-94f2-ec56e42cdbae\` - task.killed (bp6amsrcu)\\n3. \`be8228d5-1c57-43dc-9f23-9e7dba209a1a\` - task.timed_out (b4c3d8fta)\\n\\n现在让我检查 context.jsonl 中是否有 notification 消息,以及 task 目录结构。"},{"type":"text","text":"找到了所有 Notification 事件!现在检查 context.jsonl 和 task 目录:"}],"toolCalls":[{"type":"function","id":"tool_74k2gxGVF3RSOhTaEhnnvPzk","function":{"name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\nSESSION_ID=$(echo -n \\\\\\"$SMOKE_DIR\\\\\\" | md5)\\\\nSESSION_DIR=\\\\\\"$HOME/.kimi/sessions/$SESSION_ID\\\\\\"\\\\n\\\\n# 检查 context.jsonl 中的 notification 消息\\\\necho \\\\\\"=== 检查 context.jsonl 中的 notification 消息 ===\\\\\\"\\\\nfor subdir in \\\\\\"$SESSION_DIR\\\\\\"/*; do\\\\n if [ -d \\\\\\"$subdir\\\\\\" ] && [ -f \\\\\\"$subdir/context.jsonl\\\\\\" ]; then\\\\n subname=$(basename \\\\\\"$subdir\\\\\\")\\\\n count=$(grep -c '\\\\\\"role\\\\\\":\\\\\\"notification\\\\\\"' \\\\\\"$subdir/context.jsonl\\\\\\" 2>/dev/null || echo \\\\\\"0\\\\\\")\\\\n ... [truncated]"}}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"我找到了所有的 Notification 事件:\\n1. \`0e1d3b94-dae2-4dfb-b36d-717397f4d2f6\` - task.completed (b83rhswvs)\\n2. \`228b54e2-5b5d-4675-94f2-ec56e42cdbae\` - task.killed (bp6amsrcu)\\n3. \`be8228d5-1c57-43dc-9f23-9e7dba209a1a\` - task.timed_out (b4c3d8fta)\\n\\n现在让我检查 context.jsonl 中是否有 notification 消息,以及 task 目录结构。"},{"type":"text","text":"找到了所有 Notification 事件!现在检查 context.jsonl 和 task 目录:"}],"toolCalls":[{"type":"function","id":"tool_74k2gxGVF3RSOhTaEhnnvPzk","name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\nSESSION_ID=$(echo -n \\\\\\"$SMOKE_DIR\\\\\\" | md5)\\\\nSESSION_DIR=\\\\\\"$HOME/.kimi/sessions/$SESSION_ID\\\\\\"\\\\n\\\\n# 检查 context.jsonl 中的 notification 消息\\\\necho \\\\\\"=== 检查 context.jsonl 中的 notification 消息 ===\\\\\\"\\\\nfor subdir in \\\\\\"$SESSION_DIR\\\\\\"/*; do\\\\n if [ -d \\\\\\"$subdir\\\\\\" ] && [ -f \\\\\\"$subdir/context.jsonl\\\\\\" ]; then\\\\n subname=$(basename \\\\\\"$subdir\\\\\\")\\\\n count=$(grep -c '\\\\\\"role\\\\\\":\\\\\\"notification\\\\\\"' \\\\\\"$subdir/context.jsonl\\\\\\" 2>/dev/null || echo \\\\\\"0\\\\\\")\\\\n ... [truncated]"}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Command executed successfully."},{"type":"text","text":"=== 检查 context.jsonl 中的 notification 消息 ===\\nSub-session 0e1d3b94-dae2-4dfb-b36d-717397f4d2f6: notification messages = 0\\n0\\n/bin/bash: line 11: [: 0\\n0: integer expression expected\\nSub-session 228b54e2-5b5d-4675-94f2-ec56e42cdbae: notification messages = 0\\n0\\n/bin/bash: line 11: [: 0\\n0: integer expression expected\\nSub-session be8228d5-1c57-43dc-9f23-9e7dba209a1a: notification messages = 0\\n0\\n/bin/bash: line 11: [: 0\\n0: integer expression expected\\nSub-session ea3920e8-2a4b-4e38-be51-c500aab582ab: noti... [truncated]"}],"toolCalls":[],"toolCallId":"tool_74k2gxGVF3RSOhTaEhnnvPzk"}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"找到了 task 目录。现在让我检查 task 目录的内容,以及 notification 持久化目录。"}],"toolCalls":[{"type":"function","id":"tool_doClKbqjLQAuMtimAxF3mufI","function":{"name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\nSESSION_ID=$(echo -n \\\\\\"$SMOKE_DIR\\\\\\" | md5)\\\\nSESSION_DIR=\\\\\\"$HOME/.kimi/sessions/$SESSION_ID\\\\\\"\\\\n\\\\n# 检查 task 目录内容\\\\necho \\\\\\"=== 检查第一个 task 的目录结构 ===\\\\\\"\\\\nTASK_DIR=\\\\\\"$SESSION_DIR/0e1d3b94-dae2-4dfb-b36d-717397f4d2f6/tasks/b83rhswvs\\\\\\"\\\\nls -la \\\\\\"$TASK_DIR\\\\\\"\\\\n\\\\necho \\\\\\"\\\\\\"\\\\necho \\\\\\"=== spec.json ===\\\\\\"\\\\ncat \\\\\\"$TASK_DIR/spec.json\\\\\\"\\\\n\\\\necho \\\\\\"\\\\\\"\\\\necho \\\\\\"=== runtime.json ===\\\\\\"\\\\ncat \\\\\\"$TASK_DIR/runtime.json\\\\\\"\\\\n\\\\necho \\\\\\"\\\\\\"\\\\necho \\\\\\"=== output.log ===\\\\\\"\\\\ncat \\\\\\"$TASK_... [truncated]"}}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"找到了 task 目录。现在让我检查 task 目录的内容,以及 notification 持久化目录。"}],"toolCalls":[{"type":"function","id":"tool_doClKbqjLQAuMtimAxF3mufI","name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\nSESSION_ID=$(echo -n \\\\\\"$SMOKE_DIR\\\\\\" | md5)\\\\nSESSION_DIR=\\\\\\"$HOME/.kimi/sessions/$SESSION_ID\\\\\\"\\\\n\\\\n# 检查 task 目录内容\\\\necho \\\\\\"=== 检查第一个 task 的目录结构 ===\\\\\\"\\\\nTASK_DIR=\\\\\\"$SESSION_DIR/0e1d3b94-dae2-4dfb-b36d-717397f4d2f6/tasks/b83rhswvs\\\\\\"\\\\nls -la \\\\\\"$TASK_DIR\\\\\\"\\\\n\\\\necho \\\\\\"\\\\\\"\\\\necho \\\\\\"=== spec.json ===\\\\\\"\\\\ncat \\\\\\"$TASK_DIR/spec.json\\\\\\"\\\\n\\\\necho \\\\\\"\\\\\\"\\\\necho \\\\\\"=== runtime.json ===\\\\\\"\\\\ncat \\\\\\"$TASK_DIR/runtime.json\\\\\\"\\\\n\\\\necho \\\\\\"\\\\\\"\\\\necho \\\\\\"=== output.log ===\\\\\\"\\\\ncat \\\\\\"$TASK_... [truncated]"}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Command executed successfully."},{"type":"text","text":"=== 检查第一个 task 的目录结构 ===\\ntotal 40\\ndrwxr-xr-x@ 7 user staff 224 3月 17 12:58 .\\ndrwxr-xr-x@ 3 user staff 96 3月 17 12:58 ..\\n-rw-------@ 1 user staff 72 3月 17 12:58 consumer.json\\n-rw-------@ 1 user staff 72 3月 17 12:58 control.json\\n-rw-r--r--@ 1 user staff 32 3月 17 12:58 output.log\\n-rw-------@ 1 user staff 331 3月 17 12:58 runtime.json\\n-rw-------@ 1 user staff 481 3月 17 12:58 spec.json\\n\\n=== spec.json ===\\n{\\n \\"version\\": 1,\\n \\"id\\": \\"b83rhswvs\\",\\n \\"kin... [truncated]"}],"toolCalls":[],"toolCallId":"tool_doClKbqjLQAuMtimAxF3mufI"}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"所有验证都通过了:\\n1. task 目录结构正确\\n2. spec.json 包含正确的 task 信息\\n3. runtime.json 包含正确的状态信息\\n4. output.log 包含正确的输出\\n5. notification 持久化正确\\n\\n现在让我检查 notification 目录的内容。"}],"toolCalls":[{"type":"function","id":"tool_7SICneqn0eGcgCADXOFuo2yj","function":{"name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\nSESSION_ID=$(echo -n \\\\\\"$SMOKE_DIR\\\\\\" | md5)\\\\nSESSION_DIR=\\\\\\"$HOME/.kimi/sessions/$SESSION_ID\\\\\\"\\\\n\\\\n# 检查 notification 目录内容\\\\necho \\\\\\"=== 检查 notification 持久化内容 ===\\\\\\"\\\\nNOTIF_DIR=\\\\\\"$SESSION_DIR/0e1d3b94-dae2-4dfb-b36d-717397f4d2f6/notifications/n495ae5fb\\\\\\"\\\\necho \\\\\\"Notification: n495ae5fb (task.completed)\\\\\\"\\\\nls -la \\\\\\"$NOTIF_DIR\\\\\\"\\\\n\\\\necho \\\\\\"\\\\\\"\\\\necho \\\\\\"=== event.json ===\\\\\\"\\\\ncat \\\\\\"$NOTIF_DIR/event.json\\\\\\" | head -30\\\\n\\\\necho \\\\\\"\\\\\\"\\\\necho \\\\\\"=== delivery.json ===\\\\... [truncated]"}}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"所有验证都通过了:\\n1. task 目录结构正确\\n2. spec.json 包含正确的 task 信息\\n3. runtime.json 包含正确的状态信息\\n4. output.log 包含正确的输出\\n5. notification 持久化正确\\n\\n现在让我检查 notification 目录的内容。"}],"toolCalls":[{"type":"function","id":"tool_7SICneqn0eGcgCADXOFuo2yj","name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\nSESSION_ID=$(echo -n \\\\\\"$SMOKE_DIR\\\\\\" | md5)\\\\nSESSION_DIR=\\\\\\"$HOME/.kimi/sessions/$SESSION_ID\\\\\\"\\\\n\\\\n# 检查 notification 目录内容\\\\necho \\\\\\"=== 检查 notification 持久化内容 ===\\\\\\"\\\\nNOTIF_DIR=\\\\\\"$SESSION_DIR/0e1d3b94-dae2-4dfb-b36d-717397f4d2f6/notifications/n495ae5fb\\\\\\"\\\\necho \\\\\\"Notification: n495ae5fb (task.completed)\\\\\\"\\\\nls -la \\\\\\"$NOTIF_DIR\\\\\\"\\\\n\\\\necho \\\\\\"\\\\\\"\\\\necho \\\\\\"=== event.json ===\\\\\\"\\\\ncat \\\\\\"$NOTIF_DIR/event.json\\\\\\" | head -30\\\\n\\\\necho \\\\\\"\\\\\\"\\\\necho \\\\\\"=== delivery.json ===\\\\... [truncated]"}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Command executed successfully."},{"type":"text","text":"=== 检查 notification 持久化内容 ===\\nNotification: n495ae5fb (task.completed)\\ntotal 16\\ndrwxr-xr-x@ 4 user staff 128 3月 17 12:58 .\\ndrwxr-xr-x@ 3 user staff 96 3月 17 12:58 ..\\n-rw-------@ 1 user staff 332 3月 17 12:58 delivery.json\\n-rw-------@ 1 user staff 790 3月 17 12:58 event.json\\n\\n=== event.json ===\\n{\\n \\"version\\": 1,\\n \\"id\\": \\"n495ae5fb\\",\\n \\"category\\": \\"task\\",\\n \\"type\\": \\"task.completed\\",\\n \\"source_kind\\": \\"background_task\\",\\n \\"source_id\\": \\"b83rhswvs\\",\\n \\"title\\": \\"Background ta... [truncated]"}],"toolCalls":[],"toolCallId":"tool_7SICneqn0eGcgCADXOFuo2yj"}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"所有验证都通过了!现在让我总结冒烟测试的结果。"},{"type":"text","text":"所有验证完成!现在更新 todo 并汇报结论:"}],"toolCalls":[{"type":"function","id":"tool_CcJKFj7faabalZQ9X1gPrTbr","function":{"name":"SetTodoList","arguments":"{\\"todos\\": [{\\"title\\": \\"准备测试环境(创建隔离 work-dir)\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"探索轮:验证工具可用性和接口\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮1:正常路径 - 后台任务生命周期\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮2:边界条件 - 超时和停止\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮3:超时场景测试\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"观察轮:检查 session 产物\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"汇报结论\\", \\"status\\": \\"done\\"}]}"}}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"所有验证都通过了!现在让我总结冒烟测试的结果。"},{"type":"text","text":"所有验证完成!现在更新 todo 并汇报结论:"}],"toolCalls":[{"type":"function","id":"tool_CcJKFj7faabalZQ9X1gPrTbr","name":"SetTodoList","arguments":"{\\"todos\\": [{\\"title\\": \\"准备测试环境(创建隔离 work-dir)\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"探索轮:验证工具可用性和接口\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮1:正常路径 - 后台任务生命周期\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮2:边界条件 - 超时和停止\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮3:超时场景测试\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"观察轮:检查 session 产物\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"汇报结论\\", \\"status\\": \\"done\\"}]}"}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Todo list updated"}],"toolCalls":[],"toolCallId":"tool_CcJKFj7faabalZQ9X1gPrTbr"}} {"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"所有测试完成。现在整理完整的测试报告。"},{"type":"text","text":"---\\n\\n## 冒烟测试报告\\n\\n### 测试范围\\n**后台任务系统 + 通知系统** - 本次变更新增的核心功能\\n\\n### 已确认的行为 ✅\\n\\n| 功能 | 验证结果 |\\n|------|----------|\\n| **Shell 后台模式** | \`run_in_background=true\` + \`description\` 参数正常工作 |\\n| **TaskList** | 正确列出活跃/全部任务,显示 task_id/kind/status/description |\\n| **TaskOutput** | \`block=true/false\` 模式工作正常,返回完整任务状态和输出 |\\n| **TaskStop** | 成功发送 SIGTERM 停止任务,支持自定义 reason |\\n| **任务超时** | timeout 参数正确终止超时任务,exit_code=-15 |\\n| **Wire Notification** | \`Notification\` 事件正确发送,包含完整 payload |\\n| **Notification 持久化** | event.json + d... [truncated]"}],"toolCalls":[]}} {"type":"context.append_message","message":{"role":"user","content":[{"type":"text","text":"source /Users/example/proj/example-project/.venv/bin/activate"}],"toolCalls":[]}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"用户只是在执行激活虚拟环境的命令,这是一个 shell 命令。我应该执行它并确认环境已激活。"}],"toolCalls":[{"type":"function","id":"tool_oKHd9KBRZQ3pT7x7XgOWhcMJ","function":{"name":"Shell","arguments":"{\\"command\\": \\"source /Users/example/proj/example-project/.venv/bin/activate && echo \\\\\\"Virtual environment activated\\\\\\" && which python && python --version\\"}"}}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"用户只是在执行激活虚拟环境的命令,这是一个 shell 命令。我应该执行它并确认环境已激活。"}],"toolCalls":[{"type":"function","id":"tool_oKHd9KBRZQ3pT7x7XgOWhcMJ","name":"Shell","arguments":"{\\"command\\": \\"source /Users/example/proj/example-project/.venv/bin/activate && echo \\\\\\"Virtual environment activated\\\\\\" && which python && python --version\\"}"}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Command executed successfully."},{"type":"text","text":"Virtual environment activated\\n/Users/example/proj/example-project/.venv/bin/python\\nPython 3.14.2\\n"}],"toolCalls":[],"toolCallId":"tool_oKHd9KBRZQ3pT7x7XgOWhcMJ"}} ", } @@ -148,7 +148,7 @@ exports[`migration snapshot: legacy-protocol-1.3 > migration succeeds and matche "wire": "{"type":"metadata","protocol_version":"1.0","created_at":} {"type":"context.append_message","message":{"role":"user","content":[{"type":"text","text":"帮我写一个示例函数,处理字符串清理和分块"}],"toolCalls":[]}} {"type":"context.append_message","message":{"role":"user","content":[{"type":"text","text":"\\n\\nPlan mode is active. You are in a research and planning phase.\\n\\nIn plan mode, you should:\\n1. Thoroughly explore the codebase using Glob, Grep, and ReadFile tools.\\n2. Understand existing patterns and architecture.\\n3. Design an implementation approach.\\n4. Write your plan to the plan file using WriteFile.\\n5. Use AskUserQuestion if you need to clarify approaches.\\n6. When your plan is complete, call ExitPlanMode.\\n\\nRemember: DO NOT write or edit code files... [truncated]"}],"toolCalls":[]}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"用户想要写一个示例函数,处理字符串清理和分块。我先看看当前 diff 中有哪些相关代码可以参考。\\n\\n让我先检查一下当前的 git diff。"}],"toolCalls":[{"type":"function","id":"Shell:0","function":{"name":"Shell","arguments":"{\\"command\\": \\"git diff --no-color\\"}"}}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"用户想要写一个示例函数,处理字符串清理和分块。我先看看当前 diff 中有哪些相关代码可以参考。\\n\\n让我先检查一下当前的 git diff。"}],"toolCalls":[{"type":"function","id":"Shell:0","name":"Shell","arguments":"{\\"command\\": \\"git diff --no-color\\"}"}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Command executed successfully."},{"type":"text","text":"diff --git a/example.py b/example.py\\nindex 0000aaaa..1111bbbb 100644\\n--- a/example.py\\n+++ b/example.py\\n@@ -1,12 +1,16 @@\\n class Greeter:\\n def __init__(self, name: str) -> None:\\n self.name = name\\n+ self.greetings: list[str] = []\\n \\n def greet(self) -> str:\\n- return f\\"Hello, {self.name}!\\"\\n+ msg = f\\"Hello, {self.name}!\\"\\n+ self.greetings.append(msg)\\n+ return msg\\n \\n- def farewell(self) -> str:\\n- return f\\"Goodbye, {self.name}!\\"\\n+ def farewell(self, formal: bool = False) -> str:\\n+ prefix = \\"Farewell\\" if formal else \\"Goodbye\\"\\n+ return f\\"{prefix}, {self.name}!\\"\\n \\ndiff --git a/example/utils.py b/example/utils.py\\nindex 22223333..44445555 100644\\n--- a/example/utils.py\\n+++ b/example/utils.py\\n@@ -10,16 +10,20 @@ def slugify(text: str) -> str:\\n result = text.lower()\\n result = result.replace(\\" \\", \\"-\\")\\n- # Remove punctuation\\n- result = \\"\\".join(c for c in result if c.isalnum() or c == \\"-\\")\\n+ # Remove punctuation and collapse repeated dashes\\n+ cleaned = \\"\\".join(c for c in result if c.isalnum() or c == \\"-\\")\\n+ parts = [p for p in cleaned.split(\\"-\\") if p]\\n+ result = \\"-\\".join(parts)\\n return result\\n \\n \\n def chunk(items: list[str], size: int) -> list[list[str]]:\\n- if size <= 0:\\n- raise ValueError(\\"size must be positive\\")\\n- return [items[i:i + size] for i in range(0, len(items), size)]\\n+ if size <= 0:\\n+ raise ValueError(\\"chunk size must be a positive integer\\")\\n+ if not items:\\n+ return []\\n+ return [items[i : i + size] for i in range(0, len(items), size)]\\n \\n \\n def truncate(text: str, limit: int = 80) -> str:\\n@@ -32,8 +36,12 @@ def truncate(text: str, limit: int = 80) -> str:\\n return text\\n return text[: limit - 1] + \\"…\\"\\n \\n+\\n+def normalize_whitespace(text: str) -> str:\\n+ return \\" \\".join(text.split())\\n+\\n diff --git a/example/cli.py b/example/cli.py\\nindex 66667777..88889999 100644\\n--- a/example/cli.py\\n+++ b/example/cli.py\\n@@ -5,12 +5,18 @@ from .utils import slugify, truncate\\n \\n def build_parser() -> argparse.ArgumentParser:\\n parser = argparse.ArgumentParser(prog=\\"example\\")\\n- parser.add_argument(\\"--name\\", default=\\"world\\")\\n+ parser.add_argument(\\"--name\\", default=\\"world\\", help=\\"who to greet\\")\\n+ parser.add_argument(\\n+ \\"--formal\\",\\n+ action=\\"store_true\\",\\n+ help=\\"use a formal farewell tone\\",\\n+ )\\n return parser\\n \\n \\n def main(argv: list[str] | None = None) -> int:\\n parser = build_parser()\\n args = parser.parse_args(argv)\\n- print(f\\"Hello, {args.name}!\\")\\n+ greeter = Greeter(args.name)\\n+ print(greeter.greet())\\n+ print(greeter.farewell(formal=args.formal))\\n return 0\\ndiff --git a/example/config.py b/example/config.py\\nindex ccccdddd..eeeeffff 100644\\n--- a/example/config.py\\n+++ b/example/config.py\\n@@ -1,15 +1,22 @@\\n from dataclasses import dataclass, field\\n+from pathlib import Path\\n \\n \\n @dataclass\\n class AppConfig:\\n name: str = \\"example\\"\\n verbose: bool = False\\n- timeout: int = 30\\n+ timeout_seconds: int = 30\\n+ cache_dir: Path | None = None\\n features: list[str] = field(default_factory=list)\\n \\n def is_feature_enabled(self, feature: str) -> bool:\\n return feature in self.features\\n+\\n+ def with_feature(self, feature: str) -> \\"AppConfig\\":\\n+ if feature in self.features:\\n+ return self\\n+ return AppConfig(**{**self.__dict__, \\"features\\": [*self.features, feature]})\\n diff --git a/tests/test_example.py b/tests/test_example.py\\nindex aabbccdd..eeff0011 100644\\n--- a/tests/test_example.py\\n+++ b/tests/test_example.py\\n@@ -1,10 +1,22 @@\\n from example import Greeter\\n+from example.utils import slugify, chunk, normalize_whitespace\\n+from example.config import AppConfig\\n \\n \\n def test_greeter_greet() -> None:\\n g = Greeter(\\"Ada\\")\\n assert g.greet() == \\"Hello, Ada!\\"\\n+ assert g.greetings == [\\"Hello, Ada!\\"]\\n \\n \\n def test_greeter_farewell() -> None:\\n g = Greeter(\\"Ada\\")\\n assert g.farewell() == \\"Goodbye, Ada!\\"\\n+ assert g.farewell(formal=True) == \\"Farewell, Ada!\\"\\n+\\n+\\n+def test_slugify_collapses_dashes() -> None:\\n+ assert slugify(\\"hello world!!!\\") == \\"hello-world\\"\\n+ assert slugify(\\"--abc--def--\\") == \\"abc-def\\"\\n+\\n+\\n+def test_config_with_feature_is_idempotent() -> None:\\n+ cfg = AppConfig().with_feature(\\"beta\\").with_feature(\\"beta\\")\\n+ assert cfg.features == [\\"beta ... [truncated]"}],"toolCalls":[],"toolCallId":"Shell:0"}} ", } @@ -182,7 +182,7 @@ exports[`migration snapshot: recent-protocol-1.10 > migration succeeds and match {"type":"context.append_message","message":{"role":"user","content":[{"type":"text","text":"hi"}],"toolCalls":[]}} {"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"The user said \\"hi\\". This is a simple greeting. I should respond in a friendly, helpful manner. Since there's no specific task or question, I'll just greet them back and ask how I can help."},{"type":"text","text":"Hello! How can I help you today?"}],"toolCalls":[]}} {"type":"context.append_message","message":{"role":"user","content":[{"type":"text","text":"探索这个项目"}],"toolCalls":[]}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"用户要求探索这个项目。让我先了解一下项目的整体结构,然后提供一些关键信息。\\n\\n从目录结构来看,这是一个示例 Python 项目。让我先查看一些关键文件来更好地理解项目。"}],"toolCalls":[{"type":"function","id":"Shell:0","function":{"name":"Shell","arguments":"{\\"command\\": \\"find . -maxdepth 2 -type f -name \\\\\\"*.md\\\\\\" | grep -E \\\\\\"(README|CHANGELOG|CONTRIBUTING|AGENTS)\\\\\\" | sort\\"}"}},{"type":"function","id":"Shell:1","function":{"name":"Shell","arguments":"{\\"command\\": \\"ls -la pyproject.toml Makefile 2>/dev/null || echo \\\\\\"No pyproject.toml or Makefile found\\\\\\"\\"}"}},{"type":"function","id":"Shell:2","function":{"name":"Shell","arguments":"{\\"command\\": \\"git log --oneline -10 2>/dev/null || echo \\\\\\"Not a git repo or no commits\\\\\\"\\"}"}}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"用户要求探索这个项目。让我先了解一下项目的整体结构,然后提供一些关键信息。\\n\\n从目录结构来看,这是一个示例 Python 项目。让我先查看一些关键文件来更好地理解项目。"}],"toolCalls":[{"type":"function","id":"Shell:0","name":"Shell","arguments":"{\\"command\\": \\"find . -maxdepth 2 -type f -name \\\\\\"*.md\\\\\\" | grep -E \\\\\\"(README|CHANGELOG|CONTRIBUTING|AGENTS)\\\\\\" | sort\\"}"},{"type":"function","id":"Shell:1","name":"Shell","arguments":"{\\"command\\": \\"ls -la pyproject.toml Makefile 2>/dev/null || echo \\\\\\"No pyproject.toml or Makefile found\\\\\\"\\"}"},{"type":"function","id":"Shell:2","name":"Shell","arguments":"{\\"command\\": \\"git log --oneline -10 2>/dev/null || echo \\\\\\"Not a git repo or no commits\\\\\\"\\"}"}]}} {"type":"context.append_message","message":{"role":"tool","toolCallId":"Shell:1","content":[{"type":"text","text":"[tool result unavailable — session imported from kimi-cli]"}],"toolCalls":[]}} {"type":"context.append_message","message":{"role":"tool","toolCallId":"Shell:2","content":[{"type":"text","text":"[tool result unavailable — session imported from kimi-cli]"}],"toolCalls":[]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Command executed successfully."},{"type":"text","text":"./.pytest_cache/README.md\\n./AGENTS.md\\n./CHANGELOG.md\\n./CONTRIBUTING.md\\n./docs/AGENTS.md\\n./README.md\\n./tests_e2e/AGENTS.md\\n"}],"toolCalls":[],"toolCallId":"Shell:0"}} @@ -280,7 +280,7 @@ exports[`migration snapshot: with-subagent-collapsed > migration succeeds and ma }", "wire": "{"type":"metadata","protocol_version":"1.0","created_at":} {"type":"context.append_message","message":{"role":"user","content":[{"type":"text","text":"你启动三个 sub agent,每个 sub,都要运行一些需要被批准的简单指令,运行两个以上"}],"toolCalls":[]}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"用户要求我启动三个 sub agent,每个 sub agent 都需要运行一些需要被批准的简单指令,并且要求运行两个以上。\\n\\n我需要使用 Agent 工具来创建三个 sub agent。每个 sub agent 都应该执行一些需要批准的操作,比如文件写入或 shell 命令。\\n\\n让我创建三个 sub agent,每个都执行不同的需要批准的任务:\\n1. 第一个 sub agent:创建一个测试文件\\n2. 第二个 sub agent:执行一个简单的 shell 命令\\n3. 第三个 sub agent:创建另一个测试文件\\n\\n我需要并行启动这三个 sub agent。"},{"type":"text","text":"我来启动三个 sub agent,每个都会执行需要批准的简单指令。"}],"toolCalls":[{"type":"function","id":"tool_Ct3NdbCjJ2Xbl7YzithE2Dy8","function":{"name":"Agent","arguments":"{\\"description\\": \\"Sub agent 1: 创建文件\\", \\"prompt\\": \\"你是一个测试用的 sub agent。请执行以下需要批准的操作:\\\\n\\\\n1. 使用 WriteFile 工具在当前目录创建一个名为 \`subagent1_test.txt\` 的文件,内容为 \\\\\\"Hello from Sub Agent 1!\\\\\\"\\\\n\\\\n这个操作需要用户批准,请等待批准并完成操作。\\\\n\\\\n完成后,报告操作结果。\\"}"}},{"type":"function","id":"tool_OtoGSYsrDQpEaav0hLYbXDhd","function":{"name":"Agent","arguments":"{\\"description\\": \\"Sub agent 2: Shell 命令\\", \\"prompt\\": \\"你是一个测试用的 sub agent。请执行以下需要批准的操作:\\\\n\\\\n1. 使用 Shell 工具执行命令 \`echo \\\\\\"Hello from Sub Agent 2!\\\\\\" > subagent2_test.txt\`\\\\n\\\\n这个操作需要用户批准,请等待批准并完成操作。\\\\n\\\\n完成后,报告操作结果。\\"}"}},{"type":"function","id":"tool_zi4b3Pfbiia2kxPMUvjL4cVN","function":{"name":"Agent","arguments":"{\\"description\\": \\"Sub agent 3: 创建文件\\", \\"prompt\\": \\"你是一个测试用的 sub agent。请执行以下需要批准的操作:\\\\n\\\\n1. 使用 WriteFile 工具在当前目录创建一个名为 \`subagent3_test.txt\` 的文件,内容为 \\\\\\"Hello from Sub Agent 3!\\\\\\"\\\\n\\\\n这个操作需要用户批准,请等待批准并完成操作。\\\\n\\\\n完成后,报告操作结果。\\"}"}}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"用户要求我启动三个 sub agent,每个 sub agent 都需要运行一些需要被批准的简单指令,并且要求运行两个以上。\\n\\n我需要使用 Agent 工具来创建三个 sub agent。每个 sub agent 都应该执行一些需要批准的操作,比如文件写入或 shell 命令。\\n\\n让我创建三个 sub agent,每个都执行不同的需要批准的任务:\\n1. 第一个 sub agent:创建一个测试文件\\n2. 第二个 sub agent:执行一个简单的 shell 命令\\n3. 第三个 sub agent:创建另一个测试文件\\n\\n我需要并行启动这三个 sub agent。"},{"type":"text","text":"我来启动三个 sub agent,每个都会执行需要批准的简单指令。"}],"toolCalls":[{"type":"function","id":"tool_Ct3NdbCjJ2Xbl7YzithE2Dy8","name":"Agent","arguments":"{\\"description\\": \\"Sub agent 1: 创建文件\\", \\"prompt\\": \\"你是一个测试用的 sub agent。请执行以下需要批准的操作:\\\\n\\\\n1. 使用 WriteFile 工具在当前目录创建一个名为 \`subagent1_test.txt\` 的文件,内容为 \\\\\\"Hello from Sub Agent 1!\\\\\\"\\\\n\\\\n这个操作需要用户批准,请等待批准并完成操作。\\\\n\\\\n完成后,报告操作结果。\\"}"},{"type":"function","id":"tool_OtoGSYsrDQpEaav0hLYbXDhd","name":"Agent","arguments":"{\\"description\\": \\"Sub agent 2: Shell 命令\\", \\"prompt\\": \\"你是一个测试用的 sub agent。请执行以下需要批准的操作:\\\\n\\\\n1. 使用 Shell 工具执行命令 \`echo \\\\\\"Hello from Sub Agent 2!\\\\\\" > subagent2_test.txt\`\\\\n\\\\n这个操作需要用户批准,请等待批准并完成操作。\\\\n\\\\n完成后,报告操作结果。\\"}"},{"type":"function","id":"tool_zi4b3Pfbiia2kxPMUvjL4cVN","name":"Agent","arguments":"{\\"description\\": \\"Sub agent 3: 创建文件\\", \\"prompt\\": \\"你是一个测试用的 sub agent。请执行以下需要批准的操作:\\\\n\\\\n1. 使用 WriteFile 工具在当前目录创建一个名为 \`subagent3_test.txt\` 的文件,内容为 \\\\\\"Hello from Sub Agent 3!\\\\\\"\\\\n\\\\n这个操作需要用户批准,请等待批准并完成操作。\\\\n\\\\n完成后,报告操作结果。\\"}"}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"agent_id: ac1f74988\\nresumed: false\\nactual_subagent_type: coder\\nstatus: completed\\n\\n[summary]\\n## 详细操作报告\\n\\n### 任务背景\\n作为测试用的 sub agent,我被要求执行一个需要用户批准的文件写入操作。这是一个测试场景,旨在验证带批准流程的工具调用机制。\\n\\n### 执行过程\\n\\n#### 第一次尝试(被拒绝)\\n- **操作**: 使用 \`WriteFile\` 工具创建文件 \`subagent1_test.txt\`\\n- **预期内容**: \`\\"Hello from Sub Agent 1!\\"\`\\n- **结果**: 用户拒绝了此操作\\n- **用户反馈**: 要求将文件名改为 \`kk\`\\n\\n#### 第二次尝试(成功)\\n- **操作**: 使用 \`WriteFile\` 工具创建文件 \`kk\`\\n- **内容**: \`\\"Hello from Sub Agent 1!\\"\`\\n- **结果**: ✅ 成功创建/覆盖文件\\n- **文件大小**: 23 字节\\n\\n### 技术细节\\n\\n1. **工具使用**: \`W... [truncated]"}],"toolCalls":[],"toolCallId":"tool_Ct3NdbCjJ2Xbl7YzithE2Dy8"}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"agent_id: aa373b195\\nresumed: false\\nactual_subagent_type: coder\\nstatus: completed\\n\\n[summary]\\n## 详细操作结果报告\\n\\n### 1. 任务概述\\n\\n作为测试用 sub agent,我成功执行了需要用户批准的 Shell 命令操作,创建了测试文件并验证了其内容完整性。\\n\\n### 2. 执行详情\\n\\n#### 2.1 命令信息\\n- **命令类型**: Shell 命令执行\\n- **具体命令**: \`echo \\"Hello from Sub Agent 2!\\" > subagent2_test.txt\`\\n- **工作目录**: \`/Users/example/proj/example-project\`\\n- **执行时间**: 2026-03-23T21:42:43+08:00(系统参考时间)\\n\\n#### 2.2 技术实现\\n该命令使用了以下 Shell 特性:\\n- \`echo\` 命令:输出字符串到标准输出\\n- 重定向操作符 \`>\`:将标准输出重定向到文件\\n- 如果文件不存在则... [truncated]"}],"toolCalls":[],"toolCallId":"tool_OtoGSYsrDQpEaav0hLYbXDhd"}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"agent_id: aadd98577\\nresumed: false\\nactual_subagent_type: coder\\nstatus: completed\\n\\n[summary]\\n**详细操作报告 - Sub Agent 3**\\n\\n---\\n\\n## 1. 任务概述\\n\\n本次任务要求以测试用的 sub agent 身份执行一个需要用户批准的操作:在指定目录创建一个包含特定内容的文本文件。\\n\\n---\\n\\n## 2. 操作执行详情\\n\\n### 2.1 第一次尝试\\n\\n| 属性 | 详情 |\\n|------|------|\\n| **目标文件路径** | \`subagent3_test.txt\` |\\n| **请求内容** | \`Hello from Sub Agent 3!\` |\\n| **操作类型** | WriteFile(覆盖模式,默认)|\\n| **执行状态** | 被拒绝 |\\n| **用户反馈** | \\"用另一个文件名再试一次\\" |\\n\\n**技术分析**:系统在第一次请求时触发了用户审批流程。用户拒绝了原始文件名 \`subagent3_test.txt\`,并明确要求使用另一个文件名再试。这表... [truncated]"}],"toolCalls":[],"toolCallId":"tool_zi4b3Pfbiia2kxPMUvjL4cVN"}} @@ -348,7 +348,7 @@ exports[`migration snapshot: with-tool-calls > migration succeeds and matches sn "wire": "{"type":"metadata","protocol_version":"1.0","created_at":} {"type":"context.append_message","message":{"role":"user","content":[{"type":"text","text":"run echo hi"}],"toolCalls":[]}} {"type":"context.append_message","message":{"role":"user","content":[{"type":"text","text":"\\nYou are running in non-interactive mode. The user cannot answer questions or provide feedback during execution.\\n- Do NOT call AskUserQuestion. If you need to make a decision, make your best judgment and proceed.\\n- For EnterPlanMode / ExitPlanMode, they will be auto-approved. You can use them normally but expect no user feedback.\\n"}],"toolCalls":[]}} -{"type":"context.append_message","message":{"role":"assistant","content":[],"toolCalls":[{"type":"function","id":"tc1","function":{"name":"Shell","arguments":"{\\"command\\": \\"echo hi\\"}"}}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[],"toolCalls":[{"type":"function","id":"tc1","name":"Shell","arguments":"{\\"command\\": \\"echo hi\\"}"}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"ERROR: Shell blocked by hook"}],"toolCalls":[],"toolCallId":"tc1"}} ", } diff --git a/packages/migration-legacy/test/sessions/close-tool-calls.test.ts b/packages/migration-legacy/test/sessions/close-tool-calls.test.ts index e5c7d2d..412a727 100644 --- a/packages/migration-legacy/test/sessions/close-tool-calls.test.ts +++ b/packages/migration-legacy/test/sessions/close-tool-calls.test.ts @@ -6,7 +6,7 @@ function assistantWithCall(id: string): NormalizedMessage { return { role: 'assistant', content: [], - toolCalls: [{ type: 'function', id, function: { name: 'Shell', arguments: '{}' } }], + toolCalls: [{ type: 'function', id, name: 'Shell', arguments: '{}' }], }; } diff --git a/packages/migration-legacy/test/sessions/translator.test.ts b/packages/migration-legacy/test/sessions/translator.test.ts index 785aab1..82d9689 100644 --- a/packages/migration-legacy/test/sessions/translator.test.ts +++ b/packages/migration-legacy/test/sessions/translator.test.ts @@ -47,7 +47,7 @@ describe('translateContextLines', () => { const [msg] = translateContextLines([line]); expect(msg!.role).toBe('assistant'); expect(msg!.toolCalls).toEqual([ - { type: 'function', id: 'Shell:0', function: { name: 'Shell', arguments: '{}' } }, + { type: 'function', id: 'Shell:0', name: 'Shell', arguments: '{}' }, ]); }); diff --git a/packages/node-sdk/test/export-session.test.ts b/packages/node-sdk/test/export-session.test.ts index be3e000..58d65cd 100644 --- a/packages/node-sdk/test/export-session.test.ts +++ b/packages/node-sdk/test/export-session.test.ts @@ -12,7 +12,10 @@ import { type SessionSummary, } from '#/index'; import { resolveGlobalLogPath } from '../../agent-core/src/logging/logger'; -import { exportSessionDirectory } from '../../agent-core/src/session/export'; +import { + WIRE_PROTOCOL_VERSION, + exportSessionDirectory, +} from '../../agent-core/src/session/export'; import { recordingTelemetry, type TelemetryRecord } from './telemetry'; import { TEST_IDENTITY } from './test-identity'; @@ -136,7 +139,7 @@ describe('exportSessionDirectory', () => { ]); expect(result.manifest).toMatchObject({ sessionId: sid, - wireProtocolVersion: '1.0', + wireProtocolVersion: WIRE_PROTOCOL_VERSION, sessionFirstActivity: '2026-04-18T10:00:00.000Z', sessionLastActivity: '2026-04-18T10:00:03.000Z', title: 'Export Test', From 62e9836d69bb52f1446dc1d2c364ff61e999e9c4 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Mon, 25 May 2026 19:48:38 +0800 Subject: [PATCH 2/6] fix: restrict legacy tool call migration --- .changeset/restrict-toolcall-migration.md | 6 ++ .../src/agent/records/migration/v1.1.ts | 82 +++++++++--------- .../test/agent/records/migration.test.ts | 36 -------- .../test/agent/records/migration/v1.1.test.ts | 83 +++++++++++++++++++ 4 files changed, 128 insertions(+), 79 deletions(-) create mode 100644 .changeset/restrict-toolcall-migration.md create mode 100644 packages/agent-core/test/agent/records/migration/v1.1.test.ts diff --git a/.changeset/restrict-toolcall-migration.md b/.changeset/restrict-toolcall-migration.md new file mode 100644 index 0000000..c93bd88 --- /dev/null +++ b/.changeset/restrict-toolcall-migration.md @@ -0,0 +1,6 @@ +--- +"@moonshot-ai/agent-core": patch +"@moonshot-ai/kimi-code": patch +--- + +Prevent legacy tool call record migration from rewriting unrelated JSON payloads. diff --git a/packages/agent-core/src/agent/records/migration/v1.1.ts b/packages/agent-core/src/agent/records/migration/v1.1.ts index 4ca5cd3..add9d9c 100644 --- a/packages/agent-core/src/agent/records/migration/v1.1.ts +++ b/packages/agent-core/src/agent/records/migration/v1.1.ts @@ -7,68 +7,64 @@ import type { WireMigration, WireMigrationRecord } from './index'; * { name: 'xxx', arguments: 'yyy' } */ interface LegacyToolCall { - function?: { + type: 'function'; + id: string; + function: { name?: string; - arguments?: string; + arguments?: string | null; }; } function isLegacyToolCall(v: unknown): v is LegacyToolCall { - return ( - typeof v === 'object' && - v !== null && - 'function' in v && - typeof (v as Record)['function'] === 'object' && - (v as Record)['function'] !== null - ); + if (!isRecord(v)) return false; + return v['type'] === 'function' && typeof v['id'] === 'string' && isRecord(v['function']); } function migrateToolCall(v: LegacyToolCall): unknown { const { function: fn, ...rest } = v; return { ...rest, - name: fn?.name, - arguments: fn?.arguments, + name: fn.name, + arguments: fn.arguments, }; } -/** - * Recursively replace old-style ToolCall objects in a plain JSON value. - * Operates in-place (mutable) for performance. - */ -function migrateLegacyToolCalls(value: unknown): unknown { - if (Array.isArray(value)) { - for (let i = 0; i < value.length; i++) { - const item = value[i]; - if (isLegacyToolCall(item)) { - value[i] = migrateToolCall(item); - } else { - migrateLegacyToolCalls(item); - } - } - return value; - } - - if (typeof value === 'object' && value !== null) { - const obj = value as Record; - for (const key of Object.keys(obj)) { - const v = obj[key]; - if (isLegacyToolCall(v)) { - obj[key] = migrateToolCall(v); - } else { - migrateLegacyToolCalls(v); - } - } - return obj; - } - - return value; +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); } export const migrateV1_0ToV1_1: WireMigration = { sourceVersion: '1.0', targetVersion: '1.1', migrateRecord(record: WireMigrationRecord): WireMigrationRecord { - return migrateLegacyToolCalls(structuredClone(record)) as WireMigrationRecord; + // Recursively replace old-style ToolCall objects in-place. + const migrate = (value: unknown): unknown => { + if (Array.isArray(value)) { + for (let i = 0; i < value.length; i++) { + const item = value[i]; + if (isLegacyToolCall(item)) { + value[i] = migrateToolCall(item); + } else { + migrate(item); + } + } + return value; + } + + if (isRecord(value)) { + for (const key of Object.keys(value)) { + const v = value[key]; + if (isLegacyToolCall(v)) { + value[key] = migrateToolCall(v); + } else { + migrate(v); + } + } + } + + return value; + }; + + return migrate(record) as WireMigrationRecord; }, }; diff --git a/packages/agent-core/test/agent/records/migration.test.ts b/packages/agent-core/test/agent/records/migration.test.ts index e15304c..697a695 100644 --- a/packages/agent-core/test/agent/records/migration.test.ts +++ b/packages/agent-core/test/agent/records/migration.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it } from 'vitest'; import { - migrateWireRecords, migrateWireRecord, type WireMigration, } from '../../../src/agent/records/migration'; @@ -33,39 +32,4 @@ describe('wire record migrations', () => { second: true, }); }); - - it('flattens legacy tool calls when migrating v1.0 records', () => { - const migrated = migrateWireRecords( - [ - { - type: 'context.append_message', - message: { - role: 'assistant', - toolCalls: [ - { - type: 'function', - id: 'call_legacy_bash', - function: { - name: 'Bash', - arguments: '{"command":"pwd"}', - }, - }, - ], - }, - }, - ], - '1.0', - ); - - const toolCall = ( - migrated[0]?.['message'] as { - readonly toolCalls: readonly Record[]; - } - ).toolCalls[0]; - expect(toolCall).toMatchObject({ - name: 'Bash', - arguments: '{"command":"pwd"}', - }); - expect(toolCall?.['function']).toBeUndefined(); - }); }); diff --git a/packages/agent-core/test/agent/records/migration/v1.1.test.ts b/packages/agent-core/test/agent/records/migration/v1.1.test.ts new file mode 100644 index 0000000..92e84b7 --- /dev/null +++ b/packages/agent-core/test/agent/records/migration/v1.1.test.ts @@ -0,0 +1,83 @@ +import { describe, expect, it } from 'vitest'; + +import { + AGENT_WIRE_PROTOCOL_VERSION, + AgentRecords, + InMemoryAgentRecordPersistence, + type AgentRecord, +} from '../../../../src/agent/records'; +import { eventSnapshot } from '../../harness/snapshots'; + +describe('1.0 to 1.1', () => { + it('rewrites v1.0 records to the v1.1 wire shape', async () => { + const persistence = new InMemoryAgentRecordPersistence([ + { + type: 'metadata', + protocol_version: '1.0', + created_at: 1, + }, + { + type: 'context.append_message', + message: { + role: 'assistant', + content: [], + toolCalls: [ + { + type: 'function', + id: 'call_legacy_bash', + function: { + name: 'Bash', + arguments: '{"command":"pwd"}', + }, + }, + ], + }, + } as unknown as AgentRecord, + { + type: 'tools.register_user_tool', + name: 'schema_tool', + description: 'Tool with a schema field named function', + parameters: { + type: 'object', + properties: { + function: { + type: 'object', + properties: { + name: { type: 'string' }, + }, + }, + value: { type: 'string' }, + }, + required: ['function'], + }, + }, + ]); + const records = new AgentRecords(() => {}, persistence); + + await records.replay(); + + expect(persistence.records[0]).toMatchObject({ + type: 'metadata', + protocol_version: AGENT_WIRE_PROTOCOL_VERSION, + }); + expect(wireSnapshot(persistence.records)).toMatchInlineSnapshot(` + [wire] metadata { "protocol_version": "1.1", "created_at": 1 } + [wire] context.append_message { "message": { "role": "assistant", "content": [], "toolCalls": [ { "type": "function", "id": "call_legacy_bash", "name": "Bash", "arguments": "{\\"command\\":\\"pwd\\"}" } ] } } + [wire] tools.register_user_tool { "name": "schema_tool", "description": "Tool with a schema field named function", "parameters": { "type": "object", "properties": { "function": { "type": "object", "properties": { "name": { "type": "string" } } }, "value": { "type": "string" } }, "required": [ "function" ] } } + `); + }); +}); + +function wireSnapshot(records: readonly AgentRecord[]) { + return eventSnapshot( + records.map((record) => { + const { type: event, ...args } = record; + return { + type: '[wire]' as const, + event, + args, + }; + }), + new Map(), + ); +} From 5c185b3760532b7c9d0215345925a4ba88bf5b00 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Mon, 25 May 2026 20:26:40 +0800 Subject: [PATCH 3/6] fix --- .changeset/flatten-toolcall.md | 2 +- .changeset/restrict-toolcall-migration.md | 6 ------ packages/agent-core/test/session/lifecycle-hooks.test.ts | 2 +- 3 files changed, 2 insertions(+), 8 deletions(-) delete mode 100644 .changeset/restrict-toolcall-migration.md diff --git a/.changeset/flatten-toolcall.md b/.changeset/flatten-toolcall.md index 9a09dd9..0b1cdd2 100644 --- a/.changeset/flatten-toolcall.md +++ b/.changeset/flatten-toolcall.md @@ -5,4 +5,4 @@ "@moonshot-ai/migration-legacy": minor --- -Flatten `ToolCall` by inlining `name` and `arguments` directly, removing the nested `function` object and the `ToolCallFunction` interface. +Flatten tool call data by inlining tool names and arguments at the top level, and limit legacy record migration so it only rewrites matching tool call payloads. diff --git a/.changeset/restrict-toolcall-migration.md b/.changeset/restrict-toolcall-migration.md deleted file mode 100644 index c93bd88..0000000 --- a/.changeset/restrict-toolcall-migration.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@moonshot-ai/agent-core": patch -"@moonshot-ai/kimi-code": patch ---- - -Prevent legacy tool call record migration from rewriting unrelated JSON payloads. diff --git a/packages/agent-core/test/session/lifecycle-hooks.test.ts b/packages/agent-core/test/session/lifecycle-hooks.test.ts index 81d3759..a58a619 100644 --- a/packages/agent-core/test/session/lifecycle-hooks.test.ts +++ b/packages/agent-core/test/session/lifecycle-hooks.test.ts @@ -23,7 +23,7 @@ const tempDirs: string[] = []; afterEach(async () => { vi.unstubAllEnvs(); for (const dir of tempDirs.splice(0)) { - await rm(dir, { recursive: true, force: true }); + await rm(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 10 }); } }); From 41e2f348f56de3d9cb152be94f7c869d10d5ac5e Mon Sep 17 00:00:00 2001 From: _Kerman Date: Mon, 25 May 2026 21:27:03 +0800 Subject: [PATCH 4/6] fix --- apps/kimi-code/src/cli/run-prompt.ts | 26 ++++++---- apps/kimi-code/test/cli/run-prompt.test.ts | 2 +- .../src/sessions/translator.ts | 9 ++-- .../fixtures.snapshot.test.ts.snap | 50 +++++++++---------- .../test/sessions/close-tool-calls.test.ts | 2 +- .../test/sessions/translator.test.ts | 2 +- 6 files changed, 47 insertions(+), 44 deletions(-) diff --git a/apps/kimi-code/src/cli/run-prompt.ts b/apps/kimi-code/src/cli/run-prompt.ts index c48b51e..806d60a 100644 --- a/apps/kimi-code/src/cli/run-prompt.ts +++ b/apps/kimi-code/src/cli/run-prompt.ts @@ -456,8 +456,10 @@ class PromptTranscriptWriter implements PromptTurnWriter { interface PromptJsonToolCall { type: 'function'; id: string; - name: string; - arguments: string; + function: { + name: string; + arguments: string; + }; } interface PromptJsonAssistantMessage { @@ -495,15 +497,17 @@ class PromptJsonWriter implements PromptTurnWriter { writeToolCall(toolCallId: string, name: string, args: unknown): void { const existing = this.toolCalls.find((toolCall) => toolCall.id === toolCallId); if (existing !== undefined) { - existing.name = name; - existing.arguments = stringifyJsonValue(args); + existing.function.name = name; + existing.function.arguments = stringifyJsonValue(args); return; } this.toolCalls.push({ type: 'function', id: toolCallId, - name, - arguments: stringifyJsonValue(args), + function: { + name, + arguments: stringifyJsonValue(args), + }, }); } @@ -514,10 +518,10 @@ class PromptJsonWriter implements PromptTurnWriter { ): void { const toolCall = this.findOrCreateToolCall(toolCallId, name ?? ''); if (name !== undefined) { - toolCall.name = name; + toolCall.function.name = name; } if (argumentsPart !== undefined) { - toolCall.arguments += argumentsPart; + toolCall.function.arguments += argumentsPart; } } @@ -556,8 +560,10 @@ class PromptJsonWriter implements PromptTurnWriter { const toolCall: PromptJsonToolCall = { type: 'function', id: toolCallId, - name, - arguments: '', + function: { + name, + arguments: '', + }, }; this.toolCalls.push(toolCall); return toolCall; diff --git a/apps/kimi-code/test/cli/run-prompt.test.ts b/apps/kimi-code/test/cli/run-prompt.test.ts index c34cbde..0169acb 100644 --- a/apps/kimi-code/test/cli/run-prompt.test.ts +++ b/apps/kimi-code/test/cli/run-prompt.test.ts @@ -432,7 +432,7 @@ describe('runPrompt', () => { expect(stdout.text()).toBe( [ - '{"role":"assistant","content":"checking","tool_calls":[{"type":"function","id":"tc_1","name":"Shell","arguments":"{\\"command\\":\\"ls\\"}"}]}', + '{"role":"assistant","content":"checking","tool_calls":[{"type":"function","id":"tc_1","function":{"name":"Shell","arguments":"{\\"command\\":\\"ls\\"}"}}]}', '{"role":"tool","tool_call_id":"tc_1","content":"file1.py\\nfile2.py"}', '{"role":"assistant","content":"done"}', '', diff --git a/packages/migration-legacy/src/sessions/translator.ts b/packages/migration-legacy/src/sessions/translator.ts index 3bc530c..499180b 100644 --- a/packages/migration-legacy/src/sessions/translator.ts +++ b/packages/migration-legacy/src/sessions/translator.ts @@ -6,8 +6,7 @@ export interface NormalizedMessage { readonly toolCalls: ReadonlyArray<{ readonly type: 'function'; readonly id: string; - readonly name: string; - readonly arguments: string; + readonly function: { readonly name: string; readonly arguments: string }; }>; readonly toolCallId?: string; } @@ -147,8 +146,7 @@ function buildTool(obj: Record): NormalizedMessage { interface RawToolCall { readonly type: 'function'; readonly id: string; - readonly name: string; - readonly arguments: string; + readonly function: { readonly name: string; readonly arguments: string }; } function parseToolCall(raw: unknown): RawToolCall | undefined { @@ -164,8 +162,7 @@ function parseToolCall(raw: unknown): RawToolCall | undefined { return { type: 'function', id: r['id'], - name: f['name'], - arguments: args, + function: { name: f['name'], arguments: args }, }; } diff --git a/packages/migration-legacy/test/sessions/__snapshots__/fixtures.snapshot.test.ts.snap b/packages/migration-legacy/test/sessions/__snapshots__/fixtures.snapshot.test.ts.snap index a67ee58..e037c8a 100644 --- a/packages/migration-legacy/test/sessions/__snapshots__/fixtures.snapshot.test.ts.snap +++ b/packages/migration-legacy/test/sessions/__snapshots__/fixtures.snapshot.test.ts.snap @@ -27,7 +27,7 @@ exports[`migration snapshot: archived > migration succeeds and matches snapshot "wire": "{"type":"metadata","protocol_version":"1.0","created_at":} {"type":"context.append_message","message":{"role":"user","content":[{"type":"text","text":"You are a code translation assistant.\\n\\nTask:\\n- Read the file \`sample.js\` in the current working directory.\\n- Translate it into idiomatic Python 3.\\n- Write the translated code to \`translated.py\` in the current working directory.\\n\\nRules:\\n- You must read the file from disk; do not guess its contents.\\n- Preserve behavior and output.\\n- Write only Python code in translated.py (no Markdown).\\n- Overwrite translated.py if it already exists.\\n- After writing, reply with a single short ASCII confirmation se... [truncated]"}],"toolCalls":[]}} {"type":"context.append_message","message":{"role":"user","content":[{"type":"text","text":"\\nYou are running in non-interactive mode. The user cannot answer questions or provide feedback during execution.\\n- Do NOT call AskUserQuestion. If you need to make a decision, make your best judgment and proceed.\\n- For EnterPlanMode / ExitPlanMode, they will be auto-approved. You can use them normally but expect no user feedback.\\n"}],"toolCalls":[]}} -{"type":"context.append_message","message":{"role":"assistant","content":[],"toolCalls":[{"type":"function","id":"ReadFile:0","name":"ReadFile","arguments":"{\\"path\\": \\"sample.js\\"}"}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[],"toolCalls":[{"type":"function","id":"ReadFile:0","function":{"name":"ReadFile","arguments":"{\\"path\\": \\"sample.js\\"}"}}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"10 lines read from file starting from line 1. End of file reached."},{"type":"text","text":" 1\\tfunction add(a, b) {\\n 2\\t return a + b;\\n 3\\t}\\n 4\\t\\n 5\\tfunction main() {\\n 6\\t const result = add(2, 3);\\n 7\\t console.log(\`2 + 3 = \${result}\`);\\n 8\\t}\\n 9\\t\\n 10\\tmain();\\n"}],"toolCalls":[],"toolCallId":"ReadFile:0"}} ", } @@ -59,63 +59,63 @@ exports[`migration snapshot: large-100msgs > migration succeeds and matches snap }", "wire": "{"type":"metadata","protocol_version":"1.0","created_at":} {"type":"context.append_message","message":{"role":"user","content":[{"type":"text","text":"请帮我探索一下当前这个仓库的整体结构,了解它的主要模块和功能。看完之后请写一份简短的小报告,列出你认为最重要的几个模块、它们的职责,以及彼此之间的依赖关系。报告控制在一页之内,便于团队成员快速了解项目。"}],"toolCalls":[]}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"用户想要我读取 example-task skill 的 SKILL.md 文件,但他们已经在 prompt 中提供了完整的 skill 内容。不过我需要了解这个 skill 的更多细节,特别是 references/prompt-patterns.md 文件。\\n\\n用户似乎在向我介绍这个 skill,让我熟悉它的用法。我应该先检查一下这个 skill 目录的结构,看看是否有其他有用的资源文件。\\n\\n让我先了解一下这个 skill 的完整结构,以便以后需要时可以使用它。"},{"type":"text","text":"我来熟悉一下 \`example-task\` skill 的完整结构和资源。"}],"toolCalls":[{"type":"function","id":"tool_3mtoDjRlqenM0GDSjv9xRzLS","name":"Glob","arguments":"{\\"pattern\\": \\".agents/skills/example-task/**/*\\"}"}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"用户想要我读取 example-task skill 的 SKILL.md 文件,但他们已经在 prompt 中提供了完整的 skill 内容。不过我需要了解这个 skill 的更多细节,特别是 references/prompt-patterns.md 文件。\\n\\n用户似乎在向我介绍这个 skill,让我熟悉它的用法。我应该先检查一下这个 skill 目录的结构,看看是否有其他有用的资源文件。\\n\\n让我先了解一下这个 skill 的完整结构,以便以后需要时可以使用它。"},{"type":"text","text":"我来熟悉一下 \`example-task\` skill 的完整结构和资源。"}],"toolCalls":[{"type":"function","id":"tool_3mtoDjRlqenM0GDSjv9xRzLS","function":{"name":"Glob","arguments":"{\\"pattern\\": \\".agents/skills/example-task/**/*\\"}"}}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Found 5 matches for pattern \`.agents/skills/example-task/**/*\`."},{"type":"text","text":".agents/skills/example-task/SKILL.md\\n.agents/skills/example-task/references\\n.agents/skills/example-task/references/prompt-patterns.md\\n.agents/skills/example-task/scripts\\n.agents/skills/example-task/scripts/inspect_session.py"}],"toolCalls":[],"toolCallId":"tool_3mtoDjRlqenM0GDSjv9xRzLS"}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"让我读取 references/prompt-patterns.md 和 scripts/inspect_session.py 来了解完整的 skill 资源。"}],"toolCalls":[{"type":"function","id":"tool_nqkbedqxXK1aX9zAWWNEhzmM","name":"ReadFile","arguments":"{\\"path\\": \\".agents/skills/example-task/references/prompt-patterns.md\\"}"},{"type":"function","id":"tool_300OLXjS5RZxFz9LvWJP6QyJ","name":"ReadFile","arguments":"{\\"path\\": \\".agents/skills/example-task/scripts/inspect_session.py\\"}"}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"让我读取 references/prompt-patterns.md 和 scripts/inspect_session.py 来了解完整的 skill 资源。"}],"toolCalls":[{"type":"function","id":"tool_nqkbedqxXK1aX9zAWWNEhzmM","function":{"name":"ReadFile","arguments":"{\\"path\\": \\".agents/skills/example-task/references/prompt-patterns.md\\"}"}},{"type":"function","id":"tool_300OLXjS5RZxFz9LvWJP6QyJ","function":{"name":"ReadFile","arguments":"{\\"path\\": \\".agents/skills/example-task/scripts/inspect_session.py\\"}"}}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"85 lines read from file starting from line 1. End of file reached."},{"type":"text","text":" 1\\t# Prompt 模板\\n 2\\t\\n 3\\t以下模板作为脚手架使用。运行前替换占位符。\\n 4\\t\\n 5\\t## 单轮还是多轮\\n 6\\t\\n 7\\t满足以下任一条件时使用多轮:\\n 8\\t\\n 9\\t- 功能有状态\\n 10\\t- 功能依赖时序或并发\\n 11\\t- 功能需要审批、清理或恢复\\n 12\\t- session 产物本身是证据的一部分\\n 13\\t- 工具接口可能近期发生过变化\\n 14\\t\\n 15\\t仅对无状态的窄范围检查使用单轮。\\n 16\\t\\n 17\\t## 变量\\n 18\\t\\n 19\\t起草 prompt 前填写以下字段:\\n 20\\t\\n 21\\t- \`\` — 被测功能名称\\n 22\\t- \`\` — 当前场景的目标\\n 23\\t- \`\` — 需要阅读的源码路径\\n 24\\t- \`\` — 执行约束\\n 25\\t- \`\` — 成功信号\\n 26\\t- \`321 lines read from file starting from line 1. End of file reached."},{"type":"text","text":" 1\\t#!/usr/bin/env python3\\n 2\\t\\"\\"\\"Locate and summarize a Kimi CLI session for smoke-test review.\\"\\"\\"\\n 3\\t\\n 4\\tfrom __future__ import annotations\\n 5\\t\\n 6\\timport argparse\\n 7\\timport json\\n 8\\timport sys\\n 9\\tfrom collections import Counter\\n 10\\tfrom pathlib import Path\\n 11\\tfrom typing import Any\\n 12\\t\\n 13\\t\\n 14\\tdef parse_args() -> argparse.Namespace:\\n 15\\t parser = argparse.ArgumentParser(\\n 16\\t description=\\"Locate and summarize a Kimi CLI session... [truncated]"}],"toolCalls":[],"toolCallId":"tool_300OLXjS5RZxFz9LvWJP6QyJ"}} {"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"好的,我现在已经完整了解了 \`example-task\` skill 的内容。这个 skill 提供了:\\n\\n1. **SKILL.md** - 主文档,描述了如何针对 the example CLI 的新增或变更功能进行端到端冒烟测试的完整流程\\n2. **references/prompt-patterns.md** - 可复用的 prompt 模板\\n3. **scripts/inspect_session.py** - 用于检查和分析 session 产物的脚本\\n\\n用户似乎是在测试我是否能正确读取和使用这个 skill。既然用户已经在 prompt 中提供了完整的 skill 内容,我应该:\\n1. 确认我已经理解了 skill 的内容\\n2. 询问用户是否有具体的功能需要我使用这个 skill 来进行冒烟测试\\n\\n或者,用户可能是想了解这个 skill 的用法。让我给出一个总结并询问用户的具体需求。"},{"type":"text","text":"我已完整阅读 \`example-task\` skill。这是一个用于对 the example CLI 进行端到端冒烟测试的系统化流程。\\n\\n## Skill 要点总结\\n\\n**核心流程:**\\n1. **确定测试范围** - 从 \`git diff\` 推断功能边界\\n2. **读取事实来源** - 文档、代码、agent prompt、测试\\n3. **制定最小测试计划** - 正常路径、边界条件、中断/清理\\n4. **多轮执行** - 探索→执行→观察→清理\\n5. **非交互模式运行** - 使用 \`--print --work-dir /tmp/...\`\\n6. **检查产物** - \`context.jsonl\`, \`wire.jsonl\`, 后台任务等\\n7. **问题探查** - 并行多路追踪根因\\n\\n**可用资源:**\\n- \`references/prompt-patterns.md\` - 探索/执行/观察/复盘 prompt 模板\\n- \`scripts/inspect_session.py\` - Session 产物分析工具\\n\\n---\\n\\n**你想让我使用这个 skill 来测试... [truncated]"}],"toolCalls":[]}} {"type":"context.append_message","message":{"role":"user","content":[{"type":"text","text":"请基于 README 和主要源代码目录,给出一份简短的探索清单"}],"toolCalls":[]}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"用户想让我直接使用 \`git diff main --name-only\` 来查看当前分支相对于 main 分支的变更,然后基于这些变更进行冒烟测试。\\n\\n让我先执行这个命令看看有哪些文件变更。"}],"toolCalls":[{"type":"function","id":"tool_jOGB6wicU1PmefwL15U57RUT","name":"Shell","arguments":"{\\"command\\": \\"git diff main --name-only && echo \\\\\\"---\\\\\\" && git diff main --stat\\"}"}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"用户想让我直接使用 \`git diff main --name-only\` 来查看当前分支相对于 main 分支的变更,然后基于这些变更进行冒烟测试。\\n\\n让我先执行这个命令看看有哪些文件变更。"}],"toolCalls":[{"type":"function","id":"tool_jOGB6wicU1PmefwL15U57RUT","function":{"name":"Shell","arguments":"{\\"command\\": \\"git diff main --name-only && echo \\\\\\"---\\\\\\" && git diff main --stat\\"}"}}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Command executed successfully."},{"type":"text","text":".agents/skills/example-task/SKILL.md\\n.agents/skills/example-task/references/prompt-patterns.md\\n.agents/skills/example-task/scripts/inspect_session.py\\nCHANGELOG.md\\nfeature-overview.md\\ndocs/en/configuration/data-locations.md\\ndocs/en/customization/wire-mode.md\\ndocs/en/guides/interaction.md\\ndocs/en/guides/sessions.md\\ndocs/en/reference/keyboard.md\\ndocs/en/reference/kimi-command.md\\ndocs/en/reference/kimi-info.md\\ndocs/en/reference/kimi-vis.md\\ndocs/en/release-notes/changelog.md\\ndoc... [truncated]"}],"toolCalls":[],"toolCallId":"tool_jOGB6wicU1PmefwL15U57RUT"}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"从 git diff 的输出来看,这是一个非常大的变更集,主要涉及以下几个核心功能:\\n\\n1. **后台任务系统 (background/)** - 全新模块\\n - \`src/example_pkg/background/\` - 后台任务管理核心\\n - \`src/example_pkg/tools/background/\` - 后台任务工具\\n\\n2. **通知系统 (notifications/)** - 全新模块\\n - \`src/example_pkg/notifications/\` - 通知管理\\n\\n3. **Shell UI 重构** - 大规模重构\\n - \`src/example_pkg/ui/shell/\` - Shell UI 组件重构\\n - 移除了大量旧代码(placeholders, echo 等)\\n\\n4. **Wire 协议更新**\\n - \`src/example_pkg/wire/types.py\` 和 \`protocol.py\`\\n\\n5. **动态注入 (dynamic_injection.py)**\\n - 更新\\n\\n6. **测试覆盖**\\n - 大量新增测试\\n\\n这是一个... [truncated]"},{"type":"text","text":"根据 git diff 输出,这是一个大规模功能变更,主要涉及:\\n\\n1. **后台任务系统** (\`src/example_pkg/background/\`) - 全新模块\\n2. **通知系统** (\`src/example_pkg/notifications/\`) - 全新模块 \\n3. **后台任务工具** (\`src/example_pkg/tools/background/\`) - 新工具\\n4. **Shell UI 重构** (\`src/example_pkg/ui/shell/\`) - 大量重构\\n5. **Wire 协议更新** (\`src/example_pkg/wire/types.py\`)\\n6. **动态注入更新** (\`src/example_pkg/soul/dynamic_injection.py\`)\\n\\n让我开始冒烟测试流程,首先读取事实来源:"}],"toolCalls":[{"type":"function","id":"tool_TB9CacXCzJ8B11tzzOneV7MK","name":"ReadFile","arguments":"{\\"path\\": \\"feature-overview.md\\"}"},{"type":"function","id":"tool_7tKhkdZsBAFotRMTFs0BmgM6","name":"ReadFile","arguments":"{\\"path\\": \\"src/example_pkg/background/manager.py\\"}"},{"type":"function","id":"tool_DR5VfRHgGe7WZ1dXKSA5Yowh","name":"ReadFile","arguments":"{\\"path\\": \\"src/example_pkg/background/models.py\\"}"},{"type":"function","id":"tool_XZXYciY6FY2Tl4SHwuh7Elhl","name":"ReadFile","arguments":"{\\"path\\": \\"src/example_pkg/tools/background/__init__.py\\"}"}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"从 git diff 的输出来看,这是一个非常大的变更集,主要涉及以下几个核心功能:\\n\\n1. **后台任务系统 (background/)** - 全新模块\\n - \`src/example_pkg/background/\` - 后台任务管理核心\\n - \`src/example_pkg/tools/background/\` - 后台任务工具\\n\\n2. **通知系统 (notifications/)** - 全新模块\\n - \`src/example_pkg/notifications/\` - 通知管理\\n\\n3. **Shell UI 重构** - 大规模重构\\n - \`src/example_pkg/ui/shell/\` - Shell UI 组件重构\\n - 移除了大量旧代码(placeholders, echo 等)\\n\\n4. **Wire 协议更新**\\n - \`src/example_pkg/wire/types.py\` 和 \`protocol.py\`\\n\\n5. **动态注入 (dynamic_injection.py)**\\n - 更新\\n\\n6. **测试覆盖**\\n - 大量新增测试\\n\\n这是一个... [truncated]"},{"type":"text","text":"根据 git diff 输出,这是一个大规模功能变更,主要涉及:\\n\\n1. **后台任务系统** (\`src/example_pkg/background/\`) - 全新模块\\n2. **通知系统** (\`src/example_pkg/notifications/\`) - 全新模块 \\n3. **后台任务工具** (\`src/example_pkg/tools/background/\`) - 新工具\\n4. **Shell UI 重构** (\`src/example_pkg/ui/shell/\`) - 大量重构\\n5. **Wire 协议更新** (\`src/example_pkg/wire/types.py\`)\\n6. **动态注入更新** (\`src/example_pkg/soul/dynamic_injection.py\`)\\n\\n让我开始冒烟测试流程,首先读取事实来源:"}],"toolCalls":[{"type":"function","id":"tool_TB9CacXCzJ8B11tzzOneV7MK","function":{"name":"ReadFile","arguments":"{\\"path\\": \\"feature-overview.md\\"}"}},{"type":"function","id":"tool_7tKhkdZsBAFotRMTFs0BmgM6","function":{"name":"ReadFile","arguments":"{\\"path\\": \\"src/example_pkg/background/manager.py\\"}"}},{"type":"function","id":"tool_DR5VfRHgGe7WZ1dXKSA5Yowh","function":{"name":"ReadFile","arguments":"{\\"path\\": \\"src/example_pkg/background/models.py\\"}"}},{"type":"function","id":"tool_XZXYciY6FY2Tl4SHwuh7Elhl","function":{"name":"ReadFile","arguments":"{\\"path\\": \\"src/example_pkg/tools/background/__init__.py\\"}"}}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"353 lines read from file starting from line 1. End of file reached."},{"type":"text","text":" 1\\t# 后台任务实现说明\\n 2\\t\\n 3\\t这份文档是给后续开发者快速理解当前 background task / notification 实现用的内部说明,不是用户文档。\\n 4\\t\\n 5\\t## 一句话结论\\n 6\\t\\n 7\\t当前实现已经不再把 background task completion 建模成 \`dynamic injection\` 的一个特例。\\n 8\\t\\n 9\\t现在的结构是:\\n 10\\t\\n 11\\t- \`Shell(run_in_background=true)\` 负责启动后台任务\\n 12\\t- \`TaskList\` / \`TaskOutput\` / \`TaskStop\` 负责最小控制面\\n 13\\t- \`BackgroundTaskManager\` 负责 task 生命周期\\n 14\\t- \`NotificationManager\` 负责通用 notification 基建\\n 15\\t- \`task notification\` 只是 notification 基建里的一个 type\\n ... [truncated]"}],"toolCalls":[],"toolCallId":"tool_TB9CacXCzJ8B11tzzOneV7MK"}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"358 lines read from file starting from line 1. End of file reached."},{"type":"text","text":" 1\\tfrom __future__ import annotations\\n 2\\t\\n 3\\timport asyncio\\n 4\\timport os\\n 5\\timport signal\\n 6\\timport subprocess\\n 7\\timport sys\\n 8\\timport time\\n 9\\tfrom pathlib import Path\\n 10\\t\\n 11\\tfrom kaos.local import local_kaos\\n 12\\t\\n 13\\tfrom example_pkg.config import BackgroundConfig\\n 14\\tfrom example_pkg.notifications import NotificationEvent, NotificationManager\\n 15\\tfrom example_pkg.session import Session\\n 16\\tfrom example_pkg.utils.logging import logger\\n 17\\t\\n 1... [truncated]"}],"toolCalls":[],"toolCallId":"tool_7tKhkdZsBAFotRMTFs0BmgM6"}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"89 lines read from file starting from line 1. End of file reached."},{"type":"text","text":" 1\\tfrom __future__ import annotations\\n 2\\t\\n 3\\timport time\\n 4\\tfrom typing import Literal\\n 5\\t\\n 6\\tfrom pydantic import BaseModel, ConfigDict, Field\\n 7\\t\\n 8\\ttype TaskKind = Literal[\\"bash\\", \\"agent\\"]\\n 9\\ttype TaskStatus = Literal[\\"created\\", \\"starting\\", \\"running\\", \\"completed\\", \\"failed\\", \\"killed\\", \\"lost\\"]\\n 10\\ttype TaskOwnerRole = Literal[\\"root\\", \\"fixed_subagent\\", \\"dynamic_subagent\\"]\\n 11\\t\\n 12\\tTERMINAL_TASK_STATUSES: tuple[TaskStatus, ...] = (\\"completed\\", \\"failed\\"... [truncated]"}],"toolCalls":[],"toolCallId":"tool_DR5VfRHgGe7WZ1dXKSA5Yowh"}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"235 lines read from file starting from line 1. End of file reached."},{"type":"text","text":" 1\\timport time\\n 2\\tfrom pathlib import Path\\n 3\\tfrom typing import override\\n 4\\t\\n 5\\tfrom kosong.tooling import CallableTool2, ToolError, ToolReturnValue\\n 6\\tfrom pydantic import BaseModel, Field\\n 7\\t\\n 8\\tfrom example_pkg.background import format_task, format_task_list, list_task_views\\n 9\\tfrom example_pkg.soul.agent import Runtime\\n 10\\tfrom example_pkg.soul.approval import Approval\\n 11\\tfrom example_pkg.tools.display import BackgroundTaskDisplayBlock\\n 12\\tfrom example_pkg.too... [truncated]"}],"toolCalls":[],"toolCallId":"tool_XZXYciY6FY2Tl4SHwuh7Elhl"}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"好的,我已经读取了关键的事实来源。现在让我继续读取其他重要文件来理解完整的功能边界:\\n\\n1. Shell 工具的后台模式支持\\n2. Notification 系统\\n3. Wire 协议更新\\n4. KimiSoul 中的 notification 集成\\n\\n让我继续读取这些文件。"}],"toolCalls":[{"type":"function","id":"tool_4vAGVbvQFNnyAFHtAkgnKwUu","name":"ReadFile","arguments":"{\\"path\\": \\"src/example_pkg/tools/shell/__init__.py\\"}"},{"type":"function","id":"tool_YRsRNEO743zdY5vBEYPYv5QJ","name":"ReadFile","arguments":"{\\"path\\": \\"src/example_pkg/notifications/manager.py\\"}"},{"type":"function","id":"tool_Zi0gi1gBQijIF8lEEx1RiSPz","name":"ReadFile","arguments":"{\\"path\\": \\"src/example_pkg/wire/types.py\\"}"},{"type":"function","id":"tool_SorRMdJIzuUdPESVPYIbDqQU","name":"ReadFile","arguments":"{\\"path\\": \\"src/example_pkg/soul/kimisoul.py\\", \\"n_lines\\": 150}"}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"好的,我已经读取了关键的事实来源。现在让我继续读取其他重要文件来理解完整的功能边界:\\n\\n1. Shell 工具的后台模式支持\\n2. Notification 系统\\n3. Wire 协议更新\\n4. KimiSoul 中的 notification 集成\\n\\n让我继续读取这些文件。"}],"toolCalls":[{"type":"function","id":"tool_4vAGVbvQFNnyAFHtAkgnKwUu","function":{"name":"ReadFile","arguments":"{\\"path\\": \\"src/example_pkg/tools/shell/__init__.py\\"}"}},{"type":"function","id":"tool_YRsRNEO743zdY5vBEYPYv5QJ","function":{"name":"ReadFile","arguments":"{\\"path\\": \\"src/example_pkg/notifications/manager.py\\"}"}},{"type":"function","id":"tool_Zi0gi1gBQijIF8lEEx1RiSPz","function":{"name":"ReadFile","arguments":"{\\"path\\": \\"src/example_pkg/wire/types.py\\"}"}},{"type":"function","id":"tool_SorRMdJIzuUdPESVPYIbDqQU","function":{"name":"ReadFile","arguments":"{\\"path\\": \\"src/example_pkg/soul/kimisoul.py\\", \\"n_lines\\": 150}"}}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"216 lines read from file starting from line 1. End of file reached."},{"type":"text","text":" 1\\timport asyncio\\n 2\\tfrom collections.abc import Callable\\n 3\\tfrom pathlib import Path\\n 4\\tfrom typing import Self, override\\n 5\\t\\n 6\\timport kaos\\n 7\\tfrom kaos import AsyncReadable\\n 8\\tfrom kosong.tooling import CallableTool2, ToolReturnValue\\n 9\\tfrom pydantic import BaseModel, Field, model_validator\\n 10\\t\\n 11\\tfrom example_pkg.background import TaskView, format_task\\n 12\\tfrom example_pkg.soul.agent import Runtime\\n 13\\tfrom example_pkg.soul.approval import Approval\\n ... [truncated]"}],"toolCalls":[],"toolCallId":"tool_4vAGVbvQFNnyAFHtAkgnKwUu"}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"105 lines read from file starting from line 1. End of file reached."},{"type":"text","text":" 1\\tfrom __future__ import annotations\\n 2\\t\\n 3\\timport time\\n 4\\timport uuid\\n 5\\tfrom pathlib import Path\\n 6\\t\\n 7\\tfrom example_pkg.config import NotificationConfig\\n 8\\t\\n 9\\tfrom .models import (\\n 10\\t NotificationDelivery,\\n 11\\t NotificationEvent,\\n 12\\t NotificationSink,\\n 13\\t NotificationSinkState,\\n 14\\t NotificationView,\\n 15\\t)\\n 16\\tfrom .store import NotificationStore\\n 17\\t\\n 18\\t\\n 19\\tclass NotificationManager:\\n 20\\t def __init__... [truncated]"}],"toolCalls":[],"toolCallId":"tool_YRsRNEO743zdY5vBEYPYv5QJ"}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"515 lines read from file starting from line 1. End of file reached."},{"type":"text","text":" 1\\tfrom __future__ import annotations\\n 2\\t\\n 3\\timport asyncio\\n 4\\tfrom typing import Any, Literal, TypeGuard, cast\\n 5\\t\\n 6\\tfrom kosong.chat_provider import TokenUsage\\n 7\\tfrom kosong.message import (\\n 8\\t AudioURLPart,\\n 9\\t ContentPart,\\n 10\\t ImageURLPart,\\n 11\\t TextPart,\\n 12\\t ThinkPart,\\n 13\\t ToolCall,\\n 14\\t ToolCallPart,\\n 15\\t VideoURLPart,\\n 16\\t)\\n 17\\tfrom kosong.tooling import (\\n 18\\t BriefDisplayBlock,\\n 19\\t Dis... [truncated]"}],"toolCalls":[],"toolCallId":"tool_Zi0gi1gBQijIF8lEEx1RiSPz"}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"150 lines read from file starting from line 1."},{"type":"text","text":" 1\\tfrom __future__ import annotations\\n 2\\t\\n 3\\timport asyncio\\n 4\\tfrom collections.abc import Awaitable, Callable, Sequence\\n 5\\tfrom contextlib import suppress\\n 6\\tfrom dataclasses import dataclass\\n 7\\tfrom functools import partial\\n 8\\tfrom pathlib import Path\\n 9\\tfrom typing import TYPE_CHECKING, Any, Literal\\n 10\\t\\n 11\\timport kosong\\n 12\\timport tenacity\\n 13\\tfrom kosong import StepResult\\n 14\\tfrom kosong.chat_provider import (\\n 15\\t APIConnectionError... [truncated]"}],"toolCalls":[],"toolCallId":"tool_SorRMdJIzuUdPESVPYIbDqQU"}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"我已经获取了足够的背景信息。让我继续读取 KimiSoul 中 notification 相关的代码,以及相关的测试来理解完整的功能边界。"}],"toolCalls":[{"type":"function","id":"tool_okFY9oLifSxqJb8egNPac0MS","name":"ReadFile","arguments":"{\\"path\\": \\"src/example_pkg/soul/kimisoul.py\\", \\"line_offset\\": 200, \\"n_lines\\": 200}"},{"type":"function","id":"tool_CcxVl6nzWr4TfMaivaLiZxsz","name":"Grep","arguments":"{\\"pattern\\": \\"def _step|notification|claim|publish\\", \\"path\\": \\"src/example_pkg/soul/kimisoul.py\\", \\"output_mode\\": \\"content\\", \\"-n\\": true}"}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"我已经获取了足够的背景信息。让我继续读取 KimiSoul 中 notification 相关的代码,以及相关的测试来理解完整的功能边界。"}],"toolCalls":[{"type":"function","id":"tool_okFY9oLifSxqJb8egNPac0MS","function":{"name":"ReadFile","arguments":"{\\"path\\": \\"src/example_pkg/soul/kimisoul.py\\", \\"line_offset\\": 200, \\"n_lines\\": 200}"}},{"type":"function","id":"tool_CcxVl6nzWr4TfMaivaLiZxsz","function":{"name":"Grep","arguments":"{\\"pattern\\": \\"def _step|notification|claim|publish\\", \\"path\\": \\"src/example_pkg/soul/kimisoul.py\\", \\"output_mode\\": \\"content\\", \\"-n\\": true}"}}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"200 lines read from file starting from line 200."},{"type":"text","text":" 200\\t\\n 201\\t def path_getter() -> Path | None:\\n 202\\t return self.get_plan_file_path()\\n 203\\t\\n 204\\t # WriteFile gets both checker and path_getter (for plan file auto-approve)\\n 205\\t from example_pkg.tools.file.write import WriteFile\\n 206\\t\\n 207\\t write_tool = self._agent.toolset.find(\\"WriteFile\\")\\n 208\\t if isinstance(write_tool, WriteFile):\\n 209\\t write_tool.bind_plan_mode(checker, path_getter)\\n 210\\t\\n 211\\t # ExitPlanMod... [truncated]"}],"toolCalls":[],"toolCallId":"tool_okFY9oLifSxqJb8egNPac0MS"}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"26:from example_pkg.notifications import build_notification_message, extract_notification_ids\\n147: self._runtime.notifications.ack_ids(\\"llm\\", extract_notification_ids(context.history))\\n626: async def _step(self) -> StepOutcome | None:\\n634: notifications = self._runtime.notifications.claim_for_sink(\\"llm\\", limit=4)\\n635: if notifications:\\n637: build_notification_message(view, self._runtime) for view in notifications\\n640: self._runti... [truncated]"}],"toolCalls":[],"toolCallId":"tool_CcxVl6nzWr4TfMaivaLiZxsz"}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"现在我对功能有了完整的理解。让我总结一下测试范围:\\n\\n**功能边界:**\\n1. **后台任务系统** - 创建、监控、停止、输出获取\\n2. **通知系统** - 发布、消费、去重、确认\\n3. **集成点** - Shell 后台模式、TaskList/TaskOutput/TaskStop 工具、Wire 通知事件、LLM 通知消息\\n\\n**用户可感知的行为变化:**\\n1. Shell 工具新增 \`run_in_background\` 和 \`description\` 参数\\n2. 新增 TaskList/TaskOutput/TaskStop 工具\\n3. 后台任务完成后自动发送通知到 LLM context 和 Wire\\n4. 通知通过独立的 \`\` 消息进入 context\\n5. Wire 协议新增 \`Notification\` 事件类型\\n\\n**测试计划:**\\n1. **正常路径** - 启动后台任务,等待完成,验证通知\\n2. **边界条件** - 超时处理、重复停止、无效 task_id\\n3. **中断/清理** - 主动停止任务、任务丢失恢复\\n\\n让我开始制定... [truncated]"},{"type":"text","text":"现在我对功能有了完整理解。让我制定测试计划:\\n\\n## 功能边界总结\\n\\n**主要变更:**\\n1. **后台任务系统** (\`background/\`) - 全新模块,支持后台 bash 任务\\n2. **通知系统** (\`notifications/\`) - 独立基建,支持多 sink (llm/wire/shell)\\n3. **新工具** - \`TaskList\` / \`TaskOutput\` / \`TaskStop\`\\n4. **Shell 工具更新** - 新增 \`run_in_background\` + \`description\` 参数\\n5. **Wire 协议** - 新增 \`Notification\` 事件类型\\n6. **KimiSoul 集成** - 自动 claim/ack notification,compaction 后恢复活跃任务快照\\n\\n**测试范围:**\\n- 后台任务生命周期(创建→运行→完成→通知)\\n- 任务控制工具(List/Output/Stop)\\n- 通知系统(去重、claim/ack、多 sink)\\n- 集成点(Shell 后台模式、Wire 事件、LLM ... [truncated]"}],"toolCalls":[{"type":"function","id":"tool_y3SXWWQIUysddnYoklaWhUeE","name":"SetTodoList","arguments":"{\\"todos\\": [{\\"title\\": \\"准备测试环境(创建隔离 work-dir)\\", \\"status\\": \\"in_progress\\"}, {\\"title\\": \\"探索轮:验证工具可用性和接口\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"执行轮1:正常路径 - 后台任务生命周期\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"执行轮2:边界条件 - 超时和停止\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"执行轮3:中断场景 - TaskStop 和恢复\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"观察轮:检查 session 产物\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"汇报结论\\", \\"status\\": \\"pending\\"}]}"},{"type":"function","id":"tool_BH2zd6kVzHhV4pIjKLSmrUeB","name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"$(mktemp -d /tmp/kimi-smoke-XXXXXX)\\\\\\"\\\\necho \\\\\\"SMOKE_DIR=$SMOKE_DIR\\\\\\"\\\\n# 验证 CLI 可运行\\\\nuv run python -m example_pkg.cli --help 2>&1 | head -20\\"}"}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"现在我对功能有了完整的理解。让我总结一下测试范围:\\n\\n**功能边界:**\\n1. **后台任务系统** - 创建、监控、停止、输出获取\\n2. **通知系统** - 发布、消费、去重、确认\\n3. **集成点** - Shell 后台模式、TaskList/TaskOutput/TaskStop 工具、Wire 通知事件、LLM 通知消息\\n\\n**用户可感知的行为变化:**\\n1. Shell 工具新增 \`run_in_background\` 和 \`description\` 参数\\n2. 新增 TaskList/TaskOutput/TaskStop 工具\\n3. 后台任务完成后自动发送通知到 LLM context 和 Wire\\n4. 通知通过独立的 \`\` 消息进入 context\\n5. Wire 协议新增 \`Notification\` 事件类型\\n\\n**测试计划:**\\n1. **正常路径** - 启动后台任务,等待完成,验证通知\\n2. **边界条件** - 超时处理、重复停止、无效 task_id\\n3. **中断/清理** - 主动停止任务、任务丢失恢复\\n\\n让我开始制定... [truncated]"},{"type":"text","text":"现在我对功能有了完整理解。让我制定测试计划:\\n\\n## 功能边界总结\\n\\n**主要变更:**\\n1. **后台任务系统** (\`background/\`) - 全新模块,支持后台 bash 任务\\n2. **通知系统** (\`notifications/\`) - 独立基建,支持多 sink (llm/wire/shell)\\n3. **新工具** - \`TaskList\` / \`TaskOutput\` / \`TaskStop\`\\n4. **Shell 工具更新** - 新增 \`run_in_background\` + \`description\` 参数\\n5. **Wire 协议** - 新增 \`Notification\` 事件类型\\n6. **KimiSoul 集成** - 自动 claim/ack notification,compaction 后恢复活跃任务快照\\n\\n**测试范围:**\\n- 后台任务生命周期(创建→运行→完成→通知)\\n- 任务控制工具(List/Output/Stop)\\n- 通知系统(去重、claim/ack、多 sink)\\n- 集成点(Shell 后台模式、Wire 事件、LLM ... [truncated]"}],"toolCalls":[{"type":"function","id":"tool_y3SXWWQIUysddnYoklaWhUeE","function":{"name":"SetTodoList","arguments":"{\\"todos\\": [{\\"title\\": \\"准备测试环境(创建隔离 work-dir)\\", \\"status\\": \\"in_progress\\"}, {\\"title\\": \\"探索轮:验证工具可用性和接口\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"执行轮1:正常路径 - 后台任务生命周期\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"执行轮2:边界条件 - 超时和停止\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"执行轮3:中断场景 - TaskStop 和恢复\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"观察轮:检查 session 产物\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"汇报结论\\", \\"status\\": \\"pending\\"}]}"}},{"type":"function","id":"tool_BH2zd6kVzHhV4pIjKLSmrUeB","function":{"name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"$(mktemp -d /tmp/kimi-smoke-XXXXXX)\\\\\\"\\\\necho \\\\\\"SMOKE_DIR=$SMOKE_DIR\\\\\\"\\\\n# 验证 CLI 可运行\\\\nuv run python -m example_pkg.cli --help 2>&1 | head -20\\"}"}}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Todo list updated"}],"toolCalls":[],"toolCallId":"tool_y3SXWWQIUysddnYoklaWhUeE"}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Command executed successfully."},{"type":"text","text":"SMOKE_DIR=/tmp/kimi-smoke-a2i1xj\\n \\n Usage: python -m example_pkg.cli [OPTIONS] COMMAND [ARGS]... \\n \\n The Starting Point for Next-Gen Agents \\n \\n╭─ Options ───────────────────────────────────────────────────... [truncated]"}],"toolCalls":[],"toolCallId":"tool_BH2zd6kVzHhV4pIjKLSmrUeB"}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"环境准备好了,现在让我开始第一轮探索测试,验证工具可用性。"}],"toolCalls":[{"type":"function","id":"tool_mj72EeaBIxBi3NOVwE3tjxBc","name":"SetTodoList","arguments":"{\\"todos\\": [{\\"title\\": \\"准备测试环境(创建隔离 work-dir)\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"探索轮:验证工具可用性和接口\\", \\"status\\": \\"in_progress\\"}, {\\"title\\": \\"执行轮1:正常路径 - 后台任务生命周期\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"执行轮2:边界条件 - 超时和停止\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"执行轮3:中断场景 - TaskStop 和恢复\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"观察轮:检查 session 产物\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"汇报结论\\", \\"status\\": \\"pending\\"}]}"},{"type":"function","id":"tool_ocohNUErNzzNQnYsibl670ms","name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\n\\\\n# 探索轮:验证后台任务相关工具是否可用\\\\nuv run python -m example_pkg.cli \\\\\\\\\\\\n --print \\\\\\\\\\\\n --prompt \\\\\\"请列出你当前所有可用的工具,特别关注:\\\\n1. 是否有 TaskList、TaskOutput、TaskStop 这三个工具?\\\\n2. Shell 工具是否支持 run_in_background 参数?\\\\n3. 简单描述这些工具的功能。\\\\\\" \\\\\\\\\\\\n --work-dir \\\\\\"$SMOKE_DIR\\\\\\" 2>&1\\\\necho \\\\\\"exit_code=$?\\\\\\"\\", \\"timeout\\": 120}"}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"环境准备好了,现在让我开始第一轮探索测试,验证工具可用性。"}],"toolCalls":[{"type":"function","id":"tool_mj72EeaBIxBi3NOVwE3tjxBc","function":{"name":"SetTodoList","arguments":"{\\"todos\\": [{\\"title\\": \\"准备测试环境(创建隔离 work-dir)\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"探索轮:验证工具可用性和接口\\", \\"status\\": \\"in_progress\\"}, {\\"title\\": \\"执行轮1:正常路径 - 后台任务生命周期\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"执行轮2:边界条件 - 超时和停止\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"执行轮3:中断场景 - TaskStop 和恢复\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"观察轮:检查 session 产物\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"汇报结论\\", \\"status\\": \\"pending\\"}]}"}},{"type":"function","id":"tool_ocohNUErNzzNQnYsibl670ms","function":{"name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\n\\\\n# 探索轮:验证后台任务相关工具是否可用\\\\nuv run python -m example_pkg.cli \\\\\\\\\\\\n --print \\\\\\\\\\\\n --prompt \\\\\\"请列出你当前所有可用的工具,特别关注:\\\\n1. 是否有 TaskList、TaskOutput、TaskStop 这三个工具?\\\\n2. Shell 工具是否支持 run_in_background 参数?\\\\n3. 简单描述这些工具的功能。\\\\\\" \\\\\\\\\\\\n --work-dir \\\\\\"$SMOKE_DIR\\\\\\" 2>&1\\\\necho \\\\\\"exit_code=$?\\\\\\"\\", \\"timeout\\": 120}"}}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Todo list updated"}],"toolCalls":[],"toolCallId":"tool_mj72EeaBIxBi3NOVwE3tjxBc"}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Command executed successfully."},{"type":"text","text":"请列出你当前所有可用的工具,特别关注:\\n1. 是否有 TaskList、TaskOutput、TaskStop 这三个工具?\\n2. Shell 工具是否支持 run_in_background 参数?\\n3. 简单描述这些工具的功能。\\nTurnBegin(\\n user_input='请列出你当前所有可用的工具,特别关注:\\\\n1. 是否有 \\nTaskList、TaskOutput、TaskStop 这三个工具?\\\\n2. Shell 工具是否支持 \\nrun_in_background 参数?\\\\n3. 简单描述这些工具的功能。'\\n)\\nStepBegin(n=1)\\nThinkPart(\\n type='think',\\n think='用户询问我当前可用的工具列表,特别关注三个问题:\\\\n1. 是否有 \\nTaskList、TaskOutput、TaskStop 这三个工具?\\\\n2. Shell 工具是否支持 \\nrun_in_background 参数?\\\\n3. \\n简单描述这些工具的功能。\\\\n\\\\n我需要查看我的工具列表。从 system \\n提示中,我可以看到我有以下工具:\\\\n- Tas... [truncated]"}],"toolCalls":[],"toolCallId":"tool_ocohNUErNzzNQnYsibl670ms"}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"探索轮成功,工具都已正确加载。现在进入执行轮1:测试正常路径 - 启动后台任务,等待完成,验证通知。"},{"type":"text","text":"探索轮成功!工具都已正确加载。现在执行第一轮测试:"}],"toolCalls":[{"type":"function","id":"tool_bpOwCgVz6wYah52SnaJB39ZW","name":"SetTodoList","arguments":"{\\"todos\\": [{\\"title\\": \\"准备测试环境(创建隔离 work-dir)\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"探索轮:验证工具可用性和接口\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮1:正常路径 - 后台任务生命周期\\", \\"status\\": \\"in_progress\\"}, {\\"title\\": \\"执行轮2:边界条件 - 超时和停止\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"执行轮3:中断场景 - TaskStop 和恢复\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"观察轮:检查 session 产物\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"汇报结论\\", \\"status\\": \\"pending\\"}]}"},{"type":"function","id":"tool_HejoTo6Izmlets9y9hN9u1pZ","name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\n\\\\n# 执行轮1:正常路径 - 启动一个短生命周期的后台任务,验证生命周期完整\\\\nuv run python -m example_pkg.cli \\\\\\\\\\\\n --print \\\\\\\\\\\\n --prompt \\\\\\"请执行以下测试:\\\\n\\\\n1. 使用 Shell 工具启动一个后台任务:\\\\n - 命令: echo 'hello from background' && sleep 2 && echo 'task done'\\\\n - run_in_background: true\\\\n - description: '测试后台任务生命周期'\\\\n - timeout: 30\\\\n\\\\n2. 记录返回的 task_id\\\\n\\\\n3. 使用 TaskList 查看当前活跃任务\\\\n\\\\n4. 使用 TaskOutput 获取任务输出(block=true, timeout=10)\\\\n\\\\n请报告:task_id、任务状态、输出内容。\\\\\\" \\\\\\\\\\\\n --work-dir \\\\\\"$SMOKE_DIR\\\\\\" 2>&... [truncated]"}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"探索轮成功,工具都已正确加载。现在进入执行轮1:测试正常路径 - 启动后台任务,等待完成,验证通知。"},{"type":"text","text":"探索轮成功!工具都已正确加载。现在执行第一轮测试:"}],"toolCalls":[{"type":"function","id":"tool_bpOwCgVz6wYah52SnaJB39ZW","function":{"name":"SetTodoList","arguments":"{\\"todos\\": [{\\"title\\": \\"准备测试环境(创建隔离 work-dir)\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"探索轮:验证工具可用性和接口\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮1:正常路径 - 后台任务生命周期\\", \\"status\\": \\"in_progress\\"}, {\\"title\\": \\"执行轮2:边界条件 - 超时和停止\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"执行轮3:中断场景 - TaskStop 和恢复\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"观察轮:检查 session 产物\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"汇报结论\\", \\"status\\": \\"pending\\"}]}"}},{"type":"function","id":"tool_HejoTo6Izmlets9y9hN9u1pZ","function":{"name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\n\\\\n# 执行轮1:正常路径 - 启动一个短生命周期的后台任务,验证生命周期完整\\\\nuv run python -m example_pkg.cli \\\\\\\\\\\\n --print \\\\\\\\\\\\n --prompt \\\\\\"请执行以下测试:\\\\n\\\\n1. 使用 Shell 工具启动一个后台任务:\\\\n - 命令: echo 'hello from background' && sleep 2 && echo 'task done'\\\\n - run_in_background: true\\\\n - description: '测试后台任务生命周期'\\\\n - timeout: 30\\\\n\\\\n2. 记录返回的 task_id\\\\n\\\\n3. 使用 TaskList 查看当前活跃任务\\\\n\\\\n4. 使用 TaskOutput 获取任务输出(block=true, timeout=10)\\\\n\\\\n请报告:task_id、任务状态、输出内容。\\\\\\" \\\\\\\\\\\\n --work-dir \\\\\\"$SMOKE_DIR\\\\\\" 2>&... [truncated]"}}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Todo list updated"}],"toolCalls":[],"toolCallId":"tool_bpOwCgVz6wYah52SnaJB39ZW"}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Command executed successfully."},{"type":"text","text":"请执行以下测试:\\n\\n1. 使用 Shell 工具启动一个后台任务:\\n - 命令: echo 'hello from background' && sleep 2 && echo 'task done'\\n - run_in_background: true\\n - description: '测试后台任务生命周期'\\n - timeout: 30\\n\\n2. 记录返回的 task_id\\n\\n3. 使用 TaskList 查看当前活跃任务\\n\\n4. 使用 TaskOutput 获取任务输出(block=true, timeout=10)\\n\\n请报告:task_id、任务状态、输出内容。\\nTurnBegin(\\n user_input=\\"请执行以下测试:\\\\n\\\\n1. 使用 Shell 工具启动一个后台任务:\\\\n - \\n命令: echo 'hello from background' && sleep 2 && echo 'task done'\\\\n - \\nrun_in_background: true\\\\n - description: '测试后台任务生命周期'\\\\n - t... [truncated]"}],"toolCalls":[],"toolCallId":"tool_HejoTo6Izmlets9y9hN9u1pZ"}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"第一轮测试成功!关键验证点:\\n1. ✅ Shell 后台任务启动成功,返回 task_id\\n2. ✅ TaskList 正确工作\\n3. ✅ TaskOutput 正确工作,block=true 等待完成\\n4. ✅ Notification 事件正确发送(Wire 中可见 Notification 事件)\\n\\n现在进行第二轮:边界条件 - 测试超时和停止功能。"},{"type":"text","text":"第一轮成功!所有核心功能正常工作。现在测试边界条件:"}],"toolCalls":[{"type":"function","id":"tool_3tueNNJt54oG2X47QK4PLnTj","name":"SetTodoList","arguments":"{\\"todos\\": [{\\"title\\": \\"准备测试环境(创建隔离 work-dir)\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"探索轮:验证工具可用性和接口\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮1:正常路径 - 后台任务生命周期\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮2:边界条件 - 超时和停止\\", \\"status\\": \\"in_progress\\"}, {\\"title\\": \\"执行轮3:中断场景 - TaskStop 和恢复\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"观察轮:检查 session 产物\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"汇报结论\\", \\"status\\": \\"pending\\"}]}"},{"type":"function","id":"tool_NT90qPui3vOOBqIjmTgvYe9Q","name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\n\\\\n# 执行轮2:边界条件 - 超时处理和停止\\\\nuv run python -m example_pkg.cli \\\\\\\\\\\\n --print \\\\\\\\\\\\n --prompt \\\\\\"请执行以下边界条件测试:\\\\n\\\\n1. 启动一个长时间运行的后台任务(会被停止):\\\\n - 命令: for i in 1 2 3 4 5; do echo \\\\\\\\\\\\\\"line \\\\\\\\$i\\\\\\\\\\\\\\"; sleep 5; done\\\\n - run_in_background: true\\\\n - description: '测试停止功能'\\\\n - timeout: 60\\\\n\\\\n2. 使用 TaskList 确认任务在运行中\\\\n\\\\n3. 等待 3 秒后,使用 TaskStop 停止该任务\\\\n\\\\n4. 使用 TaskOutput 获取任务最终状态和输出\\\\n\\\\n请报告:task_id、停止前后的状态、输出内容、是否有正确通知。\\\\\\" \\\\\\\\\\\\n --work-dir \\\\\\"$SMOKE_DIR\\\\\\" 2>&1\\\\necho \\\\\\"... [truncated]"}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"第一轮测试成功!关键验证点:\\n1. ✅ Shell 后台任务启动成功,返回 task_id\\n2. ✅ TaskList 正确工作\\n3. ✅ TaskOutput 正确工作,block=true 等待完成\\n4. ✅ Notification 事件正确发送(Wire 中可见 Notification 事件)\\n\\n现在进行第二轮:边界条件 - 测试超时和停止功能。"},{"type":"text","text":"第一轮成功!所有核心功能正常工作。现在测试边界条件:"}],"toolCalls":[{"type":"function","id":"tool_3tueNNJt54oG2X47QK4PLnTj","function":{"name":"SetTodoList","arguments":"{\\"todos\\": [{\\"title\\": \\"准备测试环境(创建隔离 work-dir)\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"探索轮:验证工具可用性和接口\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮1:正常路径 - 后台任务生命周期\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮2:边界条件 - 超时和停止\\", \\"status\\": \\"in_progress\\"}, {\\"title\\": \\"执行轮3:中断场景 - TaskStop 和恢复\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"观察轮:检查 session 产物\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"汇报结论\\", \\"status\\": \\"pending\\"}]}"}},{"type":"function","id":"tool_NT90qPui3vOOBqIjmTgvYe9Q","function":{"name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\n\\\\n# 执行轮2:边界条件 - 超时处理和停止\\\\nuv run python -m example_pkg.cli \\\\\\\\\\\\n --print \\\\\\\\\\\\n --prompt \\\\\\"请执行以下边界条件测试:\\\\n\\\\n1. 启动一个长时间运行的后台任务(会被停止):\\\\n - 命令: for i in 1 2 3 4 5; do echo \\\\\\\\\\\\\\"line \\\\\\\\$i\\\\\\\\\\\\\\"; sleep 5; done\\\\n - run_in_background: true\\\\n - description: '测试停止功能'\\\\n - timeout: 60\\\\n\\\\n2. 使用 TaskList 确认任务在运行中\\\\n\\\\n3. 等待 3 秒后,使用 TaskStop 停止该任务\\\\n\\\\n4. 使用 TaskOutput 获取任务最终状态和输出\\\\n\\\\n请报告:task_id、停止前后的状态、输出内容、是否有正确通知。\\\\\\" \\\\\\\\\\\\n --work-dir \\\\\\"$SMOKE_DIR\\\\\\" 2>&1\\\\necho \\\\\\"... [truncated]"}}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Todo list updated"}],"toolCalls":[],"toolCallId":"tool_3tueNNJt54oG2X47QK4PLnTj"}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Command executed successfully."},{"type":"text","text":"请执行以下边界条件测试:\\n\\n1. 启动一个长时间运行的后台任务(会被停止):\\n - 命令: for i in 1 2 3 4 5; do echo \\"line $i\\"; sleep 5; done\\n - run_in_background: true\\n - description: '测试停止功能'\\n - timeout: 60\\n\\n2. 使用 TaskList 确认任务在运行中\\n\\n3. 等待 3 秒后,使用 TaskStop 停止该任务\\n\\n4. 使用 TaskOutput 获取任务最终状态和输出\\n\\n请报告:task_id、停止前后的状态、输出内容、是否有正确通知。\\nTurnBegin(\\n user_input='请执行以下边界条件测试:\\\\n\\\\n1. \\n启动一个长时间运行的后台任务(会被停止):\\\\n - 命令: for i in 1 2 3 4 5; do \\necho \\"line $i\\"; sleep 5; done\\\\n - run_in_background: true\\\\n - description: \\n\\\\'测试停止功能\\\\'\\\\n - timeou... [truncated]"}],"toolCalls":[],"toolCallId":"tool_NT90qPui3vOOBqIjmTgvYe9Q"}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"第二轮也成功!测试了停止功能,验证了:\\n1. ✅ TaskStop 成功停止任务\\n2. ✅ 通知正确发送 (task.killed)\\n3. ✅ exit_code 正确 (-15 = SIGTERM)\\n4. ✅ 输出正确捕获\\n\\n现在进行第三轮测试:测试超时处理。"},{"type":"text","text":"第二轮成功!停止功能完全正常。现在测试超时场景:"}],"toolCalls":[{"type":"function","id":"tool_TS5JUSZELJoTVcos6rp90ZUT","name":"SetTodoList","arguments":"{\\"todos\\": [{\\"title\\": \\"准备测试环境(创建隔离 work-dir)\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"探索轮:验证工具可用性和接口\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮1:正常路径 - 后台任务生命周期\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮2:边界条件 - 超时和停止\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮3:超时场景测试\\", \\"status\\": \\"in_progress\\"}, {\\"title\\": \\"观察轮:检查 session 产物\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"汇报结论\\", \\"status\\": \\"pending\\"}]}"},{"type":"function","id":"tool_J3cQ5m3qyTep0zNLWhZR3OGg","name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\n\\\\n# 执行轮3:测试超时处理\\\\nuv run python -m example_pkg.cli \\\\\\\\\\\\n --print \\\\\\\\\\\\n --prompt \\\\\\"请测试后台任务超时功能:\\\\n\\\\n1. 启动一个会超时的后台任务:\\\\n - 命令: sleep 300\\\\n - run_in_background: true\\\\n - description: '测试超时处理'\\\\n - timeout: 3 (设置为3秒,确保会超时)\\\\n\\\\n2. 使用 TaskOutput 阻塞等待任务结束(block=true, timeout=15)\\\\n\\\\n3. 检查任务最终状态是否为 timeout\\\\n\\\\n请报告:task_id、最终状态、exit_code、是否有 timeout 通知。\\\\\\" \\\\\\\\\\\\n --work-dir \\\\\\"$SMOKE_DIR\\\\\\" 2>&1\\\\necho \\\\\\"exit_code=$?\\\\\\"\\", \\"timeout\\": 180}"}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"第二轮也成功!测试了停止功能,验证了:\\n1. ✅ TaskStop 成功停止任务\\n2. ✅ 通知正确发送 (task.killed)\\n3. ✅ exit_code 正确 (-15 = SIGTERM)\\n4. ✅ 输出正确捕获\\n\\n现在进行第三轮测试:测试超时处理。"},{"type":"text","text":"第二轮成功!停止功能完全正常。现在测试超时场景:"}],"toolCalls":[{"type":"function","id":"tool_TS5JUSZELJoTVcos6rp90ZUT","function":{"name":"SetTodoList","arguments":"{\\"todos\\": [{\\"title\\": \\"准备测试环境(创建隔离 work-dir)\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"探索轮:验证工具可用性和接口\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮1:正常路径 - 后台任务生命周期\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮2:边界条件 - 超时和停止\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮3:超时场景测试\\", \\"status\\": \\"in_progress\\"}, {\\"title\\": \\"观察轮:检查 session 产物\\", \\"status\\": \\"pending\\"}, {\\"title\\": \\"汇报结论\\", \\"status\\": \\"pending\\"}]}"}},{"type":"function","id":"tool_J3cQ5m3qyTep0zNLWhZR3OGg","function":{"name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\n\\\\n# 执行轮3:测试超时处理\\\\nuv run python -m example_pkg.cli \\\\\\\\\\\\n --print \\\\\\\\\\\\n --prompt \\\\\\"请测试后台任务超时功能:\\\\n\\\\n1. 启动一个会超时的后台任务:\\\\n - 命令: sleep 300\\\\n - run_in_background: true\\\\n - description: '测试超时处理'\\\\n - timeout: 3 (设置为3秒,确保会超时)\\\\n\\\\n2. 使用 TaskOutput 阻塞等待任务结束(block=true, timeout=15)\\\\n\\\\n3. 检查任务最终状态是否为 timeout\\\\n\\\\n请报告:task_id、最终状态、exit_code、是否有 timeout 通知。\\\\\\" \\\\\\\\\\\\n --work-dir \\\\\\"$SMOKE_DIR\\\\\\" 2>&1\\\\necho \\\\\\"exit_code=$?\\\\\\"\\", \\"timeout\\": 180}"}}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Todo list updated"}],"toolCalls":[],"toolCallId":"tool_TS5JUSZELJoTVcos6rp90ZUT"}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Command executed successfully."},{"type":"text","text":"请测试后台任务超时功能:\\n\\n1. 启动一个会超时的后台任务:\\n - 命令: sleep 300\\n - run_in_background: true\\n - description: '测试超时处理'\\n - timeout: 3 (设置为3秒,确保会超时)\\n\\n2. 使用 TaskOutput 阻塞等待任务结束(block=true, timeout=15)\\n\\n3. 检查任务最终状态是否为 timeout\\n\\n请报告:task_id、最终状态、exit_code、是否有 timeout 通知。\\nTurnBegin(\\n user_input=\\"请测试后台任务超时功能:\\\\n\\\\n1. 启动一个会超时的后台任务:\\\\n - \\n命令: sleep 300\\\\n - run_in_background: true\\\\n - description: \\n'测试超时处理'\\\\n - timeout: 3 (设置为3秒,确保会超时)\\\\n\\\\n2. 使用 TaskOutput\\n阻塞等待任务结束(block=true, timeout=15)\\\\n\\\\n3. 检查任务最终状态是否为 \\n... [truncated]"}],"toolCalls":[],"toolCallId":"tool_J3cQ5m3qyTep0zNLWhZR3OGg"}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"第三轮也成功!测试了超时处理:\\n1. ✅ 任务正确超时\\n2. ✅ 状态为 failed,timed_out=true\\n3. ✅ 通知正确发送 (task.timed_out)\\n4. ✅ exit_code 正确 (-15)\\n\\n现在进入观察轮,检查 session 产物,验证 notification 是否正确写入 context 等。"},{"type":"text","text":"第三轮成功!超时处理完全正常。现在进入观察轮,检查 session 产物:"}],"toolCalls":[{"type":"function","id":"tool_n7Bz3OSHPlOZnL3ghEe8Rciw","name":"SetTodoList","arguments":"{\\"todos\\": [{\\"title\\": \\"准备测试环境(创建隔离 work-dir)\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"探索轮:验证工具可用性和接口\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮1:正常路径 - 后台任务生命周期\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮2:边界条件 - 超时和停止\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮3:超时场景测试\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"观察轮:检查 session 产物\\", \\"status\\": \\"in_progress\\"}, {\\"title\\": \\"汇报结论\\", \\"status\\": \\"pending\\"}]}"},{"type":"function","id":"tool_3s3qLBo5c7kA1pqbpX36YbVo","name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\n\\\\n# 查找 session 目录\\\\necho \\\\\\"=== Session 定位 ===\\\\\\"\\\\nSESSION_ID=$(echo -n \\\\\\"$SMOKE_DIR\\\\\\" | md5)\\\\necho \\\\\\"Work dir: $SMOKE_DIR\\\\\\"\\\\necho \\\\\\"Session ID (md5): $SESSION_ID\\\\\\"\\\\nSESSION_DIR=\\\\\\"$HOME/.kimi/sessions/$SESSION_ID\\\\\\"\\\\necho \\\\\\"Session dir: $SESSION_DIR\\\\\\"\\\\n\\\\n# 检查 session 目录是否存在\\\\nif [ -d \\\\\\"$SESSION_DIR\\\\\\" ]; then\\\\n echo \\\\\\"Session exists: YES\\\\\\"\\\\n ls -la \\\\\\"$SESSION_DIR\\\\\\"\\\\nelse\\\\n echo \\\\\\"Session exists: NO\\\\\\"\\\\n # 尝试找到最新的 session\\\\n echo \\\\\\"Finding... [truncated]"}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"第三轮也成功!测试了超时处理:\\n1. ✅ 任务正确超时\\n2. ✅ 状态为 failed,timed_out=true\\n3. ✅ 通知正确发送 (task.timed_out)\\n4. ✅ exit_code 正确 (-15)\\n\\n现在进入观察轮,检查 session 产物,验证 notification 是否正确写入 context 等。"},{"type":"text","text":"第三轮成功!超时处理完全正常。现在进入观察轮,检查 session 产物:"}],"toolCalls":[{"type":"function","id":"tool_n7Bz3OSHPlOZnL3ghEe8Rciw","function":{"name":"SetTodoList","arguments":"{\\"todos\\": [{\\"title\\": \\"准备测试环境(创建隔离 work-dir)\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"探索轮:验证工具可用性和接口\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮1:正常路径 - 后台任务生命周期\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮2:边界条件 - 超时和停止\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮3:超时场景测试\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"观察轮:检查 session 产物\\", \\"status\\": \\"in_progress\\"}, {\\"title\\": \\"汇报结论\\", \\"status\\": \\"pending\\"}]}"}},{"type":"function","id":"tool_3s3qLBo5c7kA1pqbpX36YbVo","function":{"name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\n\\\\n# 查找 session 目录\\\\necho \\\\\\"=== Session 定位 ===\\\\\\"\\\\nSESSION_ID=$(echo -n \\\\\\"$SMOKE_DIR\\\\\\" | md5)\\\\necho \\\\\\"Work dir: $SMOKE_DIR\\\\\\"\\\\necho \\\\\\"Session ID (md5): $SESSION_ID\\\\\\"\\\\nSESSION_DIR=\\\\\\"$HOME/.kimi/sessions/$SESSION_ID\\\\\\"\\\\necho \\\\\\"Session dir: $SESSION_DIR\\\\\\"\\\\n\\\\n# 检查 session 目录是否存在\\\\nif [ -d \\\\\\"$SESSION_DIR\\\\\\" ]; then\\\\n echo \\\\\\"Session exists: YES\\\\\\"\\\\n ls -la \\\\\\"$SESSION_DIR\\\\\\"\\\\nelse\\\\n echo \\\\\\"Session exists: NO\\\\\\"\\\\n # 尝试找到最新的 session\\\\n echo \\\\\\"Finding... [truncated]"}}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Todo list updated"}],"toolCalls":[],"toolCallId":"tool_n7Bz3OSHPlOZnL3ghEe8Rciw"}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Command executed successfully."},{"type":"text","text":"=== Session 定位 ===\\nWork dir: /tmp/kimi-smoke-a2i1xj\\nSession ID (md5): d1c3750c0cc0757aeac04ad5a7d6b8cf\\nSession dir: /Users/example/proj/.kimi/sessions/d1c3750c0cc0757aeac04ad5a7d6b8cf\\nSession exists: YES\\ntotal 0\\ndrwxr-xr-x@ 6 user staff 192 3月 17 12:59 .\\ndrwxr-xr-x@ 1722 user staff 55104 3月 17 12:58 ..\\ndrwxr-xr-x@ 6 user staff 192 3月 17 12:58 0e1d3b94-dae2-4dfb-b36d-717397f4d2f6\\ndrwxr-xr-x@ 6 user staff 192 3月 17 12:59 228b54e2-5b5d-4675-94f2-ec56e42cdbae\\nd... [truncated]"}],"toolCalls":[],"toolCallId":"tool_3s3qLBo5c7kA1pqbpX36YbVo"}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"找到了 session 目录,下面有多个子目录(每次运行 CLI 会创建一个新的子 session)。让我使用 inspect_session.py 来检查最新的 session。"}],"toolCalls":[{"type":"function","id":"tool_4Zcuo5i4xl2NEz1Y1EZ1IvSn","name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\n\\\\n# 使用 inspect_session.py 检查最新的 session\\\\necho \\\\\\"=== 使用 inspect_session.py 检查 ===\\\\\\"\\\\nuv run python .agents/skills/example-task/scripts/inspect_session.py --share-dir ~/.kimi 2>&1\\"}"}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"找到了 session 目录,下面有多个子目录(每次运行 CLI 会创建一个新的子 session)。让我使用 inspect_session.py 来检查最新的 session。"}],"toolCalls":[{"type":"function","id":"tool_4Zcuo5i4xl2NEz1Y1EZ1IvSn","function":{"name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\n\\\\n# 使用 inspect_session.py 检查最新的 session\\\\necho \\\\\\"=== 使用 inspect_session.py 检查 ===\\\\\\"\\\\nuv run python .agents/skills/example-task/scripts/inspect_session.py --share-dir ~/.kimi 2>&1\\"}"}}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Command executed successfully."},{"type":"text","text":"=== 使用 inspect_session.py 检查 ===\\nSession dir: /Users/example/proj/.kimi/sessions/6a7967e2a97cd71a62b8375c5ae461ba/ef00f9f9-a470-4e50-b279-59429da67d27\\n\\n== Files ==\\ncontext.jsonl (209039 bytes)\\nstate.json (188 bytes)\\nwire.jsonl (197310 bytes)\\n\\n== Context ==\\n/Users/example/proj/.kimi/sessions/6a7967e2a97cd71a62b8375c5ae461ba/ef00f9f9-a470-4e50-b279-59429da67d27/context.jsonl\\nrecords: 85\\nroles: _checkpoint=16, _system_prompt=1, _usage=27, assistant=13, tool=26, user=2\\n[74] role=assistant | tools=SetTodoLis... [truncated]"}],"toolCalls":[],"toolCallId":"tool_4Zcuo5i4xl2NEz1Y1EZ1IvSn"}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"这个 session 不是我刚才测试的那个。让我直接检查正确的 session 目录(d1c3750c0cc0757aeac04ad5a7d6b8cf),里面有多个子目录,对应三次测试运行。"}],"toolCalls":[{"type":"function","id":"tool_DmwmxRwjKGDXt1PTAyVbYAdM","name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\nSESSION_ID=$(echo -n \\\\\\"$SMOKE_DIR\\\\\\" | md5)\\\\nSESSION_DIR=\\\\\\"$HOME/.kimi/sessions/$SESSION_ID\\\\\\"\\\\n\\\\necho \\\\\\"=== 检查正确的 session 目录 ===\\\\\\"\\\\necho \\\\\\"Session dir: $SESSION_DIR\\\\\\"\\\\necho \\\\\\"\\\\\\"\\\\necho \\\\\\"=== 子目录列表 ===\\\\\\"\\\\nls -la \\\\\\"$SESSION_DIR\\\\\\"\\\\n\\\\n# 检查每个子目录的 wire.jsonl 中的 Notification 事件\\\\necho \\\\\\"\\\\\\"\\\\necho \\\\\\"=== 检查 Notification 事件 ===\\\\\\"\\\\nfor subdir in \\\\\\"$SESSION_DIR\\\\\\"/*; do\\\\n if [ -d \\\\\\"$subdir\\\\\\" ] && [ -f \\\\\\"$subdir/wire.jsonl\\\\\\" ]; then\\\\n subname=$(basename... [truncated]"}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"这个 session 不是我刚才测试的那个。让我直接检查正确的 session 目录(d1c3750c0cc0757aeac04ad5a7d6b8cf),里面有多个子目录,对应三次测试运行。"}],"toolCalls":[{"type":"function","id":"tool_DmwmxRwjKGDXt1PTAyVbYAdM","function":{"name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\nSESSION_ID=$(echo -n \\\\\\"$SMOKE_DIR\\\\\\" | md5)\\\\nSESSION_DIR=\\\\\\"$HOME/.kimi/sessions/$SESSION_ID\\\\\\"\\\\n\\\\necho \\\\\\"=== 检查正确的 session 目录 ===\\\\\\"\\\\necho \\\\\\"Session dir: $SESSION_DIR\\\\\\"\\\\necho \\\\\\"\\\\\\"\\\\necho \\\\\\"=== 子目录列表 ===\\\\\\"\\\\nls -la \\\\\\"$SESSION_DIR\\\\\\"\\\\n\\\\n# 检查每个子目录的 wire.jsonl 中的 Notification 事件\\\\necho \\\\\\"\\\\\\"\\\\necho \\\\\\"=== 检查 Notification 事件 ===\\\\\\"\\\\nfor subdir in \\\\\\"$SESSION_DIR\\\\\\"/*; do\\\\n if [ -d \\\\\\"$subdir\\\\\\" ] && [ -f \\\\\\"$subdir/wire.jsonl\\\\\\" ]; then\\\\n subname=$(basename... [truncated]"}}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Command executed successfully."},{"type":"text","text":"=== 检查正确的 session 目录 ===\\nSession dir: /Users/example/proj/.kimi/sessions/d1c3750c0cc0757aeac04ad5a7d6b8cf\\n\\n=== 子目录列表 ===\\ntotal 0\\ndrwxr-xr-x@ 6 user staff 192 3月 17 12:59 .\\ndrwxr-xr-x@ 1722 user staff 55104 3月 17 12:58 ..\\ndrwxr-xr-x@ 6 user staff 192 3月 17 12:58 0e1d3b94-dae2-4dfb-b36d-717397f4d2f6\\ndrwxr-xr-x@ 6 user staff 192 3月 17 12:59 228b54e2-5b5d-4675-94f2-ec56e42cdbae\\ndrwxr-xr-x@ 6 user staff 192 3月 17 13:00 be8228d5-1c57-43dc-9f23-9e7dba2... [truncated]"}],"toolCalls":[],"toolCallId":"tool_DmwmxRwjKGDXt1PTAyVbYAdM"}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"奇怪,没有找到 Notification 事件。可能 Notification 事件格式不一样,让我直接查看 wire.jsonl 文件内容,搜索 notification。"}],"toolCalls":[{"type":"function","id":"tool_97R6Gz8LI01cAOsQDuv9nbwm","name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\nSESSION_ID=$(echo -n \\\\\\"$SMOKE_DIR\\\\\\" | md5)\\\\nSESSION_DIR=\\\\\\"$HOME/.kimi/sessions/$SESSION_ID\\\\\\"\\\\n\\\\n# 直接查看每个子目录的 wire.jsonl 内容\\\\necho \\\\\\"=== 查看 wire.jsonl 中的通知相关内容 ===\\\\\\"\\\\nfor subdir in \\\\\\"$SESSION_DIR\\\\\\"/*; do\\\\n if [ -d \\\\\\"$subdir\\\\\\" ] && [ -f \\\\\\"$subdir/wire.jsonl\\\\\\" ]; then\\\\n subname=$(basename \\\\\\"$subdir\\\\\\")\\\\n echo \\\\\\"\\\\\\"\\\\n echo \\\\\\"--- Sub-session: $subname ---\\\\\\"\\\\n # 查找包含 notification 的行\\\\n grep -i \\\\\\"notification\\\\\\\\|task\\\\\\\\.... [truncated]"}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"奇怪,没有找到 Notification 事件。可能 Notification 事件格式不一样,让我直接查看 wire.jsonl 文件内容,搜索 notification。"}],"toolCalls":[{"type":"function","id":"tool_97R6Gz8LI01cAOsQDuv9nbwm","function":{"name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\nSESSION_ID=$(echo -n \\\\\\"$SMOKE_DIR\\\\\\" | md5)\\\\nSESSION_DIR=\\\\\\"$HOME/.kimi/sessions/$SESSION_ID\\\\\\"\\\\n\\\\n# 直接查看每个子目录的 wire.jsonl 内容\\\\necho \\\\\\"=== 查看 wire.jsonl 中的通知相关内容 ===\\\\\\"\\\\nfor subdir in \\\\\\"$SESSION_DIR\\\\\\"/*; do\\\\n if [ -d \\\\\\"$subdir\\\\\\" ] && [ -f \\\\\\"$subdir/wire.jsonl\\\\\\" ]; then\\\\n subname=$(basename \\\\\\"$subdir\\\\\\")\\\\n echo \\\\\\"\\\\\\"\\\\n echo \\\\\\"--- Sub-session: $subname ---\\\\\\"\\\\n # 查找包含 notification 的行\\\\n grep -i \\\\\\"notification\\\\\\\\|task\\\\\\\\.... [truncated]"}}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Command executed successfully."},{"type":"text","text":"=== 查看 wire.jsonl 中的通知相关内容 ===\\n\\n--- Sub-session: 0e1d3b94-dae2-4dfb-b36d-717397f4d2f6 ---\\n{\\"timestamp\\": 1773723525.133148, \\"message\\": {\\"type\\": \\"ToolResult\\", \\"payload\\": {\\"tool_call_id\\": \\"tool_KviOIHcT5WgvrVhhHtEzsDNR\\", \\"return_value\\": {\\"is_error\\": false, \\"output\\": \\"task_id: b83rhswvs\\\\nkind: bash\\\\nstatus: starting\\\\ndescription: 测试后台任务生命周期\\\\ncommand: echo 'hello from background' && sleep 2 && echo 'task done'\\\\nautomatic_notification: true\\\\nnext_step: You will be automatically notified when it comple... [truncated]"}],"toolCalls":[],"toolCallId":"tool_97R6Gz8LI01cAOsQDuv9nbwm"}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"我找到了所有的 Notification 事件:\\n1. \`0e1d3b94-dae2-4dfb-b36d-717397f4d2f6\` - task.completed (b83rhswvs)\\n2. \`228b54e2-5b5d-4675-94f2-ec56e42cdbae\` - task.killed (bp6amsrcu)\\n3. \`be8228d5-1c57-43dc-9f23-9e7dba209a1a\` - task.timed_out (b4c3d8fta)\\n\\n现在让我检查 context.jsonl 中是否有 notification 消息,以及 task 目录结构。"},{"type":"text","text":"找到了所有 Notification 事件!现在检查 context.jsonl 和 task 目录:"}],"toolCalls":[{"type":"function","id":"tool_74k2gxGVF3RSOhTaEhnnvPzk","name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\nSESSION_ID=$(echo -n \\\\\\"$SMOKE_DIR\\\\\\" | md5)\\\\nSESSION_DIR=\\\\\\"$HOME/.kimi/sessions/$SESSION_ID\\\\\\"\\\\n\\\\n# 检查 context.jsonl 中的 notification 消息\\\\necho \\\\\\"=== 检查 context.jsonl 中的 notification 消息 ===\\\\\\"\\\\nfor subdir in \\\\\\"$SESSION_DIR\\\\\\"/*; do\\\\n if [ -d \\\\\\"$subdir\\\\\\" ] && [ -f \\\\\\"$subdir/context.jsonl\\\\\\" ]; then\\\\n subname=$(basename \\\\\\"$subdir\\\\\\")\\\\n count=$(grep -c '\\\\\\"role\\\\\\":\\\\\\"notification\\\\\\"' \\\\\\"$subdir/context.jsonl\\\\\\" 2>/dev/null || echo \\\\\\"0\\\\\\")\\\\n ... [truncated]"}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"我找到了所有的 Notification 事件:\\n1. \`0e1d3b94-dae2-4dfb-b36d-717397f4d2f6\` - task.completed (b83rhswvs)\\n2. \`228b54e2-5b5d-4675-94f2-ec56e42cdbae\` - task.killed (bp6amsrcu)\\n3. \`be8228d5-1c57-43dc-9f23-9e7dba209a1a\` - task.timed_out (b4c3d8fta)\\n\\n现在让我检查 context.jsonl 中是否有 notification 消息,以及 task 目录结构。"},{"type":"text","text":"找到了所有 Notification 事件!现在检查 context.jsonl 和 task 目录:"}],"toolCalls":[{"type":"function","id":"tool_74k2gxGVF3RSOhTaEhnnvPzk","function":{"name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\nSESSION_ID=$(echo -n \\\\\\"$SMOKE_DIR\\\\\\" | md5)\\\\nSESSION_DIR=\\\\\\"$HOME/.kimi/sessions/$SESSION_ID\\\\\\"\\\\n\\\\n# 检查 context.jsonl 中的 notification 消息\\\\necho \\\\\\"=== 检查 context.jsonl 中的 notification 消息 ===\\\\\\"\\\\nfor subdir in \\\\\\"$SESSION_DIR\\\\\\"/*; do\\\\n if [ -d \\\\\\"$subdir\\\\\\" ] && [ -f \\\\\\"$subdir/context.jsonl\\\\\\" ]; then\\\\n subname=$(basename \\\\\\"$subdir\\\\\\")\\\\n count=$(grep -c '\\\\\\"role\\\\\\":\\\\\\"notification\\\\\\"' \\\\\\"$subdir/context.jsonl\\\\\\" 2>/dev/null || echo \\\\\\"0\\\\\\")\\\\n ... [truncated]"}}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Command executed successfully."},{"type":"text","text":"=== 检查 context.jsonl 中的 notification 消息 ===\\nSub-session 0e1d3b94-dae2-4dfb-b36d-717397f4d2f6: notification messages = 0\\n0\\n/bin/bash: line 11: [: 0\\n0: integer expression expected\\nSub-session 228b54e2-5b5d-4675-94f2-ec56e42cdbae: notification messages = 0\\n0\\n/bin/bash: line 11: [: 0\\n0: integer expression expected\\nSub-session be8228d5-1c57-43dc-9f23-9e7dba209a1a: notification messages = 0\\n0\\n/bin/bash: line 11: [: 0\\n0: integer expression expected\\nSub-session ea3920e8-2a4b-4e38-be51-c500aab582ab: noti... [truncated]"}],"toolCalls":[],"toolCallId":"tool_74k2gxGVF3RSOhTaEhnnvPzk"}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"找到了 task 目录。现在让我检查 task 目录的内容,以及 notification 持久化目录。"}],"toolCalls":[{"type":"function","id":"tool_doClKbqjLQAuMtimAxF3mufI","name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\nSESSION_ID=$(echo -n \\\\\\"$SMOKE_DIR\\\\\\" | md5)\\\\nSESSION_DIR=\\\\\\"$HOME/.kimi/sessions/$SESSION_ID\\\\\\"\\\\n\\\\n# 检查 task 目录内容\\\\necho \\\\\\"=== 检查第一个 task 的目录结构 ===\\\\\\"\\\\nTASK_DIR=\\\\\\"$SESSION_DIR/0e1d3b94-dae2-4dfb-b36d-717397f4d2f6/tasks/b83rhswvs\\\\\\"\\\\nls -la \\\\\\"$TASK_DIR\\\\\\"\\\\n\\\\necho \\\\\\"\\\\\\"\\\\necho \\\\\\"=== spec.json ===\\\\\\"\\\\ncat \\\\\\"$TASK_DIR/spec.json\\\\\\"\\\\n\\\\necho \\\\\\"\\\\\\"\\\\necho \\\\\\"=== runtime.json ===\\\\\\"\\\\ncat \\\\\\"$TASK_DIR/runtime.json\\\\\\"\\\\n\\\\necho \\\\\\"\\\\\\"\\\\necho \\\\\\"=== output.log ===\\\\\\"\\\\ncat \\\\\\"$TASK_... [truncated]"}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"找到了 task 目录。现在让我检查 task 目录的内容,以及 notification 持久化目录。"}],"toolCalls":[{"type":"function","id":"tool_doClKbqjLQAuMtimAxF3mufI","function":{"name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\nSESSION_ID=$(echo -n \\\\\\"$SMOKE_DIR\\\\\\" | md5)\\\\nSESSION_DIR=\\\\\\"$HOME/.kimi/sessions/$SESSION_ID\\\\\\"\\\\n\\\\n# 检查 task 目录内容\\\\necho \\\\\\"=== 检查第一个 task 的目录结构 ===\\\\\\"\\\\nTASK_DIR=\\\\\\"$SESSION_DIR/0e1d3b94-dae2-4dfb-b36d-717397f4d2f6/tasks/b83rhswvs\\\\\\"\\\\nls -la \\\\\\"$TASK_DIR\\\\\\"\\\\n\\\\necho \\\\\\"\\\\\\"\\\\necho \\\\\\"=== spec.json ===\\\\\\"\\\\ncat \\\\\\"$TASK_DIR/spec.json\\\\\\"\\\\n\\\\necho \\\\\\"\\\\\\"\\\\necho \\\\\\"=== runtime.json ===\\\\\\"\\\\ncat \\\\\\"$TASK_DIR/runtime.json\\\\\\"\\\\n\\\\necho \\\\\\"\\\\\\"\\\\necho \\\\\\"=== output.log ===\\\\\\"\\\\ncat \\\\\\"$TASK_... [truncated]"}}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Command executed successfully."},{"type":"text","text":"=== 检查第一个 task 的目录结构 ===\\ntotal 40\\ndrwxr-xr-x@ 7 user staff 224 3月 17 12:58 .\\ndrwxr-xr-x@ 3 user staff 96 3月 17 12:58 ..\\n-rw-------@ 1 user staff 72 3月 17 12:58 consumer.json\\n-rw-------@ 1 user staff 72 3月 17 12:58 control.json\\n-rw-r--r--@ 1 user staff 32 3月 17 12:58 output.log\\n-rw-------@ 1 user staff 331 3月 17 12:58 runtime.json\\n-rw-------@ 1 user staff 481 3月 17 12:58 spec.json\\n\\n=== spec.json ===\\n{\\n \\"version\\": 1,\\n \\"id\\": \\"b83rhswvs\\",\\n \\"kin... [truncated]"}],"toolCalls":[],"toolCallId":"tool_doClKbqjLQAuMtimAxF3mufI"}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"所有验证都通过了:\\n1. task 目录结构正确\\n2. spec.json 包含正确的 task 信息\\n3. runtime.json 包含正确的状态信息\\n4. output.log 包含正确的输出\\n5. notification 持久化正确\\n\\n现在让我检查 notification 目录的内容。"}],"toolCalls":[{"type":"function","id":"tool_7SICneqn0eGcgCADXOFuo2yj","name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\nSESSION_ID=$(echo -n \\\\\\"$SMOKE_DIR\\\\\\" | md5)\\\\nSESSION_DIR=\\\\\\"$HOME/.kimi/sessions/$SESSION_ID\\\\\\"\\\\n\\\\n# 检查 notification 目录内容\\\\necho \\\\\\"=== 检查 notification 持久化内容 ===\\\\\\"\\\\nNOTIF_DIR=\\\\\\"$SESSION_DIR/0e1d3b94-dae2-4dfb-b36d-717397f4d2f6/notifications/n495ae5fb\\\\\\"\\\\necho \\\\\\"Notification: n495ae5fb (task.completed)\\\\\\"\\\\nls -la \\\\\\"$NOTIF_DIR\\\\\\"\\\\n\\\\necho \\\\\\"\\\\\\"\\\\necho \\\\\\"=== event.json ===\\\\\\"\\\\ncat \\\\\\"$NOTIF_DIR/event.json\\\\\\" | head -30\\\\n\\\\necho \\\\\\"\\\\\\"\\\\necho \\\\\\"=== delivery.json ===\\\\... [truncated]"}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"所有验证都通过了:\\n1. task 目录结构正确\\n2. spec.json 包含正确的 task 信息\\n3. runtime.json 包含正确的状态信息\\n4. output.log 包含正确的输出\\n5. notification 持久化正确\\n\\n现在让我检查 notification 目录的内容。"}],"toolCalls":[{"type":"function","id":"tool_7SICneqn0eGcgCADXOFuo2yj","function":{"name":"Shell","arguments":"{\\"command\\": \\"SMOKE_DIR=\\\\\\"/tmp/kimi-smoke-a2i1xj\\\\\\"\\\\nSESSION_ID=$(echo -n \\\\\\"$SMOKE_DIR\\\\\\" | md5)\\\\nSESSION_DIR=\\\\\\"$HOME/.kimi/sessions/$SESSION_ID\\\\\\"\\\\n\\\\n# 检查 notification 目录内容\\\\necho \\\\\\"=== 检查 notification 持久化内容 ===\\\\\\"\\\\nNOTIF_DIR=\\\\\\"$SESSION_DIR/0e1d3b94-dae2-4dfb-b36d-717397f4d2f6/notifications/n495ae5fb\\\\\\"\\\\necho \\\\\\"Notification: n495ae5fb (task.completed)\\\\\\"\\\\nls -la \\\\\\"$NOTIF_DIR\\\\\\"\\\\n\\\\necho \\\\\\"\\\\\\"\\\\necho \\\\\\"=== event.json ===\\\\\\"\\\\ncat \\\\\\"$NOTIF_DIR/event.json\\\\\\" | head -30\\\\n\\\\necho \\\\\\"\\\\\\"\\\\necho \\\\\\"=== delivery.json ===\\\\... [truncated]"}}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Command executed successfully."},{"type":"text","text":"=== 检查 notification 持久化内容 ===\\nNotification: n495ae5fb (task.completed)\\ntotal 16\\ndrwxr-xr-x@ 4 user staff 128 3月 17 12:58 .\\ndrwxr-xr-x@ 3 user staff 96 3月 17 12:58 ..\\n-rw-------@ 1 user staff 332 3月 17 12:58 delivery.json\\n-rw-------@ 1 user staff 790 3月 17 12:58 event.json\\n\\n=== event.json ===\\n{\\n \\"version\\": 1,\\n \\"id\\": \\"n495ae5fb\\",\\n \\"category\\": \\"task\\",\\n \\"type\\": \\"task.completed\\",\\n \\"source_kind\\": \\"background_task\\",\\n \\"source_id\\": \\"b83rhswvs\\",\\n \\"title\\": \\"Background ta... [truncated]"}],"toolCalls":[],"toolCallId":"tool_7SICneqn0eGcgCADXOFuo2yj"}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"所有验证都通过了!现在让我总结冒烟测试的结果。"},{"type":"text","text":"所有验证完成!现在更新 todo 并汇报结论:"}],"toolCalls":[{"type":"function","id":"tool_CcJKFj7faabalZQ9X1gPrTbr","name":"SetTodoList","arguments":"{\\"todos\\": [{\\"title\\": \\"准备测试环境(创建隔离 work-dir)\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"探索轮:验证工具可用性和接口\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮1:正常路径 - 后台任务生命周期\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮2:边界条件 - 超时和停止\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮3:超时场景测试\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"观察轮:检查 session 产物\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"汇报结论\\", \\"status\\": \\"done\\"}]}"}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"所有验证都通过了!现在让我总结冒烟测试的结果。"},{"type":"text","text":"所有验证完成!现在更新 todo 并汇报结论:"}],"toolCalls":[{"type":"function","id":"tool_CcJKFj7faabalZQ9X1gPrTbr","function":{"name":"SetTodoList","arguments":"{\\"todos\\": [{\\"title\\": \\"准备测试环境(创建隔离 work-dir)\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"探索轮:验证工具可用性和接口\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮1:正常路径 - 后台任务生命周期\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮2:边界条件 - 超时和停止\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"执行轮3:超时场景测试\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"观察轮:检查 session 产物\\", \\"status\\": \\"done\\"}, {\\"title\\": \\"汇报结论\\", \\"status\\": \\"done\\"}]}"}}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Todo list updated"}],"toolCalls":[],"toolCallId":"tool_CcJKFj7faabalZQ9X1gPrTbr"}} {"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"所有测试完成。现在整理完整的测试报告。"},{"type":"text","text":"---\\n\\n## 冒烟测试报告\\n\\n### 测试范围\\n**后台任务系统 + 通知系统** - 本次变更新增的核心功能\\n\\n### 已确认的行为 ✅\\n\\n| 功能 | 验证结果 |\\n|------|----------|\\n| **Shell 后台模式** | \`run_in_background=true\` + \`description\` 参数正常工作 |\\n| **TaskList** | 正确列出活跃/全部任务,显示 task_id/kind/status/description |\\n| **TaskOutput** | \`block=true/false\` 模式工作正常,返回完整任务状态和输出 |\\n| **TaskStop** | 成功发送 SIGTERM 停止任务,支持自定义 reason |\\n| **任务超时** | timeout 参数正确终止超时任务,exit_code=-15 |\\n| **Wire Notification** | \`Notification\` 事件正确发送,包含完整 payload |\\n| **Notification 持久化** | event.json + d... [truncated]"}],"toolCalls":[]}} {"type":"context.append_message","message":{"role":"user","content":[{"type":"text","text":"source /Users/example/proj/example-project/.venv/bin/activate"}],"toolCalls":[]}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"用户只是在执行激活虚拟环境的命令,这是一个 shell 命令。我应该执行它并确认环境已激活。"}],"toolCalls":[{"type":"function","id":"tool_oKHd9KBRZQ3pT7x7XgOWhcMJ","name":"Shell","arguments":"{\\"command\\": \\"source /Users/example/proj/example-project/.venv/bin/activate && echo \\\\\\"Virtual environment activated\\\\\\" && which python && python --version\\"}"}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"用户只是在执行激活虚拟环境的命令,这是一个 shell 命令。我应该执行它并确认环境已激活。"}],"toolCalls":[{"type":"function","id":"tool_oKHd9KBRZQ3pT7x7XgOWhcMJ","function":{"name":"Shell","arguments":"{\\"command\\": \\"source /Users/example/proj/example-project/.venv/bin/activate && echo \\\\\\"Virtual environment activated\\\\\\" && which python && python --version\\"}"}}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Command executed successfully."},{"type":"text","text":"Virtual environment activated\\n/Users/example/proj/example-project/.venv/bin/python\\nPython 3.14.2\\n"}],"toolCalls":[],"toolCallId":"tool_oKHd9KBRZQ3pT7x7XgOWhcMJ"}} ", } @@ -148,7 +148,7 @@ exports[`migration snapshot: legacy-protocol-1.3 > migration succeeds and matche "wire": "{"type":"metadata","protocol_version":"1.0","created_at":} {"type":"context.append_message","message":{"role":"user","content":[{"type":"text","text":"帮我写一个示例函数,处理字符串清理和分块"}],"toolCalls":[]}} {"type":"context.append_message","message":{"role":"user","content":[{"type":"text","text":"\\n\\nPlan mode is active. You are in a research and planning phase.\\n\\nIn plan mode, you should:\\n1. Thoroughly explore the codebase using Glob, Grep, and ReadFile tools.\\n2. Understand existing patterns and architecture.\\n3. Design an implementation approach.\\n4. Write your plan to the plan file using WriteFile.\\n5. Use AskUserQuestion if you need to clarify approaches.\\n6. When your plan is complete, call ExitPlanMode.\\n\\nRemember: DO NOT write or edit code files... [truncated]"}],"toolCalls":[]}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"用户想要写一个示例函数,处理字符串清理和分块。我先看看当前 diff 中有哪些相关代码可以参考。\\n\\n让我先检查一下当前的 git diff。"}],"toolCalls":[{"type":"function","id":"Shell:0","name":"Shell","arguments":"{\\"command\\": \\"git diff --no-color\\"}"}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"用户想要写一个示例函数,处理字符串清理和分块。我先看看当前 diff 中有哪些相关代码可以参考。\\n\\n让我先检查一下当前的 git diff。"}],"toolCalls":[{"type":"function","id":"Shell:0","function":{"name":"Shell","arguments":"{\\"command\\": \\"git diff --no-color\\"}"}}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Command executed successfully."},{"type":"text","text":"diff --git a/example.py b/example.py\\nindex 0000aaaa..1111bbbb 100644\\n--- a/example.py\\n+++ b/example.py\\n@@ -1,12 +1,16 @@\\n class Greeter:\\n def __init__(self, name: str) -> None:\\n self.name = name\\n+ self.greetings: list[str] = []\\n \\n def greet(self) -> str:\\n- return f\\"Hello, {self.name}!\\"\\n+ msg = f\\"Hello, {self.name}!\\"\\n+ self.greetings.append(msg)\\n+ return msg\\n \\n- def farewell(self) -> str:\\n- return f\\"Goodbye, {self.name}!\\"\\n+ def farewell(self, formal: bool = False) -> str:\\n+ prefix = \\"Farewell\\" if formal else \\"Goodbye\\"\\n+ return f\\"{prefix}, {self.name}!\\"\\n \\ndiff --git a/example/utils.py b/example/utils.py\\nindex 22223333..44445555 100644\\n--- a/example/utils.py\\n+++ b/example/utils.py\\n@@ -10,16 +10,20 @@ def slugify(text: str) -> str:\\n result = text.lower()\\n result = result.replace(\\" \\", \\"-\\")\\n- # Remove punctuation\\n- result = \\"\\".join(c for c in result if c.isalnum() or c == \\"-\\")\\n+ # Remove punctuation and collapse repeated dashes\\n+ cleaned = \\"\\".join(c for c in result if c.isalnum() or c == \\"-\\")\\n+ parts = [p for p in cleaned.split(\\"-\\") if p]\\n+ result = \\"-\\".join(parts)\\n return result\\n \\n \\n def chunk(items: list[str], size: int) -> list[list[str]]:\\n- if size <= 0:\\n- raise ValueError(\\"size must be positive\\")\\n- return [items[i:i + size] for i in range(0, len(items), size)]\\n+ if size <= 0:\\n+ raise ValueError(\\"chunk size must be a positive integer\\")\\n+ if not items:\\n+ return []\\n+ return [items[i : i + size] for i in range(0, len(items), size)]\\n \\n \\n def truncate(text: str, limit: int = 80) -> str:\\n@@ -32,8 +36,12 @@ def truncate(text: str, limit: int = 80) -> str:\\n return text\\n return text[: limit - 1] + \\"…\\"\\n \\n+\\n+def normalize_whitespace(text: str) -> str:\\n+ return \\" \\".join(text.split())\\n+\\n diff --git a/example/cli.py b/example/cli.py\\nindex 66667777..88889999 100644\\n--- a/example/cli.py\\n+++ b/example/cli.py\\n@@ -5,12 +5,18 @@ from .utils import slugify, truncate\\n \\n def build_parser() -> argparse.ArgumentParser:\\n parser = argparse.ArgumentParser(prog=\\"example\\")\\n- parser.add_argument(\\"--name\\", default=\\"world\\")\\n+ parser.add_argument(\\"--name\\", default=\\"world\\", help=\\"who to greet\\")\\n+ parser.add_argument(\\n+ \\"--formal\\",\\n+ action=\\"store_true\\",\\n+ help=\\"use a formal farewell tone\\",\\n+ )\\n return parser\\n \\n \\n def main(argv: list[str] | None = None) -> int:\\n parser = build_parser()\\n args = parser.parse_args(argv)\\n- print(f\\"Hello, {args.name}!\\")\\n+ greeter = Greeter(args.name)\\n+ print(greeter.greet())\\n+ print(greeter.farewell(formal=args.formal))\\n return 0\\ndiff --git a/example/config.py b/example/config.py\\nindex ccccdddd..eeeeffff 100644\\n--- a/example/config.py\\n+++ b/example/config.py\\n@@ -1,15 +1,22 @@\\n from dataclasses import dataclass, field\\n+from pathlib import Path\\n \\n \\n @dataclass\\n class AppConfig:\\n name: str = \\"example\\"\\n verbose: bool = False\\n- timeout: int = 30\\n+ timeout_seconds: int = 30\\n+ cache_dir: Path | None = None\\n features: list[str] = field(default_factory=list)\\n \\n def is_feature_enabled(self, feature: str) -> bool:\\n return feature in self.features\\n+\\n+ def with_feature(self, feature: str) -> \\"AppConfig\\":\\n+ if feature in self.features:\\n+ return self\\n+ return AppConfig(**{**self.__dict__, \\"features\\": [*self.features, feature]})\\n diff --git a/tests/test_example.py b/tests/test_example.py\\nindex aabbccdd..eeff0011 100644\\n--- a/tests/test_example.py\\n+++ b/tests/test_example.py\\n@@ -1,10 +1,22 @@\\n from example import Greeter\\n+from example.utils import slugify, chunk, normalize_whitespace\\n+from example.config import AppConfig\\n \\n \\n def test_greeter_greet() -> None:\\n g = Greeter(\\"Ada\\")\\n assert g.greet() == \\"Hello, Ada!\\"\\n+ assert g.greetings == [\\"Hello, Ada!\\"]\\n \\n \\n def test_greeter_farewell() -> None:\\n g = Greeter(\\"Ada\\")\\n assert g.farewell() == \\"Goodbye, Ada!\\"\\n+ assert g.farewell(formal=True) == \\"Farewell, Ada!\\"\\n+\\n+\\n+def test_slugify_collapses_dashes() -> None:\\n+ assert slugify(\\"hello world!!!\\") == \\"hello-world\\"\\n+ assert slugify(\\"--abc--def--\\") == \\"abc-def\\"\\n+\\n+\\n+def test_config_with_feature_is_idempotent() -> None:\\n+ cfg = AppConfig().with_feature(\\"beta\\").with_feature(\\"beta\\")\\n+ assert cfg.features == [\\"beta ... [truncated]"}],"toolCalls":[],"toolCallId":"Shell:0"}} ", } @@ -182,7 +182,7 @@ exports[`migration snapshot: recent-protocol-1.10 > migration succeeds and match {"type":"context.append_message","message":{"role":"user","content":[{"type":"text","text":"hi"}],"toolCalls":[]}} {"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"The user said \\"hi\\". This is a simple greeting. I should respond in a friendly, helpful manner. Since there's no specific task or question, I'll just greet them back and ask how I can help."},{"type":"text","text":"Hello! How can I help you today?"}],"toolCalls":[]}} {"type":"context.append_message","message":{"role":"user","content":[{"type":"text","text":"探索这个项目"}],"toolCalls":[]}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"用户要求探索这个项目。让我先了解一下项目的整体结构,然后提供一些关键信息。\\n\\n从目录结构来看,这是一个示例 Python 项目。让我先查看一些关键文件来更好地理解项目。"}],"toolCalls":[{"type":"function","id":"Shell:0","name":"Shell","arguments":"{\\"command\\": \\"find . -maxdepth 2 -type f -name \\\\\\"*.md\\\\\\" | grep -E \\\\\\"(README|CHANGELOG|CONTRIBUTING|AGENTS)\\\\\\" | sort\\"}"},{"type":"function","id":"Shell:1","name":"Shell","arguments":"{\\"command\\": \\"ls -la pyproject.toml Makefile 2>/dev/null || echo \\\\\\"No pyproject.toml or Makefile found\\\\\\"\\"}"},{"type":"function","id":"Shell:2","name":"Shell","arguments":"{\\"command\\": \\"git log --oneline -10 2>/dev/null || echo \\\\\\"Not a git repo or no commits\\\\\\"\\"}"}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"用户要求探索这个项目。让我先了解一下项目的整体结构,然后提供一些关键信息。\\n\\n从目录结构来看,这是一个示例 Python 项目。让我先查看一些关键文件来更好地理解项目。"}],"toolCalls":[{"type":"function","id":"Shell:0","function":{"name":"Shell","arguments":"{\\"command\\": \\"find . -maxdepth 2 -type f -name \\\\\\"*.md\\\\\\" | grep -E \\\\\\"(README|CHANGELOG|CONTRIBUTING|AGENTS)\\\\\\" | sort\\"}"}},{"type":"function","id":"Shell:1","function":{"name":"Shell","arguments":"{\\"command\\": \\"ls -la pyproject.toml Makefile 2>/dev/null || echo \\\\\\"No pyproject.toml or Makefile found\\\\\\"\\"}"}},{"type":"function","id":"Shell:2","function":{"name":"Shell","arguments":"{\\"command\\": \\"git log --oneline -10 2>/dev/null || echo \\\\\\"Not a git repo or no commits\\\\\\"\\"}"}}]}} {"type":"context.append_message","message":{"role":"tool","toolCallId":"Shell:1","content":[{"type":"text","text":"[tool result unavailable — session imported from kimi-cli]"}],"toolCalls":[]}} {"type":"context.append_message","message":{"role":"tool","toolCallId":"Shell:2","content":[{"type":"text","text":"[tool result unavailable — session imported from kimi-cli]"}],"toolCalls":[]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"Command executed successfully."},{"type":"text","text":"./.pytest_cache/README.md\\n./AGENTS.md\\n./CHANGELOG.md\\n./CONTRIBUTING.md\\n./docs/AGENTS.md\\n./README.md\\n./tests_e2e/AGENTS.md\\n"}],"toolCalls":[],"toolCallId":"Shell:0"}} @@ -280,7 +280,7 @@ exports[`migration snapshot: with-subagent-collapsed > migration succeeds and ma }", "wire": "{"type":"metadata","protocol_version":"1.0","created_at":} {"type":"context.append_message","message":{"role":"user","content":[{"type":"text","text":"你启动三个 sub agent,每个 sub,都要运行一些需要被批准的简单指令,运行两个以上"}],"toolCalls":[]}} -{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"用户要求我启动三个 sub agent,每个 sub agent 都需要运行一些需要被批准的简单指令,并且要求运行两个以上。\\n\\n我需要使用 Agent 工具来创建三个 sub agent。每个 sub agent 都应该执行一些需要批准的操作,比如文件写入或 shell 命令。\\n\\n让我创建三个 sub agent,每个都执行不同的需要批准的任务:\\n1. 第一个 sub agent:创建一个测试文件\\n2. 第二个 sub agent:执行一个简单的 shell 命令\\n3. 第三个 sub agent:创建另一个测试文件\\n\\n我需要并行启动这三个 sub agent。"},{"type":"text","text":"我来启动三个 sub agent,每个都会执行需要批准的简单指令。"}],"toolCalls":[{"type":"function","id":"tool_Ct3NdbCjJ2Xbl7YzithE2Dy8","name":"Agent","arguments":"{\\"description\\": \\"Sub agent 1: 创建文件\\", \\"prompt\\": \\"你是一个测试用的 sub agent。请执行以下需要批准的操作:\\\\n\\\\n1. 使用 WriteFile 工具在当前目录创建一个名为 \`subagent1_test.txt\` 的文件,内容为 \\\\\\"Hello from Sub Agent 1!\\\\\\"\\\\n\\\\n这个操作需要用户批准,请等待批准并完成操作。\\\\n\\\\n完成后,报告操作结果。\\"}"},{"type":"function","id":"tool_OtoGSYsrDQpEaav0hLYbXDhd","name":"Agent","arguments":"{\\"description\\": \\"Sub agent 2: Shell 命令\\", \\"prompt\\": \\"你是一个测试用的 sub agent。请执行以下需要批准的操作:\\\\n\\\\n1. 使用 Shell 工具执行命令 \`echo \\\\\\"Hello from Sub Agent 2!\\\\\\" > subagent2_test.txt\`\\\\n\\\\n这个操作需要用户批准,请等待批准并完成操作。\\\\n\\\\n完成后,报告操作结果。\\"}"},{"type":"function","id":"tool_zi4b3Pfbiia2kxPMUvjL4cVN","name":"Agent","arguments":"{\\"description\\": \\"Sub agent 3: 创建文件\\", \\"prompt\\": \\"你是一个测试用的 sub agent。请执行以下需要批准的操作:\\\\n\\\\n1. 使用 WriteFile 工具在当前目录创建一个名为 \`subagent3_test.txt\` 的文件,内容为 \\\\\\"Hello from Sub Agent 3!\\\\\\"\\\\n\\\\n这个操作需要用户批准,请等待批准并完成操作。\\\\n\\\\n完成后,报告操作结果。\\"}"}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"think","think":"用户要求我启动三个 sub agent,每个 sub agent 都需要运行一些需要被批准的简单指令,并且要求运行两个以上。\\n\\n我需要使用 Agent 工具来创建三个 sub agent。每个 sub agent 都应该执行一些需要批准的操作,比如文件写入或 shell 命令。\\n\\n让我创建三个 sub agent,每个都执行不同的需要批准的任务:\\n1. 第一个 sub agent:创建一个测试文件\\n2. 第二个 sub agent:执行一个简单的 shell 命令\\n3. 第三个 sub agent:创建另一个测试文件\\n\\n我需要并行启动这三个 sub agent。"},{"type":"text","text":"我来启动三个 sub agent,每个都会执行需要批准的简单指令。"}],"toolCalls":[{"type":"function","id":"tool_Ct3NdbCjJ2Xbl7YzithE2Dy8","function":{"name":"Agent","arguments":"{\\"description\\": \\"Sub agent 1: 创建文件\\", \\"prompt\\": \\"你是一个测试用的 sub agent。请执行以下需要批准的操作:\\\\n\\\\n1. 使用 WriteFile 工具在当前目录创建一个名为 \`subagent1_test.txt\` 的文件,内容为 \\\\\\"Hello from Sub Agent 1!\\\\\\"\\\\n\\\\n这个操作需要用户批准,请等待批准并完成操作。\\\\n\\\\n完成后,报告操作结果。\\"}"}},{"type":"function","id":"tool_OtoGSYsrDQpEaav0hLYbXDhd","function":{"name":"Agent","arguments":"{\\"description\\": \\"Sub agent 2: Shell 命令\\", \\"prompt\\": \\"你是一个测试用的 sub agent。请执行以下需要批准的操作:\\\\n\\\\n1. 使用 Shell 工具执行命令 \`echo \\\\\\"Hello from Sub Agent 2!\\\\\\" > subagent2_test.txt\`\\\\n\\\\n这个操作需要用户批准,请等待批准并完成操作。\\\\n\\\\n完成后,报告操作结果。\\"}"}},{"type":"function","id":"tool_zi4b3Pfbiia2kxPMUvjL4cVN","function":{"name":"Agent","arguments":"{\\"description\\": \\"Sub agent 3: 创建文件\\", \\"prompt\\": \\"你是一个测试用的 sub agent。请执行以下需要批准的操作:\\\\n\\\\n1. 使用 WriteFile 工具在当前目录创建一个名为 \`subagent3_test.txt\` 的文件,内容为 \\\\\\"Hello from Sub Agent 3!\\\\\\"\\\\n\\\\n这个操作需要用户批准,请等待批准并完成操作。\\\\n\\\\n完成后,报告操作结果。\\"}"}}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"agent_id: ac1f74988\\nresumed: false\\nactual_subagent_type: coder\\nstatus: completed\\n\\n[summary]\\n## 详细操作报告\\n\\n### 任务背景\\n作为测试用的 sub agent,我被要求执行一个需要用户批准的文件写入操作。这是一个测试场景,旨在验证带批准流程的工具调用机制。\\n\\n### 执行过程\\n\\n#### 第一次尝试(被拒绝)\\n- **操作**: 使用 \`WriteFile\` 工具创建文件 \`subagent1_test.txt\`\\n- **预期内容**: \`\\"Hello from Sub Agent 1!\\"\`\\n- **结果**: 用户拒绝了此操作\\n- **用户反馈**: 要求将文件名改为 \`kk\`\\n\\n#### 第二次尝试(成功)\\n- **操作**: 使用 \`WriteFile\` 工具创建文件 \`kk\`\\n- **内容**: \`\\"Hello from Sub Agent 1!\\"\`\\n- **结果**: ✅ 成功创建/覆盖文件\\n- **文件大小**: 23 字节\\n\\n### 技术细节\\n\\n1. **工具使用**: \`W... [truncated]"}],"toolCalls":[],"toolCallId":"tool_Ct3NdbCjJ2Xbl7YzithE2Dy8"}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"agent_id: aa373b195\\nresumed: false\\nactual_subagent_type: coder\\nstatus: completed\\n\\n[summary]\\n## 详细操作结果报告\\n\\n### 1. 任务概述\\n\\n作为测试用 sub agent,我成功执行了需要用户批准的 Shell 命令操作,创建了测试文件并验证了其内容完整性。\\n\\n### 2. 执行详情\\n\\n#### 2.1 命令信息\\n- **命令类型**: Shell 命令执行\\n- **具体命令**: \`echo \\"Hello from Sub Agent 2!\\" > subagent2_test.txt\`\\n- **工作目录**: \`/Users/example/proj/example-project\`\\n- **执行时间**: 2026-03-23T21:42:43+08:00(系统参考时间)\\n\\n#### 2.2 技术实现\\n该命令使用了以下 Shell 特性:\\n- \`echo\` 命令:输出字符串到标准输出\\n- 重定向操作符 \`>\`:将标准输出重定向到文件\\n- 如果文件不存在则... [truncated]"}],"toolCalls":[],"toolCallId":"tool_OtoGSYsrDQpEaav0hLYbXDhd"}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"agent_id: aadd98577\\nresumed: false\\nactual_subagent_type: coder\\nstatus: completed\\n\\n[summary]\\n**详细操作报告 - Sub Agent 3**\\n\\n---\\n\\n## 1. 任务概述\\n\\n本次任务要求以测试用的 sub agent 身份执行一个需要用户批准的操作:在指定目录创建一个包含特定内容的文本文件。\\n\\n---\\n\\n## 2. 操作执行详情\\n\\n### 2.1 第一次尝试\\n\\n| 属性 | 详情 |\\n|------|------|\\n| **目标文件路径** | \`subagent3_test.txt\` |\\n| **请求内容** | \`Hello from Sub Agent 3!\` |\\n| **操作类型** | WriteFile(覆盖模式,默认)|\\n| **执行状态** | 被拒绝 |\\n| **用户反馈** | \\"用另一个文件名再试一次\\" |\\n\\n**技术分析**:系统在第一次请求时触发了用户审批流程。用户拒绝了原始文件名 \`subagent3_test.txt\`,并明确要求使用另一个文件名再试。这表... [truncated]"}],"toolCalls":[],"toolCallId":"tool_zi4b3Pfbiia2kxPMUvjL4cVN"}} @@ -348,7 +348,7 @@ exports[`migration snapshot: with-tool-calls > migration succeeds and matches sn "wire": "{"type":"metadata","protocol_version":"1.0","created_at":} {"type":"context.append_message","message":{"role":"user","content":[{"type":"text","text":"run echo hi"}],"toolCalls":[]}} {"type":"context.append_message","message":{"role":"user","content":[{"type":"text","text":"\\nYou are running in non-interactive mode. The user cannot answer questions or provide feedback during execution.\\n- Do NOT call AskUserQuestion. If you need to make a decision, make your best judgment and proceed.\\n- For EnterPlanMode / ExitPlanMode, they will be auto-approved. You can use them normally but expect no user feedback.\\n"}],"toolCalls":[]}} -{"type":"context.append_message","message":{"role":"assistant","content":[],"toolCalls":[{"type":"function","id":"tc1","name":"Shell","arguments":"{\\"command\\": \\"echo hi\\"}"}]}} +{"type":"context.append_message","message":{"role":"assistant","content":[],"toolCalls":[{"type":"function","id":"tc1","function":{"name":"Shell","arguments":"{\\"command\\": \\"echo hi\\"}"}}]}} {"type":"context.append_message","message":{"role":"tool","content":[{"type":"text","text":"ERROR: Shell blocked by hook"}],"toolCalls":[],"toolCallId":"tc1"}} ", } diff --git a/packages/migration-legacy/test/sessions/close-tool-calls.test.ts b/packages/migration-legacy/test/sessions/close-tool-calls.test.ts index 412a727..e5c7d2d 100644 --- a/packages/migration-legacy/test/sessions/close-tool-calls.test.ts +++ b/packages/migration-legacy/test/sessions/close-tool-calls.test.ts @@ -6,7 +6,7 @@ function assistantWithCall(id: string): NormalizedMessage { return { role: 'assistant', content: [], - toolCalls: [{ type: 'function', id, name: 'Shell', arguments: '{}' }], + toolCalls: [{ type: 'function', id, function: { name: 'Shell', arguments: '{}' } }], }; } diff --git a/packages/migration-legacy/test/sessions/translator.test.ts b/packages/migration-legacy/test/sessions/translator.test.ts index 82d9689..785aab1 100644 --- a/packages/migration-legacy/test/sessions/translator.test.ts +++ b/packages/migration-legacy/test/sessions/translator.test.ts @@ -47,7 +47,7 @@ describe('translateContextLines', () => { const [msg] = translateContextLines([line]); expect(msg!.role).toBe('assistant'); expect(msg!.toolCalls).toEqual([ - { type: 'function', id: 'Shell:0', name: 'Shell', arguments: '{}' }, + { type: 'function', id: 'Shell:0', function: { name: 'Shell', arguments: '{}' } }, ]); }); From 134493cedf4fa86b694706c58da2394ec59954fb Mon Sep 17 00:00:00 2001 From: _Kerman Date: Mon, 25 May 2026 21:30:38 +0800 Subject: [PATCH 5/6] fix: remove migration-legacy from flatten changeset --- .changeset/flatten-toolcall.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.changeset/flatten-toolcall.md b/.changeset/flatten-toolcall.md index 0b1cdd2..006bdcc 100644 --- a/.changeset/flatten-toolcall.md +++ b/.changeset/flatten-toolcall.md @@ -2,7 +2,6 @@ "@moonshot-ai/kosong": minor "@moonshot-ai/agent-core": minor "@moonshot-ai/kimi-code": minor -"@moonshot-ai/migration-legacy": minor --- Flatten tool call data by inlining tool names and arguments at the top level, and limit legacy record migration so it only rewrites matching tool call payloads. From 7c8a2a625e73a2b6088dd5786ef77f5d0015ff17 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Mon, 25 May 2026 22:15:03 +0800 Subject: [PATCH 6/6] fix: scope tool-call wire migration --- .../src/agent/records/migration/v1.1.ts | 45 ++++++++----------- .../test/agent/records/migration/v1.1.test.ts | 29 ++++++++++-- 2 files changed, 45 insertions(+), 29 deletions(-) diff --git a/packages/agent-core/src/agent/records/migration/v1.1.ts b/packages/agent-core/src/agent/records/migration/v1.1.ts index add9d9c..20f3d97 100644 --- a/packages/agent-core/src/agent/records/migration/v1.1.ts +++ b/packages/agent-core/src/agent/records/migration/v1.1.ts @@ -37,34 +37,27 @@ export const migrateV1_0ToV1_1: WireMigration = { sourceVersion: '1.0', targetVersion: '1.1', migrateRecord(record: WireMigrationRecord): WireMigrationRecord { - // Recursively replace old-style ToolCall objects in-place. - const migrate = (value: unknown): unknown => { - if (Array.isArray(value)) { - for (let i = 0; i < value.length; i++) { - const item = value[i]; - if (isLegacyToolCall(item)) { - value[i] = migrateToolCall(item); - } else { - migrate(item); - } - } - return value; - } + if (record.type !== 'context.append_message') return record; - if (isRecord(value)) { - for (const key of Object.keys(value)) { - const v = value[key]; - if (isLegacyToolCall(v)) { - value[key] = migrateToolCall(v); - } else { - migrate(v); - } - } - } - - return value; + const message = record['message'] as { + readonly toolCalls: readonly unknown[]; }; - return migrate(record) as WireMigrationRecord; + let changed = false; + const toolCalls = message.toolCalls.map((toolCall) => { + if (!isLegacyToolCall(toolCall)) return toolCall; + changed = true; + return migrateToolCall(toolCall); + }); + + if (!changed) return record; + + return { + ...record, + message: { + ...message, + toolCalls, + }, + }; }, }; diff --git a/packages/agent-core/test/agent/records/migration/v1.1.test.ts b/packages/agent-core/test/agent/records/migration/v1.1.test.ts index 92e84b7..bfc93b5 100644 --- a/packages/agent-core/test/agent/records/migration/v1.1.test.ts +++ b/packages/agent-core/test/agent/records/migration/v1.1.test.ts @@ -51,6 +51,28 @@ describe('1.0 to 1.1', () => { required: ['function'], }, }, + { + type: 'context.append_loop_event', + event: { + type: 'tool.call', + uuid: 'call_payload', + turnId: '0', + step: 1, + stepUuid: 'step_1', + toolCallId: 'call_payload', + name: 'PayloadTool', + args: { + payload: { + type: 'function', + id: 'user_payload', + function: { + name: 'do-not-migrate', + arguments: '{"keep":true}', + }, + }, + }, + }, + } as unknown as AgentRecord, ]); const records = new AgentRecords(() => {}, persistence); @@ -61,9 +83,10 @@ describe('1.0 to 1.1', () => { protocol_version: AGENT_WIRE_PROTOCOL_VERSION, }); expect(wireSnapshot(persistence.records)).toMatchInlineSnapshot(` - [wire] metadata { "protocol_version": "1.1", "created_at": 1 } - [wire] context.append_message { "message": { "role": "assistant", "content": [], "toolCalls": [ { "type": "function", "id": "call_legacy_bash", "name": "Bash", "arguments": "{\\"command\\":\\"pwd\\"}" } ] } } - [wire] tools.register_user_tool { "name": "schema_tool", "description": "Tool with a schema field named function", "parameters": { "type": "object", "properties": { "function": { "type": "object", "properties": { "name": { "type": "string" } } }, "value": { "type": "string" } }, "required": [ "function" ] } } + [wire] metadata { "protocol_version": "1.1", "created_at": 1 } + [wire] context.append_message { "message": { "role": "assistant", "content": [], "toolCalls": [ { "type": "function", "id": "call_legacy_bash", "name": "Bash", "arguments": "{\\"command\\":\\"pwd\\"}" } ] } } + [wire] tools.register_user_tool { "name": "schema_tool", "description": "Tool with a schema field named function", "parameters": { "type": "object", "properties": { "function": { "type": "object", "properties": { "name": { "type": "string" } } }, "value": { "type": "string" } }, "required": [ "function" ] } } + [wire] context.append_loop_event { "event": { "type": "tool.call", "uuid": "call_payload", "turnId": "0", "step": 1, "stepUuid": "step_1", "toolCallId": "call_payload", "name": "PayloadTool", "args": { "payload": { "type": "function", "id": "user_payload", "function": { "name": "do-not-migrate", "arguments": "{\\"keep\\":true}" } } } } } `); }); });