refactor(agent): trim obsolete loop surfaces

This commit is contained in:
_Kerman
2026-07-24 21:58:07 +08:00
parent 194b18a32f
commit 009d113e0e
40 changed files with 252 additions and 288 deletions

View File

@@ -8,11 +8,13 @@
import { randomUUID } from 'node:crypto'
import type { Context } from 'cordis'
import { Agent, AgentMessageId, agentCarrier, agentInterruptReasonOf, assembleContextFor, emitAgentEvent } from '@deepseek-ai/dsh-agent'
import { AgentMessageId, agentCarrier, agentInterruptReasonOf, assembleContextFor, emitAgentEvent } from '@deepseek-ai/dsh-agent'
import { createScope } from '@deepseek-ai/dsh-scope'
import type { Scope } from '@deepseek-ai/dsh-scope'
import type {
AgentMessage,
Agent,
AliasSendOptions,
CancelOptions,
AgentInterruptReason,
AgentOptions,
@@ -43,7 +45,7 @@ type StepOutcome =
* The concrete {@link Agent}: each `run()` owns one turn and repeats model
* steps while tools or steering require another request.
*/
export class ReactLoopAgent extends Agent {
export class ReactLoopAgent implements Agent {
/** Prompts awaiting individual turns. */
private queued: { message: AgentMessage; wakeup: boolean }[] = []
/** Input taken into the session log at step boundaries. */
@@ -75,7 +77,6 @@ export class ReactLoopAgent extends Agent {
public readonly options: AgentOptions,
public readonly session: Session,
) {
super()
this.lastTurn = session.events.findLast(event => event.type === 'turn/start')?.data.turn ?? 0
this.scope = createScope(loopCtx, this)
this.ctx = this.scope.ctx.extend({ agent: this })
@@ -118,6 +119,33 @@ export class ReactLoopAgent extends Agent {
return id
}
/** Queue one ordinary prompt turn and wake the driver. */
followup(content: ContentBlock[], options?: AliasSendOptions): AgentMessageId {
return this.send(content, {
target: 'next-turn',
wakeup: true,
source: options?.source ?? { kind: 'user' },
})
}
/** Steer the open turn, falling back to a waking prompt while idle. */
steer(content: ContentBlock[], options?: AliasSendOptions): AgentMessageId {
return this.send(content, {
target: 'next-step',
wakeup: true,
source: options?.source ?? { kind: 'user' },
})
}
/** Append model-facing context without waking the driver. */
inject(content: ContentBlock[], options?: AliasSendOptions): AgentMessageId {
return this.send(content, {
target: 'next-step',
wakeup: false,
source: options?.source ?? { kind: 'plugin', plugin: '' },
})
}
/**
* Clear all pending work and abort the active turn; the first cause wins.
* The cause is signal payload for observers and the durable turn/end

View File

@@ -140,13 +140,9 @@ describe('config-driven session id', () => {
first.inject([{ type: 'text', text: 'persist before replacement' }], {
source: { kind: 'plugin', plugin: 'test' },
})
await expect.poll(async () => {
try {
return JSON.stringify((await ctx.sessionPersistence.inspect(sessionId)).events)
} catch {
return ''
}
}).toContain('persist before replacement')
await ctx.sessions.flush(first.session)
expect(JSON.stringify((await ctx.sessionPersistence.inspect(sessionId)).events))
.toContain('persist before replacement')
const firstDisposal = firstLoop.dispose()
await cleanupStarted.promise
@@ -190,13 +186,9 @@ describe('config-driven session id', () => {
first.inject([{ type: 'text', text: 'persist before cancellation' }], {
source: { kind: 'plugin', plugin: 'test' },
})
await expect.poll(async () => {
try {
return JSON.stringify((await ctx.sessionPersistence.inspect(sessionId)).events)
} catch {
return ''
}
}).toContain('persist before cancellation')
await ctx.sessions.flush(first.session)
expect(JSON.stringify((await ctx.sessionPersistence.inspect(sessionId)).events))
.toContain('persist before cancellation')
const firstDisposal = firstLoop.dispose()
await cleanupStarted.promise

View File

@@ -436,7 +436,7 @@ describe('disposal leaves the two-state status contract balanced', () => {
await fiber.dispose()
await driverDone(agent) // must not hang
await expect.poll(() => ctx.agents.get(SessionId('scoped')) === undefined).toBe(true)
expect(ctx.agents.get(SessionId('scoped'))).toBeUndefined()
})
})

View File

@@ -161,7 +161,6 @@ describe('agent/prompt-submit', () => {
expect(log.some(e => e.type === 'turn/end')).toBe(false)
expect(log.some(e => e.type === 'user/message')).toBe(false)
expect(log.some(e => e.type === 'step/start')).toBe(false)
expect(log.some(e => e.type === 'prompt/blocked')).toBe(false)
expect(reasons).toEqual([])
})
@@ -189,7 +188,6 @@ describe('agent/prompt-submit', () => {
expect(userMsgs).toHaveLength(1)
expect(userMsgs[0]?.type === 'user/message' && userMsgs[0].data.content).toEqual([{ type: 'text', text: 'safe' }])
expect(adapter.requests.length).toBeGreaterThanOrEqual(1)
expect(log.filter(e => e.type === 'prompt/blocked')).toHaveLength(0)
expect(log.filter(e => e.type === 'turn/start')).toHaveLength(1)
expect(reasons).toEqual([{ kind: 'completed' }])
})

View File

@@ -1041,7 +1041,7 @@ describe('agent loop', () => {
await fiber.dispose()
await driverDone(agent)
await expect.poll(() => ctx.agents.get(SessionId('scoped')) === undefined).toBe(true)
expect(ctx.agents.get(SessionId('scoped'))).toBeUndefined()
})
it('creates agents from config on startup', async () => {