diff --git a/.changeset/flatten-toolcall.md b/.changeset/flatten-toolcall.md new file mode 100644 index 0000000..006bdcc --- /dev/null +++ b/.changeset/flatten-toolcall.md @@ -0,0 +1,7 @@ +--- +"@moonshot-ai/kosong": minor +"@moonshot-ai/agent-core": minor +"@moonshot-ai/kimi-code": 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. 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/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..20f3d97 --- /dev/null +++ b/packages/agent-core/src/agent/records/migration/v1.1.ts @@ -0,0 +1,63 @@ +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 { + type: 'function'; + id: string; + function: { + name?: string; + arguments?: string | null; + }; +} + +function isLegacyToolCall(v: unknown): v is LegacyToolCall { + 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, + }; +} + +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 { + if (record.type !== 'context.append_message') return record; + + const message = record['message'] as { + readonly toolCalls: readonly unknown[]; + }; + + 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/src/agent/turn/index.ts b/packages/agent-core/src/agent/turn/index.ts index 8aecd7b..d8756a7 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 d61eaa2..3b15d6b 100644 --- a/packages/agent-core/src/agent/turn/kosong-llm.ts +++ b/packages/agent-core/src/agent/turn/kosong-llm.ts @@ -189,15 +189,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/v1.1.test.ts b/packages/agent-core/test/agent/records/migration/v1.1.test.ts new file mode 100644 index 0000000..bfc93b5 --- /dev/null +++ b/packages/agent-core/test/agent/records/migration/v1.1.test.ts @@ -0,0 +1,106 @@ +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'], + }, + }, + { + 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); + + 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" ] } } + [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}" } } } } } + `); + }); +}); + +function wireSnapshot(records: readonly AgentRecord[]) { + return eventSnapshot( + records.map((record) => { + const { type: event, ...args } = record; + return { + type: '[wire]' as const, + event, + args, + }; + }), + new Map(), + ); +} 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/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 }); } }); 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 f028de2..1abc5ab 100644 --- a/packages/kosong/src/providers/anthropic.ts +++ b/packages/kosong/src/providers/anthropic.ts @@ -478,9 +478,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 { @@ -494,7 +494,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); } @@ -649,10 +649,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; } @@ -704,10 +702,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 2dfd052..b28e71c 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/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',