From 7ce330f0e4d0af6c5c37279c1e54e7e285177423 Mon Sep 17 00:00:00 2001 From: kingwl Date: Thu, 30 Jul 2026 01:58:51 +0800 Subject: [PATCH] refactor(agent): scope queue actions to edit and remove --- ...send-and-coalesced-user-messages.i18n.yaml | 4 +- ...nified-send-and-coalesced-user-messages.md | 2 +- ...ied-send-and-coalesced-user-messages.zh.md | 2 +- ...-29-addressable-queue-operations.i18n.yaml | 4 +- ...2026-07-29-addressable-queue-operations.md | 20 +++---- ...6-07-29-addressable-queue-operations.zh.md | 20 +++---- docs/architecture.i18n.yaml | 4 +- docs/architecture.md | 2 +- docs/architecture.zh.md | 2 +- docs/cordis-catalog/events.md | 42 +++++++------- docs/core-data-structures/core.i18n.yaml | 4 +- docs/core-data-structures/core.md | 13 ++--- docs/core-data-structures/core.zh.md | 13 ++--- docs/event-producer-consumer.md | 32 +++++------ packages/client/runtime/README.i18n.yaml | 4 +- packages/client/runtime/README.md | 2 +- packages/client/runtime/README.zh.md | 2 +- .../runtime/src/client/contract/session.ts | 2 +- .../src/client/sessions/conversation.ts | 1 - .../runtime/src/client/sessions/session.ts | 1 - .../client/runtime/tests/queue-store.spec.ts | 16 ++---- .../client/ui-conversation/README.i18n.yaml | 4 +- packages/client/ui-conversation/README.md | 4 +- packages/client/ui-conversation/README.zh.md | 4 +- .../src/client/input/contract.ts | 1 - .../src/client/queue/QueueDock.tsx | 7 +-- .../ui-conversation/src/client/service.ts | 2 +- .../ui-conversation/tests/queue-dock.spec.tsx | 15 +---- .../cordis/tool-cordis/src/api-catalog.ts | 8 +-- packages/core/agent-loop/README.i18n.yaml | 4 +- packages/core/agent-loop/README.md | 2 +- packages/core/agent-loop/README.zh.md | 2 +- packages/core/agent-loop/src/agent.ts | 48 +++------------- .../tests/contract-regressions.spec.ts | 57 ++++++------------- packages/core/agent/README.i18n.yaml | 4 +- packages/core/agent/README.md | 2 +- packages/core/agent/README.zh.md | 2 +- packages/core/agent/src/types.ts | 26 +++------ packages/core/scope/tests/invariant.spec.ts | 2 +- packages/host/apiproxy/README.i18n.yaml | 4 +- packages/host/apiproxy/README.md | 2 +- packages/host/apiproxy/README.zh.md | 2 +- packages/host/apiproxy/src/api-proxy.ts | 21 +++---- .../host/apiproxy/src/api/events.schema.ts | 1 - packages/host/apiproxy/src/api/events.ts | 11 ++-- .../host/apiproxy/src/api/sessions.schema.ts | 1 - packages/host/apiproxy/src/api/sessions.ts | 5 +- .../apiproxy/tests/api-proxy-commands.spec.ts | 33 +++++------ .../host/apiproxy/tests/rpc-schemas.spec.ts | 9 ++- 49 files changed, 181 insertions(+), 294 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.i18n.yaml index 54bfa56d7a..dfae9677c4 100644 --- a/.agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.i18n.yaml @@ -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 .agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.md -2026-07-22-unified-send-and-coalesced-user-messages.md: 3b2187f9e9ae24f3a03c1418daf1c0aec255b314 -2026-07-22-unified-send-and-coalesced-user-messages.zh.md: 02a21193ce63926b6f04a02cb7ea8a64fe6603cb +2026-07-22-unified-send-and-coalesced-user-messages.md: 4d0cbeff0c8a07362caa1ec18493267a9f0d2823 +2026-07-22-unified-send-and-coalesced-user-messages.zh.md: 935a1a78a6bed451c1db646dec2ec5f4f5e87949 diff --git a/.agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.md b/.agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.md index 3b2187f9e9..4d0cbeff0c 100644 --- a/.agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.md +++ b/.agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.md @@ -22,7 +22,7 @@ Separately, `context/message` and `user/message` had converged: the surface proj **`send` does not return identity.** Callers already own the complete message and its opaque `MessageId`; creation and freezing are owned by the [identified immutable message decision](2026-07-28-identified-immutable-message-values.md), not by routing. -**Inbox lifecycle events carry occurrence identities.** `agent/inbox/enqueue` (an item entered a FIFO), `agent/inbox/update` (a pending item was edited or promoted), `agent/inbox/dequeue` (the driver claimed one), and `agent/inbox/discard` (pending items were dropped) carry an `InboxItem`: an occurrence-local `InboxItemId`, the accepted `UserMessage`, and the resolved `queued | steering` placement captured at acceptance. The occurrence identity lets observers and reconnect mirrors distinguish repeated sends of the same `MessageId` without reconstructing routing from later status or session history. Injection never touches a FIFO and emits none of these. Every FIFO entry publishes one enqueue and exactly one terminal dequeue or discard; updates are non-terminal. The `dsh-agent` invariant companion asserts this FIFO conservation. +**Inbox lifecycle events carry occurrence identities.** `agent/inbox/enqueue` (an item entered a FIFO), `agent/inbox/update` (a pending queued item was edited), `agent/inbox/dequeue` (the driver claimed one), and `agent/inbox/discard` (pending items were dropped) carry an `InboxItem`: an occurrence-local `InboxItemId`, the accepted `UserMessage`, and the resolved `queued | steering` placement captured at acceptance. The occurrence identity lets observers and reconnect mirrors distinguish repeated sends of the same `MessageId` without reconstructing routing from later status or session history. Injection never touches a FIFO and emits none of these. Every FIFO entry publishes one enqueue and exactly one terminal dequeue or discard; updates are non-terminal. The `dsh-agent` invariant companion asserts this FIFO conservation. **Admission accepts next-step input without becoming a turn.** The loop opens a private next-step acceptance window before `agent/prompt-submit`, keeps it open through the turn, and closes it before `turn/end`. Steering and injection received during admission therefore remain together in the outbox and join an allowed turn. If admission blocks or fails, a context-only caller batch takes idle injection's immediate append, while steering and context staged beside it remain available to retry; neither path writes the rejected prompt. When a later prompt is admitted, retained outbox input enters its turn before that prompt, while input accepted during the current admission remains after the prompt. Closing the window before `turn/end` preserves the rule that reentrant late steering becomes an independent queued turn. `Agent.acceptsNextStep` exposes whether a `next-step` send would currently join this window; `status` remains the broader activity signal rather than a routing predicate. diff --git a/.agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.zh.md b/.agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.zh.md index 02a21193ce..935a1a78a6 100644 --- a/.agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.zh.md @@ -22,7 +22,7 @@ agent 的对外驱动接口逐渐长出三个近乎平行的动词——`send` **`send` 不返回标识。** 调用方已经持有完整消息及其不透明的 `MessageId`;消息的创建与冻结由[带标识的不可变消息值决策](2026-07-28-identified-immutable-message-values.md)负责,而不是由路由负责。 -**Inbox 生命周期事件携带单次入队标识。** `agent/inbox/enqueue`(一个队列项进入某个 FIFO)、`agent/inbox/update`(待处理项被编辑或前移)、`agent/inbox/dequeue`(驱动器认领一个项)和 `agent/inbox/discard`(待处理项被丢弃)都会携带一个 `InboxItem`:仅属于本次入队的 `InboxItemId`、已接受的 `UserMessage`,以及生产方在接受消息时捕获的已解析 `queued | steering` 放置方式。单次入队标识让观察方和重连镜像能够区分同一 `MessageId` 的多次发送,无需根据后续状态或会话历史重建路由。注入从不触及 FIFO,也不发出这些事件中的任何一个。每次 FIFO 入队都会发布一个 enqueue,并且恰好发布一个终态 dequeue 或 discard;update 不是终态。`dsh-agent` 的不变量配套断言这种 FIFO 守恒。 +**Inbox 生命周期事件携带单次入队标识。** `agent/inbox/enqueue`(一个队列项进入某个 FIFO)、`agent/inbox/update`(待处理的 queued 项被编辑)、`agent/inbox/dequeue`(驱动器认领一个项)和 `agent/inbox/discard`(待处理项被丢弃)都会携带一个 `InboxItem`:仅属于本次入队的 `InboxItemId`、已接受的 `UserMessage`,以及生产方在接受消息时捕获的已解析 `queued | steering` 放置方式。单次入队标识让观察方和重连镜像能够区分同一 `MessageId` 的多次发送,无需根据后续状态或会话历史重建路由。注入从不触及 FIFO,也不发出这些事件中的任何一个。每次 FIFO 入队都会发布一个 enqueue,并且恰好发布一个终态 dequeue 或 discard;update 不是终态。`dsh-agent` 的不变量配套断言这种 FIFO 守恒。 **准入接受 next-step 输入,但不会因此成为一个轮次。** 循环会在 `agent/prompt-submit` 前打开一个私有的 next-step 接受窗口,使其贯穿整个轮次,并在 `turn/end` 前关闭。因此,在准入期间收到的 steering 和注入会一起留在 outbox 中并加入获准轮次。如果准入被阻止或失败,仅含调用方上下文的批次会采用空闲注入的立即追加行为,而 steering 及与其一同暂存的上下文仍可重试;两种路径都不会写入被拒绝的提示词。后续提示词获准时,保留在 outbox 中的输入会先于该提示词进入其轮次,而当前准入期间接受的输入则留在提示词之后。在 `turn/end` 前关闭窗口,可以保留这样的规则:可重入的晚到 steering 会成为一个独立的排队轮次。`Agent.acceptsNextStep` 会公开一次 `next-step` 发送当前是否会加入该窗口;`status` 仍是更宽泛的活动信号,而非路由判据。 diff --git a/.agents/notes/implemented/feature/2026-07-29-addressable-queue-operations.i18n.yaml b/.agents/notes/implemented/feature/2026-07-29-addressable-queue-operations.i18n.yaml index fe91b8ccda..dd2f02db0f 100644 --- a/.agents/notes/implemented/feature/2026-07-29-addressable-queue-operations.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-29-addressable-queue-operations.i18n.yaml @@ -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 .agents/notes/implemented/feature/2026-07-29-addressable-queue-operations.md -2026-07-29-addressable-queue-operations.md: 7462b882dde0c3b25ddfb321ab339b6cd51bd170 -2026-07-29-addressable-queue-operations.zh.md: ac442421a1e21b2e09bb003ca9be1a0412374d7f +2026-07-29-addressable-queue-operations.md: 93b11dc590728a3c236971fbadf9c7c0187d943c +2026-07-29-addressable-queue-operations.zh.md: 3a8e46946ad8cbe122bc3d89c6b470654ea97baf diff --git a/.agents/notes/implemented/feature/2026-07-29-addressable-queue-operations.md b/.agents/notes/implemented/feature/2026-07-29-addressable-queue-operations.md index 7462b882dd..93b11dc590 100644 --- a/.agents/notes/implemented/feature/2026-07-29-addressable-queue-operations.md +++ b/.agents/notes/implemented/feature/2026-07-29-addressable-queue-operations.md @@ -1,4 +1,4 @@ -# Agent Note: Address pending queue occurrences for edit, remove, and promotion +# Agent Note: Address pending queue occurrences for edit and removal Status: implemented @@ -6,19 +6,17 @@ English | [中文](2026-07-29-addressable-queue-operations.zh.md) ## Problem -The Web queue rendered pending messages but could not act on one row. `MessageId` was insufficient as an address because callers may enqueue the same immutable message more than once. The browser also inferred queue retirement from turn and status events, so a row operation racing with driver claim had no authoritative outcome. - -“Send now” introduced a separate semantic choice: it could mean reorder the next independent turn, interrupt the current turn as steering, or cancel current work. Only the first interpretation preserves the queue row’s original delivery contract. +The Web queue rendered pending messages but could not edit or delete one row. `MessageId` was insufficient as an address because callers may enqueue the same immutable message more than once. The browser also inferred queue retirement from turn and status events, so a row operation racing with driver claim had no authoritative outcome. ## Decision **Each accepted FIFO occurrence has its own identity.** AgentLoop mints an opaque `InboxItemId` and publishes an `InboxItem` containing that id, the identified `UserMessage`, and its acceptance-time `queued | steering` placement. Reusing one `MessageId` creates distinct inbox identities. Injection bypasses the FIFOs and receives no inbox identity. -**Mutation ends at driver claim.** `Agent.updateInbox(id, action)` synchronously searches the pending queued and steering FIFOs. Edit replaces frozen content while preserving `InboxItemId`, `MessageId`, source, placement, wake policy, and position. Remove emits the occurrence’s terminal discard. Promote moves it to the front of its current FIFO; an ordinary queued item also becomes waking. The driver removes an occurrence before prompt admission or steering drain, so a later mutation returns `not-found` and never rewrites durable history. +**Mutation ends at driver claim.** `Agent.updateInbox(id, action)` synchronously searches the pending queued FIFO. Edit replaces frozen content while preserving `InboxItemId`, `MessageId`, source, wake policy, and position. Remove emits the occurrence’s terminal discard. Steering and driver-claimed occurrences return `not-found`, so queue operations never rewrite active-turn input or durable history. -**The live ledger is authoritative.** `agent/inbox/enqueue`, `update`, `dequeue`, and `discard` maintain a Host mirror. The wire sends complete `session/queue` snapshots rather than incremental guesses. Reconnect sends the current baseline, and every live mutation or terminal event replaces it. The client applies no optimistic edit and never retires a row from `turn/start`, `steering/message`, or status changes. +**The live ledger is authoritative.** `agent/inbox/enqueue`, `update`, `dequeue`, and `discard` maintain a Host mirror of queued occurrences. The wire sends complete `session/queue` snapshots rather than incremental guesses. Reconnect sends the current baseline, and every queued mutation or terminal event replaces it. The client applies no optimistic edit and never retires a row from durable turn events or status changes. -**Web actions preserve delivery kind.** QueueDock projects only `queued` occurrences; pending `steering` occurrences remain in the authoritative snapshot but wait for a dedicated Web interaction. It exposes edit and delete, but no send-now control. Edit is available only when all content blocks are text; the editor cannot silently drop non-text blocks. An editing row exposes only save and cancel, with Enter and Escape as their keyboard equivalents. Delete removes the exact occurrence. Protocol-level promotion remains available without being presented as a Web interaction; it never converts queued work into steering or cancels active work. +**Web actions address Queue only.** The Host excludes pending steering from `session/queue`; steering retains its existing durable transcript path after consumption. QueueDock exposes edit and delete, but no send-now control. Edit is available only when all content blocks are text; the editor cannot silently drop non-text blocks. An editing row exposes only save and cancel, with Enter and Escape as their keyboard equivalents. Delete removes the exact occurrence. ## Alternatives considered @@ -26,16 +24,16 @@ The Web queue rendered pending messages but could not act on one row. `MessageId **Apply optimistic browser mutations.** Rejected because driver claim and another client can win before the Host action. Waiting for the authoritative snapshot makes the ownership boundary visible and lets `queue-item-not-found` report a real race. -**Treat send-now as steering.** Rejected because it would change a queued independent turn into current-turn context, bypass ordinary prompt admission, and alter the one-send-one-turn guarantee. Promotion changes priority, not delivery semantics. +**Include pending steering in the queue mutation protocol.** Rejected because QueueDock has no steering interaction, and editing or deleting active-turn input would widen this feature beyond its current consumer. A dedicated steering interaction owns that delivery contract. -**Cancel the active turn before promotion.** Rejected because a row-local action must not destroy unrelated in-flight work. +**Expose a protocol-only promotion operation.** Rejected because no product interaction reorders Queue. A public operation without a current consumer would add ordering semantics and tests for speculative use. ## Verification -AgentLoop contract tests hold prompt admission while editing, removing, and promoting exact occurrences, then verify the resulting independent-turn order and terminal lifecycle events. Host schema and proxy tests cover authoritative snapshots, reconnect, typed not-found errors, and the RPC transport. Client runtime and QueueDock tests cover non-optimistic projection, queued-only Web projection, text-only editing, save and cancel affordances, removal, retirement races, disabled mixed-content editing, and the absent send-now control. Keyless browser scenarios drive the exposed edit and delete actions and keep accepted pending steering hidden until it becomes a durable transcript event through the built Web composition and real HTTP/SSE wire. +AgentLoop contract tests hold prompt admission while editing and removing exact queued occurrences, reject mutations of steering occurrences, and verify the resulting independent turn and terminal lifecycle events. Host schema and proxy tests cover queued-only authoritative snapshots, reconnect, typed not-found errors, and the RPC transport. Client runtime and QueueDock tests cover non-optimistic projection, text-only editing, save and cancel affordances, removal, retirement races, and disabled mixed-content editing. Keyless browser scenarios drive the exposed edit and delete actions through the built Web composition and real HTTP/SSE wire. ## Consequences -Pending work gains precise row operations without becoming durable session history. Occurrence identity is a live process-local capability and disappears at claim, cancellation, disposal, or restart; reconnect recovers only items still held by the live Agent. Send-now is intentionally weaker than interruption, and editing intentionally excludes mixed content until an editor can preserve every block. +Queued work gains precise row operations without becoming durable session history. Occurrence identity is a live process-local capability and disappears at claim, cancellation, disposal, or restart; reconnect recovers only queued items still held by the live Agent. Editing excludes mixed content until an editor can preserve every block, while pending steering remains outside this operation surface. The protocol now carries full queue snapshots on each change. Queues are expected to remain short, so deterministic recovery and multi-client convergence are preferred over an incremental mutation protocol. diff --git a/.agents/notes/implemented/feature/2026-07-29-addressable-queue-operations.zh.md b/.agents/notes/implemented/feature/2026-07-29-addressable-queue-operations.zh.md index ac442421a1..3a8e46946a 100644 --- a/.agents/notes/implemented/feature/2026-07-29-addressable-queue-operations.zh.md +++ b/.agents/notes/implemented/feature/2026-07-29-addressable-queue-operations.zh.md @@ -1,4 +1,4 @@ -# Agent Note(agent 决策记录):为待处理队列项提供编辑、移除与前移操作 +# Agent Note(agent 决策记录):为待处理队列项提供编辑与移除操作 Status: implemented @@ -6,19 +6,17 @@ Status: implemented ## 问题 -Web 队列能够渲染待处理消息,但无法操作其中某一行。`MessageId` 不足以充当寻址标识,因为调用方可以多次将同一条不可变消息加入队列。浏览器还会根据轮次和状态事件推断队列项已退役,因此当行操作与驱动器认领发生竞态时,系统无法给出权威结果。 - -“立即发送”还引入了另一项语义选择:它可以表示重新排序下一个独立轮次、以 steering(中途引导)方式打断当前轮次,或取消当前工作。只有第一种解释能够保留该队列行原有的投递契约。 +Web 队列能够渲染待处理消息,但无法编辑或删除其中某一行。`MessageId` 不足以充当寻址标识,因为调用方可以多次将同一条不可变消息加入队列。浏览器还会根据轮次和状态事件推断队列项已退役,因此当行操作与驱动器认领发生竞态时,系统无法给出权威结果。 ## 决策 **每次获准进入 FIFO 的项都有独立标识。** AgentLoop 会铸造不透明的 `InboxItemId`,并发布一个 `InboxItem`,其中包含该 id、已有标识的 `UserMessage`,以及接受时确定的 `queued | steering` 放置方式。复用同一个 `MessageId` 会创建不同的 inbox 标识。注入绕过 FIFO,因此不会获得 inbox 标识。 -**变更边界止于驱动器认领。** `Agent.updateInbox(id, action)` 会同步搜索仍处于待处理状态的 queued 和 steering FIFO。编辑会替换已冻结的内容,同时保留 `InboxItemId`、`MessageId`、来源、放置方式、唤醒策略和位置。移除会发出该次入队项的终态 discard。前移会把它移至当前 FIFO 的队首;普通 queued 项还会变为可唤醒。驱动器会在提示词接纳或排空 steering 之前移除该项,因此之后的变更会返回 `not-found`,绝不会改写持久历史。 +**变更边界止于驱动器认领。** `Agent.updateInbox(id, action)` 会同步搜索待处理的 queued FIFO。编辑会替换已冻结的内容,同时保留 `InboxItemId`、`MessageId`、来源、唤醒策略和位置。移除会发出该次入队项的终态 discard。steering(中途引导)项和已被驱动器认领的项会返回 `not-found`,因此队列操作绝不会改写活动轮次输入或持久历史。 -**实时账本是权威状态。** `agent/inbox/enqueue`、`update`、`dequeue` 和 `discard` 共同维护 Host 镜像。协议发送完整的 `session/queue` 快照,而非增量猜测。重连会发送当前基线,每次实时变更或终态事件都会整体替换它。客户端不会进行乐观编辑,也绝不根据 `turn/start`、`steering/message` 或状态变化退役队列行。 +**实时账本是权威状态。** `agent/inbox/enqueue`、`update`、`dequeue` 和 `discard` 共同维护 queued 入队项的 Host 镜像。协议发送完整的 `session/queue` 快照,而非增量猜测。重连会发送当前基线,每次 queued 变更或终态事件都会整体替换它。客户端不会进行乐观编辑,也绝不根据持久轮次事件或状态变化退役队列行。 -**Web 操作保持投递类型。** QueueDock 只投影 `queued` 入队项;待处理的 `steering` 入队项仍保留在权威快照中,等待 Web 提供专用交互。它只暴露编辑和删除,不提供立即发送控件。仅当所有内容块都是文本时才提供编辑功能;编辑器不能静默丢弃非文本块。编辑中的行只展示保存和取消操作,对应的键盘操作分别是 Enter 和 Escape。删除会移除对应的精确入队项。协议层仍保留前移操作,但不会把它呈现为 Web 交互;该操作绝不会把 queued 工作转换为 steering,也不会取消活动工作。 +**Web 操作只面向 Queue。** Host 从 `session/queue` 中排除待处理 steering;steering 消费后仍沿用既有的持久 transcript(文本记录)路径。QueueDock 暴露编辑和删除,不提供立即发送控件。仅当所有内容块都是文本时才提供编辑功能;编辑器不能静默丢弃非文本块。编辑中的行只展示保存和取消操作,对应的键盘操作分别是 Enter 和 Escape。删除会移除对应的精确入队项。 ## 考虑过的替代方案 @@ -26,16 +24,16 @@ Web 队列能够渲染待处理消息,但无法操作其中某一行。`Messag **在浏览器中进行乐观变更。** 不予采纳,因为驱动器认领或另一个客户端可能先于 Host 操作完成。等待权威快照可以显式呈现所有权边界,并让 `queue-item-not-found` 报告真实竞态。 -**把立即发送视为 steering。** 不予采纳,因为这会把一个独立的排队轮次变成当前轮次的上下文,绕过普通提示词接纳,并改变单次 send 单轮次保证。前移只改变优先级,不改变投递语义。 +**将待处理 steering 纳入队列变更协议。** 不予采纳,因为 QueueDock 没有 steering 交互,而编辑或删除活动轮次输入会把此功能扩展到当前消费方之外。应由专用 steering 交互负责该投递契约。 -**前移前取消活动轮次。** 不予采纳,因为仅影响某一行的操作不应破坏无关的进行中工作。 +**暴露仅协议层的前移操作。** 不予采纳,因为当前没有产品交互会重新排序 Queue。公开一个没有当前消费方的操作,会为了推测性用途引入排序语义和测试。 ## 验证 -AgentLoop 契约测试会在编辑、移除和前移对应的精确入队项时阻塞提示词接纳,随后验证所得独立轮次顺序及终态生命周期事件。Host schema 与代理测试覆盖权威快照、重连、类型化 not-found 错误和 RPC 传输。客户端运行时和 QueueDock 测试覆盖非乐观投影、Web 仅投影 queued 项、仅文本编辑、保存与取消入口、移除、退役竞态、禁用混合内容编辑,以及不提供立即发送控件。无密钥浏览器场景会通过构建后的 Web 组合和真实 HTTP/SSE 协议操作公开的编辑和删除,并使已接受的待处理 steering 在成为持久 transcript(文本记录)事件之前保持隐藏。 +AgentLoop 契约测试会在编辑和移除精确 queued 入队项时阻塞提示词接纳,拒绝对 steering 入队项的变更,并验证所得独立轮次及终态生命周期事件。Host schema 与代理测试覆盖仅含 queued 项的权威快照、重连、类型化 not-found 错误和 RPC 传输。客户端运行时和 QueueDock 测试覆盖非乐观投影、仅文本编辑、保存与取消入口、移除、退役竞态,以及禁用混合内容编辑。无密钥浏览器场景会通过构建后的 Web 组合和真实 HTTP/SSE 协议操作公开的编辑和删除。 ## 后果 -待处理工作获得精确的行操作,但不会因此成为持久会话历史。单次入队标识是进程本地的实时寻址凭据,会在认领、取消、dispose 或重启时消失;重连只能恢复仍由活跃 Agent 持有的项。立即发送有意弱于打断,而编辑也有意排除混合内容,直至编辑器能够保留每个块。 +queued 工作获得精确的行操作,但不会因此成为持久会话历史。单次入队标识是进程本地的实时寻址凭据,会在认领、取消、dispose 或重启时消失;重连只能恢复仍由活跃 Agent 持有的 queued 项。编辑会排除混合内容,直至编辑器能够保留每个块;待处理 steering 则不属于此操作接口。 现在,协议会在每次变更时携带完整队列快照。队列预期保持较短,因此系统优先选择确定性恢复和多客户端收敛,而非增量变更协议。 diff --git a/docs/architecture.i18n.yaml b/docs/architecture.i18n.yaml index 4e25927711..dfff50708f 100644 --- a/docs/architecture.i18n.yaml +++ b/docs/architecture.i18n.yaml @@ -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 docs/architecture.md -architecture.md: 9a33a85e806e04dec3bfb73de8e4781c90f37677 -architecture.zh.md: 6fb91d6511c1213fa44da8a3ca17b100e1e29b95 +architecture.md: 1fd9bd128d1bcc0dd91d46131981ea4fc331bd74 +architecture.zh.md: 8521f09c6e415f9f8d1c0a44f7534b59c876decc diff --git a/docs/architecture.md b/docs/architecture.md index 9a33a85e80..1fd9bd128d 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -79,7 +79,7 @@ choose declarative identity and fresh/resume path -> enable driving -> agent/session-start(source) -> start driver forever: wait for queued occurrence - claim (edit/remove/promote end) -> emit agent/status(running) if starting an interval + claim (edit/remove end) -> emit agent/status(running) if starting an interval open the next-step acceptance window -> agent/prompt-submit blocked or failed prompt -> close the window without opening a turn diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md index 6fb91d6511..8521f09c6e 100644 --- a/docs/architecture.zh.md +++ b/docs/architecture.zh.md @@ -79,7 +79,7 @@ choose declarative identity and fresh/resume path -> enable driving -> agent/session-start(source) -> start driver forever: wait for queued occurrence - claim (edit/remove/promote end) -> emit agent/status(running) if starting an interval + claim (edit/remove end) -> emit agent/status(running) if starting an interval open the next-step acceptance window -> agent/prompt-submit blocked or failed prompt -> close the window without opening a turn diff --git a/docs/cordis-catalog/events.md b/docs/cordis-catalog/events.md index e6a52ce5a4..dafa342d5a 100644 --- a/docs/cordis-catalog/events.md +++ b/docs/cordis-catalog/events.md @@ -32,7 +32,7 @@ Effective broad cancellation was requested, before queued/outbox work is cleared 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:319`](../../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:253`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:250`](../../packages/core/agent/src/types.ts) ### `agent/disposed` — emit @@ -74,7 +74,7 @@ An agent left the registry; AgentLoop emits this after driver quiescence and sco Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) -Source: [`packages/core/agent/src/types.ts:262`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:259`](../../packages/core/agent/src/types.ts) ### `agent/error` — emit @@ -96,7 +96,7 @@ A step or turn errored. The machine reports a failure here (plus the logger) eve Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) -Source: [`packages/core/agent/src/types.ts:443`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:433`](../../packages/core/agent/src/types.ts) ### `agent/inbox/dequeue` — emit @@ -117,7 +117,7 @@ The driver claimed one item out of the inbox: a queued item at a turn boundary, Types: [Agent](../core-data-structures/core.md) · [InboxItem](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) -Source: [`packages/core/agent/src/types.ts:307`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:297`](../../packages/core/agent/src/types.ts) ### `agent/inbox/discard` — emit @@ -140,7 +140,7 @@ Pending inbox items were dropped without delivering them, so every enqueue occur Types: [Agent](../core-data-structures/core.md) · [InboxItem](../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:309`](../../packages/core/agent/src/types.ts) ### `agent/inbox/enqueue` — emit @@ -161,29 +161,27 @@ An item entered the queued or steering inbox. `placement` is the acceptance-time Types: [Agent](../core-data-structures/core.md) · [InboxItem](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) -Source: [`packages/core/agent/src/types.ts:281`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:278`](../../packages/core/agent/src/types.ts) ### `agent/inbox/update` — emit -A still-pending inbox item changed content or position. The item id and placement remain stable; edit carries the replacement message, while promote makes this occurrence first in its current FIFO. +A still-pending queued item changed content. The item id, placement, and position remain stable while the event carries the replacement message. ```ts cordis-catalog /** - * A still-pending inbox item changed content or position. The item id and - * placement remain stable; edit carries the replacement message, while - * promote makes this occurrence first in its current FIFO. + * A still-pending queued item changed content. The item id, placement, and + * position remain stable while the event carries the replacement message. * @param agent - the owning agent. * @param item - the complete post-update occurrence. - * @param action - the applied non-terminal operation. * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. * @mode emit */ -'agent/inbox/update'( this: Scoped, agent: Agent, item: InboxItem, action: 'edit' | 'promote', ): void +'agent/inbox/update'(this: Scoped, agent: Agent, item: InboxItem): void ``` Types: [Agent](../core-data-structures/core.md) · [InboxItem](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) -Source: [`packages/core/agent/src/types.ts:292`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:287`](../../packages/core/agent/src/types.ts) ### `agent/prompt-submit` — waterfall @@ -206,7 +204,7 @@ Allow, rewrite, or block one claimed prompt before it becomes a user message or Types: [Agent](../core-data-structures/core.md) · [PromptDecision](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) · [UserMessage](../core-data-structures/session.md) -Source: [`packages/core/agent/src/types.ts:356`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:346`](../../packages/core/agent/src/types.ts) ### `agent/request` — waterfall @@ -230,7 +228,7 @@ Replace the frozen call configuration. `await next()` yields the config the mach 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:382`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:372`](../../packages/core/agent/src/types.ts) ### `agent/request-error` — waterfall @@ -260,7 +258,7 @@ Handle a model-request failure after its failed step has closed but before the f Types: [Agent](../core-data-structures/core.md) · [LlmFailure](../core-data-structures/llm-streaming.md) · [RequestError](../core-data-structures/core.md) · [RequestErrorAction](../core-data-structures/core.md) · [ResolvedRetryPolicy](../core-data-structures/llm-streaming.md) · [Scoped](../core-data-structures/scope.md) -Source: [`packages/core/agent/src/types.ts:401`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:391`](../../packages/core/agent/src/types.ts) ### `agent/session-start` — emit @@ -282,7 +280,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:332`](../../packages/core/agent/src/types.ts) ### `agent/settled` — emit @@ -307,7 +305,7 @@ One drain chain reached its terminal turn: that turn's `turn/end` is already com Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) · [SettleReason](../core-data-structures/core.md) -Source: [`packages/core/agent/src/types.ts:430`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:420`](../../packages/core/agent/src/types.ts) ### `agent/status` — emit @@ -327,7 +325,7 @@ Agent status changed (`idle` ⇄ `running`). `send()` does not enter `running` s 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:271`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:268`](../../packages/core/agent/src/types.ts) ### `agent/step` — serial @@ -351,7 +349,7 @@ Awaited serial checkpoint before EVERY request of a turn is built (the first as Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) -Source: [`packages/core/agent/src/types.ts:369`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:359`](../../packages/core/agent/src/types.ts) ### `agent/turn-stopping` — serial @@ -377,7 +375,7 @@ The turn is about to close: the model owes no response (no live tool calls, no f Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) -Source: [`packages/core/agent/src/types.ts:416`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:406`](../../packages/core/agent/src/types.ts) ## `agent-loop/*` diff --git a/docs/core-data-structures/core.i18n.yaml b/docs/core-data-structures/core.i18n.yaml index 93cb9ebf9a..6321c85127 100644 --- a/docs/core-data-structures/core.i18n.yaml +++ b/docs/core-data-structures/core.i18n.yaml @@ -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 docs/core-data-structures/core.md -core.md: a39560eecca4689186ce2d3fc183250f8698cb90 -core.zh.md: a5e3b80e11c97d64de1afd082ef02097eb61787c +core.md: dad533cee00646a40f57bd9097b2cceb8e9de9e2 +core.zh.md: 9e8afac0744fcf0df8c35dad5debce746d6614c6 diff --git a/docs/core-data-structures/core.md b/docs/core-data-structures/core.md index a39560eecc..dad533cee0 100644 --- a/docs/core-data-structures/core.md +++ b/docs/core-data-structures/core.md @@ -445,11 +445,10 @@ interface InboxItem { ``` ```ts type-equiv -/** A user-requested mutation of one still-pending inbox item. */ +/** A user-requested mutation of one still-pending queued occurrence. */ type InboxAction = | { readonly kind: 'edit'; readonly content: ContentBlock[] } | { readonly kind: 'remove' } - | { readonly kind: 'promote' } ``` ```ts type-equiv @@ -549,13 +548,11 @@ interface Agent { send(message: UserMessage, options: SendOptions): void /** - * Mutate one still-pending inbox occurrence synchronously. Editing preserves + * Mutate one still-pending queued occurrence synchronously. Editing preserves * the message identity and queue position; removal publishes its terminal - * discard; promotion moves it to the front of its current FIFO and makes a - * queued item waking. A driver-claimed item is no longer pending and returns - * `not-found`. - * @param id - independently addressable inbox occurrence. - * @param action - edit, remove, or promote operation. + * discard. Steering occurrences and driver-claimed items return `not-found`. + * @param id - independently addressable queued occurrence. + * @param action - edit or remove operation. * @returns whether the pending occurrence was found and updated. */ updateInbox(id: InboxItemId, action: InboxAction): InboxActionResult diff --git a/docs/core-data-structures/core.zh.md b/docs/core-data-structures/core.zh.md index a5e3b80e11..9e8afac074 100644 --- a/docs/core-data-structures/core.zh.md +++ b/docs/core-data-structures/core.zh.md @@ -453,11 +453,10 @@ interface InboxItem { ``` ```ts type-equiv -/** A user-requested mutation of one still-pending inbox item. */ +/** A user-requested mutation of one still-pending queued occurrence. */ type InboxAction = | { readonly kind: 'edit'; readonly content: ContentBlock[] } | { readonly kind: 'remove' } - | { readonly kind: 'promote' } ``` ```ts type-equiv @@ -557,13 +556,11 @@ interface Agent { send(message: UserMessage, options: SendOptions): void /** - * Mutate one still-pending inbox occurrence synchronously. Editing preserves + * Mutate one still-pending queued occurrence synchronously. Editing preserves * the message identity and queue position; removal publishes its terminal - * discard; promotion moves it to the front of its current FIFO and makes a - * queued item waking. A driver-claimed item is no longer pending and returns - * `not-found`. - * @param id - independently addressable inbox occurrence. - * @param action - edit, remove, or promote operation. + * discard. Steering occurrences and driver-claimed items return `not-found`. + * @param id - independently addressable queued occurrence. + * @param action - edit or remove operation. * @returns whether the pending occurrence was found and updated. */ updateInbox(id: InboxItemId, action: InboxAction): InboxActionResult diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index f4767e035d..b9538b89d5 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -8,22 +8,22 @@ 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:148`](../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) (`emitAgentEvent`) | [`goal-session`](../packages/goal/goal-session) | -| `agent/created` | `emit` | [`packages/core/agent/src/types.ts:253`](../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:262`](../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:443`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | `apiproxy`, [`goal-session`](../packages/goal/goal-session), [`session-telemetry`](../packages/telemetry/session-telemetry), [`tui`](../packages/ui/tui) | -| `agent/inbox/dequeue` | `emit` | [`packages/core/agent/src/types.ts:307`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`agent`](../packages/core/agent), `apiproxy`, [`tui`](../packages/ui/tui) | -| `agent/inbox/discard` | `emit` | [`packages/core/agent/src/types.ts:319`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`agent`](../packages/core/agent), `apiproxy`, [`tui`](../packages/ui/tui) | -| `agent/inbox/enqueue` | `emit` | [`packages/core/agent/src/types.ts:281`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`agent`](../packages/core/agent), `apiproxy`, [`goal-session`](../packages/goal/goal-session) | -| `agent/inbox/update` | `emit` | [`packages/core/agent/src/types.ts:292`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | `apiproxy` | -| `agent/prompt-submit` | `waterfall` | [`packages/core/agent/src/types.ts:356`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`goal-session`](../packages/goal/goal-session), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard), [`tui`](../packages/ui/tui) | -| `agent/request` | `waterfall` | [`packages/core/agent/src/types.ts:382`](../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:401`](../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) | -| `agent/session-start` | `emit` | [`packages/core/agent/src/types.ts:342`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`workspace-context`](../packages/context/workspace-context) | -| `agent/settled` | `emit` | [`packages/core/agent/src/types.ts:430`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`compact-basic`](../packages/compact/compact-basic) | -| `agent/status` | `emit` | [`packages/core/agent/src/types.ts:271`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`agent`](../packages/core/agent), `apiproxy`, [`goal-session`](../packages/goal/goal-session), [`tui`](../packages/ui/tui) | -| `agent/step` | `serial` | [`packages/core/agent/src/types.ts:369`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`compact-basic`](../packages/compact/compact-basic), [`plan-mode`](../packages/plan/plan-mode), [`session-checkpoint-policy`](../packages/session-persistence/session-checkpoint-policy), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill), [`user-approval`](../packages/ui/user-approval), [`workspace-context`](../packages/context/workspace-context) | -| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/types.ts:416`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | +| `agent/cancel-requested` | `emit` | [`packages/core/agent/src/types.ts:319`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`goal-session`](../packages/goal/goal-session) | +| `agent/created` | `emit` | [`packages/core/agent/src/types.ts:250`](../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:259`](../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:433`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | `apiproxy`, [`goal-session`](../packages/goal/goal-session), [`session-telemetry`](../packages/telemetry/session-telemetry), [`tui`](../packages/ui/tui) | +| `agent/inbox/dequeue` | `emit` | [`packages/core/agent/src/types.ts:297`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`agent`](../packages/core/agent), `apiproxy`, [`tui`](../packages/ui/tui) | +| `agent/inbox/discard` | `emit` | [`packages/core/agent/src/types.ts:309`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`agent`](../packages/core/agent), `apiproxy`, [`tui`](../packages/ui/tui) | +| `agent/inbox/enqueue` | `emit` | [`packages/core/agent/src/types.ts:278`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`agent`](../packages/core/agent), `apiproxy`, [`goal-session`](../packages/goal/goal-session) | +| `agent/inbox/update` | `emit` | [`packages/core/agent/src/types.ts:287`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | `apiproxy` | +| `agent/prompt-submit` | `waterfall` | [`packages/core/agent/src/types.ts:346`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`goal-session`](../packages/goal/goal-session), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard), [`tui`](../packages/ui/tui) | +| `agent/request` | `waterfall` | [`packages/core/agent/src/types.ts:372`](../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:391`](../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) | +| `agent/session-start` | `emit` | [`packages/core/agent/src/types.ts:332`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`workspace-context`](../packages/context/workspace-context) | +| `agent/settled` | `emit` | [`packages/core/agent/src/types.ts:420`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`compact-basic`](../packages/compact/compact-basic) | +| `agent/status` | `emit` | [`packages/core/agent/src/types.ts:268`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`agent`](../packages/core/agent), `apiproxy`, [`goal-session`](../packages/goal/goal-session), [`tui`](../packages/ui/tui) | +| `agent/step` | `serial` | [`packages/core/agent/src/types.ts:359`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`compact-basic`](../packages/compact/compact-basic), [`plan-mode`](../packages/plan/plan-mode), [`session-checkpoint-policy`](../packages/session-persistence/session-checkpoint-policy), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill), [`user-approval`](../packages/ui/user-approval), [`workspace-context`](../packages/context/workspace-context) | +| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/types.ts:406`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | | `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/acp/acp), `apiproxy` | | `commands/change` | `emit` | [`packages/ui/commands/src/index.ts:154`](../packages/ui/commands/src/index.ts) | [`commands`](../packages/ui/commands) (`events.dispatch`) | `apiproxy`, [`tui`](../packages/ui/tui) | | `domain/changed` | `emit` | [`packages/storage/storage-domain/src/events.ts:46`](../packages/storage/storage-domain/src/events.ts) | [`storage-domain`](../packages/storage/storage-domain) (`emit`) | `apiproxy`, [`storage-domain`](../packages/storage/storage-domain), [`workspace`](../packages/workspace/workspace) | diff --git a/packages/client/runtime/README.i18n.yaml b/packages/client/runtime/README.i18n.yaml index ae395367a8..fa4c87adb2 100644 --- a/packages/client/runtime/README.i18n.yaml +++ b/packages/client/runtime/README.i18n.yaml @@ -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/client/runtime/README.md -README.md: f0b287d31b3aa9dddd2fde8d3f95bddd290bcf8d -README.zh.md: b2e9c29f0f254fd514f201fcedf6a9cb5e05f86c +README.md: 766d8516225cd46cb1a3a80c832d1cf55e816140 +README.zh.md: 9b514afca91f604b3e895187de3b5532bf22a692 diff --git a/packages/client/runtime/README.md b/packages/client/runtime/README.md index f0b287d31b..766d851622 100644 --- a/packages/client/runtime/README.md +++ b/packages/client/runtime/README.md @@ -18,7 +18,7 @@ SlotsService gives the renderer separate bare observables for `useSessions` and ## Pending queue projection -`ConversationSnapshot.queue` is the Host's authoritative transient inbox snapshot. Each row carries its `InboxItemId`, complete editable text when every content block is text, a flattened preview, and the accepted queued-or-steering placement. `session/queue` replaces the whole projection; reconnect buffering retains only the latest snapshot, and neither durable turn events nor running-status changes guess that an item was claimed. `Session.updateQueue()` sends edit/remove/promote operations without optimistic mutation, so the next Host snapshot is the sole visible commit and a claim race can surface `queue-item-not-found`. +`ConversationSnapshot.queue` is the Host's authoritative transient Queue snapshot; pending steering stays outside this projection. Each row carries its `InboxItemId`, complete editable text when every content block is text, and a flattened preview. `session/queue` replaces the whole projection; reconnect buffering retains only the latest snapshot, and neither durable turn events nor running-status changes guess that an item was claimed. `Session.updateQueue()` sends edit/remove operations without optimistic mutation, so the next Host snapshot is the sole visible commit and a claim race can surface `queue-item-not-found`. ## Code Mode sub-dispatch index diff --git a/packages/client/runtime/README.zh.md b/packages/client/runtime/README.zh.md index b2e9c29f0f..9b514afca9 100644 --- a/packages/client/runtime/README.zh.md +++ b/packages/client/runtime/README.zh.md @@ -18,7 +18,7 @@ SlotsService 分别为 renderer 提供 `useSessions` 与 `useWorkspaces` 的裸 ## 待处理队列投影 -`ConversationSnapshot.queue` 是 Host 提供的权威瞬态 inbox 快照。每行都携带其 `InboxItemId`、所有内容块均为文本时的完整可编辑文本、扁平化预览,以及接受时确定的 queued 或 steering 放置方式。`session/queue` 会整体替换该投影;重连缓冲只保留最新快照,持久轮次事件和 running 状态变化都不会猜测某个项已被认领。`Session.updateQueue()` 发送编辑/移除/前移操作,不进行乐观更新,因此下一份 Host 快照是唯一可见的提交结果,认领竞态则会返回 `queue-item-not-found`。 +`ConversationSnapshot.queue` 是 Host 提供的权威瞬态 Queue 快照;待处理 steering(中途引导)不进入此投影。每行都携带其 `InboxItemId`、所有内容块均为文本时的完整可编辑文本,以及扁平化预览。`session/queue` 会整体替换该投影;重连缓冲只保留最新快照,持久轮次事件和 running 状态变化都不会猜测某个项已被认领。`Session.updateQueue()` 发送编辑/移除操作,不进行乐观更新,因此下一份 Host 快照是唯一可见的提交结果,认领竞态则会返回 `queue-item-not-found`。 ## Code Mode 子调用索引 diff --git a/packages/client/runtime/src/client/contract/session.ts b/packages/client/runtime/src/client/contract/session.ts index 4ceb2893bd..82bde108b4 100644 --- a/packages/client/runtime/src/client/contract/session.ts +++ b/packages/client/runtime/src/client/contract/session.ts @@ -41,7 +41,7 @@ export interface ISession { /** * Apply one mutation to a still-pending queue occurrence. * @param itemId - agent-owned inbox occurrence identity. - * @param action - edit, remove, or promote operation. + * @param action - edit or remove operation. * @returns acceptance, or a business/transport error. */ updateQueue(itemId: InboxItemId, action: QueueAction): Promise> diff --git a/packages/client/runtime/src/client/sessions/conversation.ts b/packages/client/runtime/src/client/sessions/conversation.ts index 0b3b7fd414..f68a271c63 100644 --- a/packages/client/runtime/src/client/sessions/conversation.ts +++ b/packages/client/runtime/src/client/sessions/conversation.ts @@ -222,7 +222,6 @@ export interface QueuedMessage { readonly preview: string /** Complete editable text; null when the message contains non-text blocks. */ readonly text: string | null - readonly placement: 'queued' | 'steering' } /** In-progress assistant output (chunk accumulator product). */ diff --git a/packages/client/runtime/src/client/sessions/session.ts b/packages/client/runtime/src/client/sessions/session.ts index 22abb6596a..6850e3e4c9 100644 --- a/packages/client/runtime/src/client/sessions/session.ts +++ b/packages/client/runtime/src/client/sessions/session.ts @@ -407,7 +407,6 @@ export class Session implements SessionFace { id: item.id, preview: queuePreviewOf(item.message.content), text: queueTextOf(item.message.content), - placement: item.placement, })) this.queueRev++ this.notifier.markDirty() diff --git a/packages/client/runtime/tests/queue-store.spec.ts b/packages/client/runtime/tests/queue-store.spec.ts index 02fab86728..192885b66b 100644 --- a/packages/client/runtime/tests/queue-store.spec.ts +++ b/packages/client/runtime/tests/queue-store.spec.ts @@ -21,7 +21,6 @@ const iid = (id: string): InboxItemId => id as InboxItemId interface QueueFixture { id: string body: string - placement?: 'queued' | 'steering' content?: ContentBlock[] } @@ -36,7 +35,6 @@ function queueFrame(items: QueueFixture[]): MuxFrame { content: item.content ?? text(item.body), source: { kind: 'user', rpcId: rid(`rpc-${item.id}`) } as never, }), - placement: item.placement ?? 'queued', })), } } @@ -46,15 +44,13 @@ function makeSession(): Session { } describe('queue snapshot intake', () => { - it('projects stable ids, flat previews, complete text, and placement', () => { + it('projects stable ids, flat previews, and complete text', () => { const session = makeSession() session.handleMuxEnvelope(rid('env-1'), queueFrame([ { id: 'q-1', body: '第一条 排队\n消息' }, - { id: 'q-2', body: '插话', placement: 'steering' }, ])) expect(session.getSnapshot().queue).toEqual([ - { id: 'q-1', preview: '第一条 排队 消息', text: '第一条 排队\n消息', placement: 'queued' }, - { id: 'q-2', preview: '插话', text: '插话', placement: 'steering' }, + { id: 'q-1', preview: '第一条 排队 消息', text: '第一条 排队\n消息' }, ]) }) @@ -66,7 +62,7 @@ describe('queue snapshot intake', () => { content: [{ type: 'text', text: 'hi' }, { type: 'image', data: 'x' } as never], }])) expect(session.getSnapshot().queue).toEqual([ - { id: 'q-image', preview: 'hi [image]', text: null, placement: 'queued' }, + { id: 'q-image', preview: 'hi [image]', text: null }, ]) }) @@ -90,7 +86,7 @@ describe('queue snapshot intake', () => { { id: 'q-2', body: 'two edited' }, ])) expect(session.getSnapshot().queue).toEqual([ - { id: 'q-2', preview: 'two edited', text: 'two edited', placement: 'queued' }, + { id: 'q-2', preview: 'two edited', text: 'two edited' }, ]) session.handleMuxEnvelope(rid('env-6'), queueFrame([])) expect(session.getSnapshot().queue).toEqual([]) @@ -112,12 +108,12 @@ describe('queue operation transport', () => { session.handleMuxEnvelope(rid('env-op'), queueFrame([{ id: 'q-op', body: 'pending' }])) const before = session.getSnapshot().queue - await expect(session.updateQueue(iid('q-op'), { kind: 'promote' })) + await expect(session.updateQueue(iid('q-op'), { kind: 'edit', content: text('next') })) .resolves.toEqual({ ok: true, value: { accepted: true } }) expect(api.callsOf('session.updateQueue')).toEqual([{ sessionId: SID, itemId: 'q-op', - action: { kind: 'promote' }, + action: { kind: 'edit', content: text('next') }, }]) expect(session.getSnapshot().queue).toBe(before) }) diff --git a/packages/client/ui-conversation/README.i18n.yaml b/packages/client/ui-conversation/README.i18n.yaml index 35084bce73..1a058830bf 100644 --- a/packages/client/ui-conversation/README.i18n.yaml +++ b/packages/client/ui-conversation/README.i18n.yaml @@ -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/client/ui-conversation/README.md -README.md: 5bf3077c748c6e9be2c0a54474dce06c42eb0b2d -README.zh.md: b4e1c9a23f1786c3dcba2063a9657a29445ed47f +README.md: a017de28e612474dd4ad94b75385e6320e682955 +README.zh.md: b352f6f745a1458d9f0555a5d7d269fe67b8e307 diff --git a/packages/client/ui-conversation/README.md b/packages/client/ui-conversation/README.md index 5bf3077c74..a017de28e6 100644 --- a/packages/client/ui-conversation/README.md +++ b/packages/client/ui-conversation/README.md @@ -40,5 +40,5 @@ None; this package neither assembles nor sends a provider request. - **The sparkle icon for the others tool row is a hand-drawn approximation** — the design glyph's vector geometry is not exportable locally; promotion into ui-primitives waits on an exact export. - **The approval panel's "Always allow this type" is deferred** — durable grants need a grant-storage design; only allow-once/reject answer today. - **TodoPanel truncates long item text to one ellipsized line** — the figma strip has no wrap or expand affordance; full text is not readable inline. -- **Queue edit is text-only** — rows containing non-text blocks still show a flattened preview, but their edit control is disabled because the inline editor cannot preserve those blocks. A text row's edit mode replaces delete with save and cancel; Enter saves and Escape cancels. QueueDock exposes no send-now control; protocol-level promotion remains separate from the Web interaction. -- **Web exposes pending Queue only** — QueueDock omits pending steering until steering has its own interaction. A consumed `steering/message` still renders in the durable transcript so external steering remains truthful on replay. +- **Queue edit is text-only** — rows containing non-text blocks still show a flattened preview, but their edit control is disabled because the inline editor cannot preserve those blocks. A text row's edit mode replaces delete with save and cancel; Enter saves and Escape cancels. QueueDock exposes no send-now control. +- **Web exposes pending Queue only** — the Host omits pending steering from the Queue snapshot until steering has its own interaction. A consumed `steering/message` still renders in the durable transcript so external steering remains truthful on replay. diff --git a/packages/client/ui-conversation/README.zh.md b/packages/client/ui-conversation/README.zh.md index b4e1c9a23f..b352f6f745 100644 --- a/packages/client/ui-conversation/README.zh.md +++ b/packages/client/ui-conversation/README.zh.md @@ -40,5 +40,5 @@ todo 两个面就是在该形状上的两个注册项,都是普通注册方插 - **others 工具行的闪光图标是手绘近似版本**:无法在本地导出设计字形的矢量几何;等到存在精确导出后再将其提升到 ui-primitives。 - **审批面板的「始终允许此类」暂缓**:持久授权需要授权存储设计;今天只能回答允许一次/拒绝。 - **TodoPanel 将过长条目截成单行省略号**:figma 条没有换行或展开入口,完整文本无法在行内读完。 -- **Queue 编辑仅支持文本**:包含非文本块的行仍显示扁平化预览,但由于内联编辑器无法保留这些块,其编辑控件会被禁用。文本行进入编辑模式后,删除会替换为保存和取消;Enter 保存,Escape 取消。QueueDock 不提供立即发送控件;协议层的前移操作与 Web 交互保持分离。 -- **Web 仅暴露待处理 Queue**:在 steering(中途引导)拥有专用交互之前,QueueDock 不展示待处理的 steering。已消费的 `steering/message` 仍会渲染到持久 transcript(文本记录)中,因此从外部提交的 steering 在回放时仍能如实呈现。 +- **Queue 编辑仅支持文本**:包含非文本块的行仍显示扁平化预览,但由于内联编辑器无法保留这些块,其编辑控件会被禁用。文本行进入编辑模式后,删除会替换为保存和取消;Enter 保存,Escape 取消。QueueDock 不提供立即发送控件。 +- **Web 仅暴露待处理 Queue**:在 steering(中途引导)拥有专用交互之前,Host 不会把待处理 steering 纳入 Queue 快照。已消费的 `steering/message` 仍会渲染到持久 transcript(文本记录)中,因此从外部提交的 steering 在回放时仍能如实呈现。 diff --git a/packages/client/ui-conversation/src/client/input/contract.ts b/packages/client/ui-conversation/src/client/input/contract.ts index f88ea80633..18ef92664a 100644 --- a/packages/client/ui-conversation/src/client/input/contract.ts +++ b/packages/client/ui-conversation/src/client/input/contract.ts @@ -105,7 +105,6 @@ export interface QueuedMessage { readonly id: InboxItemId readonly preview: string readonly text: string | null - readonly placement: 'queued' | 'steering' } /** Guard union of the scoped consume-token event, checked by the machine. */ diff --git a/packages/client/ui-conversation/src/client/queue/QueueDock.tsx b/packages/client/ui-conversation/src/client/queue/QueueDock.tsx index 858133a0d6..f069e6ea91 100644 --- a/packages/client/ui-conversation/src/client/queue/QueueDock.tsx +++ b/packages/client/ui-conversation/src/client/queue/QueueDock.tsx @@ -4,7 +4,7 @@ // The 'conversation.input.dock' SlotMap declaration lives in // ../contract/slots.ts beside the other input-region slots. import type { Context } from 'cordis' -import { useEffect, useMemo, useState } from 'react' +import { useEffect, useState } from 'react' import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client' import type { InboxItemId, QueueAction } from '@deepseek-ai/dsh-client-connection/client' @@ -24,10 +24,7 @@ export type QueueDockProps = PropsRuntime<'conversation.input.dock'> & QueueDock /** Queue strip: one preview line per queued message; renders null when the queue is empty. */ export function QueueDock({ useSession, updateQueue, notify }: QueueDockProps) { - const inbox = useSession(s => s.queue) - // TODO(web-steer-ui): Give pending steering its own interaction before - // exposing it; QueueDock owns only independent queued turns. - const queue = useMemo(() => inbox.filter(row => row.placement === 'queued'), [inbox]) + const queue = useSession(s => s.queue) const [editing, setEditing] = useState<{ id: InboxItemId; text: string } | null>(null) const [busy, setBusy] = useState(null) diff --git a/packages/client/ui-conversation/src/client/service.ts b/packages/client/ui-conversation/src/client/service.ts index 31da0b167b..8d624de47e 100644 --- a/packages/client/ui-conversation/src/client/service.ts +++ b/packages/client/ui-conversation/src/client/service.ts @@ -34,7 +34,7 @@ export interface IConversation { /** * Apply one operation to a pending queue occurrence. * @param itemId - agent-owned inbox occurrence identity. - * @param action - edit, remove, or promote operation. + * @param action - edit or remove operation. * @returns completion; business failures reject. */ updateQueue(itemId: InboxItemId, action: QueueAction): Promise diff --git a/packages/client/ui-conversation/tests/queue-dock.spec.tsx b/packages/client/ui-conversation/tests/queue-dock.spec.tsx index 9e83d13ccc..57ddc384f6 100644 --- a/packages/client/ui-conversation/tests/queue-dock.spec.tsx +++ b/packages/client/ui-conversation/tests/queue-dock.spec.tsx @@ -20,7 +20,7 @@ const SID = 's1' as SessionId const iid = (id: string): InboxItemId => id as InboxItemId function row(id: string, text: string | null, preview = text ?? '[image]'): QueuedMessage { - return { id: iid(id), preview, text, placement: 'queued' } + return { id: iid(id), preview, text } } function snapshotWith(queue: QueuedMessage[]): ConversationSnapshot { @@ -78,19 +78,6 @@ describe('QueueDock', () => { expect(container.innerHTML).toBe('') }) - it('hides pending steering until it has a dedicated Web interaction', () => { - const steering = { ...row('i-steer', 'steer separately'), placement: 'steering' as const } - const snap = snapshotWith([steering]) - const source = liveSession(snap) - const { container } = render() - expect(container.innerHTML).toBe('') - - act(() => { source.push(snapshotWith([steering, row('i-queue', 'queue visibly')])) }) - expect(container.textContent).toContain('queue visibly') - expect(container.textContent).not.toContain('steer separately') - expect(container.querySelectorAll('button')).toHaveLength(2) - }) - it('renders active actions and disables editing for mixed-content rows', () => { const snap = snapshotWith([ row('i-1', '第一条排队消息'), diff --git a/packages/cordis/tool-cordis/src/api-catalog.ts b/packages/cordis/tool-cordis/src/api-catalog.ts index 0083e7af2c..60f7d330d8 100644 --- a/packages/cordis/tool-cordis/src/api-catalog.ts +++ b/packages/cordis/tool-cordis/src/api-catalog.ts @@ -1171,9 +1171,9 @@ export const EVENT_API: readonly EventApiEntry[] = [ { name: 'agent/inbox/update', mode: 'emit', - signature: '\'agent/inbox/update\'( this: Scoped, agent: Agent, item: InboxItem, action: \'edit\' | \'promote\', ): void', - jsDoc: '/**\n * A still-pending inbox item changed content or position. The item id and\n * placement remain stable; edit carries the replacement message, while\n * promote makes this occurrence first in its current FIFO.\n * @param agent - the owning agent.\n * @param item - the complete post-update occurrence.\n * @param action - the applied non-terminal operation.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode emit\n */', - summary: 'A still-pending inbox item changed content or position.', + signature: '\'agent/inbox/update\'(this: Scoped, agent: Agent, item: InboxItem): void', + jsDoc: '/**\n * A still-pending queued item changed content. The item id, placement, and\n * position remain stable while the event carries the replacement message.\n * @param agent - the owning agent.\n * @param item - the complete post-update occurrence.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode emit\n */', + summary: 'A still-pending queued item changed content.', }, { name: 'agent/prompt-submit', @@ -1857,7 +1857,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'InboxAction', - declaration: 'export type InboxAction = {\n readonly kind: \'edit\';\n readonly content: ContentBlock[];\n} | {\n readonly kind: \'remove\';\n} | {\n readonly kind: \'promote\';\n};', + declaration: 'export type InboxAction = {\n readonly kind: \'edit\';\n readonly content: ContentBlock[];\n} | {\n readonly kind: \'remove\';\n};', }, { name: 'InboxActionResult', diff --git a/packages/core/agent-loop/README.i18n.yaml b/packages/core/agent-loop/README.i18n.yaml index c3517d9868..3a82c693ca 100644 --- a/packages/core/agent-loop/README.i18n.yaml +++ b/packages/core/agent-loop/README.i18n.yaml @@ -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-loop/README.md -README.md: 86a5c0525cd0ca40cbeb7e4b1acbcefc417e811d -README.zh.md: b128876afb0931a758b472cbac0aa2423b4f4ab0 +README.md: a1617a1ef871f61157e0d70a06d055168170dced +README.zh.md: 6ba945a41e700331929dabb557802c14256921fb diff --git a/packages/core/agent-loop/README.md b/packages/core/agent-loop/README.md index 86a5c0525c..a1617a1ef8 100644 --- a/packages/core/agent-loop/README.md +++ b/packages/core/agent-loop/README.md @@ -57,7 +57,7 @@ The concrete `ReactLoopAgent`, its queued input, outbox, and run controls are pa The unified `send()` primitive routes content and source by (`target` × `wakeup`); `followup`/`steer`/`inject` are its fixed-preset aliases. A `next-turn` item joins the queued FIFO, waking the driver unless `wakeup: false`; admission happens before any turn opens. The loop opens a private next-step acceptance window before `agent/prompt-submit` and closes it before `turn/end`. During that window, `steer()` and `inject()` stage in one outbox; an allowed admission opens the turn, records the prompt and returned `additionalContexts`, then drains the staged input before the first request. A blocked or failed admission writes no prompt or hook-produced context. A caller-staged context-only batch then takes idle injection's immediate append, while steering and context staged beside it remain pending for retry or a later admitted prompt. Outside the window, steering becomes a waking queued prompt and injection immediately appends `user/message` without opening a turn or running the model. -Every FIFO acceptance mints an `InboxItemId` and publishes `agent/inbox/enqueue` with the complete occurrence. `updateInbox()` owns the synchronous pending-item boundary: edit freezes replacement content without changing message identity or position, remove publishes discard, and promote moves the occurrence to the head of its queued or steering FIFO; promoting queued work also makes it waking. Edit and promote publish `agent/inbox/update`. Claim publishes `agent/inbox/dequeue` and irrevocably removes the live address before prompt admission, so a racing update returns `not-found`; `cancel()` without `keepInbox` publishes `agent/inbox/discard`. +Every FIFO acceptance mints an `InboxItemId` and publishes `agent/inbox/enqueue` with the complete occurrence. `updateInbox()` owns the synchronous queued-item boundary: edit freezes replacement content without changing message identity or position, while remove publishes discard. Edit publishes `agent/inbox/update`; steering and claimed occurrences return `not-found`. Claim publishes `agent/inbox/dequeue` and irrevocably removes the live address before prompt admission, so a racing update cannot rewrite durable history; `cancel()` without `keepInbox` publishes `agent/inbox/discard`. ### Loop lifecycle (`agent.ts`) diff --git a/packages/core/agent-loop/README.zh.md b/packages/core/agent-loop/README.zh.md index b128876afb..6ba945a41e 100644 --- a/packages/core/agent-loop/README.zh.md +++ b/packages/core/agent-loop/README.zh.md @@ -57,7 +57,7 @@ interface Config { 统一的 `send()` 原语按(`target` × `wakeup`)路由内容与来源;`followup`/`steer`/`inject` 是它的固定预设别名。`next-turn` 项加入排队 FIFO,除非 `wakeup: false`,否则会唤醒驱动器;接纳发生在任何轮次开启之前。循环在 `agent/prompt-submit` 之前打开一个私有的 next-step 接收窗口,并在 `turn/end` 之前关闭它。在该窗口内,`steer()` 与 `inject()` 会暂存到同一个 outbox;接纳获准后会开启轮次,记录提示词及其返回的 `additionalContexts`,再于首次请求前排空暂存输入。接纳被阻止或失败时,不会写入提示词或钩子生成的上下文。之后,仅含调用方暂存上下文的批次会采用空闲注入的立即追加行为,而 steering(中途引导)及与其一同暂存的上下文则继续待处理,以供重试或之后获准的提示词使用。窗口之外,steering 会成为唤醒驱动器的排队提示词,而注入会立即追加 `user/message`,不开启轮次也不运行模型。 -每次 FIFO 接受项时都会铸造一个 `InboxItemId`,并通过 `agent/inbox/enqueue` 发布完整的单次入队项。`updateInbox()` 持有同步待处理项边界:编辑会冻结替换内容,但不改变消息标识或位置;移除会发布 discard;前移会把该项移至其 queued 或 steering FIFO 的队首,其中 queued 工作还会变为可唤醒。编辑和前移会发布 `agent/inbox/update`。认领操作会发布 `agent/inbox/dequeue`,并在提示词接纳前不可逆地移除实时寻址标识,因此竞态中的更新会返回 `not-found`;`cancel()` 在不带 `keepInbox` 时会发布 `agent/inbox/discard`。 +每次 FIFO 接受项时都会铸造一个 `InboxItemId`,并通过 `agent/inbox/enqueue` 发布完整的单次入队项。`updateInbox()` 持有同步 queued 项边界:编辑会冻结替换内容,但不改变消息标识或位置;移除会发布 discard。编辑会发布 `agent/inbox/update`;steering 项和已被认领的项会返回 `not-found`。认领操作会发布 `agent/inbox/dequeue`,并在提示词接纳前不可逆地移除实时寻址标识,因此竞态中的更新无法改写持久历史;`cancel()` 在不带 `keepInbox` 时会发布 `agent/inbox/discard`。 ### 循环生命周期(`agent.ts`) diff --git a/packages/core/agent-loop/src/agent.ts b/packages/core/agent-loop/src/agent.ts index 50c5618d6b..22d3f9e05e 100644 --- a/packages/core/agent-loop/src/agent.ts +++ b/packages/core/agent-loop/src/agent.ts @@ -137,19 +137,14 @@ export class ReactLoopAgent implements Agent { emitAgentEvent(this.loopCtx, this, 'agent/inbox/enqueue', item) } - /** Apply one synchronous mutation to a still-pending inbox occurrence. */ + /** Apply one synchronous mutation to a still-pending queued occurrence. */ updateInbox(id: InboxItemIdType, action: InboxAction): InboxActionResult { const queuedIndex = this.queued.findIndex(candidate => candidate.item.id === id) - const outboxIndex = queuedIndex === -1 - ? this.outbox.findIndex(candidate => candidate.item?.id === id) - : -1 - if (queuedIndex === -1 && outboxIndex === -1) return 'not-found' + if (queuedIndex === -1) return 'not-found' - const pending = queuedIndex === -1 ? this.outbox[outboxIndex] : this.queued[queuedIndex] - /* v8 ignore next 2 -- indices are derived from these arrays in this synchronous method. */ - if (pending === undefined || pending.item === undefined) { - throw new Error(`agent "${this.id}" inbox index changed during synchronous update`) - } + const pending = this.queued[queuedIndex] + /* v8 ignore next -- the index was resolved from this array without an async boundary. */ + if (pending === undefined) throw new Error(`agent "${this.id}" queued item disappeared during update`) /* v8 ignore next -- InboxAction is a closed discriminated union; all variants are covered below. */ switch (action.kind) { @@ -158,42 +153,15 @@ export class ReactLoopAgent implements Agent { ...pending.item, message: freezeMessage({ ...pending.item.message, content: action.content }), }) - if (queuedIndex !== -1) { - const queued = this.queued[queuedIndex] - /* v8 ignore next -- the index was resolved from this array without an async boundary. */ - if (queued === undefined) throw new Error(`agent "${this.id}" queued item disappeared during edit`) - this.queued[queuedIndex] = { ...queued, item } - } else { - const outbox = this.outbox[outboxIndex] - /* v8 ignore next -- the index was resolved from this array without an async boundary. */ - if (outbox === undefined) throw new Error(`agent "${this.id}" steering item disappeared during edit`) - this.outbox[outboxIndex] = { ...outbox, message: item.message, item } - } - emitAgentEvent(this.loopCtx, this, 'agent/inbox/update', item, 'edit') + this.queued[queuedIndex] = { ...pending, item } + emitAgentEvent(this.loopCtx, this, 'agent/inbox/update', item) return 'applied' } case 'remove': { - if (queuedIndex !== -1) this.queued.splice(queuedIndex, 1) - else this.outbox.splice(outboxIndex, 1) + this.queued.splice(queuedIndex, 1) emitAgentEvent(this.loopCtx, this, 'agent/inbox/discard', [pending.item]) return 'applied' } - case 'promote': { - if (queuedIndex !== -1) { - const queued = this.queued.splice(queuedIndex, 1)[0] - /* v8 ignore next -- the index was resolved from this array without an async boundary. */ - if (queued === undefined) throw new Error(`agent "${this.id}" queued item disappeared during promotion`) - this.queued.unshift({ item: queued.item, wakeup: true }) - this.scheduleKick() - } else { - const outbox = this.outbox.splice(outboxIndex, 1)[0] - /* v8 ignore next -- the index was resolved from this array without an async boundary. */ - if (outbox === undefined) throw new Error(`agent "${this.id}" steering item disappeared during promotion`) - this.outbox.unshift(outbox) - } - emitAgentEvent(this.loopCtx, this, 'agent/inbox/update', pending.item, 'promote') - return 'applied' - } default: /* v8 ignore next -- InboxAction is a closed discriminated union. */ return assertNever(action) diff --git a/packages/core/agent-loop/tests/contract-regressions.spec.ts b/packages/core/agent-loop/tests/contract-regressions.spec.ts index 0332980bce..31e6d7c2b1 100644 --- a/packages/core/agent-loop/tests/contract-regressions.spec.ts +++ b/packages/core/agent-loop/tests/contract-regressions.spec.ts @@ -60,10 +60,9 @@ function inboxText(item: InboxItem): string { } describe('addressable inbox operations', () => { - it('edits in place, removes exactly one item, and promotes the next independent turn', async () => { + it('edits in place and removes exactly one queued item', async () => { const adapter = new MockAdapter([ textResponse('first reply'), - textResponse('promoted reply'), textResponse('edited reply'), ]) const ctx = await harness(adapter) @@ -79,13 +78,13 @@ describe('addressable inbox operations', () => { }) const pending: InboxItem[] = [] - const updates: { id: string; action: string; text: string }[] = [] + const updates: { id: string; text: string }[] = [] const discards: string[][] = [] ctx.on('agent/inbox/enqueue', (subject, item) => { if (subject === agent && inboxText(item) !== 'first') pending.push(item) }) - ctx.on('agent/inbox/update', (subject, item, action) => { - if (subject === agent) updates.push({ id: item.id, action, text: inboxText(item) }) + ctx.on('agent/inbox/update', (subject, item) => { + if (subject === agent) updates.push({ id: item.id, text: inboxText(item) }) }) ctx.on('agent/inbox/discard', (subject, items) => { if (subject === agent) discards.push(items.map(item => item.id)) @@ -95,22 +94,16 @@ describe('addressable inbox operations', () => { await admission.promise send(agent, 'remove me') send(agent, 'edit me') - send(agent, 'promote me') - expect(pending.map(inboxText)).toEqual(['remove me', 'edit me', 'promote me']) + expect(pending.map(inboxText)).toEqual(['remove me', 'edit me']) const remove = pending[0]! const edit = pending[1]! - const promote = pending[2]! expect(agent.updateInbox(edit.id, { kind: 'edit', content: [{ type: 'text', text: 'edited' }], })).toBe('applied') expect(agent.updateInbox(remove.id, { kind: 'remove' })).toBe('applied') - expect(agent.updateInbox(promote.id, { kind: 'promote' })).toBe('applied') - expect(updates).toEqual([ - { id: edit.id, action: 'edit', text: 'edited' }, - { id: promote.id, action: 'promote', text: 'promote me' }, - ]) + expect(updates).toEqual([{ id: edit.id, text: 'edited' }]) expect(discards).toEqual([[remove.id]]) const idle = waitForIdle(ctx, agent) @@ -121,11 +114,11 @@ describe('addressable inbox operations', () => { .map(event => event.type === 'user/message' ? event.data.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('') : '')) - .toEqual(['first', 'promote me', 'edited']) - expect(agent.updateInbox(promote.id, { kind: 'remove' })).toBe('not-found') + .toEqual(['first', 'edited']) + expect(agent.updateInbox(edit.id, { kind: 'remove' })).toBe('not-found') }) - it('edits, removes, and promotes steering occurrences before admission commits', async () => { + it('does not mutate steering occurrences', async () => { const adapter = new MockAdapter([textResponse('done')]) const ctx = await harness(adapter) const agent = ctx.agentLoop.create(SessionId('steering-inbox-actions'), { provider: 'mock', model: 'mock' }) @@ -137,40 +130,22 @@ describe('addressable inbox operations', () => { }) const pending: InboxItem[] = [] - const updates: { id: string; action: string; text: string }[] = [] - const discards: string[][] = [] ctx.on('agent/inbox/enqueue', (subject, item) => { if (subject === agent && item.placement === 'steering') pending.push(item) }) - ctx.on('agent/inbox/update', (subject, item, action) => { - if (subject === agent) updates.push({ id: item.id, action, text: inboxText(item) }) - }) - ctx.on('agent/inbox/discard', (subject, items) => { - if (subject === agent) discards.push(items.map(item => item.id)) - }) const idle = waitForIdle(ctx, agent) send(agent, 'admitted prompt') await entered.promise - agent.steer(createUserMessage({ content: [{ type: 'text', text: 'remove me' }], source: { kind: 'user' } })) - agent.steer(createUserMessage({ content: [{ type: 'text', text: 'edit me' }], source: { kind: 'user' } })) - agent.steer(createUserMessage({ content: [{ type: 'text', text: 'promote me' }], source: { kind: 'user' } })) - expect(pending.map(inboxText)).toEqual(['remove me', 'edit me', 'promote me']) + agent.steer(createUserMessage({ content: [{ type: 'text', text: 'keep me' }], source: { kind: 'user' } })) + expect(pending.map(inboxText)).toEqual(['keep me']) - const remove = pending[0]! - const edit = pending[1]! - const promote = pending[2]! - expect(agent.updateInbox(edit.id, { + const steering = pending[0]! + expect(agent.updateInbox(steering.id, { kind: 'edit', content: [{ type: 'text', text: 'edited' }], - })).toBe('applied') - expect(agent.updateInbox(remove.id, { kind: 'remove' })).toBe('applied') - expect(agent.updateInbox(promote.id, { kind: 'promote' })).toBe('applied') - expect(updates).toEqual([ - { id: edit.id, action: 'edit', text: 'edited' }, - { id: promote.id, action: 'promote', text: 'promote me' }, - ]) - expect(discards).toEqual([[remove.id]]) + })).toBe('not-found') + expect(agent.updateInbox(steering.id, { kind: 'remove' })).toBe('not-found') decision.resolve({ kind: 'allow' }) await idle @@ -179,7 +154,7 @@ describe('addressable inbox operations', () => { .map(event => event.type === 'steering/message' ? event.data.message.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('') : '')) - .toEqual(['promote me', 'edited']) + .toEqual(['keep me']) }) }) diff --git a/packages/core/agent/README.i18n.yaml b/packages/core/agent/README.i18n.yaml index 1ab6eb1172..78a933294a 100644 --- a/packages/core/agent/README.i18n.yaml +++ b/packages/core/agent/README.i18n.yaml @@ -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: e71d3454d22fa829af9617083c5e68586970c70a -README.zh.md: 2dac18e50ab6b4acfc899013313e5bb0069ab36c +README.md: 6bd5279ace93b6d2569833be6f102c854105c2eb +README.zh.md: cdbb0c0b70124e0037a0f7a7a03ddedf1e3b78d3 diff --git a/packages/core/agent/README.md b/packages/core/agent/README.md index e71d3454d2..6bd5279ace 100644 --- a/packages/core/agent/README.md +++ b/packages/core/agent/README.md @@ -61,7 +61,7 @@ Turn and step boundaries and the model token stream are durable `session/event` The handle every plugin programs against: - `agent.send(message, options)` — the one delivery primitive over the (`target` × `wakeup`) matrix. `message` is an already identified, frozen `UserMessage`; callers normally create it with `createUserMessage()` before routing begins. `SendOptions` owns only the `target` and `wakeup` policy. Each accepted FIFO occurrence receives its own `InboxItemId`, even when callers reuse a `MessageId`; `agent/inbox/enqueue`/`update` and the terminal `dequeue` or `discard` carry that complete `InboxItem`. `target: 'next-turn'` queues one independent FIFO item that, if admitted, becomes the sole ordinary prompt in its turn. `target: 'next-step'` with `wakeup: true` submits steering, while `target: 'next-step'` with `wakeup: false` injects durable context without running the model. The [one-send-one-turn Agent Note](../../../.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.md) owns the turn rationale. -- `agent.updateInbox(itemId, action)` — synchronously edits, removes, or promotes one still-pending occurrence. Edit keeps its `MessageId`, `InboxItemId`, source, placement, and FIFO position while replacing frozen content. Remove emits the occurrence's terminal discard. Promote moves it to the front of its current FIFO and makes an ordinary queued item waking. A claimed item has crossed the ownership boundary and returns `not-found`. +- `agent.updateInbox(itemId, action)` — synchronously edits or removes one still-pending queued occurrence. Edit keeps its `MessageId`, `InboxItemId`, source, and FIFO position while replacing frozen content; remove emits the occurrence's terminal discard. Steering and claimed occurrences return `not-found`. - `agent.followup(input)` — the `next-turn`/wakeup preset of `send()`: queue an ordinary follow-up turn and wake the driver. - `agent.steer(input)` — the `next-step`/wakeup preset: during prompt admission or an open turn, stage steering for the next safe boundary without dispatching `agent/prompt-submit`; outside that acceptance window, delegate to a woken follow-up. Admission failure leaves staged steering for retry or a later admitted prompt, while cancellation or disposal may discard it. - `agent.inject(input)` — the `next-step`/no-wakeup preset: append model-facing context without running the model; the next request sees a verbatim user-role message whose provenance is carried by the required `input.source`. During prompt admission or an open turn, injection waits in the outbox for the next safe boundary. Outside that acceptance window, it appends immediately without opening a turn; a context-only admission batch takes this fallback if admission closes without a turn, while context staged beside steering remains pending with it. Persistence reacts to `session/event` independently. Injection emits no `agent/inbox/*` event. diff --git a/packages/core/agent/README.zh.md b/packages/core/agent/README.zh.md index 2dac18e50a..cdbb0c0b70 100644 --- a/packages/core/agent/README.zh.md +++ b/packages/core/agent/README.zh.md @@ -61,7 +61,7 @@ Agent *创建* 由实现 `AgentFactory` 的插件(`dsh-agent-loop`)提供, 每个插件面向的 handle: - `agent.send(message, options)`:覆盖(`target` × `wakeup`)矩阵的唯一投递原语。`message` 是已有标识且已冻结的 `UserMessage`;调用方通常会在开始路由前使用 `createUserMessage()` 创建它。`SendOptions` 只持有 `target` 与 `wakeup` 策略。每次获准进入 FIFO 的项都会获得独立的 `InboxItemId`,即使调用方复用了同一个 `MessageId`;`agent/inbox/enqueue`/`update` 及终态 `dequeue` 或 `discard` 都会携带这一完整 `InboxItem`。`target: 'next-turn'` 排队一条独立 FIFO 项,获准后成为其轮次中唯一的普通提示词。`target: 'next-step'` 且 `wakeup: true` 提交 steering(中途引导),而 `target: 'next-step'` 且 `wakeup: false` 注入持久上下文,不运行模型。轮次原理由 [one-send-one-turn Agent Note](../../../.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.md)拥有。 -- `agent.updateInbox(itemId, action)`:同步编辑、移除或前移一个仍处于待处理状态的项。编辑会替换已冻结的内容,同时保留其 `MessageId`、`InboxItemId`、来源、放置方式与 FIFO 位置。移除会发出该项的终态 discard。前移会把它移至当前 FIFO 的队首,并使普通 queued 项能够唤醒驱动器。已被认领的项已经跨越所有权边界,因此返回 `not-found`。 +- `agent.updateInbox(itemId, action)`:同步编辑或移除一个仍处于待处理状态的 queued 入队项。编辑会替换已冻结的内容,同时保留其 `MessageId`、`InboxItemId`、来源与 FIFO 位置;移除会发出该项的终态 discard。steering 项和已被认领的项会返回 `not-found`。 - `agent.followup(input)`:`send()` 的 `next-turn`/wakeup 预设:排队一个普通后续轮次并唤醒驱动器。 - `agent.steer(input)`:`next-step`/wakeup 预设:提示词接纳期间或轮次打开时,为下一个安全边界暂存 steering,且不分发 `agent/prompt-submit`;该接收窗口之外则委托给会唤醒的后续轮次。接纳失败会保留暂存的 steering,以供重试或之后获准的提示词使用,而取消或 dispose 可能丢弃它。 - `agent.inject(input)`:`next-step`/不唤醒预设:追加面向模型的上下文而不运行模型;下一次请求会看到一条逐字的 user role 消息,其来源由必填的 `input.source` 携带。提示词接纳期间或轮次打开时,注入会在 outbox 中等待下一个安全边界。该接收窗口之外,它会立即追加而不开启轮次;如果接纳结束却未开启轮次,仅含上下文的接纳批次会采用这一回退,而与 steering 一同暂存的上下文则会随其继续待处理。持久化独立地响应 `session/event`。注入不发出 `agent/inbox/*` 事件。 diff --git a/packages/core/agent/src/types.ts b/packages/core/agent/src/types.ts index d32eb303bc..e1d978459d 100644 --- a/packages/core/agent/src/types.ts +++ b/packages/core/agent/src/types.ts @@ -50,11 +50,10 @@ export interface InboxItem { readonly placement: InboxPlacement } -/** A user-requested mutation of one still-pending inbox item. */ +/** A user-requested mutation of one still-pending queued occurrence. */ export type InboxAction = | { readonly kind: 'edit'; readonly content: ContentBlock[] } | { readonly kind: 'remove' } - | { readonly kind: 'promote' } /** Result of applying an inbox action at the synchronous ownership boundary. */ export type InboxActionResult = 'applied' | 'not-found' @@ -180,13 +179,11 @@ export interface Agent { send(message: UserMessage, options: SendOptions): void /** - * Mutate one still-pending inbox occurrence synchronously. Editing preserves + * Mutate one still-pending queued occurrence synchronously. Editing preserves * the message identity and queue position; removal publishes its terminal - * discard; promotion moves it to the front of its current FIFO and makes a - * queued item waking. A driver-claimed item is no longer pending and returns - * `not-found`. - * @param id - independently addressable inbox occurrence. - * @param action - edit, remove, or promote operation. + * discard. Steering occurrences and driver-claimed items return `not-found`. + * @param id - independently addressable queued occurrence. + * @param action - edit or remove operation. * @returns whether the pending occurrence was found and updated. */ updateInbox(id: InboxItemId, action: InboxAction): InboxActionResult @@ -280,21 +277,14 @@ declare module 'cordis' { */ 'agent/inbox/enqueue'(this: Scoped, agent: Agent, item: InboxItem): void /** - * A still-pending inbox item changed content or position. The item id and - * placement remain stable; edit carries the replacement message, while - * promote makes this occurrence first in its current FIFO. + * A still-pending queued item changed content. The item id, placement, and + * position remain stable while the event carries the replacement message. * @param agent - the owning agent. * @param item - the complete post-update occurrence. - * @param action - the applied non-terminal operation. * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. * @mode emit */ - 'agent/inbox/update'( - this: Scoped, - agent: Agent, - item: InboxItem, - action: 'edit' | 'promote', - ): void + 'agent/inbox/update'(this: Scoped, agent: Agent, item: InboxItem): 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 diff --git a/packages/core/scope/tests/invariant.spec.ts b/packages/core/scope/tests/invariant.spec.ts index 4057d248d1..a2b0b6bbe0 100644 --- a/packages/core/scope/tests/invariant.spec.ts +++ b/packages/core/scope/tests/invariant.spec.ts @@ -50,7 +50,7 @@ describe('scoped-dispatch invariants', () => { 'agent/disposed': [agent], 'agent/status': [agent, 'idle'], 'agent/inbox/enqueue': [agent, item], - 'agent/inbox/update': [agent, item, 'edit'], + 'agent/inbox/update': [agent, item], 'agent/inbox/dequeue': [agent, item], 'agent/inbox/discard': [agent, []], 'agent/cancel-requested': [agent, { kind: 'user' }], diff --git a/packages/host/apiproxy/README.i18n.yaml b/packages/host/apiproxy/README.i18n.yaml index 573bb8e2ab..ef2ab89c0b 100644 --- a/packages/host/apiproxy/README.i18n.yaml +++ b/packages/host/apiproxy/README.i18n.yaml @@ -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/host/apiproxy/README.md -README.md: c6f9ffae399b9d88b354b3312e36fe05b2d38a63 -README.zh.md: 59f31e521c5432dcf2ba64972b6b0d0e87988b23 +README.md: e4a29dc288d7279366c2a28ed43573792979aa92 +README.zh.md: 9f5eb0c4f84eb909d931dda8cbac764bf83aa2fe diff --git a/packages/host/apiproxy/README.md b/packages/host/apiproxy/README.md index c6f9ffae39..e4a29dc288 100644 --- a/packages/host/apiproxy/README.md +++ b/packages/host/apiproxy/README.md @@ -16,7 +16,7 @@ Session titles ride the generic projection pair like every other domain — the Session model routing is a session-domain contract. `session.models` returns the selected provider/model/reasoning target with provider-grouped advisory models, exact-route reasoning metadata, and provider-local lookup failures. `session.selectModel` validates the optional adapter-owned reasoning effort and replaces the complete target selected for the next prompt-assembly boundary. Catalog membership is not validation: an adapter may resolve an unlisted model, while an unavailable route or unsupported effort returns `model-unavailable`. -Pending agent input is a live control-plane contract, not session history. The gateway mirrors complete `InboxItem` occurrences from `agent/inbox/*` and broadcasts authoritative `session/queue` snapshots on every change and reconnect. `session.updateQueue` addresses one `InboxItemId`: edit replaces pending content, remove discards it, and promote moves it to the front of its current FIFO while waking ordinary queued work. A driver claim wins races by retiring the address before admission; a later operation returns `queue-item-not-found`. The client never infers retirement from turn or status events. +Pending queued input is a live control-plane contract, not session history. The gateway mirrors queued `InboxItem` occurrences from `agent/inbox/*` and broadcasts authoritative `session/queue` snapshots on every queued change and reconnect; pending steering stays outside this Web projection. `session.updateQueue` addresses one `InboxItemId`: edit replaces pending content and remove discards it. A driver claim wins races by retiring the address before admission; a later operation returns `queue-item-not-found`. The client never infers retirement from turn or status events. Workspace and Session lists are separate reconnect baselines. `workspace.create` creates a unique name or adopts an existing directory, `workspace.delete` removes only the Workspace registration, `session.create` accepts an optional preallocated Session id, and `host/workspace-changed`, `host/workspace-removed`, plus `host/session-added` carry committed increments in either arrival order. Registration deletion preserves the directory and session logs; its Sessions remain in `session.list` and become Ungrouped. `SessionSummary.blank` and the `host/session-added` frame carry the derived zero-events bit: clients hide blank sessions and reuse them per workspace, flip blank on the first `host/session-status(running:true)`, and treat `session.list` as the reconnect authority; cold summaries are never blank because lazy persistence keeps never-appended sessions out of `list()`. diff --git a/packages/host/apiproxy/README.zh.md b/packages/host/apiproxy/README.zh.md index 59f31e521c..9f5eb0c4f8 100644 --- a/packages/host/apiproxy/README.zh.md +++ b/packages/host/apiproxy/README.zh.md @@ -16,7 +16,7 @@ 会话模型路由属于会话领域契约。`session.models` 返回选中的提供方/模型/推理(reasoning)目标,以及按提供方分组的建议性模型、精确路由推理元数据和逐提供方查询失败记录。`session.selectModel` 校验由适配器持有的可选推理强度,并替换将在下一提示词组装边界使用的完整目标。目录成员关系不构成校验:适配器可以解析未列出的模型,而不可用路由或不受支持的推理强度会返回 `model-unavailable`。 -待处理 agent 输入属于实时控制平面契约,而非会话历史。网关镜像来自 `agent/inbox/*` 的完整 `InboxItem` 单次入队项,并在每次变更和重连时广播权威的 `session/queue` 快照。`session.updateQueue` 通过 `InboxItemId` 寻址单个项:编辑会替换待处理内容,移除会将其丢弃,前移会把它移至当前 FIFO 的队首,并使普通 queued 工作能够唤醒驱动器。驱动器在接纳前退役寻址标识,因此认领会赢得竞态;之后的操作返回 `queue-item-not-found`。客户端绝不根据轮次或状态事件推断项已退役。 +待处理的 queued 输入属于实时控制平面契约,而非会话历史。网关镜像来自 `agent/inbox/*` 的 queued `InboxItem` 入队项,并在每次 queued 变更和重连时广播权威的 `session/queue` 快照;待处理 steering(中途引导)不进入此 Web 投影。`session.updateQueue` 通过 `InboxItemId` 寻址单个项:编辑会替换待处理内容,移除会将其丢弃。驱动器在接纳前退役寻址标识,因此认领会赢得竞态;之后的操作返回 `queue-item-not-found`。客户端绝不根据轮次或状态事件推断项已退役。 Workspace 列表与 Session 列表是相互独立的重连基线。`workspace.create` 会创建唯一名称或接纳现有目录,`workspace.delete` 只移除 Workspace 注册记录,`session.create` 接受可选的预分配 Session id,`host/workspace-changed`、`host/workspace-removed` 与 `host/session-added` 则以任意到达顺序携带已提交的增量。删除注册记录会保留目录和会话日志;相关 Session 仍留在 `session.list` 中,并进入 Ungrouped。`SessionSummary.blank` 与 `host/session-added` 帧携带派生的零事件位:客户端隐藏空白会话并按 workspace 复用它们,在首个 `host/session-status(running:true)` 时翻转 blank,并以 `session.list` 作为重连权威;冷会话摘要永远不是空白:惰性持久化让从未追加过事件的会话根本不出现在 `list()` 中。 diff --git a/packages/host/apiproxy/src/api-proxy.ts b/packages/host/apiproxy/src/api-proxy.ts index 28cf49193e..3558949e5c 100644 --- a/packages/host/apiproxy/src/api-proxy.ts +++ b/packages/host/apiproxy/src/api-proxy.ts @@ -508,9 +508,9 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro }) /** - * Per-session inbox occurrence mirror serving the mux-open queue snapshot + * Per-session queued-occurrence mirror serving the mux-open queue snapshot * (the same refresh-recovery baseline as pending questions). Each terminal - * inbox event retires one matching occurrence, so repeated sends of the same + * queue event retires one matching occurrence, so repeated sends of the same * identified message remain visible until every occurrence is claimed. */ const queuedMirror = new Map() @@ -522,7 +522,6 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro items: items.map(item => ({ id: item.id, message: item.message, - placement: item.placement, })), }) } @@ -531,12 +530,14 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro const entries = queuedMirror.get(agent.id) if (entries === undefined) return const index = entries.findIndex(entry => entry.id === item.id) - if (index !== -1) entries.splice(index, 1) + if (index === -1) return + entries.splice(index, 1) if (entries.length === 0) queuedMirror.delete(agent.id) publishQueue(agent.id) } const disposers = [ ctx.on('agent/inbox/enqueue', (agent: Agent, item: InboxItem) => { + if (item.placement !== 'queued') return let entries = queuedMirror.get(agent.id) if (entries === undefined) { entries = [] @@ -545,18 +546,12 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro entries.push(item) publishQueue(agent.id) }), - ctx.on('agent/inbox/update', (agent: Agent, item: InboxItem, action) => { + ctx.on('agent/inbox/update', (agent: Agent, item: InboxItem) => { const entries = queuedMirror.get(agent.id) if (entries === undefined) return const index = entries.findIndex(entry => entry.id === item.id) if (index === -1) return - entries.splice(index, 1) - if (action === 'promote') { - const first = entries.findIndex(entry => entry.placement === item.placement) - entries.splice(first === -1 ? entries.length : first, 0, item) - } else { - entries.splice(index, 0, item) - } + entries.splice(index, 1, item) publishQueue(agent.id) }), ctx.on('agent/inbox/dequeue', (agent: Agent, item: InboxItem) => { @@ -567,6 +562,7 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro if (entries === undefined) return const ids = new Set(items.map(item => item.id)) const kept = entries.filter(entry => !ids.has(entry.id)) + if (kept.length === entries.length) return if (kept.length === 0) queuedMirror.delete(agent.id) else queuedMirror.set(agent.id, kept) publishQueue(agent.id) @@ -1540,7 +1536,6 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro items: items.map(item => ({ id: item.id, message: item.message, - placement: item.placement, })), })) } diff --git a/packages/host/apiproxy/src/api/events.schema.ts b/packages/host/apiproxy/src/api/events.schema.ts index d9d73b441e..4e17b1f403 100644 --- a/packages/host/apiproxy/src/api/events.schema.ts +++ b/packages/host/apiproxy/src/api/events.schema.ts @@ -50,7 +50,6 @@ export const muxFrameSchema = z.discriminatedUnion('type', [ items: z.array(z.object({ id: inboxItemIdSchema, message: messageSchema, - placement: z.union([z.literal('queued'), z.literal('steering')]), })), }), // value stays wide: it already passed its unit's own schema on the host, diff --git a/packages/host/apiproxy/src/api/events.ts b/packages/host/apiproxy/src/api/events.ts index 40e5c5a5ad..ad541b4e4d 100644 --- a/packages/host/apiproxy/src/api/events.ts +++ b/packages/host/apiproxy/src/api/events.ts @@ -32,14 +32,12 @@ export type ToolEventView = | { for: 'call'; view: ToolCallView } | { for: 'result'; view: ToolResultView } -/** One pending inbox occurrence in an authoritative queue snapshot. */ +/** One pending queued occurrence in an authoritative queue snapshot. */ export interface QueuedInboxItem { /** Agent-owned occurrence identity used by queue mutations. */ id: InboxItemId /** Complete pending message; it is not durable until the Agent claims it. */ message: Message - /** Acceptance-time FIFO classification. */ - placement: 'queued' | 'steering' } /** Streaming face of the contract: the two SSE stream openers (mux + host). */ @@ -73,10 +71,11 @@ export type MuxFrame = | { type: 'question/requested'; sessionId: SessionId; questions: AskUserQuestionItem[] } | { type: 'question/resolved'; sessionId: SessionId; questionRpcId: RpcId; outcome: 'answered' | 'cancelled' } /** - * Complete transient inbox state after every enqueue, mutation, claim, or + * Complete transient queue state after every enqueue, mutation, claim, or * discard. Pending work is not model-visible and therefore has no durable - * session event; the whole snapshot makes edit, reorder, deletion, cancel, - * and reconnect converge through one authoritative signal. + * session event; the whole snapshot makes edit, deletion, cancel, and + * reconnect converge through one authoritative signal. Pending steering is + * outside this Web queue projection. */ | { type: 'session/queue'; sessionId: SessionId; items: QueuedInboxItem[] } /** diff --git a/packages/host/apiproxy/src/api/sessions.schema.ts b/packages/host/apiproxy/src/api/sessions.schema.ts index 55b0847a13..e3744ec37a 100644 --- a/packages/host/apiproxy/src/api/sessions.schema.ts +++ b/packages/host/apiproxy/src/api/sessions.schema.ts @@ -225,7 +225,6 @@ export const sessionUpdateQueueRequestSchema = z.object({ action: z.discriminatedUnion('kind', [ z.object({ kind: z.literal('edit'), content: z.array(contentBlockSchema) }), z.object({ kind: z.literal('remove') }), - z.object({ kind: z.literal('promote') }), ]), }) as unknown as z.ZodType> diff --git a/packages/host/apiproxy/src/api/sessions.ts b/packages/host/apiproxy/src/api/sessions.ts index ecff54174b..a57c3f4b73 100644 --- a/packages/host/apiproxy/src/api/sessions.ts +++ b/packages/host/apiproxy/src/api/sessions.ts @@ -129,7 +129,6 @@ export interface SessionModels { export type QueueAction = | { kind: 'edit'; content: ContentBlock[] } | { kind: 'remove' } - | { kind: 'promote' } /** Session list entry (v1 builds no index: list does readdir+stat). */ export interface SessionSummary { @@ -237,9 +236,7 @@ export interface SessionsApi { Promise> /** - * Edits, removes, or promotes one pending inbox occurrence. Promotion means - * first in its current FIFO; a queued item is also made waking, so an idle - * agent starts it and a running agent takes it as the next independent turn. + * Edits or removes one pending queued occurrence. */ updateQueue(request: RpcRequest<{ sessionId: SessionId; itemId: InboxItemId; action: QueueAction }>): Promise> diff --git a/packages/host/apiproxy/tests/api-proxy-commands.spec.ts b/packages/host/apiproxy/tests/api-proxy-commands.spec.ts index 9212f7dd04..3d7602d296 100644 --- a/packages/host/apiproxy/tests/api-proxy-commands.spec.ts +++ b/packages/host/apiproxy/tests/api-proxy-commands.spec.ts @@ -295,7 +295,7 @@ describe('session.updateQueue', () => { payload: { sessionId: agent.id, itemId: InboxItemId('present'), - action: { kind: 'promote' }, + action: { kind: 'edit', content: [{ type: 'text', text: 'edited' }] }, }, }) expect(expectOk(applied)).toEqual({ accepted: true }) @@ -309,7 +309,7 @@ describe('session.updateQueue', () => { }) expect(expectErr(missing)).toMatchObject({ code: 'queue-item-not-found' }) expect(seen).toEqual([ - { id: 'present', action: { kind: 'promote' } }, + { id: 'present', action: { kind: 'edit', content: [{ type: 'text', text: 'edited' }] } }, { id: 'claimed', action: { kind: 'remove' } }, ]) }) @@ -322,8 +322,8 @@ describe('session/queue frames', () => { const agent = stubAgent(ctx) const live = new AbortController() const liveStream = api.events.mux({ rpcId: RpcId('t-mux-live'), payload: {} }, live.signal) - // subscribed baseline + 2 queue snapshots - const liveCollected = collect(liveStream, 3, live) + // subscribed baseline + one queued snapshot; pending steering stays off this wire. + const liveCollected = collect(liveStream, 2, live) const queued = inboxItem('i-1', inboxMessage('m-1', 'queued prompt'), 'queued') const steering = inboxItem('i-2', inboxMessage('m-2', 'steering prompt'), 'steering') @@ -332,40 +332,41 @@ describe('session/queue frames', () => { const liveFrames = (await liveCollected).filter(f => f.type === 'session/queue') expect(liveFrames).toEqual([ - { type: 'session/queue', sessionId: agent.id, items: [queued] }, - { type: 'session/queue', sessionId: agent.id, items: [queued, steering] }, + { + type: 'session/queue', + sessionId: agent.id, + items: [{ id: queued.id, message: queued.message }], + }, ]) // A fresh mux connection replays only the current authoritative snapshot. const replay = new AbortController() const replayFrames = await collect( api.events.mux({ rpcId: RpcId('t-mux-replay'), payload: {} }, replay.signal), 2, replay) - expect(replayFrames.filter(f => f.type === 'session/queue')).toEqual([liveFrames[1]]) + expect(replayFrames.filter(f => f.type === 'session/queue')).toEqual([liveFrames[0]]) }) - it('publishes edit and promotion in the authoritative order', async () => { + it('publishes edits in place in the authoritative order', async () => { const ctx = await harness() const api = createApiProxy(ctx, DEFAULTS) const agent = stubAgent(ctx) const abort = new AbortController() const collected = collect( - api.events.mux({ rpcId: RpcId('t-mux-updates'), payload: {} }, abort.signal), 6, abort) + api.events.mux({ rpcId: RpcId('t-mux-updates'), payload: {} }, abort.signal), 5, abort) const first = inboxItem('i-a', inboxMessage('m-a', 'a'), 'queued') const second = inboxItem('i-b', inboxMessage('m-b', 'b'), 'queued') const edited = inboxItem('i-b', inboxMessage('m-b', 'b edited'), 'queued') ctx.emit('agent/inbox/enqueue', agent, first) ctx.emit('agent/inbox/enqueue', agent, second) - ctx.emit('agent/inbox/update', agent, edited, 'edit') - ctx.emit('agent/inbox/update', agent, edited, 'promote') + ctx.emit('agent/inbox/update', agent, edited) ctx.emit('agent/inbox/dequeue', agent, edited) const frames = (await collected).filter(frame => frame.type === 'session/queue') expect(frames.map(frame => frame.items)).toEqual([ - [first], - [first, second], - [first, edited], - [edited, first], - [first], + [{ id: first.id, message: first.message }], + [{ id: first.id, message: first.message }, { id: second.id, message: second.message }], + [{ id: first.id, message: first.message }, { id: edited.id, message: edited.message }], + [{ id: first.id, message: first.message }], ]) }) diff --git a/packages/host/apiproxy/tests/rpc-schemas.spec.ts b/packages/host/apiproxy/tests/rpc-schemas.spec.ts index 7b4a50a428..80cd0baf4c 100644 --- a/packages/host/apiproxy/tests/rpc-schemas.spec.ts +++ b/packages/host/apiproxy/tests/rpc-schemas.spec.ts @@ -232,7 +232,7 @@ describe('sessions domain schemas', () => { sessionId: 's1', itemId: 'i1', action: { kind: 'remove' }, }).action.kind).toBe('remove') expect(() => sessionUpdateQueueRequestSchema.parse({ - sessionId: 's1', itemId: '', action: { kind: 'promote' }, + sessionId: 's1', itemId: 'i1', action: { kind: 'promote' }, })).toThrow() expect(sessionCancelValueSchema.parse({ accepted: true }).accepted).toBe(true) expect(sessionUpdateQueueValueSchema.parse({ accepted: true }).accepted).toBe(true) @@ -380,8 +380,7 @@ describe('events frame schemas', () => { { type: 'question/requested', sessionId: 's', questions: [{ id: 'q', question: 'Q?', options: [{ label: 'L' }], multiSelect: true }] }, { type: 'question/resolved', sessionId: 's', questionRpcId: 'r', outcome: 'answered' }, { type: 'session/queue', sessionId: 's', items: [ - { id: 'i1', message: { id: 'm1', role: 'user', content: [{ type: 'text', text: 'queued prompt' }], source: { kind: 'user', rpcId: 'r9' } }, placement: 'queued' }, - { id: 'i2', message: { id: 'm2', role: 'user', content: [{ type: 'text', text: 'steer' }], source: { kind: 'user' } }, placement: 'steering' }, + { id: 'i1', message: { id: 'm1', role: 'user', content: [{ type: 'text', text: 'queued prompt' }], source: { kind: 'user', rpcId: 'r9' } } }, ] }, { type: 'session/projection', sessionId: 's', key: 'todos', value: [{ content: 'x', status: 'pending' }], seq: 7 }, { type: 'stream/error', error: { code: 'internal', message: 'm', details: {} } }, @@ -402,8 +401,8 @@ describe('events frame schemas', () => { it('rejects a queue snapshot with malformed items', () => { expect(() => muxFrameSchema.parse({ type: 'session/queue', sessionId: 's', items: 'x' })).toThrow() - expect(() => muxFrameSchema.parse({ type: 'session/queue', sessionId: 's', items: [{ id: '', message: {}, placement: 'queued' }] })).toThrow() - expect(() => muxFrameSchema.parse({ type: 'session/queue', sessionId: 's', items: [{ id: 'i', message: { id: 'm', role: 'user', content: [], source: {} }, placement: 'later' }] })).toThrow() + expect(() => muxFrameSchema.parse({ type: 'session/queue', sessionId: 's', items: [{ id: '', message: {} }] })).toThrow() + expect(() => muxFrameSchema.parse({ type: 'session/queue', sessionId: 's', items: [{ id: 'i', message: { id: 'm', role: 'user', content: [], source: {} } }] })).toThrow() }) it('accepts every host frame branch', () => {