refactor agent pre-step inbox lifecycle
This commit is contained in:
@@ -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 packages/core/agent/README.md
|
||||
README.md: b21ec0dc49604a91e4b8b34b0f47ccc01d897e88
|
||||
README.zh.md: 2cf6960310af0524d3e0f10c2cd3cf97fbf1869f
|
||||
README.md: 8942dcd976f9c7c5a8109ad3979fefe2a913cba5
|
||||
README.zh.md: 274a37556a9c2af140c57e5346da8abf7bc49bf3
|
||||
|
||||
@@ -50,9 +50,11 @@ Agent *creation* is provided by the plugin implementing `AgentFactory` (`dsh-age
|
||||
|
||||
The lifecycle edges have two important local caveats. `agent/created` runs after scoped setup and after both session and agent registry entries exist. Setup is trusted composition-only code; the immediately following non-vetoing `agent/session-start` notification is the first supported startup injection point. `agent/disposed` always means the exact agent has left the registry. AgentLoop emits it after its driver is quiescent, while ordered teardown may still be detaching the session and unwinding the scope; custom agents registered directly own any stronger driver-ordering contract themselves.
|
||||
|
||||
Most interception points are cooperative waterfalls. Turn-scoped asynchronous seams receive one explicit `AbortSignal`, with `signal` immediately before a waterfall's final `next`; listeners may cooperate but must not retain it as authority over another turn. `agent/step` is the serial checkpoint before request derivation, while `agent/request-error` is the failed-model-request recovery waterfall: it receives request coordinates, normalized failure facts, the serving registration's retry policy when available, and the signal. A listener returns `{ kind: 'retry' }` without calling `next()` when it owns recovery. `agent/turn-stopping` runs before an otherwise completed turn closes. Ordinary queued prompts remain intact. The [explicit-cancellation decision](../../../.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md) owns signal lifetime; the [agent-scope runtime-design Agent Note](../../../.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md#three-execution-boundaries-are-deliberately-one-way) owns scoped dispatch and terminal settlement.
|
||||
Most interception points are cooperative waterfalls. `agent/pre-step` receives the exclusive claimed `UserMessage[]` plus a `PreStepContext` containing the proposed `turn`, `step`, and cancellation `signal`; its batch may be empty when tools already require another request. Other turn-scoped asynchronous seams receive their explicit `AbortSignal` positionally. Listeners may cooperate with a signal but must not retain it as authority over another turn. `agent/request-error` is the failed-model-request recovery waterfall: it receives request coordinates, normalized failure facts, the serving registration's retry policy when available, and the signal. A listener returns `{ kind: 'retry' }` without calling `next()` when it owns recovery. `agent/turn-stopping` runs before an otherwise completed turn closes. The [explicit-cancellation decision](../../../.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md) owns signal lifetime; the [agent-scope runtime-design Agent Note](../../../.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md#three-execution-boundaries-are-deliberately-one-way) owns scoped dispatch and terminal settlement.
|
||||
|
||||
`PromptDecision.allow.messages` is the complete identified, frozen batch admitted by prompt interception. A listener that wraps a downstream allow preserves that batch unless it intentionally replaces it. A block must choose `discardClaimed`; this affects only the submitted batch, while messages not claimed by that admission remain pending.
|
||||
`PreStepDecision` is either `{ kind: 'reject' }` or `{ kind: 'enter', messages }`. The enter branch is the complete identified, frozen batch for the proposed step. A listener that wraps downstream entry preserves that batch unless it intentionally replaces it; additions follow the waterfall's natural return order. Claiming already removed the offered messages from the inbox, so rejection does not retain them. Messages inserted after the claim remain pending for a later boundary.
|
||||
|
||||
Inbox live notifications are deliberately per-message and minimal: `agent/inbox/inserted { message }`, `agent/inbox/claimed { message, turn }`, and `agent/inbox/discarded { message }`. They complement the durable `agent/inbox/spliced` projection without adding another lifecycle envelope.
|
||||
|
||||
Turn and step boundaries and the model token stream are durable `session/event` facts rather than mirrored `agent/*` notifications. Consumers read `turn/*`, `step/*`, and `assistant/chunk` from the session feed; tool policy and outcome observation belong to the complete pipeline documented by [`dsh-tools`](../tools/README.md).
|
||||
|
||||
@@ -60,10 +62,10 @@ Turn and step boundaries and the model token stream are durable `session/event`
|
||||
|
||||
The handle every plugin programs against:
|
||||
|
||||
- `agent.inbox` — the agent-owned projection of durable `agent/inbox/spliced` events. `nextTurn` and `nextStep` expose pending `UserMessage` values; `splice(target, start, deleteCount, inserted, outcome?)` uses standard splice coordinates to insert, edit, remove, admit, or cancel them. `MessageId` is the only occurrence identity and must remain unique while pending.
|
||||
- `agent.followup(message)` — queue an ordinary `next-turn` message and wake the driver. It returns no completion handle; the message id identifies inbox and admission facts, not a later output or `turn/end`.
|
||||
- `agent.inbox` — the agent-owned projection of durable `agent/inbox/spliced` events. `nextTurn` and `nextStep` expose pending `UserMessage` values. `append`, `prepend`, `update`, `remove`, and `splice` mutate them; ordinary removals are durable cancellations and emit `agent/inbox/discarded`. `claim(target)` atomically removes the next proposed batch with pure deletion splices; the loop then emits `agent/inbox/claimed`. `MessageId` is the only occurrence identity and must remain unique while pending.
|
||||
- `agent.followup(message)` — queue an ordinary `next-turn` message and wake the driver. It returns no completion handle; the message id identifies inbox insertion, claim, and discard facts, not a later output or `turn/end`.
|
||||
- `agent.steer(message)` — queue waking `next-step` input. An idle driver schedules a turn; collecting and running drivers consume it at their next step boundary.
|
||||
- `agent.inject(message)` — queue non-waking `next-step` context. During admission or an open turn it waits for the next safe log position; otherwise it appends immediately without opening a turn.
|
||||
- `agent.inject(message)` — queue non-waking `next-step` context. A collecting or running driver claims it at the nearest later pre-step boundary; an idle driver leaves it pending until `followup()` or `steer()` wakes the driver. It may miss a request whose pre-step already claimed its batch.
|
||||
- `agent.cancel(cause, options?)` — cancel the active driver and, unless `options.keepInbox`, durably cancel all pending inbox work. Idle cancellation is a no-op.
|
||||
- `agent.whenIdle()` — observe whole-agent quiescence, including replacement work scheduled before the current driver retires. It does not settle any particular message.
|
||||
- `agent.session`, `agent.status`, `agent.options`, `agent.id`, `agent.ctx`
|
||||
@@ -82,7 +84,7 @@ The handle every plugin programs against:
|
||||
|
||||
#### What the model sees
|
||||
|
||||
`send`, `steer`, and `inject` feed the owning session. `agent/prompt-submit`, `agent/step`, and other declared events let plugins block a prompt or add durable request material; this interface contributes no fixed prose itself.
|
||||
`send`, `steer`, and `inject` feed the owning session. `agent/pre-step` and other declared events let plugins reject a proposed step or add durable request material; this interface contributes no fixed prose itself.
|
||||
|
||||
#### Token effect
|
||||
|
||||
|
||||
@@ -50,9 +50,11 @@ Agent *创建* 由实现 `AgentFactory` 的插件(`dsh-agent-loop`)提供,
|
||||
|
||||
生命周期边有两个重要的本地注意事项。`agent/created` 在作用域 setup 之后、会话与 agent 注册表条目都存在之后运行。Setup 是受信任、仅用于组合的代码;紧随其后且不可 veto 的 `agent/session-start` 通知是第一个受支持的启动注入点。`agent/disposed` 始终表示确切 agent 已离开注册表。AgentLoop 在其驱动器完全停稳后发出该事件,而有序 teardown 此时可能仍在分离会话并撤销作用域;直接注册的自定义 agent 自行拥有任何更强的驱动器顺序契约。
|
||||
|
||||
大多数拦截点都是协作式 waterfall(瀑布式事件)。轮次作用域的异步 seam 接收一个显式 `AbortSignal`,其中 `signal` 紧邻 waterfall 最终的 `next`;监听器可以配合,但不得将它保留为控制另一轮次的权限。`agent/step` 是派生请求前的串行检查点,而 `agent/request-error` 是失败模型请求的恢复 waterfall:它接收请求坐标、规范化失败事实、可用时提供服务的注册项重试策略以及信号。拥有恢复权的监听器返回 `{ kind: 'retry' }` 且不调用 `next()`。`agent/turn-stopping` 在本可完成的轮次关闭前运行。普通排队提示词保持原样。信号生命周期由[显式取消决策](../../../.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md)拥有;作用域分发与终止结算由 [agent 作用域 runtime 设计 Agent Note(agent 决策记录)](../../../.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md#three-execution-boundaries-are-deliberately-one-way)拥有。
|
||||
大多数拦截点都是协作式 waterfall(瀑布式事件)。`agent/pre-step` 接收独占的已领取 `UserMessage[]`,以及包含拟进入 `turn`、`step` 与取消 `signal` 的 `PreStepContext`;当工具已经要求继续请求时,该批次可以为空。其他轮次作用域异步 seam 仍按位置接收显式 `AbortSignal`。监听器可以配合信号,但不得将它保留为控制另一轮次的权限。`agent/request-error` 是失败模型请求的恢复 waterfall:它接收请求坐标、规范化失败事实、可用时提供服务的注册项重试策略以及信号。拥有恢复权的监听器返回 `{ kind: 'retry' }` 且不调用 `next()`。`agent/turn-stopping` 在本可完成的轮次关闭前运行。信号生命周期由[显式取消决策](../../../.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md)拥有;作用域分发与终止结算由 [agent 作用域 runtime 设计 Agent Note(agent 决策记录)](../../../.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md#three-execution-boundaries-are-deliberately-one-way)拥有。
|
||||
|
||||
`PromptDecision.allow.messages` 是提示词拦截所准入的完整、带标识且冻结的批次。包装下游 allow 的监听器会保留该批次,除非有意替换它。block 必须指定 `discardClaimed`;该字段仅影响本次提交的批次,未被此次接纳认领的消息会继续保持待处理。
|
||||
`PreStepDecision` 要么是 `{ kind: 'reject' }`,要么是 `{ kind: 'enter', messages }`。enter 分支是拟进入步骤的完整、带标识且冻结的批次。包装下游 enter 的监听器会保留该批次,除非有意替换它;新增消息遵循 waterfall 的自然返回顺序。领取操作已经把候选消息从 inbox 删除,因此 reject 不会保留它们;领取后插入的消息仍等待后续边界。
|
||||
|
||||
inbox 的实时通知刻意采用逐消息的最小载荷:`agent/inbox/inserted { message }`、`agent/inbox/claimed { message, turn }` 与 `agent/inbox/discarded { message }`。它们补充持久 `agent/inbox/spliced` 投影,但不引入另一层生命周期封套。
|
||||
|
||||
轮次和步骤边界以及模型 token 流是持久 `session/event` 事实,而不是镜像的 `agent/*` 通知。消费方从会话事件流读取 `turn/*`、`step/*` 和 `assistant/chunk`;工具策略与结果观测属于 [`dsh-tools`](../tools/README.md) 记录的完整流水线。
|
||||
|
||||
@@ -60,10 +62,10 @@ Agent *创建* 由实现 `AgentFactory` 的插件(`dsh-agent-loop`)提供,
|
||||
|
||||
每个插件面向的 handle:
|
||||
|
||||
- `agent.inbox`:agent 所拥有的持久 `agent/inbox/spliced` 事件投影。`nextTurn` 与 `nextStep` 暴露待处理的 `UserMessage` 值;`splice(target, start, deleteCount, inserted, outcome?)` 使用标准 splice 坐标插入、编辑、移除、准入或取消消息。`MessageId` 是唯一的入队项标识,在消息待处理期间必须保持唯一。
|
||||
- `agent.followup(message)`:将一条普通 `next-turn` 消息排队并唤醒驱动器。它不返回完成 handle;消息 id 标识 inbox 和准入事实,而不标识之后的输出或 `turn/end`。
|
||||
- `agent.inbox`:agent 所拥有的持久 `agent/inbox/spliced` 事件投影。`nextTurn` 与 `nextStep` 暴露待处理的 `UserMessage` 值。`append`、`prepend`、`update`、`remove` 与 `splice` 用于变更队列;普通删除是持久取消,并发出 `agent/inbox/discarded`。`claim(target)` 通过纯删除 splice 原子移除下一个候选批次,随后由循环发出 `agent/inbox/claimed`。`MessageId` 是唯一的入队项标识,在消息待处理期间必须保持唯一。
|
||||
- `agent.followup(message)`:将一条普通 `next-turn` 消息排队并唤醒驱动器。它不返回完成 handle;消息 id 标识 inbox 的插入、领取与丢弃事实,而不标识之后的输出或 `turn/end`。
|
||||
- `agent.steer(message)`:将会唤醒的 `next-step` 输入排队。空闲驱动器会调度一个轮次;collecting 和 running 驱动器会在各自的下一步骤边界消费该输入。
|
||||
- `agent.inject(message)`:将不会唤醒的 `next-step` 上下文排队。在准入期间或轮次打开时,它会等待下一个安全日志位置;否则立即追加,且不打开轮次。
|
||||
- `agent.inject(message)`:将不会唤醒的 `next-step` 上下文排队。collecting 或 running 驱动器会在最近的后续 pre-step 边界领取它;idle 驱动器则会让它保持待处理,直至 `followup()` 或 `steer()` 唤醒驱动器。若某次请求的 pre-step 已经领取完批次,它可能赶不上该请求。
|
||||
- `agent.cancel(cause, options?)`:取消活跃驱动器,并在未设置 `options.keepInbox` 时持久取消全部待处理 inbox 工作。空闲取消是空操作。
|
||||
- `agent.whenIdle()`:观察整个 agent 达到完全停稳,包括当前驱动器退役前调度的替代工作。它不结算任何特定消息。
|
||||
- `agent.session`、`agent.status`、`agent.options`、`agent.id`、`agent.ctx`
|
||||
@@ -82,7 +84,7 @@ Agent *创建* 由实现 `AgentFactory` 的插件(`dsh-agent-loop`)提供,
|
||||
|
||||
#### 模型看到的内容
|
||||
|
||||
`send`、`steer` 与 `inject` 会向所属会话提供输入。`agent/prompt-submit`、`agent/step` 和其他已声明事件让插件能够阻止提示词或添加持久请求材料;此接口本身不贡献固定文案。
|
||||
`send`、`steer` 与 `inject` 会向所属会话提供输入。`agent/pre-step` 和其他已声明事件让插件能够拒绝拟进入的步骤或添加持久请求材料;此接口本身不贡献固定文案。
|
||||
|
||||
#### Token 影响
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* @module @deepseek-ai/dsh-agent/inbox
|
||||
*/
|
||||
|
||||
import type { MessageId } from '@deepseek-ai/dsh-llm'
|
||||
import type { Session, SessionEventMap, UserMessage } from '@deepseek-ai/dsh-session'
|
||||
|
||||
/** One of the two ordered pending-message lists owned by an agent. */
|
||||
@@ -12,11 +13,22 @@ export type InboxTarget = 'next-turn' | 'next-step'
|
||||
/** Mutable state privately owned by an {@link Inbox}. */
|
||||
type InboxState = Record<InboxTarget, UserMessage[]>
|
||||
|
||||
/** Live notifications committed by inbox mutations. */
|
||||
export interface InboxNotifications {
|
||||
/** Publish one inserted message. */
|
||||
inserted(message: UserMessage): void
|
||||
/** Publish one discarded message. */
|
||||
discarded(message: UserMessage): void
|
||||
}
|
||||
|
||||
/** A replay-once projection that incrementally consumes later inbox splices. */
|
||||
export class Inbox {
|
||||
private readonly state: InboxState = { 'next-turn': [], 'next-step': [] }
|
||||
|
||||
constructor(private readonly session: Session) {
|
||||
constructor(
|
||||
private readonly session: Session,
|
||||
private readonly notifications: InboxNotifications,
|
||||
) {
|
||||
for (const event of session.events.slice(session.header.seedLength ?? 0)) {
|
||||
if (event.type !== 'agent/inbox/spliced') continue
|
||||
try {
|
||||
@@ -32,7 +44,7 @@ export class Inbox {
|
||||
return this.state['next-turn']
|
||||
}
|
||||
|
||||
/** Input awaiting admission at a step boundary. */
|
||||
/** Input awaiting the next step boundary. */
|
||||
get nextStep(): readonly UserMessage[] {
|
||||
return this.state['next-step']
|
||||
}
|
||||
@@ -42,6 +54,68 @@ export class Inbox {
|
||||
return this.nextTurn.length > 0 || this.nextStep.length > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove and return the complete batch proposed for one step. The durable
|
||||
* splices are pure deletions; the caller publishes claimed notifications.
|
||||
* @param target - whether this boundary also consumes one queued turn.
|
||||
* @returns next-step input followed by the queued turn, when requested.
|
||||
*/
|
||||
claim(target: InboxTarget): UserMessage[] {
|
||||
const claimed = this.mutate('next-step', 0, this.nextStep.length, [], false)
|
||||
if (target === 'next-turn') {
|
||||
claimed.push(...this.mutate('next-turn', 0, 1, [], false))
|
||||
}
|
||||
return claimed
|
||||
}
|
||||
|
||||
/**
|
||||
* Append one message to a pending list and durably record the insertion.
|
||||
* @param target - pending list to extend.
|
||||
* @param message - message to append.
|
||||
* @throws if the message identity is already pending.
|
||||
*/
|
||||
append(target: InboxTarget, message: UserMessage): void {
|
||||
this.splice(target, this.state[target].length, 0, [message])
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepend one message to a pending list and durably record the insertion.
|
||||
* @param target - pending list to extend.
|
||||
* @param message - message to prepend.
|
||||
* @throws if the message identity is already pending.
|
||||
*/
|
||||
prepend(target: InboxTarget, message: UserMessage): void {
|
||||
this.splice(target, 0, 0, [message])
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace one pending message in place and durably record the mutation.
|
||||
* @param target - pending list containing the message.
|
||||
* @param messageId - identity of the message to replace.
|
||||
* @param newMessage - replacement message.
|
||||
* @returns whether the message was still pending.
|
||||
* @throws if the replacement duplicates another pending message identity.
|
||||
*/
|
||||
update(target: InboxTarget, messageId: MessageId, newMessage: UserMessage): boolean {
|
||||
const index = this.state[target].findIndex(message => message.id === messageId)
|
||||
if (index < 0) return false
|
||||
this.splice(target, index, 1, [newMessage])
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove one pending message and durably record its cancellation.
|
||||
* @param target - pending list containing the message.
|
||||
* @param messageId - identity of the message to remove.
|
||||
* @returns whether the message was still pending.
|
||||
*/
|
||||
remove(target: InboxTarget, messageId: MessageId): boolean {
|
||||
const index = this.state[target].findIndex(message => message.id === messageId)
|
||||
if (index < 0) return false
|
||||
this.splice(target, index, 1, [])
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply standard splice semantics and durably record the normalized result.
|
||||
* The durable event commits before the live projection mutates, so synchronous
|
||||
@@ -51,7 +125,6 @@ export class Inbox {
|
||||
* @param start - splice position.
|
||||
* @param deleteCount - maximum number of messages to remove.
|
||||
* @param inserted - messages to insert at the resolved position.
|
||||
* @param outcome - terminal disposition of removed messages.
|
||||
* @returns messages removed by the splice.
|
||||
*/
|
||||
splice(
|
||||
@@ -59,7 +132,17 @@ export class Inbox {
|
||||
start: number,
|
||||
deleteCount: number,
|
||||
inserted: UserMessage[],
|
||||
outcome?: 'admitted' | 'canceled',
|
||||
): UserMessage[] {
|
||||
return this.mutate(target, start, deleteCount, inserted, true)
|
||||
}
|
||||
|
||||
/** Commit one normalized mutation and publish its live notifications. */
|
||||
private mutate(
|
||||
target: InboxTarget,
|
||||
start: number,
|
||||
deleteCount: number,
|
||||
inserted: UserMessage[],
|
||||
discardRemoved: boolean,
|
||||
): UserMessage[] {
|
||||
const inbox = this.state[target]
|
||||
const truncatedStart = Math.trunc(start)
|
||||
@@ -73,17 +156,22 @@ export class Inbox {
|
||||
inbox.length - actualStart,
|
||||
)
|
||||
if (actualDeleteCount === 0 && inserted.length === 0) return []
|
||||
const resolvedOutcome = outcome ?? (actualDeleteCount > 0 ? 'canceled' : undefined)
|
||||
const outcome = discardRemoved && actualDeleteCount > 0 ? 'canceled' : undefined
|
||||
const splice = {
|
||||
target,
|
||||
start: actualStart,
|
||||
...(actualDeleteCount === 0 ? {} : { removedCount: actualDeleteCount }),
|
||||
inserted,
|
||||
...(resolvedOutcome === undefined ? {} : { outcome: resolvedOutcome }),
|
||||
...(outcome === undefined ? {} : { outcome }),
|
||||
}
|
||||
this.validate(splice)
|
||||
const event = this.session.append('agent/inbox/spliced', splice)
|
||||
return inbox.splice(actualStart, actualDeleteCount, ...event.data.inserted)
|
||||
const removed = inbox.splice(actualStart, actualDeleteCount, ...event.data.inserted)
|
||||
if (discardRemoved) {
|
||||
for (const message of removed) this.notifications.discarded(message)
|
||||
}
|
||||
for (const message of event.data.inserted) this.notifications.inserted(message)
|
||||
return removed
|
||||
}
|
||||
|
||||
/** Apply one normalized durable splice to the projection. */
|
||||
|
||||
@@ -42,21 +42,26 @@ export interface CancelOptions {
|
||||
/**
|
||||
* An agent's lifecycle state, emitted on every transition as `agent/status`:
|
||||
* `idle` means no driver is scheduled or active; `running` begins when a
|
||||
* cancellable admission is scheduled and lasts while the driver drains,
|
||||
* cancellable pre-step processing is scheduled and lasts while the driver drains,
|
||||
* closes, or checkpoints turns. Disposal removes the agent from its registry;
|
||||
* it is not a third observable status.
|
||||
*/
|
||||
export type AgentStatus = 'idle' | 'running'
|
||||
|
||||
/**
|
||||
* Prompt interception result. An allowed batch replaces the submitted
|
||||
* messages; a listener wrapping `next()` preserves that batch unless it
|
||||
* intentionally replaces it. A blocked batch explicitly chooses whether to
|
||||
* discard the claimed messages; unclaimed work remains pending.
|
||||
*/
|
||||
export type PromptDecision =
|
||||
| { kind: 'allow'; messages: UserMessage[] }
|
||||
| { kind: 'block'; reason: string; discardClaimed: boolean }
|
||||
/** Coordinates and cancellation for a proposed step. */
|
||||
export interface PreStepContext {
|
||||
/** Turn that will own the step. */
|
||||
readonly turn: number
|
||||
/** Step proposed by the loop. */
|
||||
readonly step: number
|
||||
/** Current turn cancellation signal. */
|
||||
readonly signal: AbortSignal
|
||||
}
|
||||
|
||||
/** Whether and with which messages the loop enters a proposed step. */
|
||||
export type PreStepDecision =
|
||||
| { kind: 'reject' }
|
||||
| { kind: 'enter'; messages: UserMessage[] }
|
||||
|
||||
/** One failed model-request attempt presented to recovery listeners. */
|
||||
export interface RequestFailureContext {
|
||||
@@ -135,11 +140,11 @@ export interface Agent {
|
||||
steer(message: UserMessage): void
|
||||
|
||||
/**
|
||||
* Append model-facing context without running the model. Admission or an
|
||||
* open turn stages it at the next safe log position; outside that window it
|
||||
* appends immediately without opening a turn. If admission closes without a
|
||||
* turn, a context-only boundary appends immediately; context staged beside
|
||||
* steering remains pending with it.
|
||||
* Queue model-facing context for the next pre-step without waking the
|
||||
* driver. Collecting and running drivers claim it at the nearest later
|
||||
* step boundary; idle drivers leave it pending until follow-up or steering
|
||||
* wakes them. It may miss a request whose pre-step already claimed its
|
||||
* batch. Cancellation or disposal may discard pending context.
|
||||
* @param message - identified injected context and its producer provenance.
|
||||
*/
|
||||
inject(message: UserMessage): void
|
||||
@@ -178,6 +183,30 @@ declare module 'cordis' {
|
||||
* @mode emit
|
||||
*/
|
||||
'agent/status'(this: Scoped<Agent>, agent: Agent, status: AgentStatus): void
|
||||
/**
|
||||
* One message entered the live inbox.
|
||||
* @param agent - the agent whose inbox changed.
|
||||
* @param event - the inserted message.
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
* @mode emit
|
||||
*/
|
||||
'agent/inbox/inserted'(this: Scoped<Agent>, agent: Agent, event: { message: UserMessage }): void
|
||||
/**
|
||||
* One message left the inbox for a turn.
|
||||
* @param agent - the agent whose inbox changed.
|
||||
* @param event - the claimed message and owning turn.
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
* @mode emit
|
||||
*/
|
||||
'agent/inbox/claimed'(this: Scoped<Agent>, agent: Agent, event: { message: UserMessage; turn: number }): void
|
||||
/**
|
||||
* One message was discarded from the live inbox.
|
||||
* @param agent - the agent whose inbox changed.
|
||||
* @param event - the discarded message.
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
* @mode emit
|
||||
*/
|
||||
'agent/inbox/discarded'(this: Scoped<Agent>, agent: Agent, event: { message: UserMessage }): void
|
||||
// ---- session lifecycle (emit) ----
|
||||
/**
|
||||
* The session lifecycle began, once before the first turn. Use
|
||||
@@ -193,30 +222,15 @@ declare module 'cordis' {
|
||||
|
||||
// ---- the machine's extension seams ----
|
||||
/**
|
||||
* Allow, rewrite, or block one claimed inbox batch before it becomes
|
||||
* model-visible or opens a turn. Call `next()` for the unchanged default. The
|
||||
* signal controls only this admission attempt; listeners may cooperate with
|
||||
* it but must not retain it for a later attempt or turn.
|
||||
* @param agent - the agent whose driver claimed the batch.
|
||||
* @param messages - the claimed messages.
|
||||
* @param signal - the current turn's explicit abort signal.
|
||||
* Reject a proposed step or replace the messages that enter it. Calling
|
||||
* `next()` preserves the current messages.
|
||||
* @param agent - the agent proposing the step.
|
||||
* @param messages - messages removed from the inbox for this step.
|
||||
* @param context - proposed turn and step coordinates plus cancellation.
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
* @mode waterfall
|
||||
*/
|
||||
'agent/prompt-submit'(this: Scoped<Agent>, agent: Agent, messages: UserMessage[], signal: AbortSignal, next: () => Promise<PromptDecision>): Promise<PromptDecision>
|
||||
/**
|
||||
* Awaited serial checkpoint before EVERY request of a turn is built (the
|
||||
* first as well as each post-tools continuation). The single "between
|
||||
* steps" extension point: inject context, steer, or edit the session log
|
||||
* here — the request's history derives from the log right after this settles.
|
||||
* @param agent - the agent about to send a request.
|
||||
* @param turn - the open turn number.
|
||||
* @param step - the step number about to open.
|
||||
* @param signal - the turn abort signal.
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
* @mode serial
|
||||
*/
|
||||
'agent/step'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, signal: AbortSignal): Promise<void> | void
|
||||
'agent/pre-step'(this: Scoped<Agent>, agent: Agent, messages: UserMessage[], context: PreStepContext, next: () => Promise<PreStepDecision>): Promise<PreStepDecision>
|
||||
/**
|
||||
* Replace the frozen call configuration. `await next()` yields the config
|
||||
* the machine would use (agent options on the first request, the logged
|
||||
@@ -284,7 +298,7 @@ declare module '@deepseek-ai/dsh-session' {
|
||||
start: number
|
||||
removedCount?: number
|
||||
inserted: UserMessage[]
|
||||
outcome?: 'admitted' | 'canceled'
|
||||
outcome?: 'canceled'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ function stubAgent(rawId: string, overrides: Partial<Agent> = {}): Agent {
|
||||
id,
|
||||
options: {},
|
||||
session,
|
||||
inbox: new Inbox(session),
|
||||
inbox: new Inbox(session, { inserted: () => {}, discarded: () => {} }),
|
||||
status: 'idle',
|
||||
ctx: new Context(),
|
||||
send: () => {},
|
||||
|
||||
Reference in New Issue
Block a user