refactor(agent): scope queue actions to edit and remove
This commit is contained in:
+2
-2
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# 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:
|
# 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
|
# 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.md: 4d0cbeff0c8a07362caa1ec18493267a9f0d2823
|
||||||
2026-07-22-unified-send-and-coalesced-user-messages.zh.md: 02a21193ce63926b6f04a02cb7ea8a64fe6603cb
|
2026-07-22-unified-send-and-coalesced-user-messages.zh.md: 935a1a78a6bed451c1db646dec2ec5f4f5e87949
|
||||||
|
|||||||
+1
-1
@@ -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.
|
**`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.
|
**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.
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@ agent 的对外驱动接口逐渐长出三个近乎平行的动词——`send`
|
|||||||
|
|
||||||
**`send` 不返回标识。** 调用方已经持有完整消息及其不透明的 `MessageId`;消息的创建与冻结由[带标识的不可变消息值决策](2026-07-28-identified-immutable-message-values.md)负责,而不是由路由负责。
|
**`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` 仍是更宽泛的活动信号,而非路由判据。
|
**准入接受 next-step 输入,但不会因此成为一个轮次。** 循环会在 `agent/prompt-submit` 前打开一个私有的 next-step 接受窗口,使其贯穿整个轮次,并在 `turn/end` 前关闭。因此,在准入期间收到的 steering 和注入会一起留在 outbox 中并加入获准轮次。如果准入被阻止或失败,仅含调用方上下文的批次会采用空闲注入的立即追加行为,而 steering 及与其一同暂存的上下文仍可重试;两种路径都不会写入被拒绝的提示词。后续提示词获准时,保留在 outbox 中的输入会先于该提示词进入其轮次,而当前准入期间接受的输入则留在提示词之后。在 `turn/end` 前关闭窗口,可以保留这样的规则:可重入的晚到 steering 会成为一个独立的排队轮次。`Agent.acceptsNextStep` 会公开一次 `next-step` 发送当前是否会加入该窗口;`status` 仍是更宽泛的活动信号,而非路由判据。
|
||||||
|
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# 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:
|
# 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
|
# 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.md: 93b11dc590728a3c236971fbadf9c7c0187d943c
|
||||||
2026-07-29-addressable-queue-operations.zh.md: ac442421a1e21b2e09bb003ca9be1a0412374d7f
|
2026-07-29-addressable-queue-operations.zh.md: 3a8e46946ad8cbe122bc3d89c6b470654ea97baf
|
||||||
|
|||||||
@@ -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
|
Status: implemented
|
||||||
|
|
||||||
@@ -6,19 +6,17 @@ English | [中文](2026-07-29-addressable-queue-operations.zh.md)
|
|||||||
|
|
||||||
## Problem
|
## 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.
|
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.
|
||||||
|
|
||||||
“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.
|
|
||||||
|
|
||||||
## Decision
|
## 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.
|
**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
|
## 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.
|
**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
|
## 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
|
## 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.
|
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.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Agent Note(agent 决策记录):为待处理队列项提供编辑、移除与前移操作
|
# Agent Note(agent 决策记录):为待处理队列项提供编辑与移除操作
|
||||||
|
|
||||||
Status: implemented
|
Status: implemented
|
||||||
|
|
||||||
@@ -6,19 +6,17 @@ Status: implemented
|
|||||||
|
|
||||||
## 问题
|
## 问题
|
||||||
|
|
||||||
Web 队列能够渲染待处理消息,但无法操作其中某一行。`MessageId` 不足以充当寻址标识,因为调用方可以多次将同一条不可变消息加入队列。浏览器还会根据轮次和状态事件推断队列项已退役,因此当行操作与驱动器认领发生竞态时,系统无法给出权威结果。
|
Web 队列能够渲染待处理消息,但无法编辑或删除其中某一行。`MessageId` 不足以充当寻址标识,因为调用方可以多次将同一条不可变消息加入队列。浏览器还会根据轮次和状态事件推断队列项已退役,因此当行操作与驱动器认领发生竞态时,系统无法给出权威结果。
|
||||||
|
|
||||||
“立即发送”还引入了另一项语义选择:它可以表示重新排序下一个独立轮次、以 steering(中途引导)方式打断当前轮次,或取消当前工作。只有第一种解释能够保留该队列行原有的投递契约。
|
|
||||||
|
|
||||||
## 决策
|
## 决策
|
||||||
|
|
||||||
**每次获准进入 FIFO 的项都有独立标识。** AgentLoop 会铸造不透明的 `InboxItemId`,并发布一个 `InboxItem`,其中包含该 id、已有标识的 `UserMessage`,以及接受时确定的 `queued | steering` 放置方式。复用同一个 `MessageId` 会创建不同的 inbox 标识。注入绕过 FIFO,因此不会获得 inbox 标识。
|
**每次获准进入 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` 报告真实竞态。
|
**在浏览器中进行乐观变更。** 不予采纳,因为驱动器认领或另一个客户端可能先于 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 则不属于此操作接口。
|
||||||
|
|
||||||
现在,协议会在每次变更时携带完整队列快照。队列预期保持较短,因此系统优先选择确定性恢复和多客户端收敛,而非增量变更协议。
|
现在,协议会在每次变更时携带完整队列快照。队列预期保持较短,因此系统优先选择确定性恢复和多客户端收敛,而非增量变更协议。
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# 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:
|
# after editing either side, bring the other along and re-record with:
|
||||||
# pnpm run verify-translation-pairing --write docs/architecture.md
|
# pnpm run verify-translation-pairing --write docs/architecture.md
|
||||||
architecture.md: 9a33a85e806e04dec3bfb73de8e4781c90f37677
|
architecture.md: 1fd9bd128d1bcc0dd91d46131981ea4fc331bd74
|
||||||
architecture.zh.md: 6fb91d6511c1213fa44da8a3ca17b100e1e29b95
|
architecture.zh.md: 8521f09c6e415f9f8d1c0a44f7534b59c876decc
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ choose declarative identity and fresh/resume path
|
|||||||
-> enable driving -> agent/session-start(source) -> start driver
|
-> enable driving -> agent/session-start(source) -> start driver
|
||||||
forever:
|
forever:
|
||||||
wait for queued occurrence
|
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
|
open the next-step acceptance window
|
||||||
-> agent/prompt-submit
|
-> agent/prompt-submit
|
||||||
blocked or failed prompt -> close the window without opening a turn
|
blocked or failed prompt -> close the window without opening a turn
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ choose declarative identity and fresh/resume path
|
|||||||
-> enable driving -> agent/session-start(source) -> start driver
|
-> enable driving -> agent/session-start(source) -> start driver
|
||||||
forever:
|
forever:
|
||||||
wait for queued occurrence
|
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
|
open the next-step acceptance window
|
||||||
-> agent/prompt-submit
|
-> agent/prompt-submit
|
||||||
blocked or failed prompt -> close the window without opening a turn
|
blocked or failed prompt -> close the window without opening a turn
|
||||||
|
|||||||
@@ -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)
|
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
|
### `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)
|
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
|
### `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)
|
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
|
### `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)
|
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
|
### `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)
|
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
|
### `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)
|
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
|
### `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)
|
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
|
### `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
|
```ts cordis-catalog
|
||||||
/**
|
/**
|
||||||
* A still-pending inbox item changed content or position. The item id and
|
* A still-pending queued item changed content. The item id, placement, and
|
||||||
* placement remain stable; edit carries the replacement message, while
|
* position remain stable while the event carries the replacement message.
|
||||||
* promote makes this occurrence first in its current FIFO.
|
|
||||||
* @param agent - the owning agent.
|
* @param agent - the owning agent.
|
||||||
* @param item - the complete post-update occurrence.
|
* @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.
|
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||||
* @mode emit
|
* @mode emit
|
||||||
*/
|
*/
|
||||||
'agent/inbox/update'( this: Scoped<Agent>, agent: Agent, item: InboxItem, action: 'edit' | 'promote', ): void
|
'agent/inbox/update'(this: Scoped<Agent>, agent: Agent, item: InboxItem): void
|
||||||
```
|
```
|
||||||
|
|
||||||
Types: [Agent](../core-data-structures/core.md) · [InboxItem](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
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
|
### `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)
|
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
|
### `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)
|
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
|
### `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)
|
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
|
### `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)
|
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
|
### `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)
|
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
|
### `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)
|
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
|
### `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)
|
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
|
### `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)
|
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/*`
|
## `agent-loop/*`
|
||||||
|
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# 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:
|
# after editing either side, bring the other along and re-record with:
|
||||||
# pnpm run verify-translation-pairing --write docs/core-data-structures/core.md
|
# pnpm run verify-translation-pairing --write docs/core-data-structures/core.md
|
||||||
core.md: a39560eecca4689186ce2d3fc183250f8698cb90
|
core.md: dad533cee00646a40f57bd9097b2cceb8e9de9e2
|
||||||
core.zh.md: a5e3b80e11c97d64de1afd082ef02097eb61787c
|
core.zh.md: 9e8afac0744fcf0df8c35dad5debce746d6614c6
|
||||||
|
|||||||
@@ -445,11 +445,10 @@ interface InboxItem {
|
|||||||
```
|
```
|
||||||
|
|
||||||
```ts type-equiv
|
```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 =
|
type InboxAction =
|
||||||
| { readonly kind: 'edit'; readonly content: ContentBlock[] }
|
| { readonly kind: 'edit'; readonly content: ContentBlock[] }
|
||||||
| { readonly kind: 'remove' }
|
| { readonly kind: 'remove' }
|
||||||
| { readonly kind: 'promote' }
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```ts type-equiv
|
```ts type-equiv
|
||||||
@@ -549,13 +548,11 @@ interface Agent {
|
|||||||
send(message: UserMessage, options: SendOptions): void
|
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
|
* the message identity and queue position; removal publishes its terminal
|
||||||
* discard; promotion moves it to the front of its current FIFO and makes a
|
* discard. Steering occurrences and driver-claimed items return `not-found`.
|
||||||
* queued item waking. A driver-claimed item is no longer pending and returns
|
* @param id - independently addressable queued occurrence.
|
||||||
* `not-found`.
|
* @param action - edit or remove operation.
|
||||||
* @param id - independently addressable inbox occurrence.
|
|
||||||
* @param action - edit, remove, or promote operation.
|
|
||||||
* @returns whether the pending occurrence was found and updated.
|
* @returns whether the pending occurrence was found and updated.
|
||||||
*/
|
*/
|
||||||
updateInbox(id: InboxItemId, action: InboxAction): InboxActionResult
|
updateInbox(id: InboxItemId, action: InboxAction): InboxActionResult
|
||||||
|
|||||||
@@ -453,11 +453,10 @@ interface InboxItem {
|
|||||||
```
|
```
|
||||||
|
|
||||||
```ts type-equiv
|
```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 =
|
type InboxAction =
|
||||||
| { readonly kind: 'edit'; readonly content: ContentBlock[] }
|
| { readonly kind: 'edit'; readonly content: ContentBlock[] }
|
||||||
| { readonly kind: 'remove' }
|
| { readonly kind: 'remove' }
|
||||||
| { readonly kind: 'promote' }
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```ts type-equiv
|
```ts type-equiv
|
||||||
@@ -557,13 +556,11 @@ interface Agent {
|
|||||||
send(message: UserMessage, options: SendOptions): void
|
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
|
* the message identity and queue position; removal publishes its terminal
|
||||||
* discard; promotion moves it to the front of its current FIFO and makes a
|
* discard. Steering occurrences and driver-claimed items return `not-found`.
|
||||||
* queued item waking. A driver-claimed item is no longer pending and returns
|
* @param id - independently addressable queued occurrence.
|
||||||
* `not-found`.
|
* @param action - edit or remove operation.
|
||||||
* @param id - independently addressable inbox occurrence.
|
|
||||||
* @param action - edit, remove, or promote operation.
|
|
||||||
* @returns whether the pending occurrence was found and updated.
|
* @returns whether the pending occurrence was found and updated.
|
||||||
*/
|
*/
|
||||||
updateInbox(id: InboxItemId, action: InboxAction): InboxActionResult
|
updateInbox(id: InboxItemId, action: InboxAction): InboxActionResult
|
||||||
|
|||||||
@@ -8,22 +8,22 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
|||||||
| Event | Mode | Declared in | Dispatchers | Listeners |
|
| 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-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/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:253`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`goal-session`](../packages/goal/goal-session), [`tui`](../packages/ui/tui) |
|
| `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: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/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: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/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: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/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: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/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: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/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:292`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | `apiproxy` |
|
| `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: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/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:382`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent) |
|
| `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: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/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: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/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:430`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`compact-basic`](../packages/compact/compact-basic) |
|
| `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: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/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: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/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: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/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` |
|
| `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) |
|
| `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) |
|
| `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) |
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# 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:
|
# after editing either side, bring the other along and re-record with:
|
||||||
# pnpm run verify-translation-pairing --write packages/client/runtime/README.md
|
# pnpm run verify-translation-pairing --write packages/client/runtime/README.md
|
||||||
README.md: f0b287d31b3aa9dddd2fde8d3f95bddd290bcf8d
|
README.md: 766d8516225cd46cb1a3a80c832d1cf55e816140
|
||||||
README.zh.md: b2e9c29f0f254fd514f201fcedf6a9cb5e05f86c
|
README.zh.md: 9b514afca91f604b3e895187de3b5532bf22a692
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ SlotsService gives the renderer separate bare observables for `useSessions` and
|
|||||||
|
|
||||||
## Pending queue projection
|
## 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
|
## Code Mode sub-dispatch index
|
||||||
|
|
||||||
|
|||||||
@@ -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 子调用索引
|
## Code Mode 子调用索引
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export interface ISession {
|
|||||||
/**
|
/**
|
||||||
* Apply one mutation to a still-pending queue occurrence.
|
* Apply one mutation to a still-pending queue occurrence.
|
||||||
* @param itemId - agent-owned inbox occurrence identity.
|
* @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.
|
* @returns acceptance, or a business/transport error.
|
||||||
*/
|
*/
|
||||||
updateQueue(itemId: InboxItemId, action: QueueAction): Promise<RpcResult<{ accepted: true }>>
|
updateQueue(itemId: InboxItemId, action: QueueAction): Promise<RpcResult<{ accepted: true }>>
|
||||||
|
|||||||
@@ -222,7 +222,6 @@ export interface QueuedMessage {
|
|||||||
readonly preview: string
|
readonly preview: string
|
||||||
/** Complete editable text; null when the message contains non-text blocks. */
|
/** Complete editable text; null when the message contains non-text blocks. */
|
||||||
readonly text: string | null
|
readonly text: string | null
|
||||||
readonly placement: 'queued' | 'steering'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** In-progress assistant output (chunk accumulator product). */
|
/** In-progress assistant output (chunk accumulator product). */
|
||||||
|
|||||||
@@ -407,7 +407,6 @@ export class Session implements SessionFace {
|
|||||||
id: item.id,
|
id: item.id,
|
||||||
preview: queuePreviewOf(item.message.content),
|
preview: queuePreviewOf(item.message.content),
|
||||||
text: queueTextOf(item.message.content),
|
text: queueTextOf(item.message.content),
|
||||||
placement: item.placement,
|
|
||||||
}))
|
}))
|
||||||
this.queueRev++
|
this.queueRev++
|
||||||
this.notifier.markDirty()
|
this.notifier.markDirty()
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ const iid = (id: string): InboxItemId => id as InboxItemId
|
|||||||
interface QueueFixture {
|
interface QueueFixture {
|
||||||
id: string
|
id: string
|
||||||
body: string
|
body: string
|
||||||
placement?: 'queued' | 'steering'
|
|
||||||
content?: ContentBlock[]
|
content?: ContentBlock[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +35,6 @@ function queueFrame(items: QueueFixture[]): MuxFrame {
|
|||||||
content: item.content ?? text(item.body),
|
content: item.content ?? text(item.body),
|
||||||
source: { kind: 'user', rpcId: rid(`rpc-${item.id}`) } as never,
|
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', () => {
|
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()
|
const session = makeSession()
|
||||||
session.handleMuxEnvelope(rid('env-1'), queueFrame([
|
session.handleMuxEnvelope(rid('env-1'), queueFrame([
|
||||||
{ id: 'q-1', body: '第一条 排队\n消息' },
|
{ id: 'q-1', body: '第一条 排队\n消息' },
|
||||||
{ id: 'q-2', body: '插话', placement: 'steering' },
|
|
||||||
]))
|
]))
|
||||||
expect(session.getSnapshot().queue).toEqual([
|
expect(session.getSnapshot().queue).toEqual([
|
||||||
{ id: 'q-1', preview: '第一条 排队 消息', text: '第一条 排队\n消息', placement: 'queued' },
|
{ id: 'q-1', preview: '第一条 排队 消息', text: '第一条 排队\n消息' },
|
||||||
{ id: 'q-2', preview: '插话', text: '插话', placement: 'steering' },
|
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -66,7 +62,7 @@ describe('queue snapshot intake', () => {
|
|||||||
content: [{ type: 'text', text: 'hi' }, { type: 'image', data: 'x' } as never],
|
content: [{ type: 'text', text: 'hi' }, { type: 'image', data: 'x' } as never],
|
||||||
}]))
|
}]))
|
||||||
expect(session.getSnapshot().queue).toEqual([
|
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' },
|
{ id: 'q-2', body: 'two edited' },
|
||||||
]))
|
]))
|
||||||
expect(session.getSnapshot().queue).toEqual([
|
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([]))
|
session.handleMuxEnvelope(rid('env-6'), queueFrame([]))
|
||||||
expect(session.getSnapshot().queue).toEqual([])
|
expect(session.getSnapshot().queue).toEqual([])
|
||||||
@@ -112,12 +108,12 @@ describe('queue operation transport', () => {
|
|||||||
session.handleMuxEnvelope(rid('env-op'), queueFrame([{ id: 'q-op', body: 'pending' }]))
|
session.handleMuxEnvelope(rid('env-op'), queueFrame([{ id: 'q-op', body: 'pending' }]))
|
||||||
const before = session.getSnapshot().queue
|
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 } })
|
.resolves.toEqual({ ok: true, value: { accepted: true } })
|
||||||
expect(api.callsOf('session.updateQueue')).toEqual([{
|
expect(api.callsOf('session.updateQueue')).toEqual([{
|
||||||
sessionId: SID,
|
sessionId: SID,
|
||||||
itemId: 'q-op',
|
itemId: 'q-op',
|
||||||
action: { kind: 'promote' },
|
action: { kind: 'edit', content: text('next') },
|
||||||
}])
|
}])
|
||||||
expect(session.getSnapshot().queue).toBe(before)
|
expect(session.getSnapshot().queue).toBe(before)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# 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:
|
# after editing either side, bring the other along and re-record with:
|
||||||
# pnpm run verify-translation-pairing --write packages/client/ui-conversation/README.md
|
# pnpm run verify-translation-pairing --write packages/client/ui-conversation/README.md
|
||||||
README.md: 5bf3077c748c6e9be2c0a54474dce06c42eb0b2d
|
README.md: a017de28e612474dd4ad94b75385e6320e682955
|
||||||
README.zh.md: b4e1c9a23f1786c3dcba2063a9657a29445ed47f
|
README.zh.md: b352f6f745a1458d9f0555a5d7d269fe67b8e307
|
||||||
|
|||||||
@@ -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 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.
|
- **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.
|
- **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.
|
- **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** — 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.
|
- **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.
|
||||||
|
|||||||
@@ -40,5 +40,5 @@ todo 两个面就是在该形状上的两个注册项,都是普通注册方插
|
|||||||
- **others 工具行的闪光图标是手绘近似版本**:无法在本地导出设计字形的矢量几何;等到存在精确导出后再将其提升到 ui-primitives。
|
- **others 工具行的闪光图标是手绘近似版本**:无法在本地导出设计字形的矢量几何;等到存在精确导出后再将其提升到 ui-primitives。
|
||||||
- **审批面板的「始终允许此类」暂缓**:持久授权需要授权存储设计;今天只能回答允许一次/拒绝。
|
- **审批面板的「始终允许此类」暂缓**:持久授权需要授权存储设计;今天只能回答允许一次/拒绝。
|
||||||
- **TodoPanel 将过长条目截成单行省略号**:figma 条没有换行或展开入口,完整文本无法在行内读完。
|
- **TodoPanel 将过长条目截成单行省略号**:figma 条没有换行或展开入口,完整文本无法在行内读完。
|
||||||
- **Queue 编辑仅支持文本**:包含非文本块的行仍显示扁平化预览,但由于内联编辑器无法保留这些块,其编辑控件会被禁用。文本行进入编辑模式后,删除会替换为保存和取消;Enter 保存,Escape 取消。QueueDock 不提供立即发送控件;协议层的前移操作与 Web 交互保持分离。
|
- **Queue 编辑仅支持文本**:包含非文本块的行仍显示扁平化预览,但由于内联编辑器无法保留这些块,其编辑控件会被禁用。文本行进入编辑模式后,删除会替换为保存和取消;Enter 保存,Escape 取消。QueueDock 不提供立即发送控件。
|
||||||
- **Web 仅暴露待处理 Queue**:在 steering(中途引导)拥有专用交互之前,QueueDock 不展示待处理的 steering。已消费的 `steering/message` 仍会渲染到持久 transcript(文本记录)中,因此从外部提交的 steering 在回放时仍能如实呈现。
|
- **Web 仅暴露待处理 Queue**:在 steering(中途引导)拥有专用交互之前,Host 不会把待处理 steering 纳入 Queue 快照。已消费的 `steering/message` 仍会渲染到持久 transcript(文本记录)中,因此从外部提交的 steering 在回放时仍能如实呈现。
|
||||||
|
|||||||
@@ -105,7 +105,6 @@ export interface QueuedMessage {
|
|||||||
readonly id: InboxItemId
|
readonly id: InboxItemId
|
||||||
readonly preview: string
|
readonly preview: string
|
||||||
readonly text: string | null
|
readonly text: string | null
|
||||||
readonly placement: 'queued' | 'steering'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Guard union of the scoped consume-token event, checked by the machine. */
|
/** Guard union of the scoped consume-token event, checked by the machine. */
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
// The 'conversation.input.dock' SlotMap declaration lives in
|
// The 'conversation.input.dock' SlotMap declaration lives in
|
||||||
// ../contract/slots.ts beside the other input-region slots.
|
// ../contract/slots.ts beside the other input-region slots.
|
||||||
import type { Context } from 'cordis'
|
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 { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
||||||
import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client'
|
import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client'
|
||||||
import type { InboxItemId, QueueAction } from '@deepseek-ai/dsh-client-connection/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. */
|
/** Queue strip: one preview line per queued message; renders null when the queue is empty. */
|
||||||
export function QueueDock({ useSession, updateQueue, notify }: QueueDockProps) {
|
export function QueueDock({ useSession, updateQueue, notify }: QueueDockProps) {
|
||||||
const inbox = useSession(s => s.queue)
|
const queue = 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 [editing, setEditing] = useState<{ id: InboxItemId; text: string } | null>(null)
|
const [editing, setEditing] = useState<{ id: InboxItemId; text: string } | null>(null)
|
||||||
const [busy, setBusy] = useState<InboxItemId | null>(null)
|
const [busy, setBusy] = useState<InboxItemId | null>(null)
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export interface IConversation {
|
|||||||
/**
|
/**
|
||||||
* Apply one operation to a pending queue occurrence.
|
* Apply one operation to a pending queue occurrence.
|
||||||
* @param itemId - agent-owned inbox occurrence identity.
|
* @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.
|
* @returns completion; business failures reject.
|
||||||
*/
|
*/
|
||||||
updateQueue(itemId: InboxItemId, action: QueueAction): Promise<void>
|
updateQueue(itemId: InboxItemId, action: QueueAction): Promise<void>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const SID = 's1' as SessionId
|
|||||||
const iid = (id: string): InboxItemId => id as InboxItemId
|
const iid = (id: string): InboxItemId => id as InboxItemId
|
||||||
|
|
||||||
function row(id: string, text: string | null, preview = text ?? '[image]'): QueuedMessage {
|
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 {
|
function snapshotWith(queue: QueuedMessage[]): ConversationSnapshot {
|
||||||
@@ -78,19 +78,6 @@ describe('QueueDock', () => {
|
|||||||
expect(container.innerHTML).toBe('')
|
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(<QueueDock {...kitFor(snap)} useSession={source.useSession} />)
|
|
||||||
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', () => {
|
it('renders active actions and disables editing for mixed-content rows', () => {
|
||||||
const snap = snapshotWith([
|
const snap = snapshotWith([
|
||||||
row('i-1', '第一条排队消息'),
|
row('i-1', '第一条排队消息'),
|
||||||
|
|||||||
@@ -1171,9 +1171,9 @@ export const EVENT_API: readonly EventApiEntry[] = [
|
|||||||
{
|
{
|
||||||
name: 'agent/inbox/update',
|
name: 'agent/inbox/update',
|
||||||
mode: 'emit',
|
mode: 'emit',
|
||||||
signature: '\'agent/inbox/update\'( this: Scoped<Agent>, agent: Agent, item: InboxItem, action: \'edit\' | \'promote\', ): void',
|
signature: '\'agent/inbox/update\'(this: Scoped<Agent>, agent: Agent, item: InboxItem): 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 */',
|
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 inbox item changed content or position.',
|
summary: 'A still-pending queued item changed content.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'agent/prompt-submit',
|
name: 'agent/prompt-submit',
|
||||||
@@ -1857,7 +1857,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'InboxAction',
|
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',
|
name: 'InboxActionResult',
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# 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:
|
# after editing either side, bring the other along and re-record with:
|
||||||
# pnpm run verify-translation-pairing --write packages/core/agent-loop/README.md
|
# pnpm run verify-translation-pairing --write packages/core/agent-loop/README.md
|
||||||
README.md: 86a5c0525cd0ca40cbeb7e4b1acbcefc417e811d
|
README.md: a1617a1ef871f61157e0d70a06d055168170dced
|
||||||
README.zh.md: b128876afb0931a758b472cbac0aa2423b4f4ab0
|
README.zh.md: 6ba945a41e700331929dabb557802c14256921fb
|
||||||
|
|||||||
@@ -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.
|
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`)
|
### Loop lifecycle (`agent.ts`)
|
||||||
|
|
||||||
|
|||||||
@@ -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`,不开启轮次也不运行模型。
|
统一的 `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`)
|
### 循环生命周期(`agent.ts`)
|
||||||
|
|
||||||
|
|||||||
@@ -137,19 +137,14 @@ export class ReactLoopAgent implements Agent {
|
|||||||
emitAgentEvent(this.loopCtx, this, 'agent/inbox/enqueue', item)
|
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 {
|
updateInbox(id: InboxItemIdType, action: InboxAction): InboxActionResult {
|
||||||
const queuedIndex = this.queued.findIndex(candidate => candidate.item.id === id)
|
const queuedIndex = this.queued.findIndex(candidate => candidate.item.id === id)
|
||||||
const outboxIndex = queuedIndex === -1
|
if (queuedIndex === -1) return 'not-found'
|
||||||
? this.outbox.findIndex(candidate => candidate.item?.id === id)
|
|
||||||
: -1
|
|
||||||
if (queuedIndex === -1 && outboxIndex === -1) return 'not-found'
|
|
||||||
|
|
||||||
const pending = queuedIndex === -1 ? this.outbox[outboxIndex] : this.queued[queuedIndex]
|
const pending = this.queued[queuedIndex]
|
||||||
/* v8 ignore next 2 -- indices are derived from these arrays in this synchronous method. */
|
/* v8 ignore next -- the index was resolved from this array without an async boundary. */
|
||||||
if (pending === undefined || pending.item === undefined) {
|
if (pending === undefined) throw new Error(`agent "${this.id}" queued item disappeared during update`)
|
||||||
throw new Error(`agent "${this.id}" inbox index changed during synchronous update`)
|
|
||||||
}
|
|
||||||
|
|
||||||
/* v8 ignore next -- InboxAction is a closed discriminated union; all variants are covered below. */
|
/* v8 ignore next -- InboxAction is a closed discriminated union; all variants are covered below. */
|
||||||
switch (action.kind) {
|
switch (action.kind) {
|
||||||
@@ -158,42 +153,15 @@ export class ReactLoopAgent implements Agent {
|
|||||||
...pending.item,
|
...pending.item,
|
||||||
message: freezeMessage({ ...pending.item.message, content: action.content }),
|
message: freezeMessage({ ...pending.item.message, content: action.content }),
|
||||||
})
|
})
|
||||||
if (queuedIndex !== -1) {
|
this.queued[queuedIndex] = { ...pending, item }
|
||||||
const queued = this.queued[queuedIndex]
|
emitAgentEvent(this.loopCtx, this, 'agent/inbox/update', item)
|
||||||
/* 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')
|
|
||||||
return 'applied'
|
return 'applied'
|
||||||
}
|
}
|
||||||
case 'remove': {
|
case 'remove': {
|
||||||
if (queuedIndex !== -1) this.queued.splice(queuedIndex, 1)
|
this.queued.splice(queuedIndex, 1)
|
||||||
else this.outbox.splice(outboxIndex, 1)
|
|
||||||
emitAgentEvent(this.loopCtx, this, 'agent/inbox/discard', [pending.item])
|
emitAgentEvent(this.loopCtx, this, 'agent/inbox/discard', [pending.item])
|
||||||
return 'applied'
|
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:
|
default:
|
||||||
/* v8 ignore next -- InboxAction is a closed discriminated union. */
|
/* v8 ignore next -- InboxAction is a closed discriminated union. */
|
||||||
return assertNever(action)
|
return assertNever(action)
|
||||||
|
|||||||
@@ -60,10 +60,9 @@ function inboxText(item: InboxItem): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('addressable inbox operations', () => {
|
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([
|
const adapter = new MockAdapter([
|
||||||
textResponse('first reply'),
|
textResponse('first reply'),
|
||||||
textResponse('promoted reply'),
|
|
||||||
textResponse('edited reply'),
|
textResponse('edited reply'),
|
||||||
])
|
])
|
||||||
const ctx = await harness(adapter)
|
const ctx = await harness(adapter)
|
||||||
@@ -79,13 +78,13 @@ describe('addressable inbox operations', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const pending: InboxItem[] = []
|
const pending: InboxItem[] = []
|
||||||
const updates: { id: string; action: string; text: string }[] = []
|
const updates: { id: string; text: string }[] = []
|
||||||
const discards: string[][] = []
|
const discards: string[][] = []
|
||||||
ctx.on('agent/inbox/enqueue', (subject, item) => {
|
ctx.on('agent/inbox/enqueue', (subject, item) => {
|
||||||
if (subject === agent && inboxText(item) !== 'first') pending.push(item)
|
if (subject === agent && inboxText(item) !== 'first') pending.push(item)
|
||||||
})
|
})
|
||||||
ctx.on('agent/inbox/update', (subject, item, action) => {
|
ctx.on('agent/inbox/update', (subject, item) => {
|
||||||
if (subject === agent) updates.push({ id: item.id, action, text: inboxText(item) })
|
if (subject === agent) updates.push({ id: item.id, text: inboxText(item) })
|
||||||
})
|
})
|
||||||
ctx.on('agent/inbox/discard', (subject, items) => {
|
ctx.on('agent/inbox/discard', (subject, items) => {
|
||||||
if (subject === agent) discards.push(items.map(item => item.id))
|
if (subject === agent) discards.push(items.map(item => item.id))
|
||||||
@@ -95,22 +94,16 @@ describe('addressable inbox operations', () => {
|
|||||||
await admission.promise
|
await admission.promise
|
||||||
send(agent, 'remove me')
|
send(agent, 'remove me')
|
||||||
send(agent, 'edit me')
|
send(agent, 'edit me')
|
||||||
send(agent, 'promote me')
|
expect(pending.map(inboxText)).toEqual(['remove me', 'edit me'])
|
||||||
expect(pending.map(inboxText)).toEqual(['remove me', 'edit me', 'promote me'])
|
|
||||||
|
|
||||||
const remove = pending[0]!
|
const remove = pending[0]!
|
||||||
const edit = pending[1]!
|
const edit = pending[1]!
|
||||||
const promote = pending[2]!
|
|
||||||
expect(agent.updateInbox(edit.id, {
|
expect(agent.updateInbox(edit.id, {
|
||||||
kind: 'edit',
|
kind: 'edit',
|
||||||
content: [{ type: 'text', text: 'edited' }],
|
content: [{ type: 'text', text: 'edited' }],
|
||||||
})).toBe('applied')
|
})).toBe('applied')
|
||||||
expect(agent.updateInbox(remove.id, { kind: 'remove' })).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, text: 'edited' }])
|
||||||
expect(updates).toEqual([
|
|
||||||
{ id: edit.id, action: 'edit', text: 'edited' },
|
|
||||||
{ id: promote.id, action: 'promote', text: 'promote me' },
|
|
||||||
])
|
|
||||||
expect(discards).toEqual([[remove.id]])
|
expect(discards).toEqual([[remove.id]])
|
||||||
|
|
||||||
const idle = waitForIdle(ctx, agent)
|
const idle = waitForIdle(ctx, agent)
|
||||||
@@ -121,11 +114,11 @@ describe('addressable inbox operations', () => {
|
|||||||
.map(event => event.type === 'user/message'
|
.map(event => event.type === 'user/message'
|
||||||
? event.data.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('')
|
? event.data.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('')
|
||||||
: ''))
|
: ''))
|
||||||
.toEqual(['first', 'promote me', 'edited'])
|
.toEqual(['first', 'edited'])
|
||||||
expect(agent.updateInbox(promote.id, { kind: 'remove' })).toBe('not-found')
|
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 adapter = new MockAdapter([textResponse('done')])
|
||||||
const ctx = await harness(adapter)
|
const ctx = await harness(adapter)
|
||||||
const agent = ctx.agentLoop.create(SessionId('steering-inbox-actions'), { provider: 'mock', model: 'mock' })
|
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 pending: InboxItem[] = []
|
||||||
const updates: { id: string; action: string; text: string }[] = []
|
|
||||||
const discards: string[][] = []
|
|
||||||
ctx.on('agent/inbox/enqueue', (subject, item) => {
|
ctx.on('agent/inbox/enqueue', (subject, item) => {
|
||||||
if (subject === agent && item.placement === 'steering') pending.push(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)
|
const idle = waitForIdle(ctx, agent)
|
||||||
send(agent, 'admitted prompt')
|
send(agent, 'admitted prompt')
|
||||||
await entered.promise
|
await entered.promise
|
||||||
agent.steer(createUserMessage({ content: [{ type: 'text', text: 'remove me' }], source: { kind: 'user' } }))
|
agent.steer(createUserMessage({ content: [{ type: 'text', text: 'keep me' }], source: { kind: 'user' } }))
|
||||||
agent.steer(createUserMessage({ content: [{ type: 'text', text: 'edit me' }], source: { kind: 'user' } }))
|
expect(pending.map(inboxText)).toEqual(['keep me'])
|
||||||
agent.steer(createUserMessage({ content: [{ type: 'text', text: 'promote me' }], source: { kind: 'user' } }))
|
|
||||||
expect(pending.map(inboxText)).toEqual(['remove me', 'edit me', 'promote me'])
|
|
||||||
|
|
||||||
const remove = pending[0]!
|
const steering = pending[0]!
|
||||||
const edit = pending[1]!
|
expect(agent.updateInbox(steering.id, {
|
||||||
const promote = pending[2]!
|
|
||||||
expect(agent.updateInbox(edit.id, {
|
|
||||||
kind: 'edit',
|
kind: 'edit',
|
||||||
content: [{ type: 'text', text: 'edited' }],
|
content: [{ type: 'text', text: 'edited' }],
|
||||||
})).toBe('applied')
|
})).toBe('not-found')
|
||||||
expect(agent.updateInbox(remove.id, { kind: 'remove' })).toBe('applied')
|
expect(agent.updateInbox(steering.id, { kind: 'remove' })).toBe('not-found')
|
||||||
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]])
|
|
||||||
|
|
||||||
decision.resolve({ kind: 'allow' })
|
decision.resolve({ kind: 'allow' })
|
||||||
await idle
|
await idle
|
||||||
@@ -179,7 +154,7 @@ describe('addressable inbox operations', () => {
|
|||||||
.map(event => event.type === 'steering/message'
|
.map(event => event.type === 'steering/message'
|
||||||
? event.data.message.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('')
|
? event.data.message.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('')
|
||||||
: ''))
|
: ''))
|
||||||
.toEqual(['promote me', 'edited'])
|
.toEqual(['keep me'])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# 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:
|
# after editing either side, bring the other along and re-record with:
|
||||||
# pnpm run verify-translation-pairing --write packages/core/agent/README.md
|
# pnpm run verify-translation-pairing --write packages/core/agent/README.md
|
||||||
README.md: e71d3454d22fa829af9617083c5e68586970c70a
|
README.md: 6bd5279ace93b6d2569833be6f102c854105c2eb
|
||||||
README.zh.md: 2dac18e50ab6b4acfc899013313e5bb0069ab36c
|
README.zh.md: cdbb0c0b70124e0037a0f7a7a03ddedf1e3b78d3
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ Turn and step boundaries and the model token stream are durable `session/event`
|
|||||||
The handle every plugin programs against:
|
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.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.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.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.
|
- `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.
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ Agent *创建* 由实现 `AgentFactory` 的插件(`dsh-agent-loop`)提供,
|
|||||||
每个插件面向的 handle:
|
每个插件面向的 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.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.followup(input)`:`send()` 的 `next-turn`/wakeup 预设:排队一个普通后续轮次并唤醒驱动器。
|
||||||
- `agent.steer(input)`:`next-step`/wakeup 预设:提示词接纳期间或轮次打开时,为下一个安全边界暂存 steering,且不分发 `agent/prompt-submit`;该接收窗口之外则委托给会唤醒的后续轮次。接纳失败会保留暂存的 steering,以供重试或之后获准的提示词使用,而取消或 dispose 可能丢弃它。
|
- `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/*` 事件。
|
- `agent.inject(input)`:`next-step`/不唤醒预设:追加面向模型的上下文而不运行模型;下一次请求会看到一条逐字的 user role 消息,其来源由必填的 `input.source` 携带。提示词接纳期间或轮次打开时,注入会在 outbox 中等待下一个安全边界。该接收窗口之外,它会立即追加而不开启轮次;如果接纳结束却未开启轮次,仅含上下文的接纳批次会采用这一回退,而与 steering 一同暂存的上下文则会随其继续待处理。持久化独立地响应 `session/event`。注入不发出 `agent/inbox/*` 事件。
|
||||||
|
|||||||
@@ -50,11 +50,10 @@ export interface InboxItem {
|
|||||||
readonly placement: InboxPlacement
|
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 =
|
export type InboxAction =
|
||||||
| { readonly kind: 'edit'; readonly content: ContentBlock[] }
|
| { readonly kind: 'edit'; readonly content: ContentBlock[] }
|
||||||
| { readonly kind: 'remove' }
|
| { readonly kind: 'remove' }
|
||||||
| { readonly kind: 'promote' }
|
|
||||||
|
|
||||||
/** Result of applying an inbox action at the synchronous ownership boundary. */
|
/** Result of applying an inbox action at the synchronous ownership boundary. */
|
||||||
export type InboxActionResult = 'applied' | 'not-found'
|
export type InboxActionResult = 'applied' | 'not-found'
|
||||||
@@ -180,13 +179,11 @@ export interface Agent {
|
|||||||
send(message: UserMessage, options: SendOptions): void
|
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
|
* the message identity and queue position; removal publishes its terminal
|
||||||
* discard; promotion moves it to the front of its current FIFO and makes a
|
* discard. Steering occurrences and driver-claimed items return `not-found`.
|
||||||
* queued item waking. A driver-claimed item is no longer pending and returns
|
* @param id - independently addressable queued occurrence.
|
||||||
* `not-found`.
|
* @param action - edit or remove operation.
|
||||||
* @param id - independently addressable inbox occurrence.
|
|
||||||
* @param action - edit, remove, or promote operation.
|
|
||||||
* @returns whether the pending occurrence was found and updated.
|
* @returns whether the pending occurrence was found and updated.
|
||||||
*/
|
*/
|
||||||
updateInbox(id: InboxItemId, action: InboxAction): InboxActionResult
|
updateInbox(id: InboxItemId, action: InboxAction): InboxActionResult
|
||||||
@@ -280,21 +277,14 @@ declare module 'cordis' {
|
|||||||
*/
|
*/
|
||||||
'agent/inbox/enqueue'(this: Scoped<Agent>, agent: Agent, item: InboxItem): void
|
'agent/inbox/enqueue'(this: Scoped<Agent>, agent: Agent, item: InboxItem): void
|
||||||
/**
|
/**
|
||||||
* A still-pending inbox item changed content or position. The item id and
|
* A still-pending queued item changed content. The item id, placement, and
|
||||||
* placement remain stable; edit carries the replacement message, while
|
* position remain stable while the event carries the replacement message.
|
||||||
* promote makes this occurrence first in its current FIFO.
|
|
||||||
* @param agent - the owning agent.
|
* @param agent - the owning agent.
|
||||||
* @param item - the complete post-update occurrence.
|
* @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.
|
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||||
* @mode emit
|
* @mode emit
|
||||||
*/
|
*/
|
||||||
'agent/inbox/update'(
|
'agent/inbox/update'(this: Scoped<Agent>, agent: Agent, item: InboxItem): void
|
||||||
this: Scoped<Agent>,
|
|
||||||
agent: Agent,
|
|
||||||
item: InboxItem,
|
|
||||||
action: 'edit' | 'promote',
|
|
||||||
): void
|
|
||||||
/**
|
/**
|
||||||
* The driver claimed one item out of the inbox: a queued item at a turn
|
* 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
|
* boundary, or steering drained between steps. Fires after the item leaves
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ describe('scoped-dispatch invariants', () => {
|
|||||||
'agent/disposed': [agent],
|
'agent/disposed': [agent],
|
||||||
'agent/status': [agent, 'idle'],
|
'agent/status': [agent, 'idle'],
|
||||||
'agent/inbox/enqueue': [agent, item],
|
'agent/inbox/enqueue': [agent, item],
|
||||||
'agent/inbox/update': [agent, item, 'edit'],
|
'agent/inbox/update': [agent, item],
|
||||||
'agent/inbox/dequeue': [agent, item],
|
'agent/inbox/dequeue': [agent, item],
|
||||||
'agent/inbox/discard': [agent, []],
|
'agent/inbox/discard': [agent, []],
|
||||||
'agent/cancel-requested': [agent, { kind: 'user' }],
|
'agent/cancel-requested': [agent, { kind: 'user' }],
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# 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:
|
# after editing either side, bring the other along and re-record with:
|
||||||
# pnpm run verify-translation-pairing --write packages/host/apiproxy/README.md
|
# pnpm run verify-translation-pairing --write packages/host/apiproxy/README.md
|
||||||
README.md: c6f9ffae399b9d88b354b3312e36fe05b2d38a63
|
README.md: e4a29dc288d7279366c2a28ed43573792979aa92
|
||||||
README.zh.md: 59f31e521c5432dcf2ba64972b6b0d0e87988b23
|
README.zh.md: 9f5eb0c4f84eb909d931dda8cbac764bf83aa2fe
|
||||||
|
|||||||
@@ -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`.
|
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()`.
|
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()`.
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
会话模型路由属于会话领域契约。`session.models` 返回选中的提供方/模型/推理(reasoning)目标,以及按提供方分组的建议性模型、精确路由推理元数据和逐提供方查询失败记录。`session.selectModel` 校验由适配器持有的可选推理强度,并替换将在下一提示词组装边界使用的完整目标。目录成员关系不构成校验:适配器可以解析未列出的模型,而不可用路由或不受支持的推理强度会返回 `model-unavailable`。
|
会话模型路由属于会话领域契约。`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()` 中。
|
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()` 中。
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
* (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.
|
* identified message remain visible until every occurrence is claimed.
|
||||||
*/
|
*/
|
||||||
const queuedMirror = new Map<SessionId, InboxItem[]>()
|
const queuedMirror = new Map<SessionId, InboxItem[]>()
|
||||||
@@ -522,7 +522,6 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
|
|||||||
items: items.map(item => ({
|
items: items.map(item => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
message: item.message,
|
message: item.message,
|
||||||
placement: item.placement,
|
|
||||||
})),
|
})),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -531,12 +530,14 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
|
|||||||
const entries = queuedMirror.get(agent.id)
|
const entries = queuedMirror.get(agent.id)
|
||||||
if (entries === undefined) return
|
if (entries === undefined) return
|
||||||
const index = entries.findIndex(entry => entry.id === item.id)
|
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)
|
if (entries.length === 0) queuedMirror.delete(agent.id)
|
||||||
publishQueue(agent.id)
|
publishQueue(agent.id)
|
||||||
}
|
}
|
||||||
const disposers = [
|
const disposers = [
|
||||||
ctx.on('agent/inbox/enqueue', (agent: Agent, item: InboxItem) => {
|
ctx.on('agent/inbox/enqueue', (agent: Agent, item: InboxItem) => {
|
||||||
|
if (item.placement !== 'queued') return
|
||||||
let entries = queuedMirror.get(agent.id)
|
let entries = queuedMirror.get(agent.id)
|
||||||
if (entries === undefined) {
|
if (entries === undefined) {
|
||||||
entries = []
|
entries = []
|
||||||
@@ -545,18 +546,12 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
|
|||||||
entries.push(item)
|
entries.push(item)
|
||||||
publishQueue(agent.id)
|
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)
|
const entries = queuedMirror.get(agent.id)
|
||||||
if (entries === undefined) return
|
if (entries === undefined) return
|
||||||
const index = entries.findIndex(entry => entry.id === item.id)
|
const index = entries.findIndex(entry => entry.id === item.id)
|
||||||
if (index === -1) return
|
if (index === -1) return
|
||||||
entries.splice(index, 1)
|
entries.splice(index, 1, item)
|
||||||
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)
|
|
||||||
}
|
|
||||||
publishQueue(agent.id)
|
publishQueue(agent.id)
|
||||||
}),
|
}),
|
||||||
ctx.on('agent/inbox/dequeue', (agent: Agent, item: InboxItem) => {
|
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
|
if (entries === undefined) return
|
||||||
const ids = new Set(items.map(item => item.id))
|
const ids = new Set(items.map(item => item.id))
|
||||||
const kept = entries.filter(entry => !ids.has(entry.id))
|
const kept = entries.filter(entry => !ids.has(entry.id))
|
||||||
|
if (kept.length === entries.length) return
|
||||||
if (kept.length === 0) queuedMirror.delete(agent.id)
|
if (kept.length === 0) queuedMirror.delete(agent.id)
|
||||||
else queuedMirror.set(agent.id, kept)
|
else queuedMirror.set(agent.id, kept)
|
||||||
publishQueue(agent.id)
|
publishQueue(agent.id)
|
||||||
@@ -1540,7 +1536,6 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
|
|||||||
items: items.map(item => ({
|
items: items.map(item => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
message: item.message,
|
message: item.message,
|
||||||
placement: item.placement,
|
|
||||||
})),
|
})),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ export const muxFrameSchema = z.discriminatedUnion('type', [
|
|||||||
items: z.array(z.object({
|
items: z.array(z.object({
|
||||||
id: inboxItemIdSchema,
|
id: inboxItemIdSchema,
|
||||||
message: messageSchema,
|
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,
|
// value stays wide: it already passed its unit's own schema on the host,
|
||||||
|
|||||||
@@ -32,14 +32,12 @@ export type ToolEventView =
|
|||||||
| { for: 'call'; view: ToolCallView }
|
| { for: 'call'; view: ToolCallView }
|
||||||
| { for: 'result'; view: ToolResultView }
|
| { 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 {
|
export interface QueuedInboxItem {
|
||||||
/** Agent-owned occurrence identity used by queue mutations. */
|
/** Agent-owned occurrence identity used by queue mutations. */
|
||||||
id: InboxItemId
|
id: InboxItemId
|
||||||
/** Complete pending message; it is not durable until the Agent claims it. */
|
/** Complete pending message; it is not durable until the Agent claims it. */
|
||||||
message: Message
|
message: Message
|
||||||
/** Acceptance-time FIFO classification. */
|
|
||||||
placement: 'queued' | 'steering'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Streaming face of the contract: the two SSE stream openers (mux + host). */
|
/** 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/requested'; sessionId: SessionId; questions: AskUserQuestionItem[] }
|
||||||
| { type: 'question/resolved'; sessionId: SessionId; questionRpcId: RpcId; outcome: 'answered' | 'cancelled' }
|
| { 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
|
* discard. Pending work is not model-visible and therefore has no durable
|
||||||
* session event; the whole snapshot makes edit, reorder, deletion, cancel,
|
* session event; the whole snapshot makes edit, deletion, cancel, and
|
||||||
* and reconnect converge through one authoritative signal.
|
* reconnect converge through one authoritative signal. Pending steering is
|
||||||
|
* outside this Web queue projection.
|
||||||
*/
|
*/
|
||||||
| { type: 'session/queue'; sessionId: SessionId; items: QueuedInboxItem[] }
|
| { type: 'session/queue'; sessionId: SessionId; items: QueuedInboxItem[] }
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -225,7 +225,6 @@ export const sessionUpdateQueueRequestSchema = z.object({
|
|||||||
action: z.discriminatedUnion('kind', [
|
action: z.discriminatedUnion('kind', [
|
||||||
z.object({ kind: z.literal('edit'), content: z.array(contentBlockSchema) }),
|
z.object({ kind: z.literal('edit'), content: z.array(contentBlockSchema) }),
|
||||||
z.object({ kind: z.literal('remove') }),
|
z.object({ kind: z.literal('remove') }),
|
||||||
z.object({ kind: z.literal('promote') }),
|
|
||||||
]),
|
]),
|
||||||
}) as unknown as z.ZodType<RequestPayload<'session.updateQueue'>>
|
}) as unknown as z.ZodType<RequestPayload<'session.updateQueue'>>
|
||||||
|
|
||||||
|
|||||||
@@ -129,7 +129,6 @@ export interface SessionModels {
|
|||||||
export type QueueAction =
|
export type QueueAction =
|
||||||
| { kind: 'edit'; content: ContentBlock[] }
|
| { kind: 'edit'; content: ContentBlock[] }
|
||||||
| { kind: 'remove' }
|
| { kind: 'remove' }
|
||||||
| { kind: 'promote' }
|
|
||||||
|
|
||||||
/** Session list entry (v1 builds no index: list does readdir+stat). */
|
/** Session list entry (v1 builds no index: list does readdir+stat). */
|
||||||
export interface SessionSummary {
|
export interface SessionSummary {
|
||||||
@@ -237,9 +236,7 @@ export interface SessionsApi {
|
|||||||
Promise<RpcResponse<{ accepted: true; command?: { kind: 'success'; text?: string } }>>
|
Promise<RpcResponse<{ accepted: true; command?: { kind: 'success'; text?: string } }>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Edits, removes, or promotes one pending inbox occurrence. Promotion means
|
* Edits or removes one pending queued occurrence.
|
||||||
* 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.
|
|
||||||
*/
|
*/
|
||||||
updateQueue(request: RpcRequest<{ sessionId: SessionId; itemId: InboxItemId; action: QueueAction }>):
|
updateQueue(request: RpcRequest<{ sessionId: SessionId; itemId: InboxItemId; action: QueueAction }>):
|
||||||
Promise<RpcResponse<{ accepted: true }>>
|
Promise<RpcResponse<{ accepted: true }>>
|
||||||
|
|||||||
@@ -295,7 +295,7 @@ describe('session.updateQueue', () => {
|
|||||||
payload: {
|
payload: {
|
||||||
sessionId: agent.id,
|
sessionId: agent.id,
|
||||||
itemId: InboxItemId('present'),
|
itemId: InboxItemId('present'),
|
||||||
action: { kind: 'promote' },
|
action: { kind: 'edit', content: [{ type: 'text', text: 'edited' }] },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
expect(expectOk(applied)).toEqual({ accepted: true })
|
expect(expectOk(applied)).toEqual({ accepted: true })
|
||||||
@@ -309,7 +309,7 @@ describe('session.updateQueue', () => {
|
|||||||
})
|
})
|
||||||
expect(expectErr(missing)).toMatchObject({ code: 'queue-item-not-found' })
|
expect(expectErr(missing)).toMatchObject({ code: 'queue-item-not-found' })
|
||||||
expect(seen).toEqual([
|
expect(seen).toEqual([
|
||||||
{ id: 'present', action: { kind: 'promote' } },
|
{ id: 'present', action: { kind: 'edit', content: [{ type: 'text', text: 'edited' }] } },
|
||||||
{ id: 'claimed', action: { kind: 'remove' } },
|
{ id: 'claimed', action: { kind: 'remove' } },
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@@ -322,8 +322,8 @@ describe('session/queue frames', () => {
|
|||||||
const agent = stubAgent(ctx)
|
const agent = stubAgent(ctx)
|
||||||
const live = new AbortController()
|
const live = new AbortController()
|
||||||
const liveStream = api.events.mux({ rpcId: RpcId('t-mux-live'), payload: {} }, live.signal)
|
const liveStream = api.events.mux({ rpcId: RpcId('t-mux-live'), payload: {} }, live.signal)
|
||||||
// subscribed baseline + 2 queue snapshots
|
// subscribed baseline + one queued snapshot; pending steering stays off this wire.
|
||||||
const liveCollected = collect<MuxFrame>(liveStream, 3, live)
|
const liveCollected = collect<MuxFrame>(liveStream, 2, live)
|
||||||
|
|
||||||
const queued = inboxItem('i-1', inboxMessage('m-1', 'queued prompt'), 'queued')
|
const queued = inboxItem('i-1', inboxMessage('m-1', 'queued prompt'), 'queued')
|
||||||
const steering = inboxItem('i-2', inboxMessage('m-2', 'steering prompt'), 'steering')
|
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')
|
const liveFrames = (await liveCollected).filter(f => f.type === 'session/queue')
|
||||||
expect(liveFrames).toEqual([
|
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.
|
// A fresh mux connection replays only the current authoritative snapshot.
|
||||||
const replay = new AbortController()
|
const replay = new AbortController()
|
||||||
const replayFrames = await collect<MuxFrame>(
|
const replayFrames = await collect<MuxFrame>(
|
||||||
api.events.mux({ rpcId: RpcId('t-mux-replay'), payload: {} }, replay.signal), 2, replay)
|
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 ctx = await harness()
|
||||||
const api = createApiProxy(ctx, DEFAULTS)
|
const api = createApiProxy(ctx, DEFAULTS)
|
||||||
const agent = stubAgent(ctx)
|
const agent = stubAgent(ctx)
|
||||||
const abort = new AbortController()
|
const abort = new AbortController()
|
||||||
const collected = collect<MuxFrame>(
|
const collected = collect<MuxFrame>(
|
||||||
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 first = inboxItem('i-a', inboxMessage('m-a', 'a'), 'queued')
|
||||||
const second = inboxItem('i-b', inboxMessage('m-b', 'b'), 'queued')
|
const second = inboxItem('i-b', inboxMessage('m-b', 'b'), 'queued')
|
||||||
const edited = inboxItem('i-b', inboxMessage('m-b', 'b edited'), '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, first)
|
||||||
ctx.emit('agent/inbox/enqueue', agent, second)
|
ctx.emit('agent/inbox/enqueue', agent, second)
|
||||||
ctx.emit('agent/inbox/update', agent, edited, 'edit')
|
ctx.emit('agent/inbox/update', agent, edited)
|
||||||
ctx.emit('agent/inbox/update', agent, edited, 'promote')
|
|
||||||
ctx.emit('agent/inbox/dequeue', agent, edited)
|
ctx.emit('agent/inbox/dequeue', agent, edited)
|
||||||
|
|
||||||
const frames = (await collected).filter(frame => frame.type === 'session/queue')
|
const frames = (await collected).filter(frame => frame.type === 'session/queue')
|
||||||
expect(frames.map(frame => frame.items)).toEqual([
|
expect(frames.map(frame => frame.items)).toEqual([
|
||||||
[first],
|
[{ id: first.id, message: first.message }],
|
||||||
[first, second],
|
[{ id: first.id, message: first.message }, { id: second.id, message: second.message }],
|
||||||
[first, edited],
|
[{ id: first.id, message: first.message }, { id: edited.id, message: edited.message }],
|
||||||
[edited, first],
|
[{ id: first.id, message: first.message }],
|
||||||
[first],
|
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ describe('sessions domain schemas', () => {
|
|||||||
sessionId: 's1', itemId: 'i1', action: { kind: 'remove' },
|
sessionId: 's1', itemId: 'i1', action: { kind: 'remove' },
|
||||||
}).action.kind).toBe('remove')
|
}).action.kind).toBe('remove')
|
||||||
expect(() => sessionUpdateQueueRequestSchema.parse({
|
expect(() => sessionUpdateQueueRequestSchema.parse({
|
||||||
sessionId: 's1', itemId: '', action: { kind: 'promote' },
|
sessionId: 's1', itemId: 'i1', action: { kind: 'promote' },
|
||||||
})).toThrow()
|
})).toThrow()
|
||||||
expect(sessionCancelValueSchema.parse({ accepted: true }).accepted).toBe(true)
|
expect(sessionCancelValueSchema.parse({ accepted: true }).accepted).toBe(true)
|
||||||
expect(sessionUpdateQueueValueSchema.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/requested', sessionId: 's', questions: [{ id: 'q', question: 'Q?', options: [{ label: 'L' }], multiSelect: true }] },
|
||||||
{ type: 'question/resolved', sessionId: 's', questionRpcId: 'r', outcome: 'answered' },
|
{ type: 'question/resolved', sessionId: 's', questionRpcId: 'r', outcome: 'answered' },
|
||||||
{ type: 'session/queue', sessionId: 's', items: [
|
{ 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: 'i1', message: { id: 'm1', role: 'user', content: [{ type: 'text', text: 'queued prompt' }], source: { kind: 'user', rpcId: 'r9' } } },
|
||||||
{ id: 'i2', message: { id: 'm2', role: 'user', content: [{ type: 'text', text: 'steer' }], source: { kind: 'user' } }, placement: 'steering' },
|
|
||||||
] },
|
] },
|
||||||
{ type: 'session/projection', sessionId: 's', key: 'todos', value: [{ content: 'x', status: 'pending' }], seq: 7 },
|
{ type: 'session/projection', sessionId: 's', key: 'todos', value: [{ content: 'x', status: 'pending' }], seq: 7 },
|
||||||
{ type: 'stream/error', error: { code: 'internal', message: 'm', details: {} } },
|
{ 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', () => {
|
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: '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: '', message: {} }] })).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: 'i', message: { id: 'm', role: 'user', content: [], source: {} } }] })).toThrow()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('accepts every host frame branch', () => {
|
it('accepts every host frame branch', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user