mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-30 04:22:51 +00:00
fix(kap-server): include stable question and option ids in transcript question entities (#3272)
This commit is contained in:
parent
75c94c4a0e
commit
b3f08b68c8
8 changed files with 364 additions and 110 deletions
54
packages/kap-server/src/protocol/question-wire.ts
Normal file
54
packages/kap-server/src/protocol/question-wire.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import type {
|
||||
QuestionItem,
|
||||
QuestionOption,
|
||||
QuestionRequest,
|
||||
} from '@moonshot-ai/agent-core-v2';
|
||||
|
||||
import type {
|
||||
QuestionItem as ProtocolQuestionItem,
|
||||
QuestionOption as ProtocolQuestionOption,
|
||||
QuestionRequest as ProtocolQuestionRequest,
|
||||
} from './question';
|
||||
|
||||
export interface WireQuestionSource {
|
||||
readonly id: string;
|
||||
readonly createdAt: number;
|
||||
readonly payload: unknown;
|
||||
}
|
||||
|
||||
function buildOption(opt: QuestionOption, itemIdx: number, optIdx: number): ProtocolQuestionOption {
|
||||
const base: ProtocolQuestionOption = { id: `opt_${itemIdx}_${optIdx}`, label: opt.label };
|
||||
return opt.description === undefined ? base : { ...base, description: opt.description };
|
||||
}
|
||||
|
||||
function buildItem(item: QuestionItem, itemIdx: number): ProtocolQuestionItem {
|
||||
const out: ProtocolQuestionItem = {
|
||||
id: `q_${itemIdx}`,
|
||||
question: item.question,
|
||||
options: item.options.map((o, oi) => buildOption(o, itemIdx, oi)),
|
||||
};
|
||||
if (item.header !== undefined) out.header = item.header;
|
||||
if (item.body !== undefined) out.body = item.body;
|
||||
if (item.multiSelect !== undefined) out.multi_select = item.multiSelect;
|
||||
out.allow_other = true;
|
||||
if (item.otherLabel !== undefined) out.other_label = item.otherLabel;
|
||||
if (item.otherDescription !== undefined) out.other_description = item.otherDescription;
|
||||
return out;
|
||||
}
|
||||
|
||||
export function toWireQuestion(
|
||||
interaction: WireQuestionSource,
|
||||
sessionId: string,
|
||||
): ProtocolQuestionRequest {
|
||||
const req = interaction.payload as QuestionRequest;
|
||||
const createdAt = new Date(interaction.createdAt).toISOString();
|
||||
const out: ProtocolQuestionRequest = {
|
||||
question_id: interaction.id,
|
||||
session_id: sessionId,
|
||||
questions: req.questions.map((q, i) => buildItem(q, i)),
|
||||
created_at: createdAt,
|
||||
};
|
||||
if (req.turnId !== undefined) out.turn_id = req.turnId;
|
||||
if (req.toolCallId !== undefined) out.tool_call_id = req.toolCallId;
|
||||
return out;
|
||||
}
|
||||
|
|
@ -6,19 +6,16 @@ import {
|
|||
listSessionPendingInteractions,
|
||||
resumeSessionById,
|
||||
type QuestionAnswers,
|
||||
type QuestionItem,
|
||||
type QuestionOption,
|
||||
type QuestionRequest,
|
||||
type QuestionResult,
|
||||
type Scope,
|
||||
} from '@moonshot-ai/agent-core-v2';
|
||||
import { ErrorCode } from '../protocol/error-codes';
|
||||
import {
|
||||
type QuestionItem as ProtocolQuestionItem,
|
||||
type QuestionOption as ProtocolQuestionOption,
|
||||
type QuestionRequest as ProtocolQuestionRequest,
|
||||
type QuestionResponse as ProtocolQuestionResponse,
|
||||
} from '../protocol/question';
|
||||
import { toWireQuestion } from '../protocol/question-wire';
|
||||
import {
|
||||
listPendingQuestionsQuerySchema,
|
||||
listPendingQuestionsResponseSchema,
|
||||
|
|
@ -247,43 +244,6 @@ async function dismissQuestionAction(ctx: QuestionActionCtx): Promise<void> {
|
|||
});
|
||||
}
|
||||
|
||||
function buildOption(opt: QuestionOption, itemIdx: number, optIdx: number): ProtocolQuestionOption {
|
||||
const base: ProtocolQuestionOption = { id: `opt_${itemIdx}_${optIdx}`, label: opt.label };
|
||||
return opt.description === undefined ? base : { ...base, description: opt.description };
|
||||
}
|
||||
|
||||
function buildItem(item: QuestionItem, itemIdx: number): ProtocolQuestionItem {
|
||||
const out: ProtocolQuestionItem = {
|
||||
id: `q_${itemIdx}`,
|
||||
question: item.question,
|
||||
options: item.options.map((o, oi) => buildOption(o, itemIdx, oi)),
|
||||
};
|
||||
if (item.header !== undefined) out.header = item.header;
|
||||
if (item.body !== undefined) out.body = item.body;
|
||||
if (item.multiSelect !== undefined) out.multi_select = item.multiSelect;
|
||||
out.allow_other = true;
|
||||
if (item.otherLabel !== undefined) out.other_label = item.otherLabel;
|
||||
if (item.otherDescription !== undefined) out.other_description = item.otherDescription;
|
||||
return out;
|
||||
}
|
||||
|
||||
export function toWireQuestion(
|
||||
interaction: Interaction,
|
||||
sessionId: string,
|
||||
): ProtocolQuestionRequest {
|
||||
const req = interaction.payload as QuestionRequest;
|
||||
const createdAt = new Date(interaction.createdAt).toISOString();
|
||||
const out: ProtocolQuestionRequest = {
|
||||
question_id: interaction.id,
|
||||
session_id: sessionId,
|
||||
questions: req.questions.map((q, i) => buildItem(q, i)),
|
||||
created_at: createdAt,
|
||||
};
|
||||
if (req.turnId !== undefined) out.turn_id = req.turnId;
|
||||
if (req.toolCallId !== undefined) out.tool_call_id = req.toolCallId;
|
||||
return out;
|
||||
}
|
||||
|
||||
function toInProcessResponse(
|
||||
resp: ProtocolQuestionResponse,
|
||||
request?: ProtocolQuestionRequest,
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import {
|
|||
import { loadMessageHistory } from '../services/messages/messageHistory';
|
||||
import { type SessionEventBroadcaster } from '../transport/ws/v1/sessionEventBroadcaster';
|
||||
import { toWireApproval } from './approvals';
|
||||
import { toWireQuestion } from './questions';
|
||||
import { toWireQuestion } from '../protocol/question-wire';
|
||||
import { resolveSessionFacts, toWireSession } from './sessions';
|
||||
|
||||
const SNAPSHOT_MESSAGE_PAGE_SIZE = 100;
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ export function bindSessionTranscript(
|
|||
const projectorFor = (agentId: string): AgentTranscriptProjector => {
|
||||
let projector = projectors.get(agentId);
|
||||
if (projector === undefined) {
|
||||
projector = new AgentTranscriptProjector(agentId, {
|
||||
projector = new AgentTranscriptProjector(agentId, store.sessionId, {
|
||||
stepFrames: (turnId, stepId) =>
|
||||
store.getAgent(agentId)?.getTurn(turnId)?.steps.find((s) => s.stepId === stepId)?.frames,
|
||||
toolFrame: (toolCallId) => {
|
||||
|
|
@ -149,6 +149,7 @@ export function bindSessionTranscript(
|
|||
kind: interaction.kind,
|
||||
payload: interaction.payload,
|
||||
origin: interaction.origin,
|
||||
createdAt: interaction.createdAt,
|
||||
};
|
||||
applyOps(agentId, projectorFor(agentId).mapInteractionRequested(request));
|
||||
};
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ import type {
|
|||
} from '@moonshot-ai/transcript';
|
||||
|
||||
import { toLegacyPhase } from '../legacyStatus/legacyStatus';
|
||||
import { toWireQuestion } from '../../protocol/question-wire';
|
||||
import { projectPromptContentParts } from '../messages/messageProjection';
|
||||
|
||||
export interface ProjectorInteraction {
|
||||
|
|
@ -87,6 +88,7 @@ export interface ProjectorInteraction {
|
|||
readonly kind: 'approval' | 'question';
|
||||
readonly payload: unknown;
|
||||
readonly origin: { readonly agentId?: string; readonly turnId?: number };
|
||||
readonly createdAt: number;
|
||||
}
|
||||
|
||||
type PlanRevisionEvent = { readonly type: 'plan.revision' } & PlanRevision;
|
||||
|
|
@ -230,6 +232,7 @@ export class AgentTranscriptProjector {
|
|||
|
||||
constructor(
|
||||
readonly agentId: string,
|
||||
private readonly sessionId: string,
|
||||
private readonly lookups?: ProjectorLookups,
|
||||
) {}
|
||||
|
||||
|
|
@ -1443,12 +1446,21 @@ export class AgentTranscriptProjector {
|
|||
interactionKind: interaction.kind,
|
||||
toolCallId,
|
||||
state: 'pending',
|
||||
request: interaction.payload,
|
||||
request: this.wireInteractionRequest(interaction),
|
||||
};
|
||||
this.interactions.set(interaction.id, entity);
|
||||
return [{ op: 'interaction.upsert', interaction: entity }];
|
||||
}
|
||||
|
||||
private wireInteractionRequest(interaction: ProjectorInteraction): unknown {
|
||||
if (interaction.kind !== 'question') return interaction.payload;
|
||||
try {
|
||||
return toWireQuestion(interaction, this.sessionId);
|
||||
} catch {
|
||||
return interaction.payload;
|
||||
}
|
||||
}
|
||||
|
||||
mapInteractionResolved(id: string, response: unknown): TranscriptOperation[] {
|
||||
const record = this.interactions.get(id);
|
||||
if (record === undefined) return [];
|
||||
|
|
|
|||
|
|
@ -40,7 +40,8 @@ import {
|
|||
type TranscriptTurn,
|
||||
} from '@moonshot-ai/transcript';
|
||||
|
||||
import { readWireRecords } from './wireRecords';
|
||||
import { readWireRecords, type ContextRecord } from './wireRecords';
|
||||
import { toWireQuestion } from '../../protocol/question-wire';
|
||||
import { projectPromptContentParts } from '../messages/messageProjection';
|
||||
import {
|
||||
bindSessionTranscript,
|
||||
|
|
@ -513,7 +514,7 @@ export class TranscriptService {
|
|||
messages,
|
||||
sawTurnPrompt || steeredContents.size > 0 ? { taskOriginTurnTaskIds, steeredContents } : undefined,
|
||||
);
|
||||
const folded = foldWireRecordFacts(records, base);
|
||||
const folded = foldWireRecordFacts(projectQuestionInteractionRecords(records, sessionId), base);
|
||||
const status = getLiveSessionById(this.deps.core.accessor, sessionId)
|
||||
?.accessor.get(IAgentLifecycleService)
|
||||
.handleOf(agentId)
|
||||
|
|
@ -618,6 +619,38 @@ const TERMINAL_TURN_STATES: ReadonlySet<TranscriptTurn['state']> = new Set([
|
|||
'cancelled',
|
||||
]);
|
||||
|
||||
function projectQuestionInteractionRecords(
|
||||
records: readonly ContextRecord[],
|
||||
sessionId: string,
|
||||
): ContextRecord[] {
|
||||
return records.map((record) => {
|
||||
if (record.type !== 'interaction.request' || record['kind'] !== 'question') return record;
|
||||
const id = record['id'];
|
||||
const request = record['request'];
|
||||
const time = record['time'];
|
||||
if (typeof id !== 'string' || typeof time !== 'number' || !Number.isFinite(time)) {
|
||||
return record;
|
||||
}
|
||||
if (request === null || typeof request !== 'object') return record;
|
||||
try {
|
||||
const innerToolCallId = (request as { toolCallId?: unknown }).toolCallId;
|
||||
const toolCallId =
|
||||
typeof record['toolCallId'] === 'string'
|
||||
? record['toolCallId']
|
||||
: typeof innerToolCallId === 'string'
|
||||
? innerToolCallId
|
||||
: undefined;
|
||||
return {
|
||||
...record,
|
||||
toolCallId,
|
||||
request: toWireQuestion({ id, createdAt: time, payload: request }, sessionId),
|
||||
};
|
||||
} catch {
|
||||
return record;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function supersededColdAttachmentIds(
|
||||
snapshot: AgentTranscriptSnapshot,
|
||||
transcript: AgentTranscript,
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ import {
|
|||
} from '@moonshot-ai/transcript';
|
||||
|
||||
import { toWireApproval } from '../../../routes/approvals';
|
||||
import { toWireQuestion } from '../../../routes/questions';
|
||||
import { toWireQuestion } from '../../../protocol/question-wire';
|
||||
import { toWireWorkspace } from '../../../routes/workspaces';
|
||||
import { projectPromptContentParts } from '../../../services/messages/messageProjection';
|
||||
import { readLegacyStatus, toLegacyPhase } from '../../../services/legacyStatus/legacyStatus';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
|
||||
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import { execFile } from 'node:child_process';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
|
@ -50,6 +50,7 @@ import {
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { bindSessionTranscript } from '../../src/services/transcript/coreBinding';
|
||||
import { toWireQuestion } from '../../src/protocol/question-wire';
|
||||
import {
|
||||
AgentTranscriptProjector,
|
||||
type ProjectorBusEvent,
|
||||
|
|
@ -69,6 +70,8 @@ function ev(payload: Record<string, unknown>): ProjectorBusEvent {
|
|||
return payload as unknown as ProjectorBusEvent;
|
||||
}
|
||||
|
||||
const TEST_SESSION_ID = 'session-test';
|
||||
|
||||
function turnOps(turnId: string, items: ReturnType<AgentTranscript['getItems']>): TranscriptTurn {
|
||||
const turn = items.find(
|
||||
(item): item is TranscriptTurn => item.kind === 'turn' && item.turnId === turnId,
|
||||
|
|
@ -97,7 +100,7 @@ function coldTranscriptService(home: string): TranscriptService {
|
|||
|
||||
describe('AgentTranscriptProjector', () => {
|
||||
it('projects a full turn: headers, delta appends, flush, tool frames', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
const ops: TranscriptOperation[] = [];
|
||||
const feed = (event: ProjectorBusEvent): void => {
|
||||
|
|
@ -157,7 +160,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('projects the live prompt from turn.started and keeps it through turn.ended', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
const feed = (event: ProjectorBusEvent): void => {
|
||||
tx.apply(projector.map(event));
|
||||
|
|
@ -173,7 +176,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('projects turn.started promptAttachments into attachment entities and turn.attachmentIds', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
const ops: TranscriptOperation[] = [];
|
||||
const feed = (event: ProjectorBusEvent): void => {
|
||||
|
|
@ -216,7 +219,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
|
||||
it('places late-attach deltas into the engine-reported active step', () => {
|
||||
const tx = new AgentTranscript('main');
|
||||
const projector = new AgentTranscriptProjector('main', {
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID, {
|
||||
stepOrdinal: (turnId) => (turnId === 't0' ? 2 : undefined),
|
||||
});
|
||||
|
||||
|
|
@ -247,7 +250,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
frame: { kind: 'text', frameId: 't0.1.f1', role: 'assistant', text: 'Hello ' },
|
||||
},
|
||||
]);
|
||||
const projector = new AgentTranscriptProjector('main', {
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID, {
|
||||
stepFrames: (turnId, stepId) =>
|
||||
tx.getTurn(turnId)?.steps.find((s) => s.stepId === stepId)?.frames,
|
||||
});
|
||||
|
|
@ -292,7 +295,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
},
|
||||
},
|
||||
]);
|
||||
const projector = new AgentTranscriptProjector('main', {
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID, {
|
||||
toolFrame: (toolCallId) => {
|
||||
for (const item of tx.getItems()) {
|
||||
if (item.kind !== 'turn') continue;
|
||||
|
|
@ -342,7 +345,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
},
|
||||
},
|
||||
]);
|
||||
const projector = new AgentTranscriptProjector('main', {
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID, {
|
||||
toolFrame: (toolCallId) => {
|
||||
for (const item of tx.getItems()) {
|
||||
if (item.kind !== 'turn') continue;
|
||||
|
|
@ -374,7 +377,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('gives live markers their own namespace so they never collide with backfilled markers', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
tx.apply([{ op: 'marker.upsert', item: { kind: 'marker', markerId: 'm1', marker: 'skill' } }]);
|
||||
|
||||
|
|
@ -483,7 +486,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('flushes open frames on turn.ended even without step completion', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
const feed = (event: ProjectorBusEvent): void => void tx.apply(projector.map(event));
|
||||
|
||||
|
|
@ -506,7 +509,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('marks a user-cancelled turn with an interruption marker, but not programmatic aborts', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
const feed = (event: ProjectorBusEvent): void => void tx.apply(projector.map(event));
|
||||
|
||||
|
|
@ -530,7 +533,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('carries usage / finishReason / the full timing breakdown on turn.step.completed', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
const feed = (event: ProjectorBusEvent): void => void tx.apply(projector.map(event));
|
||||
|
||||
|
|
@ -590,7 +593,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('carries endReason / endMessage on turn.step.interrupted', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
const feed = (event: ProjectorBusEvent): void => void tx.apply(projector.map(event));
|
||||
|
||||
|
|
@ -613,7 +616,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('sets retry on turn.step.retrying and clears it at the terminal upsert', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
const feed = (event: ProjectorBusEvent): void => void tx.apply(projector.map(event));
|
||||
const step = (): TranscriptTurn['steps'][number] => turnOps('t1', tx.getItems()).steps[0]!;
|
||||
|
|
@ -652,7 +655,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('fills durationMs / error / accumulated step usage on turn.ended', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
const feed = (event: ProjectorBusEvent): void => void tx.apply(projector.map(event));
|
||||
|
||||
|
|
@ -698,7 +701,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('takes the turn header endedAt from the turn.ended event time', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
const feed = (event: ProjectorBusEvent): void => void tx.apply(projector.map(event));
|
||||
|
||||
|
|
@ -712,7 +715,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('accumulates tool.call.delta into inputText, kept across tool.call.started', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
const feed = (event: ProjectorBusEvent): void => void tx.apply(projector.map(event));
|
||||
const toolFrame = (toolCallId: string): TranscriptFrame | undefined =>
|
||||
|
|
@ -762,7 +765,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('overwrites tool frame progress and drops progress for unknown calls', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
const feed = (event: ProjectorBusEvent): void => void tx.apply(projector.map(event));
|
||||
|
||||
|
|
@ -807,7 +810,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('marks tool.result errors and keeps the display payload', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
const feed = (event: ProjectorBusEvent): void => void tx.apply(projector.map(event));
|
||||
|
||||
|
|
@ -835,7 +838,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('projects process tasks as shell tasks with streaming output', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
const ops: TranscriptOperation[] = [];
|
||||
const feed = (event: ProjectorBusEvent): void => {
|
||||
|
|
@ -887,7 +890,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('fills the shell task output from late stderr chunks before completing', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
|
||||
tx.apply(projector.map(ev({ type: 'shell.started', commandId: 'c1', taskId: 'task-1' })));
|
||||
|
|
@ -900,7 +903,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('routes shell output/completion via the event taskId when shell.started was missed', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
|
||||
tx.apply(
|
||||
|
|
@ -916,7 +919,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('emits a taskref when only shell.completed arrives for a command', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
|
||||
tx.apply(projector.map(ev({ type: 'shell.completed', commandId: 'c1', taskId: 'task-1', isError: true })));
|
||||
|
|
@ -926,7 +929,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('projects no-taskId shell failures under a synthetic per-command task id', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
|
||||
tx.apply(
|
||||
|
|
@ -940,7 +943,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('marks a foreground shell task terminal on shell.completed', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
|
||||
tx.apply(projector.map(ev({ type: 'shell.started', commandId: 'c1', taskId: 'task-1' })));
|
||||
|
|
@ -956,12 +959,12 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('ignores task.notified (it re-surfaces as an origin:task turn)', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
expect(projector.map(ev({ type: 'task.notified', taskId: 't' }))).toEqual([]);
|
||||
});
|
||||
|
||||
it('links spawned subagents to the spawning tool frame (member for swarm)', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
const feed = (event: ProjectorBusEvent): void => void tx.apply(projector.map(event));
|
||||
|
||||
|
|
@ -1008,7 +1011,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('keys an Agent-tool subagent row by its registered task id and folds the lifecycle', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
const feed = (event: ProjectorBusEvent): void => void tx.apply(projector.map(event));
|
||||
|
||||
|
|
@ -1067,7 +1070,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('drops the stale task mapping when a child respawns without a task id', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
const feed = (event: ProjectorBusEvent): void => void tx.apply(projector.map(event));
|
||||
|
||||
|
|
@ -1100,7 +1103,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('recovers the agent → task association from a backfilled task.started', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
const feed = (event: ProjectorBusEvent): void => void tx.apply(projector.map(event));
|
||||
|
||||
|
|
@ -1126,7 +1129,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('projects goal updates into meta.goal plus an inline marker', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
const snapshot = {
|
||||
goalId: 'g1',
|
||||
|
|
@ -1158,7 +1161,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('mirrors plan / swarm mode slices into meta.modes (only when provided)', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
|
||||
tx.apply(projector.map(ev({ type: 'agent.status.updated', planMode: true })));
|
||||
|
|
@ -1172,7 +1175,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('mirrors the tower mode slice into meta.modes', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
|
||||
tx.apply(projector.map(ev({ type: 'agent.status.updated', towerMode: true })));
|
||||
|
|
@ -1183,7 +1186,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('mirrors status slices into meta.agent (shallow-merged across slices)', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
const feed = (event: ProjectorBusEvent): void => void tx.apply(projector.map(event));
|
||||
|
||||
|
|
@ -1224,7 +1227,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('maps agent.activity.updated into meta.agent.phase', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
const feed = (event: ProjectorBusEvent): void => void tx.apply(projector.map(event));
|
||||
const turn = (overrides: Record<string, unknown>): Record<string, unknown> => ({
|
||||
|
|
@ -1298,7 +1301,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('projects plan.revision as a marker and refines the active plan badge', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
|
||||
const revision = {
|
||||
|
|
@ -1347,7 +1350,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('projects skill / plugin-command / cron / compaction / hook / undo markers', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
const feed = (event: ProjectorBusEvent): void => void tx.apply(projector.map(event));
|
||||
|
||||
|
|
@ -1404,7 +1407,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
|
||||
it('removes trailing turns and the undo marker on context.undone', () => {
|
||||
const tx = new AgentTranscript('main');
|
||||
const projector = new AgentTranscriptProjector('main', {
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID, {
|
||||
items: () => tx.getItems(),
|
||||
});
|
||||
const feed = (event: ProjectorBusEvent): void => {
|
||||
|
|
@ -1427,7 +1430,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
|
||||
it('removes multiple trailing turns and keeps taskrefs appended during them', () => {
|
||||
const tx = new AgentTranscript('main');
|
||||
const projector = new AgentTranscriptProjector('main', {
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID, {
|
||||
items: () => tx.getItems(),
|
||||
});
|
||||
const feed = (event: ProjectorBusEvent): void => {
|
||||
|
|
@ -1463,16 +1466,16 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('ignores context.undone when no removable turns exist', () => {
|
||||
const bare = new AgentTranscriptProjector('main');
|
||||
const bare = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
expect(bare.map(ev({ type: 'context.undone', agentId: 'main', turns: 1 }))).toEqual([]);
|
||||
|
||||
const projector = new AgentTranscriptProjector('main', { items: () => [] });
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID, { items: () => [] });
|
||||
expect(projector.map(ev({ type: 'context.undone', agentId: 'main', turns: 1 }))).toEqual([]);
|
||||
});
|
||||
|
||||
it('removes every turn from fromTurnId onward, including trailing non-anchor turns', () => {
|
||||
const tx = new AgentTranscript('main');
|
||||
const projector = new AgentTranscriptProjector('main', {
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID, {
|
||||
items: () => tx.getItems(),
|
||||
});
|
||||
const feed = (event: ProjectorBusEvent): void => {
|
||||
|
|
@ -1498,7 +1501,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('projects error / warning events as notice markers outside any step', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
|
||||
tx.apply(
|
||||
|
|
@ -1521,7 +1524,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('emits interactions as global entities only (no inline frame), back-links on resolve', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
const feed = (event: ProjectorBusEvent): void => void tx.apply(projector.map(event));
|
||||
|
||||
|
|
@ -1549,6 +1552,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
kind: 'approval',
|
||||
payload: request,
|
||||
origin: { agentId: 'main', turnId: 2 },
|
||||
createdAt: 1000,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -1575,7 +1579,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('surfaces a mid-turn task notification as a user input frame linked to the task', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
const feed = (event: ProjectorBusEvent): void => void tx.apply(projector.map(event));
|
||||
|
||||
|
|
@ -1604,7 +1608,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('attaches a between-steps task notification to the following step', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
const feed = (event: ProjectorBusEvent): void => void tx.apply(projector.map(event));
|
||||
|
||||
|
|
@ -1633,7 +1637,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('drops a task notification that is the turn prompt itself', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
const feed = (event: ProjectorBusEvent): void => void tx.apply(projector.map(event));
|
||||
|
||||
|
|
@ -1654,7 +1658,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('keeps a different task’s notification in a task-origin turn', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
const feed = (event: ProjectorBusEvent): void => void tx.apply(projector.map(event));
|
||||
|
||||
|
|
@ -1676,7 +1680,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('drops a buffered task notification when the turn ends before the next step', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
const feed = (event: ProjectorBusEvent): void => void tx.apply(projector.map(event));
|
||||
|
||||
|
|
@ -1702,7 +1706,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('replaces the global todo document on a confirmed TodoList write', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
const feed = (event: ProjectorBusEvent): void => void tx.apply(projector.map(event));
|
||||
|
||||
|
|
@ -1747,7 +1751,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('emits an unanchored entity when the payload has no toolCallId', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
|
||||
tx.apply(
|
||||
|
|
@ -1756,6 +1760,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
kind: 'question',
|
||||
payload: { questions: [{ question: 'Pick', options: [] }] },
|
||||
origin: { agentId: 'main', turnId: 3 },
|
||||
createdAt: 1000,
|
||||
}),
|
||||
);
|
||||
expect(tx.getItems()).toHaveLength(0);
|
||||
|
|
@ -1769,8 +1774,85 @@ describe('AgentTranscriptProjector', () => {
|
|||
expect(tx.listPendingInteractions()).toEqual([]);
|
||||
});
|
||||
|
||||
it('projects question requests onto the wire shape with stable question/option ids', () => {
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
|
||||
tx.apply(
|
||||
projector.mapInteractionRequested({
|
||||
id: 'q-wire',
|
||||
kind: 'question',
|
||||
payload: {
|
||||
toolCallId: 'call_q',
|
||||
turnId: 3,
|
||||
questions: [
|
||||
{
|
||||
question: 'Pick one',
|
||||
header: 'h',
|
||||
body: 'b',
|
||||
multiSelect: false,
|
||||
otherLabel: 'Other',
|
||||
otherDescription: 'free text',
|
||||
options: [{ label: 'A', description: 'first' }, { label: 'B' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
origin: { agentId: 'main', turnId: 3 },
|
||||
createdAt: 7000,
|
||||
}),
|
||||
);
|
||||
|
||||
const entity = tx.getInteraction('q-wire');
|
||||
expect(entity?.toolCallId).toBe('call_q');
|
||||
expect(entity?.request).toEqual({
|
||||
question_id: 'q-wire',
|
||||
session_id: TEST_SESSION_ID,
|
||||
questions: [
|
||||
{
|
||||
id: 'q_0',
|
||||
question: 'Pick one',
|
||||
header: 'h',
|
||||
body: 'b',
|
||||
multi_select: false,
|
||||
allow_other: true,
|
||||
other_label: 'Other',
|
||||
other_description: 'free text',
|
||||
options: [
|
||||
{ id: 'opt_0_0', label: 'A', description: 'first' },
|
||||
{ id: 'opt_0_1', label: 'B' },
|
||||
],
|
||||
},
|
||||
],
|
||||
created_at: new Date(7000).toISOString(),
|
||||
turn_id: 3,
|
||||
tool_call_id: 'call_q',
|
||||
});
|
||||
|
||||
tx.apply(projector.mapInteractionResolved('q-wire', { q_0: 'A' }));
|
||||
expect(tx.getInteraction('q-wire')).toMatchObject({ state: 'answered' });
|
||||
});
|
||||
|
||||
it('keeps a malformed question payload raw instead of failing the projection', () => {
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
|
||||
tx.apply(
|
||||
projector.mapInteractionRequested({
|
||||
id: 'q-raw',
|
||||
kind: 'question',
|
||||
payload: { toolCallId: 'call_x' },
|
||||
origin: { agentId: 'main' },
|
||||
createdAt: 1000,
|
||||
}),
|
||||
);
|
||||
|
||||
const entity = tx.getInteraction('q-raw');
|
||||
expect(entity?.toolCallId).toBe('call_x');
|
||||
expect(entity?.request).toEqual({ toolCallId: 'call_x' });
|
||||
});
|
||||
|
||||
it('projects prompt submitted/completed/aborted/steered as global queue entities', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
const feed = (event: ProjectorBusEvent): void => void tx.apply(projector.map(event));
|
||||
|
||||
|
|
@ -1873,7 +1955,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('projects prompt.steered media content to the wire shape (no daemon ref or path leak)', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
const feed = (event: ProjectorBusEvent): void => void tx.apply(projector.map(event));
|
||||
|
||||
|
|
@ -1901,7 +1983,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('projects turn.steer as a user frame at the next step start, pairing promptIds from prompt.steered', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
const feed = (event: ProjectorBusEvent): void => void tx.apply(projector.map(event));
|
||||
|
||||
|
|
@ -1938,7 +2020,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('projects turn.steer into the running step immediately, with daemon media as attachments', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
const ops: TranscriptOperation[] = [];
|
||||
const feed = (event: ProjectorBusEvent): void => {
|
||||
|
|
@ -1990,7 +2072,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('ignores turn.steer for non-user origins and for turns that are not running', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
const feed = (event: ProjectorBusEvent): void => void tx.apply(projector.map(event));
|
||||
|
||||
|
|
@ -2020,7 +2102,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('flushes a pending steer into the last step when the turn ends before the next step', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
const feed = (event: ProjectorBusEvent): void => void tx.apply(projector.map(event));
|
||||
|
||||
|
|
@ -2056,7 +2138,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('buffers turn.steer seen before the projector ever saw turn.started (mid-turn attach)', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
const ops: TranscriptOperation[] = [];
|
||||
const feed = (event: ProjectorBusEvent): void => {
|
||||
|
|
@ -2253,6 +2335,92 @@ describe('AgentTranscriptProjector', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('readColdSnapshot projects question requests onto the wire shape without rewriting the log', async () => {
|
||||
const home = await mkdtemp(join(tmpdir(), 'transcript-cold-question-'));
|
||||
try {
|
||||
const wireDir = join(home, 'sessions', 'ws', 's1', 'agents', 'main');
|
||||
await mkdir(wireDir, { recursive: true });
|
||||
const records = [
|
||||
{
|
||||
type: 'interaction.request',
|
||||
id: 'q-cold',
|
||||
kind: 'question',
|
||||
toolCallId: 'call_q',
|
||||
request: {
|
||||
toolCallId: 'call_q',
|
||||
questions: [{ question: 'Pick', options: [{ label: 'A' }, { label: 'B' }] }],
|
||||
},
|
||||
time: 7000,
|
||||
},
|
||||
{
|
||||
type: 'interaction.request',
|
||||
id: 'q-inner',
|
||||
kind: 'question',
|
||||
request: {
|
||||
toolCallId: 'call_inner',
|
||||
questions: [{ question: 'Inner', options: [{ label: 'X' }] }],
|
||||
},
|
||||
time: 8500,
|
||||
},
|
||||
{
|
||||
type: 'interaction.request',
|
||||
id: 'q-bad',
|
||||
kind: 'question',
|
||||
request: { toolName: 'nope' },
|
||||
time: 8000,
|
||||
},
|
||||
{
|
||||
type: 'interaction.request',
|
||||
id: 'apr-1',
|
||||
kind: 'approval',
|
||||
toolCallId: 'call_1',
|
||||
request: { toolName: 'Bash' },
|
||||
time: 9000,
|
||||
},
|
||||
];
|
||||
const wireFile = join(wireDir, 'wire.jsonl');
|
||||
const content = `${records.map((r) => JSON.stringify(r)).join('\n')}\n`;
|
||||
await writeFile(wireFile, content);
|
||||
|
||||
const service = coldTranscriptService(home);
|
||||
const snapshot = await service.readColdSnapshot('s1', 'main');
|
||||
const byId = new Map(snapshot!.interactions.map((i) => [i.interactionId, i]));
|
||||
expect(byId.get('q-cold')).toMatchObject({
|
||||
interactionKind: 'question',
|
||||
toolCallId: 'call_q',
|
||||
state: 'cancelled',
|
||||
});
|
||||
expect(byId.get('q-cold')?.request).toEqual({
|
||||
question_id: 'q-cold',
|
||||
session_id: 's1',
|
||||
questions: [
|
||||
{
|
||||
id: 'q_0',
|
||||
question: 'Pick',
|
||||
options: [
|
||||
{ id: 'opt_0_0', label: 'A' },
|
||||
{ id: 'opt_0_1', label: 'B' },
|
||||
],
|
||||
allow_other: true,
|
||||
},
|
||||
],
|
||||
created_at: new Date(7000).toISOString(),
|
||||
tool_call_id: 'call_q',
|
||||
});
|
||||
expect(byId.get('q-bad')?.request).toEqual({ toolName: 'nope' });
|
||||
expect(byId.get('q-inner')).toMatchObject({ toolCallId: 'call_inner' });
|
||||
expect(byId.get('q-inner')?.request).toMatchObject({
|
||||
question_id: 'q-inner',
|
||||
tool_call_id: 'call_inner',
|
||||
});
|
||||
expect(byId.get('apr-1')?.request).toEqual({ toolName: 'Bash' });
|
||||
await expect(readFile(wireFile, 'utf-8')).resolves.toBe(content);
|
||||
service.dropSession('s1');
|
||||
} finally {
|
||||
await rm(home, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('readColdSnapshot derives meta.activity from the final turn state when no live session exists', async () => {
|
||||
const home = await mkdtemp(join(tmpdir(), 'transcript-cold-activity-'));
|
||||
try {
|
||||
|
|
@ -2489,7 +2657,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('folds blocked turn endings into failed (engine wire contract)', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
tx.apply(projector.map(ev({ type: 'turn.started', turnId: 0, origin: { kind: 'user' } })));
|
||||
tx.apply(projector.map(ev({ type: 'turn.ended', turnId: 0, reason: 'blocked' })));
|
||||
|
|
@ -2497,7 +2665,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('tracks the prompt queue from accepted/queued through terminal', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
const feed = (event: ProjectorBusEvent): void => void tx.apply(projector.map(event));
|
||||
|
||||
|
|
@ -2511,7 +2679,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
expect(tx.getPrompt('p1')).toMatchObject({ status: 'aborted' });
|
||||
});
|
||||
|
||||
it('mirrors turn liveness into meta.activity', () => { const projector = new AgentTranscriptProjector('main');
|
||||
it('mirrors turn liveness into meta.activity', () => { const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
const feed = (event: ProjectorBusEvent): void => void tx.apply(projector.map(event));
|
||||
|
||||
|
|
@ -2526,7 +2694,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
expect(tx.getMeta().activity).toBe('idle');
|
||||
});
|
||||
|
||||
it('maps cron / task origins onto the turn header', () => { const projector = new AgentTranscriptProjector('main');
|
||||
it('maps cron / task origins onto the turn header', () => { const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
const feed = (event: ProjectorBusEvent): void => void tx.apply(projector.map(event));
|
||||
|
||||
|
|
@ -2558,7 +2726,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
});
|
||||
|
||||
it('treats subagent.started/failed/suspended within the running→failed vocabulary', () => {
|
||||
const projector = new AgentTranscriptProjector('main');
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
const feed = (event: ProjectorBusEvent): void => void tx.apply(projector.map(event));
|
||||
|
||||
|
|
@ -3207,6 +3375,32 @@ describe('bindSessionTranscript', () => {
|
|||
binding.dispose();
|
||||
});
|
||||
|
||||
it('projects live question entities with the same wire shape as the legacy question event', () => {
|
||||
const interactions = new FakeInteractionHub();
|
||||
const asked = interactions.enqueue({
|
||||
id: 'q-parity',
|
||||
kind: 'question',
|
||||
payload: {
|
||||
questions: [
|
||||
{
|
||||
question: 'Pick one',
|
||||
options: [{ label: 'A', description: 'first' }, { label: 'B' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
origin: { agentId: 'main', turnId: 0 },
|
||||
});
|
||||
const store = new TranscriptStore('s1');
|
||||
const binding = bindSessionTranscript(store, fakeSession(interactions));
|
||||
|
||||
binding.seedPendingInteractions('main');
|
||||
|
||||
const entity = store.getAgent('main')?.getInteraction('q-parity');
|
||||
expect(entity?.state).toBe('pending');
|
||||
expect(entity?.request).toEqual(toWireQuestion(asked, 's1'));
|
||||
binding.dispose();
|
||||
});
|
||||
|
||||
it('defers pendings created before their owning agent is seeded', () => {
|
||||
const interactions = new FakeInteractionHub();
|
||||
const store = new TranscriptStore('s1');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue