mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-24 16:17:57 +00:00
1538 lines
57 KiB
TypeScript
1538 lines
57 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import { filterOpsForGrade, isAppendOnly, redactSnapshotForGrade } from '#/granularity/filterOps';
|
|
import { detachGrades, gradeFor, needsResetOnTransition } from '#/granularity/grade';
|
|
import { paginateTurns } from '#/pagination/paginate';
|
|
import { ViewRegistry } from '#/view/registry';
|
|
import { groupMessagesIntoSnapshot, type HistoryContentPart } from '#/history/groupTurns';
|
|
import { foldWireRecordFacts, type HistoryWireRecord } from '#/history/foldFacts';
|
|
import {
|
|
transcriptOperationSchema,
|
|
transcriptQuerySchema,
|
|
transcriptResponseSchema,
|
|
transcriptGradeSpecSchema,
|
|
} from '#/contract/schema';
|
|
import type { TranscriptItem } from '#/model/item';
|
|
import type { AgentTranscriptSnapshot, TranscriptOperation } from '#/ops/operation';
|
|
|
|
const idLabel = (i: TranscriptItem): string =>
|
|
i.kind === 'turn' ? i.turnId : i.kind === 'marker' ? i.markerId : i.refId;
|
|
|
|
const turnOp = (n: number): TranscriptOperation => ({
|
|
op: 'turn.upsert',
|
|
turn: { kind: 'turn', turnId: `t${n}`, ordinal: n, state: 'running', origin: { kind: 'user' } },
|
|
});
|
|
|
|
const stepOp: TranscriptOperation = {
|
|
op: 'step.upsert',
|
|
turnId: 't1',
|
|
step: { kind: 'step', stepId: 't1.1', turnId: 't1', ordinal: 1, state: 'running' },
|
|
};
|
|
|
|
const frameOp: TranscriptOperation = {
|
|
op: 'frame.upsert',
|
|
turnId: 't1',
|
|
stepId: 't1.1',
|
|
frame: { kind: 'text', frameId: 't1.1.f1', role: 'assistant', text: 'full' },
|
|
};
|
|
|
|
const appendOp: TranscriptOperation = {
|
|
op: 'append',
|
|
target: { type: 'frame', turnId: 't1', stepId: 't1.1', frameId: 't1.1.f1' },
|
|
offset: 0,
|
|
text: 'chunk',
|
|
};
|
|
|
|
const promptOp: TranscriptOperation = {
|
|
op: 'prompt.upsert',
|
|
prompt: { promptId: 'p1', status: 'queued', createdAt: '2026-07-22T00:00:00.000Z' },
|
|
};
|
|
|
|
describe('granularity', () => {
|
|
const ops: TranscriptOperation[] = [
|
|
turnOp(1),
|
|
stepOp,
|
|
frameOp,
|
|
appendOp,
|
|
promptOp,
|
|
{ op: 'meta.merge', meta: { activity: 'turn' } },
|
|
];
|
|
|
|
it('off admits nothing', () => {
|
|
expect(filterOpsForGrade('off', ops)).toEqual([]);
|
|
});
|
|
|
|
it('turn admits headers and global state only', () => {
|
|
expect(filterOpsForGrade('turn', ops).map((op) => op.op)).toEqual([
|
|
'turn.upsert',
|
|
'prompt.upsert',
|
|
'meta.merge',
|
|
]);
|
|
});
|
|
|
|
it('block admits step/frame upserts but no appends', () => {
|
|
expect(filterOpsForGrade('block', ops).map((op) => op.op)).toEqual([
|
|
'turn.upsert',
|
|
'step.upsert',
|
|
'frame.upsert',
|
|
'prompt.upsert',
|
|
'meta.merge',
|
|
]);
|
|
});
|
|
|
|
it('delta admits everything', () => {
|
|
expect(filterOpsForGrade('delta', ops)).toHaveLength(ops.length);
|
|
});
|
|
|
|
it('gradeFor resolves agent override over wildcard default', () => {
|
|
const spec = { '*': 'turn', main: 'delta' } as const;
|
|
expect(gradeFor(spec, 'main')).toBe('delta');
|
|
expect(gradeFor(spec, 'sub-1')).toBe('turn');
|
|
expect(gradeFor(undefined, 'main')).toBe('off');
|
|
});
|
|
|
|
it('upgrade needs reset, downgrade does not', () => {
|
|
expect(needsResetOnTransition('turn', 'delta')).toBe(true);
|
|
expect(needsResetOnTransition('delta', 'turn')).toBe(false);
|
|
});
|
|
|
|
it('detachGrades writes explicit off so a wildcard default cannot resurrect the agent', () => {
|
|
expect(detachGrades({ '*': 'delta' }, ['main'])).toEqual({ '*': 'delta', main: 'off' });
|
|
expect(detachGrades({ '*': 'delta', main: 'turn' }, ['main'])).toEqual({
|
|
'*': 'delta',
|
|
main: 'off',
|
|
});
|
|
});
|
|
|
|
it('detachGrades deletes a listed wildcard entry', () => {
|
|
expect(detachGrades({ '*': 'delta', main: 'turn' }, ['*'])).toEqual({ main: 'turn' });
|
|
});
|
|
|
|
it('detachGrades collapses an all-off spec to undefined', () => {
|
|
expect(detachGrades({ main: 'delta' }, ['main'])).toBeUndefined();
|
|
expect(detachGrades({ '*': 'delta', main: 'off' }, ['*'])).toBeUndefined();
|
|
expect(detachGrades(undefined, ['main'])).toBeUndefined();
|
|
});
|
|
|
|
it('append-only batches are volatile-safe', () => {
|
|
expect(isAppendOnly([appendOp])).toBe(true);
|
|
expect(isAppendOnly([appendOp, frameOp])).toBe(false);
|
|
});
|
|
|
|
it('redactSnapshotForGrade strips step detail below block, keeps it at block+', () => {
|
|
const snapshot: AgentTranscriptSnapshot = {
|
|
items: [
|
|
{
|
|
kind: 'turn',
|
|
turnId: 't1',
|
|
ordinal: 1,
|
|
state: 'completed',
|
|
origin: { kind: 'user' },
|
|
prompt: 'hi',
|
|
steps: [
|
|
{
|
|
kind: 'step',
|
|
stepId: 't1.1',
|
|
turnId: 't1',
|
|
ordinal: 1,
|
|
state: 'completed',
|
|
frames: [{ kind: 'text', frameId: 't1.1.f1', role: 'assistant', text: 'body' }],
|
|
},
|
|
],
|
|
},
|
|
{ kind: 'marker', markerId: 'm1', marker: 'skill' },
|
|
],
|
|
tasks: [],
|
|
interactions: [
|
|
{
|
|
interactionId: 'appr-1',
|
|
interactionKind: 'approval' as const,
|
|
toolCallId: 'c1',
|
|
state: 'pending' as const,
|
|
},
|
|
],
|
|
attachments: [
|
|
{ attachmentId: 'att_1', mediaType: 'image/png', source: { kind: 'url' as const, url: 'https://example.com/a.png' } },
|
|
],
|
|
todos: [{ todoId: 'todo', items: [{ title: 'write tests', status: 'in_progress' as const }] }],
|
|
prompts: [{ promptId: 'p1', status: 'running' as const, createdAt: '2026-07-22T00:00:00.000Z' }],
|
|
meta: {},
|
|
};
|
|
const turnGrade = redactSnapshotForGrade('turn', snapshot);
|
|
expect(turnGrade.interactions).toHaveLength(1);
|
|
expect(turnGrade.attachments).toHaveLength(1);
|
|
expect(turnGrade.todos).toHaveLength(1);
|
|
expect(turnGrade.prompts).toHaveLength(1);
|
|
const turn = turnGrade.items[0];
|
|
expect(turn?.kind === 'turn' && turn.steps).toEqual([]);
|
|
expect(turn?.kind === 'turn' && turn.prompt).toBe('hi');
|
|
expect(turnGrade.items[1]?.kind).toBe('marker');
|
|
expect(redactSnapshotForGrade('block', snapshot)).toBe(snapshot);
|
|
expect(redactSnapshotForGrade('delta', snapshot)).toBe(snapshot);
|
|
});
|
|
});
|
|
|
|
describe('paginateTurns', () => {
|
|
const items: TranscriptItem[] = [
|
|
{ kind: 'marker', markerId: 'm0', marker: 'goal' },
|
|
...[1, 2, 3, 4, 5].flatMap((n): TranscriptItem[] => [
|
|
{
|
|
kind: 'turn',
|
|
turnId: `t${n}`,
|
|
ordinal: n,
|
|
state: 'completed',
|
|
origin: { kind: 'user' },
|
|
steps: [],
|
|
},
|
|
{ kind: 'marker', markerId: `m${n}`, marker: 'skill' },
|
|
]),
|
|
];
|
|
|
|
it('default page is the newest N turns with trailing segment items', () => {
|
|
const page = paginateTurns(items, { pageSize: 2 });
|
|
expect(page.items.map(idLabel)).toEqual(['t4', 'm4', 't5', 'm5']);
|
|
expect(page.hasMore).toBe(true);
|
|
});
|
|
|
|
it('before_turn pages toward older turns; head marker rides the oldest segment', () => {
|
|
const page = paginateTurns(items, { beforeTurn: 't4', pageSize: 2 });
|
|
expect(page.items.map(idLabel)).toEqual(['t2', 'm2', 't3', 'm3']);
|
|
expect(page.hasMore).toBe(true);
|
|
|
|
const oldest = paginateTurns(items, { beforeTurn: 't2', pageSize: 5 });
|
|
expect(oldest.items[0]).toEqual({ kind: 'marker', markerId: 'm0', marker: 'goal' });
|
|
expect(oldest.hasMore).toBe(false);
|
|
});
|
|
|
|
it('after_turn pages toward newer turns without the head unit', () => {
|
|
const page = paginateTurns(items, { afterTurn: 't3', pageSize: 2 });
|
|
expect(page.items.map(idLabel)).toEqual(['t4', 'm4', 't5', 'm5']);
|
|
expect(page.hasMore).toBe(false);
|
|
});
|
|
|
|
it('keeps head non-turn items with the newest page when turns exactly fill it', () => {
|
|
const page = paginateTurns(items, { pageSize: 5 });
|
|
expect(page.items[0]).toEqual({ kind: 'marker', markerId: 'm0', marker: 'goal' });
|
|
expect(page.items.map(idLabel)).toEqual(['m0', 't1', 'm1', 't2', 'm2', 't3', 'm3', 't4', 'm4', 't5', 'm5']);
|
|
expect(page.hasMore).toBe(false);
|
|
});
|
|
|
|
it('returns a marker-only timeline as one page with nothing older', () => {
|
|
const only = paginateTurns([{ kind: 'marker', markerId: 'm0', marker: 'goal' }], { pageSize: 3 });
|
|
expect(only.items.map(idLabel)).toEqual(['m0']);
|
|
expect(only.hasMore).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('ViewRegistry', () => {
|
|
it('dispatches on view ?? name, origin.kind and marker keys', () => {
|
|
const registry = new ViewRegistry<string>({ fallbackTool: 'generic' });
|
|
registry.registerTool('read', 'readRenderer');
|
|
registry.registerTool('swarm', 'swarmRenderer');
|
|
registry.registerInput('cron', 'cronInput');
|
|
registry.registerMarker('goal', 'goalMarker');
|
|
|
|
expect(
|
|
registry.resolveTool({ kind: 'tool', frameId: 'f', toolCallId: 'c1', name: 'Read', state: 'done' }),
|
|
).toBe('readRenderer');
|
|
expect(
|
|
registry.resolveTool({ kind: 'tool', frameId: 'f', toolCallId: 'c2', name: 'AgentSwarm', view: 'swarm', state: 'running' }),
|
|
).toBe('swarmRenderer');
|
|
expect(
|
|
registry.resolveTool({ kind: 'tool', frameId: 'f', toolCallId: 'c3', name: 'Bash', state: 'running' }),
|
|
).toBe('generic');
|
|
expect(registry.resolveInput({ kind: 'cron' })).toBe('cronInput');
|
|
expect(registry.resolveInput({ kind: 'user' })).toBeUndefined();
|
|
expect(registry.resolveMarker('goal')).toBe('goalMarker');
|
|
});
|
|
});
|
|
|
|
describe('contract schemas', () => {
|
|
it('roundtrips every op kind', () => {
|
|
const ops: TranscriptOperation[] = [
|
|
{ op: 'reset', agentId: 'main', snapshot: { items: [], tasks: [], interactions: [], attachments: [], todos: [], prompts: [], meta: {}, hasMoreOlder: true } },
|
|
turnOp(1),
|
|
stepOp,
|
|
frameOp,
|
|
appendOp,
|
|
{ op: 'marker.upsert', item: { kind: 'marker', markerId: 'm1', marker: 'goal' } },
|
|
{ op: 'taskref.upsert', item: { kind: 'taskref', refId: 'r1', taskId: 'task1' } },
|
|
{ op: 'task.upsert', task: { taskId: 'task1', kind: 'shell', state: 'running', detached: false, outputTail: '' } },
|
|
{
|
|
op: 'interaction.upsert',
|
|
interaction: { interactionId: 'appr-1', interactionKind: 'approval', toolCallId: 'c1', state: 'pending' },
|
|
},
|
|
{
|
|
op: 'attachment.upsert',
|
|
attachment: { attachmentId: 'att_1', mediaType: 'image/png', source: { kind: 'file', fileId: 'f1' } },
|
|
},
|
|
{ op: 'todo.upsert', todo: { todoId: 'todo', items: [{ title: 'x', status: 'done' }] } },
|
|
promptOp,
|
|
{ op: 'meta.merge', meta: { goal: { objective: 'x', status: 'active' } } },
|
|
{ op: 'items.remove', ids: ['t1'] },
|
|
];
|
|
for (const op of ops) {
|
|
expect(transcriptOperationSchema.parse(op)).toBeDefined();
|
|
}
|
|
});
|
|
|
|
it('roundtrips ops carrying the extended wire detail', () => {
|
|
const usage = { inputOther: 10, output: 5, inputCacheRead: 3, inputCacheCreation: 2 };
|
|
const ops: TranscriptOperation[] = [
|
|
{
|
|
op: 'reset',
|
|
agentId: 'main',
|
|
snapshot: {
|
|
items: [],
|
|
tasks: [],
|
|
interactions: [],
|
|
attachments: [],
|
|
todos: [],
|
|
prompts: [
|
|
{
|
|
promptId: 'p1',
|
|
status: 'completed',
|
|
userMessageId: 'u1',
|
|
content: [{ type: 'text', text: 'hi' }],
|
|
createdAt: '2026-07-22T00:00:00.000Z',
|
|
finishedAt: '2026-07-22T00:01:00.000Z',
|
|
steeredAt: '2026-07-22T00:00:30.000Z',
|
|
},
|
|
],
|
|
meta: {
|
|
agent: {
|
|
model: 'k2',
|
|
thinkingEffort: 'high',
|
|
usage: { byModel: { k2: usage }, currentTurn: usage, total: usage },
|
|
contextTokens: 1234,
|
|
maxContextTokens: 128000,
|
|
contextUsage: 0.01,
|
|
permission: 'auto',
|
|
phase: { kind: 'retrying', turnId: 1, step: 1, stepId: 't1.1', failedAttempt: 1, nextAttempt: 2, maxAttempts: 3, delayMs: 500, since: 1000 },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
op: 'turn.upsert',
|
|
turn: {
|
|
kind: 'turn', turnId: 't1', ordinal: 1, state: 'failed', origin: { kind: 'user' },
|
|
usage: { inputTokens: 12, outputTokens: 5, cachedTokens: 3 },
|
|
durationMs: 1500,
|
|
error: 'boom',
|
|
},
|
|
},
|
|
{
|
|
op: 'step.upsert',
|
|
turnId: 't1',
|
|
step: {
|
|
kind: 'step', stepId: 't1.1', turnId: 't1', ordinal: 1, state: 'interrupted',
|
|
usage,
|
|
finishReason: 'stop',
|
|
timing: {
|
|
llmFirstTokenLatencyMs: 120,
|
|
llmStreamDurationMs: 900,
|
|
llmRequestBuildMs: 5,
|
|
llmServerFirstTokenMs: 110,
|
|
llmServerDecodeMs: 700,
|
|
llmClientConsumeMs: 950,
|
|
},
|
|
retry: { failedAttempt: 1, nextAttempt: 2, maxAttempts: 3, delayMs: 500, errorName: 'RateLimit', errorMessage: 'slow down', statusCode: 429 },
|
|
endReason: 'aborted',
|
|
endMessage: 'user pressed escape',
|
|
},
|
|
},
|
|
{
|
|
op: 'frame.upsert',
|
|
turnId: 't1',
|
|
stepId: 't1.1',
|
|
frame: {
|
|
kind: 'tool', frameId: 't1.1.c1', toolCallId: 'c1', name: 'Bash', state: 'running',
|
|
inputText: '{"command":"ls',
|
|
progress: { kind: 'progress', text: 'half', percent: 50, customKind: 'bar', customData: { x: 1 } },
|
|
},
|
|
},
|
|
{
|
|
op: 'task.upsert',
|
|
task: {
|
|
taskId: 'task1', kind: 'subagent', state: 'completed', detached: false, outputTail: '',
|
|
resultSummary: 'scanned 12 files',
|
|
error: 'partial failure',
|
|
stateReason: 'waiting for input',
|
|
usage,
|
|
},
|
|
},
|
|
{
|
|
op: 'meta.merge',
|
|
meta: { agent: { model: 'k2', phase: { kind: 'ended', turnId: 1, reason: 'completed', durationMs: 1500, at: 2000 } } },
|
|
},
|
|
];
|
|
for (const op of ops) {
|
|
expect(transcriptOperationSchema.parse(op)).toEqual(op);
|
|
}
|
|
});
|
|
|
|
it('rejects mutually exclusive cursors and bad grades', () => {
|
|
expect(() => transcriptGradeSpecSchema.parse({ '*': 'stream' })).toThrow();
|
|
const ok = transcriptResponseSchema.safeParse({
|
|
agent_id: 'main',
|
|
items: [],
|
|
has_more: false,
|
|
tasks: [],
|
|
interactions: [],
|
|
attachments: [],
|
|
todos: [],
|
|
meta: {},
|
|
agents: [{ agentId: 'main', type: 'main' }],
|
|
pending_interactions: [],
|
|
});
|
|
expect(ok.success).toBe(true);
|
|
});
|
|
|
|
it('rejects path-hostile agent ids in the transcript query', () => {
|
|
const base = { agent_id: 'main', before_turn: undefined, after_turn: undefined, page_size: undefined };
|
|
expect(transcriptQuerySchema.safeParse({ ...base, agent_id: 'sub-1' }).success).toBe(true);
|
|
expect(transcriptQuerySchema.safeParse({ ...base, agent_id: '01HF7YAT31J7SMRT1QXGJWKR8D' }).success).toBe(true);
|
|
for (const hostile of ['../main', '..\\main', '..', 'a/b', 'a\\b', '.', 'a\0b', 'x'.repeat(200)]) {
|
|
expect(transcriptQuerySchema.safeParse({ ...base, agent_id: hostile }).success).toBe(false);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('groupMessagesIntoSnapshot (cold path)', () => {
|
|
const snapshotOf = (...parts: HistoryContentPart[]): AgentTranscriptSnapshot =>
|
|
groupMessagesIntoSnapshot([
|
|
{ role: 'user', content: parts, toolCalls: [], origin: { kind: 'user' } },
|
|
]);
|
|
|
|
it('groups flat messages into turns with folded tool results', () => {
|
|
const snapshot = groupMessagesIntoSnapshot([
|
|
{ role: 'system', content: [{ type: 'text', text: 'sys' }] },
|
|
{ role: 'user', content: [{ type: 'text', text: 'hello' }], toolCalls: [], origin: { kind: 'user' } },
|
|
{
|
|
role: 'assistant',
|
|
content: [{ type: 'think', think: 'hmm' }, { type: 'text', text: 'checking' }],
|
|
toolCalls: [{ id: 'c1', name: 'Read', arguments: '{"path":"/a"}' }],
|
|
},
|
|
{ role: 'tool', content: [{ type: 'text', text: 'file body' }], toolCallId: 'c1', toolCalls: [] },
|
|
{
|
|
role: 'assistant',
|
|
content: [{ type: 'text', text: 'done' }],
|
|
toolCalls: [],
|
|
},
|
|
{ role: 'user', content: [{ type: 'text', text: 'next' }], toolCalls: [], origin: { kind: 'user' } },
|
|
{
|
|
role: 'user',
|
|
content: [{ type: 'text', text: 'summary of old' }],
|
|
toolCalls: [],
|
|
origin: { kind: 'compaction_summary' },
|
|
},
|
|
{ role: 'user', content: [{ type: 'text', text: 'after' }], toolCalls: [], origin: { kind: 'user' } },
|
|
]);
|
|
|
|
const kinds = snapshot.items.map((i) => i.kind);
|
|
expect(kinds).toEqual(['turn', 'turn', 'marker', 'turn']);
|
|
const firstTurn = snapshot.items[0];
|
|
if (firstTurn?.kind !== 'turn') throw new Error('expected turn');
|
|
expect(firstTurn.prompt).toBe('hello');
|
|
expect(firstTurn.steps).toHaveLength(2);
|
|
const tool = firstTurn.steps[0]?.frames.find((f) => f.kind === 'tool');
|
|
expect(tool?.kind === 'tool' && tool.output).toBe('file body');
|
|
expect(tool?.kind === 'tool' && tool.input).toEqual({ path: '/a' });
|
|
const marker = snapshot.items[2];
|
|
expect(marker?.kind === 'marker' && marker.marker).toBe('compaction');
|
|
});
|
|
|
|
it('folds task-notification user messages into the current turn instead of opening their own', () => {
|
|
const snapshot = groupMessagesIntoSnapshot(
|
|
[
|
|
{ role: 'user', content: [{ type: 'text', text: 'run it' }], toolCalls: [], origin: { kind: 'user' } },
|
|
{
|
|
role: 'assistant',
|
|
content: [{ type: 'text', text: 'starting' }],
|
|
toolCalls: [{ id: 'c1', name: 'Bash', arguments: '{"command":"ls"}' }],
|
|
},
|
|
{
|
|
role: 'user',
|
|
content: [{
|
|
type: 'text',
|
|
text: '<notification id="task:task-9:completed" category="task" type="task.completed" source_kind="background_task" source_id="task-9">\nTitle: Background agent completed\nSeverity: info\ninspect done.\n</notification>',
|
|
}],
|
|
toolCalls: [],
|
|
origin: { kind: 'task', taskId: 'task-9' } as { kind: string },
|
|
},
|
|
{
|
|
role: 'user',
|
|
content: [{ type: 'text', text: '<notification id="task:task-8:completed"></notification>' }],
|
|
toolCalls: [],
|
|
origin: { kind: 'task', taskId: 'task-8' } as { kind: string },
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
content: [{ type: 'text', text: 'continuing' }],
|
|
toolCalls: [],
|
|
},
|
|
],
|
|
{ taskOriginTurnTaskIds: new Set() },
|
|
);
|
|
|
|
const turns = snapshot.items.filter((i) => i.kind === 'turn');
|
|
expect(turns).toHaveLength(1);
|
|
const turn = turns[0];
|
|
if (turn?.kind !== 'turn') throw new Error('expected turn');
|
|
const userFrames = turn.steps
|
|
.flatMap((step) => step.frames)
|
|
.filter((f) => f.kind === 'text' && f.role === 'user');
|
|
expect(userFrames.map((f) => f.kind === 'text' && f.taskId)).toEqual(['task-9', 'task-8']);
|
|
expect(userFrames[0]).toMatchObject({ text: 'Background agent completed\ninspect done.' });
|
|
const assistantTexts = turn.steps
|
|
.flatMap((step) => step.frames)
|
|
.filter((f) => f.kind === 'text' && f.role === 'assistant')
|
|
.map((f) => f.kind === 'text' && f.text);
|
|
expect(assistantTexts).toEqual(['starting', 'continuing']);
|
|
expect(turn.steps).toHaveLength(2);
|
|
expect(turn.steps[1]?.frames.map((f) => f.kind === 'text' && f.role)).toEqual([
|
|
'user',
|
|
'user',
|
|
'assistant',
|
|
]);
|
|
});
|
|
|
|
it('stops folded notification text before child output blocks', () => {
|
|
const xml = [
|
|
'<notification id="task:task-9:completed" category="task" type="task.completed" source_kind="background_task" source_id="task-9">',
|
|
'Title: Background agent completed',
|
|
'Severity: info',
|
|
'inspect done.',
|
|
'<output-file>/tmp/out.log</output-file>',
|
|
'/tmp/out.log',
|
|
'full output here',
|
|
'</notification>',
|
|
].join('\n');
|
|
const snapshot = groupMessagesIntoSnapshot(
|
|
[
|
|
{ role: 'user', content: [{ type: 'text', text: 'run' }], toolCalls: [], origin: { kind: 'user' } },
|
|
{ role: 'assistant', content: [{ type: 'text', text: 'go' }], toolCalls: [] },
|
|
{ role: 'user', content: [{ type: 'text', text: xml }], toolCalls: [], origin: { kind: 'task', taskId: 'task-9' } as { kind: string } },
|
|
{ role: 'assistant', content: [{ type: 'text', text: 'done' }], toolCalls: [] },
|
|
],
|
|
{ taskOriginTurnTaskIds: new Set() },
|
|
);
|
|
const turn = snapshot.items[0];
|
|
if (turn?.kind !== 'turn') throw new Error('expected turn');
|
|
const frame = turn.steps.flatMap((step) => step.frames).find((f) => f.kind === 'text' && f.role === 'user');
|
|
expect(frame).toMatchObject({ text: 'Background agent completed\ninspect done.' });
|
|
});
|
|
|
|
it('buffers a folded notification that arrives before the first step into that step', () => {
|
|
const xml = [
|
|
'<notification id="task:task-9:completed" category="task" type="task.completed" source_kind="background_task" source_id="task-9">',
|
|
'Title: Background agent completed',
|
|
'Severity: info',
|
|
'early done.',
|
|
'</notification>',
|
|
].join('\n');
|
|
const snapshot = groupMessagesIntoSnapshot(
|
|
[
|
|
{ role: 'user', content: [{ type: 'text', text: 'run' }], toolCalls: [], origin: { kind: 'user' } },
|
|
{ role: 'user', content: [{ type: 'text', text: xml }], toolCalls: [], origin: { kind: 'task', taskId: 'task-9' } as { kind: string } },
|
|
{ role: 'assistant', content: [{ type: 'text', text: 'go' }], toolCalls: [] },
|
|
],
|
|
{ taskOriginTurnTaskIds: new Set() },
|
|
);
|
|
const turn = snapshot.items[0];
|
|
if (turn?.kind !== 'turn') throw new Error('expected turn');
|
|
expect(turn.steps).toHaveLength(1);
|
|
expect(turn.steps[0]?.frames.map((f) => f.kind === 'text' && f.role)).toEqual([
|
|
'user',
|
|
'assistant',
|
|
]);
|
|
expect(turn.steps[0]?.frames[0]).toMatchObject({
|
|
text: 'Background agent completed\nearly done.',
|
|
taskId: 'task-9',
|
|
});
|
|
});
|
|
|
|
it('drops a folded notification when no turn is open yet', () => {
|
|
const snapshot = groupMessagesIntoSnapshot(
|
|
[
|
|
{
|
|
role: 'user',
|
|
content: [{ type: 'text', text: '<notification id="n1"></notification>' }],
|
|
toolCalls: [],
|
|
origin: { kind: 'task', taskId: 'task-9' } as { kind: string },
|
|
},
|
|
],
|
|
{ taskOriginTurnTaskIds: new Set() },
|
|
);
|
|
expect(snapshot.items).toHaveLength(0);
|
|
});
|
|
|
|
it('keeps body lines that start with an angle bracket but are not output blocks', () => {
|
|
const xml = [
|
|
'<notification id="task:task-9:completed" category="task" type="task.completed" source_kind="background_task" source_id="task-9">',
|
|
'Title: Background agent completed',
|
|
'Severity: info',
|
|
'first line.',
|
|
'<div>pasted markup</div>',
|
|
'last line.',
|
|
'</notification>',
|
|
].join('\n');
|
|
const snapshot = groupMessagesIntoSnapshot(
|
|
[
|
|
{ role: 'user', content: [{ type: 'text', text: 'run' }], toolCalls: [], origin: { kind: 'user' } },
|
|
{ role: 'assistant', content: [{ type: 'text', text: 'go' }], toolCalls: [] },
|
|
{ role: 'user', content: [{ type: 'text', text: xml }], toolCalls: [], origin: { kind: 'task', taskId: 'task-9' } as { kind: string } },
|
|
{ role: 'assistant', content: [{ type: 'text', text: 'done' }], toolCalls: [] },
|
|
],
|
|
{ taskOriginTurnTaskIds: new Set() },
|
|
);
|
|
const turn = snapshot.items[0];
|
|
if (turn?.kind !== 'turn') throw new Error('expected turn');
|
|
const frame = turn.steps.flatMap((step) => step.frames).find((f) => f.kind === 'text' && f.role === 'user');
|
|
expect(frame).toMatchObject({
|
|
text: 'Background agent completed\nfirst line.\n<div>pasted markup</div>\nlast line.',
|
|
});
|
|
});
|
|
|
|
it('parses only the leading header lines, keeping later Title-like lines as body', () => {
|
|
const xml = [
|
|
'<notification id="task:task-9:completed" category="task" type="task.completed" source_kind="background_task" source_id="task-9">',
|
|
'Title: Background agent completed',
|
|
'Severity: info',
|
|
'first line.',
|
|
'Title: details',
|
|
'last line.',
|
|
'</notification>',
|
|
].join('\n');
|
|
const snapshot = groupMessagesIntoSnapshot(
|
|
[
|
|
{ role: 'user', content: [{ type: 'text', text: 'run' }], toolCalls: [], origin: { kind: 'user' } },
|
|
{ role: 'assistant', content: [{ type: 'text', text: 'go' }], toolCalls: [] },
|
|
{ role: 'user', content: [{ type: 'text', text: xml }], toolCalls: [], origin: { kind: 'task', taskId: 'task-9' } as { kind: string } },
|
|
{ role: 'assistant', content: [{ type: 'text', text: 'done' }], toolCalls: [] },
|
|
],
|
|
{ taskOriginTurnTaskIds: new Set() },
|
|
);
|
|
const turn = snapshot.items[0];
|
|
if (turn?.kind !== 'turn') throw new Error('expected turn');
|
|
const frame = turn.steps.flatMap((step) => step.frames).find((f) => f.kind === 'text' && f.role === 'user');
|
|
expect(frame).toMatchObject({
|
|
text: 'Background agent completed\nfirst line.\nTitle: details\nlast line.',
|
|
});
|
|
});
|
|
|
|
it('expands a bundled prompt into per-skill markers and a caller-text turn', () => {
|
|
const snapshot = groupMessagesIntoSnapshot([
|
|
{
|
|
role: 'user',
|
|
content: [
|
|
{ type: 'text', text: 'rendered review block' },
|
|
{ type: 'text', text: 'rendered security block' },
|
|
{ type: 'text', text: 'please /skill:review and /skill:security' },
|
|
],
|
|
toolCalls: [],
|
|
origin: {
|
|
kind: 'user',
|
|
skillActivations: [
|
|
{ activationId: 'act-1', skillName: 'review' },
|
|
{ activationId: 'act-2', skillName: 'security', skillArgs: 'src/app.ts' },
|
|
],
|
|
} as { kind: string },
|
|
},
|
|
{ role: 'assistant', content: [{ type: 'text', text: 'done' }], toolCalls: [] },
|
|
]);
|
|
|
|
expect(snapshot.items.map((item) => item.kind)).toEqual(['marker', 'marker', 'turn']);
|
|
const first = snapshot.items[0];
|
|
expect(first?.kind === 'marker' && first.marker).toBe('skill');
|
|
expect(first?.kind === 'marker' && first.payload).toMatchObject({
|
|
text: 'rendered review block',
|
|
origin: { kind: 'skill_activation', trigger: 'user-slash', skillName: 'review' },
|
|
});
|
|
const second = snapshot.items[1];
|
|
expect(second?.kind === 'marker' && second.payload).toMatchObject({
|
|
text: 'rendered security block',
|
|
origin: { skillName: 'security', skillArgs: 'src/app.ts' },
|
|
});
|
|
const turn = snapshot.items[2];
|
|
if (turn?.kind !== 'turn') throw new Error('expected turn');
|
|
expect(turn.prompt).toBe('please /skill:review and /skill:security');
|
|
expect(turn.steps).toHaveLength(1);
|
|
});
|
|
|
|
it('maps media parts on the opening user message to attachment entities, dropping base64 bytes', () => {
|
|
const snapshot = groupMessagesIntoSnapshot([
|
|
{
|
|
role: 'user',
|
|
content: [
|
|
{ type: 'text', text: 'what is this? [Image #1]' },
|
|
{ type: 'image', source: { kind: 'base64', media_type: 'image/png', data: 'aGVsbG8=' } },
|
|
{ type: 'image', source: { kind: 'url', url: 'https://example.com/pic.png' } },
|
|
{ type: 'file', file_id: 'file_9', name: 'notes.txt', media_type: 'text/plain', size: 128 },
|
|
],
|
|
toolCalls: [],
|
|
origin: { kind: 'user' },
|
|
},
|
|
{ role: 'assistant', content: [{ type: 'text', text: 'a screenshot' }], toolCalls: [] },
|
|
]);
|
|
|
|
expect(snapshot.attachments).toHaveLength(3);
|
|
expect(snapshot.attachments[0]).toMatchObject({
|
|
attachmentId: 'att_1',
|
|
mediaType: 'image/png',
|
|
source: undefined,
|
|
});
|
|
expect(snapshot.attachments[1]).toMatchObject({
|
|
attachmentId: 'att_2',
|
|
source: { kind: 'url', url: 'https://example.com/pic.png' },
|
|
});
|
|
expect(snapshot.attachments[2]).toMatchObject({
|
|
attachmentId: 'att_3',
|
|
mediaType: 'text/plain',
|
|
name: 'notes.txt',
|
|
source: { kind: 'file', fileId: 'file_9' },
|
|
});
|
|
const firstTurn = snapshot.items[0];
|
|
if (firstTurn?.kind !== 'turn') throw new Error('expected turn');
|
|
expect(firstTurn.attachmentIds).toEqual(['att_1', 'att_2', 'att_3']);
|
|
});
|
|
|
|
it('maps persisted kimi-file media refs to attachments', () => {
|
|
const snapshot = groupMessagesIntoSnapshot([
|
|
{
|
|
role: 'user',
|
|
content: [
|
|
{
|
|
type: 'video_url',
|
|
videoUrl: { url: 'kimi-file://file_1' },
|
|
} as HistoryContentPart,
|
|
],
|
|
toolCalls: [],
|
|
origin: { kind: 'user' },
|
|
},
|
|
{
|
|
role: 'user',
|
|
content: [
|
|
{ type: 'text', text: 'what is this?' },
|
|
{
|
|
type: 'image_url',
|
|
imageUrl: { url: 'kimi-file://file_2?path=%2Fcache%2Fshot.png' },
|
|
} as HistoryContentPart,
|
|
],
|
|
toolCalls: [],
|
|
origin: { kind: 'user' },
|
|
},
|
|
]);
|
|
|
|
expect(snapshot.attachments).toEqual([
|
|
{
|
|
attachmentId: 'att_1',
|
|
mediaType: 'video/*',
|
|
source: { kind: 'session_media', fileId: 'file_1' },
|
|
},
|
|
{
|
|
attachmentId: 'att_2',
|
|
mediaType: 'image/*',
|
|
source: { kind: 'session_media', fileId: 'file_2' },
|
|
},
|
|
]);
|
|
const videoTurn = snapshot.items[0];
|
|
const imageTurn = snapshot.items[1];
|
|
if (videoTurn?.kind !== 'turn' || imageTurn?.kind !== 'turn') {
|
|
throw new Error('expected turns');
|
|
}
|
|
expect(videoTurn.attachmentIds).toEqual(['att_1']);
|
|
expect(imageTurn.attachmentIds).toEqual(['att_2']);
|
|
expect(videoTurn.prompt).toBe('');
|
|
expect(imageTurn.prompt).toBe('what is this?');
|
|
});
|
|
|
|
it('projects a daemon ref in a user-slash turn-opening input like a plain user turn', () => {
|
|
const snapshot = groupMessagesIntoSnapshot([
|
|
{
|
|
role: 'user',
|
|
content: [
|
|
{ type: 'text', text: '/gen-docs ' },
|
|
{
|
|
type: 'image_url',
|
|
imageUrl: { url: 'kimi-file://file_4?path=%2Fcache%2Fshot.png' },
|
|
} as HistoryContentPart,
|
|
],
|
|
toolCalls: [],
|
|
origin: { kind: 'skill_activation', trigger: 'user-slash', skillName: 'gen-docs' } as {
|
|
kind: string;
|
|
},
|
|
},
|
|
]);
|
|
|
|
expect(snapshot.attachments).toEqual([
|
|
{
|
|
attachmentId: 'att_1',
|
|
mediaType: 'image/*',
|
|
source: { kind: 'session_media', fileId: 'file_4' },
|
|
},
|
|
]);
|
|
expect(snapshot.items.map((item) => item.kind)).toEqual(['marker', 'turn']);
|
|
const marker = snapshot.items[0];
|
|
if (marker?.kind !== 'marker') throw new Error('expected marker');
|
|
expect((marker.payload as { text: string }).text).toBe('/gen-docs ');
|
|
const turn = snapshot.items[1];
|
|
if (turn?.kind !== 'turn') throw new Error('expected turn');
|
|
expect(turn.prompt).toBe('/gen-docs ');
|
|
expect(turn.attachmentIds).toEqual(['att_1']);
|
|
});
|
|
|
|
it('keeps a kimi-file ref as a nameless attachment and inline tags as user text', () => {
|
|
const snapshot = snapshotOf(
|
|
{ type: 'text', text: 'open <image path="/tmp/other.png"></image> please' },
|
|
{
|
|
type: 'image_url',
|
|
imageUrl: { url: 'kimi-file://file_3' },
|
|
} as HistoryContentPart,
|
|
);
|
|
|
|
expect(snapshot.attachments).toEqual([
|
|
{
|
|
attachmentId: 'att_1',
|
|
mediaType: 'image/*',
|
|
source: { kind: 'session_media', fileId: 'file_3' },
|
|
},
|
|
]);
|
|
const turn = snapshot.items[0];
|
|
if (turn?.kind !== 'turn') throw new Error('expected turn');
|
|
expect(turn.attachmentIds).toEqual(['att_1']);
|
|
expect(turn.prompt).toBe('open <image path="/tmp/other.png"></image> please');
|
|
});
|
|
|
|
it('keeps a legacy tag+ref pair as prompt text plus the ref-derived attachment', () => {
|
|
const snapshot = snapshotOf(
|
|
{ type: 'text', text: '<image path="/cache/shot.png"></image>' },
|
|
{
|
|
type: 'image_url',
|
|
imageUrl: { url: 'kimi-file://file_5?path=%2Fcache%2Fshot.png' },
|
|
} as HistoryContentPart,
|
|
);
|
|
|
|
expect(snapshot.attachments).toEqual([
|
|
{
|
|
attachmentId: 'att_1',
|
|
mediaType: 'image/*',
|
|
source: { kind: 'session_media', fileId: 'file_5' },
|
|
},
|
|
]);
|
|
const turn = snapshot.items[0];
|
|
if (turn?.kind !== 'turn') throw new Error('expected turn');
|
|
expect(turn.prompt).toBe('<image path="/cache/shot.png"></image>');
|
|
expect(turn.attachmentIds).toEqual(['att_1']);
|
|
});
|
|
|
|
it('keeps standalone <media path> tags as prompt text', () => {
|
|
const snapshot = snapshotOf(
|
|
{ type: 'text', text: '<image path="/cache/shot.png">' },
|
|
{ type: 'text', text: '<image path="/cache/shot.png" content_type="image/png"></image>' },
|
|
{ type: 'text', text: '<video path="/cache/clip.mp4">' },
|
|
);
|
|
|
|
const turn = snapshot.items[0];
|
|
if (turn?.kind !== 'turn') throw new Error('expected turn');
|
|
expect(turn.prompt).toBe(
|
|
'<image path="/cache/shot.png"><image path="/cache/shot.png" content_type="image/png"></image><video path="/cache/clip.mp4">',
|
|
);
|
|
expect(turn.attachmentIds).toBeUndefined();
|
|
expect(snapshot.attachments).toEqual([]);
|
|
});
|
|
|
|
it('keeps cold tool calls running until a result is persisted', () => {
|
|
const pending = groupMessagesIntoSnapshot([
|
|
{ role: 'user', content: [{ type: 'text', text: 'run it' }], toolCalls: [], origin: { kind: 'user' } },
|
|
{ role: 'assistant', content: [], toolCalls: [{ id: 'c1', name: 'Bash', arguments: '{}' }] },
|
|
]);
|
|
const pendingTurn = pending.items[0];
|
|
if (pendingTurn?.kind !== 'turn') throw new Error('expected turn');
|
|
const pendingTool = pendingTurn.steps[0]?.frames.find((f) => f.kind === 'tool');
|
|
expect(pendingTool?.kind === 'tool' && pendingTool.state).toBe('running');
|
|
|
|
const done = groupMessagesIntoSnapshot([
|
|
{ role: 'user', content: [{ type: 'text', text: 'run it' }], toolCalls: [], origin: { kind: 'user' } },
|
|
{ role: 'assistant', content: [], toolCalls: [{ id: 'c1', name: 'Bash', arguments: '{}' }] },
|
|
{ role: 'tool', content: [{ type: 'text', text: 'done.txt' }], toolCallId: 'c1', toolCalls: [] },
|
|
]);
|
|
const doneTurn = done.items[0];
|
|
if (doneTurn?.kind !== 'turn') throw new Error('expected turn');
|
|
const doneTool = doneTurn.steps[0]?.frames.find((f) => f.kind === 'tool');
|
|
expect(doneTool?.kind === 'tool' && doneTool.state).toBe('done');
|
|
expect(doneTool?.kind === 'tool' && doneTool.output).toBe('done.txt');
|
|
});
|
|
|
|
it('opens a turn for subagent run prompts recorded as system triggers', () => {
|
|
const snapshot = groupMessagesIntoSnapshot([
|
|
{ role: 'user', content: [{ type: 'text', text: 'hi' }], toolCalls: [], origin: { kind: 'user' } },
|
|
{ role: 'assistant', content: [{ type: 'text', text: 'answer' }], toolCalls: [] },
|
|
{
|
|
role: 'user',
|
|
content: [{ type: 'text', text: 'scan the repo' }],
|
|
toolCalls: [],
|
|
origin: { kind: 'system_trigger', name: 'subagent' } as { kind: string },
|
|
},
|
|
{ role: 'assistant', content: [{ type: 'text', text: 'scanning' }], toolCalls: [] },
|
|
]);
|
|
|
|
expect(snapshot.items.map((item) => item.kind)).toEqual(['turn', 'turn']);
|
|
const subTurn = snapshot.items[1];
|
|
if (subTurn?.kind !== 'turn') throw new Error('expected turn');
|
|
expect(subTurn.ordinal).toBe(1);
|
|
expect(subTurn.origin.kind).toBe('other');
|
|
expect(subTurn.prompt).toBeUndefined();
|
|
expect(subTurn.steps).toHaveLength(1);
|
|
});
|
|
|
|
it('starts a new turn for user-slash skill activations, keeps other triggers as markers only', () => {
|
|
const snapshot = groupMessagesIntoSnapshot([
|
|
{ role: 'user', content: [{ type: 'text', text: 'hi' }], toolCalls: [], origin: { kind: 'user' } },
|
|
{ role: 'assistant', content: [{ type: 'text', text: 'answer' }], toolCalls: [] },
|
|
{
|
|
role: 'user',
|
|
content: [{ type: 'text', text: 'skill body' }],
|
|
toolCalls: [],
|
|
origin: { kind: 'skill_activation', trigger: 'user-slash', skillName: 'gen-docs' } as {
|
|
kind: string;
|
|
},
|
|
},
|
|
{ role: 'assistant', content: [{ type: 'text', text: 'docs done' }], toolCalls: [] },
|
|
{
|
|
role: 'user',
|
|
content: [{ type: 'text', text: 'nested skill body' }],
|
|
toolCalls: [],
|
|
origin: { kind: 'skill_activation', trigger: 'model-tool', skillName: 'x' } as {
|
|
kind: string;
|
|
},
|
|
},
|
|
{ role: 'assistant', content: [{ type: 'text', text: 'still same turn' }], toolCalls: [] },
|
|
]);
|
|
|
|
expect(snapshot.items.map((item) => item.kind)).toEqual(['turn', 'marker', 'turn', 'marker']);
|
|
const slashTurn = snapshot.items[2];
|
|
if (slashTurn?.kind !== 'turn') throw new Error('expected turn');
|
|
expect(slashTurn.ordinal).toBe(1);
|
|
expect(slashTurn.origin.kind).toBe('other');
|
|
expect(slashTurn.prompt).toBe('skill body');
|
|
expect(slashTurn.steps).toHaveLength(2);
|
|
});
|
|
|
|
it('starts a promptless turn for turn-opening system triggers (goal continuation)', () => {
|
|
const snapshot = groupMessagesIntoSnapshot([
|
|
{ role: 'user', content: [{ type: 'text', text: 'hi' }], toolCalls: [], origin: { kind: 'user' } },
|
|
{ role: 'assistant', content: [{ type: 'text', text: 'answer' }], toolCalls: [] },
|
|
{
|
|
role: 'user',
|
|
content: [{ type: 'text', text: 'continue the goal' }],
|
|
toolCalls: [],
|
|
origin: { kind: 'system_trigger', name: 'goal_continuation' } as { kind: string },
|
|
},
|
|
{ role: 'assistant', content: [{ type: 'text', text: 'continued' }], toolCalls: [] },
|
|
{
|
|
role: 'user',
|
|
content: [{ type: 'text', text: 'mode reminder' }],
|
|
toolCalls: [],
|
|
origin: { kind: 'injection', variant: 'permission_mode' } as { kind: string },
|
|
},
|
|
{ role: 'assistant', content: [{ type: 'text', text: 'still same turn' }], toolCalls: [] },
|
|
]);
|
|
|
|
expect(snapshot.items.map((item) => item.kind)).toEqual(['turn', 'turn']);
|
|
const [first, second] = snapshot.items;
|
|
if (first?.kind !== 'turn' || second?.kind !== 'turn') throw new Error('expected turns');
|
|
expect(first.ordinal).toBe(0);
|
|
expect(first.steps.map((step) => step.frames.map((frame) => frame.kind))).toEqual([['text']]);
|
|
expect(second.ordinal).toBe(1);
|
|
expect(second.origin.kind).toBe('other');
|
|
expect(second.prompt).toBeUndefined();
|
|
expect(second.steps).toHaveLength(2);
|
|
});
|
|
|
|
it('hides injected user messages and maps cron origins', () => {
|
|
const snapshot = groupMessagesIntoSnapshot([
|
|
{
|
|
role: 'user',
|
|
content: [{ type: 'text', text: 'secret context' }],
|
|
toolCalls: [],
|
|
origin: { kind: 'injection' },
|
|
},
|
|
{ role: 'user', content: [{ type: 'text', text: 'hi' }], toolCalls: [], origin: { kind: 'user' } },
|
|
{
|
|
role: 'user',
|
|
content: [{ type: 'text', text: 'run report' }],
|
|
toolCalls: [],
|
|
origin: { kind: 'cron_job', jobId: 'job1' } as { kind: string },
|
|
},
|
|
]);
|
|
expect(snapshot.items).toHaveLength(2);
|
|
const cronTurn = snapshot.items[1];
|
|
expect(cronTurn?.kind === 'turn' && cronTurn.origin.kind).toBe('cron');
|
|
});
|
|
|
|
it('opens a task turn for a notification with a task-origin turn.started in the wire', () => {
|
|
const snapshot = groupMessagesIntoSnapshot(
|
|
[
|
|
{ role: 'user', content: [{ type: 'text', text: 'hi' }], toolCalls: [], origin: { kind: 'user' } },
|
|
{
|
|
role: 'assistant',
|
|
content: [{ type: 'text', text: 'answer' }],
|
|
toolCalls: [],
|
|
},
|
|
{
|
|
role: 'user',
|
|
content: [{ type: 'text', text: 'task done' }],
|
|
toolCalls: [],
|
|
origin: { kind: 'task', taskId: 'task-9' } as { kind: string },
|
|
},
|
|
],
|
|
{ taskOriginTurnTaskIds: new Set(['task-9']) },
|
|
);
|
|
|
|
const taskTurn = snapshot.items[1];
|
|
if (taskTurn?.kind !== 'turn') throw new Error('expected turn');
|
|
expect(taskTurn.origin).toMatchObject({ kind: 'task', taskId: 'task-9' });
|
|
});
|
|
|
|
it('maps legacy background_task origins to task turns, preserving the taskId', () => {
|
|
const snapshot = groupMessagesIntoSnapshot([
|
|
{ role: 'user', content: [{ type: 'text', text: 'hi' }], toolCalls: [], origin: { kind: 'user' } },
|
|
{
|
|
role: 'user',
|
|
content: [{ type: 'text', text: 'task done' }],
|
|
toolCalls: [],
|
|
origin: { kind: 'background_task', taskId: 'b83rhswvs' } as { kind: string },
|
|
},
|
|
]);
|
|
const taskTurn = snapshot.items[1];
|
|
if (taskTurn?.kind !== 'turn') throw new Error('expected turn');
|
|
expect(taskTurn.origin).toMatchObject({ kind: 'task', taskId: 'b83rhswvs' });
|
|
});
|
|
});
|
|
|
|
describe('foldWireRecordFacts (cold facts)', () => {
|
|
const baseWithMarker = (): AgentTranscriptSnapshot =>
|
|
groupMessagesIntoSnapshot([
|
|
{ role: 'user', content: [{ type: 'text', text: 'hi' }], toolCalls: [], origin: { kind: 'user' } },
|
|
{ role: 'assistant', content: [{ type: 'text', text: 'answer' }], toolCalls: [] },
|
|
{
|
|
role: 'user',
|
|
content: [{ type: 'text', text: 'summary of old' }],
|
|
toolCalls: [],
|
|
origin: { kind: 'compaction_summary' },
|
|
},
|
|
]);
|
|
|
|
it('returns the base snapshot unchanged when no fact records exist (old sessions)', () => {
|
|
const base = baseWithMarker();
|
|
const folded = foldWireRecordFacts(
|
|
[
|
|
{ type: 'metadata', protocol_version: '1.5', created_at: 1 },
|
|
{ type: 'context.append_message', message: { role: 'user' }, time: 1 },
|
|
],
|
|
base,
|
|
);
|
|
expect(folded).toEqual(base);
|
|
expect(folded.items).toBe(base.items);
|
|
});
|
|
|
|
it('folds todo records into the global todo document, last write wins', () => {
|
|
const base = baseWithMarker();
|
|
const folded = foldWireRecordFacts(
|
|
[
|
|
{ type: 'tools.update_store', key: 'todo', value: [{ title: 'old', status: 'pending' }], time: 1000 },
|
|
{ type: 'tools.update_store', key: 'other', value: [{ title: 'ignored', status: 'done' }], time: 2000 },
|
|
{
|
|
type: 'tools.update_store',
|
|
key: 'todo',
|
|
value: [
|
|
{ title: 'write tests', status: 'in_progress' },
|
|
{ title: 'ship', status: 'pending' },
|
|
{ title: 'malformed' },
|
|
],
|
|
time: 3000,
|
|
},
|
|
],
|
|
base,
|
|
);
|
|
expect(folded.todos).toEqual([
|
|
{
|
|
todoId: 'todo',
|
|
items: [
|
|
{ title: 'write tests', status: 'in_progress' },
|
|
{ title: 'ship', status: 'pending' },
|
|
],
|
|
updatedAt: new Date(3000).toISOString(),
|
|
},
|
|
]);
|
|
expect(folded.items).toBe(base.items);
|
|
});
|
|
|
|
it('folds goal create/update/clear into meta.goal with markers, last write wins', () => {
|
|
const base = baseWithMarker();
|
|
expect(base.items.some((item) => item.kind === 'marker' && item.markerId === 'm1')).toBe(true);
|
|
|
|
const folded = foldWireRecordFacts(
|
|
[
|
|
{
|
|
type: 'goal.create',
|
|
goalId: 'g1',
|
|
objective: 'fix the bug',
|
|
completionCriterion: 'tests pass',
|
|
time: 1000,
|
|
},
|
|
{ type: 'goal.update', status: 'blocked', reason: 'stuck', tokensUsed: 1200, time: 2000 },
|
|
{ type: 'goal.update', budgetLimits: { tokenBudget: 50000 }, time: 3000 },
|
|
],
|
|
base,
|
|
);
|
|
expect(folded.meta.goal).toEqual({
|
|
objective: 'fix the bug',
|
|
status: 'blocked',
|
|
completionCriterion: 'tests pass',
|
|
budgetUsed: 1200,
|
|
budgetLimit: 50000,
|
|
});
|
|
const goalMarkers = folded.items.filter(
|
|
(item) => item.kind === 'marker' && item.marker === 'goal',
|
|
);
|
|
expect(goalMarkers.map((item) => item.kind === 'marker' && item.markerId)).toEqual([
|
|
'm2',
|
|
'm3',
|
|
'm4',
|
|
]);
|
|
expect(goalMarkers[0]).toMatchObject({ at: new Date(1000).toISOString() });
|
|
expect(folded.items.slice(0, base.items.length)).toEqual(base.items);
|
|
|
|
const cleared = foldWireRecordFacts(
|
|
[
|
|
{ type: 'goal.create', goalId: 'g1', objective: 'fix the bug', time: 1000 },
|
|
{ type: 'goal.clear', time: 2000 },
|
|
],
|
|
base,
|
|
);
|
|
expect(cleared.meta.goal).toBeUndefined();
|
|
});
|
|
|
|
it('marks user-cancelled turns with interruption markers, skipping unattributable cancels', () => {
|
|
const base = baseWithMarker();
|
|
const folded = foldWireRecordFacts(
|
|
[
|
|
{ type: 'turn.cancel', turnId: 0, target: 'active', reason: 'user_cancelled', time: 1000 },
|
|
{ type: 'turn.cancel', turnId: 0, target: 'active', reason: 'user_cancelled', time: 1500 },
|
|
{ type: 'turn.cancel', turnId: 1, target: 'queued', reason: 'user_cancelled', time: 2000 },
|
|
{ type: 'turn.cancel', turnId: 2, target: 'active', reason: 'aborted', time: 3000 },
|
|
{ type: 'turn.cancel', turnId: 4, target: 'active', reason: 'aborted', time: 3500 },
|
|
{ type: 'turn.cancel', turnId: 4, target: 'active', reason: 'user_cancelled', time: 3600 },
|
|
{ type: 'turn.cancel', turnId: 3, target: 'active', time: 4000 },
|
|
],
|
|
base,
|
|
);
|
|
expect(
|
|
folded.items.filter((item) => item.kind === 'marker' && item.marker === 'interruption'),
|
|
).toEqual([
|
|
{
|
|
kind: 'marker',
|
|
markerId: 'm2',
|
|
marker: 'interruption',
|
|
payload: { turnId: 0, target: 'active', reason: 'user_cancelled' },
|
|
at: new Date(1000).toISOString(),
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('folds plan/swarm mode records into meta.modes with enter/exit markers', () => {
|
|
const base = baseWithMarker();
|
|
const folded = foldWireRecordFacts(
|
|
[
|
|
{ type: 'plan_mode.enter', id: 'plan-1', time: 1000 },
|
|
{ type: 'swarm_mode.enter', trigger: { kind: 'task' }, time: 2000 },
|
|
{ type: 'plan_mode.exit', id: 'plan-1', time: 3000 },
|
|
],
|
|
base,
|
|
);
|
|
expect(folded.meta.modes).toEqual({ swarm: {} });
|
|
const markers = folded.items
|
|
.filter((item) => item.kind === 'marker')
|
|
.map((item) => item.kind === 'marker' && item.marker);
|
|
expect(markers).toEqual(['compaction', 'plan.enter', 'swarm.enter', 'plan.exit']);
|
|
|
|
const cancelled = foldWireRecordFacts(
|
|
[
|
|
{ type: 'plan_mode.enter', id: 'plan-1', time: 1000 },
|
|
{ type: 'plan_mode.cancel', time: 2000 },
|
|
{ type: 'swarm_mode.enter', trigger: { kind: 'tool' }, time: 3000 },
|
|
{ type: 'swarm_mode.exit', time: 4000 },
|
|
],
|
|
base,
|
|
);
|
|
expect(cancelled.meta.modes).toEqual({});
|
|
|
|
const stillPlanning = foldWireRecordFacts(
|
|
[{ type: 'plan_mode.enter', id: 'plan-1', time: 1000 }],
|
|
base,
|
|
);
|
|
expect(stillPlanning.meta.modes).toEqual({ plan: {} });
|
|
});
|
|
|
|
it('folds plan.revision records into the plan badge and a timeline marker', () => {
|
|
const base = baseWithMarker();
|
|
const revision = {
|
|
type: 'plan.revision',
|
|
id: 'plan-1',
|
|
version: 2,
|
|
path: 'agents/main/plan/plan-1/v2.md',
|
|
sha256: 'deadbeef',
|
|
bytes: 512,
|
|
time: 2000,
|
|
};
|
|
const folded = foldWireRecordFacts(
|
|
[{ type: 'plan_mode.enter', id: 'plan-1', time: 1000 }, revision],
|
|
base,
|
|
);
|
|
expect(folded.meta.modes).toEqual({
|
|
plan: { reviewPath: 'agents/main/plan/plan-1/v2.md', version: 2 },
|
|
});
|
|
const revisionMarkers = folded.items.filter(
|
|
(item) => item.kind === 'marker' && item.marker === 'plan.revision',
|
|
);
|
|
expect(revisionMarkers).toEqual([
|
|
{
|
|
kind: 'marker',
|
|
markerId: 'm3',
|
|
marker: 'plan.revision',
|
|
payload: {
|
|
id: 'plan-1',
|
|
version: 2,
|
|
path: 'agents/main/plan/plan-1/v2.md',
|
|
sha256: 'deadbeef',
|
|
bytes: 512,
|
|
},
|
|
at: new Date(2000).toISOString(),
|
|
},
|
|
]);
|
|
|
|
const exited = foldWireRecordFacts(
|
|
[
|
|
{ type: 'plan_mode.enter', id: 'plan-1', time: 1000 },
|
|
revision,
|
|
{ type: 'plan_mode.exit', id: 'plan-1', time: 3000 },
|
|
],
|
|
base,
|
|
);
|
|
expect(exited.meta.modes?.plan).toBeUndefined();
|
|
expect(
|
|
exited.items.filter((item) => item.kind === 'marker' && item.marker === 'plan.revision'),
|
|
).toHaveLength(1);
|
|
|
|
const reentered = foldWireRecordFacts(
|
|
[
|
|
{ type: 'plan_mode.enter', id: 'plan-1', time: 1000 },
|
|
revision,
|
|
{ type: 'plan_mode.exit', id: 'plan-1', time: 3000 },
|
|
{ type: 'plan_mode.enter', id: 'plan-2', time: 4000 },
|
|
],
|
|
base,
|
|
);
|
|
expect(reentered.meta.modes).toEqual({ plan: {} });
|
|
});
|
|
|
|
it('folds task records into task entities and timeline taskrefs', () => {
|
|
const base = baseWithMarker();
|
|
const folded = foldWireRecordFacts(
|
|
[
|
|
{
|
|
type: 'task.started',
|
|
info: {
|
|
taskId: 'task_1',
|
|
kind: 'process',
|
|
description: 'pnpm test',
|
|
status: 'running',
|
|
startedAt: 1000,
|
|
endedAt: null,
|
|
},
|
|
time: 1000,
|
|
},
|
|
{
|
|
type: 'task.started',
|
|
info: {
|
|
taskId: 'task_2',
|
|
kind: 'agent',
|
|
description: 'scan the repo',
|
|
status: 'running',
|
|
detached: false,
|
|
agentId: 'sub-1',
|
|
startedAt: 2000,
|
|
endedAt: null,
|
|
},
|
|
time: 2000,
|
|
},
|
|
{
|
|
type: 'task.terminated',
|
|
info: {
|
|
taskId: 'task_1',
|
|
kind: 'process',
|
|
description: 'pnpm test',
|
|
status: 'completed',
|
|
startedAt: 1000,
|
|
endedAt: 5000,
|
|
},
|
|
outputTail: '42 passed',
|
|
time: 5000,
|
|
},
|
|
],
|
|
base,
|
|
);
|
|
|
|
expect(folded.tasks).toEqual([
|
|
{
|
|
taskId: 'task_1',
|
|
kind: 'shell',
|
|
state: 'completed',
|
|
detached: true,
|
|
description: 'pnpm test',
|
|
agentId: undefined,
|
|
outputTail: '42 passed',
|
|
startedAt: new Date(1000).toISOString(),
|
|
endedAt: new Date(5000).toISOString(),
|
|
},
|
|
{
|
|
taskId: 'task_2',
|
|
kind: 'subagent',
|
|
state: 'running',
|
|
detached: false,
|
|
description: 'scan the repo',
|
|
agentId: 'sub-1',
|
|
outputTail: '',
|
|
startedAt: new Date(2000).toISOString(),
|
|
endedAt: undefined,
|
|
},
|
|
]);
|
|
const refs = folded.items.filter((item) => item.kind === 'taskref');
|
|
expect(refs).toEqual([
|
|
{ kind: 'taskref', refId: 'ref-task_1', taskId: 'task_1', at: new Date(1000).toISOString() },
|
|
{ kind: 'taskref', refId: 'ref-task_2', taskId: 'task_2', at: new Date(2000).toISOString() },
|
|
]);
|
|
});
|
|
|
|
it('maps unknown task kinds to other and survives malformed task records', () => {
|
|
const base = baseWithMarker();
|
|
const folded = foldWireRecordFacts(
|
|
[
|
|
{
|
|
type: 'task.started',
|
|
info: { taskId: 'task_q', kind: 'question', status: 'running', startedAt: 1000, endedAt: null },
|
|
time: 1000,
|
|
},
|
|
{ type: 'task.started', time: 2000 },
|
|
{ type: 'task.terminated', info: { kind: 'process' }, time: 3000 },
|
|
],
|
|
base,
|
|
);
|
|
expect(folded.tasks).toHaveLength(1);
|
|
expect(folded.tasks[0]).toMatchObject({ taskId: 'task_q', kind: 'other', state: 'running' });
|
|
});
|
|
|
|
it('folds interaction request/resolved into entities with terminal states', () => {
|
|
const base = baseWithMarker();
|
|
const folded = foldWireRecordFacts(
|
|
[
|
|
{
|
|
type: 'interaction.request',
|
|
id: 'apr-1',
|
|
kind: 'approval',
|
|
toolCallId: 'call_1',
|
|
request: { toolName: 'Bash' },
|
|
time: 1000,
|
|
},
|
|
{
|
|
type: 'interaction.request',
|
|
id: 'q-1',
|
|
kind: 'question',
|
|
request: { questions: [] },
|
|
time: 2000,
|
|
},
|
|
{
|
|
type: 'interaction.resolved',
|
|
id: 'apr-1',
|
|
response: { decision: 'approved', scope: 'session' },
|
|
time: 3000,
|
|
},
|
|
{ type: 'interaction.resolved', id: 'q-1', response: null, time: 4000 },
|
|
],
|
|
base,
|
|
);
|
|
expect(folded.interactions).toEqual([
|
|
{
|
|
interactionId: 'apr-1',
|
|
interactionKind: 'approval',
|
|
toolCallId: 'call_1',
|
|
state: 'approved',
|
|
request: { toolName: 'Bash' },
|
|
response: { decision: 'approved', scope: 'session' },
|
|
},
|
|
{
|
|
interactionId: 'q-1',
|
|
interactionKind: 'question',
|
|
toolCallId: undefined,
|
|
state: 'dismissed',
|
|
request: { questions: [] },
|
|
response: null,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('cancels interactions still pending at the end of the scan (crash == cancelled)', () => {
|
|
const base = baseWithMarker();
|
|
const folded = foldWireRecordFacts(
|
|
[
|
|
{
|
|
type: 'interaction.request',
|
|
id: 'apr-9',
|
|
kind: 'approval',
|
|
request: { toolCallId: 'call_9', toolName: 'Write' },
|
|
time: 1000,
|
|
},
|
|
],
|
|
base,
|
|
);
|
|
expect(folded.interactions).toHaveLength(1);
|
|
expect(folded.interactions[0]).toMatchObject({
|
|
interactionId: 'apr-9',
|
|
state: 'cancelled',
|
|
toolCallId: 'call_9',
|
|
});
|
|
expect(folded.interactions.every((entity) => entity.state !== 'pending')).toBe(true);
|
|
});
|
|
|
|
it('skips user_tool interactions like the live path', () => {
|
|
const base = baseWithMarker();
|
|
const folded = foldWireRecordFacts(
|
|
[
|
|
{ type: 'interaction.request', id: 'ut-1', kind: 'user_tool', request: {}, time: 1000 },
|
|
{ type: 'interaction.resolved', id: 'ut-1', response: {}, time: 2000 },
|
|
] satisfies HistoryWireRecord[],
|
|
base,
|
|
);
|
|
expect(folded.interactions).toEqual([]);
|
|
});
|
|
|
|
it('folds turn.ended records into the matching turn items', () => {
|
|
const base = baseWithMarker();
|
|
const folded = foldWireRecordFacts(
|
|
[
|
|
{
|
|
type: 'turn.ended',
|
|
turnId: 0,
|
|
reason: 'failed',
|
|
error: { code: 'provider.overloaded', message: 'Overloaded', name: 'APIStatusError', retryable: true },
|
|
durationMs: 1234,
|
|
time: 5000,
|
|
},
|
|
],
|
|
base,
|
|
);
|
|
const turn = folded.items[0];
|
|
if (turn?.kind !== 'turn') throw new Error('expected turn');
|
|
expect(turn.state).toBe('failed');
|
|
expect(turn.error).toBe('Overloaded');
|
|
expect(turn.durationMs).toBe(1234);
|
|
expect(turn.endedAt).toBe(new Date(5000).toISOString());
|
|
expect(folded.items).toHaveLength(base.items.length);
|
|
});
|
|
|
|
it('applies turn.ended records last-wins per turn and folds blocked into failed', () => {
|
|
const base = baseWithMarker();
|
|
const folded = foldWireRecordFacts(
|
|
[
|
|
{ type: 'turn.ended', turnId: 0, reason: 'failed', error: { message: 'boom' }, time: 1000 },
|
|
{ type: 'turn.ended', turnId: 0, reason: 'cancelled', durationMs: 10, time: 2000 },
|
|
],
|
|
base,
|
|
);
|
|
const turn = folded.items[0];
|
|
if (turn?.kind !== 'turn') throw new Error('expected turn');
|
|
expect(turn.state).toBe('cancelled');
|
|
expect(turn.error).toBeUndefined();
|
|
expect(turn.durationMs).toBe(10);
|
|
expect(turn.endedAt).toBe(new Date(2000).toISOString());
|
|
|
|
const blocked = foldWireRecordFacts(
|
|
[{ type: 'turn.ended', turnId: 0, reason: 'blocked', time: 3000 }],
|
|
base,
|
|
);
|
|
const blockedTurn = blocked.items[0];
|
|
if (blockedTurn?.kind !== 'turn') throw new Error('expected turn');
|
|
expect(blockedTurn.state).toBe('failed');
|
|
});
|
|
|
|
it('ignores turn.ended records with unknown turn ids or malformed payloads', () => {
|
|
const base = baseWithMarker();
|
|
const folded = foldWireRecordFacts(
|
|
[
|
|
{ type: 'turn.ended', turnId: 9, reason: 'cancelled', time: 1000 },
|
|
{ type: 'turn.ended', reason: 'completed', time: 2000 },
|
|
{ type: 'turn.ended', turnId: 0, reason: 'not-a-reason', time: 3000 },
|
|
],
|
|
base,
|
|
);
|
|
const turn = folded.items[0];
|
|
if (turn?.kind !== 'turn') throw new Error('expected turn');
|
|
expect(turn.state).toBe('completed');
|
|
expect(turn.endedAt).toBe(new Date(3000).toISOString());
|
|
expect(folded.items).toHaveLength(base.items.length);
|
|
});
|
|
|
|
it('maps turn.ended around hidden retry turns replayed from the turn-clock records', () => {
|
|
const base = groupMessagesIntoSnapshot([
|
|
{ role: 'user', content: [{ type: 'text', text: 'one' }], toolCalls: [], origin: { kind: 'user' } },
|
|
{ role: 'assistant', content: [{ type: 'text', text: 'a1' }], toolCalls: [] },
|
|
{ role: 'user', content: [{ type: 'text', text: 'two' }], toolCalls: [], origin: { kind: 'user' } },
|
|
{ role: 'assistant', content: [{ type: 'text', text: 'a2' }], toolCalls: [] },
|
|
]);
|
|
const folded = foldWireRecordFacts(
|
|
[
|
|
{ type: 'turn.prompt', input: [{ type: 'text', text: 'one' }], origin: { kind: 'user' }, time: 1 },
|
|
{ type: 'turn.ended', turnId: 0, reason: 'completed', time: 2 },
|
|
{ type: 'turn.prompt', input: [], origin: { kind: 'retry' }, time: 3 },
|
|
{ type: 'turn.ended', turnId: 1, reason: 'failed', error: { message: 'retry boom' }, time: 4 },
|
|
{ type: 'turn.prompt', input: [{ type: 'text', text: 'two' }], origin: { kind: 'user' }, time: 5 },
|
|
{ type: 'turn.ended', turnId: 2, reason: 'cancelled', durationMs: 20, time: 6 },
|
|
],
|
|
base,
|
|
);
|
|
const first = folded.items[0];
|
|
if (first?.kind !== 'turn') throw new Error('expected turn');
|
|
expect(first.state).toBe('completed');
|
|
const second = folded.items[1];
|
|
if (second?.kind !== 'turn') throw new Error('expected turn');
|
|
expect(second.state).toBe('cancelled');
|
|
expect(second.error).toBeUndefined();
|
|
expect(second.durationMs).toBe(20);
|
|
});
|
|
|
|
it('maps turn.ended across queued-then-cancelled turn reservations', () => {
|
|
const base = groupMessagesIntoSnapshot([
|
|
{ role: 'user', content: [{ type: 'text', text: 'one' }], toolCalls: [], origin: { kind: 'user' } },
|
|
{ role: 'assistant', content: [{ type: 'text', text: 'a1' }], toolCalls: [] },
|
|
{ role: 'user', content: [{ type: 'text', text: 'two' }], toolCalls: [], origin: { kind: 'user' } },
|
|
{ role: 'assistant', content: [{ type: 'text', text: 'a2' }], toolCalls: [] },
|
|
]);
|
|
const folded = foldWireRecordFacts(
|
|
[
|
|
{ type: 'turn.prompt', input: [{ type: 'text', text: 'one' }], origin: { kind: 'user' }, time: 1 },
|
|
{ type: 'turn.ended', turnId: 0, reason: 'completed', time: 2 },
|
|
{ type: 'turn.cancel', turnId: 1, target: 'queued', time: 3 },
|
|
{ type: 'turn.prompt', input: [{ type: 'text', text: 'two' }], origin: { kind: 'user' }, time: 4 },
|
|
{ type: 'turn.ended', turnId: 2, reason: 'failed', error: { message: 'boom' }, time: 5 },
|
|
],
|
|
base,
|
|
);
|
|
const second = folded.items[1];
|
|
if (second?.kind !== 'turn') throw new Error('expected turn');
|
|
expect(second.state).toBe('failed');
|
|
expect(second.error).toBe('boom');
|
|
});
|
|
});
|