mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-31 10:37:12 +00:00
fix(transcript): preserve steer provenance (#3374)
* fix(transcript): preserve bundled prompt origin * refactor(transcript): narrow steer origin projection
This commit is contained in:
parent
56b5480ed0
commit
cbe0a77f3d
8 changed files with 313 additions and 41 deletions
|
|
@ -56,27 +56,29 @@ import type {
|
|||
SubagentSpawned,
|
||||
SubagentStarted,
|
||||
} from '@moonshot-ai/agent-core-v2/session/subagent/mirrorAgentRun';
|
||||
import type {
|
||||
AgentRef,
|
||||
AgentUsageMeta,
|
||||
StepHeader,
|
||||
StepUsage,
|
||||
TextFrame,
|
||||
ToolCallFrame,
|
||||
ToolFrameProgress,
|
||||
TranscriptAttachment,
|
||||
TranscriptFrame,
|
||||
TranscriptInteraction,
|
||||
TranscriptItem,
|
||||
TranscriptMarker,
|
||||
TranscriptOperation,
|
||||
TranscriptPrompt,
|
||||
TranscriptTask,
|
||||
TranscriptTodo,
|
||||
TranscriptUsage,
|
||||
TurnHeader,
|
||||
TurnOrigin,
|
||||
TurnState,
|
||||
import {
|
||||
projectTranscriptUserOrigin,
|
||||
type AgentRef,
|
||||
type AgentUsageMeta,
|
||||
type StepHeader,
|
||||
type StepUsage,
|
||||
type TextFrame,
|
||||
type ToolCallFrame,
|
||||
type ToolFrameProgress,
|
||||
type TranscriptAttachment,
|
||||
type TranscriptFrame,
|
||||
type TranscriptInteraction,
|
||||
type TranscriptItem,
|
||||
type TranscriptMarker,
|
||||
type TranscriptOperation,
|
||||
type TranscriptPrompt,
|
||||
type TranscriptTask,
|
||||
type TranscriptTodo,
|
||||
type TranscriptUsage,
|
||||
type TranscriptUserOrigin,
|
||||
type TurnHeader,
|
||||
type TurnOrigin,
|
||||
type TurnState,
|
||||
} from '@moonshot-ai/transcript';
|
||||
|
||||
import { toLegacyPhase } from '../legacyStatus/legacyStatus';
|
||||
|
|
@ -189,7 +191,11 @@ export class AgentTranscriptProjector {
|
|||
private currentTurn: TurnHeader | undefined;
|
||||
private currentStep: StepHeader | undefined;
|
||||
private pendingTaskNotifications: { text: string; taskId: string | undefined }[] = [];
|
||||
private pendingSteers: { input: readonly ContentPart[]; promptIds: readonly string[] | undefined }[] = [];
|
||||
private pendingSteers: {
|
||||
input: readonly ContentPart[];
|
||||
promptIds: readonly string[] | undefined;
|
||||
origin: TranscriptUserOrigin;
|
||||
}[] = [];
|
||||
private unpairedSteerPromptIds: string[][] = [];
|
||||
private readonly stepOrdinals = new Map<string, number>();
|
||||
private frameOrdinal = 0;
|
||||
|
|
@ -401,9 +407,30 @@ export class AgentTranscriptProjector {
|
|||
this.currentStep = step;
|
||||
ops.push({ op: 'step.upsert', turnId: step.turnId, step });
|
||||
}
|
||||
if (this.currentStep === undefined && this.pendingSteers.length > 0) {
|
||||
const ordinal = (this.stepOrdinals.get(turnId) ?? this.lookups?.stepOrdinal?.(turnId) ?? 0) + 1;
|
||||
const step: StepHeader = {
|
||||
kind: 'step',
|
||||
stepId: `${turnId}.${ordinal}`,
|
||||
turnId,
|
||||
ordinal,
|
||||
state: 'interrupted',
|
||||
endedAt: nowIso(),
|
||||
};
|
||||
this.stepOrdinals.set(turnId, ordinal);
|
||||
this.currentStep = step;
|
||||
ops.push({ op: 'step.upsert', turnId, step });
|
||||
}
|
||||
if (this.currentStep !== undefined) {
|
||||
for (const pending of this.pendingSteers) {
|
||||
this.steerUserFrame(ops, turnId, this.currentStep.stepId, pending.input, pending.promptIds);
|
||||
this.steerUserFrame(
|
||||
ops,
|
||||
turnId,
|
||||
this.currentStep.stepId,
|
||||
pending.input,
|
||||
pending.promptIds,
|
||||
pending.origin,
|
||||
);
|
||||
}
|
||||
}
|
||||
this.pendingSteers = [];
|
||||
|
|
@ -490,7 +517,7 @@ export class AgentTranscriptProjector {
|
|||
}
|
||||
this.pendingTaskNotifications = [];
|
||||
for (const pending of this.pendingSteers) {
|
||||
this.steerUserFrame(ops, turnId, stepId, pending.input, pending.promptIds);
|
||||
this.steerUserFrame(ops, turnId, stepId, pending.input, pending.promptIds, pending.origin);
|
||||
}
|
||||
this.pendingSteers = [];
|
||||
return ops;
|
||||
|
|
@ -1386,7 +1413,9 @@ export class AgentTranscriptProjector {
|
|||
|
||||
private onTurnSteered(event: TurnSteerEvent): TranscriptOperation[] {
|
||||
const origin = event.origin;
|
||||
if (origin?.kind !== 'user') return [];
|
||||
if (origin.kind !== 'user') return [];
|
||||
const frameOrigin = projectTranscriptUserOrigin(origin);
|
||||
if (frameOrigin === undefined) return [];
|
||||
const turn = this.currentTurn;
|
||||
if (turn !== undefined && turn.state !== 'running') return [];
|
||||
const skip = origin.skillActivations?.length ?? 0;
|
||||
|
|
@ -1394,10 +1423,21 @@ export class AgentTranscriptProjector {
|
|||
const step = this.currentStep;
|
||||
if (step !== undefined && step.state === 'running') {
|
||||
const ops: TranscriptOperation[] = [];
|
||||
this.steerUserFrame(ops, step.turnId, step.stepId, input, this.unpairedSteerPromptIds.shift());
|
||||
this.steerUserFrame(
|
||||
ops,
|
||||
step.turnId,
|
||||
step.stepId,
|
||||
input,
|
||||
this.unpairedSteerPromptIds.shift(),
|
||||
frameOrigin,
|
||||
);
|
||||
return ops;
|
||||
}
|
||||
this.pendingSteers.push({ input, promptIds: this.unpairedSteerPromptIds.shift() });
|
||||
this.pendingSteers.push({
|
||||
input,
|
||||
promptIds: this.unpairedSteerPromptIds.shift(),
|
||||
origin: frameOrigin,
|
||||
});
|
||||
return [];
|
||||
}
|
||||
|
||||
|
|
@ -1407,6 +1447,7 @@ export class AgentTranscriptProjector {
|
|||
stepId: string,
|
||||
input: readonly ContentPart[],
|
||||
promptIds: readonly string[] | undefined,
|
||||
origin: TranscriptUserOrigin,
|
||||
): void {
|
||||
const texts: string[] = [];
|
||||
const attachmentIds: string[] = [];
|
||||
|
|
@ -1436,6 +1477,7 @@ export class AgentTranscriptProjector {
|
|||
text: texts.join(''),
|
||||
attachmentIds: attachmentIds.length > 0 ? attachmentIds : undefined,
|
||||
promptIds,
|
||||
origin,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2093,6 +2093,7 @@ describe('AgentTranscriptProjector', () => {
|
|||
role: 'user',
|
||||
text: 'steered in',
|
||||
promptIds: ['p2'],
|
||||
origin: { kind: 'user' },
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -2127,13 +2128,27 @@ describe('AgentTranscriptProjector', () => {
|
|||
ev({
|
||||
type: 'turn.steer',
|
||||
input: [
|
||||
{ type: 'text', text: '<skill-loaded name="deploy">private instructions</skill-loaded>' },
|
||||
{ type: 'text', text: '<skill-loaded name="review">private instructions</skill-loaded>' },
|
||||
{ type: 'text', text: 'look at this' },
|
||||
{
|
||||
type: 'image_url',
|
||||
imageUrl: { url: 'kimi-file://f_img9?path=%2Fabs%2Fsession%2Fmedia%2Ff_img9.png' },
|
||||
},
|
||||
],
|
||||
origin: { kind: 'user' },
|
||||
origin: {
|
||||
kind: 'user',
|
||||
skillActivations: [
|
||||
{ activationId: 'a1', skillName: 'deploy', skillPath: '/private/deploy/SKILL.md' },
|
||||
{ activationId: 'a2', skillName: 'review', skillArgs: 'strict', skillPath: '/private/review/SKILL.md' },
|
||||
],
|
||||
attachments: [{
|
||||
name: 'secret.txt',
|
||||
mediaType: 'text/plain',
|
||||
size: 12,
|
||||
path: '/private/secret.txt',
|
||||
}],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -2142,7 +2157,20 @@ describe('AgentTranscriptProjector', () => {
|
|||
attachment: { mediaType: 'image/*', source: { kind: 'session_media', fileId: 'f_img9' } },
|
||||
});
|
||||
const frame = turnOps('t4', tx.getItems()).steps[0]?.frames[0];
|
||||
expect(frame).toMatchObject({ kind: 'text', role: 'user', text: 'look at this', promptIds: ['p2', 'p3'] });
|
||||
expect(frame).toMatchObject({
|
||||
kind: 'text',
|
||||
role: 'user',
|
||||
text: 'look at this',
|
||||
promptIds: ['p2', 'p3'],
|
||||
origin: {
|
||||
kind: 'user',
|
||||
skillActivations: [
|
||||
{ skillName: 'deploy' },
|
||||
{ skillName: 'review', skillArgs: 'strict' },
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(JSON.stringify(frame)).not.toContain('/private/');
|
||||
expect(frame?.kind === 'text' ? frame.attachmentIds : undefined).toEqual([
|
||||
attachmentOp?.op === 'attachment.upsert' ? attachmentOp.attachment.attachmentId : undefined,
|
||||
]);
|
||||
|
|
@ -2211,6 +2239,52 @@ describe('AgentTranscriptProjector', () => {
|
|||
role: 'user',
|
||||
text: 'last word',
|
||||
promptIds: ['p2'],
|
||||
origin: { kind: 'user' },
|
||||
});
|
||||
});
|
||||
|
||||
it('flushes a pending steer into a user-only step when the turn ends before its first step', () => {
|
||||
const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID);
|
||||
const tx = new AgentTranscript('main');
|
||||
const feed = (event: ProjectorBusEvent): void => void tx.apply(projector.map(event));
|
||||
|
||||
feed(ev({ type: 'turn.started', turnId: 7, origin: { kind: 'user' }, prompt: 'active' }));
|
||||
feed(
|
||||
ev({
|
||||
type: 'prompt.steered',
|
||||
activePromptId: 'p1',
|
||||
promptIds: ['p2'],
|
||||
content: [{ type: 'text', text: 'last word' }],
|
||||
steeredAt: '2026-01-01T00:00:02.000Z',
|
||||
}),
|
||||
);
|
||||
feed(
|
||||
ev({
|
||||
type: 'turn.steer',
|
||||
input: [
|
||||
{ type: 'text', text: '<skill-loaded name="review">private instructions</skill-loaded>' },
|
||||
{ type: 'text', text: 'last word' },
|
||||
],
|
||||
origin: {
|
||||
kind: 'user',
|
||||
skillActivations: [{ activationId: 'a1', skillName: 'review', skillArgs: 'strict' }],
|
||||
},
|
||||
}),
|
||||
);
|
||||
feed(ev({ type: 'turn.ended', turnId: 7, reason: 'cancelled', interruptReason: 'user_cancelled' }));
|
||||
|
||||
const turn = turnOps('t7', tx.getItems());
|
||||
expect(turn.steps).toHaveLength(1);
|
||||
expect(turn.steps[0]).toMatchObject({ state: 'interrupted' });
|
||||
expect(turn.steps[0]?.frames[0]).toMatchObject({
|
||||
kind: 'text',
|
||||
role: 'user',
|
||||
text: 'last word',
|
||||
promptIds: ['p2'],
|
||||
origin: {
|
||||
kind: 'user',
|
||||
skillActivations: [{ skillName: 'review', skillArgs: 'strict' }],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -2246,7 +2320,13 @@ describe('AgentTranscriptProjector', () => {
|
|||
expect(frameOp).toMatchObject({
|
||||
turnId: 't3',
|
||||
stepId: 't3.2',
|
||||
frame: { kind: 'text', role: 'user', text: 'steered mid-attach', promptIds: ['p2'] },
|
||||
frame: {
|
||||
kind: 'text',
|
||||
role: 'user',
|
||||
text: 'steered mid-attach',
|
||||
promptIds: ['p2'],
|
||||
origin: { kind: 'user' },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -2583,12 +2663,67 @@ describe('AgentTranscriptProjector', () => {
|
|||
kind: 'text',
|
||||
role: 'user',
|
||||
text: 'steered in',
|
||||
origin: { kind: 'user' },
|
||||
});
|
||||
} finally {
|
||||
await rm(home, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('readColdSnapshot preserves safe bundled skill provenance before the first step', async () => {
|
||||
const home = await mkdtemp(join(tmpdir(), 'transcript-cold-bundled-steer-'));
|
||||
try {
|
||||
const wireDir = join(home, 'sessions', 'ws', 's1', 'agents', 'main');
|
||||
await mkdir(wireDir, { recursive: true });
|
||||
const origin = {
|
||||
kind: 'user',
|
||||
skillActivations: [
|
||||
{ activationId: 'a1', skillName: 'deploy', skillPath: '/private/deploy/SKILL.md' },
|
||||
{ activationId: 'a2', skillName: 'review', skillArgs: 'strict', skillPath: '/private/review/SKILL.md' },
|
||||
],
|
||||
attachments: [{
|
||||
name: 'secret.txt',
|
||||
mediaType: 'text/plain',
|
||||
size: 12,
|
||||
path: '/private/secret.txt',
|
||||
}],
|
||||
};
|
||||
const content = [
|
||||
{ type: 'text', text: '<skill-loaded name="deploy">private instructions</skill-loaded>' },
|
||||
{ type: 'text', text: '<skill-loaded name="review">private instructions</skill-loaded>' },
|
||||
{ type: 'text', text: 'steered in' },
|
||||
];
|
||||
const records = [
|
||||
{ type: 'context.append_message', message: { role: 'user', content: [{ type: 'text', text: 'active' }], toolCalls: [], origin: { kind: 'user' } }, time: 1000 },
|
||||
{ type: 'turn.steer', input: content, origin, time: 3000 },
|
||||
{ type: 'context.append_message', message: { role: 'user', content, toolCalls: [], origin }, time: 3001 },
|
||||
];
|
||||
await writeFile(join(wireDir, 'wire.jsonl'), `${records.map((r) => JSON.stringify(r)).join('\n')}\n`);
|
||||
|
||||
const snapshot = await coldTranscriptService(home).readColdSnapshot('s1', 'main');
|
||||
const turn = snapshot?.items.find((item) => item.kind === 'turn');
|
||||
if (turn?.kind !== 'turn') throw new Error('expected turn');
|
||||
const frame = turn.steps.flatMap((step) => step.frames).find(
|
||||
(candidate) => candidate.kind === 'text' && candidate.role === 'user',
|
||||
);
|
||||
expect(frame).toMatchObject({
|
||||
kind: 'text',
|
||||
role: 'user',
|
||||
text: 'steered in',
|
||||
origin: {
|
||||
kind: 'user',
|
||||
skillActivations: [
|
||||
{ skillName: 'deploy' },
|
||||
{ skillName: 'review', skillArgs: 'strict' },
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(JSON.stringify(frame)).not.toContain('/private/');
|
||||
} finally {
|
||||
await rm(home, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('readColdSnapshot keeps skill-activation steers as skill markers instead of user frames', async () => {
|
||||
const home = await mkdtemp(join(tmpdir(), 'transcript-cold-skillsteer-'));
|
||||
try {
|
||||
|
|
|
|||
20
packages/transcript/src/contract/origin.ts
Normal file
20
packages/transcript/src/contract/origin.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import type { TranscriptSkillActivation, TranscriptUserOrigin } from '../model/frame';
|
||||
|
||||
export function projectTranscriptUserOrigin(origin: unknown): TranscriptUserOrigin | undefined {
|
||||
const candidate = origin as { readonly kind?: unknown; readonly skillActivations?: unknown } | undefined;
|
||||
if (candidate?.kind !== 'user') return undefined;
|
||||
if (!Array.isArray(candidate.skillActivations)) return { kind: 'user' };
|
||||
const skillActivations = candidate.skillActivations.flatMap((activation): TranscriptSkillActivation[] => {
|
||||
if (typeof activation !== 'object' || activation === null) return [];
|
||||
const value = activation as { readonly skillName?: unknown; readonly skillArgs?: unknown };
|
||||
if (typeof value.skillName !== 'string') return [];
|
||||
return [{
|
||||
skillName: value.skillName,
|
||||
skillArgs: typeof value.skillArgs === 'string' ? value.skillArgs : undefined,
|
||||
}];
|
||||
});
|
||||
return {
|
||||
kind: 'user',
|
||||
skillActivations: skillActivations.length > 0 ? skillActivations : undefined,
|
||||
};
|
||||
}
|
||||
|
|
@ -62,15 +62,29 @@ export const stepRetrySchema = z.object({
|
|||
export const turnStateSchema = z.enum(['queued', 'running', 'completed', 'failed', 'cancelled']);
|
||||
export const stepStateSchema = z.enum(['running', 'completed', 'interrupted', 'failed']);
|
||||
|
||||
export const textFrameSchema = z.object({
|
||||
export const transcriptSkillActivationSchema = z.object({
|
||||
skillName: z.string(),
|
||||
skillArgs: z.string().optional(),
|
||||
});
|
||||
|
||||
export const transcriptUserOriginSchema = z.object({
|
||||
kind: z.literal('user'),
|
||||
skillActivations: z.array(transcriptSkillActivationSchema).optional(),
|
||||
});
|
||||
|
||||
const textFrameShape = {
|
||||
kind: z.literal('text'),
|
||||
frameId: frameIdSchema,
|
||||
role: z.enum(['assistant', 'user']),
|
||||
text: z.string(),
|
||||
attachmentIds: z.array(z.string()).optional(),
|
||||
taskId: taskIdSchema.optional(),
|
||||
promptIds: z.array(z.string()).optional(),
|
||||
});
|
||||
};
|
||||
|
||||
export const textFrameSchema = z.discriminatedUnion('role', [
|
||||
z.object({ ...textFrameShape, role: z.literal('assistant'), origin: z.never().optional() }),
|
||||
z.object({ ...textFrameShape, role: z.literal('user'), origin: transcriptUserOriginSchema.optional() }),
|
||||
]);
|
||||
|
||||
export const thinkingFrameSchema = z.object({
|
||||
kind: z.literal('thinking'),
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import type { AgentTranscriptSnapshot } from '../ops/operation';
|
||||
import type { TranscriptAttachment } from '../model/attachment';
|
||||
import type { TranscriptFrame } from '../model/frame';
|
||||
import type { TranscriptFrame, TranscriptUserOrigin } from '../model/frame';
|
||||
import type { TranscriptItem, TranscriptMarker } from '../model/item';
|
||||
import type { TurnOrigin } from '../model/turn';
|
||||
import { daemonFileRefFromPairingPart } from '../contract/mediaRef';
|
||||
import { projectTranscriptUserOrigin } from '../contract/origin';
|
||||
|
||||
export type HistoryMediaSource =
|
||||
| { readonly kind: 'url'; readonly url: string }
|
||||
|
|
@ -81,6 +82,7 @@ export function groupMessagesIntoSnapshot(
|
|||
taskId: string | undefined;
|
||||
attachmentIds?: string[];
|
||||
promptIds?: readonly string[];
|
||||
origin?: TranscriptUserOrigin;
|
||||
steered?: boolean;
|
||||
}[] = [];
|
||||
let nextOrdinal = 0;
|
||||
|
|
@ -166,10 +168,16 @@ export function groupMessagesIntoSnapshot(
|
|||
if (leftovers.length === 0) return;
|
||||
pendingNotificationFrames = pendingNotificationFrames.filter((pending) => !pending.steered);
|
||||
for (const pending of leftovers) {
|
||||
const lastStep = turn?.steps.at(-1);
|
||||
if (turn === undefined || lastStep === undefined) {
|
||||
startTurn({ kind: 'user' }, pending.text, pending.attachmentIds);
|
||||
continue;
|
||||
const targetTurn = turn ?? startTurn({ kind: 'user' });
|
||||
let lastStep = targetTurn.steps.at(-1);
|
||||
if (lastStep === undefined) {
|
||||
const ordinal = targetTurn.steps.length + 1;
|
||||
lastStep = {
|
||||
stepId: `${targetTurn.turnId}.${ordinal}`,
|
||||
ordinal,
|
||||
frames: [],
|
||||
};
|
||||
targetTurn.steps.push(lastStep);
|
||||
}
|
||||
lastStep.frames.push({
|
||||
kind: 'text',
|
||||
|
|
@ -178,8 +186,9 @@ export function groupMessagesIntoSnapshot(
|
|||
text: pending.text,
|
||||
attachmentIds: pending.attachmentIds,
|
||||
promptIds: pending.promptIds,
|
||||
origin: pending.origin,
|
||||
});
|
||||
syncTurnItem(items, turn);
|
||||
syncTurnItem(items, targetTurn);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -244,6 +253,7 @@ export function groupMessagesIntoSnapshot(
|
|||
text: opening.text,
|
||||
taskId: undefined,
|
||||
attachmentIds: opening.attachmentIds,
|
||||
origin: projectTranscriptUserOrigin(message.origin),
|
||||
steered: true,
|
||||
});
|
||||
continue;
|
||||
|
|
@ -315,6 +325,7 @@ export function groupMessagesIntoSnapshot(
|
|||
taskId: pending.taskId,
|
||||
attachmentIds: pending.attachmentIds,
|
||||
promptIds: pending.promptIds,
|
||||
origin: pending.origin,
|
||||
});
|
||||
}
|
||||
pendingNotificationFrames = [];
|
||||
|
|
|
|||
|
|
@ -22,3 +22,4 @@ export * from './history/foldFacts';
|
|||
export * from './contract/schema';
|
||||
export * from './contract/events';
|
||||
export * from './contract/mediaRef';
|
||||
export * from './contract/origin';
|
||||
|
|
|
|||
|
|
@ -7,16 +7,37 @@ export type FrameRef = {
|
|||
readonly frameId: FrameId;
|
||||
};
|
||||
|
||||
export interface TextFrame {
|
||||
export interface TranscriptSkillActivation {
|
||||
readonly skillName: string;
|
||||
readonly skillArgs?: string;
|
||||
}
|
||||
|
||||
export interface TranscriptUserOrigin {
|
||||
readonly kind: 'user';
|
||||
readonly skillActivations?: readonly TranscriptSkillActivation[];
|
||||
}
|
||||
|
||||
interface TextFrameBase {
|
||||
readonly kind: 'text';
|
||||
readonly frameId: FrameId;
|
||||
readonly role: 'assistant' | 'user';
|
||||
readonly text: string;
|
||||
readonly attachmentIds?: readonly AttachmentId[];
|
||||
readonly taskId?: TaskId;
|
||||
readonly promptIds?: readonly string[];
|
||||
}
|
||||
|
||||
export interface AssistantTextFrame extends TextFrameBase {
|
||||
readonly role: 'assistant';
|
||||
readonly origin?: never;
|
||||
}
|
||||
|
||||
export interface UserTextFrame extends TextFrameBase {
|
||||
readonly role: 'user';
|
||||
readonly origin?: TranscriptUserOrigin;
|
||||
}
|
||||
|
||||
export type TextFrame = AssistantTextFrame | UserTextFrame;
|
||||
|
||||
export interface ThinkingFrame {
|
||||
readonly kind: 'thinking';
|
||||
readonly frameId: FrameId;
|
||||
|
|
|
|||
|
|
@ -372,6 +372,34 @@ describe('contract schemas', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('accepts provenance only on user text frames', () => {
|
||||
const base = {
|
||||
op: 'frame.upsert',
|
||||
turnId: 't1',
|
||||
stepId: 't1.1',
|
||||
} as const;
|
||||
expect(transcriptOperationSchema.safeParse({
|
||||
...base,
|
||||
frame: {
|
||||
kind: 'text',
|
||||
frameId: 't1.1.f1',
|
||||
role: 'user',
|
||||
text: 'steered in',
|
||||
origin: { kind: 'user', skillActivations: [{ skillName: 'review', skillArgs: 'strict' }] },
|
||||
},
|
||||
}).success).toBe(true);
|
||||
expect(transcriptOperationSchema.safeParse({
|
||||
...base,
|
||||
frame: {
|
||||
kind: 'text',
|
||||
frameId: 't1.1.f2',
|
||||
role: 'assistant',
|
||||
text: 'reply',
|
||||
origin: { kind: 'user' },
|
||||
},
|
||||
}).success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects mutually exclusive cursors and bad grades', () => {
|
||||
expect(() => transcriptGradeSpecSchema.parse({ '*': 'stream' })).toThrow();
|
||||
const ok = transcriptResponseSchema.safeParse({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue