blob: 78e922662dccf1b76f59521a2afbb6aa3ef61014 [file]
/*
* 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 { describe, test } from 'node:test';
import type { RuntimeInvocationRecord } from '@maka/core/runtime-invocation';
import type { SessionEvent } from '@maka/core/events';
import type { BackendSessionEvent } from '@maka/core/backend-types';
import type { RuntimeEvent } from '@maka/core/runtime-event';
import {
decodeRuntimeEvent,
isTerminalRuntimeEvent,
isPartialRuntimeEvent,
} from '@maka/core/runtime-event';
import {
mapCompleteStopReason,
mapSessionEventToRuntimeEvent,
createSessionEventMapMemory,
isLiveBackendSessionEvent,
} from '../session-event-runtime-mapper.js';
import type { RuntimeEventMapContext } from '../session-event-runtime-mapper.js';
import {
isUnclaimedRuntimeEventDiagnostic,
projectRuntimeEventsToStoredMessages,
} from '../runtime-event-read-model.js';
import { isNonTerminalErrorRuntimeEvent } from '../agent-run.js';
import { backfillRuntimeEventsFromStoredMessages } from '../runtime-event-backfill.js';
import { testInvocationOpening } from './invocation-fixture.js';
// ============================================================================
// 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',
now: () => 1000,
} satisfies RuntimeEventMapContext;
// ============================================================================
// Tests
// ============================================================================
describe('SessionEvent Runtime mapper', () => {
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);
assert.equal(runtimeEvent.content?.kind, 'text');
});
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',
},
});
});
test('maps provider capacity retry progress without collapsing its reason', () => {
const retry = ev({
type: 'provider_retry',
phase: 'scheduled',
attempt: 2,
maxAttempts: 10,
delayMs: 4_000,
reason: 'provider_capacity',
});
const mapped = mapSessionEventToRuntimeEvent(retry, ctx);
assert.deepEqual(mapped.actions?.stateDelta, {
providerRetry: {
phase: 'scheduled',
attempt: 2,
maxAttempts: 10,
delayMs: 4_000,
reason: 'provider_capacity',
},
});
});
});
// ============================================================================
// 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('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 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('form_request maps to one system-authored runtime action', () => {
const mapped = mapSessionEventToRuntimeEvent(
ev({
type: 'form_request',
requestId: 'form-1',
toolUseId: 'tool-1',
message: 'Choose settings',
requester: { name: 'deploy' },
fields: [{ kind: 'boolean', name: 'confirm', label: 'Confirm', required: true }],
}),
ctx,
);
assert.equal(mapped.role, 'system');
assert.equal(mapped.author, 'system');
assert.deepEqual(mapped.actions?.formRequest, {
requestId: 'form-1',
toolUseId: 'tool-1',
message: 'Choose settings',
requester: { name: 'deploy' },
fields: [{ kind: 'boolean', name: 'confirm', label: 'Confirm', required: true }],
});
});
test('form_answer_ack maps without duplicating the canonical result', () => {
const mapped = mapSessionEventToRuntimeEvent(
ev({
type: 'form_answer_ack',
requestId: 'form-1',
toolUseId: 'tool-1',
}),
ctx,
);
assert.equal(mapped.role, 'system');
assert.equal(mapped.author, 'user');
assert.deepEqual(mapped.actions?.formAnswerAccepted, { requestId: 'form-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',
},
},
form_request: {
subject: {
type: 'form_request',
id: 'e',
turnId: 'turn-1',
ts: 1,
requestId: 'form-1',
toolUseId: 'tool-1',
message: 'Choose settings',
requester: { name: 'deploy' },
fields: [{ kind: 'boolean', name: 'confirm', label: 'Confirm', required: true }],
},
},
form_answer_ack: {
subject: {
type: 'form_answer_ack',
id: 'e',
turnId: 'turn-1',
ts: 1,
requestId: 'form-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 projectionInvocation: RuntimeInvocationRecord = {
sessionId: 'session-1',
invocationId: 'invocation-1',
runId: 'run-1',
turnId: 'turn-1',
openedAt: 1,
opening: testInvocationOpening({
route: {
provenance: 'runtime',
backendKind: 'ai-sdk',
llmConnectionId: 'anthropic-connection',
llmConnectionSlug: 'anthropic',
modelId: 'model-1',
},
configuration: { cwd: '/tmp' },
}),
};
describe('SessionEvent projection coverage', () => {
test('keeps Host admission facts out of durable Runtime events', () => {
assert.throws(
() =>
mapSessionEventToRuntimeEvent(
{
type: 'message_admission',
id: 'message-admission-1',
turnId: 'turn-1',
ts: 1,
messageId: 'message-1',
outcome: 'admitted',
},
ctx,
),
/message_admission is not a backend event/,
);
});
test('keeps Host-owned Client Capability interactions out of backend projection', () => {
const request: SessionEvent = {
type: 'client_capability_request',
id: 'client-capability-request-1',
turnId: 'turn-1',
ts: 1,
requestId: 'request-1',
toolUseId: 'tool-1',
capability: 'browser',
scope: { kind: 'browser_origin', origin: 'https://example.com' },
};
const ack: SessionEvent = {
type: 'client_capability_decision_ack',
id: 'client-capability-ack-1',
turnId: 'turn-1',
ts: 2,
requestId: 'request-1',
toolUseId: 'tool-1',
decision: 'allow',
};
for (const event of [request, ack]) {
assert.equal(isLiveBackendSessionEvent(event), false);
assert.throws(
() => mapSessionEventToRuntimeEvent(event, ctx),
new RegExp(`${event.type} is not a backend event`, 'u'),
);
}
});
// 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`, () => {
const memory = createSessionEventMapMemory();
const runtimeEvents = [...(sample.before ?? []), sample.subject, ...(sample.after ?? [])]
.map((event) => mapSessionEventToRuntimeEvent(event, ctx, memory))
.filter((event) => !isNonTerminalErrorRuntimeEvent(event));
const projected = projectRuntimeEventsToStoredMessages(runtimeEvents, {
invocations: [projectionInvocation],
});
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], {
invocations: [projectionInvocation],
});
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);
});
});