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:
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-22-unified-send-and-coalesced-user-messages.md: 61f9775c7b99c783a86e6a0814dc69d548ecbbe1
|
||||
2026-07-22-unified-send-and-coalesced-user-messages.zh.md: d9ce4c482203592b8e5aada976debb6f088bb1eb
|
||||
2026-07-22-unified-send-and-coalesced-user-messages.md: d88cd60f5c09f7961d7a59dbbd0703abdd26cc1a
|
||||
2026-07-22-unified-send-and-coalesced-user-messages.zh.md: d16ed9f763a350e61abc00d3971e4a7711e8f610
|
||||
|
||||
+3
-1
@@ -20,7 +20,9 @@ Separately, `context/message` and `user/message` had converged: the surface proj
|
||||
|
||||
**Goal replay disambiguates by round, not type.** A goal state change is a round-zero goal-sourced `user/message` carrying `goal/change` metadata; a positive round is an admitted continuation prompt. `decodeGoalEvent` now takes a `user/message` and still fails loud on goal metadata under a non-goal source or a goal source lacking metadata.
|
||||
|
||||
**Three inbox events replace agent/queued.** `agent/inbox/enqueue` (an item entered a FIFO; carries `target`/`wakeup` on `InboxItemInfo`), `agent/inbox/dequeue` (the driver claimed one), and `agent/inbox/discard` (`cancel()` dropped pending items). Injection never touches a FIFO and emits none of these. Every FIFO entry publishes an enqueue, including the loop-authored continuation-reason steer (`agent/turn-continuation` returning `{ action: 'continue', reason }`), so the ledger stays balanced with its later dequeue or discard. The `dsh-agent` invariant companion asserts FIFO conservation: a per-agent outstanding count that dequeue and discard can never drive negative.
|
||||
**`send` returns an id.** `send` (and the aliases) return an opaque branded `AgentMessageId` for the accepted message; `send`'s previous return was `void`.
|
||||
|
||||
**Three inbox events replace agent/queued.** `agent/inbox/enqueue` (an item entered a FIFO), `agent/inbox/dequeue` (the driver claimed one), and `agent/inbox/discard` (`cancel()` dropped pending items) each carry an `AgentMessage` — the accepted message including its returned `id`, `target`/`wakeup`, source, and contexts — so a caller can correlate a queued item with its lifecycle. Injection never touches a FIFO and emits none of these. Every FIFO entry publishes an enqueue, including the loop-authored continuation-reason steer (`agent/turn-continuation` returning `{ action: 'continue', reason }`), so the ledger stays balanced with its later dequeue or discard. The `dsh-agent` invariant companion asserts FIFO conservation: a per-agent outstanding count that dequeue and discard can never drive negative.
|
||||
|
||||
**cancel gains keepInbox.** `cancel(cause?, { keepInbox? })`; when true it aborts the active turn but preserves queued and steering items (no discard event, and un-started work is not dropped).
|
||||
|
||||
|
||||
+3
-1
@@ -20,7 +20,9 @@ agent 的对外驱动接口逐渐长出三个近乎平行的动词——`send`
|
||||
|
||||
**goal 回放靠轮次而非类型来区分。** 一次 goal 状态变更是一条第 0 轮、来源为 goal 的 `user/message`,携带 `goal/change` 元数据;正数轮次则是一条已准入的继续执行提示词。`decodeGoalEvent` 现在接收一条 `user/message`,并仍会在非 goal 来源携带 goal 元数据、或 goal 来源缺少元数据时立即报错。
|
||||
|
||||
**三个 inbox 事件取代 agent/queued。** `agent/inbox/enqueue`(一个队列项进入某个 FIFO;在 `InboxItemInfo` 上携带 `target`/`wakeup`)、`agent/inbox/dequeue`(驱动器认领了一个)和 `agent/inbox/discard`(`cancel()` 丢弃了待处理项)。注入从不触及 FIFO,也不发出这些事件中的任何一个。每一次 FIFO 入队都会发布一个 enqueue 事件,包括由 loop 生成的携带继续原因的 steer(`agent/turn-continuation` 返回 `{ action: 'continue', reason }`),因此账目会与其后的 dequeue 或 discard 保持平衡。`dsh-agent` 的不变量配套断言 FIFO 守恒:一个按 agent 计的未结算计数,dequeue 和 discard 永远无法把它压到负数。
|
||||
**`send` 返回一个 id。** `send`(以及其别名)为被接受的消息返回一个不透明的 branded `AgentMessageId`;`send` 此前的返回值是 `void`。
|
||||
|
||||
**三个 inbox 事件取代 agent/queued。** `agent/inbox/enqueue`(一个队列项进入某个 FIFO)、`agent/inbox/dequeue`(驱动器认领了一个)和 `agent/inbox/discard`(`cancel()` 丢弃了待处理项)都携带一条 `AgentMessage`——即被接受的消息,包含其返回的 `id`、`target`/`wakeup`、来源和上下文——因此调用方可以把一个排队项与其生命周期关联起来。注入从不触及 FIFO,也不发出这些事件中的任何一个。每一次 FIFO 入队都会发布一个 enqueue 事件,包括由 loop 生成的携带继续原因的 steer(`agent/turn-continuation` 返回 `{ action: 'continue', reason }`),因此账目会与其后的 dequeue 或 discard 保持平衡。`dsh-agent` 的不变量配套断言 FIFO 守恒:一个按 agent 计的未结算计数,dequeue 和 discard 永远无法把它压到负数。
|
||||
|
||||
**cancel 新增 keepInbox。** `cancel(cause?, { keepInbox? })`;当其为 true 时,它中止活跃轮次,但保留排队项和 steering 项(不发出 discard 事件,尚未启动的工作也不会被丢弃)。
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ Effective broad cancellation was requested, before queued/steering work is clear
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [AgentCancelCause](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:329`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:353`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/created` — emit
|
||||
|
||||
@@ -54,7 +54,7 @@ A fully configured agent and live session were published. Setup is composition-o
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:269`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:293`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/disposed` — emit
|
||||
|
||||
@@ -74,7 +74,7 @@ An agent left the registry; AgentLoop emits this after driver quiescence but bef
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:278`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:302`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/error` — emit
|
||||
|
||||
@@ -96,7 +96,7 @@ A step or turn errored. The loop reports a failure here (plus the logger) even w
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:477`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:501`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/inbox/dequeue` — emit
|
||||
|
||||
@@ -108,16 +108,16 @@ The driver claimed one item out of the inbox: a queued item at a turn boundary,
|
||||
* boundary, or steering drained between steps. Fires after the item leaves
|
||||
* its FIFO and before it becomes a durable message.
|
||||
* @param agent - the agent whose inbox item was claimed.
|
||||
* @param info - the claimed item's accepted content, source, contexts, steering, and wakeup facts.
|
||||
* @param message - the claimed message (matching the `id` from its `agent/inbox/enqueue`).
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
* @mode emit
|
||||
*/
|
||||
'agent/inbox/dequeue'(this: Scoped<Agent>, agent: Agent, info: InboxItemInfo): void
|
||||
'agent/inbox/dequeue'(this: Scoped<Agent>, agent: Agent, message: AgentMessage): void
|
||||
```
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [InboxItemInfo](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
Types: [Agent](../core-data-structures/core.md) · [AgentMessage](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:309`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:333`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/inbox/discard` — emit
|
||||
|
||||
@@ -129,16 +129,16 @@ Source: [`packages/core/agent/src/types.ts:309`](../../packages/core/agent/src/t
|
||||
* delivering them. Fires once per effective clearing call with every
|
||||
* discarded item, after `agent/cancel-requested` and before the abort.
|
||||
* @param agent - the agent whose inbox was cleared.
|
||||
* @param items - the discarded items in FIFO order (queued then steering); empty when nothing was pending.
|
||||
* @param messages - the discarded messages in FIFO order (queued then steering); empty when nothing was pending.
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
* @mode emit
|
||||
*/
|
||||
'agent/inbox/discard'(this: Scoped<Agent>, agent: Agent, items: InboxItemInfo[]): void
|
||||
'agent/inbox/discard'(this: Scoped<Agent>, agent: Agent, messages: AgentMessage[]): void
|
||||
```
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [InboxItemInfo](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
Types: [Agent](../core-data-structures/core.md) · [AgentMessage](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:319`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:343`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/inbox/enqueue` — emit
|
||||
|
||||
@@ -152,16 +152,16 @@ A detached, frozen item entered the agent's inbox (queued or steering FIFO). Sou
|
||||
* is the eventual `user/message`/`steering/message`. Injection
|
||||
* (`next-step`/no-wakeup) bypasses the FIFOs and does not emit this.
|
||||
* @param agent - the agent whose inbox received the item.
|
||||
* @param info - the accepted content, source, contexts, steering, and wakeup facts.
|
||||
* @param message - the accepted message (its returned `id`, content, source, contexts, steering, and wakeup facts).
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
* @mode emit
|
||||
*/
|
||||
'agent/inbox/enqueue'(this: Scoped<Agent>, agent: Agent, info: InboxItemInfo): void
|
||||
'agent/inbox/enqueue'(this: Scoped<Agent>, agent: Agent, message: AgentMessage): void
|
||||
```
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [InboxItemInfo](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
Types: [Agent](../core-data-structures/core.md) · [AgentMessage](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:299`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:323`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/post-step` — serial
|
||||
|
||||
@@ -184,7 +184,7 @@ Awaited serial checkpoint after the response, real or synthetic tool results, in
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:427`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:451`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/pre-step` — serial
|
||||
|
||||
@@ -207,7 +207,7 @@ Awaited serial checkpoint before `step/start`; appends land outside the pending
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:358`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:382`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/prompt-submit` — waterfall
|
||||
|
||||
@@ -234,7 +234,7 @@ Allow, rewrite, or block one claimed prompt before it becomes a user message. Ca
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [ContentBlock](../core-data-structures/core.md) · [MessageSource](../core-data-structures/core.md) · [PromptDecision](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:374`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:398`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/request` — waterfall
|
||||
|
||||
@@ -259,7 +259,7 @@ Replace the frozen call configuration. Model-visible content must use logged cha
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [LlmCallConfig](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:388`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:412`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/request-error` — waterfall
|
||||
|
||||
@@ -285,7 +285,7 @@ Recover a model-request failure after its failed step has closed. `retry` opens
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [LlmFailure](../core-data-structures/llm-streaming.md) · [RequestError](../core-data-structures/core.md) · [RequestErrorDecision](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:442`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:466`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/session-prefix` — waterfall
|
||||
|
||||
@@ -311,7 +311,7 @@ Compose request-only messages placed before derived history. The frozen result i
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [Message](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:403`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:427`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/session-start` — emit
|
||||
|
||||
@@ -333,7 +333,7 @@ The session lifecycle began, once before the first turn. Use `agent.inject()` to
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) · [SessionStartSource](../core-data-structures/core.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:342`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:366`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/status` — emit
|
||||
|
||||
@@ -353,7 +353,7 @@ Agent status changed (`idle` ⇄ `running`, or → `disposed`). `send()` does no
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [AgentStatus](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:287`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:311`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/step-result` — waterfall
|
||||
|
||||
@@ -376,7 +376,7 @@ Waterfall: post-process the assembled assistant Message before tool dispatch (va
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [Message](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:415`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:439`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/turn-continuation` — waterfall
|
||||
|
||||
@@ -398,7 +398,7 @@ Override whether the turn continues. The default continues after tool calls or s
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [ContinuationDecision](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:453`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:477`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/turn-stop` — serial
|
||||
|
||||
@@ -420,7 +420,7 @@ Monotonic terminal-stop checkpoint after continuation and steering are folded; a
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [ContinuationStop](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:464`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:488`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
## `agent-loop/*`
|
||||
|
||||
|
||||
@@ -409,16 +409,30 @@ The fixed-preset aliases own `target` and `wakeup`, so they accept only the rema
|
||||
type AliasSendOptions = Omit<SendOptions, 'target' | 'wakeup'>
|
||||
```
|
||||
|
||||
The `agent/inbox/*` live events carry the resolved facts of one FIFO item; injection bypasses the FIFOs and never appears on them:
|
||||
`send` returns the accepted message's opaque `AgentMessageId`, stable across that message's `agent/inbox/*` events:
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* The resolved facts of one inbox FIFO item, carried by the `agent/inbox/*`
|
||||
* live events. Source defaults are already applied, so these are the exact
|
||||
* values the item was accepted with. `steering` is true for a `next-step`
|
||||
* item drained between steps; a `next-turn` item is claimed at a turn boundary.
|
||||
* Opaque id assigned to one accepted {@link Agent.send} message; returned by
|
||||
* `send` and carried on its `agent/inbox/*` events for correlation.
|
||||
*/
|
||||
interface InboxItemInfo {
|
||||
type AgentMessageId = Branded<'AgentMessageId'>
|
||||
```
|
||||
|
||||
The `agent/inbox/*` live events carry one accepted message; injection bypasses the FIFOs and never appears on them:
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* One accepted {@link Agent.send} message, carried by the `agent/inbox/*` live
|
||||
* events. `id` is the value `send` returned to the caller, stable across this
|
||||
* message's enqueue, dequeue, and discard events. Source defaults are already
|
||||
* applied, so these are the exact values the item was accepted with. `steering`
|
||||
* is true for a `next-step` item drained between steps; a `next-turn` item is
|
||||
* claimed at a turn boundary.
|
||||
*/
|
||||
interface AgentMessage {
|
||||
/** The id `send` returned for this message. */
|
||||
id: AgentMessageId
|
||||
content: ContentBlock[]
|
||||
source: MessageSource
|
||||
contexts: HookContext[]
|
||||
@@ -488,8 +502,9 @@ abstract class Agent {
|
||||
* input throws synchronously before any notification, enqueue, or append.
|
||||
* @param content - the model-facing content blocks to deliver.
|
||||
* @param options - target queue, wakeup decision, source, contexts, and meta.
|
||||
* @returns the accepted message's {@link AgentMessageId}, stable across its `agent/inbox/*` events.
|
||||
*/
|
||||
abstract send(content: ContentBlock[], options?: SendOptions): void
|
||||
abstract send(content: ContentBlock[], options?: SendOptions): AgentMessageId
|
||||
|
||||
/**
|
||||
* Clear queued and steering work — unless `keepInbox` — and abort the active
|
||||
@@ -512,9 +527,10 @@ abstract class Agent {
|
||||
* ordinary message of its own turn.
|
||||
* @param content - the prompt content blocks.
|
||||
* @param options - source and attached contexts.
|
||||
* @returns the accepted message's {@link AgentMessageId}.
|
||||
*/
|
||||
followup(content: ContentBlock[], options?: AliasSendOptions): void {
|
||||
this.send(content, { ...options, target: 'next-turn', wakeup: true })
|
||||
followup(content: ContentBlock[], options?: AliasSendOptions): AgentMessageId {
|
||||
return this.send(content, { ...options, target: 'next-turn', wakeup: true })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -526,9 +542,10 @@ abstract class Agent {
|
||||
* Idle steering falls back to a woken follow-up turn.
|
||||
* @param content - the steering content blocks.
|
||||
* @param options - source and attached contexts.
|
||||
* @returns the accepted message's {@link AgentMessageId}.
|
||||
*/
|
||||
steer(content: ContentBlock[], options?: AliasSendOptions): void {
|
||||
this.send(content, { ...options, target: 'next-step', wakeup: true })
|
||||
steer(content: ContentBlock[], options?: AliasSendOptions): AgentMessageId {
|
||||
return this.send(content, { ...options, target: 'next-step', wakeup: true })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -541,9 +558,10 @@ abstract class Agent {
|
||||
* `agent/error`. An omitted source defaults to `{ kind: 'plugin', plugin: '' }`.
|
||||
* @param content - the injected context content blocks.
|
||||
* @param options - source and durable model-hidden meta.
|
||||
* @returns the accepted message's {@link AgentMessageId}.
|
||||
*/
|
||||
inject(content: ContentBlock[], options?: AliasSendOptions): void {
|
||||
this.send(content, { ...options, target: 'next-step', wakeup: false })
|
||||
inject(content: ContentBlock[], options?: AliasSendOptions): AgentMessageId {
|
||||
return this.send(content, { ...options, target: 'next-step', wakeup: false })
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -8,24 +8,24 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| Event | Mode | Declared in | Dispatchers | Listeners |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:353`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | [`tui`](../packages/ui/tui) |
|
||||
| `agent/cancel-requested` | `emit` | [`packages/core/agent/src/types.ts:329`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-session`](../packages/goal/goal-session) |
|
||||
| `agent/created` | `emit` | [`packages/core/agent/src/types.ts:269`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`goal-session`](../packages/goal/goal-session), [`tui`](../packages/ui/tui) |
|
||||
| `agent/disposed` | `emit` | [`packages/core/agent/src/types.ts:278`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`goal-session`](../packages/goal/goal-session), [`tui`](../packages/ui/tui) |
|
||||
| `agent/error` | `emit` | [`packages/core/agent/src/types.ts:477`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-session`](../packages/goal/goal-session), `runtime`, [`tui`](../packages/ui/tui) |
|
||||
| `agent/inbox/dequeue` | `emit` | [`packages/core/agent/src/types.ts:309`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent) |
|
||||
| `agent/inbox/discard` | `emit` | [`packages/core/agent/src/types.ts:319`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent) |
|
||||
| `agent/inbox/enqueue` | `emit` | [`packages/core/agent/src/types.ts:299`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), [`goal-session`](../packages/goal/goal-session), [`tui`](../packages/ui/tui) |
|
||||
| `agent/post-step` | `serial` | [`packages/core/agent/src/types.ts:427`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`compact-basic`](../packages/compact/compact-basic), [`session-checkpoint-policy`](../packages/session-persistence/session-checkpoint-policy) |
|
||||
| `agent/pre-step` | `serial` | [`packages/core/agent/src/types.ts:358`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`time-context`](../packages/context/time-context), [`user-approval`](../packages/ui/user-approval) |
|
||||
| `agent/prompt-submit` | `waterfall` | [`packages/core/agent/src/types.ts:374`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`acp`](../packages/ui/acp), [`goal-session`](../packages/goal/goal-session), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) |
|
||||
| `agent/request` | `waterfall` | [`packages/core/agent/src/types.ts:388`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent) |
|
||||
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/types.ts:442`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compact-basic`](../packages/compact/compact-basic), [`llm-retry`](../packages/llm/llm-retry), [`plan-mode`](../packages/plan/plan-mode) |
|
||||
| `agent/session-prefix` | `waterfall` | [`packages/core/agent/src/types.ts:403`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`tool-skill`](../packages/skill/tool-skill), [`workspace-context`](../packages/context/workspace-context) |
|
||||
| `agent/session-start` | `emit` | [`packages/core/agent/src/types.ts:342`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) |
|
||||
| `agent/status` | `emit` | [`packages/core/agent/src/types.ts:287`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), [`goal-session`](../packages/goal/goal-session), `runtime`, [`tui`](../packages/ui/tui) |
|
||||
| `agent/step-result` | `waterfall` | [`packages/core/agent/src/types.ts:415`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - |
|
||||
| `agent/turn-continuation` | `waterfall` | [`packages/core/agent/src/types.ts:453`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode) |
|
||||
| `agent/turn-stop` | `serial` | [`packages/core/agent/src/types.ts:464`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`tool-goal`](../packages/goal/tool-goal) |
|
||||
| `agent/cancel-requested` | `emit` | [`packages/core/agent/src/types.ts:353`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-session`](../packages/goal/goal-session) |
|
||||
| `agent/created` | `emit` | [`packages/core/agent/src/types.ts:293`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`goal-session`](../packages/goal/goal-session), [`tui`](../packages/ui/tui) |
|
||||
| `agent/disposed` | `emit` | [`packages/core/agent/src/types.ts:302`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`goal-session`](../packages/goal/goal-session), [`tui`](../packages/ui/tui) |
|
||||
| `agent/error` | `emit` | [`packages/core/agent/src/types.ts:501`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-session`](../packages/goal/goal-session), `runtime`, [`tui`](../packages/ui/tui) |
|
||||
| `agent/inbox/dequeue` | `emit` | [`packages/core/agent/src/types.ts:333`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent) |
|
||||
| `agent/inbox/discard` | `emit` | [`packages/core/agent/src/types.ts:343`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent) |
|
||||
| `agent/inbox/enqueue` | `emit` | [`packages/core/agent/src/types.ts:323`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), [`goal-session`](../packages/goal/goal-session), [`tui`](../packages/ui/tui) |
|
||||
| `agent/post-step` | `serial` | [`packages/core/agent/src/types.ts:451`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`compact-basic`](../packages/compact/compact-basic), [`session-checkpoint-policy`](../packages/session-persistence/session-checkpoint-policy) |
|
||||
| `agent/pre-step` | `serial` | [`packages/core/agent/src/types.ts:382`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`time-context`](../packages/context/time-context), [`user-approval`](../packages/ui/user-approval) |
|
||||
| `agent/prompt-submit` | `waterfall` | [`packages/core/agent/src/types.ts:398`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`acp`](../packages/ui/acp), [`goal-session`](../packages/goal/goal-session), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) |
|
||||
| `agent/request` | `waterfall` | [`packages/core/agent/src/types.ts:412`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent) |
|
||||
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/types.ts:466`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compact-basic`](../packages/compact/compact-basic), [`llm-retry`](../packages/llm/llm-retry), [`plan-mode`](../packages/plan/plan-mode) |
|
||||
| `agent/session-prefix` | `waterfall` | [`packages/core/agent/src/types.ts:427`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`tool-skill`](../packages/skill/tool-skill), [`workspace-context`](../packages/context/workspace-context) |
|
||||
| `agent/session-start` | `emit` | [`packages/core/agent/src/types.ts:366`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) |
|
||||
| `agent/status` | `emit` | [`packages/core/agent/src/types.ts:311`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), [`goal-session`](../packages/goal/goal-session), `runtime`, [`tui`](../packages/ui/tui) |
|
||||
| `agent/step-result` | `waterfall` | [`packages/core/agent/src/types.ts:439`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - |
|
||||
| `agent/turn-continuation` | `waterfall` | [`packages/core/agent/src/types.ts:477`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode) |
|
||||
| `agent/turn-stop` | `serial` | [`packages/core/agent/src/types.ts:488`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`tool-goal`](../packages/goal/tool-goal) |
|
||||
| `approval/request` | `waterfall` | [`packages/ui/user-approval/src/index.ts:30`](../packages/ui/user-approval/src/index.ts) | [`user-approval`](../packages/ui/user-approval) (`waterfall`) | [`acp`](../packages/ui/acp) |
|
||||
| `commands/change` | `emit` | [`packages/ui/commands/src/index.ts:103`](../packages/ui/commands/src/index.ts) | [`commands`](../packages/ui/commands) (`events.dispatch`) | [`acp`](../packages/ui/acp), [`tui`](../packages/ui/tui) |
|
||||
| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:62`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`) | [`fs-policy`](../packages/fs/fs-policy) |
|
||||
|
||||
@@ -4,7 +4,7 @@ import Loader from '@cordisjs/plugin-loader'
|
||||
import { CallId, LlmAdapter } from '@deepseek-ai/dsh-llm'
|
||||
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import { Session, SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import AgentRegistry, { agentEvents, type Agent } from '@deepseek-ai/dsh-agent'
|
||||
import AgentRegistry, { agentEvents, AgentMessageId, type Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
|
||||
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
|
||||
@@ -42,14 +42,15 @@ function sessionAgent(session: Session, id = 'agent'): Agent {
|
||||
session,
|
||||
status: 'running',
|
||||
ctx: new Context(),
|
||||
send() {},
|
||||
followup() {},
|
||||
steer() {},
|
||||
send: () => AgentMessageId('stub'),
|
||||
followup: () => AgentMessageId('stub'),
|
||||
steer: () => AgentMessageId('stub'),
|
||||
inject(content, options) {
|
||||
session.append('user/message', {
|
||||
content,
|
||||
source: options?.source ?? { kind: 'user' },
|
||||
}, { surfaceOp: 'append' })
|
||||
return AgentMessageId('stub')
|
||||
},
|
||||
cancel() {},
|
||||
whenIdle: () => Promise.resolve(),
|
||||
|
||||
@@ -7,7 +7,7 @@ import Loader from '@cordisjs/plugin-loader'
|
||||
import * as workspaceContext from '@deepseek-ai/dsh-workspace-context'
|
||||
import LlmService, { CallId, type Message, type StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import SessionStore, { Session, SessionId, SESSION_FORMAT_VERSION, type SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import AgentRegistry, { type Agent, type HookContext } from '@deepseek-ai/dsh-agent'
|
||||
import AgentRegistry, { AgentMessageId, type Agent, type HookContext } from '@deepseek-ai/dsh-agent'
|
||||
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
|
||||
import { FileSystem, FsTargetKey, FsVersion } from '@deepseek-ai/dsh-fs'
|
||||
import type {
|
||||
@@ -174,15 +174,16 @@ function stubAgent(cwd?: string, seed: SessionEvent[] = []): Agent {
|
||||
options: {},
|
||||
session,
|
||||
status: 'idle',
|
||||
send() {},
|
||||
followup() {},
|
||||
steer() {},
|
||||
send: () => AgentMessageId('stub'),
|
||||
followup: () => AgentMessageId('stub'),
|
||||
steer: () => AgentMessageId('stub'),
|
||||
inject(content, options) {
|
||||
session.append('user/message', {
|
||||
content,
|
||||
source: options?.source ?? { kind: 'user' },
|
||||
...options?.meta !== undefined ? { meta: options.meta } : {},
|
||||
}, { surfaceOp: 'append' })
|
||||
return AgentMessageId('stub')
|
||||
},
|
||||
cancel() {},
|
||||
whenIdle: () => Promise.resolve(),
|
||||
|
||||
@@ -846,22 +846,22 @@ export const EVENT_API: readonly EventApiEntry[] = [
|
||||
{
|
||||
name: 'agent/inbox/dequeue',
|
||||
mode: 'emit',
|
||||
signature: '\'agent/inbox/dequeue\'(this: Scoped<Agent>, agent: Agent, info: InboxItemInfo): void',
|
||||
jsDoc: '/**\n * The driver claimed one item out of the inbox: a queued item at a turn\n * boundary, or steering drained between steps. Fires after the item leaves\n * its FIFO and before it becomes a durable message.\n * @param agent - the agent whose inbox item was claimed.\n * @param info - the claimed item\'s accepted content, source, contexts, steering, and wakeup facts.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode emit\n */',
|
||||
signature: '\'agent/inbox/dequeue\'(this: Scoped<Agent>, agent: Agent, message: AgentMessage): void',
|
||||
jsDoc: '/**\n * The driver claimed one item out of the inbox: a queued item at a turn\n * boundary, or steering drained between steps. Fires after the item leaves\n * its FIFO and before it becomes a durable message.\n * @param agent - the agent whose inbox item was claimed.\n * @param message - the claimed message (matching the `id` from its `agent/inbox/enqueue`).\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode emit\n */',
|
||||
summary: 'The driver claimed one item out of the inbox: a queued item at a turn boundary, or steering drained between steps.',
|
||||
},
|
||||
{
|
||||
name: 'agent/inbox/discard',
|
||||
mode: 'emit',
|
||||
signature: '\'agent/inbox/discard\'(this: Scoped<Agent>, agent: Agent, items: InboxItemInfo[]): void',
|
||||
jsDoc: '/**\n * `cancel()` (without `keepInbox`) dropped pending inbox items without\n * delivering them. Fires once per effective clearing call with every\n * discarded item, after `agent/cancel-requested` and before the abort.\n * @param agent - the agent whose inbox was cleared.\n * @param items - the discarded items in FIFO order (queued then steering); empty when nothing was pending.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode emit\n */',
|
||||
signature: '\'agent/inbox/discard\'(this: Scoped<Agent>, agent: Agent, messages: AgentMessage[]): void',
|
||||
jsDoc: '/**\n * `cancel()` (without `keepInbox`) dropped pending inbox items without\n * delivering them. Fires once per effective clearing call with every\n * discarded item, after `agent/cancel-requested` and before the abort.\n * @param agent - the agent whose inbox was cleared.\n * @param messages - the discarded messages in FIFO order (queued then steering); empty when nothing was pending.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode emit\n */',
|
||||
summary: '`cancel()` (without `keepInbox`) dropped pending inbox items without delivering them.',
|
||||
},
|
||||
{
|
||||
name: 'agent/inbox/enqueue',
|
||||
mode: 'emit',
|
||||
signature: '\'agent/inbox/enqueue\'(this: Scoped<Agent>, agent: Agent, info: InboxItemInfo): void',
|
||||
jsDoc: '/**\n * A detached, frozen item entered the agent\'s inbox (queued or steering\n * FIFO). Source defaults are already applied, so `info` holds the exact\n * accepted values. This is the enqueue-time live signal; the durable record\n * is the eventual `user/message`/`steering/message`. Injection\n * (`next-step`/no-wakeup) bypasses the FIFOs and does not emit this.\n * @param agent - the agent whose inbox received the item.\n * @param info - the accepted content, source, contexts, steering, and wakeup facts.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode emit\n */',
|
||||
signature: '\'agent/inbox/enqueue\'(this: Scoped<Agent>, agent: Agent, message: AgentMessage): void',
|
||||
jsDoc: '/**\n * A detached, frozen item entered the agent\'s inbox (queued or steering\n * FIFO). Source defaults are already applied, so `info` holds the exact\n * accepted values. This is the enqueue-time live signal; the durable record\n * is the eventual `user/message`/`steering/message`. Injection\n * (`next-step`/no-wakeup) bypasses the FIFOs and does not emit this.\n * @param agent - the agent whose inbox received the item.\n * @param message - the accepted message (its returned `id`, content, source, contexts, steering, and wakeup facts).\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode emit\n */',
|
||||
summary: 'A detached, frozen item entered the agent\'s inbox (queued or steering FIFO).',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,11 @@
|
||||
*/
|
||||
|
||||
import type { ContentBlock, MessageSource } from '@deepseek-ai/dsh-llm'
|
||||
import type { HookContext, InboxItemInfo } from '@deepseek-ai/dsh-agent'
|
||||
import type { AgentMessage, AgentMessageId, HookContext } from '@deepseek-ai/dsh-agent'
|
||||
|
||||
/** One message waiting in an agent's inbox. */
|
||||
/** One message waiting in an agent's inbox; `id` is the value `send` returned. */
|
||||
export interface InboxMessage {
|
||||
id: AgentMessageId
|
||||
content: ContentBlock[]
|
||||
source: MessageSource
|
||||
contexts: HookContext[]
|
||||
@@ -22,10 +23,10 @@ export interface InboxMessage {
|
||||
* Build the `agent/inbox/*` event payload for one inbox item.
|
||||
* @param message - the accepted inbox record.
|
||||
* @param steering - whether the item is in the steering FIFO (`next-step`).
|
||||
* @returns the live-event facts for enqueue/dequeue/discard.
|
||||
* @returns the live-event message for enqueue/dequeue/discard.
|
||||
*/
|
||||
export function inboxInfo(message: InboxMessage, steering: boolean): InboxItemInfo {
|
||||
return { content: message.content, source: message.source, contexts: message.contexts, steering, wakeup: message.wakeup }
|
||||
export function agentMessage(message: InboxMessage, steering: boolean): AgentMessage {
|
||||
return { id: message.id, content: message.content, source: message.source, contexts: message.contexts, steering, wakeup: message.wakeup }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,11 +5,12 @@
|
||||
* @module dsh-agent-loop/loop
|
||||
*/
|
||||
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import type { Context } from 'cordis'
|
||||
import type { ContentBlock, FinishReason, GenerateOptions, LlmCallConfig, LlmFailure, Message } from '@deepseek-ai/dsh-llm'
|
||||
import { isDeepStrictEqual } from 'node:util'
|
||||
import { BlockAssembler, HarnessError, LlmError, assertNever, deepFreeze, errorChain, llmFailureOf, markAgentLoopRequest } from '@deepseek-ai/dsh-llm'
|
||||
import { agentEvents, agentInterruptReasonOf, assembleContextFor } from '@deepseek-ai/dsh-agent'
|
||||
import { agentEvents, agentInterruptReasonOf, assembleContextFor, AgentMessageId } from '@deepseek-ai/dsh-agent'
|
||||
import type { AgentEventDispatch, ContinuationDecision, HookContext, PromptDecision, RequestError, RequestErrorDecision } from '@deepseek-ai/dsh-agent'
|
||||
import { canonicalHeader } from '@deepseek-ai/dsh-session'
|
||||
import type { PromptMessageData, Session, TurnEndReason, TurnTrigger } from '@deepseek-ai/dsh-session'
|
||||
@@ -19,7 +20,7 @@ import { renderPrompt } from '@deepseek-ai/dsh-system-prompt'
|
||||
import type { PromptAssembly } from '@deepseek-ai/dsh-system-prompt'
|
||||
import type {} from '@deepseek-ai/dsh-tools'
|
||||
import { executeToolCalls } from './tool-calls.ts'
|
||||
import { inboxInfo, type Inbox, type InboxMessage } from './inbox.ts'
|
||||
import { agentMessage, type Inbox, type InboxMessage } from './inbox.ts'
|
||||
import type { TurnCancellation } from './cancellation.ts'
|
||||
|
||||
/** Normalize thrown values while preserving an existing error code. */
|
||||
@@ -279,7 +280,7 @@ async function runTurn(
|
||||
const drainSteering = (): boolean => {
|
||||
const messages = handle.inbox.drainSteering()
|
||||
for (const message of messages) {
|
||||
events.emit('agent/inbox/dequeue', inboxInfo(message, true))
|
||||
events.emit('agent/inbox/dequeue', agentMessage(message, true))
|
||||
const prepared = preparePromptMessage(message.content, message.source, message.contexts)
|
||||
session.append('steering/message', { turn, ...prepared.data }, { surfaceOp: 'append' })
|
||||
for (const context of prepared.separateContexts) {
|
||||
@@ -297,7 +298,7 @@ async function runTurn(
|
||||
const message = handle.inbox.dequeueQueued()
|
||||
/* v8 ignore next 3 -- invariant guard: runLoop only calls runTurn when hasQueued */
|
||||
if (!message) throw new Error('runTurn invariant violated: no queued message at turn start')
|
||||
events.emit('agent/inbox/dequeue', inboxInfo(message, false))
|
||||
events.emit('agent/inbox/dequeue', agentMessage(message, false))
|
||||
const trigger: TurnTrigger = { kind: 'message', source: message.source }
|
||||
|
||||
let reason: TurnEndReason = { kind: 'completed' }
|
||||
@@ -542,9 +543,12 @@ async function runTurn(
|
||||
// enqueue event a public steer would, so the inbox ledger stays balanced
|
||||
// (every FIFO entry has a matching enqueue before its dequeue/discard).
|
||||
if (decision.action === 'continue' && decision.reason) {
|
||||
const item: InboxMessage = { content: decision.reason.content, source: decision.reason.source, contexts: [], wakeup: true }
|
||||
const item: InboxMessage = {
|
||||
id: AgentMessageId(randomUUID()), content: decision.reason.content,
|
||||
source: decision.reason.source, contexts: [], wakeup: true,
|
||||
}
|
||||
handle.inbox.steer(item)
|
||||
events.emit('agent/inbox/enqueue', inboxInfo(item, true))
|
||||
events.emit('agent/inbox/enqueue', agentMessage(item, true))
|
||||
}
|
||||
let shouldContinue = decision.action === 'continue'
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { AgentMessageId } from '@deepseek-ai/dsh-agent'
|
||||
import { Inbox } from '../src/inbox.ts'
|
||||
|
||||
function message(text: string) {
|
||||
return { content: [{ type: 'text' as const, text }], source: { kind: 'user' as const }, contexts: [], wakeup: true }
|
||||
return { id: AgentMessageId(text), content: [{ type: 'text' as const, text }], source: { kind: 'user' as const }, contexts: [], wakeup: true }
|
||||
}
|
||||
|
||||
function resolverPair() {
|
||||
|
||||
@@ -56,7 +56,7 @@ Turn and step boundaries and the model token stream are durable `session/event`
|
||||
|
||||
The handle every plugin programs against:
|
||||
|
||||
- `agent.send(content, options?)` — the one delivery primitive over the (`target` × `wakeup`) matrix; `Agent` is an abstract class whose `followup`/`steer`/`inject` aliases are fixed-preset delegates to it. `target: 'next-turn'` (default) queues one independent FIFO item that, if claimed, becomes the sole ordinary message in its turn; `wakeup` (default `true`) wakes a parked driver, while `wakeup: false` queues without waking. `target: 'next-step'` with `wakeup: true` submits steering, and with `wakeup: false` injects durable context without running the model. Omitting `options.source` attests direct human input as `{ kind: 'user' }` (injection defaults to `{ kind: 'plugin', plugin: '' }`) and may authorize policy consumers, so plugins, schedulers, and other non-human producers provide their own source. Content, resolved source, and `options.contexts` become one detached, deeply frozen lossless-JSON record before `agent/inbox/enqueue` and enqueue; invalid data throws synchronously, and caller or notification-listener in-place mutation cannot change the log or model input. After admission, separate contexts become injected `user/message` events, while prompt-prefix contexts are baked before the effective request in the same `user/message`; a block or replacement of the default additional-context decision can discard them. The [one-send-one-turn Agent Note](../../../.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.md) owns the turn rationale.
|
||||
- `agent.send(content, options?)` — the one delivery primitive over the (`target` × `wakeup`) matrix; `Agent` is an abstract class whose `followup`/`steer`/`inject` aliases are fixed-preset delegates to it. It returns the accepted message's opaque `AgentMessageId`, which the message's `agent/inbox/enqueue`/`dequeue`/`discard` events carry so a caller can correlate a queued item with its lifecycle. `target: 'next-turn'` (default) queues one independent FIFO item that, if claimed, becomes the sole ordinary message in its turn; `wakeup` (default `true`) wakes a parked driver, while `wakeup: false` queues without waking. `target: 'next-step'` with `wakeup: true` submits steering, and with `wakeup: false` injects durable context without running the model. Omitting `options.source` attests direct human input as `{ kind: 'user' }` (injection defaults to `{ kind: 'plugin', plugin: '' }`) and may authorize policy consumers, so plugins, schedulers, and other non-human producers provide their own source. Content, resolved source, and `options.contexts` become one detached, deeply frozen lossless-JSON record before `agent/inbox/enqueue` and enqueue; invalid data throws synchronously, and caller or notification-listener in-place mutation cannot change the log or model input. After admission, separate contexts become injected `user/message` events, while prompt-prefix contexts are baked before the effective request in the same `user/message`; a block or replacement of the default additional-context decision can discard them. The [one-send-one-turn Agent Note](../../../.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.md) owns the turn rationale.
|
||||
- `agent.followup(content, options?)` — the `next-turn`/wakeup preset of `send()`: queue an ordinary follow-up turn and wake the driver.
|
||||
- `agent.steer(content, options?)` — the `next-step`/wakeup preset: while running, queue steering for the next checkpoint without dispatching `agent/prompt-submit`; when idle, delegate to a woken follow-up. Attached contexts remain in the same frozen record; separate contexts append immediately after the steering event, while prompt-prefix contexts are baked into that steering event. Both survive late-steering conversion to queued input and disappear with their message on cancellation or terminal discard. Policy can still stop before another step; after turn close and its checkpoint, remaining steering becomes later queued input unless terminal turn policy, cancellation, or disposal discards it.
|
||||
- `agent.inject(content, options?)` — the `next-step`/no-wakeup preset: accept detached in-session context without running the model; the next request sees its `user/message` (default plugin source) with `content` rendered verbatim as a user-role message. `options.meta` persists opaque JSON state without rendering it. While a turn is open it joins that turn, deferring FIFO while the current tool batch executes and draining before turn close if execution is interrupted; while idle it is wrapped in a one-shot `injection` turn and durability checkpoint ([the turn-enclosure invariant](../../../.agents/notes/implemented/architecture/2026-06-15-turn-enclosure-invariant.md)). Injection bypasses the FIFOs and emits no `agent/inbox/*` event.
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import type { Context } from 'cordis'
|
||||
import type { Branded } from '@deepseek-ai/dsh-brand'
|
||||
import type { Scoped } from '@deepseek-ai/dsh-scope'
|
||||
import type { ContentBlock, LlmCallConfig, LlmFailure, Message, MessageSource } from '@deepseek-ai/dsh-llm'
|
||||
import type { JsonValue, Session, SessionId } from '@deepseek-ai/dsh-session'
|
||||
@@ -68,12 +69,31 @@ export interface SendOptions {
|
||||
export type AliasSendOptions = Omit<SendOptions, 'target' | 'wakeup'>
|
||||
|
||||
/**
|
||||
* The resolved facts of one inbox FIFO item, carried by the `agent/inbox/*`
|
||||
* live events. Source defaults are already applied, so these are the exact
|
||||
* values the item was accepted with. `steering` is true for a `next-step`
|
||||
* item drained between steps; a `next-turn` item is claimed at a turn boundary.
|
||||
* Opaque id assigned to one accepted {@link Agent.send} message; returned by
|
||||
* `send` and carried on its `agent/inbox/*` events for correlation.
|
||||
*/
|
||||
export interface InboxItemInfo {
|
||||
export type AgentMessageId = Branded<'AgentMessageId'>
|
||||
|
||||
/**
|
||||
* Brand a string as an {@link AgentMessageId}.
|
||||
* @param id - the generated message id.
|
||||
* @returns the same string, branded; no validation is performed.
|
||||
*/
|
||||
export function AgentMessageId(id: string): AgentMessageId {
|
||||
return id as AgentMessageId
|
||||
}
|
||||
|
||||
/**
|
||||
* One accepted {@link Agent.send} message, carried by the `agent/inbox/*` live
|
||||
* events. `id` is the value `send` returned to the caller, stable across this
|
||||
* message's enqueue, dequeue, and discard events. Source defaults are already
|
||||
* applied, so these are the exact values the item was accepted with. `steering`
|
||||
* is true for a `next-step` item drained between steps; a `next-turn` item is
|
||||
* claimed at a turn boundary.
|
||||
*/
|
||||
export interface AgentMessage {
|
||||
/** The id `send` returned for this message. */
|
||||
id: AgentMessageId
|
||||
content: ContentBlock[]
|
||||
source: MessageSource
|
||||
contexts: HookContext[]
|
||||
@@ -194,8 +214,9 @@ export abstract class Agent {
|
||||
* input throws synchronously before any notification, enqueue, or append.
|
||||
* @param content - the model-facing content blocks to deliver.
|
||||
* @param options - target queue, wakeup decision, source, contexts, and meta.
|
||||
* @returns the accepted message's {@link AgentMessageId}, stable across its `agent/inbox/*` events.
|
||||
*/
|
||||
abstract send(content: ContentBlock[], options?: SendOptions): void
|
||||
abstract send(content: ContentBlock[], options?: SendOptions): AgentMessageId
|
||||
|
||||
/**
|
||||
* Clear queued and steering work — unless `keepInbox` — and abort the active
|
||||
@@ -218,9 +239,10 @@ export abstract class Agent {
|
||||
* ordinary message of its own turn.
|
||||
* @param content - the prompt content blocks.
|
||||
* @param options - source and attached contexts.
|
||||
* @returns the accepted message's {@link AgentMessageId}.
|
||||
*/
|
||||
followup(content: ContentBlock[], options?: AliasSendOptions): void {
|
||||
this.send(content, { ...options, target: 'next-turn', wakeup: true })
|
||||
followup(content: ContentBlock[], options?: AliasSendOptions): AgentMessageId {
|
||||
return this.send(content, { ...options, target: 'next-turn', wakeup: true })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -232,9 +254,10 @@ export abstract class Agent {
|
||||
* Idle steering falls back to a woken follow-up turn.
|
||||
* @param content - the steering content blocks.
|
||||
* @param options - source and attached contexts.
|
||||
* @returns the accepted message's {@link AgentMessageId}.
|
||||
*/
|
||||
steer(content: ContentBlock[], options?: AliasSendOptions): void {
|
||||
this.send(content, { ...options, target: 'next-step', wakeup: true })
|
||||
steer(content: ContentBlock[], options?: AliasSendOptions): AgentMessageId {
|
||||
return this.send(content, { ...options, target: 'next-step', wakeup: true })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -247,9 +270,10 @@ export abstract class Agent {
|
||||
* `agent/error`. An omitted source defaults to `{ kind: 'plugin', plugin: '' }`.
|
||||
* @param content - the injected context content blocks.
|
||||
* @param options - source and durable model-hidden meta.
|
||||
* @returns the accepted message's {@link AgentMessageId}.
|
||||
*/
|
||||
inject(content: ContentBlock[], options?: AliasSendOptions): void {
|
||||
this.send(content, { ...options, target: 'next-step', wakeup: false })
|
||||
inject(content: ContentBlock[], options?: AliasSendOptions): AgentMessageId {
|
||||
return this.send(content, { ...options, target: 'next-step', wakeup: false })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,31 +316,31 @@ declare module 'cordis' {
|
||||
* is the eventual `user/message`/`steering/message`. Injection
|
||||
* (`next-step`/no-wakeup) bypasses the FIFOs and does not emit this.
|
||||
* @param agent - the agent whose inbox received the item.
|
||||
* @param info - the accepted content, source, contexts, steering, and wakeup facts.
|
||||
* @param message - the accepted message (its returned `id`, content, source, contexts, steering, and wakeup facts).
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
* @mode emit
|
||||
*/
|
||||
'agent/inbox/enqueue'(this: Scoped<Agent>, agent: Agent, info: InboxItemInfo): void
|
||||
'agent/inbox/enqueue'(this: Scoped<Agent>, agent: Agent, message: AgentMessage): void
|
||||
/**
|
||||
* The driver claimed one item out of the inbox: a queued item at a turn
|
||||
* boundary, or steering drained between steps. Fires after the item leaves
|
||||
* its FIFO and before it becomes a durable message.
|
||||
* @param agent - the agent whose inbox item was claimed.
|
||||
* @param info - the claimed item's accepted content, source, contexts, steering, and wakeup facts.
|
||||
* @param message - the claimed message (matching the `id` from its `agent/inbox/enqueue`).
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
* @mode emit
|
||||
*/
|
||||
'agent/inbox/dequeue'(this: Scoped<Agent>, agent: Agent, info: InboxItemInfo): void
|
||||
'agent/inbox/dequeue'(this: Scoped<Agent>, agent: Agent, message: AgentMessage): void
|
||||
/**
|
||||
* `cancel()` (without `keepInbox`) dropped pending inbox items without
|
||||
* delivering them. Fires once per effective clearing call with every
|
||||
* discarded item, after `agent/cancel-requested` and before the abort.
|
||||
* @param agent - the agent whose inbox was cleared.
|
||||
* @param items - the discarded items in FIFO order (queued then steering); empty when nothing was pending.
|
||||
* @param messages - the discarded messages in FIFO order (queued then steering); empty when nothing was pending.
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
* @mode emit
|
||||
*/
|
||||
'agent/inbox/discard'(this: Scoped<Agent>, agent: Agent, items: InboxItemInfo[]): void
|
||||
'agent/inbox/discard'(this: Scoped<Agent>, agent: Agent, messages: AgentMessage[]): void
|
||||
/**
|
||||
* Effective broad cancellation was requested, before queued/steering work
|
||||
* is cleared or the active turn is aborted. This observe-only notification
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { Events } from 'cordis'
|
||||
import { Session, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import AgentRegistry, {
|
||||
Agent,
|
||||
AgentMessageId,
|
||||
agentEvents,
|
||||
agentInterruptReasonOf,
|
||||
} from '@deepseek-ai/dsh-agent'
|
||||
@@ -20,7 +21,7 @@ function stubAgent(rawId: string, overrides: Partial<Agent> = {}): Agent {
|
||||
session: new Session(id),
|
||||
status: 'idle',
|
||||
ctx: new Context(),
|
||||
send() {},
|
||||
send: () => AgentMessageId('stub'),
|
||||
cancel() {},
|
||||
whenIdle() { return Promise.resolve() },
|
||||
...overrides,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { AgentMessageId, type Agent } from '@deepseek-ai/dsh-agent'
|
||||
import * as AgentInvariant from '@deepseek-ai/dsh-agent/invariant'
|
||||
import { scopeTarget } from '@deepseek-ai/dsh-scope'
|
||||
import InvariantService from '@deepseek-ai/dsh-invariants'
|
||||
@@ -58,7 +58,7 @@ describe('agent status invariants', () => {
|
||||
})
|
||||
|
||||
describe('agent inbox invariants', () => {
|
||||
const info = (steering: boolean) => ({ content: [], source: { kind: 'user' as const }, contexts: [], steering, wakeup: true })
|
||||
const info = (steering: boolean) => ({ id: AgentMessageId('m'), content: [], source: { kind: 'user' as const }, contexts: [], steering, wakeup: true })
|
||||
|
||||
it('accepts a dequeue and a discard covered by prior enqueues', async () => {
|
||||
const ctx = await setup()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import type { Events } from 'cordis'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { AgentMessageId, type Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { scopeTarget } from '@deepseek-ai/dsh-scope'
|
||||
import * as ScopeInvariant from '@deepseek-ai/dsh-scope/invariant'
|
||||
import InvariantService from '@deepseek-ai/dsh-invariants'
|
||||
@@ -42,8 +42,8 @@ describe('scoped-dispatch invariants', () => {
|
||||
'agent/created': [agent],
|
||||
'agent/disposed': [agent],
|
||||
'agent/status': [agent, 'idle'],
|
||||
'agent/inbox/enqueue': [agent, { content: [], source: { kind: 'user' }, contexts: [], steering: false, wakeup: true }],
|
||||
'agent/inbox/dequeue': [agent, { content: [], source: { kind: 'user' }, contexts: [], steering: false, wakeup: true }],
|
||||
'agent/inbox/enqueue': [agent, { id: AgentMessageId('m'), content: [], source: { kind: 'user' }, contexts: [], steering: false, wakeup: true }],
|
||||
'agent/inbox/dequeue': [agent, { id: AgentMessageId('m'), content: [], source: { kind: 'user' }, contexts: [], steering: false, wakeup: true }],
|
||||
'agent/inbox/discard': [agent, []],
|
||||
'agent/cancel-requested': [agent, { kind: 'user' }],
|
||||
'agent/session-start': [agent, 'startup'],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import Loader from '@cordisjs/plugin-loader'
|
||||
import AgentRegistry from '@deepseek-ai/dsh-agent'
|
||||
import AgentRegistry, { AgentMessageId } from '@deepseek-ai/dsh-agent'
|
||||
import type { Agent, AgentStatus, AliasSendOptions } from '@deepseek-ai/dsh-agent'
|
||||
import CommandService from '@deepseek-ai/dsh-commands'
|
||||
import GoalService from '@deepseek-ai/dsh-goal'
|
||||
@@ -48,10 +48,10 @@ function stubAgent(id: string): { agent: Agent; session: Session } {
|
||||
session,
|
||||
ctx: new Context(),
|
||||
get status() { return status },
|
||||
send() {},
|
||||
followup() {},
|
||||
steer() {},
|
||||
inject(content, options) { appendInjection(session, content, options) },
|
||||
send: () => AgentMessageId('stub'),
|
||||
followup: () => AgentMessageId('stub'),
|
||||
steer: () => AgentMessageId('stub'),
|
||||
inject(content, options) { appendInjection(session, content, options); return AgentMessageId('stub') },
|
||||
cancel() { status = 'idle' },
|
||||
whenIdle() { return Promise.resolve() },
|
||||
}
|
||||
|
||||
@@ -488,7 +488,7 @@ describe('same-session goal driving', () => {
|
||||
if (options?.source?.kind === 'goal' && (options.target ?? 'next-turn') === 'next-turn') {
|
||||
throw new Error('queue rejected')
|
||||
}
|
||||
realSend(content, options)
|
||||
return realSend(content, options)
|
||||
})
|
||||
test.ctx.goals.create(test.agent, { objective: 'handle queue failure' })
|
||||
|
||||
@@ -510,7 +510,7 @@ describe('same-session goal driving', () => {
|
||||
test.ctx.goals.disarm(test.agent)
|
||||
throw new Error('queue rejected after disarm')
|
||||
}
|
||||
realSend(content, options)
|
||||
return realSend(content, options)
|
||||
})
|
||||
test.ctx.goals.create(test.agent, { objective: 'preserve the newer activation state' })
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import AgentRegistry, { agentEvents } from '@deepseek-ai/dsh-agent'
|
||||
import AgentRegistry, { agentEvents, AgentMessageId } from '@deepseek-ai/dsh-agent'
|
||||
import type { Agent, AgentStatus, AliasSendOptions } from '@deepseek-ai/dsh-agent'
|
||||
import { HarnessError, type ContentBlock, type MessageSource } from '@deepseek-ai/dsh-llm'
|
||||
import SessionStore, { Session, SessionId } from '@deepseek-ai/dsh-session'
|
||||
@@ -64,12 +64,13 @@ function stubAgentForSession(session: Session): StubAgent {
|
||||
session,
|
||||
ctx: new Context(),
|
||||
get status() { return status },
|
||||
send() {},
|
||||
followup() {},
|
||||
steer() {},
|
||||
send: () => AgentMessageId('stub'),
|
||||
followup: () => AgentMessageId('stub'),
|
||||
steer: () => AgentMessageId('stub'),
|
||||
inject(content, options) {
|
||||
if (shouldDefer) deferred.push({ content, options })
|
||||
else appendInjection(session, content, options)
|
||||
return AgentMessageId('stub')
|
||||
},
|
||||
cancel() {},
|
||||
whenIdle() { return Promise.resolve() },
|
||||
@@ -476,7 +477,7 @@ describe('GoalService mutations', () => {
|
||||
let reject = true
|
||||
stub.agent.inject = (content, options) => {
|
||||
if (reject) throw new Error('injection rejected')
|
||||
append(content, options)
|
||||
return append(content, options)
|
||||
}
|
||||
ctx.agents.register(stub.agent)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import Loader from '@cordisjs/plugin-loader'
|
||||
import AgentRegistry, { agentEvents } from '@deepseek-ai/dsh-agent'
|
||||
import AgentRegistry, { agentEvents, AgentMessageId } from '@deepseek-ai/dsh-agent'
|
||||
import type { Agent, AgentStatus, AliasSendOptions } from '@deepseek-ai/dsh-agent'
|
||||
import GoalService, { GoalId } from '@deepseek-ai/dsh-goal'
|
||||
import type { GoalRef } from '@deepseek-ai/dsh-goal'
|
||||
@@ -31,9 +31,9 @@ function stubAgent(rawId: string, supplied?: Session): StubAgent {
|
||||
session,
|
||||
get status() { return status },
|
||||
ctx: new Context(),
|
||||
send() {},
|
||||
followup() {},
|
||||
steer() {},
|
||||
send: () => AgentMessageId('stub'),
|
||||
followup: () => AgentMessageId('stub'),
|
||||
steer: () => AgentMessageId('stub'),
|
||||
inject(content: ContentBlock[], options?: AliasSendOptions) {
|
||||
const source = options?.source ?? { kind: 'plugin', plugin: '' }
|
||||
session.append('user/message', {
|
||||
@@ -41,6 +41,7 @@ function stubAgent(rawId: string, supplied?: Session): StubAgent {
|
||||
source,
|
||||
...options?.meta === undefined ? {} : { meta: options.meta },
|
||||
}, { surfaceOp: 'append' })
|
||||
return AgentMessageId('stub')
|
||||
},
|
||||
cancel() {},
|
||||
whenIdle() { return Promise.resolve() },
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { IPty, IPtyForkOptions } from 'node-pty'
|
||||
import { Context } from 'cordis'
|
||||
import Loader from '@cordisjs/plugin-loader'
|
||||
import { Session, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { AgentMessageId, type Agent } from '@deepseek-ai/dsh-agent'
|
||||
import SandboxProvider from '@deepseek-ai/dsh-sandbox'
|
||||
import type { ConfinedArgv, SandboxPolicy } from '@deepseek-ai/dsh-sandbox'
|
||||
import SandboxPolicyService from '@deepseek-ai/dsh-sandbox-policy'
|
||||
@@ -42,7 +42,7 @@ function agent(ctx: Context): Agent {
|
||||
const id = SessionId('agent')
|
||||
return {
|
||||
id, options: {}, session: new Session(id), status: 'idle', ctx,
|
||||
send() {}, followup() {}, steer() {}, inject() {}, cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
send: () => AgentMessageId('stub'), followup: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), inject: () => AgentMessageId('stub'), cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import { Session, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import AgentRegistry from '@deepseek-ai/dsh-agent'
|
||||
import AgentRegistry, { AgentMessageId } from '@deepseek-ai/dsh-agent'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import PtyService from '@deepseek-ai/dsh-pty'
|
||||
import SandboxProvider from '@deepseek-ai/dsh-sandbox'
|
||||
@@ -34,7 +34,7 @@ function stubAgent(ctx: Context, rawId: string): Agent {
|
||||
const scope = ctx.plugin(() => {})
|
||||
return {
|
||||
id, options: {}, session: new Session(id), status: 'idle', ctx: scope.ctx,
|
||||
send() {}, followup() {}, steer() {}, inject() {}, cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
send: () => AgentMessageId('stub'), followup: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), inject: () => AgentMessageId('stub'), cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, expectTypeOf, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import { Session, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import AgentRegistry from '@deepseek-ai/dsh-agent'
|
||||
import AgentRegistry, { AgentMessageId } from '@deepseek-ai/dsh-agent'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import PtyService, { PtyError, PtySessionId } from '@deepseek-ai/dsh-pty'
|
||||
import type {
|
||||
@@ -27,10 +27,10 @@ function stubAgent(ctx: Context, rawId: string): Agent {
|
||||
session: new Session(id),
|
||||
status: 'idle',
|
||||
ctx: scopeFiber.ctx,
|
||||
send() {},
|
||||
followup() {},
|
||||
steer() {},
|
||||
inject() {},
|
||||
send: () => AgentMessageId('stub'),
|
||||
followup: () => AgentMessageId('stub'),
|
||||
steer: () => AgentMessageId('stub'),
|
||||
inject: () => AgentMessageId('stub'),
|
||||
cancel() {},
|
||||
whenIdle: () => Promise.resolve(),
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import Loader from '@cordisjs/plugin-loader'
|
||||
import Include from '@cordisjs/plugin-include'
|
||||
import { CallId } from '@deepseek-ai/dsh-llm'
|
||||
import { Session, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import AgentRegistry from '@deepseek-ai/dsh-agent'
|
||||
import AgentRegistry, { AgentMessageId } from '@deepseek-ai/dsh-agent'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRegistry from '@deepseek-ai/dsh-tools'
|
||||
@@ -40,7 +40,7 @@ function agent(ctx: Context): Agent {
|
||||
const id = SessionId('pty-loader-agent')
|
||||
const value: Agent = {
|
||||
id, options: {}, session: new Session(id), status: 'idle', ctx: scope.ctx,
|
||||
send() {}, followup() {}, steer() {}, inject() {}, cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
send: () => AgentMessageId('stub'), followup: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), inject: () => AgentMessageId('stub'), cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
}
|
||||
ctx.agents.register(value)
|
||||
return value
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import { CallId } from '@deepseek-ai/dsh-llm'
|
||||
import { Session, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import AgentRegistry from '@deepseek-ai/dsh-agent'
|
||||
import AgentRegistry, { AgentMessageId } from '@deepseek-ai/dsh-agent'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRegistry from '@deepseek-ai/dsh-tools'
|
||||
@@ -17,7 +17,7 @@ function fakeAgent(ctx: Context, rawId: string): Agent {
|
||||
const id = SessionId(rawId)
|
||||
const agent: Agent = {
|
||||
id, options: {}, session: new Session(id), status: 'idle', ctx: scope.ctx,
|
||||
send() {}, followup() {}, steer() {}, inject() {}, cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
send: () => AgentMessageId('stub'), followup: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), inject: () => AgentMessageId('stub'), cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
}
|
||||
ctx.agents.register(agent)
|
||||
return agent
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, expectTypeOf, it, vi } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import { Session, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import AgentRegistry from '@deepseek-ai/dsh-agent'
|
||||
import AgentRegistry, { AgentMessageId } from '@deepseek-ai/dsh-agent'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import TaskService, { TaskId } from '@deepseek-ai/dsh-tasks'
|
||||
import type { TaskHooks, TaskKind, TaskOutcome, TaskSnapshot, TaskStart } from '@deepseek-ai/dsh-tasks'
|
||||
@@ -23,10 +23,10 @@ function stubAgent(ctx: Context, rawId: string): Agent {
|
||||
session: new Session(id),
|
||||
status: 'idle' as const,
|
||||
ctx: scopeFiber.ctx,
|
||||
send() {},
|
||||
followup() {},
|
||||
steer() {},
|
||||
inject() {},
|
||||
send: () => AgentMessageId('stub'),
|
||||
followup: () => AgentMessageId('stub'),
|
||||
steer: () => AgentMessageId('stub'),
|
||||
inject: () => AgentMessageId('stub'),
|
||||
cancel() {},
|
||||
whenIdle() { return Promise.resolve() },
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Context } from 'cordis'
|
||||
import type { Terminal } from '@earendil-works/pi-tui'
|
||||
import AgentRegistry, {
|
||||
AgentMessageId,
|
||||
type Agent,
|
||||
type AgentCancelCause,
|
||||
type AgentOptions,
|
||||
@@ -152,16 +153,19 @@ export async function createTuiTestHarness<TerminalType extends Terminal, Exit e
|
||||
send(content, options) {
|
||||
sent.push(content)
|
||||
sentOptions.push(options)
|
||||
return AgentMessageId('stub')
|
||||
},
|
||||
followup(content, options) {
|
||||
sent.push(content)
|
||||
sentOptions.push(options)
|
||||
return AgentMessageId('stub')
|
||||
},
|
||||
steer(content, options) {
|
||||
steered.push(content)
|
||||
steeredOptions.push(options)
|
||||
return AgentMessageId('stub')
|
||||
},
|
||||
inject() {},
|
||||
inject: () => AgentMessageId('stub'),
|
||||
cancel(cause = { kind: 'user' }) {
|
||||
cancelled.push(cause)
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@ import { join, resolve } from 'node:path'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import { CombinedAutocompleteProvider, type Terminal } from '@earendil-works/pi-tui'
|
||||
import AgentRegistry, { agentEvents, assembleContextFor, type Agent } from '@deepseek-ai/dsh-agent'
|
||||
import AgentRegistry, { agentEvents, assembleContextFor, AgentMessageId, type Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { type LlmCallConfig } from '@deepseek-ai/dsh-llm'
|
||||
import CommandService, { type CommandInvocation } from '@deepseek-ai/dsh-commands'
|
||||
import SessionStore, { SessionId, type JsonValue, type SessionHeader } from '@deepseek-ai/dsh-session'
|
||||
@@ -552,7 +552,7 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
expect(result.terminal.output).not.toContain('queued')
|
||||
|
||||
const queueSteering = (text: string): void => {
|
||||
result.ctx.emit('agent/inbox/enqueue', result.agent, { content: [{ type: 'text', text }], source: { kind: 'user' }, contexts: [], steering: true, wakeup: true })
|
||||
result.ctx.emit('agent/inbox/enqueue', result.agent, { id: AgentMessageId('stub'), content: [{ type: 'text', text }], source: { kind: 'user' }, contexts: [], steering: true, wakeup: true })
|
||||
}
|
||||
const drainSteering = (text: string): void => {
|
||||
result.session.append('steering/message', { turn: 1, content: [{ type: 'text', text }], source: { kind: 'user' } }, { surfaceOp: 'append' })
|
||||
@@ -561,7 +561,7 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
// A steering queue for a different agent never touches this status line.
|
||||
const other = { ...result.agent, id: SessionId('other') } as unknown as Agent
|
||||
result.terminal.output = ''
|
||||
result.ctx.emit('agent/inbox/enqueue', other, { content: [{ type: 'text', text: 'elsewhere' }], source: { kind: 'user' }, contexts: [], steering: true, wakeup: true })
|
||||
result.ctx.emit('agent/inbox/enqueue', other, { id: AgentMessageId('stub'), content: [{ type: 'text', text: 'elsewhere' }], source: { kind: 'user' }, contexts: [], steering: true, wakeup: true })
|
||||
await tick()
|
||||
expect(result.terminal.output).not.toContain('queued')
|
||||
|
||||
@@ -574,7 +574,7 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
|
||||
// A non-steering queue (an idle-style send) leaves the badge untouched.
|
||||
result.terminal.output = ''
|
||||
result.ctx.emit('agent/inbox/enqueue', result.agent, { content: [{ type: 'text', text: 'sent' }], source: { kind: 'user' }, contexts: [], steering: false, wakeup: true })
|
||||
result.ctx.emit('agent/inbox/enqueue', result.agent, { id: AgentMessageId('stub'), content: [{ type: 'text', text: 'sent' }], source: { kind: 'user' }, contexts: [], steering: false, wakeup: true })
|
||||
drainSteering('first')
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('1 queued')
|
||||
@@ -628,7 +628,7 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
const idle = await setup()
|
||||
// A steering queue arriving while idle has no status line to badge, so the
|
||||
// refresh is a no-op beyond requesting a render.
|
||||
idle.ctx.emit('agent/inbox/enqueue', idle.agent, { content: [{ type: 'text', text: 'early' }], source: { kind: 'user' }, contexts: [], steering: true, wakeup: true })
|
||||
idle.ctx.emit('agent/inbox/enqueue', idle.agent, { id: AgentMessageId('stub'), content: [{ type: 'text', text: 'early' }], source: { kind: 'user' }, contexts: [], steering: true, wakeup: true })
|
||||
idle.session.append('tool/call', { turn: 1, step: 0, callId: 'pre' as never, name: 'bash', arguments: '{}' })
|
||||
await tick()
|
||||
expect(idle.terminal.output).not.toContain('Executing tools')
|
||||
@@ -2208,7 +2208,7 @@ describe('terminal mounting', () => {
|
||||
const session = ctx.sessions.create(SessionId('main'))
|
||||
ctx.agents.register({
|
||||
id: session.id, options: {}, session, status: 'idle', ctx,
|
||||
send() {}, followup() {}, steer() {}, inject() {}, cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
send: () => AgentMessageId('stub'), followup: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), inject: () => AgentMessageId('stub'), cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
})
|
||||
const terminal = new FakeTerminal()
|
||||
mountTui(ctx, { color: false }, { terminal, exit: vi.fn() })
|
||||
@@ -2232,7 +2232,7 @@ describe('terminal mounting', () => {
|
||||
const session = ctx.sessions.create(SessionId('main'))
|
||||
ctx.agents.register({
|
||||
id: session.id, options: {}, session, status: 'idle', ctx,
|
||||
send() {}, followup() {}, steer() {}, inject() {}, cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
send: () => AgentMessageId('stub'), followup: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), inject: () => AgentMessageId('stub'), cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
})
|
||||
const terminal = new FakeTerminal()
|
||||
// Mirror dsh-tui's own inject (minus loader, the absence under test).
|
||||
@@ -2266,14 +2266,14 @@ describe('terminal mounting', () => {
|
||||
const otherSession = ctx.sessions.create(SessionId('other-session'))
|
||||
ctx.agents.register({
|
||||
id: otherSession.id, options: {}, session: otherSession, status: 'idle', ctx,
|
||||
send() {}, followup() {}, steer() {}, inject() {}, cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
send: () => AgentMessageId('stub'), followup: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), inject: () => AgentMessageId('stub'), cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
})
|
||||
expect(terminal.started).toBe(0)
|
||||
|
||||
const session = ctx.sessions.create(SessionId('late-session'))
|
||||
const agent = {
|
||||
id: session.id, options: {}, session, status: 'idle', ctx,
|
||||
send() {}, followup() {}, steer() {}, inject() {}, cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
send: () => AgentMessageId('stub'), followup: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), inject: () => AgentMessageId('stub'), cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
} as Agent
|
||||
ctx.agents.register(agent)
|
||||
await tick()
|
||||
@@ -2303,7 +2303,7 @@ describe('terminal mounting', () => {
|
||||
const session = ctx.sessions.create(SessionId('main-session'))
|
||||
ctx.agents.register({
|
||||
id: session.id, options: {}, session, status: 'idle', ctx,
|
||||
send() {}, followup() {}, steer() {}, inject() {}, cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
send: () => AgentMessageId('stub'), followup: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), inject: () => AgentMessageId('stub'), cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
})
|
||||
await tick()
|
||||
expect(terminal.started).toBe(0)
|
||||
@@ -2345,7 +2345,7 @@ describe('terminal mounting', () => {
|
||||
session.append('step/start', { turn: 1, step: 1 })
|
||||
ctx.agents.register({
|
||||
id: session.id, options: {}, session, status: 'running', ctx,
|
||||
send() {}, followup() {}, steer() {}, inject() {}, cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
send: () => AgentMessageId('stub'), followup: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), inject: () => AgentMessageId('stub'), cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
})
|
||||
const terminal = new FakeTerminal()
|
||||
terminal.start = () => { throw new Error('terminal startup failed') }
|
||||
|
||||
@@ -35,8 +35,9 @@ export const LINK_MAP: Record<string, string> = {
|
||||
ContinuationDecision: 'core.md',
|
||||
ContinuationStop: 'core.md',
|
||||
GenerateOptions: 'core.md',
|
||||
AgentMessage: 'core.md',
|
||||
AgentMessageId: 'core.md',
|
||||
HookContext: 'core.md',
|
||||
InboxItemInfo: 'core.md',
|
||||
LlmCallConfig: 'core.md',
|
||||
LlmModelContext: 'core.md',
|
||||
LlmFailure: 'llm-streaming.md',
|
||||
|
||||
@@ -83,7 +83,12 @@
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "InboxItemInfo",
|
||||
"symbol": "AgentMessageId",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "AgentMessage",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user