refactor(agent): simplify inbox-driven turn admission

This commit is contained in:
_Kerman
2026-08-02 00:27:37 +08:00
parent d38c8bfaf3
commit dbdf270af0
104 changed files with 550 additions and 511 deletions

View File

@@ -9,8 +9,9 @@
* The state in force is folded from the session log (`plan/mode`, last one
* wins), so resume and fork restore it without a live mirror. User selections
* are held as pending intent until an in-turn step boundary. The service
* flushes from `agent/pre-step` before the affected request assembly;
* same-step request retries reuse their assembly.
* projects pending intent into the proposed step assembly, then flushes it
* from `agent/pre-step` only when the step is accepted. Same-step request
* retries reuse their assembly.
*
* The exit tool remains registered while plan mode is inactive so crossing a
* boundary changes only the prompt section, not the request tool catalog.
@@ -225,9 +226,11 @@ export class PlanModeService extends Service {
ctx.systemPrompt.section({
name: 'plan:policy',
order: 50,
text: context => context.agent !== undefined && foldPlanMode(context.agent.session.events)
? this.section
: '',
text: (context) => {
if (context.agent === undefined) return ''
const pending = this.pendingIntents.get(context.agent.session)
return (pending?.active ?? foldPlanMode(context.agent.session.events)) ? this.section : ''
},
})
// The plan projection unit (session-projection RFC): a pure double-event

View File

@@ -31,8 +31,7 @@ describe('plan-mode stream invariants', () => {
expect(() => { ctx.emit('session/event', session, event(true)) }).not.toThrow()
expect(() => { ctx.emit('session/event', session, event(false)) }).not.toThrow()
ctx.emit('session/event', session, {
type: 'turn/end', seq: 3, time: 3,
data: { turn: 1, reason: { kind: 'completed' } },
type: 'turn/end', seq: 3, time: 3, data: { turn: 1, step: 0, reason: { kind: 'completed' } },
})
})
@@ -67,7 +66,7 @@ describe('plan-mode stream invariants', () => {
const session = ctx.sessions.create()
session.append('turn/start', { turn: 1 })
session.append('plan/mode', { active: 'plan' as unknown as boolean })
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
session.append('turn/end', { turn: 1, step: 0, reason: { kind: 'completed' } })
await ctx.plugin(InvariantService, { enabled: true })
await expect(ctx.plugin(PlanModeInvariant).then(() => undefined)).rejects.toThrow(/expected a boolean/)
@@ -79,7 +78,7 @@ describe('plan-mode stream invariants', () => {
const session = ctx.sessions.create()
session.append('turn/start', { turn: 1 })
session.append('plan/mode', { active: true })
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
session.append('turn/end', { turn: 1, step: 0, reason: { kind: 'completed' } })
await ctx.plugin(InvariantService, { enabled: true })
await expect(ctx.plugin(PlanModeInvariant).then(() => undefined)).resolves.toBeUndefined()

View File

@@ -96,7 +96,7 @@ function openTurn(session: Session, turn = 0): void {
/** Close the open turn (the between-turns shape: selections commit immediately). */
function closeTurn(session: Session, turn = 0): void {
session.append('turn/end', { turn, reason: { kind: 'completed' } })
session.append('turn/end', { turn, step: 0, reason: { kind: 'completed' } })
}
/** Append a minimal `request/header` snapshot so the log has a "what the model was told" anchor. */
@@ -799,17 +799,17 @@ describe('exit_plan_mode', () => {
expect(ctx.planMode.get(agent)).toEqual({ active: true, pending: false })
})
it('an approved exit keeps plan guidance until the boundary and never removes the tool', async () => {
it('an approved exit projects the next assembly before the boundary and never removes the tool', async () => {
const { ctx, agent } = await setupWithReview({ selected: ['Approve'] })
const approved = await callExit(ctx, agent)
expect(approved.isError).toBe(false)
// Calls of the SAME assistant response (no boundary between) were
// requested under the plan-shaped header — the fold stays plan for that
// whole batch; the boundary flush is what flips the next step.
// Calls of the SAME assistant response were requested under the existing
// plan-shaped header. Pending state shapes only the proposed next
// assembly; the accepted boundary then commits the matching durable fold.
expect(foldPlanMode(agent.session.events)).toBe(true)
const assembly = await ctx.systemPrompt.assemble({ agent })
expect(assembly.tools.some(tool => tool.name === EXIT_PLAN_MODE)).toBe(true)
expect(assembly.sections.find(section => section.name === 'plan:policy')?.text).toBe(TEST_PLAN_SECTION)
expect(assembly.sections.find(section => section.name === 'plan:policy')?.text).toBe('')
await boundary(ctx, agent, 'step-start')
expect(foldPlanMode(agent.session.events)).toBe(false)
const afterExit = await ctx.systemPrompt.assemble({ agent })

View File

@@ -60,7 +60,7 @@ function runPlanCommand(session: Session, args: string, index: number): void {
function commitPlanMode(session: Session, active: boolean, turn: number): void {
session.append('turn/start', { turn })
session.append('plan/mode', { active })
session.append('turn/end', { turn, reason: { kind: 'completed' } })
session.append('turn/end', { turn, step: 0, reason: { kind: 'completed' } })
}
describe('plan projection unit', () => {