mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
refactor(agent): complete inbox lifecycle migration
This commit is contained in:
@@ -390,7 +390,9 @@ export class BasicCompactService extends CompactService {
|
||||
{
|
||||
owner: null,
|
||||
stability: 'selected-span',
|
||||
flush: () => this.ctx.sessions.flush(agent.session),
|
||||
flush: async () => {
|
||||
await this.ctx.sessions.flush(agent.session)
|
||||
},
|
||||
},
|
||||
operationSignal,
|
||||
)
|
||||
|
||||
@@ -141,11 +141,14 @@ function derivedText(session: Session): string[] {
|
||||
}
|
||||
|
||||
/** Await one classified manual-compaction rejection. */
|
||||
async function rejection(operation: Promise<unknown>): Promise<ManualCompactionError> {
|
||||
const caught: unknown = await operation.then(
|
||||
(value: unknown) => { throw new Error(`expected a rejection, resolved with ${String(value)}`) },
|
||||
(error: unknown) => error,
|
||||
)
|
||||
async function rejection(operation: Promise<unknown> | (() => Promise<unknown>)): Promise<ManualCompactionError> {
|
||||
let caught: unknown
|
||||
try {
|
||||
const value = await (typeof operation === 'function' ? operation() : operation)
|
||||
throw new Error(`expected a rejection, resolved with ${String(value)}`)
|
||||
} catch (error: unknown) {
|
||||
caught = error
|
||||
}
|
||||
if (!(caught instanceof ManualCompactionError)) {
|
||||
throw new Error(`expected a ManualCompactionError, got ${String(caught)}`)
|
||||
}
|
||||
@@ -195,15 +198,20 @@ function closedConversation(turns = 2, lastTurnNumber = turns): Session {
|
||||
return session
|
||||
}
|
||||
|
||||
/** A fake idle agent whose admission reservation is scripted per test. */
|
||||
/** A fake idle agent whose maintenance claim is scripted per test. */
|
||||
function fakeAgent(
|
||||
session: Session,
|
||||
reserve: () => (() => void) | undefined,
|
||||
maintenanceSignal = new AbortController().signal,
|
||||
): Agent {
|
||||
return {
|
||||
session,
|
||||
options: { provider: MODEL, model: MODEL },
|
||||
reserveTurnAdmission: reserve,
|
||||
runMaintenance<T>(task: (signal: AbortSignal) => Promise<T>): Promise<T> {
|
||||
const release = reserve()
|
||||
if (release === undefined) throw new Error('agent already has active work')
|
||||
return task(maintenanceSignal).finally(release)
|
||||
},
|
||||
} as unknown as Agent
|
||||
}
|
||||
|
||||
@@ -270,7 +278,7 @@ describe('compactNow through the real loop', () => {
|
||||
expect(second.some(text => text.includes(PROMPT))).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps context injected during summarization between the markers and after the checkpoint', async () => {
|
||||
it('keeps context injected during summarization pending for the next step', async () => {
|
||||
const harness = await loopHarness()
|
||||
const { agent, compact } = harness
|
||||
await seedHistory(harness)
|
||||
@@ -285,18 +293,22 @@ describe('compactNow through the real loop', () => {
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
const start = agent.session.events.findLast(event => event.type === 'compact/start')
|
||||
const injected = agent.session.events.findLast(event => event.type === 'user/message'
|
||||
&& event.data.source.kind === 'plugin' && event.data.source.plugin === 'test')
|
||||
const injected = agent.inbox.nextStep.find(message =>
|
||||
message.source.kind === 'plugin' && message.source.plugin === 'test')
|
||||
const end = agent.session.events.findLast(event => event.type === 'compact/end')
|
||||
expect(start).toBeDefined()
|
||||
expect(injected).toBeDefined()
|
||||
expect(end).toBeDefined()
|
||||
expect(start!.seq).toBeLessThan(injected!.seq)
|
||||
expect(injected!.seq).toBeLessThan(end!.seq)
|
||||
expect(result?.shadowedSeqs).not.toContain(injected?.seq)
|
||||
expect(agent.session.events.some(event => event.type === 'user/message'
|
||||
&& event.data.id === injected?.id)).toBe(false)
|
||||
|
||||
agent.followup(createUserMessage({
|
||||
content: [{ type: 'text', text: 'after compaction' }],
|
||||
source: { kind: 'user' },
|
||||
}))
|
||||
await agent.whenIdle()
|
||||
const messages = derivedText(agent.session)
|
||||
expect(messages[0]).toContain('checkpoint')
|
||||
expect(messages.at(-1)).toContain('INJECTED CONTEXT')
|
||||
expect(messages.filter(text => text.includes('INJECTED CONTEXT'))).toHaveLength(1)
|
||||
})
|
||||
|
||||
@@ -334,7 +346,7 @@ describe('compactNow through the real loop', () => {
|
||||
content: [{ type: 'text', text: 'first in line' }],
|
||||
source: { kind: 'user' },
|
||||
}))
|
||||
expect((await rejection(compact.compactNow(agent, SIGNAL))).code).toBe('busy')
|
||||
expect((await rejection(() => compact.compactNow(agent, SIGNAL))).code).toBe('busy')
|
||||
expect(compact.calls).toHaveLength(0)
|
||||
|
||||
await agent.whenIdle()
|
||||
@@ -400,7 +412,7 @@ describe('compactNow transaction and failure classification', () => {
|
||||
session.append('compact/start', { turn: null })
|
||||
const agent = fakeAgent(session, () => () => undefined)
|
||||
|
||||
const error = await rejection(compact.compactNow(agent, SIGNAL))
|
||||
const error = await rejection(() => compact.compactNow(agent, SIGNAL))
|
||||
expect(error.code).toBe('busy')
|
||||
expect(error.message).toContain('compaction lock is already active')
|
||||
expect(compact.calls).toHaveLength(0)
|
||||
@@ -448,7 +460,7 @@ describe('compactNow transaction and failure classification', () => {
|
||||
const { compact } = detachedService()
|
||||
const agent = fakeAgent(closedConversation(2), () => undefined)
|
||||
|
||||
expect((await rejection(compact.compactNow(agent, SIGNAL))).code).toBe('busy')
|
||||
expect((await rejection(() => compact.compactNow(agent, SIGNAL))).code).toBe('busy')
|
||||
expect(compact.calls).toHaveLength(0)
|
||||
})
|
||||
|
||||
@@ -685,7 +697,13 @@ describe('compactNow transaction and failure classification', () => {
|
||||
const controller = new AbortController()
|
||||
controller.abort(reason)
|
||||
|
||||
await expect(compact.compactNow(agent, controller.signal)).rejects.toBe(reason)
|
||||
let thrown: unknown
|
||||
try {
|
||||
void compact.compactNow(agent, controller.signal)
|
||||
} catch (error: unknown) {
|
||||
thrown = error
|
||||
}
|
||||
expect(thrown).toBe(reason)
|
||||
expect(reserve).not.toHaveBeenCalled()
|
||||
expect(measure).not.toHaveBeenCalled()
|
||||
expect(compact.calls).toHaveLength(0)
|
||||
@@ -713,6 +731,21 @@ describe('compactNow transaction and failure classification', () => {
|
||||
.toContain('summarizer aborted')
|
||||
})
|
||||
|
||||
it('classifies agent cancellation during maintenance as an expected cancellation', async () => {
|
||||
const { compact } = detachedService()
|
||||
const controller = new AbortController()
|
||||
const reason = new Error('agent cancelled maintenance')
|
||||
const session = closedConversation(2)
|
||||
const agent = fakeAgent(session, () => () => undefined, controller.signal)
|
||||
compact.duringSummary = () => { controller.abort(reason) }
|
||||
compact.error = new Error('summarizer observed cancellation')
|
||||
|
||||
const error = await rejection(compact.compactNow(agent, SIGNAL))
|
||||
|
||||
expect(error.code).toBe('cancelled')
|
||||
expect(error.cause).toBe(reason)
|
||||
})
|
||||
|
||||
it('aborts before committing when cancellation lands after summarization', async () => {
|
||||
const { compact } = detachedService()
|
||||
const controller = new AbortController()
|
||||
|
||||
Reference in New Issue
Block a user