|
|
@@ -302,6 +302,33 @@ function viewFor(event: SessionEvent, log: readonly SessionEvent[]): ToolEventVi
|
|
|
return undefined
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * Fixture parallel of the plan unit's double-event fold: `command/run`
|
|
|
+ * records named `plan` set the wanted target (`off` → false, else true);
|
|
|
+ * `plan/mode` commits and clears it. `wanted` is exposed for the prompt
|
|
|
+ * boundary (the fixture's agent/step parallel).
|
|
|
+ */
|
|
|
+function foldPlan(log: readonly SessionEvent[]): { active: boolean; pending: boolean; wanted: boolean | null } {
|
|
|
+ let active = false
|
|
|
+ let wanted: boolean | null = null
|
|
|
+ for (const event of log) {
|
|
|
+ const item = event as unknown as { type: string; data?: Record<string, unknown> }
|
|
|
+ if (item.type === 'command/run' && item.data?.['name'] === 'plan') {
|
|
|
+ wanted = String(item.data['args'] ?? '').trim() !== 'off'
|
|
|
+ } else if (item.type === 'plan/mode') {
|
|
|
+ active = item.data?.['active'] === true
|
|
|
+ wanted = null
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return { active, pending: wanted !== null && wanted !== active, wanted }
|
|
|
+}
|
|
|
+
|
|
|
+/** The plan projection's wire view over the full log. */
|
|
|
+function planViewOf(log: readonly SessionEvent[]): { active: boolean; pending: boolean } {
|
|
|
+ const plan = foldPlan(log)
|
|
|
+ return { active: plan.active, pending: plan.pending }
|
|
|
+}
|
|
|
+
|
|
|
/** Fixture parallel of the host's projection units: whole current values per key over the full log. */
|
|
|
function projectionValuesOf(log: readonly SessionEvent[]): Record<string, unknown> {
|
|
|
const values: Record<string, unknown> = {}
|
|
|
@@ -311,6 +338,8 @@ function projectionValuesOf(log: readonly SessionEvent[]): Record<string, unknow
|
|
|
}
|
|
|
// Always present (tool-todo unit composed): null when no plan stands.
|
|
|
values['todos'] = backscanTodos(log) ?? null
|
|
|
+ // Always present (plan-mode unit composed): the {active, pending} view.
|
|
|
+ values['plan'] = planViewOf(log)
|
|
|
return values
|
|
|
}
|
|
|
|
|
|
@@ -333,6 +362,17 @@ function projectionFramesOf(id: SessionId, log: readonly SessionEvent[], event:
|
|
|
seq: event.seq,
|
|
|
}]
|
|
|
}
|
|
|
+ // The plan unit advances on its two folded event kinds.
|
|
|
+ if (type === 'plan/mode' || (type === 'command/run'
|
|
|
+ && (event as unknown as { data: { name?: string } }).data.name === 'plan')) {
|
|
|
+ return [{
|
|
|
+ type: 'session/projection',
|
|
|
+ sessionId: id,
|
|
|
+ key: 'plan',
|
|
|
+ value: planViewOf(log),
|
|
|
+ seq: event.seq,
|
|
|
+ }]
|
|
|
+ }
|
|
|
return []
|
|
|
}
|
|
|
|
|
|
@@ -804,6 +844,12 @@ export function createFixtureApi(options: FixtureOptions = {}): ApiProxy {
|
|
|
nextTurn.set(id, turn + 1)
|
|
|
setRunning(id, true)
|
|
|
append(id, { type: 'turn/start', data: { turn, trigger: { kind: 'message', source: { kind: 'user' } } } })
|
|
|
+ // Boundary flush parallel (the host's agent/step seam): an outstanding
|
|
|
+ // /plan selection commits as plan/mode inside the opened turn.
|
|
|
+ const plan = foldPlan(logOf(id))
|
|
|
+ if (plan.wanted !== null && plan.wanted !== plan.active) {
|
|
|
+ append(id, { type: 'plan/mode', data: { active: plan.wanted } })
|
|
|
+ }
|
|
|
append(id, { type: 'user/message', surfaceOp: 'append', data: userMessage(content) })
|
|
|
startReply(
|
|
|
id,
|
|
|
@@ -935,6 +981,7 @@ export function createFixtureApi(options: FixtureOptions = {}): ApiProxy {
|
|
|
{ name: 'compact', description: 'fixture:压缩当前会话上下文' },
|
|
|
{ name: 'echo', description: 'fixture:回显参数', input: { hint: 'text to echo' } },
|
|
|
{ name: 'goal-fixture', description: 'fixture:目标样本命令', input: { hint: '<objective>' } },
|
|
|
+ { name: 'plan', description: 'Enter or leave plan mode', input: { hint: '[off|message]' } },
|
|
|
],
|
|
|
})
|
|
|
},
|
|
|
@@ -954,6 +1001,10 @@ export function createFixtureApi(options: FixtureOptions = {}): ApiProxy {
|
|
|
compact: 'fixture:已压缩(假动作)',
|
|
|
echo: args.trim(),
|
|
|
'goal-fixture': `fixture:goal 已设置(${id})`,
|
|
|
+ // Mirrors the host handler's wording; the projection frame carries the state.
|
|
|
+ plan: args.trim() === 'off'
|
|
|
+ ? 'Leaving plan mode (applies from the next step).'
|
|
|
+ : 'Entering plan mode (applies from the next step). Use /plan off to leave.',
|
|
|
}
|
|
|
const text = name === undefined ? undefined : outcomes[name]
|
|
|
if (name === undefined || text === undefined) return ok(request, { matched: false as const })
|