Merge remote-tracking branch 'origin/master' into xtr/react-loop-simplification
# Conflicts: # .agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.i18n.yaml # .agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.md # .agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.zh.md # .agents/notes/implemented/feature/2026-07-21-tui-skill-slash-command.i18n.yaml # .agents/notes/implemented/feature/2026-07-21-tui-skill-slash-command.md # .agents/notes/implemented/feature/2026-07-21-tui-skill-slash-command.zh.md # .agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.i18n.yaml # .agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.md # .agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.zh.md # docs/architecture.i18n.yaml # docs/cordis-catalog/events.md # docs/cordis-catalog/services.md # docs/core-data-structures/core.i18n.yaml # docs/core-data-structures/core.md # docs/core-data-structures/core.zh.md # docs/defensive-patterns.i18n.yaml # packages/client/runtime/src/client/sessions/session.ts # packages/client/runtime/tests/queue-store.spec.ts # packages/context/time-context/tests/time-context.spec.ts # packages/context/workspace-context/tests/workspace-context.spec.ts # packages/cordis/tool-cordis/src/api-catalog.ts # packages/core/agent-loop/README.i18n.yaml # packages/core/agent-loop/README.md # packages/core/agent-loop/README.zh.md # packages/core/agent-loop/src/agent.ts # packages/core/agent/README.i18n.yaml # packages/core/agent/README.md # packages/core/agent/README.zh.md # packages/core/agent/src/types.ts # packages/core/agent/tests/agent.spec.ts # packages/core/scope/src/scoped-events.generated.ts # packages/goal/command-goal/tests/command-goal.spec.ts # packages/goal/goal-session/src/index.ts # packages/goal/goal-session/tests/goal-session.spec.ts # packages/goal/goal/tests/goal.spec.ts # packages/goal/goal/tests/projection.spec.ts # packages/goal/tool-goal/tests/tool-goal.spec.ts # packages/host/apiproxy/src/api-proxy.ts # packages/host/apiproxy/src/api/events.schema.ts # packages/host/apiproxy/src/api/events.ts # packages/host/apiproxy/tests/api-proxy-workspace.spec.ts # packages/llm/llm/README.i18n.yaml # packages/llm/llm/README.zh.md # packages/llm/llm/src/index.ts # packages/pty/pty-local/tests/index.spec.ts # packages/pty/pty-local/tests/local.spec.ts # packages/pty/pty/tests/service.spec.ts # packages/pty/tool-pty/tests/loader-composition.spec.ts # packages/pty/tool-pty/tests/tools.spec.ts # packages/skill/tool-skill/tests/tool-skill.spec.ts # packages/tasks/tasks-local/tests/tasks.spec.ts # packages/ui/tui/src/index.ts # packages/ui/tui/tests/harness.ts # packages/ui/tui/tests/tui.spec.ts # scripts/gen-cordis-catalog.ts # scripts/type-equiv.manifest.json
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.md
|
||||
2026-07-17-one-send-one-turn.md: 4787a25042f3b002d6c247202d5614a0f7bfa673
|
||||
2026-07-17-one-send-one-turn.zh.md: c44c99771b22f7ecb1b802f8758a103d2774a140
|
||||
2026-07-17-one-send-one-turn.md: 3ae43f137206f25bdbc563875c17e24211f17d6b
|
||||
2026-07-17-one-send-one-turn.zh.md: 5ccdb2192048ecf795415bcd427f967df6a609fb
|
||||
|
||||
@@ -6,7 +6,7 @@ English | [中文](2026-07-17-one-send-one-turn.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Suppose a caller submits message A and then message B with two `Agent.followup()` calls. Implicit batching can put A and B in one turn simply because both are waiting when the driver reads its queue. The caller made two calls, but the loop silently turns them into one unit of work.
|
||||
Suppose a caller submits message A and then message B with two `Agent.send()` calls. Implicit batching can put A and B in one turn simply because both are waiting when the driver reads its queue. The caller made two calls, but the loop silently turns them into one unit of work.
|
||||
|
||||
That grouping depends on timing rather than caller intent. Calls from one synchronous stack, neighboring microtasks, event listeners, and model callbacks could be grouped differently even though every caller used the same API.
|
||||
|
||||
@@ -14,15 +14,15 @@ This grouping changes behavior, not just the number of model calls. One ordinary
|
||||
|
||||
## Decision
|
||||
|
||||
The rule is simple: each successful `followup()` creates one independent FIFO queue item. If that item runs, it is the only ordinary message in its turn. An item can be dropped before it starts, so the precise guarantee is at most one turn rather than exactly one; two follow-ups are never silently combined.
|
||||
The rule is simple: each successful `send()` creates one independent FIFO queue item. If that item runs, it is the only ordinary message in its turn. An item can be dropped before it starts, so the precise guarantee is at most one turn rather than exactly one; two sends are never silently combined.
|
||||
|
||||
Before enqueueing an item, `followup()` checks the agent state and makes a detached, deeply frozen snapshot of the content and resolved source. After enqueueing it, the agent publishes `agent/queued`.
|
||||
Before enqueueing an item, `send()` checks the agent state and accepts an already identified, deeply frozen message. It mints an occurrence-local `InboxItemId` and publishes `agent/inbox/enqueue`; the pending occurrence remains addressable under the [addressable queue operations](../feature/2026-07-29-addressable-queue-operations.md) decision until the driver claims or discards it.
|
||||
|
||||
If messages A and B are both processed, B's turn starts only after A records `turn/end` and A's durability checkpoint settles. B's request therefore sees whatever closed result A left in the same session log. A checkpoint error is reported, but settlement only releases this ordering barrier; it does not make a failed write durable. Broad `cancel()`, disposal, or a failure before `turn/start` can instead discard an unstarted item without opening an empty turn.
|
||||
|
||||
Prompt admission decides one message at a time before a turn opens. An allowed prompt becomes that turn's `user/message`; a blocked prompt is discarded without opening a turn or writing session history. Mixed-batch and all-blocked-batch branches do not exist.
|
||||
|
||||
The no-batching rule applies only to ordinary `followup()`. Running `steer()` puts input in the outbox. While a turn remains open, the loop records that input at the next step boundary and steering makes another step the default. A failure before that boundary leaves the steering staged without waking the agent; a request-error retry action or a later prompt takes it, while cancellation or disposal can discard it. When the agent is idle, `steer()` creates an independent ordinary queue item.
|
||||
The no-batching rule applies only to ordinary `send()`. Running `steer()` puts input in the outbox. While a turn remains open, the loop records that input at the next step boundary and steering makes another step the default. A failure before that boundary leaves the steering staged without waking the agent; a request-error retry action or a later prompt takes it, while cancellation or disposal can discard it. When the agent is idle, `steer()` delegates to `send()`, so it creates an independent ordinary queue item.
|
||||
|
||||
`inject()` continues to add model-facing context without submitting an ordinary message. During a turn it waits in the outbox for a safe step boundary; while idle it appends a `user/message` directly, without opening a turn or running the model. Persistence owns the resulting eager drain. `cancel()` remains a whole-agent operation that can clear all unstarted ordinary and steering input and abort the current step. `status` and `whenIdle()` also describe the whole agent, not one message. Several one-message turns can share one `running` interval, so `running` does not prove that a turn is open.
|
||||
|
||||
@@ -40,6 +40,6 @@ The no-batching rule applies only to ordinary `followup()`. Running `steer()` pu
|
||||
|
||||
## Consequences
|
||||
|
||||
Ordinary turn boundaries are predictable: messages A and B stay separate, and B runs only after A has closed and reached its checkpoint. Callers still do not receive a per-send completion or cancellation handle; broad cancellation can discard the entire unstarted tail, while status and quiescence remain agent-wide observations.
|
||||
Ordinary turn boundaries are predictable: messages A and B stay separate, and B runs only after A has closed and reached its checkpoint. Callers still do not receive a per-send completion handle; a pending occurrence can be removed through its live `InboxItemId`, broad cancellation can discard the entire unstarted tail, and status and quiescence remain agent-wide observations.
|
||||
|
||||
The trade-off is more model requests and more checkpoints. A busy queue can take longer to drain and can grow under sustained producers. Ordinary-send batching returns only through an explicit, measured contract.
|
||||
|
||||
@@ -6,7 +6,7 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
假设调用方连续两次调用 `Agent.followup()`,先提交消息 A,再提交消息 B。隐式批处理可能只因为驱动器读取队列时两条消息都在等待,就把 A、B 放进同一个轮次。调用方明明调用了两次,agent loop(智能体循环)却悄悄把它们变成一个工作单元。
|
||||
假设调用方连续两次调用 `Agent.send()`,先提交消息 A,再提交消息 B。隐式批处理可能只因为驱动器读取队列时两条消息都在等待,就把 A、B 放进同一个轮次。调用方明明调用了两次,agent loop(智能体循环)却悄悄把它们变成一个工作单元。
|
||||
|
||||
这种分组取决于运行时机,而不是调用方的意图。因此,即使所有调用方使用相同 API,来自同一个同步调用栈、相邻微任务、事件监听器和模型回调的调用也可能产生不同分组。
|
||||
|
||||
@@ -14,15 +14,15 @@ Status: implemented
|
||||
|
||||
## 决策
|
||||
|
||||
规则很简单:一次成功的 `followup()` 创建一个独立的 FIFO 队列项。该队列项如果运行,就是所在轮次中唯一的普通消息。队列项可能在启动前被丢弃,因此精确保证是最多一个轮次,而不是必定一个轮次;两次 follow-up 绝不会被悄悄合并。
|
||||
规则很简单:一次成功的 `send()` 创建一个独立的 FIFO 队列项。该队列项如果运行,就是所在轮次中唯一的普通消息。队列项可能在启动前被丢弃,因此精确保证是最多一个轮次,而不是必定一个轮次;两次 send 绝不会被悄悄合并。
|
||||
|
||||
队列项入队之前,`followup()` 会检查 agent 状态,并为内容和解析后的来源创建一份脱离调用方对象、经过深度冻结的快照。队列项入队之后,agent 发布 `agent/queued`。
|
||||
队列项入队之前,`send()` 会检查 agent 状态,并接受已有标识且经过深度冻结的消息。它会铸造一个仅属于本次入队的 `InboxItemId`,并发布 `agent/inbox/enqueue`;根据[可寻址队列操作](../feature/2026-07-29-addressable-queue-operations.md)决策,在驱动器认领或丢弃该项之前,这次待处理入队始终可以被寻址。
|
||||
|
||||
如果消息 A、B 都进入处理,B 的轮次只能在 A 记录 `turn/end` 且 A 的持久性检查点处理结束后开始。因此,B 的请求能看到 A 在同一会话日志中留下的已关闭结果。检查点错误会照常报告,但处理结束只表示解除这道顺序屏障,不表示失败的写入已经持久化。广义 `cancel()`、dispose(资源释放)或 `turn/start` 之前的失败也可能丢弃尚未启动的队列项,而不打开一个空轮次。
|
||||
|
||||
提示词准入会在轮次打开前,每次只决定一条消息。获准提示词成为该轮次的 `user/message`;被阻止的提示词会被丢弃,不打开轮次,也不写入会话历史。实现中不存在混合批次或全阻止批次分支。
|
||||
|
||||
上述不合批规则只适用于普通 `followup()`。agent 运行时,`steer()` 会把输入放入 outbox。只要当前轮次仍然打开,agent loop 就会在下一个步骤边界记录该输入,而 steering(中途引导)会默认让循环再执行一个步骤。在到达该边界前发生失败,会让 steering 保持暂存且不唤醒 agent;请求错误的重试动作或后续提示词会取走它,而取消或 dispose 可以将其丢弃。agent 空闲时,`steer()` 会创建一个独立的普通队列项。
|
||||
上述不合批规则只适用于普通 `send()`。agent 运行时,`steer()` 会把输入放入 outbox。只要当前轮次仍然打开,agent loop 就会在下一个步骤边界记录该输入,而 steering(中途引导)会默认让循环再执行一个步骤。在到达该边界前发生失败,会让 steering 保持暂存且不唤醒 agent;请求错误的重试动作或后续提示词会取走它,而取消或 dispose 可以将其丢弃。agent 空闲时,`steer()` 会委托给 `send()`,因此创建一个独立的普通队列项。
|
||||
|
||||
`inject()` 继续添加面向模型的上下文,而不提交普通消息。轮次打开时,该上下文会留在 outbox 中,等待安全的步骤边界;agent 空闲时,系统会直接追加一条 `user/message`,既不打开轮次,也不运行模型。持久化层独立负责由此产生的即时排空。`cancel()` 仍是面向整个 agent 的操作,可以清空所有尚未启动的普通输入和 steering,并中止当前步骤。`status` 和 `whenIdle()` 描述的也是整个 agent,而不是某一条消息。多个单消息轮次可以共用一个 `running` 区间,因此 `running` 不表示轮次一定处于打开状态。
|
||||
|
||||
@@ -40,6 +40,6 @@ Status: implemented
|
||||
|
||||
## 后果
|
||||
|
||||
普通轮次的边界可预测:消息 A、B 始终分开,B 只能在 A 关闭并到达检查点后运行。调用方仍然拿不到逐次 send 的完成或取消句柄;广义取消可以丢弃整个尚未启动的队尾,状态和静止性也仍是面向整个 agent 的观察。
|
||||
普通轮次的边界可预测:消息 A、B 始终分开,B 只能在 A 关闭并到达检查点后运行。调用方仍然拿不到逐次 send 的完成句柄;待处理项可通过其仍有效的 `InboxItemId` 移除,广义取消可以丢弃整个尚未启动的队尾,而状态与完全停稳仍是面向整个 agent 的观察。
|
||||
|
||||
代价是模型请求和检查点都会增加。繁忙队列可能需要更长时间才能清空;如果生产方持续提交消息,队列也可能增长。只有建立显式且经过测量的契约后,才能重新引入普通 send 批处理。
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-07-28-local-json-tree-renderer.md
|
||||
2026-07-28-local-json-tree-renderer.md: 5e8e6d6968319ada53e65a48e9e03b297dc0141c
|
||||
2026-07-28-local-json-tree-renderer.zh.md: 221880391da05551d05ad9e3dadb909e2e74fa68
|
||||
@@ -0,0 +1,32 @@
|
||||
# Agent Note: Local JSON tree renderer
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-28-local-json-tree-renderer.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The read-only JSON inspector used by the [trajectory ledger](../feature/2026-07-27-trajectory-inspection-ledger.md) needs compact object and array previews, explicit array paths for copy actions, fixed-open and collapsible root modes, and keyboard navigation. `react-json-view-lite` exposes neither custom node rendering nor row identity, so satisfying those requirements through that dependency requires a package-manager patch against compiled distribution files and DOM traversal that reconstructs data paths from visible labels. The patch behaves as an untyped fork while its source maps and upstream source remain unchanged.
|
||||
|
||||
## Decision
|
||||
|
||||
`JsonTree` owns its recursive presentation in `dsh-client-ui-primitives`.
|
||||
|
||||
- Each rendered row receives its value and property path directly. Object keys and array indexes extend that path during recursion, so copy actions never recover application data from rendered DOM text.
|
||||
- Expandable rows render the compact preview locally and mount child rows only while expanded. `expandTopLevel` selects between a fixed-open bracket frame and a collapsible root node without changing the public component contract.
|
||||
- The tree keeps one tabbable expander among visible nodes. Pointer activation claims that tab stop; Up and Down move it cyclically, while Left and Right collapse or expand the focused node.
|
||||
- `react-json-view-lite` is not a package dependency and has no pnpm patch. Focused component tests pin previews, expansion, keyboard focus, and array copy paths.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep the distribution patch.** Rejected because the application-specific renderer and array identity contract would remain hidden in generated third-party files, and every dependency update would require reviewing a fork without matching source maps.
|
||||
|
||||
**Use the upstream renderer without previews.** Rejected because `{…}` and `[…]` discard the compact payload context that the trajectory inspector uses for scanning.
|
||||
|
||||
**Inject previews and row metadata after render.** Rejected because effects or mutation observers would depend on the same private DOM structure while splitting one row between React ownership and imperative mutation.
|
||||
|
||||
**Adopt a larger JSON viewer.** Rejected because editing, search, and theme systems are outside the current read-only contract; the added dependency surface would not remove the inspector-specific copy and layout code.
|
||||
|
||||
## Consequences
|
||||
|
||||
The JSON inspector has one source-level owner, explicit data flow, accurate array paths, and no patched dependency. The package now owns recursive rendering, expansion state, ARIA tree structure, and roving focus behavior, so changes to those semantics require focused component coverage. The implementation remains intentionally read-only and limited to the preview, navigation, and copy behavior used by current consumers.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Agent Note:本地 JSON 树渲染器
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-28-local-json-tree-renderer.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
[轨迹检查记录表](../feature/2026-07-27-trajectory-inspection-ledger.md)使用的只读 JSON 检查器需要提供紧凑的对象和数组预览、供复制操作使用的明确数组路径、固定展开与可折叠两种根节点模式,以及键盘导航。`react-json-view-lite` 既不提供自定义节点渲染,也不提供行标识;要通过该依赖满足这些要求,就必须使用包管理器为编译后的发布文件打补丁,并遍历 DOM,从可见标签中还原数据路径。该补丁实际上相当于一个不受类型约束的 fork,但其源码映射与上游源码均未同步修改。
|
||||
|
||||
## 决策
|
||||
|
||||
`dsh-client-ui-primitives` 中的 `JsonTree` 自行负责递归呈现。
|
||||
|
||||
- 每个渲染行都直接接收自身的值和属性路径。递归时,对象键和数组索引会附加到路径末尾,因此复制操作无需再从 DOM 渲染文本中反向还原应用数据。
|
||||
- 可展开行在本地渲染紧凑预览,仅在展开时挂载子行。`expandTopLevel` 可选择固定展开的括号框架或可折叠根节点,而不改变组件的公开契约。
|
||||
- 树中所有可见节点仅保留一个可通过 Tab 键聚焦的展开控件。使用指针激活控件后,该控件会成为 Tab 键焦点位置;上、下方向键循环移动焦点,左、右方向键折叠或展开当前焦点所在节点。
|
||||
- `react-json-view-lite` 不在 `dsh-client-ui-primitives` 的依赖项中,也没有 pnpm 补丁。针对性组件测试锁定预览、展开、键盘焦点和数组复制路径。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
**保留发布文件补丁。** 不予采纳:应用专用的渲染逻辑和数组标识契约仍会隐藏在生成的第三方文件中;每次更新依赖,都必须评审一个没有配套源码映射的 fork。
|
||||
|
||||
**直接使用不提供预览的上游渲染器。** 不予采纳:`{…}` 和 `[…]` 会丢失轨迹检查器扫读时所需的紧凑载荷上下文。
|
||||
|
||||
**渲染后注入预览和行元数据。** 不予采纳:Effect 或 MutationObserver 仍会依赖同一套私有 DOM 结构,同时使 React 和命令式变更机制分别负责同一行的不同部分。
|
||||
|
||||
**采用更大型的 JSON 查看器。** 不予采纳:编辑、搜索与主题系统不在当前只读契约范围内;扩大依赖范围也无法去除检查器专用的复制和布局代码。
|
||||
|
||||
## 后果
|
||||
|
||||
JSON 检查器在源码层由单一实现负责,具有显式数据流和准确的数组路径,且没有经过补丁修改的依赖。`dsh-client-ui-primitives` 负责递归渲染、展开状态、ARIA 树结构和焦点循环移动行为,因此修改这些语义时必须提供针对性组件测试。实现有意保持只读,仅包含当前消费方使用的预览、导航与复制行为。
|
||||
Reference in New Issue
Block a user