refactor(agent): expose mutable inbox state

This commit is contained in:
_Kerman
2026-07-30 15:35:18 +08:00
parent c0ef93efc8
commit 4370004360
52 changed files with 534 additions and 913 deletions

View File

@@ -1198,7 +1198,7 @@ export function createTuiChat(
appendNotice(`Agent "${agent.id}" is disposed.`, 'error')
return
}
if (agent.acceptsNextStep) {
if (agent.status === 'running') {
// Steering is never subject to prompt admission; an attached snapshot
// drains beside it at the same step boundary through the outbox.
if (attachedContext !== undefined) {

View File

@@ -36,8 +36,6 @@ interface FakeAgent extends Agent {
export interface TuiHarnessOptions {
status?: AgentStatus
/** Override the fake agent's next-step capability independently of status. */
acceptsNextStep?: boolean
config?: Config
/** Leave the session event log empty instead of seeding one turn and step. */
omitInitialLifecycle?: boolean
@@ -193,9 +191,6 @@ export async function createTuiTestHarness<TerminalType extends Terminal, Exit e
options: options.agentOptions ?? { provider: 'deepseek', model: 'deepseek-v4-flash' },
session,
status: options.status ?? 'idle',
get acceptsNextStep() {
return options.acceptsNextStep ?? this.status === 'running'
},
ctx,
sent,
sentMessages,
@@ -205,13 +200,6 @@ export async function createTuiTestHarness<TerminalType extends Terminal, Exit e
injected,
injectedOptions,
cancelled,
send(input, options) {
sent.push(input.content)
sentMessages.push(input)
sentOptions.push(options)
return input.id
},
updateInbox: () => 'not-found',
followup(input) {
sent.push(input.content)
sentMessages.push(input)

View File

@@ -2636,45 +2636,6 @@ describe('pi-tui chat lifecycle and transcript', () => {
await dispose(result)
})
it('keeps a referenced prompt on admission when running no longer accepts next-step input', async () => {
const result = await setup({
status: 'running',
acceptsNextStep: false,
omitInitialLifecycle: true,
async configureContext(ctx) {
ctx.provide('tools', { get: () => undefined } as never)
await ctx.plugin(TestSessionQueryService)
await ctx.plugin(SessionReferenceService)
const source = ctx.sessions.create(SessionId('admission-src'), {
meta: { cwd: process.cwd(), createdAt: 1 },
})
appendUser(source, 'source background')
source.append('session/title', {
title: 'Admission source',
messageSeqs: [0],
source: { kind: 'fallback' },
})
},
})
result.terminal.send(formatSessionReferenceMention({
sessionId: SessionId('admission-src'),
label: 'Admission source',
}))
result.terminal.send('\r')
await vi.waitFor(() => { expect(result.agent.sent).toHaveLength(1) })
expect(result.agent.steered).toHaveLength(0)
expect(result.agent.injected).toHaveLength(0)
const decision = await agentEvents(result.ctx, result.agent).waterfall(
'agent/prompt-submit', result.agent.sentMessages[0]!,
new AbortController().signal, () => Promise.resolve({ kind: 'allow' as const }),
)
expect(decision.kind === 'allow' && decision.additionalContexts?.[0]?.source)
.toMatchObject({ kind: 'session-reference', references: [{ sessionId: 'admission-src' }] })
await dispose(result)
})
it('releases the reference-admission wrapper on the ordinary allowed path', async () => {
const result = await setup({
async configureContext(ctx) {
@@ -4974,8 +4935,8 @@ describe('terminal mounting', () => {
ctx.provide('tools', { get: () => undefined } as never)
const session = ctx.sessions.create(SessionId('main'))
ctx.agents.register({
id: session.id, options: {}, session, status: 'idle', acceptsNextStep: false, ctx,
followup: () => {}, steer: () => {}, inject: () => {}, send: () => {}, updateInbox: () => 'not-found', cancel() {}, whenIdle: () => Promise.resolve(),
id: session.id, options: {}, session, status: 'idle', ctx,
followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
})
const terminal = new FakeTerminal()
mountTui(ctx, { theme: { color: false } }, { terminal, exit: vi.fn() })
@@ -4999,8 +4960,8 @@ describe('terminal mounting', () => {
ctx.provide('tools', { get: () => undefined } as never)
const session = ctx.sessions.create(SessionId('main'))
ctx.agents.register({
id: session.id, options: {}, session, status: 'idle', acceptsNextStep: false, ctx,
followup: () => {}, steer: () => {}, inject: () => {}, send: () => {}, updateInbox: () => 'not-found', cancel() {}, whenIdle: () => Promise.resolve(),
id: session.id, options: {}, session, status: 'idle', ctx,
followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
})
const terminal = new FakeTerminal()
// Mirror dsh-tui's own inject (minus loader, the absence under test).
@@ -5034,15 +4995,15 @@ describe('terminal mounting', () => {
const otherSession = ctx.sessions.create(SessionId('other-session'))
ctx.agents.register({
id: otherSession.id, options: {}, session: otherSession, status: 'idle', acceptsNextStep: false, ctx,
followup: () => {}, steer: () => {}, inject: () => {}, send: () => {}, updateInbox: () => 'not-found', cancel() {}, whenIdle: () => Promise.resolve(),
id: otherSession.id, options: {}, session: otherSession, status: 'idle', ctx,
followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
})
expect(terminal.started).toBe(0)
const session = ctx.sessions.create(SessionId('late-session'))
const agent = {
id: session.id, options: {}, session, status: 'idle', acceptsNextStep: false, ctx,
followup: () => {}, steer: () => {}, inject: () => {}, send: () => {}, updateInbox: () => 'not-found', cancel() {}, whenIdle: () => Promise.resolve(),
id: session.id, options: {}, session, status: 'idle', ctx,
followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
} as Agent
ctx.agents.register(agent)
await tick()
@@ -5072,8 +5033,8 @@ describe('terminal mounting', () => {
const session = ctx.sessions.create(SessionId('main-session'))
ctx.agents.register({
id: session.id, options: {}, session, status: 'idle', acceptsNextStep: false, ctx,
followup: () => {}, steer: () => {}, inject: () => {}, send: () => {}, updateInbox: () => 'not-found', cancel() {}, whenIdle: () => Promise.resolve(),
id: session.id, options: {}, session, status: 'idle', ctx,
followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
})
await tick()
expect(terminal.started).toBe(0)
@@ -5116,8 +5077,8 @@ describe('terminal mounting', () => {
session.append('turn/start', { turn: 1 })
session.append('step/start', { turn: 1, step: 1 })
ctx.agents.register({
id: session.id, options: {}, session, status: 'running', acceptsNextStep: true, ctx,
followup: () => {}, steer: () => {}, inject: () => {}, send: () => {}, updateInbox: () => 'not-found', cancel() {}, whenIdle: () => Promise.resolve(),
id: session.id, options: {}, session, status: 'running', ctx,
followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
})
const terminal = new FakeTerminal()
terminal.start = () => { throw new Error('terminal startup failed') }