feat(agent): rename InboxItemInfo to AgentMessage with an id; send returns it

Add a branded AgentMessageId assigned to each accepted send message and
returned from send/followup/steer/inject (was void). Rename the inbox
event payload InboxItemInfo to AgentMessage, carrying that id so a caller
can correlate a queued item with its enqueue/dequeue/discard events.
This commit is contained in:
Turtle
2026-07-23 20:45:29 +08:00
parent b63abe80d6
commit 3fd72f7c74
32 changed files with 242 additions and 170 deletions
+14 -9
View File
@@ -6,15 +6,16 @@
* @module dsh-agent-loop/agent
*/
import { randomUUID } from 'node:crypto'
import type { Context } from 'cordis'
import { agentEvents } from '@deepseek-ai/dsh-agent'
import { agentEvents, AgentMessageId } from '@deepseek-ai/dsh-agent'
import { Agent } from '@deepseek-ai/dsh-agent'
import type { AgentCancelCause, AgentOptions, AgentStatus, CancelOptions, HookContext, SendOptions } from '@deepseek-ai/dsh-agent'
import { deepFreeze, errorChain } from '@deepseek-ai/dsh-llm'
import type { ContentBlock, MessageSource } from '@deepseek-ai/dsh-llm'
import { snapshotJsonValue, type Session, type SessionId } from '@deepseek-ai/dsh-session'
import { DISPOSED_INTERRUPT_REASON, TurnCancellation } from './cancellation.ts'
import { Inbox, inboxInfo, type InboxMessage } from './inbox.ts'
import { Inbox, agentMessage, type InboxMessage } from './inbox.ts'
import { isTurnOpen, lastTurnNumber, runLoop } from './loop.ts'
/** Sessions already claimed by a concrete driver construction. */
@@ -196,9 +197,11 @@ export class ReactLoopAgent extends Agent {
* materialization reads every nested field once; deep freeze prevents later
* caller mutation before an inbox or deferred-injection queue drains it.
*/
private acceptMessage(content: ContentBlock[], source: MessageSource, wakeup: boolean, options?: SendOptions): InboxMessage {
private acceptMessage(
id: AgentMessageId, content: ContentBlock[], source: MessageSource, wakeup: boolean, options?: SendOptions,
): InboxMessage {
const contexts = options?.contexts ?? []
const accepted = snapshotJsonValue({ content, source, contexts, wakeup })
const accepted = snapshotJsonValue({ id, content, source, contexts, wakeup })
if (accepted === undefined) {
throw new TypeError('agent message content, source, and contexts must be losslessly JSON-serializable')
}
@@ -219,23 +222,25 @@ export class ReactLoopAgent extends Agent {
if (this._status === 'disposed') throw new Error(`agent "${this.id}" is disposed`)
}
send(content: ContentBlock[], options?: SendOptions): void {
send(content: ContentBlock[], options?: SendOptions): AgentMessageId {
this.assertNotDisposed()
const id = AgentMessageId(randomUUID())
const target = options?.target ?? 'next-turn'
const wakeup = options?.wakeup ?? true
// next-step/no-wakeup is injection: durable context without running the model.
if (target === 'next-step' && !wakeup) { this.injectContext(content, options); return }
if (target === 'next-step' && !wakeup) { this.injectContext(content, options); return id }
// next-step/wakeup is steering into the running turn; idle falls back to a
// woken follow-up turn (there is no active turn to attach to).
const steering = target === 'next-step' && this._status === 'running'
const source = options?.source ?? { kind: 'user' }
const accepted = this.acceptMessage(content, source, wakeup, options)
const accepted = this.acceptMessage(id, content, source, wakeup, options)
if (steering) {
this.#inbox.steer(accepted)
} else {
this.#inbox.enqueue(accepted, wakeup)
}
agentEvents(this.loopCtx, this).emit('agent/inbox/enqueue', inboxInfo(accepted, steering))
agentEvents(this.loopCtx, this).emit('agent/inbox/enqueue', agentMessage(accepted, steering))
return id
}
/** The `next-step`/no-wakeup injection path: durable context, no FIFO, no run. */
@@ -346,7 +351,7 @@ export class ReactLoopAgent extends Agent {
// Clear work already present before abort observers run.
this.#inbox.clear()
if (discarded.length > 0) {
const items = discarded.map(({ message, steering }) => inboxInfo(message, steering))
const items = discarded.map(({ message, steering }) => agentMessage(message, steering))
agentEvents(this.loopCtx, this).emit('agent/inbox/discard', items)
}
}