refactor(agent): simplify inbox-driven turn admission
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user