fix(agent): route loop dispatches through prebuilt fused dispatcher
Address review feedback on PR #1738: - ReactLoopAgent builds its AgentEventDispatch once in the constructor and routes every emit/serial/waterfall through it, so hot-path dispatches no longer allocate a carrier and dispatcher per call; the public carrier field is gone (fused dispatcher is private). - agentEvents accepts an optional prebuilt carrier. - The fused payload builder spreads the payload before the injected agent so a structurally acceptable payload carrying an agent field can never override the subject. - Regenerate doc graphs; re-record core + architecture + affected Agent Note translation pairs; add payload-object event contract Agent Note.
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
import type {
|
||||
Agent,
|
||||
AgentCancelCause,
|
||||
AgentEventDispatch,
|
||||
AgentOptions,
|
||||
AgentStatus,
|
||||
CancelOptions,
|
||||
@@ -14,7 +15,7 @@ import type {
|
||||
PreStepDecision,
|
||||
RequestErrorAction,
|
||||
} from '@deepseek-ai/dsh-agent'
|
||||
import { Inbox, agentCarrier, assembleContextFor, emitAgentEvent } from '@deepseek-ai/dsh-agent'
|
||||
import { Inbox, agentEvents, assembleContextFor } from '@deepseek-ai/dsh-agent'
|
||||
import type { GenerateOptions, LlmCallConfig, Message, PreparedLlmCall } from '@deepseek-ai/dsh-llm'
|
||||
import {
|
||||
BlockAssembler,
|
||||
@@ -24,7 +25,7 @@ import {
|
||||
errorChain,
|
||||
markAgentLoopRequest,
|
||||
} from '@deepseek-ai/dsh-llm'
|
||||
import type { Scope, Scoped } from '@deepseek-ai/dsh-scope'
|
||||
import type { Scope } from '@deepseek-ai/dsh-scope'
|
||||
import { createScope } from '@deepseek-ai/dsh-scope'
|
||||
import type { EpochHeader, RequestContext, Session, SessionId, TurnEndReason, UserMessage } from '@deepseek-ai/dsh-session'
|
||||
import { canonicalHeader, headerEquals } from '@deepseek-ai/dsh-session'
|
||||
@@ -69,8 +70,8 @@ export class ReactLoopAgent implements Agent {
|
||||
readonly scope: Scope
|
||||
readonly ctx: Context
|
||||
|
||||
/** Fused scope carrier, built once in the constructor for every dispatch. */
|
||||
readonly carrier: Scoped<Agent>
|
||||
/** Fused dispatcher, built once in the constructor so hot-path dispatches never allocate. */
|
||||
private readonly dispatch: AgentEventDispatch
|
||||
|
||||
/** Whether this loop instance has appended its initial/resume request anchor. */
|
||||
private requestHeaderLogged = false
|
||||
@@ -82,11 +83,11 @@ export class ReactLoopAgent implements Agent {
|
||||
public readonly options: AgentOptions,
|
||||
public readonly session: Session,
|
||||
) {
|
||||
this.carrier = agentCarrier(this)
|
||||
this.dispatch = agentEvents(loopCtx, this)
|
||||
this.inbox = new Inbox(session, {
|
||||
inserted: (message) => { emitAgentEvent(loopCtx, this, 'agent/inbox/inserted', { message }) },
|
||||
discarded: (message) => { emitAgentEvent(loopCtx, this, 'agent/inbox/discarded', { message }) },
|
||||
claimed: (message, turn) => { emitAgentEvent(loopCtx, this, 'agent/inbox/claimed', { message, turn }) },
|
||||
inserted: (message) => { this.dispatch.emit('agent/inbox/inserted', { message }) },
|
||||
discarded: (message) => { this.dispatch.emit('agent/inbox/discarded', { message }) },
|
||||
claimed: (message, turn) => { this.dispatch.emit('agent/inbox/claimed', { message, turn }) },
|
||||
})
|
||||
const lastTurn = session.events.findLast(event => event.type === 'turn/start')?.data.turn ?? 0
|
||||
this.phase = { kind: 'idle', lastTurn }
|
||||
@@ -105,7 +106,7 @@ export class ReactLoopAgent implements Agent {
|
||||
this.phase = next
|
||||
const status = this.status
|
||||
if (status !== previousStatus) {
|
||||
emitAgentEvent(this.loopCtx, this, 'agent/status', { status })
|
||||
this.dispatch.emit('agent/status', { status })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,7 +184,7 @@ export class ReactLoopAgent implements Agent {
|
||||
private throwError(error: unknown): never {
|
||||
const turn = this.phase.kind === 'running' ? this.phase.turn : this.phase.lastTurn
|
||||
const step = this.phase.kind === 'running' ? this.phase.step : 0
|
||||
emitAgentEvent(this.loopCtx, this, 'agent/error', { turn, step, error })
|
||||
this.dispatch.emit('agent/error', { turn, step, error })
|
||||
throw error
|
||||
}
|
||||
|
||||
@@ -209,8 +210,8 @@ export class ReactLoopAgent implements Agent {
|
||||
signal.throwIfAborted()
|
||||
const sections = renderContextSections(assembly)
|
||||
const context = this.runtimeContext.project(joinContextSections(sections), sections)
|
||||
const decision = await this.loopCtx.waterfall(
|
||||
this.carrier, 'agent/pre-step', { agent: this, messages: claimed, ...position, signal },
|
||||
const decision = await this.dispatch.waterfall(
|
||||
'agent/pre-step', { messages: claimed, ...position, signal },
|
||||
(): Promise<PreStepDecision> => Promise.resolve<PreStepDecision>({
|
||||
kind: 'enter',
|
||||
messages: context === undefined ? claimed : [...claimed, context],
|
||||
@@ -271,7 +272,7 @@ export class ReactLoopAgent implements Agent {
|
||||
}
|
||||
signal.throwIfAborted()
|
||||
if (turnEnds && this.inbox.nextStep.length === 0) {
|
||||
await this.loopCtx.serial(this.carrier, 'agent/turn-stopping', { agent: this, turn, signal })
|
||||
await this.dispatch.serial('agent/turn-stopping', { turn, signal })
|
||||
signal.throwIfAborted()
|
||||
}
|
||||
if (turnEnds && this.inbox.nextStep.length === 0) break
|
||||
@@ -328,9 +329,8 @@ export class ReactLoopAgent implements Agent {
|
||||
signal.throwIfAborted()
|
||||
const finish = assembler.finish
|
||||
if (finish.kind === 'error' || finish.kind === 'aborted') {
|
||||
const action = await this.loopCtx.waterfall(
|
||||
this.carrier, 'agent/request-error', {
|
||||
agent: this,
|
||||
const action = await this.dispatch.waterfall(
|
||||
'agent/request-error', {
|
||||
turn,
|
||||
step,
|
||||
provider: request.provider,
|
||||
@@ -412,8 +412,8 @@ export class ReactLoopAgent implements Agent {
|
||||
...maxTokens === undefined ? {} : { maxTokens },
|
||||
},
|
||||
))
|
||||
const proposedConfig = await this.loopCtx.waterfall(
|
||||
this.carrier, 'agent/request', { agent: this, turn, step, signal },
|
||||
const proposedConfig = await this.dispatch.waterfall(
|
||||
'agent/request', { turn, step, signal },
|
||||
() => Promise.resolve(seedConfig),
|
||||
)
|
||||
signal.throwIfAborted()
|
||||
|
||||
Reference in New Issue
Block a user