| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| import assert from 'node:assert/strict'; |
| import { MODEL_FAILURE_MESSAGE_MAX_BYTES } from '@maka/core/model-failure'; |
| import { describe, test } from 'node:test'; |
| import type { RuntimeInvocationRecord } from '@maka/core/runtime-invocation'; |
| import type { CreateSessionInput, SessionListFilter } from '@maka/core/runtime-inputs'; |
| import type { RuntimeEvent, RuntimeEventActions } from '@maka/core/runtime-event'; |
| import { runtimeEventHasModelVisibleContent } from '@maka/core/runtime-event'; |
| import type { SessionHeader, SessionSummary, StoredMessage, TurnRecord } from '@maka/core/session'; |
| import { deriveTurnRecords, decodeCanonicalMessage } from '@maka/core/session'; |
| import type { CanonicalPermissionOutcomeRecord } from '../interaction-authority.js'; |
| import { |
| createRuntimeEventStoredMessageProjector, |
| isHardRuntimeEventReadModelDiagnostic, |
| isUnclaimedRuntimeEventDiagnostic, |
| projectTranscriptToolResult, |
| projectRuntimeEventsToStoredMessages, |
| projectRuntimeEventsToStoredMessagesWithArchiveStatuses, |
| } from '../runtime-event-read-model.js'; |
| import { buildRuntimeEventModelReplayPlan } from '../model-history.js'; |
| import { RuntimeReadModel } from '../runtime-read-model.js'; |
| import { backfillRuntimeEventsFromStoredMessages } from '../runtime-event-backfill.js'; |
| import { BackendRegistry, SessionManager, type SessionStore } from '../session-manager.js'; |
| import { testInvocationOpening } from './invocation-fixture.js'; |
| |
| const ts = 1_800_000_000_000; |
| const sessionId = 'sess-1'; |
| const runId = 'run-1'; |
| const turnId = 'turn-1'; |
| const invocationId = 'inv-1'; |
| let eventSeq = 0; |
| |
| /** The same invocation, ended a different way. */ |
| function endedAs( |
| status: 'completed' | 'failed' | 'aborted', |
| failureClass?: string, |
| ): RuntimeInvocationRecord { |
| return { |
| ...invocation, |
| terminalEvent: { |
| ...invocation.terminalEvent!, |
| status, |
| ...(failureClass ? { actions: { endInvocation: true, stateDelta: { failureClass } } } : {}), |
| }, |
| }; |
| } |
| |
| const invocation: RuntimeInvocationRecord = { |
| sessionId, |
| invocationId, |
| runId, |
| turnId, |
| openedAt: ts, |
| opening: testInvocationOpening({ |
| route: { |
| provenance: 'runtime', |
| backendKind: 'ai-sdk', |
| llmConnectionId: 'anthropic-connection', |
| llmConnectionSlug: 'anthropic', |
| modelId: 'claude-sonnet-4-5', |
| }, |
| configuration: { cwd: '/tmp/work' }, |
| lineage: { parentTurnId: 'parent-turn' }, |
| }), |
| terminalEvent: { |
| id: `${runId}-terminal`, |
| sessionId, |
| invocationId, |
| runId, |
| turnId, |
| ts: ts + 20, |
| partial: false, |
| role: 'system', |
| author: 'system', |
| status: 'completed', |
| }, |
| }; |
| |
| function ev(overrides: Partial<RuntimeEvent>): RuntimeEvent { |
| eventSeq += 1; |
| return { |
| id: `event-${eventSeq}`, |
| invocationId, |
| runId, |
| sessionId, |
| turnId, |
| ts, |
| partial: false, |
| role: 'system', |
| author: 'system', |
| ...overrides, |
| }; |
| } |
| |
| function baseEvents(): RuntimeEvent[] { |
| return [ |
| ev({ |
| id: 'evt-user', |
| ts: ts + 1, |
| role: 'user', |
| author: 'user', |
| content: { kind: 'text', text: 'read the file' }, |
| refs: { storedMessageId: 'legacy-user' }, |
| }), |
| ev({ |
| id: 'evt-tool-call', |
| ts: ts + 2, |
| role: 'model', |
| author: 'agent', |
| content: { |
| kind: 'function_call', |
| id: 'tool-1', |
| name: 'Read', |
| args: { path: '/tmp/a.txt' }, |
| }, |
| actions: { stateDelta: { displayName: 'Read file', intent: 'inspect' } }, |
| refs: { toolCallId: 'tool-1' }, |
| }), |
| ev({ |
| id: 'evt-permission-request', |
| ts: ts + 3, |
| role: 'system', |
| author: 'system', |
| actions: { |
| permissionRequest: { |
| kind: 'tool_permission', |
| requestId: 'req-1', |
| toolUseId: 'tool-1', |
| toolName: 'Read', |
| category: 'read', |
| reason: 'custom', |
| args: { path: '/tmp/a.txt' }, |
| rememberForTurnAllowed: true, |
| hint: 'needs read access', |
| }, |
| }, |
| refs: { toolCallId: 'tool-1' }, |
| }), |
| ev({ |
| id: 'evt-permission-decision', |
| ts: ts + 4, |
| role: 'system', |
| author: 'user', |
| actions: { |
| permissionDecision: { |
| requestId: 'req-1', |
| decision: 'allow', |
| rememberForTurn: true, |
| }, |
| }, |
| refs: { toolCallId: 'tool-1' }, |
| }), |
| ev({ |
| id: 'evt-tool-result', |
| ts: ts + 5, |
| role: 'tool', |
| author: 'tool', |
| content: { |
| kind: 'function_response', |
| id: 'tool-1', |
| name: 'Read', |
| result: { kind: 'text', text: 'file contents' }, |
| }, |
| actions: { stateDelta: { durationMs: 42 } }, |
| refs: { toolCallId: 'tool-1', storedMessageId: 'legacy-result' }, |
| }), |
| ev({ |
| id: 'evt-assistant', |
| ts: ts + 6, |
| role: 'model', |
| author: 'agent', |
| content: { kind: 'text', text: 'The file says: file contents' }, |
| refs: { storedMessageId: 'legacy-assistant' }, |
| }), |
| ev({ |
| id: 'evt-token', |
| ts: ts + 7, |
| role: 'system', |
| author: 'system', |
| actions: { |
| tokenUsage: { |
| input: 100, |
| output: 25, |
| cacheRead: 10, |
| costUsd: 0.002, |
| systemPromptHash: 'sys-hash', |
| runtimeSteps: 3, |
| contextRemaining: 9000, |
| }, |
| }, |
| refs: { providerRequestTraceId: 'provider-trace-1' }, |
| }), |
| ev({ |
| id: 'evt-complete', |
| ts: ts + 8, |
| role: 'system', |
| author: 'system', |
| status: 'completed', |
| actions: { endInvocation: true }, |
| }), |
| ]; |
| } |
| |
| function equivalentLegacyMessages(): StoredMessage[] { |
| return [ |
| { |
| type: 'user', |
| id: 'legacy-user', |
| turnId, |
| ts: ts + 1, |
| text: 'read the file', |
| }, |
| { |
| type: 'tool_call', |
| id: 'tool-1', |
| turnId, |
| ts: ts + 2, |
| toolName: 'Read', |
| displayName: 'Read file', |
| intent: 'inspect', |
| args: { path: '/tmp/a.txt' }, |
| }, |
| { |
| type: 'permission_decision', |
| id: 'req-1', |
| turnId, |
| ts: ts + 4, |
| toolUseId: 'tool-1', |
| toolName: 'Read', |
| decision: 'allow', |
| rememberForTurn: true, |
| hint: 'needs read access', |
| }, |
| { |
| type: 'tool_result', |
| id: 'legacy-result', |
| turnId, |
| ts: ts + 5, |
| toolUseId: 'tool-1', |
| isError: false, |
| content: { kind: 'text', text: 'file contents' }, |
| durationMs: 42, |
| }, |
| { |
| type: 'assistant', |
| id: 'legacy-assistant', |
| turnId, |
| ts: ts + 6, |
| text: 'The file says: file contents', |
| modelId: 'claude-sonnet-4-5', |
| }, |
| { |
| type: 'token_usage', |
| id: 'evt-token', |
| turnId, |
| ts: ts + 7, |
| input: 100, |
| output: 25, |
| cacheRead: 10, |
| costUsd: 0.002, |
| systemPromptHash: 'sys-hash', |
| runtimeSteps: 3, |
| contextRemaining: 9000, |
| providerRequestTraceId: 'provider-trace-1', |
| }, |
| { |
| type: 'turn_state', |
| id: 'evt-complete', |
| turnId, |
| ts: ts + 8, |
| status: 'completed', |
| parentTurnId: 'parent-turn', |
| }, |
| ]; |
| } |
| |
| describe('projectRuntimeEventsToStoredMessages', () => { |
| test('streaming projection is equivalent to the batch read model', () => { |
| const events = baseEvents(); |
| const streamed = createRuntimeEventStoredMessageProjector({ invocations: [invocation] }); |
| for (const event of events) streamed.push(event); |
| |
| assert.deepStrictEqual( |
| streamed.finish(), |
| projectRuntimeEventsToStoredMessages(events, { invocations: [invocation] }), |
| ); |
| }); |
| |
| // A transcript page may cut an invocation at any committed event. Rows that |
| // appear for a prefix must be exactly the rows the whole invocation later |
| // attributes to those same events, or a page would change once the Turn ends. |
| test('every event prefix projects the rows the full ledger attributes to it', () => { |
| const events = [ |
| ev({ |
| id: 'evt-prefix-user', |
| ts: ts + 1, |
| role: 'user', |
| author: 'user', |
| content: { kind: 'text', text: 'read the file' }, |
| }), |
| ev({ |
| id: 'evt-prefix-thinking', |
| ts: ts + 2, |
| role: 'model', |
| author: 'agent', |
| content: { kind: 'thinking', text: 'look first', signature: 'sig' }, |
| refs: { providerEventId: 'step-1' }, |
| }), |
| ev({ |
| id: 'evt-prefix-text', |
| ts: ts + 3, |
| role: 'model', |
| author: 'agent', |
| content: { kind: 'text', text: 'Reading it.' }, |
| refs: { providerEventId: 'step-1' }, |
| }), |
| ev({ |
| id: 'evt-prefix-call', |
| ts: ts + 4, |
| role: 'model', |
| author: 'agent', |
| content: { kind: 'function_call', id: 'tool-p', name: 'Read', args: { path: '/a' } }, |
| refs: { toolCallId: 'tool-p', providerEventId: 'step-1' }, |
| }), |
| ev({ |
| id: 'evt-prefix-result', |
| ts: ts + 5, |
| role: 'tool', |
| author: 'tool', |
| content: { |
| kind: 'function_response', |
| id: 'tool-p', |
| name: 'Read', |
| result: { kind: 'text', text: 'contents' }, |
| }, |
| refs: { toolCallId: 'tool-p' }, |
| }), |
| ev({ |
| id: 'evt-prefix-usage', |
| ts: ts + 6, |
| actions: { tokenUsage: { input: 10, output: 5 } }, |
| }), |
| ev({ |
| id: 'evt-prefix-ended', |
| ts: ts + 7, |
| status: 'completed', |
| actions: { endInvocation: true }, |
| }), |
| ]; |
| const project = (prefix: readonly RuntimeEvent[]) => |
| projectRuntimeEventsToStoredMessages(prefix, { invocations: [invocation] }); |
| const full = project(events); |
| assert.deepStrictEqual(full.diagnostics, []); |
| assert.deepStrictEqual( |
| full.messages.map((message) => message.type), |
| ['user', 'assistant', 'tool_call', 'tool_result', 'token_usage', 'turn_state'], |
| ); |
| |
| for (let k = 1; k <= events.length; k += 1) { |
| const seen = new Set(events.slice(0, k).map((event) => event.id)); |
| const expected = full.messages.filter((_, index) => seen.has(full.sourceEventIds[index]!)); |
| const prefix = project(events.slice(0, k)); |
| assert.deepStrictEqual(prefix.messages, expected, `prefix of ${k} events`); |
| assert.deepStrictEqual(prefix.diagnostics, [], `prefix of ${k} events`); |
| } |
| assert.deepStrictEqual( |
| project(events.slice(0, 2)).messages.map((message) => message.type), |
| ['user'], |
| ); |
| }); |
| |
| test('unclaimed thinking is a defect only once its invocation has ended', () => { |
| const thinking = ev({ |
| id: 'evt-orphan-thinking', |
| role: 'model', |
| author: 'agent', |
| content: { kind: 'thinking', text: 'no answer followed' }, |
| refs: { providerEventId: 'step-orphan' }, |
| }); |
| const ended = ev({ |
| id: 'evt-orphan-ended', |
| status: 'completed', |
| actions: { endInvocation: true }, |
| }); |
| |
| assert.deepStrictEqual( |
| projectRuntimeEventsToStoredMessages([thinking], { invocations: [invocation] }).diagnostics, |
| [], |
| ); |
| assert.deepStrictEqual( |
| projectRuntimeEventsToStoredMessages([thinking, ended], { |
| invocations: [invocation], |
| }).diagnostics.map((diagnostic) => [diagnostic.code, diagnostic.eventId]), |
| [['unsupported_event', 'evt-orphan-thinking']], |
| ); |
| }); |
| |
| test('loads canonical permission outcomes after the streaming scan and preserves acceptance order', () => { |
| const request = ev({ |
| id: 'evt-canonical-request', |
| ts: ts + 1, |
| actions: { |
| permissionRequest: { |
| kind: 'tool_permission', |
| requestId: 'req-canonical', |
| toolUseId: 'tool-canonical', |
| toolName: 'Read', |
| category: 'read', |
| reason: 'custom', |
| args: { path: '/tmp/a.txt' }, |
| rememberForTurnAllowed: true, |
| hint: 'read it', |
| }, |
| }, |
| refs: { toolCallId: 'tool-canonical' }, |
| }); |
| const accepted = ev({ |
| id: 'evt-canonical-accepted', |
| ts: ts + 2, |
| status: 'completed', |
| role: 'tool', |
| author: 'user', |
| content: { |
| kind: 'function_response', |
| id: 'tool-canonical', |
| name: 'Read', |
| result: { kind: 'text', text: 'result before permission' }, |
| }, |
| actions: { |
| permissionAnswerAccepted: { requestId: 'req-canonical' }, |
| tokenUsage: { input: 10, output: 5 }, |
| endInvocation: true, |
| }, |
| refs: { toolCallId: 'tool-canonical' }, |
| }); |
| const later = ev({ |
| id: 'evt-after-permission', |
| ts: ts + 3, |
| role: 'user', |
| author: 'user', |
| content: { kind: 'text', text: 'after' }, |
| }); |
| const options: Parameters<typeof createRuntimeEventStoredMessageProjector>[0] = { |
| invocations: [invocation], |
| }; |
| const projector = createRuntimeEventStoredMessageProjector(options); |
| projector.push(request); |
| projector.push(accepted); |
| // A later malformed duplicate must not rewrite the request identity that |
| // was paired when the acceptance entered the ledger. |
| projector.push( |
| ev({ |
| id: 'evt-duplicate-request', |
| actions: { |
| permissionRequest: { |
| kind: 'tool_permission', |
| requestId: 'req-canonical', |
| toolUseId: 'different-tool', |
| toolName: 'Write', |
| category: 'file_write', |
| reason: 'custom', |
| args: { path: '/tmp/other.txt' }, |
| rememberForTurnAllowed: true, |
| }, |
| }, |
| }), |
| ); |
| projector.push(later); |
| assert.deepStrictEqual(projector.permissionRequestIds, ['req-canonical']); |
| |
| const canonical: CanonicalPermissionOutcomeRecord = { |
| sessionId, |
| runId, |
| turnId, |
| requestId: 'req-canonical', |
| request: { |
| kind: 'permission', |
| toolUseId: 'tool-canonical', |
| prompt: { |
| kind: 'tool_permission', |
| toolName: 'Read', |
| category: 'read', |
| reason: 'custom', |
| review: { kind: 'path', operation: 'read', path: '/tmp/a.txt' }, |
| rememberForTurnAllowed: true, |
| }, |
| }, |
| outcome: { |
| kind: 'permission_answer', |
| decision: 'allow', |
| rememberForTurn: false, |
| reviewer: 'user', |
| committedAt: ts + 2, |
| }, |
| }; |
| options.canonicalPermissionOutcomes = new Map([['req-canonical', canonical]]); |
| |
| const out = projector.finish(); |
| assert.deepStrictEqual( |
| out.messages.map((message) => message.type), |
| ['tool_result', 'permission_decision', 'token_usage', 'turn_state', 'user'], |
| ); |
| assert.deepStrictEqual(out.sourceEventIds, [ |
| accepted.id, |
| accepted.id, |
| accepted.id, |
| accepted.id, |
| later.id, |
| ]); |
| assert.deepStrictEqual(out.diagnostics, []); |
| }); |
| |
| test('projects decoded tool results during push and reports emitted messages', () => { |
| const emitted: Array<{ type: StoredMessage['type']; sourceEventId: string }> = []; |
| const projector = createRuntimeEventStoredMessageProjector({ |
| invocations: [invocation], |
| projectToolResult: (_event, decoded) => |
| decoded.kind === 'text' ? { kind: 'text', text: decoded.text.slice(0, 4) } : decoded, |
| onMessage: (message, sourceEventId) => emitted.push({ type: message.type, sourceEventId }), |
| }); |
| const event = ev({ |
| id: 'evt-large-result', |
| role: 'tool', |
| author: 'tool', |
| content: { |
| kind: 'function_response', |
| id: 'tool-large', |
| name: 'Bash', |
| result: { kind: 'text', text: 'large result' }, |
| }, |
| refs: { toolCallId: 'tool-large' }, |
| }); |
| projector.push(event); |
| |
| assert.deepStrictEqual(emitted, [{ type: 'tool_result', sourceEventId: event.id }]); |
| assert.deepStrictEqual(projector.finish().messages[0], { |
| type: 'tool_result', |
| id: event.id, |
| turnId, |
| ts, |
| toolUseId: 'tool-large', |
| isError: false, |
| content: { kind: 'text', text: 'larg' }, |
| }); |
| }); |
| |
| test('bounds local terminal transcript output to a recoverable tail preview', async () => { |
| const event = ev({ |
| id: 'evt-terminal/result', |
| role: 'tool', |
| author: 'tool', |
| content: { |
| kind: 'function_response', |
| id: 'tool-terminal', |
| name: 'Bash', |
| result: { kind: 'text', text: 'provider-facing result' }, |
| modelProjection: { version: 1, kind: 'json', value: { kind: 'terminal' } }, |
| }, |
| refs: { toolCallId: 'tool-terminal' }, |
| }); |
| const terminal = { |
| kind: 'terminal' as const, |
| cwd: '/workspace', |
| cmd: 'large-command', |
| status: 'failed' as const, |
| exitCode: 7, |
| failureMessage: 'failed', |
| output: { |
| mode: 'pipes' as const, |
| stdout: Array.from({ length: 30 }, (_, index) => `stdout-${index + 1}`).join('\n'), |
| stderr: 'x'.repeat(2048), |
| stdoutTruncated: false, |
| stderrTruncated: false, |
| redacted: false, |
| }, |
| }; |
| |
| const projected = projectTranscriptToolResult(event, terminal); |
| assert.equal(projected.kind, 'terminal'); |
| if (projected.kind !== 'terminal' || projected.output.mode !== 'pipes') return; |
| assert.equal(projected.cwd, terminal.cwd); |
| assert.equal(projected.cmd, terminal.cmd); |
| assert.equal(projected.status, terminal.status); |
| assert.equal(projected.exitCode, terminal.exitCode); |
| assert.equal(projected.failureMessage, terminal.failureMessage); |
| assert.equal(projected.output.stdoutTruncated, true); |
| assert.equal(projected.output.stderrTruncated, true); |
| assert.match(projected.output.stdout, /stdout-30$/); |
| assert.doesNotMatch(projected.output.stdout, /stdout-1\n/); |
| assert.match(projected.output.stderr, /maka:\/\/runtime\/tool-results\/evt-terminal%2Fresult/); |
| |
| const sourceEvent = { |
| ...event, |
| content: { ...event.content!, result: terminal }, |
| } as RuntimeEvent; |
| const view = await new RuntimeReadModel({ |
| runtimeEventStore: { |
| listSessionInvocations: async () => [{ ...invocation, terminalEvent: undefined }], |
| readSessionRuntimeEventEntries: async () => [], |
| readRuntimeEvents: async () => [sourceEvent], |
| } as never, |
| }).getSessionView(sessionId); |
| const result = view.messages.find((message) => message.type === 'tool_result'); |
| assert.deepEqual(result?.content, projected); |
| assert.deepEqual(view.events[0]?.content, sourceEvent.content); |
| |
| assert.strictEqual( |
| projectTranscriptToolResult( |
| { |
| ...event, |
| content: { ...event.content!, providerExecuted: true } as RuntimeEvent['content'], |
| }, |
| terminal, |
| ), |
| terminal, |
| ); |
| }); |
| |
| test('exposes a session image ref as a Markdown image source to the model', () => { |
| const replay = buildRuntimeEventModelReplayPlan([ |
| ev({ |
| role: 'user', |
| author: 'user', |
| content: { |
| kind: 'text', |
| text: 'show this', |
| attachments: [ |
| { |
| kind: 'image', |
| name: 'preview.png', |
| mimeType: 'image/png', |
| bytes: 3, |
| ref: { |
| kind: 'session_file', |
| sessionId, |
| relativePath: 'attachment-123', |
| }, |
| }, |
| ], |
| }, |
| }), |
| ]); |
| |
| const item = replay.items[0]; |
| assert.equal(item?.kind, 'text'); |
| assert.match( |
| item?.kind === 'text' ? item.content : '', |
| /Markdown image source: "maka:\/\/runtime\/attachments\/attachment-123"/, |
| ); |
| }); |
| |
| test('projects user displayText from RuntimeEvent text content', () => { |
| const typed = '/skill:alpha 帮我整理'; |
| const envelope = 'The user explicitly invoked…\n\n<user-message>\n帮我整理\n</user-message>'; |
| const out = projectRuntimeEventsToStoredMessages( |
| [ |
| ev({ |
| id: 'evt-user-skill', |
| ts: ts + 1, |
| role: 'user', |
| author: 'user', |
| content: { kind: 'text', text: envelope, displayText: typed }, |
| refs: { storedMessageId: 'user-skill' }, |
| }), |
| ], |
| { invocations: [invocation] }, |
| ); |
| assert.deepStrictEqual(out.messages, [ |
| { |
| type: 'user', |
| id: 'user-skill', |
| turnId, |
| ts: ts + 1, |
| text: envelope, |
| displayText: typed, |
| }, |
| ]); |
| }); |
| |
| test('full RuntimeEvent turn projects legacy-compatible rows', () => { |
| const out = projectRuntimeEventsToStoredMessages(baseEvents(), { invocations: [invocation] }); |
| |
| assert.deepStrictEqual( |
| out.messages.map((message) => message.type), |
| [ |
| 'user', |
| 'tool_call', |
| 'permission_decision', |
| 'tool_result', |
| 'assistant', |
| 'token_usage', |
| 'turn_state', |
| ], |
| ); |
| assert.partialDeepStrictEqual(out.messages[1], { |
| type: 'tool_call', |
| id: 'tool-1', |
| toolName: 'Read', |
| displayName: 'Read file', |
| intent: 'inspect', |
| }); |
| assert.partialDeepStrictEqual(out.messages[2], { |
| type: 'permission_decision', |
| id: 'req-1', |
| toolUseId: 'tool-1', |
| toolName: 'Read', |
| decision: 'allow', |
| hint: 'needs read access', |
| }); |
| assert.partialDeepStrictEqual(out.messages[3], { |
| type: 'tool_result', |
| id: 'legacy-result', |
| toolUseId: 'tool-1', |
| durationMs: 42, |
| }); |
| assert.partialDeepStrictEqual(out.messages[4], { |
| type: 'assistant', |
| modelId: 'claude-sonnet-4-5', |
| text: 'The file says: file contents', |
| }); |
| assert.partialDeepStrictEqual(out.messages[6], { |
| type: 'turn_state', |
| status: 'completed', |
| parentTurnId: 'parent-turn', |
| }); |
| assert.deepStrictEqual(out.diagnostics, []); |
| }); |
| |
| test('projects provider-native search through the canonical read model while replay keeps raw output', () => { |
| const rawProviderOutput = [ |
| { |
| type: 'web_search_result', |
| url: 'https://maka.example/', |
| title: 'Maka', |
| pageAge: null, |
| encryptedContent: 'encrypted-result', |
| }, |
| ]; |
| const events = [ |
| ev({ |
| id: 'evt-native-text', |
| ts: ts + 1, |
| role: 'model', |
| author: 'agent', |
| content: { |
| kind: 'text', |
| text: 'Maka is current.', |
| providerOptions: { |
| openai: { |
| itemId: 'message-1', |
| annotations: [{ type: 'url_citation', url: 'https://maka.example/' }], |
| }, |
| }, |
| }, |
| refs: { providerEventId: 'step-native' }, |
| }), |
| ev({ |
| id: 'evt-native-call', |
| ts: ts + 2, |
| role: 'model', |
| author: 'agent', |
| content: { |
| kind: 'function_call', |
| id: 'search-1', |
| name: 'WebSearch', |
| args: { query: 'latest Maka' }, |
| providerOptions: { anthropic: { type: 'server_tool_use' } }, |
| providerExecuted: true, |
| }, |
| refs: { toolCallId: 'search-1', stepId: 'step-native' }, |
| }), |
| ev({ |
| id: 'evt-native-result', |
| ts: ts + 3, |
| role: 'tool', |
| author: 'tool', |
| content: { |
| kind: 'function_response', |
| id: 'search-1', |
| name: 'WebSearch', |
| result: { |
| kind: 'web_search', |
| provider: 'model', |
| query: 'latest Maka', |
| rows: [ |
| { |
| title: 'Maka', |
| url: 'https://maka.example/', |
| snippet: '', |
| source: 'maka.example', |
| }, |
| ], |
| }, |
| providerExecuted: true, |
| providerOutput: rawProviderOutput, |
| }, |
| refs: { toolCallId: 'search-1' }, |
| }), |
| ]; |
| |
| const projected = projectRuntimeEventsToStoredMessages(events, { invocations: [invocation] }); |
| assert.deepStrictEqual(projected.diagnostics, []); |
| assert.partialDeepStrictEqual(projected.messages[0], { type: 'assistant' }); |
| assert.deepStrictEqual( |
| (projected.messages[0] as { providerOptions?: unknown }).providerOptions, |
| { |
| openai: { |
| itemId: 'message-1', |
| annotations: [{ type: 'url_citation', url: 'https://maka.example/' }], |
| }, |
| }, |
| ); |
| assert.partialDeepStrictEqual(projected.messages[1], { |
| type: 'tool_call', |
| providerExecuted: true, |
| }); |
| assert.deepStrictEqual( |
| (projected.messages[1] as { providerOptions?: unknown }).providerOptions, |
| { |
| anthropic: { type: 'server_tool_use' }, |
| }, |
| ); |
| assert.partialDeepStrictEqual(projected.messages[2], { |
| type: 'tool_result', |
| providerExecuted: true, |
| providerOutput: rawProviderOutput, |
| }); |
| assert.deepStrictEqual((projected.messages[2] as { content?: unknown }).content, { |
| kind: 'web_search', |
| provider: 'model', |
| query: 'latest Maka', |
| rows: [ |
| { |
| title: 'Maka', |
| url: 'https://maka.example/', |
| snippet: '', |
| source: 'maka.example', |
| }, |
| ], |
| }); |
| |
| const replay = buildRuntimeEventModelReplayPlan(events); |
| assert.deepStrictEqual(replay.diagnostics, []); |
| assert.partialDeepStrictEqual( |
| replay.items.find((item) => item.kind === 'tool_result' && item.toolCallId === 'search-1'), |
| { |
| output: rawProviderOutput, |
| providerExecuted: true, |
| }, |
| ); |
| }); |
| |
| test('projects an AskUserQuestion round trip without a legacy row for the live request', () => { |
| const out = projectRuntimeEventsToStoredMessages( |
| [ |
| ev({ |
| id: 'evt-question-call', |
| ts: ts + 1, |
| role: 'model', |
| author: 'agent', |
| content: { |
| kind: 'function_call', |
| id: 'question-tool-1', |
| name: 'AskUserQuestion', |
| args: { questions: [{ question: 'Choose', options: [{ label: 'Extend' }] }] }, |
| }, |
| refs: { toolCallId: 'question-tool-1' }, |
| }), |
| ev({ |
| id: 'evt-question-request', |
| ts: ts + 2, |
| actions: { |
| userQuestionRequest: { |
| requestId: 'question-1', |
| toolUseId: 'question-tool-1', |
| questions: [{ question: 'Choose', options: [{ label: 'Extend' }] }], |
| }, |
| }, |
| refs: { toolCallId: 'question-tool-1' }, |
| }), |
| ev({ |
| id: 'evt-question-result', |
| ts: ts + 3, |
| role: 'tool', |
| author: 'tool', |
| content: { |
| kind: 'function_response', |
| id: 'question-tool-1', |
| name: 'AskUserQuestion', |
| result: { kind: 'json', value: { answers: ['Extend'] } }, |
| }, |
| refs: { toolCallId: 'question-tool-1' }, |
| }), |
| ev({ |
| id: 'evt-question-complete', |
| ts: ts + 4, |
| status: 'completed', |
| actions: { endInvocation: true }, |
| }), |
| ], |
| { invocations: [invocation] }, |
| ); |
| |
| assert.deepStrictEqual( |
| out.messages.map((message) => message.type), |
| ['tool_call', 'tool_result', 'turn_state'], |
| ); |
| assert.deepStrictEqual(out.diagnostics, []); |
| }); |
| |
| test('replays generic provider tool results without Maka result decoding', () => { |
| const events = [ |
| ev({ |
| id: 'evt-generic-primitive-call', |
| role: 'model', |
| author: 'agent', |
| content: { |
| kind: 'function_call', |
| id: 'generic-primitive-1', |
| name: 'ProviderPrimitive', |
| args: {}, |
| }, |
| }), |
| ev({ |
| id: 'evt-generic-primitive-result', |
| role: 'tool', |
| author: 'tool', |
| content: { |
| kind: 'function_response', |
| id: 'generic-primitive-1', |
| name: 'ProviderPrimitive', |
| result: 42 as never, |
| }, |
| }), |
| ev({ |
| id: 'evt-generic-json-call', |
| role: 'model', |
| author: 'agent', |
| content: { |
| kind: 'function_call', |
| id: 'generic-json-1', |
| name: 'ProviderJson', |
| args: {}, |
| }, |
| }), |
| ev({ |
| id: 'evt-generic-json-result', |
| role: 'tool', |
| author: 'tool', |
| content: { |
| kind: 'function_response', |
| id: 'generic-json-1', |
| name: 'ProviderJson', |
| result: { providerPayload: true, values: [1, 2, 3] } as never, |
| }, |
| }), |
| ev({ |
| id: 'evt-generic-subagent-collision-call', |
| role: 'model', |
| author: 'agent', |
| content: { |
| kind: 'function_call', |
| id: 'generic-subagent-collision-1', |
| name: 'ProviderJson', |
| args: {}, |
| }, |
| }), |
| ev({ |
| id: 'evt-generic-subagent-collision-result', |
| role: 'tool', |
| author: 'tool', |
| content: { |
| kind: 'function_response', |
| id: 'generic-subagent-collision-1', |
| name: 'ProviderJson', |
| result: { |
| kind: 'subagent', |
| status: 'waiting_permission', |
| providerPayload: true, |
| } as never, |
| }, |
| }), |
| ]; |
| |
| const replay = buildRuntimeEventModelReplayPlan(events); |
| |
| assert.deepStrictEqual( |
| replay.items.map((item) => item.kind), |
| ['tool_call', 'tool_result', 'tool_call', 'tool_result', 'tool_call', 'tool_result'], |
| ); |
| assert.deepStrictEqual( |
| replay.items.filter((item) => item.kind === 'tool_result').map((item) => item.output), |
| [ |
| 42, |
| { providerPayload: true, values: [1, 2, 3] }, |
| { |
| kind: 'subagent', |
| status: 'waiting_permission', |
| providerPayload: true, |
| }, |
| ], |
| ); |
| assert.deepStrictEqual(replay.diagnostics, []); |
| }); |
| |
| test('folds retired permission modes while projecting persisted tool results', () => { |
| const out = projectRuntimeEventsToStoredMessages( |
| [ |
| ev({ |
| id: 'evt-persisted-subagent-result', |
| role: 'tool', |
| author: 'tool', |
| content: { |
| kind: 'function_response', |
| id: 'tool-subagent', |
| name: 'subagent', |
| result: { |
| kind: 'subagent', |
| agentName: 'Researcher', |
| turnId: 'child-turn', |
| runId: 'child-run', |
| status: 'completed', |
| permissionMode: 'execute', |
| summary: 'done', |
| artifactIds: [], |
| } as never, |
| }, |
| refs: { toolCallId: 'tool-subagent' }, |
| }), |
| ], |
| { invocations: [invocation] }, |
| ); |
| |
| const projected = out.messages.find((message) => message.type === 'tool_result'); |
| assert.deepStrictEqual( |
| projected?.type === 'tool_result' && projected.content.kind === 'subagent' |
| ? projected.content.permissionMode |
| : undefined, |
| 'ask', |
| ); |
| assert.deepStrictEqual(out.diagnostics, []); |
| }); |
| |
| test('folds retired ExploreAgent results before model replay', () => { |
| const result = { |
| kind: 'explore_agent', |
| ok: false, |
| terminalStatus: 'failed', |
| mode: 'read_only', |
| objective: 'Trace the session lifecycle.', |
| roots: ['packages/runtime'], |
| queries: ['SessionManager'], |
| filesInspected: 0, |
| filesSkipped: 0, |
| bytesRead: 0, |
| candidateFiles: [], |
| matches: [], |
| notes: [], |
| summary: '未完成:目标无效。', |
| report: '', |
| reason: 'invalid_objective', |
| message: '目标无效。', |
| } as const; |
| const events = [ |
| ev({ |
| id: 'evt-explore-call', |
| role: 'model', |
| author: 'agent', |
| content: { |
| kind: 'function_call', |
| id: 'explore-1', |
| name: 'ExploreAgent', |
| args: { objective: result.objective }, |
| }, |
| }), |
| ev({ |
| id: 'evt-explore-result', |
| role: 'tool', |
| author: 'tool', |
| content: { |
| kind: 'function_response', |
| id: 'explore-1', |
| name: 'ExploreAgent', |
| result: result as never, |
| }, |
| }), |
| ]; |
| |
| const replay = buildRuntimeEventModelReplayPlan(events); |
| |
| assert.deepStrictEqual( |
| (replay.items.find((item) => item.kind === 'tool_result') as { output?: unknown } | undefined) |
| ?.output, |
| { kind: 'text', text: '未完成:目标无效。' }, |
| ); |
| assert.deepStrictEqual(replay.diagnostics, []); |
| }); |
| |
| test('restores a settled Agent Swarm function response', () => { |
| const result = { |
| kind: 'agent_swarm' as const, |
| status: 'completed' as const, |
| items: [ |
| { |
| itemId: 'contract', |
| index: 0, |
| profile: 'local_read', |
| started: true, |
| agentId: 'local-read', |
| agentName: 'Local Read', |
| turnId: 'child-turn', |
| runId: 'child-run', |
| status: 'completed' as const, |
| summary: 'Verified the contract.', |
| artifactIds: [], |
| startedAt: ts + 1, |
| completedAt: ts + 2, |
| durationMs: 1, |
| }, |
| ], |
| startedAt: ts, |
| completedAt: ts + 2, |
| durationMs: 2, |
| }; |
| const out = projectRuntimeEventsToStoredMessages( |
| [ |
| ev({ |
| id: 'evt-agent-swarm-result', |
| role: 'tool', |
| author: 'tool', |
| content: { |
| kind: 'function_response', |
| id: 'tool-agent-swarm', |
| name: 'agent_swarm', |
| result, |
| }, |
| refs: { toolCallId: 'tool-agent-swarm' }, |
| }), |
| ], |
| { invocations: [invocation] }, |
| ); |
| |
| const projected = out.messages.find((message) => message.type === 'tool_result'); |
| assert.deepStrictEqual( |
| projected?.type === 'tool_result' ? projected.content : undefined, |
| result, |
| ); |
| assert.deepStrictEqual(out.diagnostics, []); |
| }); |
| |
| test('projects first-observed step content order for stable live handoff', () => { |
| const out = projectRuntimeEventsToStoredMessages( |
| [ |
| ev({ |
| ts: ts + 1, |
| role: 'model', |
| author: 'agent', |
| content: { kind: 'function_call', id: 'tool-1', name: 'Read', args: {} }, |
| refs: { toolCallId: 'tool-1', stepId: 'message-1' }, |
| }), |
| ev({ |
| ts: ts + 2, |
| role: 'model', |
| author: 'agent', |
| content: { kind: 'thinking', text: 'late reasoning' }, |
| refs: { providerEventId: 'message-1' }, |
| }), |
| ev({ |
| ts: ts + 3, |
| role: 'model', |
| author: 'agent', |
| content: { kind: 'text', text: 'answer' }, |
| refs: { providerEventId: 'message-1' }, |
| }), |
| ], |
| { invocations: [invocation] }, |
| ); |
| |
| const assistant = out.messages.find((message) => message.type === 'assistant'); |
| assert.deepStrictEqual( |
| (assistant as unknown as { contentOrder?: string[] } | undefined)?.contentOrder, |
| ['tools', 'thinking', 'text'], |
| ); |
| }); |
| |
| test('archived tool-result placeholders project to diagnostic tool-result rows', () => { |
| const events = baseEvents(); |
| const toolResult = events.find((event) => event.id === 'evt-tool-result'); |
| if (toolResult?.content?.kind !== 'function_response') |
| throw new Error('fixture missing tool result'); |
| toolResult.content.result = { |
| kind: 'maka.archived_tool_result', |
| rewriteVersion: 1, |
| artifactId: 'artifact-tool-result', |
| runtimeEventId: 'evt-tool-result', |
| toolCallId: 'tool-1', |
| toolName: 'Read', |
| bodySha256: 'a'.repeat(64), |
| originalEstimatedTokens: 200, |
| originalBytes: 800, |
| reason: 'stale_tool_result_pruned_before_compact', |
| }; |
| |
| const out = projectRuntimeEventsToStoredMessages(events, { invocations: [invocation] }); |
| const projected = out.messages.find((message) => message.type === 'tool_result'); |
| |
| assert.partialDeepStrictEqual(projected, { type: 'tool_result', toolUseId: 'tool-1' }); |
| assert.deepStrictEqual((projected as { content?: unknown } | undefined)?.content, { |
| kind: 'archived_tool_result', |
| status: 'not_loaded', |
| artifactId: 'artifact-tool-result', |
| bodySha256: 'a'.repeat(64), |
| runtimeEventId: 'evt-tool-result', |
| toolCallId: 'tool-1', |
| toolName: 'Read', |
| originalEstimatedTokens: 200, |
| originalBytes: 800, |
| rewriteVersion: 1, |
| reason: 'stale_tool_result_pruned_before_compact', |
| }); |
| assert.deepStrictEqual( |
| out.diagnostics.map((diag) => diag.code), |
| ['archived_tool_result_placeholder'], |
| ); |
| }); |
| |
| test('legacy archived tool-result placeholders gain a deterministic ArchiveRead ref for replay', () => { |
| const events = baseEvents(); |
| const toolResult = events.find((event) => event.id === 'evt-tool-result'); |
| if (toolResult?.content?.kind !== 'function_response') |
| throw new Error('fixture missing tool result'); |
| toolResult.content.result = { |
| kind: 'maka.archived_tool_result', |
| rewriteVersion: 1, |
| artifactId: 'artifact-tool-result', |
| runtimeEventId: 'evt-tool-result', |
| toolCallId: 'tool-1', |
| toolName: 'Read', |
| bodySha256: 'a'.repeat(64), |
| originalEstimatedTokens: 200, |
| originalBytes: 800, |
| reason: 'stale_tool_result_pruned_before_compact', |
| }; |
| |
| const replay = buildRuntimeEventModelReplayPlan(events); |
| const result = replay.items.find( |
| (item) => item.kind === 'tool_result' && item.toolCallId === 'tool-1', |
| ); |
| assert.equal( |
| result?.kind === 'tool_result' && typeof result.output === 'object' && result.output !== null |
| ? (result.output as { resourceRef?: string }).resourceRef |
| : undefined, |
| `maka://archive/artifact-tool-result/${'a'.repeat(64)}/800`, |
| ); |
| }); |
| |
| test('archive status wrapper can project missing and corrupt rows without changing sync defaults', () => { |
| const events = baseEvents(); |
| const toolResult = events.find((event) => event.id === 'evt-tool-result'); |
| if (toolResult?.content?.kind !== 'function_response') |
| throw new Error('fixture missing tool result'); |
| toolResult.content.result = { |
| kind: 'maka.archived_tool_result', |
| rewriteVersion: 1, |
| artifactId: 'artifact-tool-result', |
| runtimeEventId: 'evt-tool-result', |
| toolCallId: 'tool-1', |
| toolName: 'Read', |
| bodySha256: 'a'.repeat(64), |
| originalEstimatedTokens: 200, |
| originalBytes: 800, |
| reason: 'stale_tool_result_pruned_before_compact', |
| }; |
| |
| const defaultOut = projectRuntimeEventsToStoredMessages(events, { invocations: [invocation] }); |
| const defaultProjected = defaultOut.messages.find((message) => message.type === 'tool_result'); |
| assert.partialDeepStrictEqual(defaultProjected, { type: 'tool_result' }); |
| assert.strictEqual(archivedStatus(defaultProjected), 'not_loaded'); |
| |
| const missingOut = projectRuntimeEventsToStoredMessagesWithArchiveStatuses(events, { |
| invocations: [invocation], |
| archiveStatuses: { 'evt-tool-result': 'missing' }, |
| }); |
| const missingProjected = missingOut.messages.find((message) => message.type === 'tool_result'); |
| assert.partialDeepStrictEqual(missingProjected, { type: 'tool_result' }); |
| assert.strictEqual(archivedStatus(missingProjected), 'missing'); |
| |
| const corruptOut = projectRuntimeEventsToStoredMessagesWithArchiveStatuses(events, { |
| invocations: [invocation], |
| archiveStatuses: [{ runtimeEventId: 'evt-tool-result', status: 'corrupt' }], |
| }); |
| const corruptProjected = corruptOut.messages.find((message) => message.type === 'tool_result'); |
| assert.partialDeepStrictEqual(corruptProjected, { type: 'tool_result' }); |
| assert.strictEqual(archivedStatus(corruptProjected), 'corrupt'); |
| }); |
| |
| test('partial RuntimeEvents are excluded', () => { |
| const out = projectRuntimeEventsToStoredMessages( |
| [ |
| ev({ |
| id: 'evt-partial', |
| partial: true, |
| role: 'model', |
| author: 'agent', |
| content: { kind: 'text', text: 'streaming' }, |
| }), |
| ev({ |
| id: 'evt-final', |
| role: 'model', |
| author: 'agent', |
| content: { kind: 'text', text: 'final' }, |
| }), |
| ], |
| { invocations: [invocation] }, |
| ); |
| |
| assert.strictEqual(out.messages.length, 1); |
| assert.partialDeepStrictEqual(out.messages[0], { type: 'assistant', text: 'final' }); |
| assert.deepStrictEqual( |
| out.diagnostics.map((diag) => diag.code), |
| ['partial_skipped'], |
| ); |
| }); |
| |
| test('tool dispatch recovery facts are accepted without creating legacy message rows', () => { |
| const out = projectRuntimeEventsToStoredMessages( |
| [ |
| ev({ |
| id: 'toolop-1-dispatch', |
| role: 'system', |
| author: 'system', |
| actions: { |
| toolDispatch: { |
| protocol: 't1_after_preflight_v1', |
| operationId: 'toolop-1', |
| providerToolCallId: 'tool-1', |
| toolName: 'Bash', |
| canonicalArgsHash: 'sha256:args', |
| recoveryMode: 'reconcile', |
| }, |
| }, |
| refs: { toolCallId: 'tool-1', operationId: 'toolop-1' }, |
| }), |
| ], |
| { invocations: [invocation] }, |
| ); |
| |
| assert.deepStrictEqual(out.messages, []); |
| assert.deepStrictEqual(out.diagnostics, []); |
| }); |
| |
| test('question answer acknowledgements remain non-visible audit facts', () => { |
| const out = projectRuntimeEventsToStoredMessages( |
| [ |
| ev({ |
| id: 'question-1-answered', |
| role: 'system', |
| author: 'user', |
| actions: { userQuestionAnswerAccepted: { requestId: 'question-1' } }, |
| refs: { toolCallId: 'tool-1' }, |
| }), |
| ], |
| { invocations: [invocation] }, |
| ); |
| |
| assert.deepStrictEqual(out.messages, []); |
| assert.deepStrictEqual(out.diagnostics, []); |
| }); |
| |
| test('terminal recovery bundle facts are accepted without creating legacy message rows', () => { |
| const out = projectRuntimeEventsToStoredMessages( |
| [ |
| ev({ |
| id: 'toolop-1-reconcile', |
| role: 'system', |
| author: 'system', |
| actions: { |
| toolRecovery: { |
| kind: 'maka.tool.reconcile_result', |
| version: 1, |
| payload: { |
| protocol: 'tool_reconcile_v1', |
| operationId: 'toolop-1', |
| observation: 'unreadable', |
| observationSchema: 'state_identity_v1', |
| observationDigest: |
| 'sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', |
| }, |
| }, |
| }, |
| refs: { toolCallId: 'tool-1', operationId: 'toolop-1' }, |
| }), |
| ev({ |
| id: 'toolop-1-decision', |
| role: 'system', |
| author: 'system', |
| actions: { |
| toolRecovery: { |
| kind: 'maka.tool.recovery_decision', |
| version: 1, |
| payload: { |
| protocol: 'tool_recovery_v1', |
| operationId: 'toolop-1', |
| disposition: 'parked', |
| reasonCode: 'reconcile_unreadable', |
| evidenceEventIds: ['call-1', 'dispatch-1', 'toolop-1-reconcile'], |
| }, |
| }, |
| }, |
| refs: { toolCallId: 'tool-1', operationId: 'toolop-1' }, |
| }), |
| ], |
| { invocations: [invocation] }, |
| ); |
| |
| assert.deepStrictEqual(out.messages, []); |
| assert.deepStrictEqual(out.diagnostics, []); |
| }); |
| |
| test('continuation-start recovery facts are accepted without creating legacy message rows', () => { |
| const out = projectRuntimeEventsToStoredMessages( |
| [ |
| ev({ |
| id: 'continuation-start', |
| role: 'system', |
| author: 'system', |
| actions: { |
| continuationStart: { |
| protocol: 'continuation_start_v2', |
| provenance: 'runtime_admission', |
| claimId: 'claim-1', |
| boundaryDigest: |
| 'sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', |
| immediateSource: { |
| sessionId: 'session-1', |
| invocationId: 'source-invocation', |
| runId: 'source-run', |
| turnId: 'source-turn', |
| highWater: 2, |
| prefixDigest: |
| 'sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', |
| }, |
| replayManifestDigest: |
| 'sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', |
| providerProjectionVersion: 1, |
| providerReplayDigest: |
| 'sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc', |
| }, |
| }, |
| }), |
| ], |
| { invocations: [invocation] }, |
| ); |
| |
| assert.deepStrictEqual(out.messages, []); |
| assert.deepStrictEqual(out.diagnostics, []); |
| }); |
| |
| test('sandbox boundary request and decision facts are accepted without creating legacy message rows', () => { |
| const out = projectRuntimeEventsToStoredMessages( |
| [ |
| ev({ |
| id: 'sandbox-boundary-request', |
| ts: ts + 1, |
| role: 'system', |
| author: 'system', |
| actions: { |
| stateDelta: { |
| sandboxBoundaryRequest: { |
| requestId: 'boundary-1', |
| toolUseId: 'tool-1', |
| justification: 'read a file outside the workspace', |
| expansion: { |
| filesystem: { |
| entries: [{ path: '/tmp/outside.txt', access: 'read', scope: 'exact' }], |
| }, |
| }, |
| }, |
| }, |
| }, |
| refs: { toolCallId: 'tool-1' }, |
| }), |
| ev({ |
| id: 'sandbox-boundary-decision', |
| ts: ts + 2, |
| role: 'system', |
| author: 'user', |
| actions: { |
| stateDelta: { |
| sandboxBoundaryDecision: { |
| requestId: 'boundary-1', |
| decision: 'allow', |
| status: 'approved', |
| revision: 2, |
| }, |
| }, |
| }, |
| refs: { toolCallId: 'tool-1' }, |
| }), |
| ], |
| { invocations: [invocation] }, |
| ); |
| |
| assert.deepStrictEqual(out.messages, []); |
| assert.deepStrictEqual(out.diagnostics, []); |
| }); |
| |
| // Claiming the key alone would let any shape ride in under a control-fact |
| // name. Only what the Runtime mapper actually emits is a canonical fact: every field, |
| // the system/user identity, and the tool-call reference. |
| const wellFormedBoundaryRequest = () => |
| ev({ |
| id: 'sandbox-boundary-request-case', |
| ts: ts + 1, |
| role: 'system', |
| author: 'system', |
| actions: { |
| stateDelta: { |
| sandboxBoundaryRequest: { |
| requestId: 'boundary-1', |
| toolUseId: 'tool-1', |
| justification: 'read a file outside the workspace', |
| expansion: { |
| filesystem: { |
| entries: [{ path: '/tmp/outside.txt', access: 'read', scope: 'exact' }], |
| }, |
| }, |
| }, |
| }, |
| }, |
| refs: { toolCallId: 'tool-1' }, |
| }); |
| |
| const wellFormedBoundaryDecision = () => |
| ev({ |
| id: 'sandbox-boundary-decision-case', |
| ts: ts + 2, |
| role: 'system', |
| author: 'user', |
| actions: { |
| stateDelta: { |
| sandboxBoundaryDecision: { |
| requestId: 'boundary-1', |
| decision: 'allow', |
| status: 'approved', |
| revision: 2, |
| }, |
| }, |
| }, |
| refs: { toolCallId: 'tool-1' }, |
| }); |
| |
| function corruptedBoundaryEvent( |
| base: RuntimeEvent, |
| key: 'sandboxBoundaryRequest' | 'sandboxBoundaryDecision', |
| mutate: (payload: Record<string, unknown>, event: RuntimeEvent) => RuntimeEvent | void, |
| ): RuntimeEvent { |
| const clone = structuredClone(base) as RuntimeEvent; |
| const payload = clone.actions?.stateDelta?.[key] as Record<string, unknown>; |
| return mutate(payload, clone) ?? clone; |
| } |
| |
| const malformedBoundaryCases: Array<[string, () => RuntimeEvent]> = [ |
| [ |
| 'request without a justification', |
| () => |
| corruptedBoundaryEvent(wellFormedBoundaryRequest(), 'sandboxBoundaryRequest', (payload) => { |
| delete payload.justification; |
| }), |
| ], |
| [ |
| 'request with an unusable expansion', |
| () => |
| corruptedBoundaryEvent(wellFormedBoundaryRequest(), 'sandboxBoundaryRequest', (payload) => { |
| payload.expansion = {}; |
| }), |
| ], |
| [ |
| 'request that lost its tool-call reference', |
| () => |
| corruptedBoundaryEvent( |
| wellFormedBoundaryRequest(), |
| 'sandboxBoundaryRequest', |
| (_payload, event) => ({ ...event, refs: undefined }), |
| ), |
| ], |
| [ |
| 'request attributed to someone other than the system', |
| () => |
| corruptedBoundaryEvent( |
| wellFormedBoundaryRequest(), |
| 'sandboxBoundaryRequest', |
| (_payload, event) => ({ ...event, role: 'user', author: 'tool' }), |
| ), |
| ], |
| [ |
| 'decision without a status', |
| () => |
| corruptedBoundaryEvent( |
| wellFormedBoundaryDecision(), |
| 'sandboxBoundaryDecision', |
| (payload) => { |
| delete payload.status; |
| }, |
| ), |
| ], |
| [ |
| 'decision with a status the boundary never settles to', |
| () => |
| corruptedBoundaryEvent( |
| wellFormedBoundaryDecision(), |
| 'sandboxBoundaryDecision', |
| (payload) => { |
| payload.status = 'pending'; |
| }, |
| ), |
| ], |
| [ |
| 'decision without a revision', |
| () => |
| corruptedBoundaryEvent( |
| wellFormedBoundaryDecision(), |
| 'sandboxBoundaryDecision', |
| (payload) => { |
| delete payload.revision; |
| }, |
| ), |
| ], |
| [ |
| 'decision attributed to the agent instead of the user', |
| () => |
| corruptedBoundaryEvent( |
| wellFormedBoundaryDecision(), |
| 'sandboxBoundaryDecision', |
| (_payload, event) => ({ ...event, author: 'agent' }), |
| ), |
| ], |
| ]; |
| |
| for (const [name, makeEvent] of malformedBoundaryCases) { |
| // The claim stays exact: a malformed shape is never admitted as a canonical |
| // boundary fact. Its severity is a separate question, and a control fact |
| // owns no chat row, so a broken one costs a reader nothing the session view |
| // would otherwise show. |
| test(`a sandbox boundary ${name} stays unclaimed`, () => { |
| const out = projectRuntimeEventsToStoredMessages([makeEvent()], { |
| invocations: [invocation], |
| }); |
| |
| assert.deepStrictEqual(out.messages, []); |
| assert.deepStrictEqual( |
| out.diagnostics.map((diagnostic) => diagnostic.code), |
| ['unclaimed_control_fact'], |
| ); |
| }); |
| } |
| |
| test('model thinking attaches to the assistant text row that shares its step message id', () => { |
| // Real emission and backfill give a step's thinking and text the same message |
| // id (providerEventId / storedMessageId), so the projection pairs by id. |
| const out = projectRuntimeEventsToStoredMessages( |
| [ |
| ev({ |
| id: 'evt-thinking', |
| ts: ts + 5, |
| role: 'model', |
| author: 'agent', |
| content: { |
| kind: 'thinking', |
| text: 'private reasoning', |
| signature: 'sig-1', |
| providerOptions: { maka: { kimiReasoningField: 'reasoning' } }, |
| }, |
| refs: { storedMessageId: 'legacy-assistant' }, |
| }), |
| ev({ |
| id: 'evt-assistant', |
| ts: ts + 6, |
| role: 'model', |
| author: 'agent', |
| content: { kind: 'text', text: 'visible answer' }, |
| refs: { storedMessageId: 'legacy-assistant' }, |
| }), |
| ], |
| { invocations: [invocation] }, |
| ); |
| const legacy: StoredMessage[] = [ |
| { |
| type: 'assistant', |
| id: 'legacy-assistant', |
| turnId, |
| ts: ts + 6, |
| text: 'visible answer', |
| modelId: 'claude-sonnet-4-5', |
| thinking: { |
| text: 'private reasoning', |
| signature: 'sig-1', |
| providerOptions: { maka: { kimiReasoningField: 'reasoning' } }, |
| }, |
| }, |
| ]; |
| |
| assert.deepStrictEqual(out.messages, legacy); |
| assert.deepStrictEqual(out.diagnostics, []); |
| }); |
| |
| test('per-step thinking pairs each step assistant row by its own message id', () => { |
| // Two steps in one turn, each with its own signed thinking. The ledger order |
| // per step is thinking → text (finish-step flush), and each step's thinking |
| // carries its step message id, so it must attach to its own assistant row — |
| // not the last row of the turn. |
| const out = projectRuntimeEventsToStoredMessages( |
| [ |
| ev({ |
| id: 'evt-think-1', |
| ts: ts + 1, |
| role: 'model', |
| author: 'agent', |
| content: { kind: 'thinking', text: 'reasoning one', signature: 'sig-1' }, |
| refs: { providerEventId: 'step-1' }, |
| }), |
| ev({ |
| id: 'evt-text-1', |
| ts: ts + 2, |
| role: 'model', |
| author: 'agent', |
| content: { kind: 'text', text: 'answer one' }, |
| refs: { providerEventId: 'step-1' }, |
| }), |
| ev({ |
| id: 'evt-think-2', |
| ts: ts + 3, |
| role: 'model', |
| author: 'agent', |
| content: { kind: 'thinking', text: 'reasoning two', signature: 'sig-2' }, |
| refs: { providerEventId: 'step-2' }, |
| }), |
| ev({ |
| id: 'evt-text-2', |
| ts: ts + 4, |
| role: 'model', |
| author: 'agent', |
| content: { kind: 'text', text: 'answer two' }, |
| refs: { providerEventId: 'step-2' }, |
| }), |
| ], |
| { invocations: [invocation] }, |
| ); |
| |
| const assistants = out.messages.filter((message) => message.type === 'assistant'); |
| assert.deepStrictEqual(assistants, [ |
| { |
| type: 'assistant', |
| id: 'step-1', |
| turnId, |
| ts: ts + 2, |
| text: 'answer one', |
| modelId: 'claude-sonnet-4-5', |
| thinking: { text: 'reasoning one', signature: 'sig-1' }, |
| }, |
| { |
| type: 'assistant', |
| id: 'step-2', |
| turnId, |
| ts: ts + 4, |
| text: 'answer two', |
| modelId: 'claude-sonnet-4-5', |
| thinking: { text: 'reasoning two', signature: 'sig-2' }, |
| }, |
| ]); |
| assert.deepStrictEqual(out.diagnostics, []); |
| }); |
| |
| test('unsupported and incomplete events are diagnostic-only', () => { |
| const out = projectRuntimeEventsToStoredMessages( |
| [ |
| ev({ |
| id: 'evt-thinking', |
| role: 'model', |
| author: 'agent', |
| content: { kind: 'thinking', text: 'private reasoning' }, |
| }), |
| ev({ |
| id: 'evt-permission-orphan', |
| actions: { |
| permissionDecision: { |
| requestId: 'missing-request', |
| decision: 'deny', |
| }, |
| }, |
| }), |
| ev({ |
| id: 'evt-invalid-result', |
| role: 'tool', |
| author: 'tool', |
| content: { |
| kind: 'function_response', |
| id: 'tool-x', |
| name: 'Read', |
| result: 'plain string is not ToolResultContent', |
| }, |
| }), |
| ev({ id: 'evt-ended', status: 'completed', actions: { endInvocation: true } }), |
| ], |
| { invocations: [invocation] }, |
| ); |
| |
| assert.deepStrictEqual( |
| out.messages.map((message) => message.type), |
| ['turn_state'], |
| ); |
| // The orphaned permission decision carries no content, so its catch-all is |
| // the soft code — but the projector that tried to build its row and failed |
| // still reports `incomplete_event`, which stays hard. Downgrading the |
| // catch-all never downgrades a projector that attempted a message. |
| assert.deepStrictEqual( |
| out.diagnostics.map((diag) => diag.code), |
| [ |
| 'incomplete_event', |
| 'unclaimed_control_fact', |
| 'incomplete_event', |
| 'unsupported_event', |
| 'unsupported_event', |
| ], |
| ); |
| }); |
| |
| // Where the projection draws the line between a view it can still serve and |
| // one it must refuse: an unclaimed event that carries no content owns no chat |
| // row, so nothing a reader would have seen is missing. |
| test('an unclaimed control-only event is soft and leaves every message intact', () => { |
| const out = projectRuntimeEventsToStoredMessages( |
| [ |
| ev({ id: 'evt-user', role: 'user', author: 'user', content: { kind: 'text', text: 'hi' } }), |
| ev({ |
| id: 'evt-control', |
| actions: { stateDelta: { somethingTheProjectionWasNeverTaught: true } }, |
| }), |
| ev({ |
| id: 'evt-assistant', |
| role: 'model', |
| author: 'agent', |
| content: { kind: 'text', text: 'hello' }, |
| }), |
| ], |
| { invocations: [invocation] }, |
| ); |
| |
| assert.deepStrictEqual( |
| out.messages.map((message) => message.id), |
| ['evt-user', 'evt-assistant'], |
| ); |
| assert.deepStrictEqual( |
| out.diagnostics.map((diagnostic) => diagnostic.code), |
| ['unclaimed_control_fact'], |
| ); |
| assert.deepStrictEqual( |
| out.diagnostics.map((diagnostic) => diagnostic.eventId), |
| ['evt-control'], |
| ); |
| assert.strictEqual(out.diagnostics.some(isHardRuntimeEventReadModelDiagnostic), false); |
| }); |
| |
| test('an unclaimed event that carries content stays hard', () => { |
| const out = projectRuntimeEventsToStoredMessages( |
| [ |
| ev({ |
| id: 'evt-future-content', |
| role: 'model', |
| author: 'agent', |
| content: { kind: 'not_yet_projected', text: 'a reader would have seen this' } as never, |
| }), |
| ], |
| { invocations: [invocation] }, |
| ); |
| |
| assert.deepStrictEqual(out.messages, []); |
| assert.deepStrictEqual( |
| out.diagnostics.map((diagnostic) => diagnostic.code), |
| ['unsupported_event'], |
| ); |
| assert.strictEqual(out.diagnostics.every(isHardRuntimeEventReadModelDiagnostic), true); |
| }); |
| |
| test('failed terminal RuntimeEvent maps to failed turn state with the class it states', () => { |
| const out = projectRuntimeEventsToStoredMessages( |
| [ |
| ev({ |
| id: 'evt-failed', |
| ts: ts + 9, |
| status: 'failed', |
| actions: { endInvocation: true, stateDelta: { failureClass: 'tool_failed' } }, |
| }), |
| ], |
| { |
| invocations: [endedAs('failed', 'tool_failed')], |
| }, |
| ); |
| |
| assert.deepStrictEqual(out.messages, [ |
| { |
| type: 'turn_state', |
| id: 'evt-failed', |
| turnId, |
| ts: ts + 9, |
| status: 'failed', |
| parentTurnId: 'parent-turn', |
| errorClass: 'tool_failed', |
| }, |
| ]); |
| assert.deepStrictEqual(out.diagnostics, []); |
| }); |
| |
| test('failure diagnostics survive terminal projection and serialization round-trip', () => { |
| const message = 'Quota exceeded for api_key=sk-test-diagnostic-value (status=429)'; |
| const out = projectRuntimeEventsToStoredMessages( |
| [ |
| ev({ |
| id: 'provider-failed', |
| status: 'failed', |
| content: { |
| kind: 'error', |
| message, |
| retry: { decision: 'declined', because: 'side_effects' }, |
| }, |
| actions: { endInvocation: true, stateDelta: { failureClass: 'rate_limit' } }, |
| }), |
| ], |
| { invocations: [endedAs('failed', 'rate_limit')] }, |
| ); |
| const live = deriveTurnRecords(out.messages)[0]; |
| const roundTripped = deriveTurnRecords( |
| JSON.parse(JSON.stringify(out.messages)).map(decodeCanonicalMessage), |
| )[0]; |
| assert.equal(live.failureMessage, message); |
| assert.deepEqual(roundTripped, live); |
| assert.equal(live.errorClass, 'rate_limit'); |
| assert.throws(() => |
| decodeCanonicalMessage({ ...out.messages[0], failureMessage: '界'.repeat(2048) }), |
| ); |
| assert.deepEqual(live.retry, { decision: 'declined', because: 'side_effects' }); |
| }); |
| |
| test('bounds terminal diagnostics before publishing decodable turn states', () => { |
| const out = projectRuntimeEventsToStoredMessages( |
| [ |
| ev({ |
| status: 'failed', |
| content: { kind: 'error', message: '界'.repeat(2048) }, |
| actions: { endInvocation: true, stateDelta: { failureClass: 'unknown' } }, |
| }), |
| ], |
| { invocations: [endedAs('failed', 'unknown')] }, |
| ); |
| const turn = deriveTurnRecords(out.messages)[0]; |
| assert.ok(turn.failureMessage?.startsWith('界')); |
| assert.ok(Buffer.byteLength(turn.failureMessage!) <= MODEL_FAILURE_MESSAGE_MAX_BYTES); |
| assert.doesNotThrow(() => out.messages.map(decodeCanonicalMessage)); |
| }); |
| |
| test('a session written with the retired context_budget_exhausted reads back as context_overflow', () => { |
| // The runtime no longer decides locally that a request cannot be made to |
| // fit, so that outcome is gone from the live contract. Sessions persisted |
| // before still carry it, and must still decode — as the one name that |
| // survives. |
| const out = projectRuntimeEventsToStoredMessages( |
| [ |
| ev({ |
| id: 'evt-budget-exhausted', |
| ts: ts + 9, |
| status: 'failed', |
| actions: { |
| endInvocation: true, |
| stateDelta: { |
| stopReason: 'context_budget_exhausted', |
| failureClass: 'context_budget_exhausted', |
| contextBudgetExhaustedDetail: 'head_anchor_exceeds_capacity', |
| }, |
| }, |
| }), |
| ], |
| { |
| invocations: [endedAs('failed', 'context_budget_exhausted')], |
| }, |
| ); |
| |
| assert.deepStrictEqual( |
| out.messages.find((message) => message.type === 'turn_state'), |
| { |
| type: 'turn_state', |
| id: 'evt-budget-exhausted', |
| turnId, |
| ts: ts + 9, |
| status: 'failed', |
| parentTurnId: 'parent-turn', |
| errorClass: 'context_overflow', |
| }, |
| ); |
| assert.deepStrictEqual(out.diagnostics, []); |
| }); |
| |
| test('tool step cap terminal fact projects a persistent system notice', () => { |
| const out = projectRuntimeEventsToStoredMessages( |
| [ |
| ev({ |
| id: 'evt-step-limit', |
| ts: ts + 9, |
| status: 'failed', |
| actions: { |
| endInvocation: true, |
| stateDelta: { stopReason: 'step_limit', failureClass: 'tool_step_cap_reached' }, |
| }, |
| }), |
| ], |
| { |
| invocations: [endedAs('failed', 'tool_step_cap_reached')], |
| }, |
| ); |
| |
| assert.deepStrictEqual( |
| out.messages.find((message) => message.type === 'system_note'), |
| { |
| type: 'system_note', |
| id: 'evt-step-limit:step-limit-notice', |
| turnId, |
| ts: ts + 9, |
| kind: 'step_limit', |
| }, |
| ); |
| }); |
| |
| test('aborted terminal RuntimeEvent preserves abort source from runtime state', () => { |
| const out = projectRuntimeEventsToStoredMessages( |
| [ |
| ev({ |
| id: 'evt-aborted', |
| ts: ts + 9, |
| status: 'aborted', |
| actions: { endInvocation: true, stateDelta: { abortSource: 'renderer.stop_button' } }, |
| }), |
| ], |
| { |
| invocations: [endedAs('aborted')], |
| }, |
| ); |
| |
| assert.deepStrictEqual(out.messages, [ |
| { |
| type: 'turn_state', |
| id: 'evt-aborted', |
| turnId, |
| ts: ts + 9, |
| status: 'aborted', |
| parentTurnId: 'parent-turn', |
| abortedAt: ts + 9, |
| abortSource: 'renderer.stop_button', |
| }, |
| ]); |
| assert.deepStrictEqual(out.diagnostics, []); |
| }); |
| |
| // The omission is `classifyRuntimeEventTerminalFact`'s to report. Repeating it |
| // here would turn a transcript row that reads fine into an unreadable Session. |
| test('aborted terminal RuntimeEvent that states no source still projects its turn state', () => { |
| const out = projectRuntimeEventsToStoredMessages( |
| [ |
| ev({ |
| id: 'evt-aborted', |
| ts: ts + 9, |
| status: 'aborted', |
| actions: { endInvocation: true }, |
| }), |
| ], |
| { |
| invocations: [endedAs('aborted')], |
| }, |
| ); |
| |
| assert.partialDeepStrictEqual(out.messages[0], { |
| type: 'turn_state', |
| status: 'aborted', |
| abortedAt: ts + 9, |
| }); |
| assert.deepStrictEqual(out.diagnostics, []); |
| }); |
| |
| test('projects tool_call stepId from refs so the UI timeline keeps step pairing', () => { |
| const stepCall = (id: string, stepId?: string) => |
| ev({ |
| id: `evt-${id}`, |
| role: 'model' as const, |
| author: 'agent' as const, |
| content: { |
| kind: 'function_call' as const, |
| id, |
| name: 'Read', |
| args: { path: '/tmp/a.txt' }, |
| }, |
| refs: { toolCallId: id, ...(stepId ? { stepId } : {}) }, |
| }); |
| |
| const withStep = projectRuntimeEventsToStoredMessages([stepCall('tool-step', 'step-1')], { |
| invocations: [invocation], |
| }); |
| assert.partialDeepStrictEqual(withStep.messages[0], { |
| type: 'tool_call', |
| id: 'tool-step', |
| stepId: 'step-1', |
| }); |
| |
| // Legacy events without refs.stepId must not grow a stepId key: the UI |
| // uses its absence to pick the backward-compatible tools-first ordering. |
| const withoutStep = projectRuntimeEventsToStoredMessages([stepCall('tool-legacy')], { |
| invocations: [invocation], |
| }); |
| const legacyCall = withoutStep.messages[0]; |
| assert.partialDeepStrictEqual(legacyCall, { type: 'tool_call', id: 'tool-legacy' }); |
| assert.strictEqual(legacyCall && 'stepId' in legacyCall, false); |
| }); |
| |
| test('retains nested CodeMode identity on tool rows used by the UI read model', () => { |
| const identity = { |
| origin: 'code_mode' as const, |
| modelVisibility: 'hidden' as const, |
| refs: { |
| toolCallId: 'nested-1', |
| parentToolCallId: 'exec-1', |
| parentOperationId: 'exec-op-1', |
| }, |
| }; |
| const out = projectRuntimeEventsToStoredMessages( |
| [ |
| ev({ |
| ...identity, |
| role: 'model', |
| author: 'agent', |
| content: { kind: 'function_call', id: 'nested-1', name: 'Read', args: {} }, |
| }), |
| ev({ |
| ...identity, |
| role: 'tool', |
| author: 'tool', |
| content: { |
| kind: 'function_response', |
| id: 'nested-1', |
| name: 'Read', |
| result: { kind: 'text', text: 'ok' }, |
| }, |
| }), |
| ], |
| { invocations: [invocation] }, |
| ); |
| |
| assert.strictEqual(out.messages.length, 2); |
| for (const message of out.messages) { |
| assert.partialDeepStrictEqual(message, { |
| origin: 'code_mode', |
| modelVisibility: 'hidden', |
| parentToolCallId: 'exec-1', |
| parentOperationId: 'exec-op-1', |
| }); |
| } |
| }); |
| |
| test('projects tool_call activityKind from runtime state for replay', () => { |
| const out = projectRuntimeEventsToStoredMessages( |
| [ |
| ev({ |
| id: 'evt-tool-kind', |
| role: 'model', |
| author: 'agent', |
| content: { |
| kind: 'function_call', |
| id: 'tool-kind', |
| name: 'CustomCommand', |
| args: {}, |
| }, |
| actions: { stateDelta: { activityKind: 'command' } }, |
| refs: { toolCallId: 'tool-kind' }, |
| }), |
| ], |
| { invocations: [invocation] }, |
| ); |
| |
| assert.partialDeepStrictEqual(out.messages[0], { |
| type: 'tool_call', |
| id: 'tool-kind', |
| activityKind: 'command', |
| }); |
| }); |
| }); |
| |
| /** |
| * One reachable event per `RuntimeEventActions` field, each entry typed to its |
| * own key so it cannot drift onto another field or be filled with a placeholder. |
| * |
| * This is the premise the read model's soft path rests on. An unclaimed |
| * content-free event degrades the view instead of withholding it, which is only |
| * safe while every action a reader can meet is claimed — several of them |
| * (`permissionDecision`, `tokenUsage`, the terminal fact) do produce rows, and |
| * `runtime-event-backfill.ts` already writes a content-free event that becomes a |
| * visible `permission_decision`. The SessionEvent mapper contract |
| * only covers events built by `mapSessionEventToRuntimeEvent`; tool-runtime, |
| * terminal-run-commit and the backfill write RuntimeEvents directly. Keying this |
| * table on the action surface itself covers those paths too. |
| */ |
| type ActionCoverageSamples = { |
| [K in keyof Required<RuntimeEventActions>]: { |
| /** The action value under test, typed to its own key. */ |
| action: Required<RuntimeEventActions>[K]; |
| /** The rest of the event, as the field's real emitter writes it. */ |
| event?: Partial<RuntimeEvent>; |
| }; |
| }; |
| |
| const ACTION_COVERAGE_SAMPLES: ActionCoverageSamples = { |
| coordination: { |
| action: { |
| actionId: 'clarify', |
| userText: 'Which task?', |
| result: { disposition: 'clarify', coordinationTurnId: 'turn-1' }, |
| clarification: 'Please name a task.', |
| }, |
| }, |
| handoffPause: { |
| action: { |
| protocol: 'runtime_handoff_pause_v1', |
| handoffId: 'handoff', |
| hostEpoch: 'host', |
| remainingSteps: null, |
| rootRunId: 'root', |
| successorRunId: 'next', |
| successorInvocationId: 'next', |
| claimId: 'claim', |
| }, |
| }, |
| // `stateDelta` is an open record, so only named shapes are claimed and this |
| // entry covers the field, not its contents. A new key inside a state delta is |
| // out of reach of any contract keyed on the action surface. |
| stateDelta: { action: { continuationStart: true } }, |
| managedMutationTerminal: { |
| action: { |
| protocol: 'managed_mutation_terminal_v1', |
| operationId: 'coverage-operation', |
| dispatchEventId: 'coverage-dispatch', |
| workspaceInstanceId: 'instance_44444444444444444444444444444444', |
| terminalKind: 'no_workspace_change', |
| }, |
| }, |
| continuationStart: { |
| action: { |
| protocol: 'continuation_start_v2', |
| provenance: 'runtime_admission', |
| claimId: 'coverage-claim', |
| boundaryDigest: 'sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', |
| immediateSource: { |
| sessionId: 'coverage-session', |
| invocationId: 'coverage-invocation', |
| runId: 'coverage-run', |
| turnId: 'coverage-turn', |
| highWater: 1, |
| prefixDigest: 'sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', |
| }, |
| replayManifestDigest: |
| 'sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc', |
| providerProjectionVersion: 1, |
| providerReplayDigest: |
| 'sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd', |
| }, |
| }, |
| artifactDelta: { action: { 'artifact-1': 42 } }, |
| permissionRequest: { |
| action: { |
| kind: 'tool_permission', |
| requestId: 'coverage-request', |
| toolUseId: 'coverage-tool', |
| toolName: 'Read', |
| category: 'read', |
| reason: 'custom', |
| args: { path: '/tmp/a.txt' }, |
| rememberForTurnAllowed: true, |
| }, |
| event: { refs: { toolCallId: 'coverage-tool' } }, |
| }, |
| permissionDecision: { |
| action: { requestId: 'coverage-request', decision: 'allow', toolName: 'Read' }, |
| event: { refs: { toolCallId: 'coverage-tool' } }, |
| }, |
| // The canonical outcome lives in InteractionStore, so a standalone acceptance |
| // reports an `incomplete_event`. That is a completeness diagnostic, not a |
| // coverage gap: the projection still claims the field. |
| permissionAnswerAccepted: { |
| action: { requestId: 'coverage-request' }, |
| event: { author: 'user', refs: { toolCallId: 'coverage-tool' } }, |
| }, |
| permissionClosureAccepted: { |
| action: { requestId: 'coverage-request', reason: 'timed_out' }, |
| }, |
| userQuestionRequest: { |
| action: { |
| requestId: 'coverage-question', |
| toolUseId: 'coverage-question-tool', |
| questions: [{ question: 'Choose', options: [{ label: 'Extend' }] }], |
| }, |
| event: { refs: { toolCallId: 'coverage-question-tool' } }, |
| }, |
| userQuestionAnswerAccepted: { |
| action: { requestId: 'coverage-question' }, |
| event: { author: 'user', refs: { toolCallId: 'coverage-question-tool' } }, |
| }, |
| formRequest: { |
| action: { |
| requestId: 'coverage-form', |
| toolUseId: 'coverage-form-tool', |
| message: 'Choose settings', |
| requester: { name: 'deploy' }, |
| fields: [{ kind: 'boolean', name: 'confirm', label: 'Confirm', required: true }], |
| }, |
| event: { refs: { toolCallId: 'coverage-form-tool' } }, |
| }, |
| formAnswerAccepted: { |
| action: { requestId: 'coverage-form' }, |
| event: { author: 'user', refs: { toolCallId: 'coverage-form-tool' } }, |
| }, |
| transferToAgent: { action: 'agent-b' }, |
| // The terminal fact is one of the actions that does own a row, and the event |
| // states the outcome it ends on. |
| endInvocation: { action: true, event: { status: 'completed' } }, |
| tokenUsage: { action: { input: 10, output: 5 } }, |
| toolDispatch: { |
| action: { |
| protocol: 't1_after_preflight_v1', |
| operationId: 'coverage-op', |
| providerToolCallId: 'coverage-tool', |
| toolName: 'Bash', |
| canonicalArgsHash: 'sha256:args', |
| recoveryMode: 'reconcile', |
| }, |
| event: { refs: { toolCallId: 'coverage-tool', operationId: 'coverage-op' } }, |
| }, |
| toolRecovery: { |
| action: { |
| kind: 'maka.tool.reconcile_result', |
| version: 1, |
| payload: { |
| protocol: 'tool_reconcile_v1', |
| operationId: 'coverage-op', |
| observation: 'unreadable', |
| observationSchema: 'state_identity_v1', |
| observationDigest: |
| 'sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', |
| }, |
| }, |
| event: { refs: { toolCallId: 'coverage-tool', operationId: 'coverage-op' } }, |
| }, |
| workspaceFact: { |
| action: { |
| kind: 'maka.workspace.epoch_opened', |
| version: 1, |
| payload: { |
| protocol: 'workspace_epoch_opened_v1', |
| repositoryId: `repository_${'1'.repeat(32)}`, |
| workspaceId: `workspace_${'2'.repeat(32)}`, |
| workspaceEpochId: `epoch_${'3'.repeat(32)}`, |
| workspaceInstanceId: `instance_${'4'.repeat(32)}`, |
| initialWorkspaceVersionId: `version_${'5'.repeat(32)}`, |
| mode: 'managed_worktree', |
| objectFormat: 'sha1', |
| sourceCommitOid: '1'.repeat(40), |
| sourceTreeOid: '2'.repeat(40), |
| materializationProfileDigest: `sha256:${'3'.repeat(64)}`, |
| materializationSemantics: 'git_tree_materialized_with_fixed_config_v1', |
| policyHash: `sha256:${'4'.repeat(64)}`, |
| }, |
| }, |
| }, |
| runtimeProtocol: { action: { toolBoundary: 't1_after_preflight_v1' } }, |
| }; |
| |
| describe('system note projection', () => { |
| test('projects a turn-scoped note back into its transcript row', () => { |
| const out = projectRuntimeEventsToStoredMessages( |
| [ |
| ev({ |
| id: 'evt-note', |
| content: { |
| kind: 'system_note', |
| note: 'context_compacted', |
| data: { removedMessages: 12 }, |
| }, |
| modelVisibility: 'hidden', |
| refs: { storedMessageId: 'legacy-note' }, |
| }), |
| ], |
| { invocations: [invocation] }, |
| ); |
| |
| assert.deepStrictEqual(out.diagnostics, []); |
| assert.deepStrictEqual(out.messages, [ |
| { |
| type: 'system_note', |
| id: 'legacy-note', |
| turnId, |
| ts, |
| kind: 'context_compacted', |
| data: { removedMessages: 12 }, |
| }, |
| ]); |
| }); |
| |
| test('converts a legacy turn-scoped note and reads back the same row', () => { |
| const note: StoredMessage = { |
| type: 'system_note', |
| id: 'legacy-step-limit', |
| turnId, |
| ts, |
| kind: 'step_limit', |
| data: { steps: 40 }, |
| }; |
| |
| const backfilled = backfillRuntimeEventsFromStoredMessages({ |
| run: { sessionId, invocationId, runId, turnId }, |
| outcome: { status: 'completed', ts }, |
| messages: [note], |
| modelHistory: 'full', |
| now: () => ts, |
| }); |
| |
| assert.deepStrictEqual(backfilled.diagnostics, []); |
| const projected = projectRuntimeEventsToStoredMessages(backfilled.events, { |
| invocations: [invocation], |
| }); |
| assert.deepStrictEqual( |
| projected.messages.filter((message) => message.type === 'system_note'), |
| [note], |
| ); |
| }); |
| |
| test('leaves a session-level note out of the run ledger', () => { |
| const backfilled = backfillRuntimeEventsFromStoredMessages({ |
| run: { sessionId, invocationId, runId, turnId }, |
| messages: [ |
| { |
| type: 'system_note', |
| id: 'legacy-mode-change', |
| turnId, |
| ts, |
| kind: 'mode_change', |
| data: { from: 'ask', to: 'bypass' }, |
| }, |
| ], |
| modelHistory: 'full', |
| now: () => ts, |
| }); |
| |
| assert.deepStrictEqual( |
| backfilled.events.filter((event) => event.content?.kind === 'system_note'), |
| [], |
| ); |
| assert.partialDeepStrictEqual(backfilled.diagnostics, [{ code: 'skipped_high_risk_message' }]); |
| }); |
| }); |
| |
| describe('legacy transcript conversion keeps every row', () => { |
| const convert = (messages: readonly StoredMessage[]) => |
| backfillRuntimeEventsFromStoredMessages({ |
| run: { sessionId, invocationId, runId, turnId }, |
| outcome: { status: 'completed', ts }, |
| messages, |
| modelHistory: 'full', |
| now: () => ts, |
| }); |
| |
| test('keeps a tool result whose call is not in the turn, out of model replay', () => { |
| const orphan: StoredMessage = { |
| type: 'tool_result', |
| id: 'legacy-orphan-result', |
| turnId, |
| ts, |
| toolUseId: 'tool-gone', |
| isError: false, |
| content: { kind: 'text', text: 'done' }, |
| }; |
| |
| const converted = convert([orphan]); |
| const response = converted.events.find((event) => event.content?.kind === 'function_response'); |
| assert.strictEqual(response?.modelVisibility, 'hidden'); |
| assert.strictEqual(runtimeEventHasModelVisibleContent(response as RuntimeEvent), false); |
| |
| const projected = projectRuntimeEventsToStoredMessages(converted.events, { |
| invocations: [invocation], |
| }); |
| assert.partialDeepStrictEqual( |
| projected.messages.filter((message) => message.type === 'tool_result'), |
| [{ id: 'legacy-orphan-result', toolUseId: 'tool-gone' }], |
| ); |
| }); |
| |
| test('keeps a provider-native call whose opaque output was not retained', () => { |
| const converted = convert([ |
| { |
| type: 'tool_call', |
| id: 'tool-native', |
| turnId, |
| ts, |
| toolName: 'WebSearch', |
| args: { query: 'maka' }, |
| providerExecuted: true, |
| }, |
| ]); |
| |
| const call = converted.events.find((event) => event.content?.kind === 'function_call'); |
| assert.strictEqual(call?.modelVisibility, 'hidden'); |
| assert.partialDeepStrictEqual(converted.diagnostics, [ |
| { code: 'skipped_provider_native_replay_gap' }, |
| ]); |
| |
| const projected = projectRuntimeEventsToStoredMessages(converted.events, { |
| invocations: [invocation], |
| }); |
| assert.partialDeepStrictEqual( |
| projected.messages.filter((message) => message.type === 'tool_call'), |
| [{ id: 'tool-native', toolName: 'WebSearch' }], |
| ); |
| }); |
| |
| test('converts a permission decision on its own evidence', () => { |
| const decision: StoredMessage = { |
| type: 'permission_decision', |
| id: 'request-1', |
| turnId, |
| ts, |
| toolUseId: 'tool-1', |
| toolName: 'Bash', |
| decision: 'allow', |
| hint: 'rm -rf build', |
| }; |
| |
| const converted = convert([decision]); |
| assert.deepStrictEqual(converted.diagnostics, []); |
| |
| const projected = projectRuntimeEventsToStoredMessages(converted.events, { |
| invocations: [invocation], |
| }); |
| assert.deepStrictEqual( |
| projected.messages.filter((message) => message.type === 'permission_decision'), |
| [decision], |
| ); |
| }); |
| |
| test('ends a turn whose transcript never said how it ended', () => { |
| const converted = backfillRuntimeEventsFromStoredMessages({ |
| run: { sessionId, invocationId, runId, turnId }, |
| messages: [{ type: 'user', id: 'legacy-user', turnId, ts, text: 'hello' }], |
| modelHistory: 'full', |
| now: () => ts, |
| }); |
| |
| const terminal = converted.events.filter((event) => event.actions?.endInvocation); |
| assert.partialDeepStrictEqual(terminal, [{ status: 'failed' }]); |
| assert.strictEqual(terminal[0]?.actions?.stateDelta?.failureClass, 'missing_terminal_event'); |
| assert.partialDeepStrictEqual(converted.diagnostics, [{ code: 'synthesized_terminal_event' }]); |
| }); |
| }); |
| |
| describe('RuntimeEventActions projection coverage', () => { |
| for (const [field, sample] of Object.entries(ACTION_COVERAGE_SAMPLES)) { |
| test(`actions.${field} projects without an unclaimed-event diagnostic`, () => { |
| const actions = { [field]: sample.action } as RuntimeEventActions; |
| // Guards an entry that names a field but leaves it absent at runtime. |
| assert.strictEqual(field in actions, true); |
| const out = projectRuntimeEventsToStoredMessages([ev({ ...sample.event, actions })], { |
| invocations: [invocation], |
| }); |
| |
| assert.deepStrictEqual(out.diagnostics.filter(isUnclaimedRuntimeEventDiagnostic), []); |
| }); |
| } |
| }); |
| |
| describe('token usage projection', () => { |
| test('carries the cross-turn request anchor both ways', () => { |
| const lastRequestAnchor = { inputTokens: 120, outputTokens: 30 }; |
| const anchored = ev({ |
| id: 'evt-token-anchor', |
| role: 'system', |
| author: 'system', |
| actions: { tokenUsage: { input: 370, output: 60, lastRequestAnchor } }, |
| }); |
| const projected = projectRuntimeEventsToStoredMessages( |
| [ |
| ev({ |
| id: 'evt-anchor-user', |
| role: 'user', |
| author: 'user', |
| content: { kind: 'text', text: 'read the file' }, |
| }), |
| anchored, |
| ], |
| { invocations: [invocation] }, |
| ); |
| const usage = projected.messages.find((message) => message.type === 'token_usage'); |
| assert.partialDeepStrictEqual(usage, { type: 'token_usage', input: 370, lastRequestAnchor }); |
| |
| const backfilled = backfillRuntimeEventsFromStoredMessages({ |
| run: { sessionId, invocationId, runId, turnId }, |
| messages: projected.messages, |
| now: () => ts, |
| }); |
| assert.deepStrictEqual( |
| backfilled.events.find((event) => event.actions?.tokenUsage)?.actions?.tokenUsage |
| ?.lastRequestAnchor, |
| lastRequestAnchor, |
| ); |
| }); |
| }); |
| |
| describe('SessionManager read behavior', () => { |
| test('getMessages requires RuntimeReadModel stores instead of reading SessionStore messages directly', async () => { |
| const messages: StoredMessage[] = equivalentLegacyMessages(); |
| const store = new ReadOnlyStore(messages); |
| const manager = new SessionManager({ |
| store, |
| backends: new BackendRegistry(), |
| newId: () => 'id', |
| now: () => ts, |
| }); |
| |
| await expectRejects( |
| manager.getMessages(sessionId), |
| /RuntimeReadModel requires AgentRunStore and RuntimeEventStore/, |
| ); |
| assert.strictEqual(store.readMessagesCalls, 0); |
| }); |
| }); |
| |
| class ReadOnlyStore implements SessionStore { |
| readMessagesCalls = 0; |
| |
| constructor(private readonly messages: StoredMessage[]) {} |
| |
| async createSubagent( |
| _input: CreateSessionInput, |
| ): Promise<{ header: SessionHeader; created: boolean }> { |
| throw new Error('not implemented'); |
| } |
| |
| async create(_input: CreateSessionInput): Promise<SessionHeader> { |
| throw new Error('not implemented'); |
| } |
| |
| async setExecutionBoundaryKind(): Promise<never> { |
| throw new Error('not implemented'); |
| } |
| |
| async readExecutionBoundary(): Promise<never> { |
| throw new Error('not implemented'); |
| } |
| |
| async list(_filter?: SessionListFilter): Promise<SessionSummary[]> { |
| return []; |
| } |
| |
| async readHeader(id: string): Promise<SessionHeader> { |
| return makeHeader(id); |
| } |
| |
| async readMessages(_sessionId: string): Promise<StoredMessage[]> { |
| this.readMessagesCalls += 1; |
| return [...this.messages]; |
| } |
| |
| async readMessagesAfter( |
| _sessionId: string, |
| request: { afterSequence?: number; maxMessages: number }, |
| ): Promise<{ |
| records: readonly { sequence: number; message: StoredMessage }[]; |
| highWaterSequence: number | null; |
| }> { |
| this.readMessagesCalls += 1; |
| return { |
| records: this.messages |
| .map((message, sequence) => ({ sequence, message })) |
| .filter(({ sequence }) => sequence > (request.afterSequence ?? -1)) |
| .slice(0, request.maxMessages), |
| highWaterSequence: this.messages.length > 0 ? this.messages.length - 1 : null, |
| }; |
| } |
| |
| async listTurns(_sessionId: string): Promise<TurnRecord[]> { |
| return deriveTurnRecords(this.messages); |
| } |
| |
| async appendMessage(_sessionId: string, _m: StoredMessage): Promise<void> { |
| throw new Error('not implemented'); |
| } |
| |
| async appendMessages(_sessionId: string, _ms: StoredMessage[]): Promise<void> { |
| throw new Error('not implemented'); |
| } |
| |
| async updateHeader(id: string, patch: Partial<SessionHeader>): Promise<SessionHeader> { |
| return { ...makeHeader(id), ...patch }; |
| } |
| |
| async setFlagged(_sessionId: string, _isFlagged: boolean): Promise<void> {} |
| async rename(_sessionId: string, _name: string): Promise<void> {} |
| async remove(_sessionId: string): Promise<void> {} |
| } |
| |
| async function expectRejects(promise: Promise<unknown>, pattern: RegExp): Promise<void> { |
| try { |
| await promise; |
| } catch (error) { |
| assert.match(String(error instanceof Error ? error.message : String(error)), pattern); |
| return; |
| } |
| throw new Error(`Expected promise to reject with ${pattern}`); |
| } |
| |
| function archivedStatus(message: StoredMessage | undefined): string | undefined { |
| if (message?.type !== 'tool_result') return undefined; |
| return message.content.kind === 'archived_tool_result' ? message.content.status : undefined; |
| } |
| |
| function makeHeader(id: string): SessionHeader { |
| return { |
| id, |
| workspaceRoot: '/tmp/work', |
| cwd: '/tmp/work', |
| createdAt: ts, |
| name: 'Session', |
| titleIsManual: true, |
| isFlagged: false, |
| labels: [], |
| isArchived: false, |
| status: 'active', |
| hasUnread: false, |
| backend: 'fake', |
| llmConnectionSlug: 'fake', |
| connectionLocked: false, |
| model: 'fake-model', |
| permissionMode: 'ask', |
| schemaVersion: 1, |
| }; |
| } |
| |
| test('Coordination receipts materialize as host facts, never assistant output', () => { |
| const receipt = { |
| actionId: 'clarify', |
| userText: 'Which task?', |
| clarification: 'Please name a task.', |
| result: { disposition: 'clarify' as const, coordinationTurnId: turnId }, |
| }; |
| const out = projectRuntimeEventsToStoredMessages( |
| [ |
| ev({ |
| id: 'coordination', |
| author: 'host', |
| modelVisibility: 'hidden', |
| actions: { coordination: receipt }, |
| }), |
| ], |
| { invocations: [invocation] }, |
| ); |
| assert.ok( |
| out.messages.some( |
| (message) => message.type === 'workhub_coordination' && message.kind === 'action_receipt', |
| ), |
| ); |
| assert.equal( |
| out.messages.some((message) => message.type === 'assistant'), |
| false, |
| ); |
| }); |