Merge remote-tracking branch 'origin/master' into dshw/pr-deepseek-harness-deepseek-harness-2300

This commit is contained in:
_Kerman
2026-08-19 15:39:30 +08:00
175 changed files with 4332 additions and 551 deletions
@@ -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/architecture/2026-08-10-cancelled-stream-prefix-finalize.md
2026-08-10-cancelled-stream-prefix-finalize.md: 0cae25b786922fba8204d68ca9c0a669e43d76a0
2026-08-10-cancelled-stream-prefix-finalize.zh.md: e961ea6a51f74dcc244e4ad8970eae4cbe4c9a6c
@@ -0,0 +1,39 @@
# Agent Note: Cancelled streams finalize their delivered prefix
Status: implemented
English | [中文](2026-08-10-cancelled-stream-prefix-finalize.zh.md)
## Problem
A cancelled stream can leave `assistant/chunk` events that clients continue rendering while `deriveMessages()` excludes them because no `assistant/message` records the delivered prefix. A follow-up such as "expand on your second point" then lacks text the user read, and a fork at the cancelled turn inherits the same gap.
The model history must contain assistant content that remains visible to the user after cancellation.
## Decision
`ReactLoopAgent.step()` catches cancellation while consuming a model stream, when its `BlockAssembler`, logged chunk seqs, and provider route identify the delivered prefix. It appends that prefix as the step's `assistant/message` with `interrupted: true`, `surfaceOp: 'append'`, and `sourceEventSeqs` containing exactly the logged chunks. The append precedes `step/end` and the aborted `turn/end`.
`BlockAssembler.interruptedBlocks()` returns closed and open `text` and `reasoning` blocks with non-whitespace content in stream order. It omits tool calls because interruption precedes dispatch and no real result exists; it also omits empty blocks and open unknown block types. An empty result appends no assistant message. Provider `error` and `aborted` finishes leave the stream-consumption scope before `agent/request-error`, so provider failures and cancellation during recovery commit no content from the failed request.
Chat and Trajectory Conversation Definitions read `interrupted` from the durable message. Chat renders the Stopped marker, while Trajectory keeps the provider request in the error lifecycle after `step/end` and retains the durable result seq and provenance. Cancellation during tool execution follows the tool scheduler contract because the assistant message has already committed: started calls produce real results, and undispatched calls receive `ABORTED_BEFORE_DISPATCH` results.
## Alternatives considered
**Always discard the prefix.** This avoids a new durable marker but makes every cancel-then-follow-up and fork omit assistant content that remains visible to the user.
**Assemble the prefix from chunks during projection.** `deriveMessages()` and client Conversation Definitions would each need interruption assembly rules, and the log would have no authoritative assistant message for the prefix. This also expands model history beyond the three `SurfaceEventType` events.
**Retain complete tool calls with synthetic aborted results.** These calls never dispatched, so synthetic results would claim an execution outcome that did not occur and add content the user did not receive as a tool result.
**Append a model-visible interruption message such as `[interrupted by user]`.** This can tell the model that the prefix is incomplete, but it requires a separate source type, projection rule, UI treatment, and localized wording. The durable aborted `turn/end` preserves the fact needed for that later decision.
## Consequences
Post-cancel follow-ups and forks include the delivered prefix. The ACP bridge drains ordered assistant output before settling the prompt, so the final `agent_message_chunk` update precedes the cancelled stop reason.
Terminal provider errors still discard their streamed prefix. That asymmetry remains because an error turn ends without the user's cancellation decision and requires its own retention policy.
## Testing
`packages/core/agent-loop/tests/cancel.spec.ts` covers content, cited seqs, event order, next-request parity, reasoning-only output, tool-call omission, recovery cancellation, and the empty-prefix case. `packages/llm/llm/tests/assembler.spec.ts` covers `interruptedBlocks()`. `packages/client/ui-conversation/tests/conversation-node-definitions.client.spec.ts` and `packages/client/ui-trajectory/tests/conversation-definitions.client.spec.ts` cover both client projections. The keyless `cancel` ACP snapshot and `goal-round-driver` goal snapshot cover assembled applications.
@@ -0,0 +1,39 @@
# Agent Note: 被取消的流定稿其已送达前缀
Status: implemented
[English](2026-08-10-cancelled-stream-prefix-finalize.md) | 中文
## Problem
被取消的流可能留下客户端继续渲染的 `assistant/chunk` 事件,但如果没有 `assistant/message` 记录已送达前缀,`deriveMessages()` 就会排除这部分内容。后续的「第二点展开讲讲」之类追问会缺少用户已读到的文本,在该轮次上创建的分支也会继承这个缺口。
模型历史必须包含取消后仍对用户可见的 assistant 内容。
## Decision
`ReactLoopAgent.step()` 在消费模型流期间捕捉取消,此时 `BlockAssembler`、已记录的分片 seq 和提供方路由可以确定已送达前缀。循环把该前缀追加为 step 的 `assistant/message`,并设置 `interrupted: true``surfaceOp: 'append'` 以及恰好包含已记录分片的 `sourceEventSeqs`。该追加先于 `step/end` 和记录 aborted 的 `turn/end`
`BlockAssembler.interruptedBlocks()` 按流顺序返回内容非空白的已闭合和未闭合 `text``reasoning` 块。打断先于分派,没有真实工具结果,因此它会省略工具调用,也会省略空块和未闭合的未知块类型。返回结果为空时不追加 assistant 消息。提供方的 `error``aborted` finish 会在 `agent/request-error` 前离开流消费范围,因此提供方故障和恢复期间的取消都不会提交失败请求的内容。
Chat 和 Trajectory Conversation Definition 从持久消息读取 `interrupted`。Chat 渲染 Stopped 标记,Trajectory 则在 `step/end` 后把提供方请求保持在 error 生命周期,并保留持久结果 seq 和提供方信息。工具执行期间的取消遵循工具调度器约定,因为 assistant 消息已提交:已启动的调用生成真实结果,未分派的调用获得 `ABORTED_BEFORE_DISPATCH` 结果。
## Alternatives considered
**始终丢弃前缀。** 这能避免新增持久标记,但每次取消后的追问和分支都会缺少仍对用户可见的 assistant 内容。
**在投影时从分片组装前缀。** `deriveMessages()` 和客户端 Conversation Definition 都需要实现打断组装规则,日志中也没有该前缀的权威 assistant 消息。这还会让模型历史超出三类 `SurfaceEventType` 事件。
**保留完整工具调用并合成 aborted 结果。** 这些调用从未分派,合成结果会声称一个并未发生的执行结果,还会增加用户未收到的工具结果内容。
**追加 `[interrupted by user]` 之类模型可见的打断消息。** 这可以告诉模型前缀并不完整,但需要独立的来源类型、投影规则、UI 处理和本地化文案。持久的 aborted `turn/end` 保留了该后续决策所需的事实。
## Consequences
取消后的追问和分支会包含已送达前缀。ACP 桥会在结算 prompt 前排空按序传送的 assistant 输出,因此最后一条 `agent_message_chunk` 更新先于 cancelled stop reason。
终局提供方错误仍会丢弃已流出前缀。该不对称保留,因为 error 轮次的结束不来自用户的取消决定,需要独立的保留策略。
## Testing
`packages/core/agent-loop/tests/cancel.spec.ts` 覆盖内容、引用的 seq、事件顺序、下一请求的一致性、仅 reasoning 的输出、工具调用省略、恢复期间的取消和空前缀情形。`packages/llm/llm/tests/assembler.spec.ts` 覆盖 `interruptedBlocks()``packages/client/ui-conversation/tests/conversation-node-definitions.client.spec.ts``packages/client/ui-trajectory/tests/conversation-definitions.client.spec.ts` 覆盖两种客户端投影。keyless 的 `cancel` ACP 快照和 `goal-round-driver` goal 快照覆盖完整应用。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-21-cross-session-references.md
2026-07-21-cross-session-references.md: f49c75a9d4e3304b44d12a3df85430ffb494b8a9
2026-07-21-cross-session-references.zh.md: 81a1970497bf175948a70bb1982f336f19d4d19e
2026-07-21-cross-session-references.md: 774a948345e3d45adbba47ef6a7edd3e6f0740b2
2026-07-21-cross-session-references.zh.md: fd864203d17954167646edf3b1a62946f8fa3f23
@@ -6,33 +6,35 @@ English | [中文](2026-07-21-cross-session-references.zh.md)
## Problem
TUI users need to bring relevant work from another conversation into one new message without resuming, forking, or granting the source transcript authority over the current session. The harness already exposes exact session enumeration and raw event inspection, but every host independently parsing logs would duplicate compaction folding, filtering by cited source-event seqs, size limits, error behavior, and persistence. Encoding host markup directly into the agent message contract would also bind the core loop to one UI syntax.
Web users need to bring relevant work from another conversation into one new message without resuming, forking, or granting the source transcript authority over the current session. The harness already exposes exact session enumeration and raw event inspection, but every host independently parsing logs would duplicate compaction folding, provenance filtering, size limits, error behavior, and persistence. Encoding host markup directly into the agent message contract would also bind the core loop to one UI syntax.
## Decision
`@deepseek-ai/dsh-session-reference` is one context consumer service at `ctx.sessionReferenceResolver`. Hosts normalize their protocol into `SessionReferenceInput[]` and call `prepare()` before delivery. The service returns detached readable content plus an optional identified, frozen `UserMessage` snapshot; core agent packages do not parse session URIs or read another log.
`@deepseek-ai/dsh-session-reference` is one context consumer service at `ctx.sessionReferenceResolver`. Its outer `agent/pre-step` listener parses canonical mentions in accepted direct user messages and calls `prepare()` without adding reference behavior to a host gateway. The service returns detached readable content plus an optional identified, frozen `UserMessage` snapshot; core agent packages do not parse session URIs or read another log.
`dsh-session:<base64url(JSON.stringify(sessionId))>` is the canonical host-independent identifier. JSON string encoding precedes base64url so quotes, slashes, backslashes, Unicode, newlines, and every other JavaScript string value round-trip without delimiter ambiguity. TUI renders that URI inside `@[label](uri)`; text-only clients may use the same inline mention. Explicit Markdown mentions reject malformed URIs. Bare text becomes a reference only for a non-empty base64url-shaped payload, whose decode must still be canonical; empty or punctuation-only uses remain ordinary discussion text.
`dsh-session:<base64url(JSON.stringify(sessionId))>` is the canonical host-independent identifier. JSON string encoding precedes base64url so quotes, slashes, backslashes, Unicode, newlines, and every other JavaScript string value round-trip without delimiter ambiguity. Web receives that URI inside the Host-produced `@[label](uri)` mention and keeps it behind an atomic session chip; text-only clients may use the same inline mention. Explicit Markdown mentions reject malformed URIs. Bare text becomes a reference only for a non-empty base64url-shaped payload, whose decode must still be canonical; empty or punctuation-only uses remain ordinary discussion text.
The service uses `ctx.sessionQuery.readSurface(sessionId)`, which loads one live-preferred corpus observation, folds it with the session package's canonical surface algorithm, and returns a detached header, capture seq, and current nodes. FTS is not a dependency: discovery matches id, cwd, or the latest folded title, while message bodies remain outside the candidate layer. Non-empty queries batch title observations across the visible corpus with bounded persisted-log concurrency and cancellation; a dedicated title index can replace that discovery path without changing reference identity or preparation.
## Snapshot and projection
Preparation deduplicates in first-appearance order, rejects the target id, enforces a configurable limit with a hard maximum of three references, and performs all reads in parallel. It returns no partially prepared context: any read, cancellation, validation, or budget error rejects the operation before `followup()` or `steer()`. Cancellation races in-flight discovery and exact reads, so a host settles promptly even when a persistence backend cannot interrupt its pending operation; any late backend settlement is observed but cannot enqueue the message. A source is read before enqueue, so later source messages, compaction, deletion, or persistence replacement cannot change the target session.
Preparation deduplicates in first-appearance order, rejects the target id, enforces a configurable limit with a hard maximum of three references, and performs all reads in parallel. It returns no partial context: any read, cancellation, validation, or budget error ends the turn before the accepted messages enter model-visible history. Cancellation races in-flight discovery and exact reads, so the listener settles promptly even when a persistence backend cannot interrupt its pending operation. A queued message captures each source when it reaches `agent/pre-step`; later source messages, compaction, deletion, or persistence replacement cannot change the context recorded in the target session.
Projection retains direct-user messages and steering, completed assistant text, and checkpoint user messages carrying the canonical source exported by `dsh-compaction`. That marker is part of the compaction capability contract rather than a backend package name. When a source prompt already contains baked prefix context, projection reads only its model-hidden display content, so referencing that target later does not recursively propagate an earlier snapshot. Projection excludes shadowed pre-compaction nodes, tools and results, reasoning, injected context, other plugin user messages, log-only records, and incomplete assistant chunks. Repeated compaction therefore exposes only the latest folded checkpoint lineage still on the current surface plus its retained tail; there is no raw/current switch and no shadow recovery.
Projection retains direct-user messages and steering, completed assistant text, and checkpoint user messages carrying the canonical source exported by `dsh-compaction`. That marker is part of the compaction capability contract rather than a backend package name. Reference snapshots remain separate sourced `user/message` events, so projection excludes them as injected context and never recursively propagates an earlier snapshot. Projection also excludes shadowed pre-compaction nodes, tools and results, reasoning, other plugin user messages, log-only records, and incomplete assistant chunks. Repeated compaction therefore exposes only the latest folded checkpoint lineage still on the current surface plus its retained tail; there is no raw/current switch and no shadow recovery.
One aggregated context is serialized as JSON beneath a fixed untrusted-background warning. The warning tells the model not to follow instructions, permission claims, or tool requests from referenced sessions unless the current user repeats them. Tag-safe serialization emits every data `<` as the lossless JSON escape `\u003c`; source strings therefore cannot spell the surrounding XML-like tags or escape the data region. The same serializer drives each source's independent byte accounting. AgentLoop persists the snapshot as a sourced `user/message` immediately before the direct `user/message`; target replay therefore satisfies the model-visible/log-reconstructable invariant without a new event type, placement mode, or prompt envelope.
## Message ownership
TUI owns the snapshot/direct-message transaction without extending the generic inbox record. While the agent is idle, it installs a one-shot outer `agent/pre-step` listener before `followup()`; an enter decision receives the snapshot as another message, while rejection or an earlier ordinary discard releases the listener and writes neither message. While the agent is running, TUI calls `inject(snapshot)` then `steer(prompt)`, placing both in the next-step inbox for the same later claim. A rejecting or failed pre-step leaves that claimed pair removed; messages inserted after the claim remain pending. The [separate-context decision](../architecture/2026-07-24-separate-context-injection-from-turn-execution.md) owns this generic delivery boundary.
The service's outer `agent/pre-step` listener calls downstream listeners first and processes only an `enter` decision. It parses each accepted direct user message, preserves that message's id while replacing canonical mentions with readable labels, and inserts the frozen snapshot immediately before that message. Queue edits and queue-to-steer relocation need no reference-specific state because the final claimed messages are the input to preparation. The [separate-context decision](../architecture/2026-07-24-separate-context-injection-from-turn-execution.md) owns this context ordering.
Reference preparation is not a new steering protocol and does not create a turn by itself. Idle delivery uses `followup()` and pre-step entry; running delivery uses the shared next-step inbox while retaining snapshot order.
Reference preparation is not a new delivery protocol and does not create a turn by itself. A preparation failure terminates the already accepted turn through the agent loop's existing plugin-failure path.
## Host adapters
TUI combines session candidates with the existing `@` file provider. Candidate lookup matches case-insensitive substrings of the session id, cwd, or latest folded title, displays that title, and falls back to the session id when a title observation is absent or fails. Lookup follows the editor's cancellation signal, and session id, cwd, and mention labels escape external terminal controls while the canonical URI retains the original id. TUI prepares only submissions containing structured mentions, disables duplicate submit while awaiting snapshots, restores failed input, renders the readable direct content as the user message, and renders session-reference source metadata as a compact source list instead of exposing the complete JSON in the terminal.
The unified Web `@` source combines session candidates with Host-backed file discovery. Session candidate lookup matches case-insensitive substrings of the session id, cwd, or latest folded title, displays that title, and falls back to the session id when a title observation is absent or fails. Lookup follows the request's cancellation signal, and session id, cwd, and mention labels escape external control characters while the canonical URI retains the original id.
Web exposes file and session discovery through generated Remote methods on their owning services, as detailed in [Web file and session references](2026-07-27-web-file-and-session-references.md). Session picks are atomic chips backed by the Host-produced canonical mention. Ordinary `session.prompt` delivery carries that mention without a reference-specific API Proxy route. Replay associates the separate session-reference context with its neighboring direct message and renders a compact source summary instead of exposing the snapshot JSON.
The [automation-only ACP transport](../simplification/2026-07-23-acp-automation-only-protocol.md) deliberately does not mount session-query or session-reference services.
@@ -45,16 +47,16 @@ Each of at most three references is independently capped at 65,536 UTF-8 bytes b
- **Wait for SQLite FTS5** — rejected because snapshot correctness requires exact id reads and canonical surface folding, not content search. FTS improves discovery only.
- **Put mention syntax in agent delivery methods** — rejected because it would make the core protocol parse one host's presentation syntax and prevent typed non-text hosts from sharing the semantic layer.
- **Implement references separately in each host** — rejected because projection, security warning, retention, and persistence would drift across hosts.
- **Attach context to `SendOptions` and the direct prompt's inbox record** — rejected because generic delivery would own a domain transaction through admission, steering, cancellation, and observation. A domain-specific admission wrapper and the existing next-step inbox preserve the required pairing without enlarging every direct prompt.
- **Bake the prefix host-side before `followup()`** — rejected because `agent/pre-step` must inspect and rewrite only the direct prompt. Keeping the snapshot as a separate sourced message preserves that boundary and lets TUI hide background bytes from the direct user bubble.
- **Attach context to `SendOptions` and the direct prompt's inbox record** — rejected because generic delivery would own a domain transaction through admission, steering, cancellation, and observation. The domain listener can prepare the final claimed message without enlarging every direct prompt.
- **Bake the prefix host-side before `followup()`** — rejected because `agent/pre-step` must inspect and rewrite only the direct prompt. Keeping the snapshot as a separate sourced message preserves that boundary and lets Web hide background bytes from the direct user bubble.
- **Replay the raw source log or restore shadowed events** — rejected because compact defines the current model surface and may intentionally retire sensitive or expensive history.
- **Resume or fork the source** — rejected because the feature supplies read-only background for one target message, not identity or lifecycle continuity.
- **Inject at request time by rereading the source** — rejected because the reference would become nondeterministic, cancellation races could alter its bytes, and target replay would depend on external mutable state.
- **Reread the source after the model step enters** — rejected because target replay would depend on external mutable state instead of the logged snapshot.
## Verification
Unit and integration coverage pins URI round-trips and text-boundary punctuation, explicit malformed references, id/cwd/title candidate matching and ranking, failed title-observation fallback, candidate cancellation, terminal-control escaping, projection exclusions, non-recursive snapshot projection, backend-independent compact checkpoints, tag-safe framing, deduplication, self-reference, count limits, all-or-nothing reads, prompt cancellation against a non-settling storage read, independent per-source byte retention, prompt blocking, admission-time staging, send/steer placement, title isolation, missing capability, and compact TUI replay. One keyless terminal snapshot types a title-only substring against an opaque session id and pins the rendered candidate. Another keyless TUI snapshot runs the real agent loop: the source surface replaces old user/assistant history with a compact checkpoint, the target submits a mention, and the captured model request contains a sourced snapshot message followed by the readable current prompt, without either shadowed string.
Unit and integration coverage pins URI round-trips and text-boundary punctuation, explicit malformed references, id/cwd/title candidate matching and ranking, failed title-observation fallback, candidate cancellation, control-character escaping, projection exclusions, non-recursive snapshot projection, backend-independent compact checkpoints, tag-safe framing, deduplication, self-reference, count limits, all-or-nothing reads, cancellation against a non-settling storage read, independent per-source byte retention, frozen message ownership, pre-step parsing and insertion, downstream rejection, node-owned replay association, title isolation, and the generated Remote discovery faces. A keyless Web snapshot pins the assembled reference selection path.
## Consequences
The new plugin is the stable semantic boundary and adds no persistence schema, event type, FTS dependency, source subscription, or compact shadow access. The standard TUI demo bundle mounts it explicitly and exposes its count and per-source byte limits in its config; custom hosts remain unchanged until they mount the service and adapt their input. Reference contexts increase target history size within configured bounds and can later be summarized by ordinary target compaction, after which the source session is irrelevant.
The new plugin is the stable semantic boundary and adds no persistence schema, event type, FTS dependency, source subscription, or compact shadow access. The standard CLI composition mounts it explicitly for Web and exposes its count and per-source byte limits in config; custom hosts remain unchanged until they mount the service and adapt their input. Reference contexts increase target history size within configured bounds and can later be summarized by ordinary target compaction, after which the source session is irrelevant.
@@ -6,33 +6,35 @@ Status: implemented
## 问题
TUI 用户需要把另一场对话中的相关工作带入一条新消息,但不恢复、不 fork,也不让源 transcript(文本记录)对当前会话拥有权威性。harness 已经提供准确的会话枚举与原始事件检查,但若每个宿主都独立解析日志,就会重复实现压缩(compaction)折叠、按被引用来源事件 seq 过滤、大小限制、错误行为和持久化。把宿主标记直接编码进 agent(智能体)消息约定,还会让核心循环绑定某一种 UI 语法。
Web 用户需要把另一场对话中的相关工作带入一条新消息,但不恢复、不 fork,也不让源 transcript(文本记录)对当前会话拥有权威性。harness 已经提供准确的会话枚举与原始事件检查,但若每个宿主都独立解析日志,就会重复实现压缩(compaction)折叠、来源过滤、大小限制、错误行为和持久化。把宿主标记直接编码进 agent(智能体)消息约定,还会让核心循环绑定某一种 UI 语法。
## 决策
`@deepseek-ai/dsh-session-reference` 是注册在 `ctx.sessionReferenceResolver` 上的单一上下文消费服务。宿主先把各自的协议规范化为 `SessionReferenceInput[]`,并在交付前调用 `prepare()`。该服务返回分离的可读内容和一份可选的、带标识且冻结的 `UserMessage` 快照;核心 agent 包既不解析会话 URI,也不读取其他日志。
`@deepseek-ai/dsh-session-reference` 是注册在 `ctx.sessionReferenceResolver` 上的单一上下文消费服务。它的外层 `agent/pre-step` 监听器会解析已接受直接用户消息中的规范 mention,并调用 `prepare()`,宿主网关无需添加引用行为。该服务返回分离的可读内容和一份可选的、带标识且冻结的 `UserMessage` 快照;核心 agent 包既不解析会话 URI,也不读取其他日志。
`dsh-session:<base64url(JSON.stringify(sessionId))>` 是与宿主无关的规范标识符。系统先执行 JSON 字符串编码,再执行 base64url 编码,因此引号、正斜杠、反斜杠、Unicode、换行符以及其他任意 JavaScript 字符串值都能无损往返,不会因分隔符产生歧义。TUI 把该 URI 渲染到 `@[label](uri)` ;纯文本客户端可以使用同一种行内提及标记。显式 Markdown 提及标记会拒绝格式错误的 URI。裸文本只有在负载非空且形状符合 base64url 时才会成为引用,而且解码过程仍须通过规范性校验;空负载或只含标点符号的用法仍按普通讨论文本处理。
`dsh-session:<base64url(JSON.stringify(sessionId))>` 是与宿主无关的规范标识符。系统先执行 JSON 字符串编码,再执行 base64url 编码,因此引号、正斜杠、反斜杠、Unicode、换行符以及其他任意 JavaScript 字符串值都能无损往返,不会因分隔符产生歧义。Web 接收由 Host 生成、包含该 URI `@[label](uri)` 提及标记,并把它封装为原子 session chip;纯文本客户端可以使用同一种行内提及标记。显式 Markdown 提及标记会拒绝格式错误的 URI。裸文本只有在负载非空且形状符合 base64url 时才会成为引用,而且解码过程仍须通过规范性校验;空负载或只含标点符号的用法仍按普通讨论文本处理。
该服务使用 `ctx.sessionQuery.readSurface(sessionId)`:它优先从实时会话加载一次语料观察结果,使用会话包的规范表层算法执行折叠,并返回与源数据分离的会话头、捕获序号和当前节点。FTS 不是依赖项:候选发现会匹配 id、cwd 或最新折叠后的标题,而消息主体不进入候选层。非空查询会对可见语料中的标题观察结果执行批处理,以有界并发读取持久化日志,并支持取消;专用标题索引可以替换这条发现路径,而无需改变引用标识或准备过程。
## 快照与投影
准备过程按首次出现的顺序去重、拒绝目标会话自身的 id,并且执行可配置的数量限制,但引用硬上限为三个,所有读取均并行执行。该过程不会返回部分完成的上下文:任何读取、取消、校验或预算错误都会在调用 `followup()``steer()` 前拒绝本次操作。取消会与进行中的候选发现和精确读取竞速,因此即使持久化后端无法中断待处理操作,宿主也能及时结束等待;后端迟到的完成结果仍会被观察,但不能让消息入队。源会话在入队前完成读取,因此源会话后续新增消息、执行压缩、被删除或替换持久化内容,都无法改变目标会话中的快照
准备过程按首次出现的顺序去重、拒绝目标会话自身的 id,并且执行可配置的数量限制,但引用硬上限为三个,所有读取均并行执行。该过程不会返回部分上下文:任何读取、取消、校验或预算错误都会在已接受消息进入面向模型的历史之前结束该轮次。取消会与进行中的候选发现和精确读取竞速,因此即使持久化后端无法中断待处理操作,监听器也能及时结束等待。queued 消息到达 `agent/pre-step` 时会捕获每个源;此后源会话新增消息、执行压缩、被删除或替换持久化内容,都无法改变目标会话中记录的上下文
投影会保留直接用户消息与 steering(中途引导)、已完成的 assistant 文本,以及携带由 `dsh-compaction` 导出的规范来源标记的检查点用户消息。该标记属于压缩能力约定的一部分,而非某个后端包名称。当源提示词已包含合并写入的前缀上下文时,投影只读取其模型不可见的显示内容,因此后续引用该目标不会递归传播先的快照。投影会排除压缩前已被遮蔽的节点、工具及其结果、推理(reasoning)、注入的上下文、其他插件用户消息、仅用于日志的记录,以及尚未完成的 assistant 分片。因此,重复压缩只会暴露当前表层仍保留的最新折叠检查点谱系及其尾部消息;系统不提供 raw/current 开关,也不恢复被遮蔽的内容。
投影会保留直接用户消息与 steering(中途引导)、已完成的 assistant 文本,以及携带由 `dsh-compaction` 导出的规范来源标记的检查点用户消息。该标记属于压缩能力约定的一部分,而非某个后端包名称。引用快照始终是独立且带来源的 `user/message` 事件,因此投影会把它们作为注入上下文排除,绝不递归传播先的快照。投影会排除压缩前已被遮蔽的节点、工具及其结果、推理(reasoning)、其他插件用户消息、仅用于日志的记录,以及尚未完成的 assistant 分片。因此,重复压缩只会暴露当前表层仍保留的最新折叠检查点谱系及其尾部消息;系统不提供 raw/current 开关,也不恢复被遮蔽的内容。
系统把一个聚合上下文序列化为 JSON,并置于固定的不可信背景警告之后。该警告要求模型不要遵循被引用会话中的指令、权限声明或工具请求,除非当前用户再次提出这些内容。标签安全序列化会把数据中的每个 `<` 无损转义为 JSON `\u003c`;因此源字符串无法拼出外围类似 XML 的标签,也无法逃逸数据区域。同一个序列化器会独立核算每个源的字节数。AgentLoop 会把快照持久化为一条带来源信息的 `user/message`,紧接在直接 `user/message` 之前。因此,目标回放无需新增事件类型、放置模式或提示词封套,也能满足「模型可见/日志可重建」不变量。
## 消息所有权
TUI 负责快照/直接消息事务,不扩展通用收件箱记录。agent 空闲时,它会在调用 `followup()` 前安装一次性的外层 `agent/pre-step` 监听器enter 决策会把快照作为另一条消息接收,而 reject 或更早的普通丢弃会释放监听器,并且不写入任何消息。agent 运行时,TUI 会依次调用 `inject(snapshot)``steer(prompt)`,把两者放入 next-step inbox,等待后续同一次领取。pre-step reject 或失败会使这对已领取消息维持已移除状态;领取后插入的消息继续等待。这一通用交付边界由[上下文分离决策](../architecture/2026-07-24-separate-context-injection-from-turn-execution.md)规定。
该服务的外层 `agent/pre-step` 监听器会先调用下游监听器,并且只处理 `enter` 决策。它会解析每条已接受的直接用户消息,在把规范 mention 替换为可读标签时保留消息 id,并把冻结快照插入到该消息紧前。最终领取的消息是准备过程的输入,因此队列编辑和从 queue 移动到 steer 不需要引用专用状态。[上下文分离决策](../architecture/2026-07-24-separate-context-injection-from-turn-execution.md)规定了这一上下文顺序
引用准备过程不是新的 steering 协议,本身也不会创建轮次。空闲交付使用 `followup()` 和 pre-step 进入决策;运行期间的交付使用共享 next-step inbox,并保持快照顺序
引用准备过程不是新的投递协议,本身也不会创建轮次。准备失败会通过 agent loop 的现有插件失败路径终止已经接受的轮次
## 宿主适配器
TUI 把会话候选与现有 `@` 文件提供方组合在一起。候选查询会对 session id、cwd 或最新折叠后的标题执行不区分大小写的子串匹配,显示该标题,并在没有标题观察结果或标题观察失败时回退到 session id。候选查询遵循编辑器的取消信号;session id、cwd 和提及标签中的外部终端控制字符会被转义,但规范 URI 仍保留原始 id。TUI 只准备包含结构化提及标记的提交;等待快照时禁用重复提交;失败时恢复输入;它把可读的直接内容渲染为用户消息,并把会话引用来源元数据渲染为精简的来源列表,不在终端中暴露完整 JSON。
统一的 Web `@` source 把会话候选与 Host 支持的文件发现组合在一起。会话候选查询会对 session id、cwd 或最新折叠后的标题执行不区分大小写的子串匹配,显示该标题,并在没有标题观察结果或标题观察失败时回退到 session id。查询遵循请求的取消信号;session id、cwd 和提及标签中的外部控制字符会被转义,但规范 URI 仍保留原始 id。
Web 通过所属服务上的生成 Remote 方法提供文件与会话发现,详见 [Web 文件与会话引用](2026-07-27-web-file-and-session-references.md)。session 选择项是由 Host 生成的规范 mention 支撑的原子 chip。普通 `session.prompt` 投递会携带该 mention,无需引用专用 API Proxy 路由。回放会把独立的 session-reference 上下文与相邻直接消息关联起来,并渲染精简来源摘要,而不暴露快照 JSON。
[仅面向自动化的 ACPAgent Client Protocol)传输层](../simplification/2026-07-23-acp-automation-only-protocol.md)有意不挂载会话查询或会话引用服务。
@@ -45,16 +47,16 @@ TUI 把会话候选与现有 `@` 文件提供方组合在一起。候选查询
- **等待 SQLite FTS5**:不予采纳,因为快照正确性依赖按准确 id 读取和规范表层折叠,而不是内容搜索。FTS 只改进候选发现。
- **把提及标记语法放入 agent 投递方法**:不予采纳,因为这会迫使核心协议解析某个宿主的展示语法,并阻止带类型的非文本宿主复用同一语义层。
- **在每个宿主中分别实现引用**:不予采纳,因为投影、安全警告、保留策略和持久化会在不同宿主之间逐渐偏离。
- **把上下文附加到 `SendOptions` 和直接提示词的收件箱记录**:不予采纳,因为通用投递将不得不负责贯穿准入、steering、取消和观察的领域事务。领域专用的准入包装层和现有 next-step inbox 可以保持所需配对,而无需扩大每条直接提示词。
- **在调用 `followup()` 前由宿主合并前缀**:不予采纳,因为 `agent/pre-step` 必须只检查和改写直接提示词。将快照保留为独立的带来源消息,可以维持该边界,并让 TUI 从直接用户气泡中隐藏背景字节。
- **把上下文附加到 `SendOptions` 和直接提示词的收件箱记录**:不予采纳,因为通用投递将不得不负责贯穿准入、steering、取消和观察的领域事务。领域监听器可以准备最终领取的消息,无需扩大每条直接提示词。
- **在调用 `followup()` 前由宿主合并前缀**:不予采纳,因为 `agent/pre-step` 必须只检查和改写直接提示词。将快照保留为独立的带来源消息,可以维持该边界,并让 Web 从直接用户气泡中隐藏背景字节。
- **回放原始源日志或恢复被遮蔽的事件**:不予采纳,因为压缩定义了当前模型表层,并且可能有意淘汰敏感或开销高昂的历史内容。
- **恢复或 fork 源会话**:不予采纳,因为本功能只为一条目标消息提供只读背景,不提供身份或生命周期连续性。
- **在请求时重新读取源会话并注入**:不予采纳,因为这会让引用变得不确定,取消竞态可能改变其字节内容,目标回放会依赖可变的外部状态。
- **模型步骤进入后重新读取源会话**:不予采纳,因为目标回放会依赖可变的外部状态,而不是已记录的快照
## 验证
单元与集成测试覆盖 URI 无损往返与文本边界标点、显式格式错误的引用、按 id/cwd/标题进行候选匹配与排序、标题观察失败时的回退、候选查询取消、终端控制字符转义、投影排除规则、快照的非递归投影、与后端无关的压缩检查点、标签安全封套、去重、自引用、数量限制、读取的全有或全无、存储读取不结束时取消提示词、逐源独立字节保留、提示词阻止、准入期间的暂存、send/steer 放置方式、标题隔离、能力缺失和压缩场景下的 TUI 回放。一个无密钥终端快照会在会话 id 不透明的情况下输入一个只与标题匹配的子串,并固定渲染出的候选项。另一个无密钥 TUI 快照会运行真实的 agent loop(智能体循环):源表层用一个压缩检查点替换旧的用户/assistant 历史,目标会话提交一个提及标记,捕获到的模型请求包含一条带来源的快照消息,后面跟随可读的当前提示词,并且不包含任一被遮蔽的字符串
单元与集成测试覆盖 URI 无损往返与文本边界标点、显式格式错误的引用、按 id/cwd/标题进行候选匹配与排序、标题观察失败时的回退、候选查询取消、控制字符转义、投影排除规则、快照的非递归投影、与后端无关的压缩检查点、标签安全封套、去重、自引用、数量限制、读取的全有或全无、存储读取不结束时取消、逐源独立字节保留、冻结消息所有权、pre-step 解析和插入、下游拒绝、节点负责的回放关联、标题隔离,以及生成的 Remote 发现接口。一个无密钥 Web 快照会固定组装后的引用选择路径
## 后果
新插件构成稳定的语义边界,不会新增持久化 schema、事件类型、FTS 依赖、源会话订阅或对压缩所遮蔽内容的访问。标准 TUI 演示组合包会显式挂载它,并在自身配置中暴露引用数量和逐源字节上限;自定义宿主在挂载该服务并适配输入前保持不变。引用上下文会在配置的界限内增大目标历史,随后可由目标会话的普通压缩进行摘要;完成压缩后,源会话便不再相关。
新插件构成稳定的语义边界,不会新增持久化 schema、事件类型、FTS 依赖、源会话订阅或对压缩所遮蔽内容的访问。标准 CLI 组合会为 Web 显式挂载它,并在配置中暴露引用数量和逐源字节上限;自定义宿主在挂载该服务并适配输入前保持不变。引用上下文会在配置的界限内增大目标历史,随后可由目标会话的普通压缩进行摘要;完成压缩后,源会话便不再相关。
@@ -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/feature/2026-07-27-web-file-and-session-references.md
2026-07-27-web-file-and-session-references.md: ad8e5c53832a567bd38d1d1e560122cb8b630daa
2026-07-27-web-file-and-session-references.zh.md: acb016866efc42ef3ea9f661cb10ee1459cf1a6b
@@ -0,0 +1,49 @@
# Agent Note: Web file and session references
Status: implemented
English | [中文](2026-07-27-web-file-and-session-references.zh.md)
## Problem
The Web composer had a reusable slash/reference trigger pipeline, but its `@` source was inert subagent-label text. Web needed Host-backed workspace-path discovery and structured cross-session snapshots without scanning the Host filesystem in the browser or binding session identity to a display label.
## Decision
Web exposes one combined `@file` and `@session` menu through `@deepseek-ai/dsh-client-ui-reference`. For each unquoted query it starts both Remote discovery calls concurrently and deterministically orders files before sessions with locale-registered labels; non-selectable file and session section headings distinguish the two contiguous candidate sections without entering the keyboard-selection index. An open quoted token searches files only. Either candidate domain may fail independently without hiding successful rows from the other.
The file capability follows the three-package seam: `@deepseek-ai/dsh-file-reference` owns `ctx.fileReferences`, the shared `@path` token grammar, candidate shape, and stable model guidance; `@deepseek-ai/dsh-file-reference-local` owns bounded per-agent Host-filesystem indexes, invalidation, and scoped prompt installation; `dsh-client-ui-reference` consumes the generated Remote namespaces and shared grammar. A file pick remains path-only prompt text and a directory pick retriggers completion below its trailing slash.
A session pick is an atomic composer reference. Its visible label is presentation, while its hidden value and clipboard form are the canonical `@[label](dsh-session:…)` mention produced by the Host. Ordinary `session.prompt` delivery carries that mention unchanged. The session-reference service parses accepted direct user messages at `agent/pre-step`, captures every source, replaces the canonical mention with readable text while preserving the direct message id, and inserts the frozen snapshot immediately before that message. The API Proxy contains no reference-specific route, dependency, or error code.
The input machine keeps ordinary draft text and atomic references until the default sink reports Host acceptance. Serialization or prompt transport failure returns the same draft to editing. After acceptance, reference preparation belongs to the agent turn; a malformed mention, failed source read, cancellation, or budget failure terminates that turn. The logged prompt remains the replay authority. The concrete user and steering chat-node definition associates labels from an immediately preceding session-reference context, so the renderer receives the association from its own node data and shows a compact source summary instead of snapshot JSON.
## Reference transaction
```text
type @ → parallel file/session Remote calls → pick path text or canonical session chip
→ serialize draft → ordinary session.prompt enqueue
→ agent/pre-step parses mentions → capture sources → context + readable prompt
```
File lookup is advisory and cancellable; selection itself performs no read. Session preparation is all-or-nothing for one accepted model step. A queued message captures each source when the message is claimed, so queue edits and queue-to-steer relocation use the same path without gateway coordination.
## Alternatives considered
**Implement file discovery and grammar inside the Web client.** Rejected because browser-side code cannot safely access the Host workspace, while duplicating grammar, ranking, bounds, and invalidation would drift from the Host provider.
**Scan files through ordinary filesystem-tool RPCs.** Rejected because recursive fuzzy discovery is editor latency work, not a model-facing exact filesystem operation, and would couple the menu to tool policy and provider round trips.
**Eagerly attach selected file contents.** Rejected because selection would spend context before relevance is known and bypass the logged, auditable `read` call/result sequence.
**Represent sessions as plain `@label` text.** Rejected because labels are neither stable nor unique and cannot identify the source snapshot. Canonical Host-produced mentions preserve opaque session identity while keeping a readable display.
**Clear the composer before prompt admission settles.** Rejected because a transport or admission failure would lose the only editable copy of the request and visually claim acceptance that never occurred.
## Verification
Package tests pin shared file grammar and ranking, cache invalidation and lifecycle cleanup, parallel Web lookup, quoted paths, independent candidate failure, cancellation, grouped headings that do not alter option indexes, file/directory continuation, canonical session chips, adjacent-reference and adjacent-text reference projection, codec round-trip, generated Remote type inference, pre-step preparation, downstream rejection, and chat-node-owned label association. The keyless assembled Web snapshot renders the available reference sections, selects a file, then selects a session reference through the real client composition.
## Consequences
Web now uses the shared `@file` discovery seam and structured session-reference identity, while Host services remain the authority for filesystem and session access. File and session discovery are unary Remote contracts on the owning services, so generated client types replace handwritten RPC interfaces and browser bundles remain free of Node APIs. Candidate lookup failures remain quiet menu degradation. Reference preparation failures occur after prompt acceptance and end the agent turn. File references cost only path text plus stable conditional guidance, whereas session references retain the bounded snapshot cost and trust framing owned by `dsh-session-reference`.
@@ -0,0 +1,49 @@
# Agent Note: Web 文件与会话引用
Status: implemented
[English](2026-07-27-web-file-and-session-references.md) | 中文
## 问题
Web 输入框已有可复用的斜杠命令/引用触发流水线,但它的 `@` source 只是不会产生实际作用的 subagent 标签文本。Web 需要由宿主提供工作区路径发现和结构化跨会话快照,同时避免在浏览器中扫描宿主文件系统或把会话身份绑定到显示标签。
## 决策
Web 通过 `@deepseek-ai/dsh-client-ui-reference` 暴露一个合并的 `@file``@session` 菜单。每次处理未加引号的查询时,它会并发启动两项 Remote 发现调用,以确定性顺序把文件排在会话之前,并使用注册在 locale 字典中的标签;不可选择的文件与会话分组标题会区分两个连续的候选分组,且不会进入键盘选择索引。尚未闭合的带引号 token 只搜索文件。任一候选领域都可以独立失败,不会隐藏另一领域成功返回的行。
文件功能遵循由三个包构成的 seam`@deepseek-ai/dsh-file-reference` 拥有 `ctx.fileReferences`、共享 `@path` token 语法、候选形状和稳定的模型指引;`@deepseek-ai/dsh-file-reference-local` 拥有每个 agent(智能体)有界的宿主文件系统索引、失效处理和作用域内的提示词安装;`dsh-client-ui-reference` 消费生成的 Remote 命名空间与共享语法。选择文件后仍只会把路径文本写入提示词,选择目录则会在其尾部斜杠后重新触发补全。
选择会话会创建一个原子的输入框引用。可见标签只用于呈现,隐藏值和剪贴板形式则是宿主生成的规范 `@[label](dsh-session:…)` mention。普通 `session.prompt` 投递会原样携带该 mention。session-reference 服务会在 `agent/pre-step` 解析已接受的直接用户消息,捕获每个源,在保留直接消息 id 的同时把规范 mention 替换为可读文本,并把冻结快照插入到该消息紧前。API Proxy 不包含引用专用路由、依赖或错误码。
输入状态机在默认 sink 报告宿主已接受前,会保留普通草稿文本和原子引用。序列化或提示词传输失败后,同一草稿会回到可编辑状态。接受后,引用准备属于 agent 轮次;格式错误的 mention、源读取失败、取消或预算失败会终止该轮次。已记录的提示词仍是回放权威。具体的 user 和 steering chat-node 定义会关联紧邻前一条 session-reference 上下文中的标签,因此渲染器会从自身节点数据接收关联信息,并显示精简的来源摘要,而不是快照 JSON。
## 引用事务
```text
type @ → parallel file/session Remote calls → pick path text or canonical session chip
→ serialize draft → ordinary session.prompt enqueue
→ agent/pre-step parses mentions → capture sources → context + readable prompt
```
文件查询仅供参考且可取消;选择操作本身不会读取文件。会话准备针对一个已接受的模型步骤保持全有或全无。queued 消息被领取时会捕获每个源,因此队列编辑和从 queue 移动到 steer 使用同一路径,无需网关协调。
## 备选方案
**在 Web 客户端内部实现文件发现与语法。** 不予采纳,因为浏览器侧代码无法安全访问宿主工作区,而且重复的语法、排序、边界和失效处理会与宿主提供方产生偏差。
**通过普通文件系统工具 RPC 扫描文件。** 不予采纳,因为递归模糊发现属于编辑器低延迟工作,而不是面向模型的精确文件系统操作;该方案还会把菜单与工具策略及提供方往返绑定。
**选择文件时立即附加其内容。** 不予采纳,因为该方案会在尚未确定相关性时消耗上下文,并绕过可从日志重建、可审计的 `read` 调用/结果序列。
**用普通 `@label` 文本表示会话。** 不予采纳,因为标签既不稳定也不唯一,无法标识源快照。宿主生成的规范提及标记既能保留不透明会话身份,也能保持显示内容易读。
**提示词准入结算前清空输入框。** 不予采纳,因为传输或准入失败会丢失请求唯一可编辑的副本,并在视觉上错误表示一个从未成功的接受操作。
## 验证
包(package)测试固定共享文件语法和排序、缓存失效及生命周期清理、Web 并行查询、带引号的路径、候选项独立失败、取消、不改变候选项索引的分组标题、文件/目录继续补全、规范会话 chip、相邻引用及相邻文本条件下的引用投影、codec 无损往返、生成的 Remote 类型推断、pre-step 准备、下游拒绝,以及 chat node 自有的标签关联。无密钥的装配 Web 快照会渲染可用的引用分组,并通过真实客户端组合依次选择文件和会话引用。
## 后果
Web 现在使用共享的 `@file` 发现 seam 和结构化会话引用身份,宿主服务仍然是文件系统与会话访问的权威来源。文件和会话发现都是所属服务上的一元 Remote 契约,因此生成的客户端类型会替代手写 RPC 接口,浏览器 bundle 中也不包含 Node API。候选查询失败仍会让菜单静默降级。引用准备失败发生在提示词已接受之后,并会结束 agent 轮次。文件引用只产生路径文本和稳定的条件式指引成本,而会话引用仍保留 `dsh-session-reference` 所拥有的有界快照开销与信任限定文本。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.md
2026-07-27-web-subagent-conversations.md: cdf2b12e69b359b8ab00d4b107aa78a21691ac4e
2026-07-27-web-subagent-conversations.zh.md: 697bfeef86e2ef456aed6ee6ad8eae1173c96a6f
2026-07-27-web-subagent-conversations.md: d20e55429d04d4308ae26d001575e664058c7dea
2026-07-27-web-subagent-conversations.zh.md: 67c9b5224abea6c0710303ae56e315ba34c7ac09
@@ -85,7 +85,7 @@ The shipped Web composition mounts SQLite session query beside JSONL persistence
**Put the adapter in the webserver.** Rejected because catalog and continuation are channel-independent client capabilities; the webserver only carries validated messages.
**Create a new UI package.** Rejected because `ui-subagent` already owns Web subagent references and is the coherent owner for catalog and addressed-child presentation.
**Put Host-backed file and session references in this package.** Rejected because catalog and addressed-child presentation depend on subagent lineage, while combined reference discovery is a separate Host capability consumed by [`ui-reference`](../../../../packages/client/ui-reference/README.md).
**Auto-resume an absent parent.** Rejected because continuation requires the exact live direct parent. Child navigation must not mutate the parent lifecycle.
@@ -85,7 +85,7 @@ one-shot 行始终会用文案替代输入框,说明执行记录为只读。
**将适配器放入 webserver。** 不予采纳,因为目录与继续执行是通道无关的客户端能力;webserver 只承载已校验的消息。
**新建 UI 包。** 不予采纳,因为 `ui-subagent` 已经负责 Web subagent 引用,也是目录与已寻址 child 呈现的统一 owner
**把由 Host 支撑的文件与会话引用放进本包。** 不予采纳,因为目录与已寻址 child 呈现依赖 subagent 谱系,而组合引用发现是独立的 Host 功能,由 [`ui-reference`](../../../../packages/client/ui-reference/README.md) 消费
**自动恢复缺失的 parent。** 不予采纳,因为继续执行要求确切的存活直接 parent。child 导航不得改变 parent 生命周期。
+113
View File
@@ -0,0 +1,113 @@
// Web e2e scenario: the shipped composition discovers local files and cold
// sessions through the real Host, groups both domains in the shared @ menu,
// and projects each pick back into the composer without issuing a model call.
import { writeFile } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
import { join } from 'node:path'
import type { Browser, Page } from 'playwright'
import { chromium } from 'playwright'
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import {
SESSION_FORMAT_VERSION,
Session,
SessionId,
} from '@deepseek-ai/dsh-session'
import type {} from '@deepseek-ai/dsh-session-title'
import {
assertFixtureInventory,
captureStableAria,
compareOrRefreshGolden,
launchWebScaffold,
seedSession,
watchConsole,
webSnapshotMode,
type WebScaffold,
} from './scaffold.ts'
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/reference-composer', import.meta.url))
const MENU_EXPECTED = join(SNAPSHOT_DIR, 'menu.expected.md')
const MODE = webSnapshotMode()
const SOURCE_SESSION_ID = 'reference-source-session'
/** Build one closed source session with a stable title for reference discovery. */
function sourceSessionFixture(): string {
const session = Session.create(SessionId(SOURCE_SESSION_ID))
session.append('turn/start', {
turn: 1,
})
const user = session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'Research context for the reference menu.' }],
source: { kind: 'user' },
}), { surfaceOp: 'append' })
session.append('session/title', {
title: 'Research notes',
messageSeqs: [user.seq],
source: { kind: 'fallback' },
})
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
return [
JSON.stringify({
type: 'session',
version: SESSION_FORMAT_VERSION,
id: '{{sessionId}}',
createdAt: 0,
cwd: '{{cwd}}',
}),
...session.events.map(event => JSON.stringify(event)),
'',
].join('\n')
}
describe.skipIf(MODE === 'record')('web e2e: file and session references through the real host', () => {
let scaffold: WebScaffold
let browser: Browser
let page: Page
let tripwire: ReturnType<typeof watchConsole>
beforeAll(async () => {
scaffold = await launchWebScaffold({})
await seedSession(scaffold, sourceSessionFixture(), SOURCE_SESSION_ID)
browser = await chromium.launch()
page = await newEnglishPage(browser)
tripwire = watchConsole(page)
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
await connectFreshWorkspace(page, scaffold.workspaceCwd)
await writeFile(join(scaffold.workspaceCwd, 'workspace', 'reference.txt'), 'reference fixture\n')
}, 120_000)
afterAll(async () => {
await browser?.close()
await scaffold?.close()
})
it('groups both sources and projects file text plus an atomic session chip', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-reference-composer'))
const input = page.locator('textarea').first()
const menu = page.getByRole('listbox', { name: 'Trigger suggestions' })
await input.fill('@')
await expect.poll(() => menu.getByRole('option').count(), { timeout: 15_000 }).toBeGreaterThanOrEqual(2)
const snapshot = await captureStableAria(page, '[role="listbox"]', scaffold.workspaceCwd)
await compareOrRefreshGolden(MENU_EXPECTED, snapshot, MODE)
expect(snapshot).toContain('Files & folders')
expect(snapshot).toContain('Session conversations')
expect(snapshot).toContain('File \u00b7 reference.txt')
expect(snapshot).toContain('Session \u00b7 Research notes')
expect(snapshot).not.toContain('text: Subagents')
await input.fill('@reference')
await menu.getByRole('option', { name: /File \u00b7 reference\.txt/ }).click()
await expect.poll(() => input.inputValue()).toBe('@reference.txt ')
await input.fill('@Research')
await menu.getByRole('option', { name: /Session \u00b7 Research notes/ }).click()
await expect.poll(() => page.locator('[data-decoration="chip"]').textContent()).toBe('@Research notes')
expect(tripwire.pageErrors).toEqual([])
expect(tripwire.warnings).toEqual([])
await assertFixtureInventory(SNAPSHOT_DIR, ['menu.expected.md'])
})
})
+3 -2
View File
@@ -856,8 +856,9 @@ function normalizeAria(snapshot: string, workspaceCwd: string): string {
// Seeded compaction prices realized file paths, whose length differs
// between local worktrees and CI scratch directories.
.replace(/(Compacted \d+ history items \(~)\d+( tokens\))/g, '$1{{tokens}}$2')
// Message IconActions clocks widen by calendar day/year; collapse every
// format so goldens stay stable across midnight and year changes.
// Session summaries and Message IconActions clocks cross calendar
// boundaries; collapse every shape so goldens stay stable across them.
.replace(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z/g, '{{timestamp}}')
.replace(/\d{4}年\d{1,2}月\d{1,2}日 \d{2}:\d{2}/g, '{{clock}}')
.replace(/\d{1,2}月\d{1,2}日 \d{2}:\d{2}/g, '{{clock}}')
.replace(/(?<!\d)\d{1,2}:\d{2}:\d{2}(?:\.\d+)?(?:\s*[AP]M)?(?!\d)/gi, '{{clock}}')
@@ -20,9 +20,13 @@
- text: Stopped
- button "Copy":
- img
- button "Good response":
- img
- button "Bad response":
- img
- button "Branch into a new conversation":
- img
- text: {{clock}} Ran for {{duration}}
- text: {{clock}} Ran for {{duration}} TTFT {{duration}}
- textbox "Message the agent"
- button "Commands":
- img
@@ -31,4 +35,4 @@
- text: DeepSeek-V4-Flash
- img
- button "Send message" [disabled]
- text: 1 turns · 1 steps
- text: 1 turns · 1 steps LLM {{duration}} TTFT avg {{duration}}
@@ -20,9 +20,13 @@
- text: Stopped
- button "Copy":
- img
- button "Good response":
- img
- button "Bad response":
- img
- button "Branch into a new conversation":
- img
- text: {{clock}} Ran for {{duration}}
- text: {{clock}} Ran for {{duration}} TTFT {{duration}}
- button "2 queued messages" [expanded]
- list:
- listitem:
@@ -50,4 +54,4 @@
- text: DeepSeek-V4-Flash
- img
- button "Send message" [disabled]
- text: 1 turns · 1 steps
- text: 1 turns · 1 steps LLM {{duration}} TTFT avg {{duration}}
@@ -0,0 +1,5 @@
- listbox "Trigger suggestions":
- text: reference Files & folders
- option "File · reference.txt reference.txt" [selected]
- text: Session conversations
- option "Session · Research notes reference-source-session · {{cwd}} · {{timestamp}}"
@@ -30,6 +30,10 @@
- button "Copy":
- img
- tooltip "Copy"
- button "Good response":
- img
- button "Bad response":
- img
- button "Branch into a new conversation":
- img
- text: {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s
+1 -2
View File
@@ -106,9 +106,8 @@ describe('web e2e: mid-turn steering lands durably and visibly', () => {
// this exact occurrence into the current turn's steering outbox.
await input.fill(STEER)
await input.press('Enter')
const queued = page.getByText(STEER, { exact: true })
await queued.waitFor({ timeout: 10_000 })
const queuedRow = page.getByRole('listitem').filter({ hasText: STEER })
await queuedRow.waitFor({ timeout: 10_000 })
const steerButton = queuedRow.getByRole('button', { name: 'Steer queued message' })
await expect.poll(() => steerButton.isEnabled(), { timeout: 10_000 }).toBe(true)
await steerButton.click({ timeout: 10_000 })
+1
View File
@@ -63,6 +63,7 @@
"tests/message-feedback.e2e.ts",
"tests/message-feedback-layout.e2e.ts",
"tests/markdown-images.e2e.ts",
"tests/reference-composer.e2e.ts",
"tests/math-rendering.e2e.ts",
"tests/markdown-cjk-strong.e2e.ts",
"tests/markdown-inline-code-links.e2e.ts",
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/capability-seams.md
capability-seams.md: 710c399510b6b123123e0a9586d86bfc3a96dff9
capability-seams.zh.md: e0559d464ba1ecd2160eaba40c24bfd829e4a0d6
capability-seams.md: 23fc498cc280518489aadbe752a1702d43f17972
capability-seams.zh.md: 0a8822bc8d1f0296ac6698b59fda60666faaf54a
+6
View File
@@ -66,6 +66,9 @@ flowchart LR
svc_sessionQuery["ctx.sessionQuery<br/>Session reads, traces, filters, and search"]
pkg_session_reference["session-reference"]
pkg_tool_session_query["tool-session-query"]
pkg_file_reference["file-reference"]
svc_fileReferences["ctx.fileReferences<br/>File reference discovery"]
pkg_file_reference_local["file-reference-local"]
svc_sessionReferenceResolver["ctx.sessionReferenceResolver<br/>Cross-session snapshot preparation"]
pkg_session_title["session-title"]
svc_sessionTitle["ctx.sessionTitle<br/>Log-backed session titles"]
@@ -222,6 +225,8 @@ flowchart LR
pkg_directory_picker_browse --> svc_directoryPicker
pkg_directory_picker_native --> svc_directoryPicker
pkg_e2b --> svc_e2b
pkg_file_reference --> svc_fileReferences
pkg_file_reference_local --> svc_fileReferences
pkg_fs --> svc_fs
pkg_fs_e2b --> svc_fs
pkg_fs_local --> svc_fs
@@ -433,6 +438,7 @@ flowchart LR
| `ctx.messageFeedback` | `core` | [`message-feedback`](../packages/feedback/message-feedback) | - | - | - | Owns local per-assistant-message feedback, lifecycle and target validation, per-item compare-and-set, and the Host unary Remote contract without entering Session history or telemetry. |
| `ctx.workspaceRegistry` | `core` | [`workspace`](../packages/workspace/workspace) | - | `apiproxy` | - | Owns WorkspaceId-branded records over the domain facility; stable sessionIds accounts drive Host RPC and GUI projections. |
| `ctx.sessionQuery` | `seam` | [`session-query`](../packages/session-query/session-query) | [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | [`session-reference`](../packages/context/session-reference), [`tool-session-query`](../packages/session-query/tool-session-query) | - | The interface supplies exact reads, filters, and traces; its concrete backend adds full-text reconciliation, ranking, snippets, and cursor generations, while the model consumer owns workspace authority and cursor-free rendering. |
| `ctx.fileReferences` | `seam` | [`file-reference`](../packages/context/file-reference) | [`file-reference-local`](../packages/context/file-reference-local) | - | - | The interface returns path-only completion candidates within the addressed Agent cwd through its unary Remote contract; providers own namespace access and ranking without reading file contents. |
| `ctx.sessionReferenceResolver` | `core` | [`session-reference`](../packages/context/session-reference) | - | - | - | Projects bounded current-surface conversation snapshots into durable untrusted message context; host adapters own mention syntax. |
| `ctx.sessionTitle` | `seam` | [`session-title`](../packages/session/session-title) | [`session-title-first-prompt-llm`](../packages/session/session-title-first-prompt-llm), [`session-title-all-prompts-llm`](../packages/session/session-title-all-prompts-llm) | - | - | Owns the deterministic fallback, latest-title fold, and sole optional asynchronous provider registration. |
| `ctx.systemPrompt` | `core` | [`system-prompt`](../packages/core/system-prompt) | - | [`agent-loop`](../packages/core/agent-loop), [`tools`](../packages/core/tools), [`tool-fs`](../packages/fs/tool-fs), [`tool-terminal`](../packages/terminal/tool-terminal), [`tool-web`](../packages/web/tool-web) | - | Collects prompt sections and model-facing tool schemas for each step. |
+6
View File
@@ -68,6 +68,9 @@ flowchart LR
svc_sessionQuery["ctx.sessionQuery<br/>Session reads, traces, filters, and search"]
pkg_session_reference["session-reference"]
pkg_tool_session_query["tool-session-query"]
pkg_file_reference["file-reference"]
svc_fileReferences["ctx.fileReferences<br/>File reference discovery"]
pkg_file_reference_local["file-reference-local"]
svc_sessionReferenceResolver["ctx.sessionReferenceResolver<br/>Cross-session snapshot preparation"]
pkg_session_title["session-title"]
svc_sessionTitle["ctx.sessionTitle<br/>Log-backed session titles"]
@@ -224,6 +227,8 @@ flowchart LR
pkg_directory_picker_browse --> svc_directoryPicker
pkg_directory_picker_native --> svc_directoryPicker
pkg_e2b --> svc_e2b
pkg_file_reference --> svc_fileReferences
pkg_file_reference_local --> svc_fileReferences
pkg_fs --> svc_fs
pkg_fs_e2b --> svc_fs
pkg_fs_local --> svc_fs
@@ -435,6 +440,7 @@ flowchart LR
| `ctx.messageFeedback` | `core` | [`message-feedback`](../packages/feedback/message-feedback) | - | - | - | 拥有本地逐 assistant 消息反馈、生命周期与目标校验、逐条目 compare-and-set 及 Host 一元 Remote 契约,且不进入 Session 历史或遥测。 |
| `ctx.workspaceRegistry` | `core` | [`workspace`](../packages/workspace/workspace) | - | `apiproxy` | - | 通过领域设施拥有带 WorkspaceId 品牌类型的记录;稳定的 sessionIds 账户驱动 Host RPC 与 GUI 投影。 |
| `ctx.sessionQuery` | `seam` | [`session-query`](../packages/session-query/session-query) | [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | [`session-reference`](../packages/context/session-reference), [`tool-session-query`](../packages/session-query/tool-session-query) | - | 该接口提供精确读取、过滤和追踪;具体后端还提供全文协调、排序、摘要片段和游标世代,而模型消费方负责工作区权限与不含游标的渲染。 |
| `ctx.fileReferences` | `seam` | [`file-reference`](../packages/context/file-reference) | [`file-reference-local`](../packages/context/file-reference-local) | - | - | 该接口通过其一元 Remote 契约返回指定 Agent cwd 内仅含路径的补全候选;提供方负责命名空间访问和排序,但不会读取文件内容。 |
| `ctx.sessionReferenceResolver` | `core` | [`session-reference`](../packages/context/session-reference) | - | - | - | 将当前表层中有界的对话快照投影为持久但不可信的消息上下文;Host 适配器负责提及语法。 |
| `ctx.sessionTitle` | `seam` | [`session-title`](../packages/session/session-title) | [`session-title-first-prompt-llm`](../packages/session/session-title-first-prompt-llm), [`session-title-all-prompts-llm`](../packages/session/session-title-all-prompts-llm) | - | - | 负责确定性回退、最新标题折叠区,以及唯一的可选异步提供方注册。 |
| `ctx.systemPrompt` | `core` | [`system-prompt`](../packages/core/system-prompt) | - | [`agent-loop`](../packages/core/agent-loop), [`tools`](../packages/core/tools), [`tool-fs`](../packages/fs/tool-fs), [`tool-terminal`](../packages/terminal/tool-terminal), [`tool-web`](../packages/web/tool-web) | - | 为每个步骤收集提示词各部分和面向模型的工具 schema。 |
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/config-catalog.md
config-catalog.md: 5e2461e1ee38687ff5a53607ab491d2c5b10ab9b
config-catalog.zh.md: d2df9fbae4b20ee1ef827a7dc0788a454a55186c
config-catalog.md: dcba2eb562799e8dad84e5ee77e4d0d22144a228
config-catalog.zh.md: 1e06d0d0ea6aec1f5c669f9780afed9bf685dcda
+22
View File
@@ -587,6 +587,26 @@ export interface Config {
Source: [`packages/e2b/e2b/src/index.ts:43`](../packages/e2b/e2b/src/index.ts)
<a id="deepseek-aidsh-file-reference-local"></a>
## `@deepseek-ai/dsh-file-reference-local`
Requires: `agents`
```ts config-catalog
/** Local file-reference discovery configuration. */
export interface Config {
/** Maximum ranked candidates returned for one query. */
maxResults?: number
/** Maximum indexed files and directories per agent workspace. */
maxEntries?: number
/** Directory basenames never traversed or offered. */
excludedDirectories?: string[]
}
```
Source: [`packages/context/file-reference-local/src/index.ts:35`](../packages/context/file-reference-local/src/index.ts)
<a id="deepseek-aidsh-fs-local"></a>
## `@deepseek-ai/dsh-fs-local`
@@ -3206,6 +3226,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
- `@deepseek-ai/dsh-client-ui-model-selection` ([`packages/client/ui-model-selection/src/index.ts`](../packages/client/ui-model-selection/src/index.ts))
- `@deepseek-ai/dsh-client-ui-permission-presets` ([`packages/client/ui-permission-presets/src/index.ts`](../packages/client/ui-permission-presets/src/index.ts))
- `@deepseek-ai/dsh-client-ui-plan` ([`packages/client/ui-plan/src/index.ts`](../packages/client/ui-plan/src/index.ts))
- `@deepseek-ai/dsh-client-ui-reference` ([`packages/client/ui-reference/src/index.ts`](../packages/client/ui-reference/src/index.ts))
- `@deepseek-ai/dsh-client-ui-renderer` ([`packages/client/ui-renderer/src/index.ts`](../packages/client/ui-renderer/src/index.ts))
- `@deepseek-ai/dsh-client-ui-settings` ([`packages/client/ui-settings/src/index.ts`](../packages/client/ui-settings/src/index.ts))
- `@deepseek-ai/dsh-client-ui-settings-general` ([`packages/client/ui-settings-general/src/index.ts`](../packages/client/ui-settings-general/src/index.ts))
@@ -3260,6 +3281,7 @@ Abstract service classes — a deployment loads a concrete implementation packag
- `@deepseek-ai/dsh-code-runtime` — abstract `CodeRuntime` ([`packages/code-runtime/code-runtime/src/index.ts`](../packages/code-runtime/code-runtime/src/index.ts))
- `@deepseek-ai/dsh-compaction` — abstract `CompactionEngine` ([`packages/compaction/compaction/src/index.ts`](../packages/compaction/compaction/src/index.ts))
- `@deepseek-ai/dsh-credentials` — abstract `CredentialProvider` ([`packages/credentials/credentials/src/index.ts`](../packages/credentials/credentials/src/index.ts))
- `@deepseek-ai/dsh-file-reference` — abstract `FileReferenceService` ([`packages/context/file-reference/src/index.ts`](../packages/context/file-reference/src/index.ts))
- `@deepseek-ai/dsh-fs` — abstract `FileSystem` ([`packages/fs/fs/src/index.ts`](../packages/fs/fs/src/index.ts))
- `@deepseek-ai/dsh-host-directory-picker` — abstract `DirectoryPicker` ([`packages/host/directory-picker/src/index.ts`](../packages/host/directory-picker/src/index.ts))
- `@deepseek-ai/dsh-jobs` — abstract `JobRegistry` ([`packages/jobs/jobs/src/index.ts`](../packages/jobs/jobs/src/index.ts))
+22
View File
@@ -589,6 +589,26 @@ export interface Config {
来源:[`packages/e2b/e2b/src/index.ts:43`](../packages/e2b/e2b/src/index.ts)
<a id="deepseek-aidsh-file-reference-local"></a>
## `@deepseek-ai/dsh-file-reference-local`
需要:`agents`
```ts config-catalog
/** Local file-reference discovery configuration. */
export interface Config {
/** Maximum ranked candidates returned for one query. */
maxResults?: number
/** Maximum indexed files and directories per agent workspace. */
maxEntries?: number
/** Directory basenames never traversed or offered. */
excludedDirectories?: string[]
}
```
来源:[`packages/context/file-reference-local/src/index.ts:35`](../packages/context/file-reference-local/src/index.ts)
<a id="deepseek-aidsh-fs-local"></a>
## `@deepseek-ai/dsh-fs-local`
@@ -3210,6 +3230,7 @@ export interface Config {
- `@deepseek-ai/dsh-client-ui-model-selection`[`packages/client/ui-model-selection/src/index.ts`](../packages/client/ui-model-selection/src/index.ts)
- `@deepseek-ai/dsh-client-ui-permission-presets`[`packages/client/ui-permission-presets/src/index.ts`](../packages/client/ui-permission-presets/src/index.ts)
- `@deepseek-ai/dsh-client-ui-plan`[`packages/client/ui-plan/src/index.ts`](../packages/client/ui-plan/src/index.ts)
- `@deepseek-ai/dsh-client-ui-reference`[`packages/client/ui-reference/src/index.ts`](../packages/client/ui-reference/src/index.ts)
- `@deepseek-ai/dsh-client-ui-renderer`[`packages/client/ui-renderer/src/index.ts`](../packages/client/ui-renderer/src/index.ts)
- `@deepseek-ai/dsh-client-ui-settings`[`packages/client/ui-settings/src/index.ts`](../packages/client/ui-settings/src/index.ts)
- `@deepseek-ai/dsh-client-ui-settings-general`[`packages/client/ui-settings-general/src/index.ts`](../packages/client/ui-settings-general/src/index.ts)
@@ -3264,6 +3285,7 @@ export interface Config {
- `@deepseek-ai/dsh-code-runtime` — 抽象 `CodeRuntime`[`packages/code-runtime/code-runtime/src/index.ts`](../packages/code-runtime/code-runtime/src/index.ts)
- `@deepseek-ai/dsh-compaction` — 抽象 `CompactionEngine`[`packages/compaction/compaction/src/index.ts`](../packages/compaction/compaction/src/index.ts)
- `@deepseek-ai/dsh-credentials` — 抽象 `Credentials`[`packages/credentials/credentials/src/index.ts`](../packages/credentials/credentials/src/index.ts)
- `@deepseek-ai/dsh-file-reference` — 抽象 `FileReferenceService`[`packages/context/file-reference/src/index.ts`](../packages/context/file-reference/src/index.ts)
- `@deepseek-ai/dsh-fs` — 抽象 `FileSystem`[`packages/fs/fs/src/index.ts`](../packages/fs/fs/src/index.ts)
- `@deepseek-ai/dsh-host-directory-picker` — 抽象 `DirectoryPicker`[`packages/host/directory-picker/src/index.ts`](../packages/host/directory-picker/src/index.ts)
- `@deepseek-ai/dsh-jobs` — 抽象 `JobRegistry`[`packages/jobs/jobs/src/index.ts`](../packages/jobs/jobs/src/index.ts)
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/event-producer-consumer.md
event-producer-consumer.md: c906ee6329fac66e7391c266213dd150dd5b8e09
event-producer-consumer.zh.md: 77bf401b7215bd263c0d84f04e0eabe6b28b7915
event-producer-consumer.md: 2e4bec93f5b9f68ab885d50bab27e9a9a6036028
event-producer-consumer.zh.md: e3f5602287d7b688a2191937871e1639dc271e65
+4 -4
View File
@@ -9,13 +9,13 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| --- | --- | --- | --- | --- |
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:183`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
| `agent-preset/selected` | `emit` | [`packages/preset/agent-presets/src/types.ts:13`](../packages/preset/agent-presets/src/types.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`) | `apiproxy` |
| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:159`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), [`tool-team`](../packages/experimental/tool-team) |
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:168`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-team`](../packages/experimental/tool-team) |
| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:159`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), [`tool-team`](../packages/experimental/tool-team) |
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:168`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-team`](../packages/experimental/tool-team) |
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `apiproxy`, [`goal-round-driver`](../packages/goal/goal-round-driver), [`session-telemetry`](../packages/session/session-telemetry) |
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:197`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) |
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:205`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:186`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:231`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-skill`](../packages/skill/tool-skill) |
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:231`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-reference`](../packages/context/session-reference), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-skill`](../packages/skill/tool-skill) |
| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:244`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent) |
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:260`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`llm-retry`](../packages/llm/llm-retry) |
| `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:217`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`team`](../packages/experimental/team) |
@@ -40,7 +40,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
| `session/created` | `emit` | [`packages/core/session/src/index.ts:54`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`time-context`](../packages/context/time-context), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:64`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`team`](../packages/experimental/team) |
| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-presets`](../packages/preset/agent-presets), `apiproxy`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`team`](../packages/experimental/team), [`token-meter`](../packages/llm/token-meter), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-presets`](../packages/preset/agent-presets), `apiproxy`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`team`](../packages/experimental/team), [`token-meter`](../packages/llm/token-meter), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:85`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) |
| `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:48`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `apiproxy` |
| `settings/updated` | `emit` | [`packages/settings/settings/src/types.ts:35`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) |
+4 -4
View File
@@ -11,13 +11,13 @@
| --- | --- | --- | --- | --- |
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:183`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
| `agent-preset/selected` | `emit` | [`packages/preset/agent-presets/src/types.ts:13`](../packages/preset/agent-presets/src/types.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`) | `apiproxy` |
| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:159`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), [`tool-team`](../packages/experimental/tool-team) |
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:168`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-team`](../packages/experimental/tool-team) |
| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:159`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), [`tool-team`](../packages/experimental/tool-team) |
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:168`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-team`](../packages/experimental/tool-team) |
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `apiproxy`, [`goal-round-driver`](../packages/goal/goal-round-driver), [`session-telemetry`](../packages/session/session-telemetry) |
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:197`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) |
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:205`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:186`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:231`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-skill`](../packages/skill/tool-skill) |
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:231`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-reference`](../packages/context/session-reference), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-skill`](../packages/skill/tool-skill) |
| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:244`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent) |
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:260`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`llm-retry`](../packages/llm/llm-retry) |
| `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:217`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`team`](../packages/experimental/team) |
@@ -42,7 +42,7 @@
| `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
| `session/created` | `emit` | [`packages/core/session/src/index.ts:54`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`time-context`](../packages/context/time-context), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:64`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`team`](../packages/experimental/team) |
| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-presets`](../packages/preset/agent-presets), `apiproxy`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`team`](../packages/experimental/team), [`token-meter`](../packages/llm/token-meter), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-presets`](../packages/preset/agent-presets), `apiproxy`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`team`](../packages/experimental/team), [`token-meter`](../packages/llm/token-meter), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:85`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) |
| `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:48`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `apiproxy` |
| `settings/updated` | `emit` | [`packages/settings/settings/src/types.ts:35`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) |
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/module-graph.md
module-graph.md: 85a13ae2e1a6eb37ea691211c18a98ac9bd0910a
module-graph.zh.md: 14e54eda19f8f0b9d67c216b88447b05b724bcff
module-graph.md: ac5faf74ca156b859859c08d0c9872c332b7fd1c
module-graph.zh.md: 71de2ac3d566bec5f878619ca68ea779ddabc387
+29 -3
View File
@@ -139,6 +139,7 @@ flowchart TD
pkg_client_ui_permission_presets["client-ui-permission-presets"]
pkg_client_ui_plan["client-ui-plan"]
pkg_client_ui_primitives["client-ui-primitives"]
pkg_client_ui_reference["client-ui-reference"]
pkg_client_ui_renderer["client-ui-renderer"]
pkg_client_ui_settings["client-ui-settings"]
pkg_client_ui_settings_general["client-ui-settings-general"]
@@ -170,6 +171,8 @@ flowchart TD
end
subgraph group_context["packages/context"]
pkg_agent_instructions["agent-instructions"]
pkg_file_reference["file-reference"]
pkg_file_reference_local["file-reference-local"]
pkg_session_reference["session-reference"]
pkg_time_context["time-context"]
pkg_tmux_context["tmux-context"]
@@ -511,6 +514,9 @@ flowchart TD
pkg_web_search_deepseek --> pkg_web
pkg_spill_local --> pkg_invariants
pkg_spill_local --> pkg_spill
pkg_file_reference --> pkg_agent
pkg_file_reference --> pkg_invariants
pkg_file_reference --> pkg_typert_protocol
pkg_time_context --> pkg_agent
pkg_time_context --> pkg_invariants
pkg_time_context --> pkg_session
@@ -835,6 +841,11 @@ flowchart TD
pkg_agent_instructions --> pkg_llm
pkg_agent_instructions --> pkg_session
pkg_agent_instructions --> pkg_tools
pkg_file_reference_local --> pkg_agent
pkg_file_reference_local --> pkg_file_reference
pkg_file_reference_local --> pkg_invariants
pkg_file_reference_local --> pkg_system_prompt
pkg_file_reference_local --> pkg_tools
pkg_session_reference --> pkg_agent
pkg_session_reference --> pkg_compaction
pkg_session_reference --> pkg_invariants
@@ -842,6 +853,7 @@ flowchart TD
pkg_session_reference --> pkg_output_retention
pkg_session_reference --> pkg_session
pkg_session_reference --> pkg_session_query
pkg_session_reference --> pkg_typert_protocol
pkg_cordis_host_runner --> pkg_agent
pkg_cordis_host_runner --> pkg_brand
pkg_cordis_host_runner --> pkg_invariants
@@ -1167,6 +1179,7 @@ flowchart TD
pkg_api_remotes --> pkg_commands
pkg_api_remotes --> pkg_cordis_host_runner
pkg_api_remotes --> pkg_credentials
pkg_api_remotes --> pkg_file_reference
pkg_api_remotes --> pkg_goal
pkg_api_remotes --> pkg_host_plugin_inventory
pkg_api_remotes --> pkg_invariants
@@ -1174,6 +1187,7 @@ flowchart TD
pkg_api_remotes --> pkg_message_feedback
pkg_api_remotes --> pkg_session
pkg_api_remotes --> pkg_session_persistence
pkg_api_remotes --> pkg_session_reference
pkg_api_remotes --> pkg_settings
pkg_api_remotes --> pkg_typert_registry
pkg_client_runtime --> pkg_agent
@@ -1211,6 +1225,7 @@ flowchart TD
pkg_client_test_runtime --> pkg_invariants
pkg_client_ui_input_trigger --> pkg_client_locale
pkg_client_ui_input_trigger --> pkg_client_runtime
pkg_client_ui_input_trigger --> pkg_file_reference
pkg_client_ui_input_trigger --> pkg_invariants
pkg_client_ui_settings_models --> pkg_api_remotes
pkg_client_ui_settings_models --> pkg_client_connection
@@ -1240,6 +1255,14 @@ flowchart TD
pkg_client_ui_layout --> pkg_client_runtime
pkg_client_ui_layout --> pkg_client_ui_theme
pkg_client_ui_layout --> pkg_invariants
pkg_client_ui_reference --> pkg_api_remotes
pkg_client_ui_reference --> pkg_client_locale
pkg_client_ui_reference --> pkg_client_runtime
pkg_client_ui_reference --> pkg_client_ui_input_trigger
pkg_client_ui_reference --> pkg_file_reference
pkg_client_ui_reference --> pkg_invariants
pkg_client_ui_reference --> pkg_session_reference
pkg_client_ui_reference --> pkg_typert_protocol
pkg_cordis_client_runner --> pkg_api_remotes
pkg_cordis_client_runner --> pkg_client_connection
pkg_cordis_client_runner --> pkg_client_modules
@@ -1498,6 +1521,7 @@ flowchart TD
| [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
| [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`launch-environment`](../packages/util/launch-environment), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`web`](../packages/web/web) |
| [`spill-local`](../packages/spill/spill-local) | `spill` | [`invariants`](../packages/runtime-diagnostics/invariants), [`spill`](../packages/spill/spill) |
| [`file-reference`](../packages/context/file-reference) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-protocol`](../packages/typert/protocol) |
| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
| [`message-feedback`](../packages/feedback/message-feedback) | `feedback` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol) |
| [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
@@ -1558,7 +1582,8 @@ flowchart TD
| [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`command-compact`](../packages/compaction/command-compact) | `compaction` | [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`agent-instructions`](../packages/context/agent-instructions) | `context` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools) |
| [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query) |
| [`file-reference-local`](../packages/context/file-reference-local) | `context` | [`agent`](../packages/core/agent), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`typert-protocol`](../packages/typert/protocol) |
| [`cordis-host-runner`](../packages/extensions/cordis-host-runner) | `extensions` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) |
| [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder) | `guard` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`tools`](../packages/core/tools) |
| [`tool-call-timeout-policy`](../packages/guard/timeout-policy) | `guard` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
@@ -1609,18 +1634,19 @@ flowchart TD
| [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) |
| [`api-gateway`](../packages/api/gateway) | `api` | [`client-connection`](../packages/client/connection), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-registry`](../packages/typert/registry) |
| [`acp-demo`](../packages/examples/acp-demo) | `examples` | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-spine-demo`](../packages/examples/agent-spine-demo), [`app-boot`](../packages/boot/app-boot), [`invariants`](../packages/runtime-diagnostics/invariants), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`tools`](../packages/core/tools) |
| [`api-remotes`](../packages/api/remotes) | `api` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`commands`](../packages/interaction/commands), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`credentials`](../packages/credentials/credentials), [`goal`](../packages/goal/goal), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`settings`](../packages/settings/settings), [`typert-registry`](../packages/typert/registry) |
| [`api-remotes`](../packages/api/remotes) | `api` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`commands`](../packages/interaction/commands), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`credentials`](../packages/credentials/credentials), [`file-reference`](../packages/context/file-reference), [`goal`](../packages/goal/goal), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-reference`](../packages/context/session-reference), [`settings`](../packages/settings/settings), [`typert-registry`](../packages/typert/registry) |
| [`client-runtime`](../packages/client/runtime) | `client` | [`agent`](../packages/core/agent), [`api-remotes`](../packages/api/remotes), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`commands`](../packages/interaction/commands), [`host-apiproxy`](../packages/host/apiproxy), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-title`](../packages/session/session-title), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry) |
| [`client-ui-renderer`](../packages/client/ui-renderer) | `client` | [`client-runtime`](../packages/client/runtime), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-settings`](../packages/client/ui-settings) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
| [`client-locale`](../packages/client/locale) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
| [`client-test-runtime`](../packages/test-support/client-runtime) | `test-support` | [`client-runtime`](../packages/client/runtime), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-slots`](../packages/client/ui-slots), [`host-apiproxy`](../packages/host/apiproxy), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-input-trigger`](../packages/client/ui-input-trigger) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-input-trigger`](../packages/client/ui-input-trigger) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-settings-models`](../packages/client/ui-settings-models) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-settings-plugin-inventory`](../packages/client/ui-settings-plugin-inventory) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-settings-plugins`](../packages/client/ui-settings-plugins) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-theme`](../packages/client/ui-theme) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-settings`](../packages/client/ui-settings), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
| [`client-ui-layout`](../packages/client/ui-layout) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-reference`](../packages/client/ui-reference) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants), [`session-reference`](../packages/context/session-reference), [`typert-protocol`](../packages/typert/protocol) |
| [`cordis-client-runner`](../packages/extensions/cordis-client-runner) | `extensions` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`client-runtime`](../packages/client/runtime), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`agent`](../packages/core/agent), [`api-remotes`](../packages/api/remotes), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-layout`](../packages/client/ui-layout), [`client-ui-settings`](../packages/client/ui-settings), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`session-stats`](../packages/session/session-stats), [`settings`](../packages/settings/settings), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`tools`](../packages/core/tools) |
| [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-layout`](../packages/client/ui-layout), [`invariants`](../packages/runtime-diagnostics/invariants) |
+29 -3
View File
@@ -141,6 +141,7 @@ flowchart TD
pkg_client_ui_permission_presets["client-ui-permission-presets"]
pkg_client_ui_plan["client-ui-plan"]
pkg_client_ui_primitives["client-ui-primitives"]
pkg_client_ui_reference["client-ui-reference"]
pkg_client_ui_renderer["client-ui-renderer"]
pkg_client_ui_settings["client-ui-settings"]
pkg_client_ui_settings_general["client-ui-settings-general"]
@@ -172,6 +173,8 @@ flowchart TD
end
subgraph group_context["packages/context"]
pkg_agent_instructions["agent-instructions"]
pkg_file_reference["file-reference"]
pkg_file_reference_local["file-reference-local"]
pkg_session_reference["session-reference"]
pkg_time_context["time-context"]
pkg_tmux_context["tmux-context"]
@@ -513,6 +516,9 @@ flowchart TD
pkg_web_search_deepseek --> pkg_web
pkg_spill_local --> pkg_invariants
pkg_spill_local --> pkg_spill
pkg_file_reference --> pkg_agent
pkg_file_reference --> pkg_invariants
pkg_file_reference --> pkg_typert_protocol
pkg_time_context --> pkg_agent
pkg_time_context --> pkg_invariants
pkg_time_context --> pkg_session
@@ -837,6 +843,11 @@ flowchart TD
pkg_agent_instructions --> pkg_llm
pkg_agent_instructions --> pkg_session
pkg_agent_instructions --> pkg_tools
pkg_file_reference_local --> pkg_agent
pkg_file_reference_local --> pkg_file_reference
pkg_file_reference_local --> pkg_invariants
pkg_file_reference_local --> pkg_system_prompt
pkg_file_reference_local --> pkg_tools
pkg_session_reference --> pkg_agent
pkg_session_reference --> pkg_compaction
pkg_session_reference --> pkg_invariants
@@ -844,6 +855,7 @@ flowchart TD
pkg_session_reference --> pkg_output_retention
pkg_session_reference --> pkg_session
pkg_session_reference --> pkg_session_query
pkg_session_reference --> pkg_typert_protocol
pkg_cordis_host_runner --> pkg_agent
pkg_cordis_host_runner --> pkg_brand
pkg_cordis_host_runner --> pkg_invariants
@@ -1169,6 +1181,7 @@ flowchart TD
pkg_api_remotes --> pkg_commands
pkg_api_remotes --> pkg_cordis_host_runner
pkg_api_remotes --> pkg_credentials
pkg_api_remotes --> pkg_file_reference
pkg_api_remotes --> pkg_goal
pkg_api_remotes --> pkg_host_plugin_inventory
pkg_api_remotes --> pkg_invariants
@@ -1176,6 +1189,7 @@ flowchart TD
pkg_api_remotes --> pkg_message_feedback
pkg_api_remotes --> pkg_session
pkg_api_remotes --> pkg_session_persistence
pkg_api_remotes --> pkg_session_reference
pkg_api_remotes --> pkg_settings
pkg_api_remotes --> pkg_typert_registry
pkg_client_runtime --> pkg_agent
@@ -1213,6 +1227,7 @@ flowchart TD
pkg_client_test_runtime --> pkg_invariants
pkg_client_ui_input_trigger --> pkg_client_locale
pkg_client_ui_input_trigger --> pkg_client_runtime
pkg_client_ui_input_trigger --> pkg_file_reference
pkg_client_ui_input_trigger --> pkg_invariants
pkg_client_ui_settings_models --> pkg_api_remotes
pkg_client_ui_settings_models --> pkg_client_connection
@@ -1242,6 +1257,14 @@ flowchart TD
pkg_client_ui_layout --> pkg_client_runtime
pkg_client_ui_layout --> pkg_client_ui_theme
pkg_client_ui_layout --> pkg_invariants
pkg_client_ui_reference --> pkg_api_remotes
pkg_client_ui_reference --> pkg_client_locale
pkg_client_ui_reference --> pkg_client_runtime
pkg_client_ui_reference --> pkg_client_ui_input_trigger
pkg_client_ui_reference --> pkg_file_reference
pkg_client_ui_reference --> pkg_invariants
pkg_client_ui_reference --> pkg_session_reference
pkg_client_ui_reference --> pkg_typert_protocol
pkg_cordis_client_runner --> pkg_api_remotes
pkg_cordis_client_runner --> pkg_client_connection
pkg_cordis_client_runner --> pkg_client_modules
@@ -1500,6 +1523,7 @@ flowchart TD
| [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
| [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`launch-environment`](../packages/util/launch-environment), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`web`](../packages/web/web) |
| [`spill-local`](../packages/spill/spill-local) | `spill` | [`invariants`](../packages/runtime-diagnostics/invariants), [`spill`](../packages/spill/spill) |
| [`file-reference`](../packages/context/file-reference) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-protocol`](../packages/typert/protocol) |
| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
| [`message-feedback`](../packages/feedback/message-feedback) | `feedback` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol) |
| [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
@@ -1560,7 +1584,8 @@ flowchart TD
| [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`command-compact`](../packages/compaction/command-compact) | `compaction` | [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`agent-instructions`](../packages/context/agent-instructions) | `context` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools) |
| [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query) |
| [`file-reference-local`](../packages/context/file-reference-local) | `context` | [`agent`](../packages/core/agent), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`typert-protocol`](../packages/typert/protocol) |
| [`cordis-host-runner`](../packages/extensions/cordis-host-runner) | `extensions` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) |
| [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder) | `guard` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`tools`](../packages/core/tools) |
| [`tool-call-timeout-policy`](../packages/guard/timeout-policy) | `guard` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
@@ -1611,18 +1636,19 @@ flowchart TD
| [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) |
| [`api-gateway`](../packages/api/gateway) | `api` | [`client-connection`](../packages/client/connection), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-registry`](../packages/typert/registry) |
| [`acp-demo`](../packages/examples/acp-demo) | `examples` | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-spine-demo`](../packages/examples/agent-spine-demo), [`app-boot`](../packages/boot/app-boot), [`invariants`](../packages/runtime-diagnostics/invariants), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`tools`](../packages/core/tools) |
| [`api-remotes`](../packages/api/remotes) | `api` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`commands`](../packages/interaction/commands), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`credentials`](../packages/credentials/credentials), [`goal`](../packages/goal/goal), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`settings`](../packages/settings/settings), [`typert-registry`](../packages/typert/registry) |
| [`api-remotes`](../packages/api/remotes) | `api` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`commands`](../packages/interaction/commands), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`credentials`](../packages/credentials/credentials), [`file-reference`](../packages/context/file-reference), [`goal`](../packages/goal/goal), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-reference`](../packages/context/session-reference), [`settings`](../packages/settings/settings), [`typert-registry`](../packages/typert/registry) |
| [`client-runtime`](../packages/client/runtime) | `client` | [`agent`](../packages/core/agent), [`api-remotes`](../packages/api/remotes), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`commands`](../packages/interaction/commands), [`host-apiproxy`](../packages/host/apiproxy), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-title`](../packages/session/session-title), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry) |
| [`client-ui-renderer`](../packages/client/ui-renderer) | `client` | [`client-runtime`](../packages/client/runtime), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-settings`](../packages/client/ui-settings) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
| [`client-locale`](../packages/client/locale) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
| [`client-test-runtime`](../packages/test-support/client-runtime) | `test-support` | [`client-runtime`](../packages/client/runtime), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-slots`](../packages/client/ui-slots), [`host-apiproxy`](../packages/host/apiproxy), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-input-trigger`](../packages/client/ui-input-trigger) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-input-trigger`](../packages/client/ui-input-trigger) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-settings-models`](../packages/client/ui-settings-models) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-settings-plugin-inventory`](../packages/client/ui-settings-plugin-inventory) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-settings-plugins`](../packages/client/ui-settings-plugins) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-theme`](../packages/client/ui-theme) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-settings`](../packages/client/ui-settings), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
| [`client-ui-layout`](../packages/client/ui-layout) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-reference`](../packages/client/ui-reference) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants), [`session-reference`](../packages/context/session-reference), [`typert-protocol`](../packages/typert/protocol) |
| [`cordis-client-runner`](../packages/extensions/cordis-client-runner) | `extensions` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`client-runtime`](../packages/client/runtime), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`agent`](../packages/core/agent), [`api-remotes`](../packages/api/remotes), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-layout`](../packages/client/ui-layout), [`client-ui-settings`](../packages/client/ui-settings), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`session-stats`](../packages/session/session-stats), [`settings`](../packages/settings/settings), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`tools`](../packages/core/tools) |
| [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-layout`](../packages/client/ui-layout), [`invariants`](../packages/runtime-diagnostics/invariants) |
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/persistence-catalog.md
persistence-catalog.md: b680bccf22f7840663e5268eb3feeb6b16f7fd42
persistence-catalog.zh.md: 4b50582fa55dbdc672d8c45debe108b97a55f0e2
persistence-catalog.md: cbe9cec195ad899ba27dd08743d64090ae7277f2
persistence-catalog.zh.md: 8057a0910e505b95872c341283d1acdbf033faaf
+14 -10
View File
@@ -90,7 +90,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
}[T]
```
Sources: [`packages/core/session/src/types.ts:336`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:343`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:372`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:404`](../packages/core/session/src/types.ts)
Sources: [`packages/core/session/src/types.ts:340`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:347`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:376`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:408`](../packages/core/session/src/types.ts)
## Events
@@ -226,14 +226,18 @@ Source: [`packages/core/session/src/types.ts:266`](../packages/core/session/src/
* Assembled assistant message for one step (derived history uses this).
* Carries the step's `usage` when the adapter reported token accounting, so
* the model output and its accounting travel together (there is no separate
* usage record). `usage` is absent when the adapter reported none.
* usage record). `usage` is absent when the adapter reported none. A turn
* cancelled mid-stream finalizes its delivered text/reasoning prefix as this
* event with `interrupted: true`; undispatched tool calls are absent. The
* marker distinguishes that prefix without re-deriving interruption from turn
* boundaries. An aborted turn with no such event streamed no visible content.
*/
'assistant/message': { turn: number; step: number; message: AssistantMessage; usage?: TokenUsage }
'assistant/message': { turn: number; step: number; message: AssistantMessage; usage?: TokenUsage; interrupted?: true }
```
Types: [TokenUsage](subsystems/llm-streaming.md)
Source: [`packages/core/session/src/types.ts:273`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:277`](../packages/core/session/src/types.ts)
### `command/*`
@@ -543,7 +547,7 @@ Source: [`packages/plan/plan-mode/src/index.ts:54`](../packages/plan/plan-mode/s
'request/context': RequestContext
```
Source: [`packages/core/session/src/types.ts:309`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:313`](../packages/core/session/src/types.ts)
<a id="requestheader--log-only"></a>
@@ -557,7 +561,7 @@ Source: [`packages/core/session/src/types.ts:309`](../packages/core/session/src/
'request/header': { header: EpochHeader; reason: RequestHeaderReason }
```
Source: [`packages/core/session/src/types.ts:304`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:308`](../packages/core/session/src/types.ts)
### `sandbox/*`
@@ -632,7 +636,7 @@ Source: [`packages/schedule/schedule/src/types.ts:219`](../packages/schedule/sch
'session/end-seed': Record<string, never>
```
Source: [`packages/core/session/src/types.ts:332`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:336`](../packages/core/session/src/types.ts)
<a id="sessiontitle--log-only"></a>
@@ -778,7 +782,7 @@ Source: [`packages/experimental/team/src/types.ts:208`](../packages/experimental
Types: [TodoItem](subsystems/session.md)
Source: [`packages/core/session/src/types.ts:299`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:303`](../packages/core/session/src/types.ts)
### `tool/*`
@@ -797,7 +801,7 @@ Source: [`packages/core/session/src/types.ts:299`](../packages/core/session/src/
Types: [CallId](subsystems/core.md)
Source: [`packages/core/session/src/types.ts:279`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:283`](../packages/core/session/src/types.ts)
<a id="toolcode-dispatch--log-only"></a>
@@ -872,7 +876,7 @@ Source: [`packages/core/tools/src/types.ts:40`](../packages/core/tools/src/types
}
```
Source: [`packages/core/session/src/types.ts:291`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:295`](../packages/core/session/src/types.ts)
### `tool-workflow/*`
+14 -10
View File
@@ -92,7 +92,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
}[T]
```
来源:[`packages/core/session/src/types.ts:336`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:343`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:372`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:404`](../packages/core/session/src/types.ts)
来源:[`packages/core/session/src/types.ts:340`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:347`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:376`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:408`](../packages/core/session/src/types.ts)
## 事件
@@ -228,14 +228,18 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
* Assembled assistant message for one step (derived history uses this).
* Carries the step's `usage` when the adapter reported token accounting, so
* the model output and its accounting travel together (there is no separate
* usage record). `usage` is absent when the adapter reported none.
* usage record). `usage` is absent when the adapter reported none. A turn
* cancelled mid-stream finalizes its delivered text/reasoning prefix as this
* event with `interrupted: true`; undispatched tool calls are absent. The
* marker distinguishes that prefix without re-deriving interruption from turn
* boundaries. An aborted turn with no such event streamed no visible content.
*/
'assistant/message': { turn: number; step: number; message: AssistantMessage; usage?: TokenUsage }
'assistant/message': { turn: number; step: number; message: AssistantMessage; usage?: TokenUsage; interrupted?: true }
```
类型:[TokenUsage](subsystems/llm-streaming.md)
来源:[`packages/core/session/src/types.ts:273`](../packages/core/session/src/types.ts)
来源:[`packages/core/session/src/types.ts:277`](../packages/core/session/src/types.ts)
### `command/*`
@@ -545,7 +549,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
'request/context': RequestContext
```
来源:[`packages/core/session/src/types.ts:309`](../packages/core/session/src/types.ts)
来源:[`packages/core/session/src/types.ts:313`](../packages/core/session/src/types.ts)
<a id="requestheader--log-only"></a>
@@ -559,7 +563,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
'request/header': { header: EpochHeader; reason: RequestHeaderReason }
```
来源:[`packages/core/session/src/types.ts:304`](../packages/core/session/src/types.ts)
来源:[`packages/core/session/src/types.ts:308`](../packages/core/session/src/types.ts)
### `sandbox/*`
@@ -634,7 +638,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
'session/end-seed': Record<string, never>
```
来源:[`packages/core/session/src/types.ts:332`](../packages/core/session/src/types.ts)
来源:[`packages/core/session/src/types.ts:336`](../packages/core/session/src/types.ts)
<a id="sessiontitle--log-only"></a>
@@ -780,7 +784,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
类型:[TodoItem](subsystems/session.md)
来源:[`packages/core/session/src/types.ts:299`](../packages/core/session/src/types.ts)
来源:[`packages/core/session/src/types.ts:303`](../packages/core/session/src/types.ts)
### `tool/*`
@@ -799,7 +803,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
类型:[CallId](subsystems/core.md)
来源:[`packages/core/session/src/types.ts:279`](../packages/core/session/src/types.ts)
来源:[`packages/core/session/src/types.ts:283`](../packages/core/session/src/types.ts)
<a id="toolcode-dispatch--log-only"></a>
@@ -874,7 +878,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
}
```
来源:[`packages/core/session/src/types.ts:291`](../packages/core/session/src/types.ts)
来源:[`packages/core/session/src/types.ts:295`](../packages/core/session/src/types.ts)
### `tool-workflow/*`
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/subsystems/llm-streaming.md
llm-streaming.md: c1b2ab5f1e0926864f25409c021691d078df9e0f
llm-streaming.zh.md: 7bf04bf7a4a7de63d20eb67461842ff49f3b189d
llm-streaming.md: 8731a535267bddd2bf99a06a2beee92d3083232d
llm-streaming.zh.md: b9e71bba7428dafbbc7a366a97c2d7ddc9a87d46
+10 -1
View File
@@ -298,7 +298,8 @@ One keep/drop decision covers content and metadata together: a `max-tokens` fini
* {@link ContentBlock}s and a final assistant {@link Message}.
*
* The agent loop feeds it while logging raw chunks for replay fidelity, then
* reads `blocks()` / `message()` / `usage` / `finish` once the stream ends.
* reads `blocks()` / `message()` / `usage` / `finish` once the stream ends,
* or `interruptedBlocks()` when cancellation cut the stream short.
*
* Tolerant of delta-only protocols (no block-start/end); deltas arriving for
* an index already closed by `block-end` are ignored (malformed stream) so a
@@ -317,6 +318,14 @@ declare class BlockAssembler {
* its accumulated deltas (an unknown block type never closed by `block-end` throws).
*/
blocks(): ContentBlock[];
/**
* Assemble the prefix an interrupted stream can safely finalize: closed and
* open text/reasoning blocks with non-whitespace content, in stream order.
* Tool calls are omitted because interruption precedes dispatch; retaining
* one would require a fabricated result. Open unknown blocks are also omitted.
* @returns the kept blocks; empty when nothing streamed before the interruption.
*/
interruptedBlocks(): ContentBlock[];
/** Usage from the `usage` chunk; undefined until one arrives. */
get usage(): TokenUsage | undefined;
/** Finish reason from the `finish` chunk; `{kind: 'stop'}` when the stream ended without one. */
+10 -1
View File
@@ -304,7 +304,8 @@ interface TokenUsage {
* {@link ContentBlock}s and a final assistant {@link Message}.
*
* The agent loop feeds it while logging raw chunks for replay fidelity, then
* reads `blocks()` / `message()` / `usage` / `finish` once the stream ends.
* reads `blocks()` / `message()` / `usage` / `finish` once the stream ends,
* or `interruptedBlocks()` when cancellation cut the stream short.
*
* Tolerant of delta-only protocols (no block-start/end); deltas arriving for
* an index already closed by `block-end` are ignored (malformed stream) so a
@@ -323,6 +324,14 @@ declare class BlockAssembler {
* its accumulated deltas (an unknown block type never closed by `block-end` throws).
*/
blocks(): ContentBlock[];
/**
* Assemble the prefix an interrupted stream can safely finalize: closed and
* open text/reasoning blocks with non-whitespace content, in stream order.
* Tool calls are omitted because interruption precedes dispatch; retaining
* one would require a fabricated result. Open unknown blocks are also omitted.
* @returns the kept blocks; empty when nothing streamed before the interruption.
*/
interruptedBlocks(): ContentBlock[];
/** Usage from the `usage` chunk; undefined until one arrives. */
get usage(): TokenUsage | undefined;
/** Finish reason from the `finish` chunk; `{kind: 'stop'}` when the stream ended without one. */
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/subsystems/session-reference.md
session-reference.md: 4140eb2a27d79ee0975d97ae9e96a1b502d74187
session-reference.zh.md: 9dcfdbd07d4df1c54c51dc06d36af2f052969783
session-reference.md: e3ef2c00b7ddbeed9fa3f9d074df436d8c0cc7b5
session-reference.zh.md: 74063c1525a1f7f67663e9b11148e18902b22748
+71 -5
View File
@@ -2,9 +2,23 @@
English | [中文](session-reference.zh.md)
Structured cross-session reference requests and prepared message contexts. The [package contract](../../packages/context/session-reference) defines canonical URIs, current-surface projection, tag-safe JSON and byte retention, stable errors, and the untrusted model prompt. Host adapters use these types instead of passing their UI mention syntax into the agent core.
Host-backed file discovery plus structured cross-session reference requests and prepared message contexts. The [file-reference contract](../../packages/context/file-reference) owns path-only completion records and grammar; the [session-reference contract](../../packages/context/session-reference) defines canonical URIs, current-surface projection, tag-safe JSON and byte retention, stable errors, and the untrusted model prompt. Host adapters use these types instead of passing their UI mention syntax into the agent core.
Source: [`packages/context/session-reference/src/types.ts`](../../packages/context/session-reference/src/types.ts)
Sources: [`packages/context/file-reference/src/types.ts`](../../packages/context/file-reference/src/types.ts) · [`packages/context/session-reference/src/types.ts`](../../packages/context/session-reference/src/types.ts)
## File candidates
`FileReferenceCandidate` is the path-only discovery result. The addressed agent supplies the working-directory scope; providers decide ranking and namespace access without reading file contents.
```ts type-equiv
/** One path-only completion candidate inside the target session cwd. */
interface FileReferenceCandidate {
/** User-facing path accepted by normal prompts and filesystem tools. */
path: string
/** Directories keep completion open; files finish the mention. */
kind: 'file' | 'directory'
}
```
## Inputs and candidates
@@ -36,6 +50,16 @@ interface SessionReferenceCandidate {
}
```
The `sessionReferenceResolver/candidates` Remote method serves the same discovery to browser consumers and attaches each candidate's canonical prompt mention.
```ts type-equiv
/** One discovery candidate carrying its canonical prompt mention. */
interface SessionReferenceMentionCandidate extends SessionReferenceCandidate {
/** Canonical `@[label](dsh-session:…)` mention serialized into the prompt draft. */
mention: string
}
```
## Prepared messages
Preparation preserves readable current-message content and returns at most one aggregated context.
@@ -74,6 +98,37 @@ type SessionReferenceErrorCode =
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxfilereferences--filereferenceservice-abstract-seam"></a>
### `ctx.fileReferences``FileReferenceService` (abstract seam)
Host capability for cancellable file-reference discovery.
```ts cordis-catalog
/**
* List file and directory candidates for one agent's working directory.
* @param agent - target agent whose session cwd bounds discovery.
* @param query - path text following `@` or `@"`.
* @param signal - caller cancellation.
* @returns deterministic path-only candidates.
*/
abstract list( agent: Agent, query: string, signal: AbortSignal, ): Promise<FileReferenceCandidate[]>
/**
* Remote face of {@link list}; the decorator cannot mark the abstract
* member, so this concrete adapter carries the identical contract.
* @param agent - target agent whose session cwd bounds discovery.
* @param query - path text following `@` or `@"`.
* @param signal - caller cancellation.
* @returns deterministic path-only candidates.
*/
@Remote('list') remoteExportList( agent: Agent, query: string, signal: AbortSignal, ): Promise<FileReferenceCandidate[]>
```
Types: [Agent](core.md)
Source: [`packages/context/file-reference/src/index.ts:27`](../../packages/context/file-reference/src/index.ts)
<a id="ctxsessionreferenceresolver--sessionreferenceresolver"></a>
### `ctx.sessionReferenceResolver``SessionReferenceResolver`
@@ -92,11 +147,22 @@ Exact-read consumer that prepares immutable cross-session message context.
async listCandidates( agent: Agent, query: string = '', limit: number = this.config.candidateLimit, signal?: AbortSignal, ): Promise<SessionReferenceCandidate[]>
/**
* Snapshot all references before enqueue and return one aggregated durable context.
* Remote face of {@link listCandidates}: the configured candidate limit
* applies, and every candidate carries the canonical mention a host inserts
* into the prompt draft.
* @param agent - target agent; self is excluded and its cwd drives ranking.
* @param query - optional case-insensitive session-id/cwd/title substring.
* @param signal - caller cancellation.
* @returns mention-carrying candidates in rank order.
*/
@Remote('candidates') async remoteExportCandidates( agent: Agent, query: string, signal: AbortSignal, ): Promise<SessionReferenceMentionCandidate[]>
/**
* Snapshot all references for one accepted direct message and return one aggregated durable context.
* @param agent - target agent; references to it are rejected.
* @param content - already host-normalized readable message content.
* @param references - structured source sessions in mention order.
* @param signal - optional cancellation boundary for host request teardown.
* @param signal - optional cancellation boundary for the active turn.
* @returns detached content and optional referenced-session context.
*/
async prepare( agent: Agent, content: ContentBlock[], references: SessionReferenceInput[], signal?: AbortSignal, ): Promise<PreparedReferencedMessage>
@@ -104,5 +170,5 @@ async prepare( agent: Agent, content: ContentBlock[], references: SessionReferen
Types: [Agent](core.md) · [ContentBlock](llm-streaming.md)
Source: [`packages/context/session-reference/src/index.ts:70`](../../packages/context/session-reference/src/index.ts)
Source: [`packages/context/session-reference/src/index.ts:75`](../../packages/context/session-reference/src/index.ts)
<!-- END GENERATED cordis-surface -->
+71 -5
View File
@@ -2,9 +2,23 @@
[English](session-reference.md) | 中文
结构化的跨会话引用请求与准备后的消息上下文。[约定](../../packages/context/session-reference) 定义规范 URI、当前表层投影、标签安全的 JSON 与字节保留、稳定错误和不可信的模型提示词。宿主适配器使用这些类型,而不会把各自 UI 的提及语法传入 agent(智能体)核心。
由 Host 支撑的文件发现,以及结构化的跨会话引用请求与准备后的消息上下文。[文件引用约定](../../packages/context/file-reference)负责仅含路径的补全记录与语法;[会话引用约定](../../packages/context/session-reference)定义规范 URI、当前表层投影、标签安全的 JSON 与字节保留、稳定错误和不可信的模型提示词。宿主适配器使用这些类型,而不会把各自 UI 的提及语法传入 agent(智能体)核心。
来源:[`packages/context/session-reference/src/types.ts`](../../packages/context/session-reference/src/types.ts)
来源:[`packages/context/file-reference/src/types.ts`](../../packages/context/file-reference/src/types.ts) · [`packages/context/session-reference/src/types.ts`](../../packages/context/session-reference/src/types.ts)
## 文件候选项
`FileReferenceCandidate` 是仅含路径的发现结果。被寻址的 agent 提供工作目录范围;提供方负责排序和命名空间访问,但不会读取文件内容。
```ts type-equiv
/** One path-only completion candidate inside the target session cwd. */
interface FileReferenceCandidate {
/** User-facing path accepted by normal prompts and filesystem tools. */
path: string
/** Directories keep completion open; files finish the mention. */
kind: 'file' | 'directory'
}
```
## 输入与候选项
@@ -36,6 +50,16 @@ interface SessionReferenceCandidate {
}
```
`sessionReferenceResolver/candidates` Remote 方法向浏览器消费方提供同一发现能力,并为每个候选附上规范提示词 mention。
```ts type-equiv
/** One discovery candidate carrying its canonical prompt mention. */
interface SessionReferenceMentionCandidate extends SessionReferenceCandidate {
/** Canonical `@[label](dsh-session:…)` mention serialized into the prompt draft. */
mention: string
}
```
## 准备后的消息
准备过程保留可读的当前消息内容,并最多返回一个聚合上下文。
@@ -74,6 +98,37 @@ type SessionReferenceErrorCode =
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxfilereferences--filereferenceservice-abstract-seam"></a>
### `ctx.fileReferences``FileReferenceService` (abstract seam)
Host capability for cancellable file-reference discovery.
```ts cordis-catalog
/**
* List file and directory candidates for one agent's working directory.
* @param agent - target agent whose session cwd bounds discovery.
* @param query - path text following `@` or `@"`.
* @param signal - caller cancellation.
* @returns deterministic path-only candidates.
*/
abstract list( agent: Agent, query: string, signal: AbortSignal, ): Promise<FileReferenceCandidate[]>
/**
* Remote face of {@link list}; the decorator cannot mark the abstract
* member, so this concrete adapter carries the identical contract.
* @param agent - target agent whose session cwd bounds discovery.
* @param query - path text following `@` or `@"`.
* @param signal - caller cancellation.
* @returns deterministic path-only candidates.
*/
@Remote('list') remoteExportList( agent: Agent, query: string, signal: AbortSignal, ): Promise<FileReferenceCandidate[]>
```
Types: [Agent](core.md)
Source: [`packages/context/file-reference/src/index.ts:27`](../../packages/context/file-reference/src/index.ts)
<a id="ctxsessionreferenceresolver--sessionreferenceresolver"></a>
### `ctx.sessionReferenceResolver``SessionReferenceResolver`
@@ -92,11 +147,22 @@ Exact-read consumer that prepares immutable cross-session message context.
async listCandidates( agent: Agent, query: string = '', limit: number = this.config.candidateLimit, signal?: AbortSignal, ): Promise<SessionReferenceCandidate[]>
/**
* Snapshot all references before enqueue and return one aggregated durable context.
* Remote face of {@link listCandidates}: the configured candidate limit
* applies, and every candidate carries the canonical mention a host inserts
* into the prompt draft.
* @param agent - target agent; self is excluded and its cwd drives ranking.
* @param query - optional case-insensitive session-id/cwd/title substring.
* @param signal - caller cancellation.
* @returns mention-carrying candidates in rank order.
*/
@Remote('candidates') async remoteExportCandidates( agent: Agent, query: string, signal: AbortSignal, ): Promise<SessionReferenceMentionCandidate[]>
/**
* Snapshot all references for one accepted direct message and return one aggregated durable context.
* @param agent - target agent; references to it are rejected.
* @param content - already host-normalized readable message content.
* @param references - structured source sessions in mention order.
* @param signal - optional cancellation boundary for host request teardown.
* @param signal - optional cancellation boundary for the active turn.
* @returns detached content and optional referenced-session context.
*/
async prepare( agent: Agent, content: ContentBlock[], references: SessionReferenceInput[], signal?: AbortSignal, ): Promise<PreparedReferencedMessage>
@@ -104,5 +170,5 @@ async prepare( agent: Agent, content: ContentBlock[], references: SessionReferen
Types: [Agent](core.md) · [ContentBlock](llm-streaming.md)
Source: [`packages/context/session-reference/src/index.ts:70`](../../packages/context/session-reference/src/index.ts)
Source: [`packages/context/session-reference/src/index.ts:75`](../../packages/context/session-reference/src/index.ts)
<!-- END GENERATED cordis-surface -->
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/subsystems/session.md
session.md: aea9d00b38e384e7a973ce168c3a75a62e70a8bb
session.zh.md: 8c56029af5144569f1ab6df73a8fe2278f9ef5b4
session.md: 3dc8762468efd2f1515ae10e7400a7d85941b461
session.zh.md: 3edbe86c3dea7dcfaa3c1fef6a5ca9e4a4b2c36a
+6 -2
View File
@@ -59,9 +59,13 @@ interface SessionEventMap {
* Assembled assistant message for one step (derived history uses this).
* Carries the step's `usage` when the adapter reported token accounting, so
* the model output and its accounting travel together (there is no separate
* usage record). `usage` is absent when the adapter reported none.
* usage record). `usage` is absent when the adapter reported none. A turn
* cancelled mid-stream finalizes its delivered text/reasoning prefix as this
* event with `interrupted: true`; undispatched tool calls are absent. The
* marker distinguishes that prefix without re-deriving interruption from turn
* boundaries. An aborted turn with no such event streamed no visible content.
*/
'assistant/message': { turn: number; step: number; message: AssistantMessage; usage?: TokenUsage }
'assistant/message': { turn: number; step: number; message: AssistantMessage; usage?: TokenUsage; interrupted?: true }
/**
* The model requested one tool invocation: `name` with the raw `arguments`
* JSON string exactly as the model produced it (unparsed). `callId` pairs the
+6 -2
View File
@@ -59,9 +59,13 @@ interface SessionEventMap {
* Assembled assistant message for one step (derived history uses this).
* Carries the step's `usage` when the adapter reported token accounting, so
* the model output and its accounting travel together (there is no separate
* usage record). `usage` is absent when the adapter reported none.
* usage record). `usage` is absent when the adapter reported none. A turn
* cancelled mid-stream finalizes its delivered text/reasoning prefix as this
* event with `interrupted: true`; undispatched tool calls are absent. The
* marker distinguishes that prefix without re-deriving interruption from turn
* boundaries. An aborted turn with no such event streamed no visible content.
*/
'assistant/message': { turn: number; step: number; message: AssistantMessage; usage?: TokenUsage }
'assistant/message': { turn: number; step: number; message: AssistantMessage; usage?: TokenUsage; interrupted?: true }
/**
* The model requested one tool invocation: `name` with the raw `arguments`
* JSON string exactly as the model produced it (unparsed). `callId` pairs the
@@ -57,6 +57,7 @@
{"type":"user/message","seq":55,"time":0,"data":{"content":[{"type":"text","text":"<goal_round>\nObjective: \"Finish the ACP goal-round-driver snapshot proof\"\nRound: 2/2\n\nContinue working toward the objective in this same session. Treat the current workspace, tool results, and durable session state as authoritative; inspect them instead of assuming earlier narration is still current. Make concrete progress and verify the result. Before claiming completion, gather evidence that the whole objective is achieved, read the current goal, and mark it complete. If work remains, leave the goal active for the next round. Follow the configured goal-tool policy before reporting a blocker.\n</goal_round>"}],"source":{"kind":"goal","goalId":"goal-{{sessionId}}","revision":1,"round":2},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
{"type":"assistant/chunk","seq":56,"time":0,"data":{"turn":3,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
{"type":"assistant/chunk","seq":57,"time":0,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":0,"text":"partial"}}}
{"type":"step/end","seq":58,"time":0,"data":{"turn":3,"step":1}}
{"type":"turn/end","seq":59,"time":0,"data":{"turn":3,"reason":{"kind":"aborted","reason":{"kind":"user"}}}}
{"type":"goal/change","seq":60,"time":0,"data":{"kind":"goal/change","version":1,"operation":"pause","goal":{"id":"goal-{{sessionId}}","revision":2,"objective":"Finish the ACP goal-round-driver snapshot proof","phase":"paused","maxGoalRounds":2},"roundsStarted":2,"createdAt":0,"updatedAt":0}}
{"type":"assistant/message","seq":58,"time":0,"data":{"turn":3,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"partial"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"interrupted":true},"sourceEventSeqs":[56,57],"surfaceOp":"append"}
{"type":"step/end","seq":59,"time":0,"data":{"turn":3,"step":1}}
{"type":"turn/end","seq":60,"time":0,"data":{"turn":3,"reason":{"kind":"aborted","reason":{"kind":"user"}}}}
{"type":"goal/change","seq":61,"time":0,"data":{"kind":"goal/change","version":1,"operation":"pause","goal":{"id":"goal-{{sessionId}}","revision":2,"objective":"Finish the ACP goal-round-driver snapshot proof","phase":"paused","maxGoalRounds":2},"roundsStarted":2,"createdAt":0,"updatedAt":0}}
@@ -3,3 +3,4 @@
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"GOAL READY"}}}}
{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"GOAL ROUND ONE"}}}}
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"partial"}}}}
@@ -10,5 +10,6 @@
{"type":"request/context","seq":8,"time":1785730444532,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
{"type":"assistant/chunk","seq":9,"time":1785498791456,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
{"type":"assistant/chunk","seq":10,"time":1785730444541,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"partial"}}}
{"type":"step/end","seq":11,"time":1785730444547,"data":{"turn":1,"step":1}}
{"type":"turn/end","seq":12,"time":1785730444547,"data":{"turn":1,"reason":{"kind":"aborted","reason":{"kind":"user"}}}}
{"type":"assistant/message","seq":11,"time":1786334791338,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"partial"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"104e9294-f9b8-4248-b7df-0b7e2a069c0a"},"interrupted":true},"sourceEventSeqs":[9,10],"surfaceOp":"append"}
{"type":"step/end","seq":12,"time":1786334791338,"data":{"turn":1,"step":1}}
{"type":"turn/end","seq":13,"time":1786334791338,"data":{"turn":1,"reason":{"kind":"aborted","reason":{"kind":"user"}}}}
@@ -1,3 +1,4 @@
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}}
{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}"}}
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"partial"}}}}
{"jsonrpc":"2.0","id":3,"result":{"stopReason":"cancelled"}}
+10
View File
@@ -502,6 +502,16 @@
"tests/**/*.ts"
]
},
"packages/context/file-reference": {
"ignoreDependencies": [
"zod"
]
},
"packages/context/session-reference": {
"ignoreDependencies": [
"zod"
]
},
"packages/session/session-checkpoint-policy": {
"entry": [
"tests/**/*.spec.ts",
+3 -1
View File
@@ -464,7 +464,9 @@ describe('ACP prompt lifecycle', () => {
await expect(harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'two' }] }))
.resolves.toEqual({ stopReason: 'end_turn' })
await vi.waitFor(() => { expect(messageText(harness!)).toBe('next') })
// 'partial' is the cancelled turn's finalized prefix update; 'next' proves
// the second prompt settled independently of the aborted turn's late end.
await vi.waitFor(() => { expect(messageText(harness!)).toBe('partialnext') })
})
it('a retry turn adopts the prompt instead of rejecting at the failed turn end', async () => {
+12 -8
View File
@@ -58,37 +58,41 @@
"@deepseek-ai/dsh-typert-protocol": "workspace:^"
},
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:^",
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-agent-presets": "workspace:^",
"@deepseek-ai/dsh-api-gateway": "workspace:^",
"@deepseek-ai/dsh-commands": "workspace:^",
"@deepseek-ai/dsh-credentials": "workspace:^",
"@deepseek-ai/dsh-goal": "workspace:^",
"@deepseek-ai/dsh-cordis-host-runner": "workspace:^",
"@deepseek-ai/dsh-credentials": "workspace:^",
"@deepseek-ai/dsh-file-reference": "workspace:^",
"@deepseek-ai/dsh-goal": "workspace:^",
"@deepseek-ai/dsh-host-plugin-inventory": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-agent-presets": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-message-feedback": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-session-persistence": "workspace:^",
"@deepseek-ai/dsh-session-reference": "workspace:^",
"@deepseek-ai/dsh-settings": "workspace:^",
"@deepseek-ai/dsh-typert-registry": "workspace:^"
"@deepseek-ai/dsh-typert-registry": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-agent-presets": "workspace:^",
"@deepseek-ai/dsh-api-gateway": "workspace:^",
"@deepseek-ai/dsh-commands": "workspace:^",
"@deepseek-ai/dsh-credentials": "workspace:^",
"@deepseek-ai/dsh-goal": "workspace:^",
"@deepseek-ai/dsh-cordis-host-runner": "workspace:^",
"@deepseek-ai/dsh-credentials": "workspace:^",
"@deepseek-ai/dsh-file-reference": "workspace:^",
"@deepseek-ai/dsh-goal": "workspace:^",
"@deepseek-ai/dsh-host-plugin-inventory": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-agent-presets": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-message-feedback": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-session-persistence": "workspace:^",
"@deepseek-ai/dsh-session-reference": "workspace:^",
"@deepseek-ai/dsh-settings": "workspace:^",
"@deepseek-ai/dsh-typert-registry": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
+10 -1
View File
@@ -4,16 +4,20 @@ import type { Context } from '@deepseek-ai/cordis'
import commandsRemote from '@deepseek-ai/dsh-commands/remote'
import goalsRemote from '@deepseek-ai/dsh-goal/remote'
import dynamicRemote from '@deepseek-ai/dsh-cordis-host-runner/remote'
import fileReferencesRemote from '@deepseek-ai/dsh-file-reference/remote'
import pluginInventoryRemote from '@deepseek-ai/dsh-host-plugin-inventory/remote'
import messageFeedbackRemote from '@deepseek-ai/dsh-message-feedback/remote'
import sessionReferencesRemote from '@deepseek-ai/dsh-session-reference/remote'
import type { TypertClientRemote } from '@deepseek-ai/dsh-typert-protocol'
export type { TypertClientRemote as ClientRemote } from '@deepseek-ai/dsh-typert-protocol'
export type { PluginInventorySnapshot } from '@deepseek-ai/dsh-host-plugin-inventory/types'
export type {} from '@deepseek-ai/dsh-commands/remote'
export type {} from '@deepseek-ai/dsh-file-reference/remote'
export type {} from '@deepseek-ai/dsh-goal/remote'
export type {} from '@deepseek-ai/dsh-host-plugin-inventory/remote'
export type {} from '@deepseek-ai/dsh-message-feedback/remote'
export type {} from '@deepseek-ai/dsh-session-reference/remote'
// The forwarded-event allowlist's selection seat: without it in the consumer's
// compilation face `TypertRemoteEvent` is `never` and every `$on` call fails.
export type { ApiRemoteForwardedEvent } from '../types.ts'
@@ -86,6 +90,10 @@ export type {
// reason: a Client contribution names what it sends without importing a Host
// package, and this assembly is where both planes legitimately meet.
export type { JsonValue } from '@deepseek-ai/dsh-session/types'
// Reference-discovery result vocabulary for the fileReferences and
// sessionReferenceResolver namespaces.
export type { FileReferenceCandidate } from '@deepseek-ai/dsh-file-reference/types'
export type { SessionReferenceMentionCandidate } from '@deepseek-ai/dsh-session-reference/types'
declare module '@deepseek-ai/cordis' {
interface Context {
@@ -106,7 +114,8 @@ export async function apply(ctx: Context): Promise<() => Promise<void>> {
const disposers: Array<() => Promise<void>> = []
try {
for (const contribution of [
commandsRemote, goalsRemote, dynamicRemote, pluginInventoryRemote, messageFeedbackRemote,
commandsRemote, goalsRemote, dynamicRemote, fileReferencesRemote,
pluginInventoryRemote, messageFeedbackRemote, sessionReferencesRemote,
]) {
disposers.push(await ctx.remote.$mount(contribution))
}
@@ -24,6 +24,12 @@
"path": "../../credentials/credentials"
},
{
"path": "../../context/file-reference"
},
{
"path": "../../context/session-reference"
},
{
"path": "../../extensions/cordis-host-runner"
},
+10 -1
View File
@@ -79,6 +79,12 @@
writeEveryEvents: 200
writeIntervalMs: 5000
- id: session-reference
name: '@deepseek-ai/dsh-session-reference'
- id: file-reference-local
name: '@deepseek-ai/dsh-file-reference-local'
# Whole-log turn/step counts for the chat stats strip (the sessionStats
# projection key); the projection registry itself is a base-layer row.
- id: session-stats
@@ -226,7 +232,7 @@
name: '@deepseek-ai/dsh-client-ui-workspace'
# Input triggers: the '/' | '@' pipeline (ui-input-trigger), the command surface over
# it (ui-commands), and the two reference sources (ui-skill / ui-subagent).
# it (ui-commands), and the reference sources (ui-skill / ui-reference).
- id: ui-input-trigger
name: '@deepseek-ai/dsh-client-ui-input-trigger'
@@ -239,6 +245,9 @@
- id: ui-subagent
name: '@deepseek-ai/dsh-client-ui-subagent'
- id: ui-reference
name: '@deepseek-ai/dsh-client-ui-reference'
# Background jobs: the session-header list over the jobsBySession mirror.
- id: ui-jobs
name: '@deepseek-ai/dsh-client-ui-jobs'
+4
View File
@@ -76,6 +76,7 @@
"@deepseek-ai/dsh-client-ui-sidebar": "workspace:^",
"@deepseek-ai/dsh-client-ui-skill": "workspace:^",
"@deepseek-ai/dsh-client-ui-input-trigger": "workspace:^",
"@deepseek-ai/dsh-client-ui-reference": "workspace:^",
"@deepseek-ai/dsh-client-ui-subagent": "workspace:^",
"@deepseek-ai/dsh-client-ui-jobs": "workspace:^",
"@deepseek-ai/dsh-client-ui-theme": "workspace:^",
@@ -95,8 +96,11 @@
"@deepseek-ai/dsh-host-directory-picker-native": "workspace:^",
"@deepseek-ai/dsh-host-plugin-inventory": "workspace:^",
"@deepseek-ai/dsh-host-webserver": "workspace:^",
"@deepseek-ai/dsh-file-reference": "workspace:^",
"@deepseek-ai/dsh-file-reference-local": "workspace:^",
"@deepseek-ai/dsh-message-feedback": "workspace:^",
"@deepseek-ai/dsh-session-projection-cache": "workspace:^",
"@deepseek-ai/dsh-session-reference": "workspace:^",
"@deepseek-ai/dsh-session-log-export": "workspace:^",
"@deepseek-ai/dsh-session-stats": "workspace:^",
"@deepseek-ai/dsh-storage": "workspace:^",
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/client/README.md
README.md: cff74ddb048df8b37643f4c44f01e2bda4160a77
README.zh.md: 5e5d623509b9152fb7b59ea228cfa42720b737af
README.md: fe57d58a0f4fa1c9bff2699ffb363c80197fc5ed
README.zh.md: c721710c9f20ba10f20392c207e1de4169ec0e12
+1
View File
@@ -29,6 +29,7 @@ The browser side of the dsh web GUI: shell boot, browser-host communication, sha
| [`ui-commands/`](ui-commands/README.md) | Provides session-aware command discovery and dispatch. |
| [`ui-input-trigger/`](ui-input-trigger/README.md) | Coordinates inline command and reference suggestions. |
| [`ui-skill/`](ui-skill/README.md) | Adds skill references to inline suggestions. |
| [`ui-reference/`](ui-reference/README.md) | Unified Web `@file` / `@session` reference source. |
| [`ui-subagent/`](ui-subagent/README.md) | Provides subagent navigation, child transcript states, and inline references. |
| [`ui-jobs/`](ui-jobs/README.md) | Lists this session's background jobs in the conversation header. |
| [`ui-model-selection/`](ui-model-selection/README.md) | Provides model selection in conversation surfaces. |
+1
View File
@@ -29,6 +29,7 @@ dsh web GUI 的浏览器侧:shell 启动、浏览器与宿主通信、共享 U
| [`ui-commands/`](ui-commands/README.md) | 提供会话感知的命令发现与分发。 |
| [`ui-input-trigger/`](ui-input-trigger/README.md) | 协调内联命令和引用建议。 |
| [`ui-skill/`](ui-skill/README.md) | 向内联建议添加 skill(技能)引用。 |
| [`ui-reference/`](ui-reference/README.md) | 统一的 Web `@file` / `@session` 引用 source。 |
| [`ui-subagent/`](ui-subagent/README.md) | 提供 subagent(子 agent)导航、子级 transcript(文本记录)的状态和内联引用。 |
| [`ui-jobs/`](ui-jobs/README.md) | 在会话标题栏列出当前会话的后台任务。 |
| [`ui-model-selection/`](ui-model-selection/README.md) | 在对话界面中提供模型选择。 |
@@ -1866,6 +1866,51 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
})
/** Canonical fixture implementation of the generated Goal Remote contract. */
/** Canonical fixture implementation of the generated reference-discovery Remote contracts. */
const referenceRemotes = {
files(id: SessionId, query: string): RpcResult<{ path: string; kind: 'file' | 'directory' }[]> {
const missing = requireGoalSession(id)
if (missing !== undefined) return missing
const needle = query.toLocaleLowerCase()
const items = [
{ path: 'notes', kind: 'directory' as const },
{ path: 'README.md', kind: 'file' as const },
{ path: 'notes/demo.txt', kind: 'file' as const },
].filter(item => item.path.toLocaleLowerCase().includes(needle))
return { ok: true, value: items }
},
sessions(id: SessionId, query: string): RpcResult<{
sessionId: SessionId
label: string
cwd?: string
createdAt: number
mention: string
}[]> {
const missing = requireGoalSession(id)
if (missing !== undefined) return missing
const needle = query.toLocaleLowerCase()
const value = sessions
.filter(item => item.sessionId !== id)
.filter(item => String(item.sessionId).toLocaleLowerCase().includes(needle)
|| item.cwd?.toLocaleLowerCase().includes(needle) === true)
.map((item) => {
const label = item.sessionId === sid('fx-beta') ? 'Fixture child session' : String(item.sessionId)
const encoded = btoa(JSON.stringify(item.sessionId))
.replaceAll('+', '-')
.replaceAll('/', '_')
.replace(/=+$/u, '')
return {
sessionId: item.sessionId,
label,
...item.cwd === undefined ? {} : { cwd: item.cwd },
createdAt: item.updatedAt,
mention: `@[${label}](dsh-session:${encoded})`,
}
})
return { ok: true, value }
},
}
const goalRemotes = {
create(id: SessionId, request: { objective: string; maxGoalRounds?: number }): RpcResult<{ ref: FxGoalRef }> {
const missing = requireGoalSession(id)
@@ -3053,6 +3098,7 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
args: {
agentId: SessionId
line?: string
query?: string
images?: readonly unknown[]
ref?: { id: string; revision: number }
request?: { objective?: string; maxGoalRounds?: number }
@@ -3062,6 +3108,8 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
switch (endpoint) {
case 'commands/list': return Promise.resolve(commandRemotes.list(sessionId))
case 'commands/execute': return Promise.resolve(commandRemotes.execute(sessionId, args.line as string, args.images ?? []))
case 'fileReferences/list': return Promise.resolve(referenceRemotes.files(sessionId, args.query ?? ''))
case 'sessionReferenceResolver/candidates': return Promise.resolve(referenceRemotes.sessions(sessionId, args.query ?? ''))
case 'goals/create': return Promise.resolve(goalRemotes.create(sessionId, {
objective: args.request?.objective as string,
...args.request?.maxGoalRounds === undefined ? {} : { maxGoalRounds: args.request.maxGoalRounds },
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/client/runtime/README.md
README.md: 7d0e90809aaffb9503f2222e1f7426b13d72d7d5
README.zh.md: d0a43c32b4861d02786aa0c42e1af72047c9032a
README.md: eae49163c14f122a3603bcaa8a61aaac7a9a7270
README.zh.md: 9e8f82a0bcf0d2021c9b16bd51acf3c72dd04cb2
+1 -1
View File
@@ -56,7 +56,7 @@ The Chat builder keeps one mutable keyed store per Session. Content updates noti
## Trajectory request data
Trajectory Definitions assemble one chronological, purpose-discriminated provider-request stream. Assistant requests always carry their numeric `turn` and `step`; compaction requests carry `step: 0` and a `turn` owner that may be `null`. That null owner means a manual compaction ran standalone between turns, not that it belongs to either adjacent turn. A `session/end-seed` boundary closes an unmatched compaction request as an error at the boundary time with `Compaction was interrupted before completion.`; a later start projects as an independent request instead of overwriting the orphan.
Trajectory Definitions assemble one chronological, purpose-discriminated provider-request stream. Assistant requests always carry their numeric `turn` and `step`; compaction requests carry `step: 0` and a `turn` owner that may be `null`. That null owner means a manual compaction ran standalone between turns, not that it belongs to either adjacent turn. A cancellation-finalized `assistant/message` retains its durable result seq and provider provenance but does not complete the request; `step/end` classifies that request as an error. A `session/end-seed` boundary closes an unmatched compaction request as an error at the boundary time with `Compaction was interrupted before completion.`; a later start projects as an independent request instead of overwriting the orphan.
## Code Mode child-call tree
+1 -1
View File
@@ -56,7 +56,7 @@ Chat builder 为每个 Session 保留一个 mutable keyed store。内容更新
## Trajectory 请求数据
Trajectory Definition 组装出一条按时间顺序排列、以用途为判别字段的提供方请求流。助手请求始终携带数值型 `turn``step`;压缩请求携带 `step: 0`,其 `turn` 所有者可以是 `null`。这个 null 所有者表示手动压缩独立运行在两个轮次之间,并不表示它属于任一相邻轮次。`session/end-seed` 边界会在边界时刻将未匹配的压缩请求以错误状态结束,错误固定为 `Compaction was interrupted before completion.`;后续 start 会投影为独立请求,而不会覆盖这项遗留的未匹配请求。
Trajectory Definition 组装出一条按时间顺序排列、以用途为判别字段的提供方请求流。助手请求始终携带数值型 `turn``step`;压缩请求携带 `step: 0`,其 `turn` 所有者可以是 `null`。这个 null 所有者表示手动压缩独立运行在两个轮次之间,并不表示它属于任一相邻轮次。由取消定稿的 `assistant/message` 会保留持久结果 seq 和提供方信息,但不会将请求标记为完成;`step/end` 会把该请求归类为错误。`session/end-seed` 边界会在边界时刻将未匹配的压缩请求以错误状态结束,错误固定为 `Compaction was interrupted before completion.`;后续 start 会投影为独立请求,而不会覆盖这项遗留的未匹配请求。
## Code Mode 子调用树
@@ -38,7 +38,11 @@ export interface ISession {
* @param mode - 'queue' appends a turn; 'steer' interrupts the running one.
* @returns acceptance, or the business error (also mirrored into snapshot.promptError).
*/
prompt(content: PromptContentPart[], mode: 'queue' | 'steer'): Promise<RpcResult<{ accepted: true }>>
prompt(
content: PromptContentPart[],
mode: 'queue' | 'steer',
signal?: AbortSignal,
): Promise<RpcResult<{ accepted: true }>>
/**
* Resolve one durable image referenced by this session.
* @param attachmentId - opaque id found in the folded session log.
+1 -1
View File
@@ -85,7 +85,7 @@ export {
} from './sessions/conversation.ts'
export { emptyAssistantBlock } from './sessions/partial.ts'
export { isTokenDelta } from './sessions/assistant-timing.ts'
export { contextForm, contextProvenance } from './sessions/context-provenance.ts'
export { contextForm, contextProvenance, sessionRecallLabels } from './sessions/context-provenance.ts'
export { displayFailureMessage } from './sessions/failure-display.ts'
export type {
ConversationContext, ConversationContextOriginKind,
@@ -58,6 +58,19 @@ function joined(names: string[]): string | null {
return names.length > 0 ? names.join(', ') : null
}
/**
* The referenced-session labels of one durable `session-reference` recall
* source, in first-seen order; empty for every other source shape, including
* a foreign or older log whose reference entries carry no readable label.
* @param source - the logged `user/message` source, exactly as recorded.
* @returns distinct non-empty reference labels.
*/
export function sessionRecallLabels(source: unknown): string[] {
const record = asRecord(source)
if (record === null || readString(record, 'kind') !== 'session-reference') return []
return collect(record, 'references', 'label')
}
/**
* Project one durable message source onto its transcript role and producer name.
*
@@ -92,14 +92,14 @@ export interface AssistantTiming {
completedTime: number
}
/** A finalized (or interruption-frozen) assistant message. */
/** A finalized assistant message or an interruption-frozen streaming prefix. */
export interface AssistantMessageNode {
kind: 'assistant'
seq: number
/**
* Stable identity of the finalized model output, carried from the
* `assistant/message` event. Absent on interruption-frozen partials: those
* were never finalized, so they address no durable message.
* Stable identity carried from the `assistant/message` event. Absent only on
* synthetic interruption fallbacks assembled from chunks without a durable
* assistant message.
*/
messageId?: MessageId
/** Unix epoch ms from the source session event (or turn/end when frozen from a partial). */
@@ -112,8 +112,9 @@ export interface AssistantMessageNode {
requestConfig?: AssistantRequestConfig
/** Timing derived from the recorded step/chunk/message event sequence. */
timing?: AssistantTiming
/** Frozen partial of an aborted turn (no finalize ever arrives): rendered with a marker.
* Synthetic seq (fractional, derived from the turn/end seq) keeps it ordered inside the flow. */
/** Prefix of an aborted turn, rendered with a marker. A durable
* finalized prefix uses its event seq; a chunk-only fallback uses a fractional
* seq derived from the closing boundary to keep it ordered inside the flow. */
interrupted?: true
}
@@ -187,7 +187,11 @@ export class Session implements SessionFace {
* @param mode - queue appends after the current turn; steer interrupts it.
* @returns the prompt result (also mirrored into promptError on failure).
*/
async prompt(content: PromptContentPart[], mode: 'queue' | 'steer'): Promise<RpcResult<{ accepted: true }>> {
async prompt(
content: PromptContentPart[],
mode: 'queue' | 'steer',
signal?: AbortSignal,
): Promise<RpcResult<{ accepted: true }>> {
this.promptError = null
this.lastAgentError = null
// Synchronous, before the first await: the blank → engaging edge must be
@@ -204,7 +208,7 @@ export class Session implements SessionFace {
mode,
content,
clientTimeZone: resolvedClientTimeZone(),
})).result
}, signal)).result
} else if (this.address.mode === 'one-shot') {
result = {
ok: false,
@@ -231,7 +235,7 @@ export class Session implements SessionFace {
? [{ type: 'text' as const, text: part.text }]
: []),
clientTimeZone: resolvedClientTimeZone(),
})).result
}, signal)).result
result = routed.ok ? { ok: true, value: { accepted: true } } : routed
}
}
+1 -1
View File
@@ -57,7 +57,7 @@ function styleInjectionModule(
* Everything else under @deepseek-ai/* is either a module-table entry
* (external) or a leak the purity gate rejects.
*/
export const INLINE_SAFE = /^@deepseek-ai\/dsh-(host-apiproxy|session|llm|tools|brand)(\/|$)/
export const INLINE_SAFE = /^@deepseek-ai\/dsh-(host-apiproxy|file-reference|session|llm|tools|brand)(\/|$)/
/**
* Vendored framework libraries: rescoped into @deepseek-ai, so the gate below
@@ -337,6 +337,7 @@ export function apply(ctx: Context): void {
inputTriggers.toggleSource('command', {
trigger: '/',
query: '',
quoted: false,
position: snapshot.draft.slice(0, selection.start).trim() === '' ? 'leading' : 'inline',
span: { ...selection, draftRev: snapshot.draftRev },
})
@@ -28,6 +28,11 @@
color: var(--dsw-alias-label-primary);
}
.referenceSummary {
color: var(--dsw-alias-label-tertiary);
font-size: 12px;
line-height: 18px;
}
.contextRow {
padding: 2px 0;
}
@@ -269,7 +274,8 @@
}
/* Reference chip projection inside a user bubble (`<skill>name</skill>` model
spans render as chips; free geometry no textarea pairing here). */
spans and metadata-confirmed sessions render as chips; free geometry means
the textarea overlay's metric pairing does not apply here). */
.refChip {
display: inline-block;
margin: 0 2px;
@@ -152,21 +152,36 @@ function TurnMaxTokensItem({ t }: {
* scan as the composer, minus the lexicon: sent tokens were validated at
* compose time, so shape alone decorates).
*/
function projectUserText(text: string): ReactNode {
function projectUserText(text: string, sessionLabels: readonly string[]): ReactNode {
const ranges: { start: number; end: number; label: string; kind: 'session' | 'plain' }[] = []
for (const rawLabel of [...new Set(sessionLabels)].sort((a, b) => b.length - a.length)) {
const label = `@${rawLabel}`
let start = text.indexOf(label)
while (start >= 0) {
ranges.push({ start, end: start + label.length, label, kind: 'session' })
start = text.indexOf(label, start + label.length)
}
}
const re = /(^|\s)([/@][\w-]+)(?=\s|$)/g
const parts: ReactNode[] = []
let cursor = 0
let m: RegExpExecArray | null
while ((m = re.exec(text)) !== null) {
const tokenStart = m.index + (m[1]?.length ?? 0)
const label = m[2] ?? ''
ranges.push({ start: tokenStart, end: tokenStart + label.length, label, kind: 'plain' })
}
ranges.sort((a, b) => a.start - b.start || b.end - a.end)
const parts: ReactNode[] = []
let cursor = 0
for (const range of ranges) {
if (range.start < cursor) continue
const { start: tokenStart, end, label, kind } = range
if (tokenStart > cursor) parts.push(<MessageText key={cursor} text={text.slice(cursor, tokenStart)} />)
parts.push(
<span key={tokenStart} className={css.refChip} data-ref-chip={label.startsWith('@') ? 'subagent' : 'skill'}>
<span key={tokenStart} className={css.refChip} data-ref-chip={kind === 'session' ? 'session' : label.startsWith('@') ? 'subagent' : 'skill'}>
{label}
</span>,
)
cursor = tokenStart + label.length
cursor = end
}
if (parts.length === 0) return <MessageText text={text} />
if (cursor < text.length) parts.push(<MessageText key={cursor} text={text.slice(cursor)} />)
@@ -175,7 +190,7 @@ function projectUserText(text: string): ReactNode {
/** Right-aligned bubble shared by user and steering rows. */
function UserStyleBubble({
content, renderMessageImages, actions, pending = false, t,
content, renderMessageImages, actions, pending = false, referenceLabels = [], t,
}: {
content: readonly unknown[]
renderMessageImages: ChatNodeOwnerProps['renderMessageImages']
@@ -183,6 +198,8 @@ function UserStyleBubble({
actions?: (text: string) => ReactNode
/** Whether this is the Host-authoritative pre-admission steering projection. */
pending?: boolean
/** Exact session mention labels associated by the adjacent recall node. */
referenceLabels?: readonly string[]
t: ChatViewSlotProps['t']
}): ReactNode {
const { text, images, rest } = contentParts(content)
@@ -193,9 +210,14 @@ function UserStyleBubble({
<div className={css.userStack}>
{renderMessageImages({ images, align: 'end' })}
{showBubble && <div className={css.bubble}>
{projectUserText(text)}
{projectUserText(text, referenceLabels)}
{rest.map((block, i) => <JsonBlock key={i} label={t('message.extraBlock')} payload={block} truncatedLabel={truncated} />)}
</div>}
{referenceLabels.length > 0 && (
<div className={css.referenceSummary}>
{t('message.referenceSummary', { labels: referenceLabels.join(t('message.referenceSeparator')) })}
</div>
)}
</div>
{actions?.(text)}
</div>
@@ -240,6 +262,7 @@ export const UserMessageNodeView = memo(function UserMessageNodeView({
<UserStyleBubble
content={data.content}
renderMessageImages={renderMessageImages}
{...data.referenceLabels === undefined ? {} : { referenceLabels: data.referenceLabels }}
t={t}
actions={text => (
<MessageIconActions
@@ -163,6 +163,7 @@ function finalNode(
firstTokenTime: state.firstTokenTime ?? null,
completedTime: event.time,
},
...event.data.interrupted === true ? { interrupted: true } : {},
}
}
const location = context.start?.location ?? context.matches.at(-1)?.location
@@ -3,19 +3,29 @@ import type {
ContextMessageNode, ConversationNodeDefinition, SteeringMessageNode, UserMessageNode,
} from '@deepseek-ai/dsh-client-runtime/client'
import {
contextForm, contextProvenance, isAppendSurfaceEvent, isReplacementSurfaceEvent,
contextForm, contextProvenance, isAppendSurfaceEvent, isReplacementSurfaceEvent, sessionRecallLabels,
} from '@deepseek-ai/dsh-client-runtime/client'
import type { InboxState } from './inbox.ts'
import { chatNode } from './common.ts'
type MessageNode = UserMessageNode | SteeringMessageNode | ContextMessageNode
interface ReferencedUserMessageNode extends UserMessageNode {
/** Labels cited by the immediately preceding session-reference context. */
readonly referenceLabels?: readonly string[]
}
interface ReferencedSteeringMessageNode extends SteeringMessageNode {
/** Labels cited by the immediately preceding session-reference context. */
readonly referenceLabels?: readonly string[]
}
type MessageNode = ReferencedUserMessageNode | ReferencedSteeringMessageNode | ContextMessageNode
declare module '@deepseek-ai/dsh-client-ui-conversation/client' {
interface ChatNodeDataMap {
/** Ordinary turn-opening user message. */
user: UserMessageNode
user: ReferencedUserMessageNode
/** User message admitted into an active turn. */
steering: SteeringMessageNode
steering: ReferencedSteeringMessageNode
/** Non-user context injected into model history. */
context: ContextMessageNode
}
@@ -51,6 +61,11 @@ export const messageDefinition: ConversationNodeDefinition<MessageNode> = {
}
}
const claimed = reader.previous<InboxState>('inbox-next-step')?.state.claimed.has(String(event.data.id)) === true
const previous = reader.previous<MessageNode>('input-message')
const labels = previous?.state.kind === 'context' && previous.state.seq + 1 === event.seq
? sessionRecallLabels(previous.state.source)
: []
const referenceLabels = labels.length === 0 ? {} : { referenceLabels: labels }
return claimed
? {
kind: 'steering',
@@ -59,6 +74,7 @@ export const messageDefinition: ConversationNodeDefinition<MessageNode> = {
time: event.time,
content: event.data.content,
source: event.data.source,
...referenceLabels,
}
: {
kind: 'user',
@@ -66,6 +82,7 @@ export const messageDefinition: ConversationNodeDefinition<MessageNode> = {
time: event.time,
content: event.data.content,
source: event.data.source,
...referenceLabels,
}
},
update: context => context.state,
@@ -232,7 +232,7 @@ export interface InputState {
export interface SubmitAttempt {
readonly seq: number
readonly signal: AbortSignal
/** Draft at enter time; rollback restores it only while the live draft still equals it. */
/** Draft at enter time; settlement clears it only after acceptance. */
readonly draftSnapshot: string
/** Default-message delivery intent retained while slash adjudication is pending. */
readonly mode: InputSubmitMode
@@ -271,10 +271,7 @@ export type InputEvent =
| { readonly type: 'adjudicated'; readonly attempt: SubmitAttempt; readonly outcome: PickOutcome }
| { readonly type: 'adjudication-failed'; readonly attempt: SubmitAttempt; readonly message: string }
| { readonly type: 'submit-settled'; readonly attempt: SubmitAttempt; readonly ok: boolean; readonly outcome?: SubmitOutcome; readonly message?: string }
/**
* An ordinary (default-sink) send was accepted: clear the draft as a COMMIT
* undo must not resurrect sent content (mirrors submit-settled's success arm).
*/
/** Commit an image-only send whose empty draft did not need an attempt. */
| { readonly type: 'send-committed' }
| { readonly type: 'release' }
@@ -286,5 +283,5 @@ export type InputEvent =
export type InputEffect =
| { readonly type: 'adjudicate'; readonly attempt: SubmitAttempt; readonly draft: string }
| { readonly type: 'begin-submit'; readonly attempt: SubmitAttempt; readonly claim: CommandClaim; readonly args: string }
| { readonly type: 'default-sink'; readonly draft: string; readonly mode: InputSubmitMode }
| { readonly type: 'default-sink'; readonly attempt: SubmitAttempt; readonly draft: string; readonly mode: InputSubmitMode }
| { readonly type: 'notice'; readonly level: 'info' | 'error'; readonly text: string }
@@ -10,7 +10,7 @@ import type { ClientContext, ObservableSnapshot, SnapshotStore } from '@deepseek
import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
import type {
ArbitrateKey, ArbitrateOutcome, CommandClaim, ConsumeTokenRequest, PickOutcome,
ReferenceInsert, InputTriggerController, SubmitImageAttachment, TokenSpan,
ReferenceInsert, InputTriggerController, SubmitImageAttachment, SubmitOutcome, TokenSpan,
} from '@deepseek-ai/dsh-client-ui-input-trigger/client'
import type {
DraftAttachmentId, EditRange, EditSelection, InputActions, InputEffect, InputNotice, InputState,
@@ -45,7 +45,12 @@ export interface SessionInputDeps {
*/
steerQueue?: (() => void) | undefined
/** The plain-message sink (send choreography / materialize fork — the hub owns it). */
defaultSink(text: string, imageIds: readonly DraftAttachmentId[], mode: InputSubmitMode): void
defaultSink(
text: string,
imageIds: readonly DraftAttachmentId[],
mode: InputSubmitMode,
signal: AbortSignal,
): Promise<SubmitOutcome>
/** Command-plane image plumbing (the hub owns the conversation face and the copy). */
commandImages: {
/** Resolve ordered draft ids to wire payloads without sending them; rejects when an id no longer resolves. */
@@ -95,6 +100,8 @@ export class SessionInputShell implements SessionInput {
private noticeSeq = 0
private lastDraft = ''
private imageIds: readonly DraftAttachmentId[] = []
/** One image-only send at a time: Enter during the Host round-trip is a no-op. */
private imageSendInFlight = false
private disposed = false
/** Draft persistence mirror (chat store write; receives the clipboard projection, never raw placeholders). */
private mirrorFn: ((text: string) => void) | undefined
@@ -150,16 +157,6 @@ export class SessionInputShell implements SessionInput {
this.publish()
}
/**
* Restore a failed attempt before any images added after its admission.
* @param ids - failed attempt image ids.
*/
restoreImages(ids: readonly DraftAttachmentId[]): void {
const current = new Set(this.imageIds)
this.imageIds = [...ids.filter(id => !current.has(id)), ...this.imageIds]
this.publish()
}
/**
* Clear the draft as a successful-send commit: no undo unit is recorded and
* the undo history is cut, so Ctrl/Cmd-Z cannot resurrect sent content
@@ -211,7 +208,19 @@ export class SessionInputShell implements SessionInput {
*/
submit(mode: InputSubmitMode = 'queue'): void {
if (this.snapshot.draft.trim() === '' && this.imageIds.length > 0) {
if (this.snapshot.phase === 'plain') this.deps.defaultSink('', [...this.imageIds], mode)
if (this.snapshot.phase === 'plain' && !this.imageSendInFlight) {
const imageIds = [...this.imageIds]
this.imageSendInFlight = true
void this.deps.defaultSink('', imageIds, mode, new AbortController().signal).then((outcome) => {
this.imageSendInFlight = false
if (this.disposed) return
if (outcome.kind === 'success') this.commitSend(imageIds)
else if (outcome.text !== undefined) this.notify('error', outcome.text)
}, (error: unknown) => {
this.imageSendInFlight = false
if (!this.disposed) this.notify('error', error instanceof Error ? error.message : String(error))
})
}
return
}
// Claimed pre-gate: a claim that does not declare image acceptance never
@@ -350,13 +359,21 @@ export class SessionInputShell implements SessionInput {
* a scan-derived decoration, never state.
* @param text - the plain reference text to splice in (e.g. `/name `).
* @param span - pick-time span snapshot (draftRev CAS).
* @param keepCompleting - re-track at the caret after the splice so an open
* token (a directory pick's trailing slash) reopens the menu.
* @returns whether the text was applied.
*/
insertText(text: string, span: TokenSpan): boolean {
insertText(text: string, span: TokenSpan, keepCompleting = false): boolean {
const snapshot = this.core.state
if (span.draftRev !== snapshot.draftRev) return false
const draft = snapshot.draft
this.setDraft(draft.slice(0, span.start) + text + draft.slice(span.end))
if (keepCompleting) {
// Machine-driven draft replacement never passes through onChange, so
// re-track at the caret inside the still-open token (see space()).
const next = this.snapshot
this.deps.inputTriggers?.()?.track(next.draft, span.start + text.length, { tier: guardOf(next.phase) }, next.draftRev)
}
return true
}
@@ -421,7 +438,7 @@ export class SessionInputShell implements SessionInput {
return
}
case 'default-sink': {
this.sinkSerialized(fx.draft, fx.mode)
this.sinkSerialized(fx.attempt, fx.draft, fx.mode)
return
}
default:
@@ -436,11 +453,11 @@ export class SessionInputShell implements SessionInput {
* send notice + draft and chips retained, never a silent downgrade to
* the clipboard text. Chip-free drafts skip the async detour.
*/
private sinkSerialized(draft: string, mode: InputSubmitMode): void {
private sinkSerialized(attempt: SubmitAttempt, draft: string, mode: InputSubmitMode): void {
const imageIds = [...this.imageIds]
const occurrences = this.core.state.occurrences
if (occurrences.length === 0) {
this.deps.defaultSink(draft.trim(), imageIds, mode)
this.settleSubmit(attempt, this.deps.defaultSink(draft.trim(), imageIds, mode, attempt.signal), imageIds)
return
}
const inputTriggers = this.deps.inputTriggers?.()
@@ -460,13 +477,45 @@ export class SessionInputShell implements SessionInput {
cursor = part.offset + 1
}
out += draft.slice(cursor)
this.deps.defaultSink(out.trim(), imageIds, mode)
this.settleSubmit(attempt, this.deps.defaultSink(out.trim(), imageIds, mode, attempt.signal), imageIds)
},
(error: unknown) => {
controller.abort()
if (this.disposed) return
if (this.dead(attempt)) return
const message = error instanceof Error ? error.message : String(error)
this.notify('error', message)
this.run(this.core.dispatch({ type: 'submit-settled', attempt, ok: false, message }))
},
)
}
/** Settle one admission attempt; successful sends consume only their captured images. */
private settleSubmit(
attempt: SubmitAttempt,
pending: Promise<SubmitOutcome>,
imageIds: readonly DraftAttachmentId[] = [],
): void {
pending.then(
(outcome) => {
if (this.dead(attempt)) return
if (outcome.kind === 'success' && imageIds.length > 0) {
const submitted = new Set(imageIds)
this.imageIds = this.imageIds.filter(id => !submitted.has(id))
}
this.run(this.core.dispatch({
type: 'submit-settled',
attempt,
ok: outcome.kind === 'success',
outcome,
}))
},
(error: unknown) => {
if (this.dead(attempt)) return
this.run(this.core.dispatch({
type: 'submit-settled',
attempt,
ok: false,
message: error instanceof Error ? error.message : String(error),
}))
},
)
}
@@ -519,6 +568,7 @@ export class SessionInputShell implements SessionInput {
}
this.run(this.core.dispatch({
type: 'submit-settled', attempt, ok: outcome.kind === 'success', outcome,
...(outcome.kind === 'error' && outcome.text === undefined ? { message: 'command failed' } : {}),
}))
},
(error: unknown) => {
@@ -9,7 +9,7 @@
* real host entity, so the sink is one unconditional prompt path.
*/
import type { ClientContext, ISessions, SessionBinding, SessionFace, SessionId } from '@deepseek-ai/dsh-client-runtime/client'
import type { InputTriggerController, SubmitImageAttachment } from '@deepseek-ai/dsh-client-ui-input-trigger/client'
import type { InputTriggerController, SubmitImageAttachment, SubmitOutcome } from '@deepseek-ai/dsh-client-ui-input-trigger/client'
import type { TranslateNS } from '@deepseek-ai/dsh-client-locale/client'
import { queueReadFaceOf } from '../queue/store.ts'
import type { ComposerKeyboard, DraftAttachmentId, SessionInputResolver, SessionInput } from './contract.ts'
@@ -29,7 +29,8 @@ interface ConversationAttachmentFace {
text: string,
imageIds: readonly DraftAttachmentId[],
mode: InputSubmitMode,
): Promise<void>
signal?: AbortSignal,
): Promise<SubmitOutcome>
serializeDraftImages(imageIds: readonly DraftAttachmentId[]): Promise<readonly SubmitImageAttachment[]>
releaseDraftImage(id: DraftAttachmentId): void
}
@@ -76,7 +77,7 @@ export class InputHub implements SessionInputResolver {
inputTriggers: () => this.controller(actx),
popup: () => this.popup(actx),
queue: queueReadFaceOf(session),
defaultSink: (text, imageIds, mode) => { this.sink(session, text, imageIds, mode) },
defaultSink: (text, imageIds, mode, signal) => this.sink(session, text, imageIds, mode, signal),
steerQueue: () => { void this.steerQueue(session, shell) },
commandImages: {
serialize: ids => this.conversation().serializeDraftImages(ids),
@@ -105,7 +106,7 @@ export class InputHub implements SessionInputResolver {
actx.on('slash/input-consume-token', req =>
shell.consumeToken(req.guard) ? true : undefined),
actx.on('slash/input-insert-text', req =>
shell.insertText(req.text, req.span) ? true : undefined),
shell.insertText(req.text, req.span, req.continue === true) ? true : undefined),
]
return () => {
for (const off of offs) off()
@@ -166,20 +167,10 @@ export class InputHub implements SessionInputResolver {
text: string,
imageIds: readonly DraftAttachmentId[],
mode: InputSubmitMode,
): void {
if (text === '' && imageIds.length === 0) return
const shell = this.shells.get(session.sessionId)
// Commit, not an editable clear: undo must not resurrect sent content.
shell?.commitSend(imageIds)
void this.conversation().sendSession(session, text, imageIds, mode).catch(() => {
if (this.shells.get(session.sessionId) === shell) {
shell?.restoreImages(imageIds)
if (shell?.snapshot.draft === '') shell.setDraft(text)
return
}
const conversation = this.rootCtx.get('conversation') as ConversationAttachmentFace | undefined
for (const id of imageIds) conversation?.releaseDraftImage(id)
})
signal: AbortSignal,
): Promise<SubmitOutcome> {
if (text === '' && imageIds.length === 0) return Promise.resolve({ kind: 'success' })
return this.conversation().sendSession(session, text, imageIds, mode, signal)
}
/**
@@ -481,7 +481,9 @@ export class InputMachine {
this.phase = 'adjudicating'
return [{ type: 'adjudicate', attempt, draft: this.draft }]
}
return [{ type: 'default-sink', draft: this.draft, mode }]
const attempt = this.beginAttempt(mode)
this.phase = 'submitting'
return [{ type: 'default-sink', attempt, draft: this.draft, mode }]
}
private onAdjudicated(attempt: SubmitAttempt, outcome: Extract<InputEvent, { type: 'adjudicated' }>['outcome']): InputEffect[] {
@@ -499,11 +501,18 @@ export class InputMachine {
}
// 'handled' (source dealt internally), {insert} (no enter-time span
// semantics), or a miss: all land plain; only the miss flows to the sink.
if (outcome === undefined) {
this.phase = 'submitting'
return [{
type: 'default-sink',
attempt,
draft: attempt.draftSnapshot,
mode: attempt.mode,
}]
}
this.inflight = undefined
this.phase = 'plain'
return outcome === undefined
? [{ type: 'default-sink', draft: attempt.draftSnapshot, mode: attempt.mode }]
: []
return []
}
private onAdjudicationFailed(attempt: SubmitAttempt, message: string): InputEffect[] {
@@ -522,7 +531,13 @@ export class InputMachine {
this.phase = 'plain'
this.claim = undefined
this.occurrences = []
this.adopt('')
// Text appended after the sent snapshot during the Host round-trip
// survives the commit; edits interleaved with committed content cannot
// be separated from it, so only a pure suffix is retained.
const snapshot = flight.attempt.draftSnapshot
this.adopt(this.draft !== snapshot && this.draft.startsWith(snapshot)
? this.draft.slice(snapshot.length)
: '')
// Committed content is gone for good: undo must not resurrect a sent draft.
this.log = []
this.redoStack = []
@@ -532,24 +547,24 @@ export class InputMachine {
? [{ type: 'notice', level: ev.outcome.kind === 'error' ? 'error' : 'info', text: ev.outcome.text }]
: []
}
const text = ev.message ?? ev.outcome?.text ?? 'command failed'
// Drift guard: keep the enter-time draft (same claim) only while the
// live draft still equals it; user input typed during flight wins.
const text = ev.message ?? ev.outcome?.text
// Keep the same command claim only while the live draft still equals the
// enter-time draft; user input typed during flight wins.
// Claimed re-entry additionally requires the watch to hold — an
// enter-path snapshot may carry leading whitespace the token never had.
if (this.draft === flight.attempt.draftSnapshot
&& this.claim !== undefined && this.draft.startsWith(this.claim.token)) {
this.phase = 'claimed'
return [{ type: 'notice', level: 'error', text }]
return text === undefined ? [] : [{ type: 'notice', level: 'error', text }]
}
this.phase = 'plain'
this.claim = undefined
return [{ type: 'notice', level: 'error', text }]
return text === undefined ? [] : [{ type: 'notice', level: 'error', text }]
}
/** Ordinary send accepted: clear as a commit (no undo unit; sent content
* must not be resurrectable same discipline as submit-settled success). */
/** Cut undo state after an accepted image-only send. */
private onSendCommitted(): InputEffect[] {
if (this.phase !== 'plain') return []
this.claim = undefined
this.occurrences = []
this.adopt('')
@@ -99,6 +99,8 @@ export const zh = {
'message.extraBlock': '附加内容块',
'message.contextInjection': '上下文注入',
'message.contextRecall': '跨会话召回',
'message.referenceSummary': '引用会话 · {labels}',
'message.referenceSeparator': '、',
'message.context.instructions.loaded': '已载入',
'message.context.instructions.added': '已新增',
'message.context.instructions.updated': '已更新',
@@ -274,6 +276,8 @@ export const en = {
'message.extraBlock': 'Extra content block',
'message.contextInjection': 'Context injection',
'message.contextRecall': 'Session recall',
'message.referenceSummary': 'Referenced session · {labels}',
'message.referenceSeparator': ', ',
'message.context.instructions.loaded': 'loaded',
'message.context.instructions.added': 'added',
'message.context.instructions.updated': 'updated',
@@ -13,7 +13,7 @@ import type { Context } from '@deepseek-ai/cordis'
// error, so scope resolution goes through the sessions service (scopeOf
// method) instead of the standalone helper.
import type { ISessions, SessionFace, SessionId } from '@deepseek-ai/dsh-client-runtime/client'
import type { SubmitImageAttachment } from '@deepseek-ai/dsh-client-ui-input-trigger/client'
import type { SubmitImageAttachment, SubmitOutcome } from '@deepseek-ai/dsh-client-ui-input-trigger/client'
import type { ImageAttachmentRef, ImageMediaType } from '@deepseek-ai/dsh-attachment'
import type { ComposerAttachment } from './contract/slots.ts'
import type { QueueAction, QueueItemId } from './contract/queue.ts'
@@ -139,22 +139,26 @@ export class ConversationController extends Service implements IConversation {
* @param text - serialized prompt text.
* @param imageIds - ordered draft-local attachment ids.
* @param mode - queue or steer delivery selected by composer policy.
* @param signal - optional cancellation for the complete Host admission.
* @returns the Host admission outcome; local attachment preparation failures reject.
*/
async sendSession(
session: SessionFace,
text: string,
imageIds: readonly DraftAttachmentId[],
mode: InputSubmitMode,
): Promise<void> {
signal?: AbortSignal,
): Promise<SubmitOutcome> {
const attachments = this.draftImages(imageIds)
if (attachments.length !== imageIds.length) {
throw new Error('conversation.sendSession: one or more draft images are no longer available')
}
const uploaded = await this.serializeImages(attachments.map(attachment => attachment.file))
const content = [...uploaded, ...(text === '' ? [] : [{ type: 'text' as const, text }])]
const result = await session.prompt(content, mode)
if (!result.ok) throw new Error(`conversation.send failed: ${result.error.code}: ${result.error.message}`)
const result = await session.prompt(content, mode, signal)
if (!result.ok) return { kind: 'error' }
this.releaseDraftImages(attachments)
return { kind: 'success' }
}
/**
@@ -2,7 +2,7 @@
// apply inject factories exercised end to end against the terminal thin
// API: the strict session API (views triple, draft mirror), the
// provide-channel input face (machine-sink submit choreography incl.
// optimistic clear + failure restore), the resident API (selectWorkspace
// transactional clear + failure retention), the resident API (selectWorkspace
// draft carrying), the composer-bar stop face, openDetails = select action +
// layout orchestration, and the closeDetails details API. Complements
// chat-apply.spec.tsx (registration) and selection-survival.spec.tsx (store
@@ -150,7 +150,7 @@ describe('conversation slot inject API', () => {
await b.runtime.dispose()
})
it('the provide-channel input face submits through the machine sink: trim, optimistic clear, failure restore without clobber', async () => {
it('the provide-channel input face submits through the machine sink: trim, transactional clear, failure retains the draft', async () => {
const b = await bench()
const { injected } = b.conversationApi(ROOT)
const { state, actions } = b.inputApi(ROOT)
@@ -159,20 +159,23 @@ describe('conversation slot inject API', () => {
actions.submit()
expect(b.sessionFake.prompt).not.toHaveBeenCalled()
expect(state.getSnapshot().draft).toBe(' ')
// Success: cleared and stays cleared.
// Success: the draft clears only after the sink settles.
actions.setDraft('hello')
actions.submit()
expect(state.getSnapshot().draft).toBe('')
await Promise.resolve()
expect(b.sessionFake.prompt).toHaveBeenCalledWith([{ type: 'text', text: 'hello' }], 'queue')
// Failure: restored (draft still empty when the rejection lands).
await vi.waitFor(() => {
expect(state.getSnapshot().draft).toBe('')
})
expect(b.sessionFake.prompt).toHaveBeenCalledWith([{ type: 'text', text: 'hello' }], 'queue', expect.any(AbortSignal))
// Failure: the draft is retained through the round-trip.
b.sessionFake.prompt.mockResolvedValueOnce({ ok: false, error: { code: 'agent-busy', message: 'b', details: { reason: 'b' } } })
actions.setDraft('retry me')
actions.submit()
await vi.waitFor(() => {
expect(state.getSnapshot().draft).toBe('retry me')
expect(b.sessionFake.prompt).toHaveBeenCalledTimes(2)
})
// Failure landing after new typing: no clobber (restore fills empty only).
await new Promise(r => setTimeout(r, 0))
expect(state.getSnapshot().draft).toBe('retry me')
// Failure landing after new typing: no clobber (the interleaved edit wins).
b.sessionFake.prompt.mockResolvedValueOnce({ ok: false, error: { code: 'agent-busy', message: 'b', details: { reason: 'b' } } })
actions.submit()
actions.setDraft('typed during flight')
@@ -48,10 +48,11 @@ const RETRY_ID = 'retry-fixture' as Extract<ConversationNode, { kind: 'model-ret
interface MessageItemProps {
readonly node: ConversationNode
readonly t: ChatNodeViewProps['t']
readonly referenceLabels?: readonly string[]
}
/** Legacy-node fixture adapter for the independently registered renderers. */
function MessageItem({ node, t: translate }: MessageItemProps) {
function MessageItem({ node, t: translate, referenceLabels }: MessageItemProps) {
const kind = node.kind === 'assistant' ? 'assistant-step' : node.kind
const viewNode: ChatConversationViewNode = {
key: `fixture:${node.kind}:${node.seq}`,
@@ -61,7 +62,11 @@ function MessageItem({ node, t: translate }: MessageItemProps) {
anchorSeq: node.seq,
location: { kind: 'session' },
visibility: 'visible',
data: node.kind === 'model-retry' ? { attempts: [node], current: node } : node,
data: node.kind === 'model-retry'
? { attempts: [node], current: node }
: (node.kind === 'user' || node.kind === 'steering') && referenceLabels !== undefined
? { ...node, referenceLabels }
: node,
}
const props = { node: viewNode, t: translate, renderMessageImages } as ChatNodeViewProps
switch (node.kind) {
@@ -82,6 +87,25 @@ function MessageItem({ node, t: translate }: MessageItemProps) {
}
describe('MessageItem arms', () => {
it('renders an adjacent session mention as a chip even without trailing whitespace', () => {
const view = render(
<MessageItem
t={t}
referenceLabels={['你好']}
node={{
kind: 'user',
seq: 1,
time: 1_000,
content: [{ type: 'text', text: '@你好这个在讲啥' }] as never,
source: null,
}}
/>,
)
expect(view.container.querySelector('[data-ref-chip="session"]')?.textContent).toBe('@你好')
expect(view.getByText('这个在讲啥')).toBeTruthy()
expect(view.getByText('引用会话 · 你好')).toBeTruthy()
})
it('user bubbles expose clock / copy and neither branch nor edit; copy writes the text', () => {
const writeText = vi.fn().mockResolvedValue(undefined)
Object.defineProperty(navigator, 'clipboard', {
@@ -160,6 +160,20 @@ describe('built-in conversation node Definitions', () => {
expect(interrupted?.data).toMatchObject({ status: 'interrupted' })
expect((interrupted?.data as AssistantChatData).finalNode?.interrupted).toBe(true)
const markedValue = assembler([
at(20, 'turn/start', { turn: 3 }),
at(21, 'step/start', { turn: 3, step: 1 }),
at(22, 'assistant/message', {
turn: 3,
step: 1,
message: assistantMessage('assistant-3', 'cut short'),
interrupted: true,
}, { surfaceOp: 'append' }),
])
const marked = node(snapshot(markedValue), 'assistant-step')
expect(marked?.data).toMatchObject({ status: 'interrupted', blocks: [{ kind: 'text', text: 'cut short' }] })
expect((marked?.data as AssistantChatData).finalNode?.interrupted).toBe(true)
const hiddenValue = assembler([
at(20, 'turn/start', { turn: 3 }),
at(21, 'step/start', { turn: 3, step: 1 }),
@@ -514,6 +528,31 @@ describe('built-in conversation node Definitions', () => {
})
})
it('associates session-reference labels inside the adjacent direct-message node', () => {
const referenceSource = {
kind: 'session-reference',
form: 'recall',
version: 1,
references: [
{ sessionId: 'source-a', label: 'Research' },
{ sessionId: 'source-b', label: 'Review' },
],
}
const value = assembler([
at(1, 'user/message', {
...textMessage('reference-context', 'snapshot'),
source: referenceSource,
}, { surfaceOp: 'append' }),
at(2, 'user/message', textMessage('citing-user', '@Research and @Review'), { surfaceOp: 'append' }),
at(4, 'user/message', textMessage('later-user', 'unrelated'), { surfaceOp: 'append' }),
])
const current = snapshot(value)
const users = [...current.nodes.values()].filter(candidate => candidate.kind === 'user')
expect(users[0]?.data).toMatchObject({ referenceLabels: ['Research', 'Review'] })
expect(users[1]?.data).not.toHaveProperty('referenceLabels')
})
it('keeps replacement copies out of Chat business nodes', () => {
const value = assembler([
at(1, 'turn/start', { turn: 1 }),
@@ -13,6 +13,7 @@ import {
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
import type { ClientContext, ConversationSnapshot, SessionId } from '@deepseek-ai/dsh-client-runtime/client'
import type { SubmitOutcome } from '@deepseek-ai/dsh-client-ui-input-trigger/client'
import { SessionInputShell } from '../src/client/input/facade.ts'
import type {
ComposerAttachment, ComposerAttachmentsOwnerProps,
@@ -104,7 +105,12 @@ function row(id: string): ConversationSnapshot['queue'][number] {
/** Real machine behind the bar entry: sink spy, no slash pipeline (plain text goes straight to the sink). */
function bench(over?: BenchOptions) {
const sink = vi.fn()
const sink = vi.fn<(
text: string,
imageIds: readonly DraftAttachmentId[],
mode: 'queue' | 'steer',
signal: AbortSignal,
) => Promise<SubmitOutcome>>(() => Promise.resolve({ kind: 'success' }))
const lex = over?.lexicon
const session = createSnapshotStore<ConversationSnapshot>(snapshotOf({
running: over?.running ?? false,
@@ -209,9 +215,11 @@ function bench(over?: BenchOptions) {
}
function attachmentOwner(slotCalls: readonly { key: string; owner: unknown }[]): ComposerAttachmentsOwnerProps {
const call = slotCalls.find(candidate => candidate.key === 'conversation.input.attachments')
if (call === undefined) throw new Error('attachment slot was not rendered')
return call.owner as ComposerAttachmentsOwnerProps
for (let i = slotCalls.length - 1; i >= 0; i -= 1) {
const call = slotCalls[i]
if (call?.key === 'conversation.input.attachments') return call.owner as ComposerAttachmentsOwnerProps
}
throw new Error('attachment slot was not rendered')
}
describe('image draft rail', () => {
@@ -333,18 +341,28 @@ describe('image draft rail', () => {
expect(attachmentOwner(result.slotCalls).canAcceptDrop).toBe(false)
})
it('sends an image-only draft and exposes removal through the attachment slot', () => {
it('sends an image-only draft and exposes removal through the attachment slot', async () => {
const file = new File([Uint8Array.of(1)], 'pixel.png', { type: 'image/png' })
const attachment = { kind: 'image' as const, id: 'draft-1' as DraftAttachmentId, file, previewUrl: 'blob:draft-1' }
const result = bench({ attachments: [attachment] })
const extra = new File([Uint8Array.of(2)], 'extra.png', { type: 'image/png' })
const attachments = [
{ kind: 'image' as const, id: 'draft-1' as DraftAttachmentId, file, previewUrl: 'blob:draft-1' },
{ kind: 'image' as const, id: 'draft-2' as DraftAttachmentId, file: extra, previewUrl: 'blob:draft-2' },
]
const result = bench({ attachments })
const { view, textarea, sink, removeImage } = result
expect((view.getByRole('button', { name: '发送消息' }) as HTMLButtonElement).disabled).toBe(false)
fireEvent.keyDown(textarea, { key: 'Enter' })
expect(sink).toHaveBeenCalledWith('', ['draft-1'], 'queue')
const owner = attachmentOwner(result.slotCalls)
expect(owner.attachments).toEqual([attachment])
owner.onRemoveImage(attachment.id)
expect(removeImage).toHaveBeenCalledWith('draft-1')
act(() => { owner.onRemoveImage('draft-2' as DraftAttachmentId) })
expect(removeImage).toHaveBeenCalledWith('draft-2')
let settle!: (outcome: SubmitOutcome) => void
sink.mockImplementationOnce(() => new Promise<SubmitOutcome>((resolve) => { settle = resolve }))
fireEvent.keyDown(textarea, { key: 'Enter' })
expect(sink).toHaveBeenCalledWith('', ['draft-1'], 'queue', expect.any(AbortSignal))
expect(attachmentOwner(result.slotCalls).attachments).toEqual([attachments[0]])
await act(async () => { settle({ kind: 'success' }) })
await vi.waitFor(() => {
expect(attachmentOwner(result.slotCalls).attachments).toEqual([])
})
})
it('announces an image-intake rejection as a fading toast, repeatable for the same reason', () => {
@@ -439,8 +457,11 @@ describe('Enter semantics', () => {
it('plain Enter submits queue mode through the machine; repeat and empty are suppressed', () => {
const { textarea, sink } = bench({ draft: 'hello' })
fireEvent.keyDown(textarea, { key: 'Enter' })
expect(sink).toHaveBeenCalledWith('hello', [], 'queue')
expect(sink).toHaveBeenCalledWith('hello', [], 'queue', expect.any(AbortSignal))
// The submitting-phase lock, not draft emptiness, suppresses the repeat:
// the draft is still uncleared while the sink round-trip is in flight.
fireEvent.keyDown(textarea, { key: 'Enter', repeat: true })
fireEvent.keyDown(textarea, { key: 'Enter' })
expect(sink).toHaveBeenCalledTimes(1)
const empty = bench({ draft: ' ' })
fireEvent.keyDown(empty.textarea, { key: 'Enter' })
@@ -464,15 +485,15 @@ describe('Enter semantics', () => {
it('Ctrl/Meta+Enter sends normally while idle and steers while running', () => {
const idle = bench({ draft: 'hello' })
fireEvent.keyDown(idle.textarea, { key: 'Enter', metaKey: true })
expect(idle.sink).toHaveBeenCalledWith('hello', [], 'queue')
expect(idle.sink).toHaveBeenCalledWith('hello', [], 'queue', expect.any(AbortSignal))
const busyCtrl = bench({ running: true, draft: 'steer with ctrl' })
fireEvent.keyDown(busyCtrl.textarea, { key: 'Enter', ctrlKey: true })
expect(busyCtrl.sink).toHaveBeenCalledWith('steer with ctrl', [], 'steer')
expect(busyCtrl.sink).toHaveBeenCalledWith('steer with ctrl', [], 'steer', expect.any(AbortSignal))
const busyMeta = bench({ running: true, draft: 'steer with cmd' })
fireEvent.keyDown(busyMeta.textarea, { key: 'Enter', metaKey: true })
expect(busyMeta.sink).toHaveBeenCalledWith('steer with cmd', [], 'steer')
expect(busyMeta.sink).toHaveBeenCalledWith('steer with cmd', [], 'steer', expect.any(AbortSignal))
})
it('empty-draft Cmd/Ctrl+Enter steers the whole queue instead of submitting', () => {
@@ -537,7 +558,7 @@ describe('Enter semantics', () => {
const steerQueue = vi.fn()
const { textarea, sink } = bench({ running: true, queue: [row('q-1')], draft: '插话', steerQueue })
fireEvent.keyDown(textarea, { key: 'Enter', ctrlKey: true })
expect(sink).toHaveBeenCalledWith('插话', [], 'steer')
expect(sink).toHaveBeenCalledWith('插话', [], 'steer', expect.any(AbortSignal))
expect(steerQueue).not.toHaveBeenCalled()
})
@@ -585,7 +606,7 @@ describe('running and lock semantics', () => {
expect(textarea.disabled).toBe(false)
fireEvent.change(textarea, { target: { value: '排队消息2' } })
fireEvent.keyDown(textarea, { key: 'Enter' })
expect(sink).toHaveBeenCalledWith('排队消息2', [], 'queue')
expect(sink).toHaveBeenCalledWith('排队消息2', [], 'queue', expect.any(AbortSignal))
expect(button.getAttribute('aria-label')).toBe('停止生成')
fireEvent.click(button)
expect(stop).toHaveBeenCalledTimes(1)
@@ -594,17 +615,17 @@ describe('running and lock semantics', () => {
it('running plain Enter follows the busy-state Steer preference', () => {
const { textarea, sink } = bench({ running: true, busyEnter: 'steer', draft: '直接插话' })
fireEvent.keyDown(textarea, { key: 'Enter' })
expect(sink).toHaveBeenCalledWith('直接插话', [], 'steer')
expect(sink).toHaveBeenCalledWith('直接插话', [], 'steer', expect.any(AbortSignal))
})
it('running Cmd/Ctrl+Enter uses the opposite of the busy-state Enter preference', () => {
const meta = bench({ running: true, busyEnter: 'steer', draft: '排到下一轮' })
fireEvent.keyDown(meta.textarea, { key: 'Enter', metaKey: true })
expect(meta.sink).toHaveBeenCalledWith('排到下一轮', [], 'queue')
expect(meta.sink).toHaveBeenCalledWith('排到下一轮', [], 'queue', expect.any(AbortSignal))
const ctrl = bench({ running: true, busyEnter: 'steer', draft: 'also queue' })
fireEvent.keyDown(ctrl.textarea, { key: 'Enter', ctrlKey: true })
expect(ctrl.sink).toHaveBeenCalledWith('also queue', [], 'queue')
expect(ctrl.sink).toHaveBeenCalledWith('also queue', [], 'queue', expect.any(AbortSignal))
})
it('running continuable subagent keeps Send beside an independent Stop', () => {
@@ -624,7 +645,7 @@ describe('running and lock semantics', () => {
expect(interruptButton).not.toBeNull()
expect(textarea.disabled).toBe(false)
fireEvent.click(button)
expect(sink).toHaveBeenCalledWith('后续消息', [], 'queue')
expect(sink).toHaveBeenCalledWith('后续消息', [], 'queue', expect.any(AbortSignal))
fireEvent.click(interruptButton!)
expect(stop).toHaveBeenCalledTimes(1)
})
@@ -681,11 +702,11 @@ describe('running and lock semantics', () => {
}
const plain = bench({ running: true, busyEnter: 'steer', draft: 'plain', subagent })
fireEvent.keyDown(plain.textarea, { key: 'Enter' })
expect(plain.sink).toHaveBeenCalledWith('plain', [], 'queue')
expect(plain.sink).toHaveBeenCalledWith('plain', [], 'queue', expect.any(AbortSignal))
const accelerated = bench({ running: true, draft: 'accelerated', subagent })
fireEvent.keyDown(accelerated.textarea, { key: 'Enter', metaKey: true })
expect(accelerated.sink).toHaveBeenCalledWith('accelerated', [], 'queue')
expect(accelerated.sink).toHaveBeenCalledWith('accelerated', [], 'queue', expect.any(AbortSignal))
})
it('disabled (session removed) locks the textarea and chrome', () => {
@@ -698,7 +719,7 @@ describe('running and lock semantics', () => {
it('idle primary sends and disables on empty draft', () => {
const { button, sink } = bench({ draft: 'go' })
fireEvent.click(button)
expect(sink).toHaveBeenCalledWith('go', [], 'queue')
expect(sink).toHaveBeenCalledWith('go', [], 'queue', expect.any(AbortSignal))
const empty = bench()
expect(empty.button.disabled).toBe(true)
})
@@ -72,16 +72,17 @@ describe('input-machine: plain × enter', () => {
it('non-command text falls to the default sink', () => {
const m = new InputMachine()
m.dispatch({ type: 'draft-changed', draft: 'hello world' })
expect(m.dispatch({ type: 'enter', mode: 'queue' }))
.toEqual([{ type: 'default-sink', draft: 'hello world', mode: 'queue' }])
expect(m.state.phase).toBe('plain')
const effect = effectAt(m.dispatch({ type: 'enter', mode: 'queue' }), 0, 'default-sink')
expect(effect).toMatchObject({ draft: 'hello world', mode: 'queue' })
expect(effect.attempt.draftSnapshot).toBe('hello world')
expect(m.state.phase).toBe('submitting')
})
it('retains an explicit steer mode on the default sink effect', () => {
const m = new InputMachine()
m.dispatch({ type: 'draft-changed', draft: 'steer now' })
expect(m.dispatch({ type: 'enter', mode: 'steer' }))
.toEqual([{ type: 'default-sink', draft: 'steer now', mode: 'steer' }])
expect(effectAt(m.dispatch({ type: 'enter', mode: 'steer' }), 0, 'default-sink'))
.toMatchObject({ draft: 'steer now', mode: 'steer' })
})
it('leading "/" enters adjudicating with a minted attempt carrying the draft snapshot', () => {
@@ -104,8 +105,8 @@ describe('input-machine: plain × enter', () => {
it('a non-whitespace prefix before "/" is not leading — default sink', () => {
const m = new InputMachine()
m.dispatch({ type: 'draft-changed', draft: '第一行\n/goal x' })
expect(m.dispatch({ type: 'enter', mode: 'queue' }))
.toEqual([{ type: 'default-sink', draft: '第一行\n/goal x', mode: 'queue' }])
expect(effectAt(m.dispatch({ type: 'enter', mode: 'queue' }), 0, 'default-sink'))
.toMatchObject({ draft: '第一行\n/goal x', mode: 'queue' })
})
})
@@ -134,9 +135,12 @@ describe('input-machine: adjudication outcomes', () => {
it('undefined outcome falls back to the default sink', () => {
const m = new InputMachine()
const attempt = enterAdjudicating(m, '/unknown thing', 'steer')
expect(m.dispatch({ type: 'adjudicated', attempt, outcome: undefined }))
.toEqual([{ type: 'default-sink', draft: '/unknown thing', mode: 'steer' }])
expect(m.state.phase).toBe('plain')
expect(effectAt(
m.dispatch({ type: 'adjudicated', attempt, outcome: undefined }),
0,
'default-sink',
)).toMatchObject({ attempt, draft: '/unknown thing', mode: 'steer' })
expect(m.state.phase).toBe('submitting')
})
it("'handled' lands plain with zero effects (popup shell path)", () => {
@@ -484,6 +488,22 @@ describe('input-machine: undo / redo', () => {
expect(m.dispatch({ type: 'undo' })).toEqual([])
expect(m.state.draft).toBe('')
})
it('keeps a suffix typed during the round-trip and drops interleaved edits with the commit', () => {
const m = new InputMachine()
m.dispatch({ type: 'draft-changed', draft: 'hello' })
const effect = effectAt(m.dispatch({ type: 'enter', mode: 'queue' }), 0, 'default-sink')
m.dispatch({ type: 'draft-changed', draft: 'hello world' })
m.dispatch({ type: 'submit-settled', attempt: effect.attempt, ok: true })
expect(m.state.draft).toBe(' world')
const n = new InputMachine()
n.dispatch({ type: 'draft-changed', draft: 'hello' })
const second = effectAt(n.dispatch({ type: 'enter', mode: 'queue' }), 0, 'default-sink')
n.dispatch({ type: 'draft-changed', draft: 'hXello' })
n.dispatch({ type: 'submit-settled', attempt: second.attempt, ok: true })
expect(n.state.draft).toBe('')
})
})
describe('input-machine: paste plane', () => {
@@ -80,7 +80,7 @@ function bench(over?: {
submit?: (args: string) => Promise<SubmitOutcome>
serialize?: (ids: readonly DraftAttachmentId[]) => Promise<readonly SubmitImageAttachment[]>
}) {
const sink = vi.fn()
const sink = vi.fn(() => Promise.resolve<SubmitOutcome>({ kind: 'success' }))
const serialize = vi.fn(over?.serialize ?? (() => Promise.resolve<readonly SubmitImageAttachment[]>([])))
const release = vi.fn()
const shell = new SessionInputShell({ actx: SCTX, defaultSink: sink, commandImages: { serialize, release, unsupportedNotice: (token: string) => `${token.trim()} images-unsupported` } })
@@ -104,13 +104,15 @@ function bench(over?: {
}
describe('matrix row: plain', () => {
it('enter falls to the default sink; no claim on the currency; edits free', () => {
it('enter falls to the default sink; no claim on the currency; edits free', async () => {
const { textarea, shell, sink } = bench()
fireEvent.change(textarea, { target: { value: '普通消息' } })
expect(shell.snapshot.claim).toBeUndefined()
fireEvent.keyDown(textarea, { key: 'Enter' })
expect(sink).toHaveBeenCalledWith('普通消息', [], 'queue')
expect(shell.snapshot.phase).toBe('plain')
expect(sink).toHaveBeenCalledWith('普通消息', [], 'queue', expect.any(AbortSignal))
expect(shell.snapshot.phase).toBe('submitting')
await vi.waitFor(() => { expect(shell.snapshot.phase).toBe('plain') })
expect(shell.snapshot.claim).toBeUndefined()
})
})
@@ -299,7 +301,7 @@ describe('matrix row: locked (session disabled)', () => {
expect((textarea).disabled).toBe(false)
fireEvent.change(textarea, { target: { value: '排队' } })
fireEvent.keyDown(textarea, { key: 'Enter' })
expect(sink).toHaveBeenCalledWith('排队', [], 'queue')
expect(sink).toHaveBeenCalledWith('排队', [], 'queue', expect.any(AbortSignal))
})
})
@@ -0,0 +1,204 @@
/**
* Reference-submit transaction coverage: chips serialize through their
* owner, stay resident through Host rejection, and clear only after an
* accepted prompt.
*/
import { describe, expect, it, vi } from 'vitest'
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
import type { InputTriggerController, SubmitOutcome } from '@deepseek-ai/dsh-client-ui-input-trigger/client'
import { SessionInputShell } from '../src/client/input/facade.ts'
import type { DraftAttachmentId } from '../src/client/input/contract.ts'
import { PLACEHOLDER } from '../src/client/input/machine.ts'
const mention = '@[Research](dsh-session:InNvdXJjZSI)'
const commandImages = {
serialize: () => Promise.resolve([]),
release: () => {},
unsupportedNotice: (token: string) => `${token.trim()} images-unsupported`,
}
function chip(shell: SessionInputShell): void {
shell.setDraft('@res')
const accepted = shell.insertReference({
source: 'reference',
ref: mention,
label: '@Research',
clipboardText: mention,
}, {
start: 0,
end: 4,
draftRev: shell.snapshot.draftRev,
})
expect(accepted).toBe(true)
}
describe('reference submission', () => {
it('retains the chip on Host failure and clears it only after a later accepted retry', async () => {
const serializeReference = vi.fn(() => Promise.resolve(mention))
const sink = vi.fn<(
_text: string,
_imageIds: readonly DraftAttachmentId[],
_mode: 'queue' | 'steer',
_signal: AbortSignal,
) => Promise<SubmitOutcome>>()
.mockResolvedValueOnce({ kind: 'error', text: 'snapshot unavailable' })
.mockResolvedValueOnce({ kind: 'success' })
const inputTriggers = {
serializeReference,
track: vi.fn(),
} as unknown as InputTriggerController
const shell = new SessionInputShell({
actx: {} as ClientContext,
inputTriggers: () => inputTriggers,
defaultSink: sink,
commandImages,
})
chip(shell)
expect(shell.snapshot).toMatchObject({
draft: `${PLACEHOLDER} `,
occurrences: [{ source: 'reference', ref: mention, label: '@Research' }],
})
shell.submit('queue')
expect(shell.snapshot.phase).toBe('submitting')
await vi.waitFor(() => {
expect(shell.snapshot.phase).toBe('plain')
})
expect(sink).toHaveBeenNthCalledWith(1, mention, [], 'queue', expect.any(AbortSignal))
expect(shell.snapshot).toMatchObject({
draft: `${PLACEHOLDER} `,
occurrences: [{ source: 'reference', ref: mention, label: '@Research' }],
})
expect(shell.notices.getSnapshot()).toMatchObject({
level: 'error',
text: 'snapshot unavailable',
})
shell.submit('queue')
await vi.waitFor(() => {
expect(shell.snapshot.draft).toBe('')
})
expect(sink).toHaveBeenNthCalledWith(2, mention, [], 'queue', expect.any(AbortSignal))
expect(shell.snapshot.occurrences).toEqual([])
expect(serializeReference).toHaveBeenCalledTimes(2)
})
it('blocks submission and retains the chip when its owner cannot serialize it', async () => {
const sink = vi.fn()
const inputTriggers = {
serializeReference: () => Promise.reject(new Error('reference codec unavailable')),
track: vi.fn(),
} as unknown as InputTriggerController
const shell = new SessionInputShell({
actx: {} as ClientContext,
inputTriggers: () => inputTriggers,
defaultSink: sink,
commandImages,
})
chip(shell)
shell.submit()
await vi.waitFor(() => {
expect(shell.snapshot.phase).toBe('plain')
})
expect(sink).not.toHaveBeenCalled()
expect(shell.snapshot.draft).toBe(`${PLACEHOLDER} `)
expect(shell.snapshot.occurrences).toHaveLength(1)
expect(shell.notices.getSnapshot()).toMatchObject({
level: 'error',
text: 'reference codec unavailable',
})
})
it('aborts Host-side preparation when the input shell is disposed', () => {
let signal: AbortSignal | undefined
const shell = new SessionInputShell({
actx: {} as ClientContext,
defaultSink: (_text, _imageIds, _mode, received) => {
signal = received
return new Promise<SubmitOutcome>(() => {})
},
commandImages,
})
shell.setDraft('send this')
shell.submit()
expect(signal?.aborted).toBe(false)
shell.dispose()
expect(signal?.aborted).toBe(true)
expect(shell.snapshot.phase).toBe('plain')
expect(shell.snapshot.draft).toBe('send this')
})
it('retains a rejected default message without duplicating its prompt error notice', async () => {
const shell = new SessionInputShell({
actx: {} as ClientContext,
defaultSink: () => Promise.resolve({ kind: 'error' }),
commandImages,
})
shell.setDraft('retry this')
shell.submit()
await vi.waitFor(() => {
expect(shell.snapshot.phase).toBe('plain')
})
expect(shell.snapshot.draft).toBe('retry this')
expect(shell.notices.getSnapshot()).toBeNull()
})
})
describe('submit transaction hardening', () => {
it('sends one image-only prompt per settlement, ignoring Enter during the round-trip', async () => {
let settle!: (outcome: SubmitOutcome) => void
const sink = vi.fn(() => new Promise<SubmitOutcome>((resolve) => { settle = resolve }))
const shell = new SessionInputShell({
actx: {} as ClientContext,
defaultSink: sink,
commandImages,
})
expect(shell.addImages(['img-1' as DraftAttachmentId])).toBe(true)
shell.submit('queue')
shell.submit('queue')
expect(sink).toHaveBeenCalledTimes(1)
settle({ kind: 'success' })
await vi.waitFor(() => {
expect(shell.snapshot.imageIds).toEqual([])
})
expect(shell.addImages(['img-2' as DraftAttachmentId])).toBe(true)
shell.submit('queue')
expect(sink).toHaveBeenCalledTimes(2)
})
it('retains an image-only rejection without duplicating its prompt error notice', async () => {
const sink = vi.fn(() => Promise.resolve<SubmitOutcome>({ kind: 'error' }))
const shell = new SessionInputShell({
actx: {} as ClientContext,
defaultSink: sink,
commandImages,
})
const imageId = 'img-1' as DraftAttachmentId
shell.addImages([imageId])
shell.submit()
await Promise.resolve()
await Promise.resolve()
expect(shell.snapshot.imageIds).toEqual([imageId])
expect(shell.notices.getSnapshot()).toBeNull()
})
it('re-tracks at the caret when a continuing insert-text splice lands (directory descent)', () => {
const track = vi.fn()
const shell = new SessionInputShell({
actx: {} as ClientContext,
inputTriggers: () => ({ track } as unknown as InputTriggerController),
defaultSink: vi.fn(),
commandImages,
})
shell.setDraft('@sr')
const applied = shell.insertText('@src/', { start: 0, end: 3, draftRev: shell.snapshot.draftRev }, true)
expect(applied).toBe(true)
expect(shell.snapshot.draft).toBe('@src/')
expect(track).toHaveBeenCalledWith('@src/', 5, { tier: 'plain' }, shell.snapshot.draftRev)
track.mockClear()
shell.insertText(' plain ', { start: 0, end: 0, draftRev: shell.snapshot.draftRev })
expect(track).not.toHaveBeenCalled()
})
})
@@ -119,7 +119,7 @@ async function scopedBench(register?: (inputTriggers: InputTriggerService) => vo
register?.(inputTriggers)
const actx = sessions.scope(sessionId)!
const controller = inputTriggers.sessionOf(actx)
const sink = vi.fn()
const sink = vi.fn(() => Promise.resolve<SubmitOutcome>({ kind: 'success' }))
const serialize = vi.fn((ids: readonly DraftAttachmentId[]) => Promise.resolve(ids.map(() => PNG)))
const release = vi.fn()
const shell = new SessionInputShell({ actx, inputTriggers: () => controller, defaultSink: sink, commandImages: { serialize, release, unsupportedNotice: (token: string) => `${token.trim()} images-unsupported` } })
@@ -165,6 +165,7 @@ async function scopedBench(register?: (inputTriggers: InputTriggerService) => vo
controller.toggleSource('command', {
trigger: '/',
query: '',
quoted: false,
position: snapshot.draft.slice(0, selection.start).trim() === '' ? 'leading' : 'inline',
span: { ...selection, draftRev: snapshot.draftRev },
})
@@ -262,7 +263,7 @@ describe('scenario D: execute-kind /compact', () => {
act(() => { b2.shell.setDraft('/compact 现在') })
fireEvent.keyDown(b2.textarea, { key: 'Enter' })
// execute with trailing → matchEnter answers undefined → default sink.
await vi.waitFor(() => { expect(b2.sink).toHaveBeenCalledWith('/compact 现在', [], 'queue') })
await vi.waitFor(() => { expect(b2.sink).toHaveBeenCalledWith('/compact 现在', [], 'queue', expect.any(AbortSignal)) })
expect(b2.executed).toHaveLength(0)
})
})
@@ -343,8 +344,8 @@ describe('scenario I: unknown /xyz + enter', () => {
const b = await bench()
act(() => { b.shell.setDraft('/xyz 干点啥') })
fireEvent.keyDown(b.textarea, { key: 'Enter' })
await vi.waitFor(() => { expect(b.sink).toHaveBeenCalledWith('/xyz 干点啥', [], 'queue') })
expect(b.shell.snapshot.phase).toBe('plain')
await vi.waitFor(() => { expect(b.sink).toHaveBeenCalledWith('/xyz 干点啥', [], 'queue', expect.any(AbortSignal)) })
await vi.waitFor(() => { expect(b.shell.snapshot.phase).toBe('plain') })
expect(b.execute).not.toHaveBeenCalled()
})
@@ -31,7 +31,7 @@ import type { ViewTab } from '../src/client/contract/views.ts'
/** Machine-backed wiring over a sink spy. */
function fakeWiring() {
const sink = vi.fn()
const sink = vi.fn(() => Promise.resolve({ kind: 'success' as const }))
const shell = new SessionInputShell({ actx: {} as ClientContext, defaultSink: sink, commandImages: { serialize: () => Promise.resolve([]), release: () => {}, unsupportedNotice: (token: string) => `${token.trim()} images-unsupported` } })
return { wiring: shell, sink, shell }
}
@@ -309,7 +309,7 @@ describe('ConversationRoot resident composer', () => {
fireEvent.change(box, { target: { value: 'ordinary revised' } })
expect(b.chat.store.getSnapshot().draft).toBe('ordinary revised')
fireEvent.keyDown(box, { key: 'Enter' })
expect(b.sink).toHaveBeenCalledWith('ordinary revised', [], 'queue')
expect(b.sink).toHaveBeenCalledWith('ordinary revised', [], 'queue', expect.any(AbortSignal))
expect((b.view.getByRole('button', { name: 'Child' }) as HTMLButtonElement).disabled).toBe(true)
expect(b.view.queryByText('Root')).toBeNull()
})
@@ -50,7 +50,8 @@
"@deepseek-ai/dsh-client-locale": "workspace:^",
"@deepseek-ai/dsh-client-runtime": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
"@deepseek-ai/cordis": "workspace:^",
"@deepseek-ai/dsh-file-reference": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-client-locale": "workspace:^",
@@ -61,7 +62,8 @@
"@deepseek-ai/dsh-invariants": "workspace:^",
"@types/react": "~18.3.1",
"@deepseek-ai/cordis": "workspace:^",
"react": "^18.2.0"
"react": "^18.2.0",
"@deepseek-ai/dsh-file-reference": "workspace:^"
},
"files": [
"lib/index.js",
@@ -60,6 +60,20 @@
background: var(--dsw-alias-interactive-bg-hover);
}
.sectionTitle {
flex: none;
min-height: 26px;
padding: 6px 10px 2px;
color: var(--dsw-alias-label-tertiary);
font-size: 12px;
font-weight: 500;
line-height: 18px;
}
.sectionTitle:not(:first-child) {
margin-top: 4px;
}
.itemIcon {
display: inline-flex;
flex: none;
@@ -87,25 +87,29 @@ export function MenuView({ menu, onPick, onDismiss, t }: MenuViewProps) {
: group.items.map((item, index) => {
const active = highlight !== null && highlight.source === group.source && highlight.index === index
return (
<button
key={`${group.source}:${item.name}`}
id={optionId(group.source, index)}
type="button"
role="option"
aria-selected={active}
className={clsx(css.item, active && css.active)}
// mousedown, not click: the textarea keeps focus (combobox
// pattern) — preventing default stops the focus steal, and the
// pick runs before any blur-driven teardown.
onMouseDown={(ev) => {
ev.preventDefault()
onPick(group.source, index)
}}
>
{item.icon !== undefined && <span className={css.itemIcon} aria-hidden>{item.icon}</span>}
<span className={css.itemName}>{item.name}</span>
{item.description !== undefined && <span className={css.itemDescription}>{item.description}</span>}
</button>
<Fragment key={optionId(group.source, index)}>
{item.section !== undefined && item.section !== group.items[index - 1]?.section
? <div className={css.sectionTitle} role="presentation">{item.section}</div>
: null}
<button
id={optionId(group.source, index)}
type="button"
role="option"
aria-selected={active}
className={clsx(css.item, active && css.active)}
// mousedown, not click: the textarea keeps focus (combobox
// pattern) — preventing default stops the focus steal, and the
// pick runs before any blur-driven teardown.
onMouseDown={(ev) => {
ev.preventDefault()
onPick(group.source, index)
}}
>
{item.icon !== undefined && <span className={css.itemIcon} aria-hidden>{item.icon}</span>}
<span className={css.itemName}>{item.name}</span>
{item.description !== undefined && <span className={css.itemDescription}>{item.description}</span>}
</button>
</Fragment>
)
})}
</Fragment>
@@ -101,6 +101,7 @@ export class InputTriggerController {
const prev = this.menu.getSnapshot()
const same = !launched && prev.open && prev.hit !== null
&& prev.hit.trigger === hit.trigger && prev.hit.query === hit.query
&& prev.hit.quoted === hit.quoted
&& prev.hit.span.start === hit.span.start && prev.hit.span.end === hit.span.end
this.hit = hit
if (same) return
@@ -324,7 +325,11 @@ export class InputTriggerController {
return actx.bail(actx, 'slash/input-begin-command', { claim: outcome.claim, span }) === true
}
if ('text' in outcome) {
return actx.bail(actx, 'slash/input-insert-text', { text: outcome.text, span }) === true
return actx.bail(actx, 'slash/input-insert-text', {
text: outcome.text,
span,
...outcome.continue === true ? { continue: true } : {},
}) === true
}
return actx.bail(actx, 'slash/input-insert-reference', { reference: outcome.insert, span }) === true
}
@@ -367,7 +372,12 @@ export class InputTriggerController {
const projection = this.project()
for (const source of roster) {
void source
.candidates(projection, { query: hit.query, position: hit.position, signal: controller.signal })
.candidates(projection, {
query: hit.query,
quoted: hit.quoted,
position: hit.position,
signal: controller.signal,
})
.then(
(items) => {
if (controller.signal.aborted) return
@@ -11,6 +11,8 @@ export interface TriggerHit {
readonly trigger: TriggerChar
/** Text between the trigger char and the caret, live-filtered. */
readonly query: string
/** True only for an open quoted `@file` token. */
readonly quoted: boolean
/** leading = draft trimmed (whitespace incl. newlines) starts with the token. */
readonly position: TriggerPosition
/** Token span; draftRev injected by the caller. */
@@ -19,8 +21,9 @@ export interface TriggerHit {
/**
* Detect a trigger token at the caret under the given guard tier.
* Word-boundary rule: the char before the trigger is start-of-line,
* whitespace, or punctuation; `user@host` and URL '/' do not trigger.
* `@` uses the shared file-reference start/whitespace grammar; `/` accepts
* punctuation boundaries with URL carve-outs. `user@host` and URL `/` do not
* trigger.
* Returns null when no trigger is live at the caret.
*/
export type DetectTrigger = (draft: string, caret: number, guard: TriggerGuard) => TriggerHit | null
@@ -3,6 +3,7 @@
* the caret for a live trigger char under the guard tier and applies the
* word-boundary rules. Zero React / DOM / cordis.
*/
import { activeAtToken } from '@deepseek-ai/dsh-file-reference/grammar'
import type { TriggerChar } from '../types.ts'
import type { DetectTrigger } from './contract.ts'
@@ -29,10 +30,10 @@ function boundaryOk(draft: string, index: number, char: TriggerChar): boolean {
}
/**
* Detect a trigger token at the caret. Scans left from the caret and stops
* at the first whitespace (the token under edit never spans whitespace);
* trigger chars failing the guard tier or the word boundary are treated as
* ordinary token chars and the scan continues (`user@host`, URL slashes).
* Detect a trigger token at the caret. `@` first uses the shared grammar,
* including an open quoted token that may span whitespace. Slash detection
* scans left to the first whitespace; slashes failing the word boundary are
* treated as ordinary token chars and the scan continues (URL slashes).
* Guard tiers: plain = both chars live; claimed = '/' fully suppressed,
* '@' live; frozen = none.
*
@@ -46,15 +47,27 @@ function boundaryOk(draft: string, index: number, char: TriggerChar): boolean {
*/
export const detectTrigger: DetectTrigger = (draft, caret, guard) => {
if (guard.tier === 'frozen') return null
const at = activeAtToken(draft, caret)
if (at !== undefined) {
const start = caret - at.prefix.length
return {
trigger: '@',
query: at.query,
quoted: at.quoted,
position: draft.search(/\S/) === start ? 'leading' : 'inline',
span: { start, end: caret, draftRev: 0 },
}
}
for (let i = caret - 1; i >= 0; i--) {
const ch = draft.charAt(i)
if (WHITESPACE.test(ch)) return null
if (ch !== '/' && ch !== '@') continue
if (guard.tier === 'claimed' && ch === '/') continue
if (ch !== '/') continue
if (guard.tier === 'claimed') continue
if (!boundaryOk(draft, i, ch)) continue
return {
trigger: ch,
query: draft.slice(i + 1, caret),
quoted: false,
position: draft.search(/\S/) === i ? 'leading' : 'inline',
span: { start: i, end: caret, draftRev: 0 },
}
+10 -2
View File
@@ -1,6 +1,6 @@
/**
* Frozen cross-package contract for the input trigger pipeline. Types only
* no runtime code. Sources (ui-commands / ui-skill / ui-subagent) and the
* no runtime code. Sources (ui-commands / ui-skill / ui-reference) and the
* conversation input layer import from here; changes require main-thread
* arbitration.
*
@@ -35,6 +35,10 @@ export interface InputTriggerCandidate {
readonly description?: string
readonly icon?: string
readonly hint?: string
/** Optional visual group heading shared by adjacent candidates. */
readonly section?: string
/** Opaque source-owned pick payload. */
readonly value?: string
}
/** Pick-moment snapshot of the trigger token span. CAS: stale draftRev ⇒ the whole action no-ops. */
@@ -110,7 +114,7 @@ export interface SubmitOutcome {
export type PickOutcome =
| { readonly claim: CommandClaim }
| { readonly insert: ReferenceInsert }
| { readonly text: string }
| { readonly text: string; readonly continue?: boolean }
| 'handled'
| undefined
@@ -127,6 +131,8 @@ export interface SubmitEnvelope {
/** Candidate request passed to a source. The signal is superseded on query change / menu close. */
export interface CandidateRequest {
readonly query: string
/** Whether the active @file token is an open quoted path. */
readonly quoted?: boolean
readonly position: TriggerPosition
readonly signal: AbortSignal
}
@@ -256,6 +262,8 @@ export interface InsertTextRequest {
/** Literal replacement for the trigger token span (e.g. `/name `). */
readonly text: string
readonly span: TokenSpan
/** Keep completion open after the splice (directory descent): the input re-tracks at the caret. */
readonly continue?: boolean
}
declare module '@deepseek-ai/cordis' {
@@ -91,6 +91,17 @@ describe('detectTrigger guard tiers', () => {
})
describe('detectTrigger span and query', () => {
it('keeps an open quoted @file token active across spaces', () => {
const draft = 'read @"docs/design notes'
expect(atEnd(draft)).toMatchObject({
trigger: '@',
query: 'docs/design notes',
quoted: true,
position: 'inline',
span: { start: 5, end: draft.length },
})
})
it('spans trigger char to caret with a placeholder draftRev', () => {
const hit = detectTrigger('say /goal', 9, plain)
expect(hit?.span).toEqual({ start: 4, end: 9, draftRev: 0 })
@@ -7,6 +7,7 @@ import { exactMatch, MENU_CLOSED, menuReduce, seedGroups } from '../src/core/men
const hit = (query = ''): TriggerHit => ({
trigger: '/',
query,
quoted: false,
position: 'leading',
span: { start: 0, end: 1 + query.length, draftRev: 1 },
})
@@ -19,6 +19,7 @@ import { MenuView } from '../src/client/MenuView.tsx'
const hit: TriggerHit = {
trigger: '/',
query: 'g',
quoted: false,
position: 'leading',
span: { start: 0, end: 2, draftRev: 1 },
}
@@ -99,6 +100,31 @@ describe('MenuView', () => {
expect(titles(view.container)).toEqual(['命令', 'mystery', '技能'])
})
it('renders contiguous candidate sections once without changing option indexes', () => {
const { onPick } = mount(openState({
groups: [{
source: 'reference',
status: 'ready',
items: [
{ name: 'Folder · src/', section: '文件与文件夹' },
{ name: 'File · README.md', section: '文件与文件夹' },
{ name: 'Session · Research', section: 'Session 对话' },
],
}],
highlight: { source: 'reference', index: 0 },
}))
expect(screen.getAllByText('文件与文件夹')).toHaveLength(1)
expect(screen.getAllByText('Session 对话')).toHaveLength(1)
const options = screen.getAllByRole('option')
expect(options.map(option => option.textContent)).toEqual([
'Folder · src/',
'File · README.md',
'Session · Research',
])
fireEvent.mouseDown(options[2]!)
expect(onPick).toHaveBeenCalledWith('reference', 2)
})
it('exposes the highlight via aria-activedescendant and aria-selected', () => {
mount(openState({ highlight: { source: 'command', index: 1 } }))
const listbox = screen.getByRole('listbox')
@@ -359,6 +359,7 @@ describe('programmatic source launcher', () => {
const hit = {
trigger: '/' as const,
query: '',
quoted: false,
position: 'leading' as const,
span: { start: 2, end: 5, draftRev: 7 },
}
@@ -385,6 +386,7 @@ describe('programmatic source launcher', () => {
const hit = {
trigger: '/' as const,
query: '',
quoted: false,
position: 'leading' as const,
span: { start: 0, end: 0, draftRev: 1 },
}
@@ -496,6 +498,18 @@ describe('pick / scoped input events', () => {
expect(controller.menu.getSnapshot().open).toBe(false)
})
it('forwards a continuing text outcome so a directory pick keeps completion open', async () => {
const { controller, actx } = pickBench(() => ({ text: '@src/', continue: true }))
const texts: Array<{ text: string; continue?: boolean }> = []
actx.on('slash/input-insert-text', (req) => {
texts.push(req)
return true
})
await tick()
controller.pick('command', 0)
expect(texts).toEqual([{ text: '@src/', continue: true, span: { start: 0, end: 2, draftRev: 3 } }])
})
it('a text outcome the input declines answers false on the space path', async () => {
const src: InputTriggerSource = {
trigger: '/',

Some files were not shown because too many files have changed in this diff Show More