blob: c6d82084d389c52c361b43f9fa5bddc0790ab31e [file]
import assert from 'node:assert/strict';
import { describe, test } from 'node:test';
import { decodeStoredMessageForRecovery, type BackendKind } from '@maka/core';
import type { AgentRunHeader } from '@maka/core';
import type { SandboxBoundaryResponse } from '@maka/core';
import type { SessionEvent } from '@maka/core';
import type { BackendSendInput, BackendSessionEvent } from '@maka/core';
import type { RuntimeEvent } from '@maka/core';
import { decodeRuntimeEvent, isTerminalRuntimeEvent, isPartialRuntimeEvent } from '@maka/core';
import {
AiSdkFlow,
mapCompleteStopReason,
mapSessionEventToRuntimeEvent,
createSessionEventMapMemory,
} from '../ai-sdk-flow.js';
import { flowSupportsControl } from '../agent-flow.js';
import type { AgentBackend } from '@maka/core';
import { RuntimeRunner } from '../runtime-runner.js';
import type { InvocationContext } from '../invocation-context.js';
import {
isUnclaimedRuntimeEventDiagnostic,
projectRuntimeEventsToStoredMessages,
} from '../runtime-event-read-model.js';
import { isNonTerminalErrorRuntimeEvent } from '../agent-run.js';
import { backfillRuntimeEventsFromStoredMessages } from '../runtime-event-backfill.js';
// ============================================================================
// Fake backend — scripted SessionEvent stream + recorded control calls
// ============================================================================
interface ScriptedBackendCtor {
kind?: BackendKind;
sessionId?: string;
events: SessionEvent[];
/** Optional gate: send() awaits this after yielding each event. */
gate?: () => Promise<void>;
stopFailure?: Error;
}
class ScriptedBackend implements AgentBackend {
readonly kind: BackendKind;
readonly sessionId: string;
readonly stopCalls: Array<'user_stop' | 'redirect'> = [];
readonly permissionCalls: SandboxBoundaryResponse[] = [];
readonly sendInputs: BackendSendInput[] = [];
disposeCalls = 0;
sendCalls = 0;
yieldedEvents = 0;
private readonly events: SessionEvent[];
private readonly gate?: () => Promise<void>;
private readonly stopFailure?: Error;
constructor(c: ScriptedBackendCtor) {
this.kind = c.kind ?? 'ai-sdk';
this.sessionId = c.sessionId ?? 'session-1';
this.events = c.events;
this.gate = c.gate;
this.stopFailure = c.stopFailure;
}
async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
this.sendCalls += 1;
this.sendInputs.push(input);
for (const e of this.events) {
this.yieldedEvents += 1;
yield e;
if (this.gate) await this.gate();
}
}
async stop(reason: 'user_stop' | 'redirect'): Promise<void> {
this.stopCalls.push(reason);
if (this.stopFailure) throw this.stopFailure;
}
async respondToSandboxBoundary(decision: SandboxBoundaryResponse): Promise<void> {
this.permissionCalls.push(decision);
}
async dispose(): Promise<void> {
this.disposeCalls += 1;
}
}
// ============================================================================
// Event builders
// ============================================================================
let __seq = 0;
type DistributiveOmit<T, K extends keyof any> = T extends any ? Omit<T, K> : never;
function ev(
e: DistributiveOmit<SessionEvent, 'id' | 'turnId' | 'ts'> & Partial<Pick<SessionEvent, 'ts'>>,
): SessionEvent {
__seq += 1;
return { id: `evt-${__seq}`, turnId: 'turn-1', ts: e.ts ?? __seq, ...e } as SessionEvent;
}
const ctx = {
sessionId: 'session-1',
invocationId: 'inv-1',
runId: 'run-1',
turnId: 'turn-1',
source: 'test',
startedAt: 999,
request: {
sessionId: 'session-1',
invocationId: 'inv-1',
runId: 'run-1',
turnId: 'turn-1',
text: 'hi',
source: 'test',
},
newId: () => 'rt-id',
now: () => 1000,
} satisfies InvocationContext;
function collect(stream: AsyncIterable<RuntimeEvent>): Promise<RuntimeEvent[]> {
const out: RuntimeEvent[] = [];
return (async () => {
for await (const e of stream) out.push(e);
return out;
})();
}
// ============================================================================
// Tests
// ============================================================================
describe('AiSdkFlow seam', () => {
test('maps the original steering content digest into the durable Runtime event', () => {
const digest = `sha256:${'a'.repeat(64)}` as const;
const runtimeEvent = mapSessionEventToRuntimeEvent(
{
id: 'steering-event',
turnId: 'turn-1',
ts: 1,
type: 'steering_message',
messageId: 'steering-message',
content: { text: '<invoked-skill>Prepared</invoked-skill>' },
submittedContentDigest: digest,
},
ctx,
);
assert.equal(runtimeEvent.refs?.sourceMessageDigest, digest);
});
test('implements AgentFlow + AgentFlowControl and reflects the wrapped backend', () => {
const backend = new ScriptedBackend({ events: [] });
const flow = new AiSdkFlow({ backend });
assert.equal(flow.kind, 'ai-sdk');
assert.equal(flow.sessionId, 'session-1');
assert.equal(typeof flow.run, 'function');
assert.equal(flowSupportsControl(flow), true);
assert.equal(flow.backendRef, backend);
// Structural: an AiSdkFlow is assignable to the AgentFlow contract.
const _asFlow: import('../agent-flow.js').AgentFlow = flow;
void _asFlow;
});
test('single-flights a pending backend stop and retries after it settles', async () => {
const stopFailure = new Error('backend stop failed');
const backend = new ScriptedBackend({ events: [], stopFailure });
const flow = new AiSdkFlow({ backend });
const results = await Promise.allSettled([flow.stop('user_stop'), flow.stop('redirect')]);
assert.deepEqual(
results.map((result) => result.status),
['rejected', 'rejected'],
);
assert.equal(results[0]?.status === 'rejected' && results[0].reason, stopFailure);
assert.equal(results[1]?.status === 'rejected' && results[1].reason, stopFailure);
assert.deepEqual(backend.stopCalls, ['user_stop']);
await assert.rejects(flow.stop('redirect'), stopFailure);
assert.deepEqual(backend.stopCalls, ['user_stop', 'redirect']);
});
test('maps a normal turn preserving event order and terminal guarantee', async () => {
const backend = new ScriptedBackend({
events: [
ev({ type: 'text_delta', messageId: 'm1', text: 'Hel' }),
ev({ type: 'text_delta', messageId: 'm1', text: 'lo' }),
ev({ type: 'text_complete', messageId: 'm1', text: 'Hello' }),
ev({
type: 'token_usage',
input: 10,
output: 5,
costUsd: 0.001,
systemPromptHash: 'sys-hash',
providerRequestTraceId: 'provider-trace-1',
}),
ev({ type: 'complete', stopReason: 'end_turn' }),
],
});
const flow = new AiSdkFlow({ backend });
const out = await collect(flow.run(ctx, { text: 'hi', context: [] }));
assert.equal(out.length, 5);
// Order preserved.
assert.deepEqual(
out.map((e) => e.content?.kind ?? null),
['text', 'text', 'text', null, null],
);
// Deltas are partial; complete is not.
assert.equal(isPartialRuntimeEvent(out[0]), true);
assert.equal(isPartialRuntimeEvent(out[2]), false);
// Identity spine propagated.
assert.equal(out[0].invocationId, 'inv-1');
assert.equal(out[0].runId, 'run-1');
assert.equal(out[0].sessionId, 'session-1');
assert.equal(out[0].turnId, 'turn-1');
// id reused from source for 1:1 dedup linkage.
assert.equal(out[0].id, 'evt-1');
// Token usage carried as an action.
assert.deepEqual(out[3].actions?.tokenUsage, {
input: 10,
output: 5,
costUsd: 0.001,
systemPromptHash: 'sys-hash',
});
assert.deepEqual(out[3].refs, { providerRequestTraceId: 'provider-trace-1' });
// Stream closes with a terminal event.
assert.equal(isTerminalRuntimeEvent(out[out.length - 1]), true);
assert.equal(out[out.length - 1].status, 'completed');
assert.equal(out[out.length - 1].actions?.endInvocation, true);
// send was invoked exactly once with the turn id.
assert.equal(backend.sendCalls, 1);
});
test('RuntimeRunner dispatches AiSdkFlow with defined context and preserved attachments', async () => {
const attachment = {
kind: 'image' as const,
name: 'chart.png',
mimeType: 'image/png',
bytes: 123,
ref: {
kind: 'session_file' as const,
sessionId: 'session-1',
relativePath: 'attachments/chart.png',
},
};
const history = [
{
type: 'user' as const,
id: 'u-prev',
turnId: 'turn-prev',
ts: 1,
text: 'previous',
},
];
const runtimeContext: RuntimeEvent[] = [
{
id: 'rt-prev',
invocationId: 'inv-prev',
runId: 'run-prev',
sessionId: 'session-1',
turnId: 'turn-prev',
ts: 1,
partial: false,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'previous' },
},
];
const backend = new ScriptedBackend({
events: [
ev({ type: 'text_complete', messageId: 'm1', text: 'ok' }),
ev({ type: 'complete', stopReason: 'end_turn' }),
],
});
const flow = new AiSdkFlow({ backend });
let idSeq = 0;
const runner = new RuntimeRunner({
flow,
providers: {
newId: () => `rt-${(idSeq += 1)}`,
now: () => 1000,
},
});
const result = await runner.run({
sessionId: 'session-1',
turnId: 'turn-1',
text: 'hi',
attachments: [attachment],
context: history,
runtimeContext,
source: 'test',
});
assert.equal(result.status, 'completed');
assert.equal(result.finalOutput, 'ok');
assert.equal(backend.sendInputs.length, 1);
assert.deepEqual(backend.sendInputs[0], {
invocationId: 'rt-1',
runId: 'rt-2',
turnId: 'turn-1',
text: 'hi',
attachments: [attachment],
context: history,
runtimeContext,
});
});
test('maps thinking deltas/signature onto model thinking content', async () => {
const backend = new ScriptedBackend({
events: [
ev({ type: 'thinking_delta', messageId: 'm1', text: 'hm' }),
ev({ type: 'thinking_complete', messageId: 'm1', text: 'hmm', signature: 'sig' }),
ev({ type: 'complete', stopReason: 'end_turn' }),
],
});
const flow = new AiSdkFlow({ backend });
const out = await collect(flow.run(ctx, { text: 'hi', context: [] }));
assert.equal(isPartialRuntimeEvent(out[0]), true);
assert.equal(out[1].content?.kind, 'thinking');
assert.equal((out[1].content as { signature?: string }).signature, 'sig');
assert.equal(isPartialRuntimeEvent(out[1]), false);
});
test('preserves toolName linkage between tool_start and tool_result', async () => {
const backend = new ScriptedBackend({
events: [
ev({ type: 'tool_start', toolUseId: 'tu-1', toolName: 'read', args: { path: '/a' } }),
ev({
type: 'tool_result',
toolUseId: 'tu-1',
isError: false,
content: { kind: 'text', text: 'body' },
durationMs: 42,
}),
ev({ type: 'complete', stopReason: 'end_turn' }),
],
});
const flow = new AiSdkFlow({ backend });
const out = await collect(flow.run(ctx, { text: 'read it', context: [] }));
// tool_start -> function_call
const call = out[0];
assert.equal(call.role, 'model');
assert.equal(call.author, 'agent');
assert.equal(call.content?.kind, 'function_call');
const fnCall = call.content as { id: string; name: string; args: unknown };
assert.equal(fnCall.name, 'read');
assert.equal(fnCall.id, 'tu-1');
assert.equal(call.refs?.toolCallId, 'tu-1');
// tool_result -> function_response with the remembered name
const result = out[1];
assert.equal(result.role, 'tool');
assert.equal(result.author, 'tool');
assert.equal(result.content?.kind, 'function_response');
const fnResp = result.content as {
id: string;
name: string;
result: unknown;
isError?: boolean;
};
assert.equal(fnResp.name, 'read', 'tool_result recovers toolName from the prior tool_start');
assert.equal(fnResp.isError, undefined);
assert.equal(result.refs?.toolCallId, 'tu-1');
assert.deepEqual(result.actions?.stateDelta, { durationMs: 42 });
});
test('maps sandbox boundary requests and decisions as first-class runtime actions', async () => {
const backend = new ScriptedBackend({
events: [
ev({
type: 'sandbox_boundary_request',
requestId: 'boundary-1',
toolUseId: 'tu-boundary',
justification: 'Write the requested export.',
expansion: {
filesystem: {
entries: [{ path: '/tmp/export.txt', access: 'write', scope: 'exact' }],
},
},
}),
ev({
type: 'sandbox_boundary_decision_ack',
requestId: 'boundary-1',
toolUseId: 'tu-boundary',
decision: 'allow',
status: 'approved',
revision: 2,
}),
ev({ type: 'complete', stopReason: 'end_turn' }),
],
});
const flow = new AiSdkFlow({ backend });
const out = await collect(flow.run(ctx, { text: 'do it', context: [] }));
const req = out[0];
assert.equal(req.author, 'system');
assert.deepEqual(req.actions?.stateDelta?.sandboxBoundaryRequest, {
requestId: 'boundary-1',
toolUseId: 'tu-boundary',
justification: 'Write the requested export.',
expansion: {
filesystem: {
entries: [{ path: '/tmp/export.txt', access: 'write', scope: 'exact' }],
},
},
});
const ack = out[1];
assert.equal(ack.author, 'user');
assert.deepEqual(ack.actions?.stateDelta?.sandboxBoundaryDecision, {
requestId: 'boundary-1',
decision: 'allow',
status: 'approved',
revision: 2,
});
assert.equal(out[2].status, 'completed');
});
test('drops legacy permission events at backend ingress and rejects direct mapping', async () => {
const legacyEvents = [
ev({
type: 'permission_request',
kind: 'tool_permission',
requestId: 'legacy-request',
toolUseId: 'legacy-tool',
toolName: 'Write',
category: 'file_write',
reason: 'file_write',
args: { path: '/tmp/example' },
rememberForTurnAllowed: true,
}),
ev({
type: 'permission_answer_ack',
requestId: 'legacy-request',
toolUseId: 'legacy-tool',
}),
ev({
type: 'permission_closure_ack',
requestId: 'legacy-request',
toolUseId: 'legacy-tool',
reason: 'timed_out',
}),
ev({
type: 'permission_decision_ack',
requestId: 'legacy-request',
toolUseId: 'legacy-tool',
decision: 'deny',
}),
];
const observed: string[] = [];
const backend = new ScriptedBackend({
events: [...legacyEvents, ev({ type: 'complete', stopReason: 'end_turn' })],
});
const flow = new AiSdkFlow({
backend,
onSessionEvent: (event) => {
observed.push(event.type);
},
});
const out = await collect(flow.run(ctx, { text: 'do it', context: [] }));
assert.deepEqual(
out.map((event) => event.status),
['completed'],
);
assert.deepEqual(observed, ['complete']);
for (const legacyEvent of legacyEvents) {
assert.throws(
() => mapSessionEventToRuntimeEvent(legacyEvent, ctx, createSessionEventMapMemory()),
/legacy permission event/,
);
}
});
test('maps the error path preserving error content + terminal failed', async () => {
const backend = new ScriptedBackend({
events: [
ev({
type: 'error',
recoverable: false,
code: 'AUTH',
reason: 'auth_failed',
message: 'no token',
}),
ev({ type: 'complete', stopReason: 'error' }),
],
});
const flow = new AiSdkFlow({ backend });
const out = await collect(flow.run(ctx, { text: 'hi', context: [] }));
const err = out[0];
assert.equal(err.content?.kind, 'error');
const errContent = err.content as { code?: string; reason?: string; message: string };
assert.equal(errContent.message, 'no token');
assert.equal(errContent.code, 'AUTH');
assert.equal(errContent.reason, 'auth_failed');
// error event itself is non-terminal; the trailing complete carries failed.
assert.equal(isTerminalRuntimeEvent(err), false);
assert.equal(out[1].status, 'failed');
assert.equal(isTerminalRuntimeEvent(out[1]), true);
});
test('synthesizes a failed terminal event when the backend exhausts without one', async () => {
const seen: SessionEvent[] = [];
let idSeq = 0;
const backend = new ScriptedBackend({
events: [ev({ type: 'text_delta', messageId: 'm1', text: 'partial answer' })],
});
const flow = new AiSdkFlow({
backend,
onSessionEvent: (sessionEvent) => {
seen.push(sessionEvent);
},
});
const out = await collect(
flow.run(
{ ...ctx, newId: () => `synthetic-${(idSeq += 1)}`, now: () => 2000 },
{ text: 'hi', context: [] },
),
);
assert.deepEqual(
seen.map((event) => event.type),
['text_delta', 'error', 'complete'],
);
assert.equal(seen[1]?.type, 'error');
assert.equal(
(seen[1] as Extract<SessionEvent, { type: 'error' }>).reason,
'missing_terminal_event',
);
assert.equal(seen[2]?.type, 'complete');
assert.equal((seen[2] as Extract<SessionEvent, { type: 'complete' }>).stopReason, 'error');
assert.equal(out.at(-2)?.content?.kind, 'error');
assert.equal(
(out.at(-2)?.content as { reason?: string } | undefined)?.reason,
'missing_terminal_event',
);
assert.equal(out.at(-1)?.status, 'failed');
assert.equal(out.filter(isTerminalRuntimeEvent).length, 1);
});
test('maps the abort path to exactly one terminal event', async () => {
const backend = new ScriptedBackend({
events: [
ev({ type: 'text_delta', messageId: 'm1', text: 'par' }),
ev({ type: 'abort', reason: 'user_stop' }),
ev({ type: 'complete', stopReason: 'user_stop' }),
],
});
const flow = new AiSdkFlow({ backend });
const out = await collect(flow.run(ctx, { text: 'hi', context: [] }));
// AgentFlow guarantees exactly one terminal event, so the trailing
// complete(user_stop) from the legacy backend is coalesced away.
assert.equal(out.length, 2);
assert.equal(out[1].status, 'aborted');
assert.equal(out[1].actions?.endInvocation, true);
assert.equal(isTerminalRuntimeEvent(out[1]), true);
assert.equal(out.filter(isTerminalRuntimeEvent).length, 1);
});
test('stops yielding after the first terminal event', async () => {
const backend = new ScriptedBackend({
events: [
ev({ type: 'abort', reason: 'user_stop' }),
ev({ type: 'text_delta', messageId: 'm1', text: 'after-terminal' }),
ev({ type: 'complete', stopReason: 'user_stop' }),
],
});
const flow = new AiSdkFlow({ backend });
const out = await collect(flow.run(ctx, { text: 'hi', context: [] }));
assert.equal(out.length, 1);
assert.equal(out[0]?.status, 'aborted');
assert.equal(isTerminalRuntimeEvent(out[0]), true);
});
test('can silently drain backend events after a terminal while coalescing duplicate terminals', async () => {
const seen: SessionEvent[] = [];
const backend = new ScriptedBackend({
events: [
ev({ type: 'abort', reason: 'user_stop' }),
ev({ type: 'text_delta', messageId: 'm1', text: 'cleanup-after-terminal' }),
ev({ type: 'complete', stopReason: 'user_stop' }),
],
});
const flow = new AiSdkFlow({
backend,
drainAfterTerminal: true,
onSessionEvent: (sessionEvent) => {
seen.push(sessionEvent);
},
});
const out = await collect(flow.run(ctx, { text: 'hi', context: [] }));
assert.equal(backend.yieldedEvents, 3);
assert.deepEqual(
seen.map((event) => event.type),
['abort'],
);
assert.deepEqual(
out.map((event) => event.content?.kind ?? event.status ?? null),
['aborted'],
);
assert.equal(out.filter(isTerminalRuntimeEvent).length, 1);
});
test('reports terminal onSessionEvent failures before accepting the terminal event', async () => {
const seenErrors: string[] = [];
const backend = new ScriptedBackend({
events: [
ev({ type: 'complete', stopReason: 'end_turn' }),
ev({ type: 'text_delta', messageId: 'm1', text: 'after-terminal' }),
],
});
const flow = new AiSdkFlow({
backend,
drainAfterTerminal: true,
onSessionEvent: () => {
throw new Error('terminal write failed');
},
onError: (error) => {
seenErrors.push(error instanceof Error ? error.message : String(error));
},
});
await assert.rejects(
collect(flow.run(ctx, { text: 'hi', context: [] })),
/terminal write failed/,
);
assert.deepEqual(seenErrors, ['terminal write failed']);
assert.equal(backend.yieldedEvents, 1);
});
test('RuntimeRunner consumes AiSdkFlow abort as one coherent failed outcome', async () => {
const backend = new ScriptedBackend({
events: [
ev({ type: 'text_delta', messageId: 'm1', text: 'par' }),
ev({ type: 'abort', reason: 'user_stop' }),
ev({ type: 'complete', stopReason: 'user_stop' }),
],
});
const flow = new AiSdkFlow({ backend });
let idSeq = 0;
const runner = new RuntimeRunner({
flow,
providers: {
newId: () => `id-${(idSeq += 1)}`,
now: () => 1000,
},
});
const result = await runner.run({
sessionId: 'session-1',
turnId: 'turn-1',
text: 'hi',
source: 'test',
});
assert.equal(result.status, 'failed');
assert.equal(result.failure?.class, 'aborted');
assert.equal(result.events.filter(isTerminalRuntimeEvent).length, 1);
});
test('delegates stop / respondToSandboxBoundary / dispose to the wrapped backend', async () => {
const backend = new ScriptedBackend({ events: [] });
const flow = new AiSdkFlow({ backend });
await flow.stop('redirect');
await flow.respondToSandboxBoundary({ requestId: 'r', decision: 'allow' });
await flow.dispose();
assert.deepEqual(backend.stopCalls, ['redirect']);
assert.deepEqual(backend.permissionCalls, [{ requestId: 'r', decision: 'allow' }]);
assert.equal(backend.disposeCalls, 1);
});
test('throws on session id mismatch between ctx and backend', async () => {
const backend = new ScriptedBackend({ sessionId: 'session-1', events: [] });
const flow = new AiSdkFlow({ backend });
await assert.rejects(
collect(flow.run({ ...ctx, sessionId: 'other' }, { text: 'hi', context: [] })),
/AiSdkFlow session mismatch/,
);
});
test('bridges FlowInput.abortSignal onto backend.stop("user_stop")', async () => {
let releaseGate: () => void = () => {};
const gate = new Promise<void>((resolve) => {
releaseGate = resolve;
});
const backend = new ScriptedBackend({
events: [
ev({ type: 'text_delta', messageId: 'm1', text: 'x' }),
ev({ type: 'complete', stopReason: 'end_turn' }),
],
gate: () => gate,
});
// stop releases the gate so send() can advance to the terminal event.
const realStop = backend.stop.bind(backend);
backend.stop = async (reason) => {
await realStop(reason);
releaseGate();
};
const flow = new AiSdkFlow({ backend });
const ctrl = new AbortController();
const runPromise = collect(
flow.run(ctx, { text: 'hi', context: [], abortSignal: ctrl.signal }),
);
// Let the generator yield the first event and park on the gate.
await new Promise((r) => setTimeout(r, 0));
ctrl.abort();
const out = await runPromise;
assert.deepEqual(backend.stopCalls, ['user_stop']);
assert.equal(out.length, 2);
assert.equal(isTerminalRuntimeEvent(out[out.length - 1]), true);
});
test('maps provider retry progress as a partial non-terminal runtime fact', () => {
const retry = ev({
type: 'provider_retry',
phase: 'scheduled',
attempt: 2,
maxAttempts: 10,
delayMs: 4_000,
reason: 'rate_limit',
});
const mapped = mapSessionEventToRuntimeEvent(retry, ctx);
assert.equal(mapped.partial, true);
assert.equal(isTerminalRuntimeEvent(mapped), false);
assert.deepEqual(mapped.actions?.stateDelta, {
providerRetry: {
phase: 'scheduled',
attempt: 2,
maxAttempts: 10,
delayMs: 4_000,
reason: 'rate_limit',
},
});
});
});
describe('token usage durable round trip', () => {
test('token usage fields survive SessionEvent projection and legacy backfill', () => {
const contextBudget = {
enabled: true,
estimatedTokensBefore: 100,
estimatedTokensAfter: 80,
keptTurns: 2,
droppedTurns: 1,
keptEvents: 4,
droppedEvents: 2,
};
const usage = {
input: 100,
output: 25,
cacheHitInput: 40,
cacheMissInput: 60,
cacheWriteInput: 10,
cacheMissInputSource: 'explicit' as const,
reasoning: 5,
total: 125,
rawFinishReason: 'stop',
runtimeSteps: 3,
cacheRead: 40,
cacheCreation: 10,
costUsd: 0.002,
systemPromptHash: 'system-hash',
contextRemaining: 9000,
prefixHash: 'prefix-hash',
prefixChangeReason: 'stable' as const,
requestShapeHash: 'request-shape-hash',
requestShapeChangeReason: 'stable' as const,
promptSegments: [{ kind: 'system_prompt' as const, chars: 400, estimatedTokens: 100 }],
contextBudget,
};
const sessionEvent: SessionEvent = {
type: 'token_usage',
id: 'usage-1',
turnId: 'turn-1',
ts: 123,
...usage,
providerRequestTraceId: 'provider-trace-1',
};
const runtimeEvent = decodeRuntimeEvent(
JSON.parse(
JSON.stringify(
mapSessionEventToRuntimeEvent(sessionEvent, ctx, createSessionEventMapMemory()),
),
),
);
assert.deepEqual(runtimeEvent.actions?.tokenUsage, usage);
assert.equal(runtimeEvent.refs?.providerRequestTraceId, 'provider-trace-1');
const projected = projectRuntimeEventsToStoredMessages([runtimeEvent], {
runHeaders: [],
});
const projectedMessage = projected.messages[0];
assert.ok(projectedMessage);
const stored = decodeStoredMessageForRecovery(JSON.parse(JSON.stringify(projectedMessage)));
assert.deepEqual(stored, {
type: 'token_usage',
id: 'usage-1',
turnId: 'turn-1',
ts: 123,
...usage,
providerRequestTraceId: 'provider-trace-1',
});
assert.deepEqual(projected.diagnostics, []);
const backfilled = backfillRuntimeEventsFromStoredMessages({
run: {
runId: 'run-1',
sessionId: 'session-1',
turnId: 'turn-1',
status: 'completed',
backendKind: 'ai-sdk',
llmConnectionSlug: 'anthropic',
modelId: 'model-1',
cwd: '/tmp',
permissionMode: 'ask',
createdAt: 100,
updatedAt: 200,
},
messages: [stored],
newId: () => 'backfilled-usage-1',
now: () => 999,
});
const backfilledEvent = backfilled.events[0];
assert.ok(backfilledEvent);
const replayed = decodeRuntimeEvent(JSON.parse(JSON.stringify(backfilledEvent)));
assert.deepEqual(replayed.actions?.tokenUsage, usage);
assert.equal(replayed.refs?.providerRequestTraceId, 'provider-trace-1');
});
});
// ============================================================================
// Pure mapping unit tests
// ============================================================================
describe('mapSessionEventToRuntimeEvent (pure)', () => {
test('mapCompleteStopReason covers all stop reasons', () => {
assert.equal(mapCompleteStopReason('end_turn'), 'completed');
assert.equal(mapCompleteStopReason('max_tokens'), 'completed');
assert.equal(mapCompleteStopReason('plan_handoff'), 'completed');
assert.equal(mapCompleteStopReason('graph_yield'), 'completed');
assert.equal(mapCompleteStopReason('permission_handoff'), 'completed');
assert.equal(mapCompleteStopReason('user_stop'), 'aborted');
assert.equal(mapCompleteStopReason('error'), 'failed');
assert.equal(mapCompleteStopReason('step_limit'), 'failed');
});
test('step_limit uses the established tool-step-cap failure class', () => {
const mapped = mapSessionEventToRuntimeEvent(
ev({ type: 'complete', stopReason: 'step_limit' }),
ctx,
createSessionEventMapMemory(),
);
assert.deepEqual(mapped.actions?.stateDelta, {
stopReason: 'step_limit',
failureClass: 'tool_step_cap_reached',
});
});
test('context_budget_exhausted keeps its detail in the durable terminal state', () => {
const mapped = mapSessionEventToRuntimeEvent(
ev({
type: 'complete',
stopReason: 'context_budget_exhausted',
contextBudgetExhaustedDetail: 'head_anchor_exceeds_capacity',
}),
ctx,
createSessionEventMapMemory(),
);
assert.equal(mapped.status, 'failed');
assert.deepEqual(mapped.actions?.stateDelta, {
stopReason: 'context_budget_exhausted',
failureClass: 'context_budget_exhausted',
contextBudgetExhaustedDetail: 'head_anchor_exceeds_capacity',
});
});
test('tool_output_delta and tool_progress map to partial tool-role heartbeats', () => {
const mem = createSessionEventMapMemory();
const a = mapSessionEventToRuntimeEvent(
ev({
type: 'tool_output_delta',
sessionId: 'session-1',
toolCallId: 'tu-1',
toolUseId: 'tu-1',
seq: 1,
stream: 'stdout',
chunk: 'c',
redacted: false,
createdAt: 1,
}),
ctx,
mem,
);
assert.equal(a.partial, true);
assert.equal(a.role, 'tool');
assert.equal(a.author, 'tool');
assert.equal(a.refs?.toolCallId, 'tu-1');
const b = mapSessionEventToRuntimeEvent(
ev({ type: 'tool_progress', toolUseId: 'tu-1', chunk: 'c' }),
ctx,
mem,
);
assert.equal(b.partial, true);
assert.equal(b.role, 'tool');
});
test('tool_start maps its semantic activity kind into runtime state', () => {
const event = mapSessionEventToRuntimeEvent(
ev({
type: 'tool_start',
toolUseId: 'tu-kind',
toolName: 'CustomCommand',
activityKind: 'command',
args: {},
}),
ctx,
createSessionEventMapMemory(),
);
assert.equal(event.actions?.stateDelta?.activityKind, 'command');
});
test('tool activity mapping retains nested CodeMode replay and parent identity', () => {
const memory = createSessionEventMapMemory();
const start = mapSessionEventToRuntimeEvent(
ev({
type: 'tool_start',
toolUseId: 'nested-1',
toolName: 'Read',
operationId: 'nested-op-1',
args: {},
origin: 'code_mode',
modelVisibility: 'hidden',
parentToolCallId: 'exec-1',
parentOperationId: 'exec-op-1',
}),
ctx,
memory,
);
const result = mapSessionEventToRuntimeEvent(
ev({
type: 'tool_result',
toolUseId: 'nested-1',
operationId: 'nested-op-1',
isError: false,
content: { kind: 'text', text: 'ok' },
origin: 'code_mode',
modelVisibility: 'hidden',
parentToolCallId: 'exec-1',
parentOperationId: 'exec-op-1',
}),
ctx,
memory,
);
for (const event of [start, result]) {
assert.equal(event.origin, 'code_mode');
assert.equal(event.modelVisibility, 'hidden');
assert.equal(event.refs?.parentToolCallId, 'exec-1');
assert.equal(event.refs?.parentOperationId, 'exec-op-1');
}
});
test('owns independent tool args across SessionEvent to RuntimeEvent mappings', () => {
const sourceArgs = { content: 'approved', layout: { cols: 120 } };
const sourceEvent = ev({
type: 'tool_start',
toolUseId: 'tu-owned',
toolName: 'Write',
args: sourceArgs,
});
const mapped = mapSessionEventToRuntimeEvent(sourceEvent, ctx, createSessionEventMapMemory());
const mappedArgs = (
mapped.content?.kind === 'function_call' ? mapped.content.args : undefined
) as typeof sourceArgs;
assert.notStrictEqual(mappedArgs, sourceArgs);
assert.notStrictEqual(mappedArgs.layout, sourceArgs.layout);
sourceArgs.layout.cols = 80;
assert.equal(mappedArgs.layout.cols, 120);
mappedArgs.content = 'runtime';
assert.equal(sourceArgs.content, 'approved');
});
test('plan_submitted maps to an agent-authored state delta', () => {
const a = mapSessionEventToRuntimeEvent(
ev({ type: 'plan_submitted', planId: 'p1', title: 'T', markdownPath: '/p.md' }),
ctx,
);
assert.equal(a.role, 'system');
assert.equal(a.author, 'agent');
assert.deepEqual(a.actions?.stateDelta, { planId: 'p1', title: 'T', markdownPath: '/p.md' });
});
test('user_question_request maps to one system-authored runtime action', () => {
const mapped = mapSessionEventToRuntimeEvent(
ev({
type: 'user_question_request',
requestId: 'question-1',
toolUseId: 'tool-1',
questions: [
{
question: 'Choose an approach',
options: [
{ label: 'Extend', description: 'Reuse the runtime seam' },
{ label: 'Separate' },
],
},
],
}),
ctx,
);
assert.equal(mapped.role, 'system');
assert.equal(mapped.author, 'system');
assert.deepEqual(mapped.actions?.userQuestionRequest, {
requestId: 'question-1',
toolUseId: 'tool-1',
questions: [
{
question: 'Choose an approach',
options: [
{ label: 'Extend', description: 'Reuse the runtime seam' },
{ label: 'Separate' },
],
},
],
});
});
test('user_question_answer_ack maps without duplicating the canonical answer', () => {
const mapped = mapSessionEventToRuntimeEvent(
ev({
type: 'user_question_answer_ack',
requestId: 'question-1',
toolUseId: 'tool-1',
}),
ctx,
);
assert.equal(mapped.role, 'system');
assert.equal(mapped.author, 'user');
assert.deepEqual(mapped.actions?.userQuestionAnswerAccepted, {
requestId: 'question-1',
});
assert.equal(mapped.refs?.toolCallId, 'tool-1');
});
test('tool_result without a prior tool_start still maps (name falls back to empty)', () => {
const a = mapSessionEventToRuntimeEvent(
ev({
type: 'tool_result',
toolUseId: 'orphan',
isError: true,
content: { kind: 'text', text: 'boom' },
}),
ctx,
);
const fnResp = a.content as { name: string; isError?: boolean };
assert.equal(fnResp.name, '');
assert.equal(fnResp.isError, true);
});
test('branch is propagated when present on the context', () => {
const a = mapSessionEventToRuntimeEvent(ev({ type: 'complete', stopReason: 'end_turn' }), {
...ctx,
branch: 'agent-b',
});
assert.equal(a.branch, 'agent-b');
});
});
// ============================================================================
// Projection coverage contract
// ============================================================================
/**
* One sample per backend-mappable SessionEvent variant. `subject` is typed to
* its own key, so a new variant cannot be satisfied by an empty list or by
* some other event that happens to project cleanly; `before` and `after` carry
* only the companions that variant's projection needs.
*/
type ProjectionSamples = {
[K in BackendSessionEvent['type']]: {
subject: Extract<BackendSessionEvent, { type: K }>;
before?: SessionEvent[];
after?: SessionEvent[];
};
};
const PROJECTION_SAMPLES: ProjectionSamples = {
text_delta: {
subject: { type: 'text_delta', id: 'e', turnId: 'turn-1', ts: 1, messageId: 'm1', text: 'h' },
},
text_complete: {
subject: {
type: 'text_complete',
id: 'e',
turnId: 'turn-1',
ts: 1,
messageId: 'm1',
text: 'hi',
},
},
thinking_delta: {
subject: {
type: 'thinking_delta',
id: 'e',
turnId: 'turn-1',
ts: 1,
messageId: 'm1',
text: 'h',
},
},
thinking_complete: {
subject: {
type: 'thinking_complete',
id: 'e1',
turnId: 'turn-1',
ts: 1,
messageId: 'm1',
text: 'why',
},
// Thinking is held until the assistant text row that shares its message id.
after: [
{ type: 'text_complete', id: 'e2', turnId: 'turn-1', ts: 2, messageId: 'm1', text: 'hi' },
],
},
tool_start: {
subject: {
type: 'tool_start',
id: 'e',
turnId: 'turn-1',
ts: 1,
toolUseId: 'tool-1',
toolName: 'Read',
args: { path: '/tmp/a' },
},
},
tool_output_delta: {
subject: {
type: 'tool_output_delta',
id: 'e',
turnId: 'turn-1',
ts: 1,
sessionId: 'session-1',
toolCallId: 'tool-1',
toolUseId: 'tool-1',
seq: 1,
stream: 'stdout',
chunk: 'out',
redacted: false,
createdAt: 1,
},
},
tool_progress: {
subject: {
type: 'tool_progress',
id: 'e',
turnId: 'turn-1',
ts: 1,
toolUseId: 'tool-1',
chunk: 'x',
},
},
tool_result_preview: {
subject: {
type: 'tool_result_preview',
id: 'e',
turnId: 'turn-1',
ts: 1,
toolUseId: 'tool-1',
isError: false,
content: {
kind: 'subagent',
childSessionId: 'child-1',
agentName: 'Local Read',
turnId: 'child-turn',
status: 'running',
permissionMode: 'explore',
},
},
},
tool_result: {
subject: {
type: 'tool_result',
id: 'e2',
turnId: 'turn-1',
ts: 2,
toolUseId: 'tool-1',
isError: false,
content: { kind: 'text', text: 'ok' },
},
// A result carries no tool name of its own; the mapper reads it from the call.
before: [
{
type: 'tool_start',
id: 'e1',
turnId: 'turn-1',
ts: 1,
toolUseId: 'tool-1',
toolName: 'Read',
args: { path: '/tmp/a' },
},
],
},
sandbox_boundary_request: {
subject: {
type: 'sandbox_boundary_request',
id: 'e',
turnId: 'turn-1',
ts: 1,
requestId: 'boundary-1',
toolUseId: 'tool-1',
justification: 'read a file outside the workspace',
expansion: {
filesystem: { entries: [{ path: '/tmp/outside.txt', access: 'read', scope: 'exact' }] },
},
},
},
sandbox_boundary_decision_ack: {
subject: {
type: 'sandbox_boundary_decision_ack',
id: 'e',
turnId: 'turn-1',
ts: 1,
requestId: 'boundary-1',
toolUseId: 'tool-1',
decision: 'allow',
status: 'approved',
revision: 2,
},
},
user_question_request: {
subject: {
type: 'user_question_request',
id: 'e',
turnId: 'turn-1',
ts: 1,
requestId: 'q-1',
toolUseId: 'tool-1',
questions: [{ question: 'Which one?', options: [{ label: 'A', description: 'a' }] }],
},
},
user_question_answer_ack: {
subject: {
type: 'user_question_answer_ack',
id: 'e',
turnId: 'turn-1',
ts: 1,
requestId: 'q-1',
toolUseId: 'tool-1',
},
},
plan_submitted: {
subject: {
type: 'plan_submitted',
id: 'e',
turnId: 'turn-1',
ts: 1,
planId: 'plan-1',
title: 'Plan',
},
},
token_usage: {
subject: {
type: 'token_usage',
id: 'e',
turnId: 'turn-1',
ts: 1,
input: 10,
output: 5,
total: 15,
},
},
steering_message: {
subject: {
type: 'steering_message',
id: 'e',
turnId: 'turn-1',
ts: 1,
messageId: 'm2',
content: { text: 'steer' },
},
},
provider_retry: {
subject: {
type: 'provider_retry',
id: 'e',
turnId: 'turn-1',
ts: 1,
phase: 'started',
attempt: 2,
maxAttempts: 3,
reason: 'rate_limit',
},
},
error: {
subject: {
type: 'error',
id: 'e1',
turnId: 'turn-1',
ts: 1,
recoverable: false,
message: 'boom',
},
// An error is always followed by a terminal complete carrying the failure.
after: [{ type: 'complete', id: 'e2', turnId: 'turn-1', ts: 2, stopReason: 'error' }],
},
complete: {
subject: { type: 'complete', id: 'e', turnId: 'turn-1', ts: 1, stopReason: 'end_turn' },
},
abort: { subject: { type: 'abort', id: 'e', turnId: 'turn-1', ts: 1, reason: 'user_stop' } },
};
const projectionRunHeader: AgentRunHeader = {
runId: 'run-1',
sessionId: 'session-1',
turnId: 'turn-1',
status: 'completed',
backendKind: 'ai-sdk',
llmConnectionSlug: 'anthropic',
modelId: 'model-1',
cwd: '/tmp',
permissionMode: 'ask',
createdAt: 1,
updatedAt: 2,
completedAt: 2,
};
describe('SessionEvent projection coverage', () => {
// The contract is over what a reader can actually meet: every mapped event
// AgentRun admits to the ledger has to project. It asserts on the unclaimed
// codes at either severity, not on the hard one alone — a control fact whose
// gap only degrades the view is still a gap, and must be found here rather
// than by a user opening the session.
for (const [type, sample] of Object.entries(PROJECTION_SAMPLES)) {
test(`${type} projects without an unclaimed-event diagnostic`, () => {
let seq = 0;
const memory = createSessionEventMapMemory();
const runtimeEvents = [...(sample.before ?? []), sample.subject, ...(sample.after ?? [])]
.map((event) =>
mapSessionEventToRuntimeEvent(
event,
{
...ctx,
newId: () => {
seq += 1;
return `rt-${seq}`;
},
},
memory,
),
)
.filter((event) => !isNonTerminalErrorRuntimeEvent(event));
const projected = projectRuntimeEventsToStoredMessages(runtimeEvents, {
runHeaders: [projectionRunHeader],
});
assert.deepEqual(projected.diagnostics.filter(isUnclaimedRuntimeEventDiagnostic), []);
});
}
// The guard's fallback is what a variant added without a claim actually
// becomes. It has to stay on the degradable side of the line: control-only,
// so the session it lands in still opens, and still reported so the gap the
// coverage contract would have caught is not invisible at runtime.
test('an unmapped SessionEvent maps to a reported control-only fact', () => {
const unmapped = { type: 'not_yet_mapped', id: 'e', turnId: 'turn-1', ts: 1 };
const memory = createSessionEventMapMemory();
const runtimeEvent = mapSessionEventToRuntimeEvent(
unmapped as unknown as SessionEvent,
ctx,
memory,
);
assert.equal(runtimeEvent.content, undefined);
assert.equal(runtimeEvent.actions?.stateDelta?.unmappedSessionEventType, 'not_yet_mapped');
const projected = projectRuntimeEventsToStoredMessages([runtimeEvent], {
runHeaders: [projectionRunHeader],
});
assert.deepEqual(projected.messages, []);
// Filtered through the predicate the contract above uses, not just compared
// to the code string: dropping the soft code from the predicate would
// otherwise loosen the contract to `unsupported_event` only, silently.
assert.deepEqual(
projected.diagnostics.filter(isUnclaimedRuntimeEventDiagnostic).map((d) => d.code),
['unclaimed_control_fact'],
);
assert.equal(projected.diagnostics.length, 1);
});
});