refactor(agent): trim obsolete loop surfaces
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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' }])
|
||||
})
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user