refactor(agent): simplify inbox-driven turn admission
This commit is contained in:
@@ -220,8 +220,8 @@ describe('new-session default', () => {
|
||||
defaultPreset: 'danger-full-access',
|
||||
})
|
||||
const legacy = freshSession('legacy-source')
|
||||
legacy.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
legacy.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
|
||||
legacy.append('turn/start', { turn: 1 })
|
||||
legacy.append('turn/end', { turn: 1, step: 0, reason: { kind: 'completed' } })
|
||||
const resumed = ctx.sessions.create(SessionId('legacy-resumed'), { seed: legacy.events })
|
||||
expect(ctx.permission.current(resumed.events)).toBe('workspace-write')
|
||||
expect(resumed.events.slice(-3).map(event => event.type)).toEqual([
|
||||
|
||||
@@ -225,7 +225,6 @@ export async function createTuiTestHarness<TerminalType extends Terminal, Exit e
|
||||
injectedOptions.push(input)
|
||||
return input.id
|
||||
},
|
||||
reserveTurnAdmission: () => undefined,
|
||||
cancel(cause) {
|
||||
cancelled.push(cause)
|
||||
},
|
||||
|
||||
@@ -382,7 +382,7 @@ describe('TUI terminal-state snapshots', () => {
|
||||
],
|
||||
})
|
||||
session.append('step/end', { turn: 1, step: 1 })
|
||||
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
|
||||
session.append('turn/end', { turn: 1, step: 1, reason: { kind: 'completed' } })
|
||||
session.append('turn/start', {
|
||||
turn: 2,
|
||||
})
|
||||
@@ -429,7 +429,7 @@ describe('TUI terminal-state snapshots', () => {
|
||||
},
|
||||
}),
|
||||
}, { surfaceOp: 'append' })
|
||||
harness.session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
|
||||
harness.session.append('turn/end', { turn: 1, step: 1, reason: { kind: 'completed' } })
|
||||
await checkpoint('retry-recovered', harness.terminal, { includeScrollback: true })
|
||||
await disposeSnapshot(harness)
|
||||
})
|
||||
@@ -448,9 +448,7 @@ describe('TUI terminal-state snapshots', () => {
|
||||
delayMs: 1_000,
|
||||
failure: { message: 'temporary transport failure', code: 'TRANSPORT' },
|
||||
})
|
||||
harness.session.append('turn/end', {
|
||||
turn: 1,
|
||||
reason: { kind: 'aborted', reason: { kind: 'user' } },
|
||||
harness.session.append('turn/end', { turn: 1, step: 1, reason: { kind: 'aborted', reason: { kind: 'user' } },
|
||||
})
|
||||
})
|
||||
await checkpoint('retry-cancelled', harness.terminal, { includeScrollback: true })
|
||||
@@ -466,12 +464,8 @@ describe('TUI terminal-state snapshots', () => {
|
||||
step: 3,
|
||||
chunk: { type: 'text-delta', index: 0, text: 'discarded terminal partial output' },
|
||||
})
|
||||
harness.session.append('turn/end', {
|
||||
turn: 1,
|
||||
reason: {
|
||||
kind: 'error',
|
||||
error: { message: 'provider still unavailable', code: 'SERVER', status: 503 },
|
||||
},
|
||||
harness.session.append('turn/end', { turn: 1, step: 3, reason: { kind: 'error', error: { message: 'provider still unavailable', code: 'SERVER', status: 503 },
|
||||
},
|
||||
})
|
||||
})
|
||||
await checkpoint('retry-exhausted', harness.terminal, { includeScrollback: true })
|
||||
@@ -647,9 +641,7 @@ describe('TUI terminal-state snapshots', () => {
|
||||
todos: [{ content: `Unsafe todo ${CONTROL_PROBE}`, status: 'in_progress' }],
|
||||
})
|
||||
session.append('step/end', { turn: 1, step: 1 })
|
||||
session.append('turn/end', {
|
||||
turn: 1,
|
||||
reason: { kind: 'error', error: `Unsafe turn error ${CONTROL_PROBE}` },
|
||||
session.append('turn/end', { turn: 1, step: 1, reason: { kind: 'error', error: `Unsafe turn error ${CONTROL_PROBE}` },
|
||||
})
|
||||
},
|
||||
}, { columns: 100, rows: 34 })
|
||||
@@ -809,24 +801,18 @@ describe('TUI terminal-state snapshots', () => {
|
||||
harness.terminal.send('\r')
|
||||
agentEvents(harness.ctx, harness.agent).emit('agent/error', 1, 1, new Error('provider stream failed after partial output'))
|
||||
harness.session.append('step/end', { turn: 1, step: 1 })
|
||||
harness.session.append('turn/end', {
|
||||
turn: 1,
|
||||
reason: { kind: 'error', error: 'provider stream failed after partial output' },
|
||||
harness.session.append('turn/end', { turn: 1, step: 1, reason: { kind: 'error', error: 'provider stream failed after partial output' },
|
||||
})
|
||||
harness.session.append('turn/start', { turn: 2 })
|
||||
harness.session.append('turn/end', {
|
||||
turn: 2,
|
||||
reason: { kind: 'interrupted' },
|
||||
harness.session.append('turn/end', { turn: 2, step: 0, reason: { kind: 'interrupted' },
|
||||
})
|
||||
harness.session.append('turn/start', { turn: 3 })
|
||||
harness.session.append('turn/end', {
|
||||
turn: 3,
|
||||
reason: { kind: 'aborted', reason: { kind: 'disposed' } },
|
||||
harness.session.append('turn/end', { turn: 3, step: 0, reason: { kind: 'aborted', reason: { kind: 'disposed' } },
|
||||
})
|
||||
harness.session.append('turn/start', { turn: 4 })
|
||||
// A merge-extensible turn-end kind unknown to the TUI still surfaces its
|
||||
// name so the agent never stops without a visible reason.
|
||||
harness.session.append('turn/end', { turn: 4, reason: { kind: 'plugin-policy' } as never })
|
||||
harness.session.append('turn/end', { turn: 4, step: 0, reason: { kind: 'plugin-policy' } as never })
|
||||
})
|
||||
await checkpoint('errors-and-help', harness.terminal, { includeScrollback: true })
|
||||
|
||||
@@ -877,7 +863,7 @@ describe('TUI terminal-state snapshots', () => {
|
||||
}),
|
||||
}, surfaceOp: 'append' },
|
||||
{ type: 'step/end', seq: 5, time: Date.parse(`${day}T00:00:06Z`), data: { turn: 1, step: 1 } },
|
||||
{ type: 'turn/end', seq: 6, time: Date.parse(`${day}T00:00:07Z`), data: { turn: 1, reason: { kind: 'completed' } } },
|
||||
{ type: 'turn/end', seq: 6, time: Date.parse(`${day}T00:00:07Z`), data: { turn: 1, step: 1, reason: { kind: 'completed' } } },
|
||||
{ type: 'session/title', seq: 7, time: Date.parse(`${day}T00:00:08Z`), data: { title, messageSeqs: [1], source: { kind: 'fallback' } } },
|
||||
// A prior pickup, dated well after the work: the picker must still
|
||||
// show the work's date, not the pickup's.
|
||||
|
||||
@@ -271,7 +271,7 @@ describe('goodbye message and /resume', () => {
|
||||
}),
|
||||
}, surfaceOp: 'append' },
|
||||
{ type: 'step/end', seq: 5, time: time + 5, data: { turn: 1, step: 1 } },
|
||||
{ type: 'turn/end', seq: 6, time: time + 6, data: { turn: 1, reason } },
|
||||
{ type: 'turn/end', seq: 6, time: time + 6, data: { turn: 1, step: 1, reason } },
|
||||
{ type: 'session/title', seq: 7, time: time + 7, data: { title, messageSeqs: [1], source: { kind: 'fallback' } } },
|
||||
]
|
||||
|
||||
@@ -466,7 +466,7 @@ describe('goodbye message and /resume', () => {
|
||||
|
||||
it.each([
|
||||
[{ kind: 'aborted', reason: { kind: 'user' } }, 'cancelled'],
|
||||
[{ kind: 'error', error: 'failed' }, 'error'],
|
||||
[{ kind: 'error', step: 1, error: 'failed' }, 'error'],
|
||||
[{ kind: 'aborted', reason: { kind: 'disposed' } }, 'disposed'],
|
||||
[{ kind: 'max-tokens' }, 'max tokens'],
|
||||
[{ kind: 'interrupted' }, 'interrupted'],
|
||||
@@ -1372,12 +1372,10 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
}), { surfaceOp: 'append' })
|
||||
appendAssistant(result.session, [])
|
||||
result.session.append('step/end', { turn: 1, step: 1 })
|
||||
result.session.append('turn/end', {
|
||||
turn: 1,
|
||||
reason: { kind: 'aborted', reason: { kind: 'user' } },
|
||||
result.session.append('turn/end', { turn: 1, step: 1, reason: { kind: 'aborted', reason: { kind: 'user' } },
|
||||
})
|
||||
result.session.append('turn/start', { turn: 2 })
|
||||
result.session.append('turn/end', { turn: 2, reason: { kind: 'completed' } })
|
||||
result.session.append('turn/end', { turn: 2, step: 0, reason: { kind: 'completed' } })
|
||||
result.session.append('turn/start', { turn: 3 })
|
||||
result.session.append('step/start', { turn: 3, step: 1 })
|
||||
result.session.append('assistant/chunk', {
|
||||
@@ -1871,7 +1869,7 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
session.append('assistant/chunk', { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'done' } })
|
||||
clock += 1_000
|
||||
session.append('step/end', { turn: 1, step: 1 })
|
||||
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
|
||||
session.append('turn/end', { turn: 1, step: 1, reason: { kind: 'completed' } })
|
||||
},
|
||||
})
|
||||
result.agent.status = 'running'
|
||||
@@ -4001,31 +3999,25 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
agentEvents(events.ctx, unrelatedAgent).emit('agent/disposed')
|
||||
agentEvents(events.ctx, events.agent).emit('agent/error', 1, 1, new Error('live failure'))
|
||||
events.session.append('step/end', { turn: 1, step: 1 })
|
||||
events.session.append('turn/end', { turn: 1, reason: { kind: 'error', error: 'live failure' } })
|
||||
events.session.append('turn/end', { turn: 1, step: 1, reason: { kind: 'error', error: 'live failure' } })
|
||||
events.session.append('turn/start', { turn: 2 })
|
||||
events.session.append('turn/end', { turn: 2, reason: { kind: 'error', error: 'durable failure' } })
|
||||
events.session.append('turn/end', { turn: 2, step: 0, reason: { kind: 'error', error: 'durable failure' } })
|
||||
events.session.append('turn/start', { turn: 3 })
|
||||
events.session.append('turn/end', {
|
||||
turn: 3,
|
||||
reason: { kind: 'aborted', reason: { kind: 'user' } },
|
||||
events.session.append('turn/end', { turn: 3, step: 0, reason: { kind: 'aborted', reason: { kind: 'user' } },
|
||||
})
|
||||
events.session.append('turn/start', { turn: 4 })
|
||||
events.session.append('turn/end', { turn: 4, reason: { kind: 'max-tokens' } })
|
||||
events.session.append('turn/end', { turn: 4, step: 0, reason: { kind: 'max-tokens' } })
|
||||
events.session.append('turn/start', { turn: 5 })
|
||||
events.session.append('turn/end', { turn: 5, reason: { kind: 'interrupted' } })
|
||||
events.session.append('turn/end', { turn: 5, step: 0, reason: { kind: 'interrupted' } })
|
||||
events.session.append('turn/start', { turn: 6 })
|
||||
events.session.append('turn/end', {
|
||||
turn: 6,
|
||||
reason: { kind: 'error', error: { message: 'structured provider failure', code: 'SERVER' } },
|
||||
events.session.append('turn/end', { turn: 6, step: 0, reason: { kind: 'error', error: { message: 'structured provider failure', code: 'SERVER' } },
|
||||
})
|
||||
events.session.append('turn/start', { turn: 8 })
|
||||
events.session.append('turn/end', {
|
||||
turn: 8,
|
||||
reason: { kind: 'aborted', reason: { kind: 'disposed' } },
|
||||
events.session.append('turn/end', { turn: 8, step: 0, reason: { kind: 'aborted', reason: { kind: 'disposed' } },
|
||||
})
|
||||
events.session.append('turn/start', { turn: 9 })
|
||||
// Merge-extensible reason kind unknown to the TUI still names the stop.
|
||||
events.session.append('turn/end', { turn: 9, reason: { kind: 'plugin-policy' } as never })
|
||||
events.session.append('turn/end', { turn: 9, step: 0, reason: { kind: 'plugin-policy' } as never })
|
||||
agentEvents(events.ctx, events.agent).emit('agent/disposed')
|
||||
await tick()
|
||||
expect(events.terminal.output).toContain('live failure')
|
||||
@@ -5454,8 +5446,8 @@ describe('terminal mounting', () => {
|
||||
const session = ctx.sessions.create(SessionId('main'))
|
||||
ctx.agents.register({
|
||||
id: session.id, options: {}, session, inbox: new Inbox(session, { inserted: () => {}, discarded: () => {} }),
|
||||
status: 'idle', acceptsNextStep: false, ctx,
|
||||
send: () => {}, updateInbox: () => 'not-found', reserveTurnAdmission: () => undefined,
|
||||
status: 'idle', ctx,
|
||||
send: () => {},
|
||||
followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
})
|
||||
const terminal = new FakeTerminal()
|
||||
@@ -5481,8 +5473,8 @@ describe('terminal mounting', () => {
|
||||
const session = ctx.sessions.create(SessionId('main'))
|
||||
ctx.agents.register({
|
||||
id: session.id, options: {}, session, inbox: new Inbox(session, { inserted: () => {}, discarded: () => {} }),
|
||||
status: 'idle', acceptsNextStep: false, ctx,
|
||||
send: () => {}, updateInbox: () => 'not-found', reserveTurnAdmission: () => undefined,
|
||||
status: 'idle', ctx,
|
||||
send: () => {},
|
||||
followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
})
|
||||
const terminal = new FakeTerminal()
|
||||
@@ -5518,8 +5510,8 @@ describe('terminal mounting', () => {
|
||||
const otherSession = ctx.sessions.create(SessionId('other-session'))
|
||||
ctx.agents.register({
|
||||
id: otherSession.id, options: {}, session: otherSession, inbox: new Inbox(otherSession, { inserted: () => {}, discarded: () => {} }),
|
||||
status: 'idle', acceptsNextStep: false, ctx,
|
||||
send: () => {}, updateInbox: () => 'not-found', reserveTurnAdmission: () => undefined,
|
||||
status: 'idle', ctx,
|
||||
send: () => {},
|
||||
followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
})
|
||||
expect(terminal.started).toBe(0)
|
||||
@@ -5527,8 +5519,8 @@ describe('terminal mounting', () => {
|
||||
const session = ctx.sessions.create(SessionId('late-session'))
|
||||
const agent = {
|
||||
id: session.id, options: {}, session, inbox: new Inbox(session, { inserted: () => {}, discarded: () => {} }),
|
||||
status: 'idle', acceptsNextStep: false, ctx,
|
||||
send: () => {}, updateInbox: () => 'not-found', reserveTurnAdmission: () => undefined,
|
||||
status: 'idle', ctx,
|
||||
send: () => {},
|
||||
followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
} as Agent
|
||||
ctx.agents.register(agent)
|
||||
@@ -5560,8 +5552,8 @@ describe('terminal mounting', () => {
|
||||
const session = ctx.sessions.create(SessionId('main-session'))
|
||||
ctx.agents.register({
|
||||
id: session.id, options: {}, session, inbox: new Inbox(session, { inserted: () => {}, discarded: () => {} }),
|
||||
status: 'idle', acceptsNextStep: false, ctx,
|
||||
send: () => {}, updateInbox: () => 'not-found', reserveTurnAdmission: () => undefined,
|
||||
status: 'idle', ctx,
|
||||
send: () => {},
|
||||
followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
})
|
||||
await tick()
|
||||
@@ -5606,8 +5598,8 @@ describe('terminal mounting', () => {
|
||||
session.append('step/start', { turn: 1, step: 1 })
|
||||
ctx.agents.register({
|
||||
id: session.id, options: {}, session, inbox: new Inbox(session, { inserted: () => {}, discarded: () => {} }),
|
||||
status: 'running', acceptsNextStep: true, ctx,
|
||||
send: () => {}, updateInbox: () => 'not-found', reserveTurnAdmission: () => undefined,
|
||||
status: 'running', ctx,
|
||||
send: () => {},
|
||||
followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
})
|
||||
const terminal = new FakeTerminal()
|
||||
|
||||
@@ -102,6 +102,22 @@ const NEVER_SENTENCE = 'Approval prompts are disabled in this session: actions t
|
||||
/** Model-facing statement for an interactive policy that may still fail closed. */
|
||||
const ASK_SENTENCE = 'Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.'
|
||||
|
||||
/** Read the latest visible policy from the runtime-context projection owned by system-prompt. */
|
||||
function toldApprovalPolicy(session: Session): ApprovalPolicy | undefined {
|
||||
const messages = session.deriveMessages()
|
||||
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
||||
const message = messages[index]
|
||||
if (message?.source.kind !== 'plugin' || message.source.plugin !== '@deepseek-ai/dsh-system-prompt') continue
|
||||
for (const block of message.content) {
|
||||
if (block.type !== 'text') continue
|
||||
if (block.text.includes(NEVER_SENTENCE)) return 'never'
|
||||
if (block.text.includes(ASK_SENTENCE)) return 'ask'
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* The session's approval-policy override: the last `approval/policy` event in
|
||||
* the log, or undefined when the session never switched (callers apply the
|
||||
@@ -249,8 +265,7 @@ export class ApprovalService extends Service {
|
||||
// Same fold effectivePolicy performs — override is scanned here anyway
|
||||
// for POSITIONAL attribution; the default lives once, in the method.
|
||||
const current = this.effectivePolicy(session)
|
||||
const header = session.requestHeader()
|
||||
const told = toldApprovalPolicy(header?.system)
|
||||
const told = toldApprovalPolicy(session)
|
||||
// Cold start (nothing ever told) narrates nothing — the section about
|
||||
// to go out states the truth, and there is no delta to explain.
|
||||
if (told === undefined || told === current) return decision
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { CallId } from '@deepseek-ai/dsh-llm'
|
||||
import { agentEvents, type Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { CallId, createUserMessage } from '@deepseek-ai/dsh-llm'
|
||||
import { carrierKeyOf, createScope } from '@deepseek-ai/dsh-scope'
|
||||
import type { Scope } from '@deepseek-ai/dsh-scope'
|
||||
import SessionStore, { Session, SessionId } from '@deepseek-ai/dsh-session'
|
||||
@@ -352,8 +352,6 @@ describe('ApprovalService.request', () => {
|
||||
describe('approval policy (the approval/policy fold)', () => {
|
||||
const NEVER_SENTENCE = 'Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).'
|
||||
const ASK_SENTENCE = 'Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.'
|
||||
const ASK_MARKER = '<!-- dsh-user-approval-policy:ask -->'
|
||||
const NEVER_MARKER = '<!-- dsh-user-approval-policy:never -->'
|
||||
|
||||
/**
|
||||
* An agent stand-in over a REAL Session — gate, section, and narrator fold
|
||||
@@ -372,22 +370,23 @@ describe('approval policy (the approval/policy fold)', () => {
|
||||
|
||||
const submitPrompt = async (ctx: Context, agent: Agent): Promise<void> => {
|
||||
const signal = new AbortController().signal
|
||||
const configured = ctx.get('approval')?.config.policy ?? 'ask'
|
||||
const current = effectiveApprovalPolicy(agent.session.events) ?? configured
|
||||
const runtimeContext = createUserMessage({
|
||||
content: [{ type: 'text', text: current === 'never' ? NEVER_SENTENCE : ASK_SENTENCE }],
|
||||
source: { kind: 'plugin', plugin: '@deepseek-ai/dsh-system-prompt' },
|
||||
})
|
||||
const decision = await agentEvents(ctx, agent).waterfall(
|
||||
'agent/pre-step',
|
||||
[],
|
||||
{ turn: 1, step: 1, signal },
|
||||
() => Promise.resolve({ kind: 'enter' as const, messages: [] }),
|
||||
() => Promise.resolve({ kind: 'enter' as const, messages: [runtimeContext] }),
|
||||
)
|
||||
if (decision.kind === 'enter') {
|
||||
for (const message of decision.messages) {
|
||||
agent.session.append('user/message', message, { surfaceOp: 'append' })
|
||||
}
|
||||
const configured = ctx.get('approval')?.config.policy ?? 'ask'
|
||||
const current = effectiveApprovalPolicy(agent.session.events) ?? configured
|
||||
appendHeader(
|
||||
agent.session,
|
||||
current === 'never' ? `${NEVER_SENTENCE}\n${NEVER_MARKER}` : ASK_MARKER,
|
||||
)
|
||||
appendHeader(agent.session)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,9 +397,17 @@ describe('approval policy (the approval/policy fold)', () => {
|
||||
? [event.data.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('')]
|
||||
: [])
|
||||
|
||||
/** Append a `request/header` snapshot whose system text is exactly `system`. */
|
||||
function appendHeader(session: Session, system: string): void {
|
||||
session.append('request/header', { header: { config: { provider: 'mock', model: 'mock' }, system }, reason: 'initial' })
|
||||
/** Append the stable system header that follows one entered prompt. */
|
||||
function appendHeader(session: Session): void {
|
||||
session.append('request/header', { header: { config: { provider: 'mock', model: 'mock' }, system: 'persona' }, reason: 'initial' })
|
||||
}
|
||||
|
||||
/** Append one model-visible runtime-context snapshot owned by system-prompt. */
|
||||
function appendToldPolicy(session: Session, policy: 'ask' | 'never'): void {
|
||||
session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: policy === 'never' ? NEVER_SENTENCE : ASK_SENTENCE }],
|
||||
source: { kind: 'plugin', plugin: '@deepseek-ai/dsh-system-prompt' },
|
||||
}), { surfaceOp: 'append' })
|
||||
}
|
||||
|
||||
it('folds to the last event, or undefined without one', () => {
|
||||
@@ -542,7 +549,8 @@ describe('approval policy (the approval/policy fold)', () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(ApprovalService)
|
||||
const { agent, session } = sessionAgent('sess-narr-rejected')
|
||||
appendHeader(session, ASK_MARKER)
|
||||
appendToldPolicy(session, 'ask')
|
||||
appendHeader(session)
|
||||
setApprovalPolicy(session, 'never')
|
||||
const signal = new AbortController().signal
|
||||
|
||||
@@ -557,13 +565,14 @@ describe('approval policy (the approval/policy fold)', () => {
|
||||
expect(narrations(session)).toEqual([])
|
||||
})
|
||||
|
||||
it('reads what the model was told back from the folded header text after a restart', async () => {
|
||||
// A session whose last request carried the never sentence resumes under
|
||||
it('reads what the model was told from visible runtime context after a restart', async () => {
|
||||
// A session whose retained context stated never resumes under
|
||||
// an ask default: the narrator attributes the change to the operator.
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(ApprovalService)
|
||||
const { agent, session } = sessionAgent('sess-narr-2')
|
||||
appendHeader(session, `persona\n\n${NEVER_SENTENCE}\n${NEVER_MARKER}`)
|
||||
appendToldPolicy(session, 'never')
|
||||
appendHeader(session)
|
||||
await submitPrompt(ctx, agent)
|
||||
expect(narrations(session)).toEqual(['The approval policy changed from "never" to "ask" (changed by the operator/config).'])
|
||||
})
|
||||
@@ -581,7 +590,8 @@ describe('approval policy (the approval/policy fold)', () => {
|
||||
})
|
||||
await ctx.plugin(ApprovalService)
|
||||
const { agent, session } = sessionAgent('sess-narr-retry')
|
||||
appendHeader(session, ASK_MARKER)
|
||||
appendToldPolicy(session, 'ask')
|
||||
appendHeader(session)
|
||||
setApprovalPolicy(session, 'never')
|
||||
|
||||
await expect(submitPrompt(ctx, agent)).rejects.toThrow('outer failure')
|
||||
@@ -594,7 +604,8 @@ describe('approval policy (the approval/policy fold)', () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(ApprovalService)
|
||||
const { agent, session } = sessionAgent('sess-narr-inherited')
|
||||
appendHeader(session, ASK_MARKER)
|
||||
appendToldPolicy(session, 'ask')
|
||||
appendHeader(session)
|
||||
session.append('approval/policy', { policy: 'never', source: 'delegation' })
|
||||
|
||||
await submitPrompt(ctx, agent)
|
||||
@@ -602,11 +613,12 @@ describe('approval policy (the approval/policy fold)', () => {
|
||||
expect(narrations(session)).toEqual(['The approval policy changed from "ask" to "never" (inherited from the delegating session).'])
|
||||
})
|
||||
|
||||
it('narrates a config default drift from the logged ask marker', async () => {
|
||||
it('narrates a config default drift from retained runtime context', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(ApprovalService, { policy: 'never' })
|
||||
const { agent, session } = sessionAgent('sess-narr-3')
|
||||
appendHeader(session, `persona only\n${ASK_MARKER}`)
|
||||
appendToldPolicy(session, 'ask')
|
||||
appendHeader(session)
|
||||
await submitPrompt(ctx, agent)
|
||||
expect(narrations(session)).toEqual(['The approval policy changed from "ask" to "never" (changed by the operator/config).'])
|
||||
})
|
||||
@@ -615,40 +627,64 @@ describe('approval policy (the approval/policy fold)', () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(ApprovalService, { policy: 'never' })
|
||||
const { agent, session } = sessionAgent('sess-narr-4')
|
||||
appendHeader(session, `persona only\n${ASK_MARKER}`)
|
||||
appendToldPolicy(session, 'ask')
|
||||
appendHeader(session)
|
||||
setApprovalPolicy(session, 'ask')
|
||||
appendHeader(session, `persona only\n${ASK_MARKER}`)
|
||||
appendHeader(session)
|
||||
await submitPrompt(ctx, agent)
|
||||
expect(narrations(session)).toEqual([])
|
||||
})
|
||||
|
||||
it('does not infer never from deployment prose that quotes the never sentence', async () => {
|
||||
it('does not infer never from an unowned message that quotes the never sentence', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(ApprovalService)
|
||||
const { agent, session } = sessionAgent('sess-narr-spoof-prose')
|
||||
appendHeader(session, `persona quotes this warning: ${NEVER_SENTENCE}\n${ASK_MARKER}`)
|
||||
session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: NEVER_SENTENCE }],
|
||||
source: { kind: 'user' },
|
||||
}), { surfaceOp: 'append' })
|
||||
appendToldPolicy(session, 'ask')
|
||||
appendHeader(session)
|
||||
await submitPrompt(ctx, agent)
|
||||
expect(narrations(session)).toEqual([])
|
||||
})
|
||||
|
||||
it('treats a legacy header with no source-owned marker as untold', async () => {
|
||||
it('treats a legacy header with no owned runtime context as untold', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(ApprovalService, { policy: 'never' })
|
||||
const { agent, session } = sessionAgent('sess-narr-unmarked-header')
|
||||
appendHeader(session, 'legacy persona-only header')
|
||||
appendHeader(session)
|
||||
await submitPrompt(ctx, agent)
|
||||
expect(narrations(session)).toEqual([])
|
||||
})
|
||||
|
||||
it('uses the service marker after an earlier persona marker', async () => {
|
||||
it('uses the latest owned runtime-context snapshot', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(ApprovalService)
|
||||
const { agent, session } = sessionAgent('sess-narr-spoof-marker')
|
||||
appendHeader(session, `persona quotes ${NEVER_MARKER}\n${ASK_MARKER}`)
|
||||
appendToldPolicy(session, 'never')
|
||||
appendToldPolicy(session, 'ask')
|
||||
appendHeader(session)
|
||||
await submitPrompt(ctx, agent)
|
||||
expect(narrations(session)).toEqual([])
|
||||
})
|
||||
|
||||
it('does not fall through a newer complete runtime-context snapshot', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(ApprovalService)
|
||||
const { agent, session } = sessionAgent('sess-narr-latest-context')
|
||||
appendToldPolicy(session, 'never')
|
||||
session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'Current runtime context:\n\nUnrelated context only.' }],
|
||||
source: { kind: 'plugin', plugin: '@deepseek-ai/dsh-system-prompt' },
|
||||
}), { surfaceOp: 'append' })
|
||||
appendHeader(session)
|
||||
|
||||
await submitPrompt(ctx, agent)
|
||||
|
||||
expect(narrations(session)).toEqual([])
|
||||
})
|
||||
|
||||
it('disposes the service prompt section and pre-step narrator together (HMR safety)', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt)
|
||||
@@ -659,12 +695,14 @@ describe('approval policy (the approval/policy fold)', () => {
|
||||
(await ctx.systemPrompt.assemble({ agent: live.agent })).contexts.find(context => context.name === 'approval:policy')
|
||||
expect(await contextFor()).toBeDefined()
|
||||
|
||||
appendHeader(live.session, `persona\n${ASK_MARKER}`)
|
||||
appendToldPolicy(live.session, 'ask')
|
||||
appendHeader(live.session)
|
||||
setApprovalPolicy(live.session, 'never')
|
||||
await submitPrompt(ctx, live.agent)
|
||||
expect(narrations(live.session)).toEqual(['The approval policy changed from "ask" to "never" (changed by the user).'])
|
||||
|
||||
appendHeader(afterDispose.session, `persona\n${ASK_MARKER}`)
|
||||
appendToldPolicy(afterDispose.session, 'ask')
|
||||
appendHeader(afterDispose.session)
|
||||
setApprovalPolicy(afterDispose.session, 'never')
|
||||
await fiber.dispose()
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ describe('approval invariants', () => {
|
||||
await ctx.plugin(InvariantService)
|
||||
await ctx.plugin(ApprovalInvariant)
|
||||
expect(() => session.append('approval/decided', { id, outcome: 'cancelled' })).not.toThrow()
|
||||
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
|
||||
session.append('turn/end', { turn: 1, step: 0, reason: { kind: 'completed' } })
|
||||
})
|
||||
|
||||
it('adopts a bare session first observed through publication', async () => {
|
||||
@@ -77,7 +77,7 @@ describe('approval invariants', () => {
|
||||
await ctx.plugin(SessionStore)
|
||||
const session = ctx.sessions.create()
|
||||
startTurn(session)
|
||||
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
|
||||
session.append('turn/end', { turn: 1, step: 0, reason: { kind: 'completed' } })
|
||||
session.append('approval/asked', {
|
||||
id: ApprovalRequestId('ask-replay'), toolName: 'bash',
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user