refactor(agent-loop): rely on eager session persistence

This commit is contained in:
_Kerman
2026-07-24 16:40:33 +08:00
parent 6945a2c37d
commit b3c1abac67
13 changed files with 28 additions and 125 deletions

View File

@@ -6,7 +6,7 @@ import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { bindReactLoopAgentContext, prepareReactLoopAgent, type ReactLoopAgent } from '../src/agent.ts'
import { bindReactLoopAgentContext, prepareReactLoopAgent } from '../src/agent.ts'
import { MockAdapter, textResponse } from './mock-adapter.ts'
function driverDone(agent: Agent): Promise<void> {
@@ -83,30 +83,19 @@ describe('Agent', () => {
await ctx.fiber.dispose()
})
it('idle inject() appends context and flushes without opening a turn', async () => {
it('idle inject() appends context without opening a turn or requesting a flush', async () => {
const adapter = new MockAdapter([textResponse('ok')])
const ctx = await harness(adapter)
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
const release = Promise.withResolvers<void>()
let flushes = 0
ctx.on('session/flush', async (session) => {
if (session !== agent.session) return
flushes += 1
await release.promise
})
ctx.on('session/flush', () => { flushes += 1 })
agent.inject([{ type: 'text', text: 'context' }], { source: { kind: 'plugin', plugin: 'p' } })
expect(agent.session.events.map(event => event.type)).toEqual(['user/message'])
expect(agent.status).toBe('idle')
expect(adapter.requests).toHaveLength(0)
let idle = false
const settled = agent.whenIdle().then(() => { idle = true })
await Promise.resolve()
expect(flushes).toBe(1)
expect(idle).toBe(false)
release.resolve()
await settled
await agent.whenIdle()
expect(flushes).toBe(0)
})
it('inject() defaults its source to an empty plugin, never user', async () => {
@@ -119,35 +108,15 @@ describe('Agent', () => {
await agent.whenIdle()
})
it('idle inject() contains a failing flush without inventing an agent turn error', async () => {
const adapter = new MockAdapter([textResponse('ok')])
const ctx = await harness(adapter)
ctx.on('session/flush', () => { throw new Error('disk gone') })
const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => undefined)
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
const errors: Error[] = []
ctx.on('agent/error', (_agent, _turn, _step, error) => { errors.push(error) })
expect(() => { agent.inject([{ type: 'text', text: 'notice' }], { source: { kind: 'plugin', plugin: 'p' } }) }).not.toThrow()
await agent.whenIdle()
expect(errors).toEqual([])
expect(agent.session.events.map(event => event.type)).toEqual(['user/message'])
expect(warn).toHaveBeenCalledWith(expect.stringContaining('flush after idle injection failed'))
warn.mockRestore()
})
it('idle inject() does not flush input rejected before append', async () => {
it('idle inject() rejects invalid input before append', async () => {
const adapter = new MockAdapter([textResponse('ok')])
const ctx = await harness(adapter)
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
let flushes = 0
ctx.on('session/flush', () => { flushes += 1 })
expect(() => {
agent.inject([{ type: 'text', text: 'x', bad: 1n } as never], { source: { kind: 'plugin', plugin: 'p' } })
}).toThrow(/non-JSON-serializable/)
expect(agent.session.events).toHaveLength(0)
expect(flushes).toBe(0)
})
it('steer() when idle falls through to send() and starts a turn', async () => {

View File

@@ -474,28 +474,6 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
await ctx2.fiber.dispose()
})
it('an idle inject() is flushed durably on its own (survives without explicit flush/dispose)', async () => {
// No clean disposal follows, so disk presence proves the idle injection's
// own checkpoint ran without a synthetic turn.
const adapter1 = new MockAdapter([textResponse('answer')])
const { ctx: ctx1, root } = await persistentHarness(adapter1)
const a1 = (await ctx1.agents.create({ sessionId: SessionId('inject-sess'), meta: { cwd: '/w' } })).agent
a1.followup([{ type: 'text', text: 'q' }], { source: { kind: 'user' } })
await waitForIdle(ctx1, a1)
a1.inject([{ type: 'text', text: 'background task 42 finished' }], { source: { kind: 'plugin', plugin: 'tool-bash' } })
await a1.whenIdle()
// A SEPARATE backend reads the on-disk log — proving the inject persisted
// itself, not a later dispose drain.
const probe = new Context()
await probe.plugin(SessionStore)
await probe.plugin(SessionPersistenceJsonl, { root })
const loaded = await probe.sessionPersistence.load(SessionId('inject-sess'))
expect(JSON.stringify(loaded.events)).toContain('background task 42 finished')
await probe.fiber.dispose()
await ctx1.fiber.dispose()
})
it('an idle inject() survives persist + resume without a synthetic turn', async () => {
const adapter1 = new MockAdapter([textResponse('answer')])
const { ctx: ctx1, root } = await persistentHarness(adapter1)

View File

@@ -1041,33 +1041,4 @@ describe('agent scope lifecycle', () => {
await ctx.fiber.dispose()
})
it('handle.dispose() awaits an idle-injection flush before unregistering or detaching', async () => {
const ctx = await harness()
const handle = await ctx.agents.create({
sessionId: SessionId('idle-flush-s'),
agentOptions: { provider: 'mock', model: 'mock' },
})
const gate = Promise.withResolvers<undefined>()
let flushStarted = false
ctx.on('session/flush', (session) => {
if (session !== handle.agent.session) return
flushStarted = true
return gate.promise
})
handle.agent.inject(text('durable idle context'), { source: { kind: 'plugin', plugin: 'test' } })
expect(flushStarted).toBe(true)
let disposed = false
const disposal = handle.dispose().then(() => { disposed = true })
await new Promise(resolve => setTimeout(resolve, 0))
expect(disposed).toBe(false)
expect(ctx.agents.get(SessionId('idle-flush-s'))).toBe(handle.agent)
expect(ctx.sessions.get(SessionId('idle-flush-s'))).toBe(handle.agent.session)
gate.resolve(undefined)
await disposal
expect(ctx.agents.get(SessionId('idle-flush-s'))).toBeUndefined()
expect(ctx.sessions.get(SessionId('idle-flush-s'))).toBeUndefined()
})
})