diff --git a/.agents/notes/implemented/feature/2026-07-21-cross-session-references.i18n.yaml b/.agents/notes/implemented/feature/2026-07-21-cross-session-references.i18n.yaml index b07c28f618..0cc877effb 100644 --- a/.agents/notes/implemented/feature/2026-07-21-cross-session-references.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-21-cross-session-references.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-21-cross-session-references.md -2026-07-21-cross-session-references.md: eaa8191a0e71edfc4ea056788e24f513e495cdce -2026-07-21-cross-session-references.zh.md: f9af2960d7905c91ea1cbdbb982ce7c0bf987aa2 +2026-07-21-cross-session-references.md: d9aaaca442ba939b4221351e0df3601294674587 +2026-07-21-cross-session-references.zh.md: 17c3f91225f5f5ae2c2b1a639cc9d3c1794a490b diff --git a/.agents/notes/implemented/feature/2026-07-21-cross-session-references.md b/.agents/notes/implemented/feature/2026-07-21-cross-session-references.md index eaa8191a0e..d9aaaca442 100644 --- a/.agents/notes/implemented/feature/2026-07-21-cross-session-references.md +++ b/.agents/notes/implemented/feature/2026-07-21-cross-session-references.md @@ -34,7 +34,7 @@ Reference preparation is not a new steering protocol and does not create a turn 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 discovery and preparation through `reference.sessions` and `session.prompt`, 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. The composer retains text and chips until preparation and enqueue succeed, restoring them unchanged after failure; replay associates the separate session-reference context with its neighboring direct message and renders a compact source summary instead of exposing the snapshot JSON. +Web exposes discovery through the `sessionReferenceResolver/candidates` Remote method and preparation through `session.prompt`, 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. The composer retains text and chips until preparation and enqueue succeed, restoring them unchanged after failure; 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. diff --git a/.agents/notes/implemented/feature/2026-07-21-cross-session-references.zh.md b/.agents/notes/implemented/feature/2026-07-21-cross-session-references.zh.md index f9af2960d7..17c3f91225 100644 --- a/.agents/notes/implemented/feature/2026-07-21-cross-session-references.zh.md +++ b/.agents/notes/implemented/feature/2026-07-21-cross-session-references.zh.md @@ -34,7 +34,7 @@ Web 负责快照/直接消息事务,不扩展通用收件箱记录。交付 统一的 Web `@` source 把会话候选与 Host 支持的文件发现组合在一起。会话候选查询会对 session id、cwd 或最新折叠后的标题执行不区分大小写的子串匹配,显示该标题,并在没有标题观察结果或标题观察失败时回退到 session id。查询遵循请求的取消信号;session id、cwd 和提及标签中的外部控制字符会被转义,但规范 URI 仍保留原始 id。 -Web 通过 `reference.sessions` 和 `session.prompt` 暴露发现与准备流程,详见 [Web 文件与会话引用](2026-07-27-web-file-and-session-references.md)。session 选择项是由 Host 生成的规范提及标记支撑的原子 chip。composer 会保留文本和 chip,直到准备与入队均成功;失败后会原样恢复。回放会把独立的 session-reference 上下文与相邻直接消息关联起来,并渲染精简来源摘要,而不暴露快照 JSON。 +Web 通过 `sessionReferenceResolver/candidates` Remote 方法暴露发现流程、通过 `session.prompt` 暴露准备流程,详见 [Web 文件与会话引用](2026-07-27-web-file-and-session-references.md)。session 选择项是由 Host 生成的规范提及标记支撑的原子 chip。composer 会保留文本和 chip,直到准备与入队均成功;失败后会原样恢复。回放会把独立的 session-reference 上下文与相邻直接消息关联起来,并渲染精简来源摘要,而不暴露快照 JSON。 [仅面向自动化的 ACP(Agent Client Protocol)传输层](../simplification/2026-07-23-acp-automation-only-protocol.md)有意不挂载会话查询或会话引用服务。 diff --git a/.agents/notes/implemented/feature/2026-07-27-web-file-and-session-references.i18n.yaml b/.agents/notes/implemented/feature/2026-07-27-web-file-and-session-references.i18n.yaml index 9016d377fb..fcd057ebd7 100644 --- a/.agents/notes/implemented/feature/2026-07-27-web-file-and-session-references.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-27-web-file-and-session-references.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-27-web-file-and-session-references.md -2026-07-27-web-file-and-session-references.md: dca1b6eb63eb96f54da85e8524f98018e7496f4b -2026-07-27-web-file-and-session-references.zh.md: 7c87a0aa54b4b528cb81967b818e815c8812ba8d +2026-07-27-web-file-and-session-references.md: 5032525d6362b1afa0da387c2dd3d5f2d961139f +2026-07-27-web-file-and-session-references.zh.md: fce6ec0d35b7712d2e966932144b468f88b67862 diff --git a/.agents/notes/implemented/feature/2026-07-27-web-file-and-session-references.md b/.agents/notes/implemented/feature/2026-07-27-web-file-and-session-references.md index dca1b6eb63..5032525d63 100644 --- a/.agents/notes/implemented/feature/2026-07-27-web-file-and-session-references.md +++ b/.agents/notes/implemented/feature/2026-07-27-web-file-and-session-references.md @@ -10,18 +10,18 @@ The Web composer had a reusable slash/reference trigger pipeline, but its `@` so ## Decision -Web exposes one combined `@file` and `@session` menu through `@deepseek-ai/dsh-client-ui-reference`. For each unquoted query it starts both Host lookups concurrently and deterministically orders files before sessions with stable labels; non-selectable `文件与文件夹` and `Session 对话` 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. +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 Host RPC and shared grammar. A file pick remains path-only prompt text and a directory pick retriggers completion below its trailing slash. +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. `session.prompt` parses those mentions and calls `ctx.sessionReferences.prepare()` before delivery. Queued delivery binds the prepared context to the exact message id through a one-shot `agent/prompt-submit` wrapper and attaches it only to an allowed decision; steering delivery calls `inject()` immediately before `steer()`. Invalid mentions, cancellation, missing capability, source-read failure, and budget failure deliver nothing. +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. `session.prompt` parses those mentions and calls `ctx.sessionReferenceResolver.prepare()` before delivery. Delivery binds the prepared context to the exact message id through a one-shot outer `agent/pre-step` listener that inserts the frozen snapshot immediately before that message on an enter decision; ordinary discard or agent disposal releases the listener, and queue-to-steer relocation preserves the pairing. Invalid mentions, cancellation, missing capability, source-read failure, and budget failure deliver nothing. -The input machine keeps ordinary draft text and atomic references until the default sink reports Host acceptance. Serialization or RPC failure returns the same draft to editing. On success the logged prompt envelope remains the replay authority: the browser renders adjacent metadata-confirmed session references as separate reference chips and preserves that projection when following text is adjacent without whitespace, plus a compact session-source summary instead of the snapshot JSON baked into model content. +The input machine keeps ordinary draft text and atomic references until the default sink reports Host acceptance. Serialization or Remote failure returns the same draft to editing. On success the logged prompt envelope remains the replay authority: the browser renders adjacent metadata-confirmed session references as separate reference chips and preserves that projection when following text is adjacent without whitespace, plus a compact session-source summary instead of the snapshot JSON baked into model content. ## Reference transaction ```text -type @ → parallel file/session RPCs → pick path text or canonical session chip +type @ → parallel file/session Remote calls → pick path text or canonical session chip → serialize draft → Host parses and prepares all sessions → enqueue once ↘ any pre-enqueue failure: retain the unchanged editable draft ``` @@ -42,8 +42,8 @@ File lookup is advisory and cancellable; selection itself performs no read. Sess ## 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, Host wire validation, all-or-nothing prompt preparation, and draft retention across serialization and RPC failures. The keyless assembled Web snapshot renders the available reference sections, selects a file, then selects a session reference through the real client composition. +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, the owning services' Remote faces, all-or-nothing prompt preparation, and draft retention across serialization and Remote failures. 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. The new file-reference seam adds two packages and one Host RPC domain, but keeps browser bundles free of Node APIs and permits another provider to align completion with a remote filesystem. Candidate lookup failures remain quiet menu degradation; submission failures remain explicit and recoverable. 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`. +Web now uses the shared `@file` discovery seam and structured session-reference identity, while Host services remain the authority for filesystem and session access. The new file-reference seam adds two packages whose discovery methods are unary Remote contracts on the owning services, keeps browser bundles free of Node APIs, and permits another provider to align completion with a remote filesystem. Candidate lookup failures remain quiet menu degradation; submission failures remain explicit and recoverable. 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`. diff --git a/.agents/notes/implemented/feature/2026-07-27-web-file-and-session-references.zh.md b/.agents/notes/implemented/feature/2026-07-27-web-file-and-session-references.zh.md index 7c87a0aa54..fce6ec0d35 100644 --- a/.agents/notes/implemented/feature/2026-07-27-web-file-and-session-references.zh.md +++ b/.agents/notes/implemented/feature/2026-07-27-web-file-and-session-references.zh.md @@ -10,18 +10,18 @@ Web 输入框已有可复用的斜杠命令/引用触发流水线,但它的 ## 决策 -Web 通过 `@deepseek-ai/dsh-client-ui-reference` 暴露一个合并的 `@file` 与 `@session` 菜单。每次处理未加引号的查询时,它会并发启动两项宿主查询,以确定性顺序把文件排在会话之前,并使用稳定标签;不可选择的 `文件与文件夹` 和 `Session 对话` 标题会区分两个连续的候选分组,且不会进入键盘选择索引。尚未闭合的带引号 token 只搜索文件。任一候选领域都可以独立失败,不会隐藏另一领域成功返回的行。 +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` 消费宿主 RPC 与共享语法。选择文件后仍只会把路径文本写入提示词,选择目录则会在其尾部斜杠后重新触发补全。 +文件功能遵循由三个包构成的 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:…)` 提及标记。`session.prompt` 会解析这些提及标记,并在递送前调用 `ctx.sessionReferences.prepare()`。队列递送通过一次性 `agent/prompt-submit` 包装层把准备后的上下文绑定到精确消息 id,并且只为获准决策附加该上下文;steering 递送会紧邻 `steer()` 之前调用 `inject()`。无效提及标记、取消、功能缺失、读取源会话失败和预算失败都不会递送消息。 +选择会话会创建一个原子的输入框引用。可见标签只用于呈现,隐藏值和剪贴板形式则是宿主生成的规范 `@[label](dsh-session:…)` 提及标记。`session.prompt` 会解析这些提及标记,并在递送前调用 `ctx.sessionReferenceResolver.prepare()`。递送通过一次性的外层 `agent/pre-step` 监听器把准备后的上下文绑定到精确消息 id:进入决策时把冻结快照插入到该消息紧前,普通丢弃或 agent(智能体)销毁会释放监听器,队列转 steering 的迁移保持这一配对。无效提及标记、取消、功能缺失、读取源会话失败和预算失败都不会递送消息。 -输入状态机在默认 sink 报告宿主已接受前,会保留普通草稿文本和原子引用。序列化或 RPC 失败后,同一草稿会回到可编辑状态。成功后,日志中的提示词封套仍是回放的权威来源:浏览器会把相邻且经元数据确认的会话引用分别渲染为独立的引用 chip,即使后续文本无空白紧邻,也会保持该投影;同时显示精简的会话来源摘要,而不会显示嵌入模型内容中的快照 JSON。 +输入状态机在默认 sink 报告宿主已接受前,会保留普通草稿文本和原子引用。序列化或 Remote 调用失败后,同一草稿会回到可编辑状态。成功后,日志中的提示词封套仍是回放的权威来源:浏览器会把相邻且经元数据确认的会话引用分别渲染为独立的引用 chip,即使后续文本无空白紧邻,也会保持该投影;同时显示精简的会话来源摘要,而不会显示嵌入模型内容中的快照 JSON。 ## 引用事务 ```text -type @ → parallel file/session RPCs → pick path text or canonical session chip +type @ → parallel file/session Remote calls → pick path text or canonical session chip → serialize draft → Host parses and prepares all sessions → enqueue once ↘ any pre-enqueue failure: retain the unchanged editable draft ``` @@ -42,8 +42,8 @@ type @ → parallel file/session RPCs → pick path text or canonical session ch ## 验证 -包(package)测试固定共享文件语法和排序、缓存失效及生命周期清理、Web 并行查询、带引号的路径、候选项独立失败、取消、不改变候选项索引的分组标题、文件/目录继续补全、规范会话 chip、相邻引用及相邻文本条件下的引用投影、codec 无损往返、宿主协议校验、全有或全无的提示词准备,以及在序列化和 RPC 失败时保留草稿。无密钥的装配 Web 快照会渲染可用的引用分组,并通过真实客户端组合依次选择文件和会话引用。 +包(package)测试固定共享文件语法和排序、缓存失效及生命周期清理、Web 并行查询、带引号的路径、候选项独立失败、取消、不改变候选项索引的分组标题、文件/目录继续补全、规范会话 chip、相邻引用及相邻文本条件下的引用投影、codec 无损往返、所属服务的 Remote 调用面、全有或全无的提示词准备,以及在序列化和 Remote 调用失败时保留草稿。无密钥的装配 Web 快照会渲染可用的引用分组,并通过真实客户端组合依次选择文件和会话引用。 ## 后果 -Web 现在使用共享的 `@file` 发现 seam 和结构化会话引用身份,宿主服务仍然是文件系统与会话访问的权威来源。新的文件引用 seam 增加了两个包和一个宿主 RPC 领域,但浏览器 bundle 中不包含 Node API,并允许其他提供方让补全与远程文件系统对齐。候选查询失败仍会让菜单静默降级;提交失败仍会显式报告且可恢复。文件引用只产生路径文本和稳定的条件式指引成本,而会话引用仍保留 `dsh-session-reference` 所拥有的有界快照开销与信任限定文本。 +Web 现在使用共享的 `@file` 发现 seam 和结构化会话引用身份,宿主服务仍然是文件系统与会话访问的权威来源。新的文件引用 seam 增加了两个包,其发现方法是所属服务上的一元 Remote 契约;浏览器 bundle 中不包含 Node API,并允许其他提供方让补全与远程文件系统对齐。候选查询失败仍会让菜单静默降级;提交失败仍会显式报告且可恢复。文件引用只产生路径文本和稳定的条件式指引成本,而会话引用仍保留 `dsh-session-reference` 所拥有的有界快照开销与信任限定文本。 diff --git a/apps/web/tests/reference-composer.e2e.ts b/apps/web/tests/reference-composer.e2e.ts index 8a1da34157..c67a975cbd 100644 --- a/apps/web/tests/reference-composer.e2e.ts +++ b/apps/web/tests/reference-composer.e2e.ts @@ -92,8 +92,8 @@ describe.skipIf(MODE === 'record')('web e2e: file and session references through 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('\u6587\u4ef6\u4e0e\u6587\u4ef6\u5939') - expect(snapshot).toContain('Session \u5bf9\u8bdd') + 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') diff --git a/apps/web/tests/snapshots/reference-composer/menu.expected.md b/apps/web/tests/snapshots/reference-composer/menu.expected.md index 1b2a95f426..f42be2dc47 100644 --- a/apps/web/tests/snapshots/reference-composer/menu.expected.md +++ b/apps/web/tests/snapshots/reference-composer/menu.expected.md @@ -1,5 +1,5 @@ - listbox "Trigger suggestions": - - text: reference 文件与文件夹 + - text: reference Files & folders - option "File · reference.txt reference.txt" [selected] - - text: Session 对话 + - text: Session conversations - option "Session · Research notes reference-source-session · {{cwd}} · {{timestamp}}" diff --git a/docs/capability-seams.i18n.yaml b/docs/capability-seams.i18n.yaml index bcc81a9737..da9ced92e9 100644 --- a/docs/capability-seams.i18n.yaml +++ b/docs/capability-seams.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/capability-seams.md -capability-seams.md: dddbb3822af6b0863c7f181e34f8eaa9ec68d87f -capability-seams.zh.md: 893d034b610ce9568ca88aa49b94e087fcff005d +capability-seams.md: 134f95f0efacafaf70017d61a8a1d468a79d4572 +capability-seams.zh.md: 48e880236298fc0b475fca9fb6d23aec549c059e diff --git a/docs/capability-seams.md b/docs/capability-seams.md index dddbb3822a..134f95f0ef 100644 --- a/docs/capability-seams.md +++ b/docs/capability-seams.md @@ -321,7 +321,6 @@ flowchart LR svc_dynamicCordisRunner --> pkg_tool_cordis svc_e2b --> pkg_fs_e2b svc_e2b --> pkg_subprocess_e2b - svc_fileReferences --> pkg_apiproxy svc_fs --> pkg_tool_fs svc_invariants --> pkg_agent svc_invariants --> pkg_agent_loop @@ -434,7 +433,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) | `apiproxy` | - | The interface returns path-only completion candidates within the addressed Agent cwd; providers own namespace access and ranking without reading file contents. | +| `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. | diff --git a/docs/capability-seams.zh.md b/docs/capability-seams.zh.md index 893d034b61..48e8802362 100644 --- a/docs/capability-seams.zh.md +++ b/docs/capability-seams.zh.md @@ -323,7 +323,6 @@ flowchart LR svc_dynamicCordisRunner --> pkg_tool_cordis svc_e2b --> pkg_fs_e2b svc_e2b --> pkg_subprocess_e2b - svc_fileReferences --> pkg_apiproxy svc_fs --> pkg_tool_fs svc_invariants --> pkg_agent svc_invariants --> pkg_agent_loop @@ -436,7 +435,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) | `apiproxy` | - | 该接口返回指定 Agent cwd 内仅含路径的补全候选;提供方负责命名空间访问和排序,但不会读取文件内容。 | +| `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。 | diff --git a/docs/event-producer-consumer.i18n.yaml b/docs/event-producer-consumer.i18n.yaml index f53cf3ea2f..4a61e2d00c 100644 --- a/docs/event-producer-consumer.i18n.yaml +++ b/docs/event-producer-consumer.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/event-producer-consumer.md -event-producer-consumer.md: e6a99e2378634e04b3f649268c10bbff53967754 -event-producer-consumer.zh.md: 026fa2d9503e157500035149e7fd7c90e2c77355 +event-producer-consumer.md: f088251b46c66588bd932141df448b7312ac5c52 +event-producer-consumer.zh.md: ef2b5a45465f1a6ed985cc04496573b300fec606 diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index e6a99e2378..f088251b46 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -10,7 +10,7 @@ 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), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule) | -| `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) | +| `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), `apiproxy`, [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) | | `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`) | `apiproxy`, [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) | diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md index 026fa2d950..ef2b5a4546 100644 --- a/docs/event-producer-consumer.zh.md +++ b/docs/event-producer-consumer.zh.md @@ -12,7 +12,7 @@ | `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), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule) | -| `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) | +| `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), `apiproxy`, [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) | | `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`) | `apiproxy`, [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) | diff --git a/docs/subsystems/session-reference.i18n.yaml b/docs/subsystems/session-reference.i18n.yaml index eadc4bdfef..535685927d 100644 --- a/docs/subsystems/session-reference.i18n.yaml +++ b/docs/subsystems/session-reference.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/session-reference.md -session-reference.md: b21fb854c42f3199000617be0d6b2fb18f200b3e -session-reference.zh.md: 4bec1e1f5a7324d6687c77ca8cf03fe448650669 +session-reference.md: 2380584e8bdc70ac1fee28849c8d09f69d55fd9d +session-reference.zh.md: 974723e95d6f7f68d45ddb8efd65db3fa9470fc4 diff --git a/docs/subsystems/session-reference.md b/docs/subsystems/session-reference.md index b21fb854c4..2380584e8b 100644 --- a/docs/subsystems/session-reference.md +++ b/docs/subsystems/session-reference.md @@ -4,7 +4,7 @@ English | [中文](session-reference.zh.md) 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. -Sources: [`packages/context/file-reference/src/index.ts`](../../packages/context/file-reference/src/index.ts) · [`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 @@ -50,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. @@ -103,11 +113,21 @@ Host capability for cancellable file-reference discovery. * @returns deterministic path-only candidates. */ abstract list( agent: Agent, query: string, signal: AbortSignal, ): Promise + +/** + * 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 ``` Types: [Agent](core.md) -Source: [`packages/context/file-reference/src/index.ts:32`](../../packages/context/file-reference/src/index.ts) +Source: [`packages/context/file-reference/src/index.ts:27`](../../packages/context/file-reference/src/index.ts) @@ -126,6 +146,17 @@ Exact-read consumer that prepares immutable cross-session message context. */ async listCandidates( agent: Agent, query: string = '', limit: number = this.config.candidateLimit, signal?: AbortSignal, ): Promise +/** + * 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 + /** * Snapshot all references before enqueue and return one aggregated durable context. * @param agent - target agent; references to it are rejected. @@ -139,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) diff --git a/docs/subsystems/session-reference.zh.md b/docs/subsystems/session-reference.zh.md index 4bec1e1f5a..974723e95d 100644 --- a/docs/subsystems/session-reference.zh.md +++ b/docs/subsystems/session-reference.zh.md @@ -4,7 +4,7 @@ 由 Host 支撑的文件发现,以及结构化的跨会话引用请求与准备后的消息上下文。[文件引用约定](../../packages/context/file-reference)负责仅含路径的补全记录与语法;[会话引用约定](../../packages/context/session-reference)定义规范 URI、当前表层投影、标签安全的 JSON 与字节保留、稳定错误和不可信的模型提示词。宿主适配器使用这些类型,而不会把各自 UI 的提及语法传入 agent(智能体)核心。 -来源:[`packages/context/file-reference/src/index.ts`](../../packages/context/file-reference/src/index.ts) · [`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) ## 文件候选项 @@ -50,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 +} +``` + ## 准备后的消息 准备过程保留可读的当前消息内容,并最多返回一个聚合上下文。 @@ -103,11 +113,21 @@ Host capability for cancellable file-reference discovery. * @returns deterministic path-only candidates. */ abstract list( agent: Agent, query: string, signal: AbortSignal, ): Promise + +/** + * 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 ``` Types: [Agent](core.md) -Source: [`packages/context/file-reference/src/index.ts:32`](../../packages/context/file-reference/src/index.ts) +Source: [`packages/context/file-reference/src/index.ts:27`](../../packages/context/file-reference/src/index.ts) @@ -126,6 +146,17 @@ Exact-read consumer that prepares immutable cross-session message context. */ async listCandidates( agent: Agent, query: string = '', limit: number = this.config.candidateLimit, signal?: AbortSignal, ): Promise +/** + * 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 + /** * Snapshot all references before enqueue and return one aggregated durable context. * @param agent - target agent; references to it are rejected. @@ -139,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) diff --git a/docs/subsystems/typert.i18n.yaml b/docs/subsystems/typert.i18n.yaml index 7e1662a731..f0f042eecf 100644 --- a/docs/subsystems/typert.i18n.yaml +++ b/docs/subsystems/typert.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/typert.md -typert.md: 5840a6e85f716f4a9adfdfc24395ffab03f5c480 -typert.zh.md: 0c274d13185408313ead71b7a870b119eba2879b +typert.md: 863ab9821bbf3681ae43df817cc04018e275390c +typert.zh.md: 4883b4be51a09d8e63001284e42bd7e60497bf1d diff --git a/docs/subsystems/typert.md b/docs/subsystems/typert.md index 5840a6e85f..863ab9821b 100644 --- a/docs/subsystems/typert.md +++ b/docs/subsystems/typert.md @@ -248,7 +248,7 @@ Root interface of the unified API. New client-request domain = one new file pair respond(message: ClientResponse): Promise ``` -Source: [`packages/host/apiproxy/src/api/index.ts:23`](../../packages/host/apiproxy/src/api/index.ts) +Source: [`packages/host/apiproxy/src/api/index.ts:22`](../../packages/host/apiproxy/src/api/index.ts) diff --git a/docs/subsystems/typert.zh.md b/docs/subsystems/typert.zh.md index 0c274d1318..4883b4be51 100644 --- a/docs/subsystems/typert.zh.md +++ b/docs/subsystems/typert.zh.md @@ -248,7 +248,7 @@ Root interface of the unified API. New client-request domain = one new file pair respond(message: ClientResponse): Promise ``` -Source: [`packages/host/apiproxy/src/api/index.ts:23`](../../packages/host/apiproxy/src/api/index.ts) +Source: [`packages/host/apiproxy/src/api/index.ts:22`](../../packages/host/apiproxy/src/api/index.ts) diff --git a/knip.json b/knip.json index 3017292382..977a42e3af 100644 --- a/knip.json +++ b/knip.json @@ -501,6 +501,16 @@ "tests/**/*.ts" ] }, + "packages/context/file-reference": { + "ignoreDependencies": [ + "zod" + ] + }, + "packages/context/session-reference": { + "ignoreDependencies": [ + "zod" + ] + }, "packages/session/session-checkpoint-policy": { "entry": [ "tests/**/*.spec.ts", diff --git a/packages/api/remotes/package.json b/packages/api/remotes/package.json index edc6d65704..c2059492b4 100644 --- a/packages/api/remotes/package.json +++ b/packages/api/remotes/package.json @@ -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:^" diff --git a/packages/api/remotes/src/client/index.ts b/packages/api/remotes/src/client/index.ts index 8afe0aee68..6a5164f160 100644 --- a/packages/api/remotes/src/client/index.ts +++ b/packages/api/remotes/src/client/index.ts @@ -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' @@ -39,7 +43,6 @@ export type { MuxFrame, PromptContentPart, QuestionResponsePayload, QueueAction, RpcError, RpcId, RpcReceipt, RpcRequest, RpcResponse, RpcResult, SessionId, SessionModels, SessionSearchItem, SessionSummary, SettingsNamespaceView, SettingsPathOpView, SkillEntry, StreamChunk, - FileReferenceItem, SessionReferenceItem, SubagentAddress, SubagentCatalog, JobView, ToolCallView, ToolEventView, ToolResultView, WorkspaceId, WorkspaceView, } from '@deepseek-ai/dsh-client-connection/client' @@ -87,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 { @@ -107,7 +114,8 @@ export async function apply(ctx: Context): Promise<() => Promise> { const disposers: Array<() => Promise> = [] try { for (const contribution of [ - commandsRemote, goalsRemote, dynamicRemote, pluginInventoryRemote, messageFeedbackRemote, + commandsRemote, goalsRemote, dynamicRemote, fileReferencesRemote, + pluginInventoryRemote, messageFeedbackRemote, sessionReferencesRemote, ]) { disposers.push(await ctx.remote.$mount(contribution)) } diff --git a/packages/api/remotes/tsconfig.client.json b/packages/api/remotes/tsconfig.client.json index a0161dd047..49c7276d42 100644 --- a/packages/api/remotes/tsconfig.client.json +++ b/packages/api/remotes/tsconfig.client.json @@ -24,6 +24,12 @@ "path": "../../credentials/credentials" }, + { + "path": "../../context/file-reference" + }, + { + "path": "../../context/session-reference" + }, { "path": "../../extensions/cordis-host-runner" }, diff --git a/packages/client/README.i18n.yaml b/packages/client/README.i18n.yaml index 28cccce524..f92d47b80d 100644 --- a/packages/client/README.i18n.yaml +++ b/packages/client/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/README.md -README.md: f22ae99113f7f313c60304ac4c5fc53c8e3172ab -README.zh.md: c4aa20c9f5f317bbe5dbb6f6731f6eb6d5d7ac7c +README.md: 8665f35590335358654e0e563bea7b57585e64ae +README.zh.md: 6222c3c2cb14c28af37daf82cdf39fd84f245f6d diff --git a/packages/client/README.md b/packages/client/README.md index f22ae99113..8665f35590 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -30,6 +30,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. | diff --git a/packages/client/README.zh.md b/packages/client/README.zh.md index c4aa20c9f5..6222c3c2cb 100644 --- a/packages/client/README.zh.md +++ b/packages/client/README.zh.md @@ -30,6 +30,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) | 在对话界面中提供模型选择。 | diff --git a/packages/client/connection/src/client/api.ts b/packages/client/connection/src/client/api.ts index b65eb61c79..1b7627b293 100644 --- a/packages/client/connection/src/client/api.ts +++ b/packages/client/connection/src/client/api.ts @@ -11,7 +11,6 @@ export type { DirectoryEntry, DirectoryListing, ResponseValue, WorkspaceApi, WorkspaceId, WorkspaceView, SkillsApi, SkillEntry, - ReferencesApi, FileReferenceItem, SessionReferenceItem, ModelCatalogFailure, ModelCatalogModel, ModelProviderGroup, ModelReasoning, ModelReasoningEffort, ModelSelection, QueueAction, QueuedInboxItem, SessionModels, GoalsApi, GoalRef, diff --git a/packages/client/connection/src/client/fixture.ts b/packages/client/connection/src/client/fixture.ts index 67389e2745..f10329f0f2 100644 --- a/packages/client/connection/src/client/fixture.ts +++ b/packages/client/connection/src/client/fixture.ts @@ -1824,6 +1824,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) @@ -2789,43 +2834,6 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld { }) }, }, - references: { - files: (request) => { - const missing = requireSession(request) - if (missing !== undefined) return missing - const query = request.payload.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(query)) - return ok(request, { items }) - }, - sessions: (request) => { - const missing = requireSession(request) - if (missing !== undefined) return missing - const query = request.payload.query.toLocaleLowerCase() - const items = sessions - .filter(item => item.sessionId !== request.payload.sessionId) - .filter(item => String(item.sessionId).toLocaleLowerCase().includes(query) - || item.cwd?.toLocaleLowerCase().includes(query) === 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(request, { items }) - }, - }, goals: { // Compatibility face only: old API Proxy payloads and acknowledgements // adapt to the canonical fixture Remote implementation above. @@ -3041,6 +3049,7 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld { args: { agentId: SessionId line?: string + query?: string ref?: { id: string; revision: number } request?: { objective?: string; maxGoalRounds?: number } } @@ -3049,6 +3058,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)) + 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 }, @@ -3143,8 +3154,6 @@ export class FixtureApiClient extends AbstractApiClient { case 'workspace.insertSessionBefore': return this.api.workspace.insertSessionBefore(request) case 'workspace.archiveSession': return this.api.workspace.archiveSession(request) case 'skill.list': return this.api.skills.list(request) - case 'reference.files': return this.api.references.files(request, signal) - case 'reference.sessions': return this.api.references.sessions(request, signal) case 'agentPreset.list': return this.api.agentPresets.list(request) case 'agentPreset.select': return this.api.agentPresets.select(request) case 'agentPreset.read': return this.api.agentPresets.read(request) diff --git a/packages/client/connection/src/client/index.ts b/packages/client/connection/src/client/index.ts index 559c1ecfac..c4470b13aa 100644 --- a/packages/client/connection/src/client/index.ts +++ b/packages/client/connection/src/client/index.ts @@ -19,7 +19,6 @@ export type { DirectoryEntry, DirectoryListing, ToolCallView, ToolResultView, WorkspaceApi, WorkspaceId, WorkspaceView, SkillsApi, SkillEntry, - ReferencesApi, FileReferenceItem, SessionReferenceItem, ModelCatalogFailure, ModelCatalogModel, ModelProviderGroup, ModelReasoning, MessageId, ModelReasoningEffort, ModelSelection, QueueAction, QueuedInboxItem, SessionModels, SubagentsApi, SubagentAddress, SubagentCatalog, SubagentListEntry, SubagentPromptReceipt, diff --git a/packages/client/connection/tests/fake-api.client.ts b/packages/client/connection/tests/fake-api.client.ts index 4bcf1d4ef4..bee4fc0ce0 100644 --- a/packages/client/connection/tests/fake-api.client.ts +++ b/packages/client/connection/tests/fake-api.client.ts @@ -195,11 +195,6 @@ export class FakeApiClient implements IApiClient { list: (payload: unknown) => this.record('skill.list', payload, this.onSkillList(payload)), } - readonly references: IApiClient['references'] = { - files: (payload: unknown) => this.record('reference.files', payload, Promise.resolve(ok({ items: [] }))), - sessions: (payload: unknown) => this.record('reference.sessions', payload, Promise.resolve(ok({ items: [] }))), - } - readonly goals: IApiClient['goals'] = { create: payload => this.record('goal.create', payload, Promise.resolve(ok({ ref: { id: 'fake-goal' as never, revision: 1 } }))), edit: payload => this.record('goal.edit', payload, Promise.resolve(ok({ ref: { id: 'fake-goal' as never, revision: 1 } }))), diff --git a/packages/client/runtime/tests/fake-api.client.ts b/packages/client/runtime/tests/fake-api.client.ts index c91d147827..33a0efbbfd 100644 --- a/packages/client/runtime/tests/fake-api.client.ts +++ b/packages/client/runtime/tests/fake-api.client.ts @@ -248,11 +248,6 @@ export class FakeApiClient implements IApiClient { list: (payload: unknown) => this.record('skill.list', payload, this.onSkillList(payload)), } - readonly references: IApiClient['references'] = { - files: (payload: unknown) => this.record('reference.files', payload, Promise.resolve(ok({ items: [] }))), - sessions: (payload: unknown) => this.record('reference.sessions', payload, Promise.resolve(ok({ items: [] }))), - } - readonly goals: IApiClient['goals'] = { create: payload => this.record('goal.create', payload, Promise.resolve(ok({ ref: { id: 'fake-goal' as never, revision: 1 } }))), edit: payload => this.record('goal.edit', payload, Promise.resolve(ok({ ref: { id: 'fake-goal' as never, revision: 1 } }))), diff --git a/packages/client/ui-reference/README.i18n.yaml b/packages/client/ui-reference/README.i18n.yaml index 6df31a89a3..a1c62f669c 100644 --- a/packages/client/ui-reference/README.i18n.yaml +++ b/packages/client/ui-reference/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-reference/README.md -README.md: 7a96265f5db483569e3a3e6629303cb4b67a6b19 -README.zh.md: d991760bbbaf2e48e8f65447f9146a3f393c9db1 +README.md: c9ad377d2757a9dfea97c0660ca56cfb6beb6d20 +README.zh.md: 7e027abfc6237d7260fe00ff82af9a7103281804 diff --git a/packages/client/ui-reference/README.md b/packages/client/ui-reference/README.md index 7a96265f5d..c9ad377d27 100644 --- a/packages/client/ui-reference/README.md +++ b/packages/client/ui-reference/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -Unified Web `@file` and `@session` source. The browser starts `reference.files` and `reference.sessions` Host RPCs together for an unquoted token, deterministically orders files before sessions with stable `Folder ·`, `File ·`, and `Session ·` labels, renders the rows under the non-selectable `文件与文件夹` and `Session 对话` headings, and degrades either failed candidate domain independently. An open `@"…` token searches files only. +Unified Web `@file` and `@session` source. The browser starts the `fileReferences/list` and `sessionReferenceResolver/candidates` Remote calls together for an unquoted token, deterministically orders files before sessions with locale-registered folder/file/session labels, renders the rows under non-selectable file and session section headings, and degrades either failed candidate domain independently. An open `@"…` token searches files only. File picks insert the natural text defined by the shared `@path` grammar. A file closes completion and adds a trailing space; a directory keeps the menu active at its trailing slash so the user can descend another level. Paths containing whitespace use `@"path with spaces"`, and a quote the user opened explicitly remains quoted. @@ -20,6 +20,6 @@ Candidate browsing has no model effect. A selected file or session changes only ## Known Limitations and Deferred Work -- **Candidate failure is intentionally quiet** — one unavailable or failed reference RPC yields no rows for that domain, while prompt submission still reports session-reference preparation failures through the ordinary send path. +- **Candidate failure is intentionally quiet** — one unavailable or failed Remote discovery call yields no rows for that domain, while prompt submission still reports session-reference preparation failures through the ordinary send path. - **No browser-side file scan** — Web completion requires a mounted Host `ctx.fileReferences` provider; the browser cannot fall back to its own filesystem. - **Session search remains metadata-only** — discovery filters session id, cwd, and the latest log-backed title through `ctx.sessionReferenceResolver`; message bodies and full transcripts are not searched. diff --git a/packages/client/ui-reference/README.zh.md b/packages/client/ui-reference/README.zh.md index d991760bbb..7e027abfc6 100644 --- a/packages/client/ui-reference/README.zh.md +++ b/packages/client/ui-reference/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -统一的 Web `@file` 与 `@session` source。对于未加引号的 token,浏览器会同时启动 `reference.files` 和 `reference.sessions` 宿主 RPC,以确定性顺序把文件排在会话之前,并使用稳定的 `Folder ·`、`File ·` 和 `Session ·` 标签;各行分别渲染在不可选择的 `文件与文件夹` 和 `Session 对话` 标题下,任一候选领域的失败都会独立降级。尚未闭合的 `@"…` token 只搜索文件。 +统一的 Web `@file` 与 `@session` source。对于未加引号的 token,浏览器会同时启动 `fileReferences/list` 和 `sessionReferenceResolver/candidates` Remote 调用,以确定性顺序把文件排在会话之前,并使用注册在 locale 字典中的文件夹、文件与会话标签;各行分别渲染在不可选择的文件与会话分组标题下,任一候选领域的失败都会独立降级。尚未闭合的 `@"…` token 只搜索文件。 选择文件会插入共享 `@path` 语法所定义的自然文本。文件会关闭补全并追加一个尾随空格;目录则让菜单在尾部斜杠处保持活跃,用户可以继续进入下一层。包含空白的路径使用 `@"path with spaces"`,用户显式打开的引号会继续保留。 @@ -20,6 +20,6 @@ ## 已知限制与暂缓事项 -- **候选失败有意保持静默**:引用 RPC 不可用或失败时,该领域不产生候选行;提示词提交仍会通过普通发送路径报告会话引用准备失败。 +- **候选失败有意保持静默**:Remote 发现调用不可用或失败时,该领域不产生候选行;提示词提交仍会通过普通发送路径报告会话引用准备失败。 - **浏览器侧不扫描文件**:Web 补全需要挂载宿主 `ctx.fileReferences` 提供方;浏览器无法回退到自身文件系统。 - **会话搜索仍仅使用元数据**:发现流程通过 `ctx.sessionReferenceResolver` 筛选 session id、cwd 和以日志为依据的最新标题;不搜索消息主体或完整 transcript(文本记录)。 diff --git a/packages/client/ui-reference/package.json b/packages/client/ui-reference/package.json index 09d3f8edb2..f5133b54c5 100644 --- a/packages/client/ui-reference/package.json +++ b/packages/client/ui-reference/package.json @@ -32,8 +32,9 @@ "dsh": { "client": { "inject": [ - "@deepseek-ai/dsh-api-remotes", "@deepseek-ai/dsh-client-runtime", + "@deepseek-ai/dsh-api-remotes", + "@deepseek-ai/dsh-client-locale", "@deepseek-ai/dsh-client-ui-input-trigger" ], "platform": "web" @@ -46,18 +47,26 @@ "license": "MIT", "peerDependencies": { "@deepseek-ai/dsh-api-remotes": "workspace:^", + "@deepseek-ai/dsh-client-locale": "workspace:^", "@deepseek-ai/dsh-client-runtime": "workspace:^", "@deepseek-ai/dsh-client-ui-input-trigger": "workspace:^", - "@deepseek-ai/dsh-invariants": "workspace:^", + "@deepseek-ai/dsh-client-ui-slots": "workspace:^", "@deepseek-ai/dsh-file-reference": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", + "@deepseek-ai/dsh-session-reference": "workspace:^", + "@deepseek-ai/dsh-typert-protocol": "workspace:^", "@deepseek-ai/cordis": "workspace:^" }, "devDependencies": { "@deepseek-ai/dsh-api-remotes": "workspace:^", + "@deepseek-ai/dsh-client-locale": "workspace:^", "@deepseek-ai/dsh-client-runtime": "workspace:^", "@deepseek-ai/dsh-client-ui-input-trigger": "workspace:^", - "@deepseek-ai/dsh-invariants": "workspace:^", + "@deepseek-ai/dsh-client-ui-slots": "workspace:^", "@deepseek-ai/dsh-file-reference": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", + "@deepseek-ai/dsh-session-reference": "workspace:^", + "@deepseek-ai/dsh-typert-protocol": "workspace:^", "@deepseek-ai/cordis": "workspace:^" }, "files": [ diff --git a/packages/client/ui-reference/src/client/index.ts b/packages/client/ui-reference/src/client/index.ts index 18b6bd8607..f838083f63 100644 --- a/packages/client/ui-reference/src/client/index.ts +++ b/packages/client/ui-reference/src/client/index.ts @@ -1,47 +1,71 @@ /** * Unified Web `@` reference source. File and session discovery run through - * cancellable Host RPCs in parallel with deterministic ordering and labels. + * the cancellable generated Remote namespaces in parallel with deterministic + * ordering and labels. * * @module @deepseek-ai/dsh-client-ui-reference/client */ -import type { ConnectionHandle, FileReferenceItem, SessionReferenceItem } from '@deepseek-ai/dsh-api-remotes/client' -import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client' +// Type-only: pulls the generated Remote API and ctx.remote merge through the Client assembly boundary. +import type {} from '@deepseek-ai/dsh-api-remotes/client' +// Type-only: pulls the locale plugin's Context merge (ctx.locale). +import type {} from '@deepseek-ai/dsh-client-locale/client' +import type { ClientContext, SessionId } from '@deepseek-ai/dsh-client-runtime/client' import type { ClientSessionContext, InputTriggerServiceContract, InputTriggerSource, } from '@deepseek-ai/dsh-client-ui-input-trigger/client' import { formatFileMention } from '@deepseek-ai/dsh-file-reference/grammar' +import type { FileReferenceCandidate } from '@deepseek-ai/dsh-file-reference/types' +import type { SessionReferenceMentionCandidate } from '@deepseek-ai/dsh-session-reference/types' +import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol' +import { en, NS, zh, type ReferenceKey } from './locales.ts' -const FILE_SECTION = '文件与文件夹' -const SESSION_SECTION = 'Session 对话' +/** + * The two Remote calls this source needs. The generated face wraps every + * business result in {@link RemoteResult}: a carrier failure arrives as the + * `ok: false` branch rather than a rejection, so discovery reads one envelope + * per domain and either domain can fail without hiding the other. + */ +interface ReferenceRemotes { + readonly fileReferences: { + list: (agentId: SessionId, query: string, signal?: AbortSignal) => Promise> + } + readonly sessionReferenceResolver: { + candidates: (agentId: SessionId, query: string, signal?: AbortSignal) => Promise> + } +} -/** Required services: the slash registry and Host connection. */ -export const inject = ['inputTriggers', 'connection'] +/** Required services: the trigger registry, the Remote namespaces, and the copy. */ +export const inject = [ + 'inputTriggers', 'locale', 'remote', 'remote.fileReferences', 'remote.sessionReferenceResolver', +] /** * Register the combined `@file` / `@session` source. * @param ctx - client root context. */ export function apply(ctx: ClientContext): void { - const references = (ctx.get('connection') as ConnectionHandle).api.references + ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-reference: dictionaries') + const t = ctx.locale.bind(NS) + const remote: ReferenceRemotes = ctx.remote const source: InputTriggerSource = { trigger: '@', name: 'reference', async candidates(session: ClientSessionContext, { query, quoted, signal }) { - const files = references.files({ sessionId: session.sessionId, query }, signal).then( - response => response.result.ok ? response.result.value.items : [], + const files = remote.fileReferences.list(session.sessionId, query, signal).then( + result => result.ok ? result.value : [], () => [], ) const sessions = quoted === true - ? Promise.resolve([] as SessionReferenceItem[]) - : references.sessions({ sessionId: session.sessionId, query }, signal).then( - response => response.result.ok ? response.result.value.items : [], + ? Promise.resolve([] as SessionReferenceMentionCandidate[]) + : remote.sessionReferenceResolver.candidates(session.sessionId, query, signal).then( + result => result.ok ? result.value : [], () => [], ) const [fileItems, sessionItems] = await Promise.all([files, sessions]) if (signal.aborted) return [] return [ - ...fileItems.flatMap(candidate => fileCandidate(candidate, quoted === true)), - ...sessionItems.map(sessionCandidate), + ...fileItems.flatMap(candidate => fileCandidate(candidate, quoted === true, t)), + ...sessionItems.map(candidate => sessionCandidate(candidate, t)), ] }, onPick({ candidate }) { @@ -73,11 +97,13 @@ export function apply(ctx: ClientContext): void { ctx.effect(() => inputTriggers.registerSource(source), 'ui-reference: @ source') } +type Translate = (key: ReferenceKey) => string + type ReferenceCandidateValue = - | { kind: 'file'; fileKind: FileReferenceItem['kind']; mention: string } + | { kind: 'file'; fileKind: FileReferenceCandidate['kind']; mention: string } | { kind: 'session'; label: string; mention: string } -function fileCandidate(candidate: FileReferenceItem, preserveQuote: boolean) { +function fileCandidate(candidate: FileReferenceCandidate, preserveQuote: boolean, t: Translate) { const mention = formatFileMention(candidate, preserveQuote) if (mention === undefined) return [] const name = candidate.path.slice(candidate.path.lastIndexOf('/') + 1) @@ -88,15 +114,15 @@ function fileCandidate(candidate: FileReferenceItem, preserveQuote: boolean) { mention, } return [{ - name: `${directory ? 'Folder' : 'File'} · ${name}${directory ? '/' : ''}`, + name: `${t(directory ? 'candidate.folder' : 'candidate.file')} · ${name}${directory ? '/' : ''}`, description: candidate.path, - section: FILE_SECTION, + section: t('section.files'), value: JSON.stringify(value), }] } -function sessionCandidate(candidate: SessionReferenceItem) { - const location = candidate.cwd ?? '(no cwd)' +function sessionCandidate(candidate: SessionReferenceMentionCandidate, t: Translate) { + const location = candidate.cwd ?? t('candidate.noCwd') const description = `${candidate.label === candidate.sessionId ? '' : `${candidate.sessionId} · `}${location} · ${new Date(candidate.createdAt).toISOString()}` const value: ReferenceCandidateValue = { kind: 'session', @@ -104,9 +130,9 @@ function sessionCandidate(candidate: SessionReferenceItem) { mention: candidate.mention, } return { - name: `Session · ${candidate.label}`, + name: `${t('candidate.session')} · ${candidate.label}`, description, - section: SESSION_SECTION, + section: t('section.sessions'), value: JSON.stringify(value), } } diff --git a/packages/client/ui-reference/src/client/locales.ts b/packages/client/ui-reference/src/client/locales.ts new file mode 100644 index 0000000000..5ccb5574f8 --- /dev/null +++ b/packages/client/ui-reference/src/client/locales.ts @@ -0,0 +1,34 @@ +/** `reference` namespace dictionaries for the unified `@` source. */ + +/** Dictionary namespace owned by this plugin. */ +export const NS = 'reference' + +/** Simplified Chinese dictionary (the key-set source of truth). */ +export const zh = { + 'section.files': '文件与文件夹', + 'section.sessions': 'Session 对话', + 'candidate.file': '文件', + 'candidate.folder': '文件夹', + 'candidate.session': 'Session', + 'candidate.noCwd': '(无工作目录)', +} satisfies Record + +/** The reference namespace key union. */ +export type ReferenceKey = keyof typeof zh + +declare module '@deepseek-ai/dsh-client-ui-slots' { + interface LocaleNamespaceMap { + /** The unified `@` reference menu's copy. */ + reference: ReferenceKey + } +} + +/** English dictionary, checked complete against the zh key set. */ +export const en = { + 'section.files': 'Files & folders', + 'section.sessions': 'Session conversations', + 'candidate.file': 'File', + 'candidate.folder': 'Folder', + 'candidate.session': 'Session', + 'candidate.noCwd': '(no cwd)', +} satisfies Record diff --git a/packages/client/ui-reference/tests/browser-plugin.client.spec.ts b/packages/client/ui-reference/tests/browser-plugin.client.spec.ts index 78c7946039..11357fcc23 100644 --- a/packages/client/ui-reference/tests/browser-plugin.client.spec.ts +++ b/packages/client/ui-reference/tests/browser-plugin.client.spec.ts @@ -1,28 +1,31 @@ /** - * Web reference source coverage: Host-backed file/session discovery, + * Web reference source coverage: Remote-backed file/session discovery, * deterministic ordering and labels, quoted-path suppression, pick projections, codec * round-trip, and registration lifecycle. */ -import { Context } from '@deepseek-ai/cordis' +import { Context, Service } from '@deepseek-ai/cordis' import { describe, expect, it, vi } from 'vitest' -import type { FileReferenceItem, SessionReferenceItem } from '@deepseek-ai/dsh-api-remotes/client' +import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client' import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client' import type { CandidateRequest, ClientSessionContext, InputTriggerCandidate, InputTriggerSource, } from '@deepseek-ai/dsh-client-ui-input-trigger/client' +import type { FileReferenceCandidate } from '@deepseek-ai/dsh-file-reference/types' +import type { SessionReferenceMentionCandidate } from '@deepseek-ai/dsh-session-reference/types' import { apply, inject } from '../src/client/index.ts' const sid = (value: string): SessionId => value as SessionId const session: ClientSessionContext = { sessionId: sid('target') } -type ReferenceResponse = - | { result: { ok: true; value: { items: T[] } } } - | { result: { ok: false; error: { code: string; message: string } } } +type RemoteEnvelope = + | { ok: true; value: T } + | { ok: false; error: { code: string; message: string; details: object } } -type ReferenceLookup = ( - payload: unknown, +type RemoteLookup = ( + agentId: SessionId, + query: string, signal?: AbortSignal, -) => Promise> +) => Promise> function request( query: string, @@ -37,30 +40,22 @@ function request( } async function bench( - files: ReferenceLookup = vi.fn(() => Promise.resolve({ - result: { - ok: true as const, - value: { - items: [ - { path: 'src', kind: 'directory' as const }, - { path: 'docs/a b.md', kind: 'file' as const }, - ], - }, - }, + files: RemoteLookup = vi.fn(() => Promise.resolve({ + ok: true as const, + value: [ + { path: 'src', kind: 'directory' as const }, + { path: 'docs/a b.md', kind: 'file' as const }, + ], })), - sessions: ReferenceLookup = vi.fn(() => Promise.resolve({ - result: { - ok: true as const, - value: { - items: [{ - sessionId: sid('source'), - label: 'Research', - cwd: '/project', - createdAt: 1_700_000_000_000, - mention: '@[Research](dsh-session:InNvdXJjZSI)', - }], - }, - }, + sessions: RemoteLookup = vi.fn(() => Promise.resolve({ + ok: true as const, + value: [{ + sessionId: sid('source'), + label: 'Research', + cwd: '/project', + createdAt: 1_700_000_000_000, + mention: '@[Research](dsh-session:InNvdXJjZSI)', + }], })), ): Promise<{ ctx: Context; fiber: ReturnType; source: InputTriggerSource }> { const ctx = new Context() @@ -71,7 +66,15 @@ async function bench( return () => { source = undefined } }, }) - ctx.provide('connection', { api: { references: { files, sessions } } } as never) + class RemoteService extends Service { + constructor(serviceCtx: Context) { + super(serviceCtx, 'remote') + } + } + new RemoteService(ctx) + ctx.provide('remote.fileReferences', { list: files }) + ctx.provide('remote.sessionReferenceResolver', { candidates: sessions }) + ctx.provide('locale', new LocaleRuntime(ctx)) const fiber = ctx.plugin({ inject: [...inject], apply }) await fiber.await() if (source === undefined) throw new Error('reference source was not registered') @@ -80,80 +83,74 @@ async function bench( describe('apply', () => { it('declares its services and releases the @ reference registration on disposal', async () => { - expect(inject).toEqual(['inputTriggers', 'connection']) - const ctx = new Context() + expect(inject).toEqual([ + 'inputTriggers', 'locale', 'remote', 'remote.fileReferences', 'remote.sessionReferenceResolver', + ]) + const { fiber } = await bench() let registered: InputTriggerSource | undefined + const ctx = new Context() ctx.provide('inputTriggers', { registerSource(source: InputTriggerSource) { registered = source return () => { registered = undefined } }, }) - ctx.provide('connection', { - api: { - references: { - files: () => Promise.resolve({ result: { ok: true, value: { items: [] } } }), - sessions: () => Promise.resolve({ result: { ok: true, value: { items: [] } } }), - }, - }, - } as never) - const fiber = ctx.plugin({ inject: [...inject], apply }) - await fiber.await() + class RemoteService extends Service { + constructor(serviceCtx: Context) { + super(serviceCtx, 'remote') + } + } + new RemoteService(ctx) + ctx.provide('remote.fileReferences', { list: () => Promise.resolve({ ok: true, value: [] }) }) + ctx.provide('remote.sessionReferenceResolver', { candidates: () => Promise.resolve({ ok: true, value: [] }) }) + ctx.provide('locale', new LocaleRuntime(ctx)) + const ownFiber = ctx.plugin({ inject: [...inject], apply }) + await ownFiber.await() expect(registered).toMatchObject({ trigger: '@', name: 'reference' }) - await fiber.dispose() + await ownFiber.dispose() expect(registered).toBeUndefined() + await fiber.dispose() }) }) describe('candidates', () => { - it('starts both Host lookups together and renders files before sessions with stable labels', async () => { + it('starts both Remote lookups together and renders files before sessions with stable labels', async () => { let releaseFiles!: () => void let releaseSessions!: () => void const files = vi.fn(() => new Promise<{ - result: { ok: true; value: { items: { path: string; kind: 'file' | 'directory' }[] } } + ok: true + value: { path: string; kind: 'file' | 'directory' }[] }>((resolve) => { releaseFiles = () => { resolve({ - result: { - ok: true, - value: { - items: [ - { path: 'src', kind: 'directory' }, - { path: 'docs/a b.md', kind: 'file' }, - ], - }, - }, + ok: true, + value: [ + { path: 'src', kind: 'directory' }, + { path: 'docs/a b.md', kind: 'file' }, + ], }) } })) const sessions = vi.fn(() => new Promise<{ - result: { - ok: true - value: { - items: { - sessionId: SessionId - label: string - cwd: string - createdAt: number - mention: string - }[] - } - } + ok: true + value: { + sessionId: SessionId + label: string + cwd: string + createdAt: number + mention: string + }[] }>((resolve) => { releaseSessions = () => { resolve({ - result: { - ok: true, - value: { - items: [{ - sessionId: sid('source'), - label: 'Research', - cwd: '/project', - createdAt: 1_700_000_000_000, - mention: '@[Research](dsh-session:InNvdXJjZSI)', - }], - }, - }, + ok: true, + value: [{ + sessionId: sid('source'), + label: 'Research', + cwd: '/project', + createdAt: 1_700_000_000_000, + mention: '@[Research](dsh-session:InNvdXJjZSI)', + }], }) } })) @@ -165,12 +162,12 @@ describe('candidates', () => { releaseFiles() await expect(pending).resolves.toEqual([ expect.objectContaining({ - name: 'Folder · src/', + name: '文件夹 · src/', description: 'src', section: '文件与文件夹', }), expect.objectContaining({ - name: 'File · a b.md', + name: '文件 · a b.md', description: 'docs/a b.md', section: '文件与文件夹', }), @@ -185,29 +182,23 @@ describe('candidates', () => { it('suppresses sessions for an open quoted path and degrades each failed domain independently', async () => { const files = vi.fn() .mockResolvedValueOnce({ - result: { - ok: true as const, - value: { items: [{ path: 'README.md', kind: 'file' as const }] }, - }, + ok: true as const, + value: [{ path: 'README.md', kind: 'file' as const }], }) .mockRejectedValueOnce(new Error('file scan failed')) const sessions = vi.fn(() => Promise.resolve({ - result: { - ok: true as const, - value: { - items: [{ - sessionId: sid('source'), - label: 'Research', - cwd: '/project', - createdAt: 0, - mention: '@[Research](dsh-session:InNvdXJjZSI)', - }], - }, - }, + ok: true as const, + value: [{ + sessionId: sid('source'), + label: 'Research', + cwd: '/project', + createdAt: 0, + mention: '@[Research](dsh-session:InNvdXJjZSI)', + }], })) const { source } = await bench(files, sessions) const quoted = await source.candidates(session, request('READ', { quoted: true })) - expect(quoted).toEqual([expect.objectContaining({ name: 'File · README.md' })]) + expect(quoted).toEqual([expect.objectContaining({ name: '文件 · README.md' })]) expect(source.onPick({ candidate: quoted[0]!, session, @@ -229,55 +220,43 @@ describe('candidates', () => { await expect(pending).resolves.toEqual([]) }) - it('treats Host errors as empty domains and filters paths that cannot be mentioned', async () => { + it('treats Remote failures as empty domains and filters paths that cannot be mentioned', async () => { const files = vi.fn(() => Promise.resolve({ - result: { - ok: true as const, - value: { items: [{ path: 'bad\nname', kind: 'file' as const }] }, - }, + ok: true as const, + value: [{ path: 'bad\nname', kind: 'file' as const }], })) const sessions = vi.fn() .mockRejectedValueOnce(new Error('session lookup failed')) .mockResolvedValueOnce({ - result: { - ok: false as const, - error: { code: 'reference-failed', message: 'session lookup failed' }, - }, + ok: false as const, + error: { code: 'internal', message: 'session lookup failed', details: {} }, }) const { source } = await bench(files, sessions) await expect(source.candidates(session, request('bad'))).resolves.toEqual([]) files.mockResolvedValueOnce({ - result: { - ok: false as const, - error: { code: 'reference-failed', message: 'file lookup failed' }, - }, + ok: false as const, + error: { code: 'internal', message: 'file lookup failed', details: {} }, } as never) await expect(source.candidates(session, request('bad'))).resolves.toEqual([]) }) it('omits redundant session ids and labels sessions without a cwd', async () => { - const files = vi.fn(() => Promise.resolve({ - result: { ok: true as const, value: { items: [] } }, - })) + const files = vi.fn(() => Promise.resolve({ ok: true as const, value: [] })) const sessions = vi.fn(() => Promise.resolve({ - result: { - ok: true as const, - value: { - items: [{ - sessionId: sid('same'), - label: 'same', - createdAt: 0, - mention: '@[same](dsh-session:InNhbWUi)', - }], - }, - }, + ok: true as const, + value: [{ + sessionId: sid('same'), + label: 'same', + createdAt: 0, + mention: '@[same](dsh-session:InNhbWUi)', + }], })) const { source } = await bench(files, sessions) await expect(source.candidates(session, request('same'))).resolves.toEqual([ expect.objectContaining({ name: 'Session · same', - description: '(no cwd) · 1970-01-01T00:00:00.000Z', + description: '(无工作目录) · 1970-01-01T00:00:00.000Z', }), ]) }) diff --git a/packages/client/ui-reference/tsconfig.json b/packages/client/ui-reference/tsconfig.json index 9e7c8c27e5..4fdd19dbc2 100644 --- a/packages/client/ui-reference/tsconfig.json +++ b/packages/client/ui-reference/tsconfig.json @@ -8,11 +8,26 @@ "src" ], "references": [ + { + "path": "../../../vendor/cordis" + }, { "path": "../../api/remotes/tsconfig.client.json" }, { - "path": "../../../vendor/cordis" + "path": "../../context/file-reference" + }, + { + "path": "../../context/session-reference" + }, + { + "path": "../../runtime-diagnostics/invariants" + }, + { + "path": "../../typert/protocol" + }, + { + "path": "../locale" }, { "path": "../runtime" @@ -21,10 +36,7 @@ "path": "../ui-input-trigger" }, { - "path": "../../context/file-reference" - }, - { - "path": "../../runtime-diagnostics/invariants" + "path": "../ui-slots" } ] } diff --git a/packages/context/README.i18n.yaml b/packages/context/README.i18n.yaml index 349b7f3784..fbfd8dfa06 100644 --- a/packages/context/README.i18n.yaml +++ b/packages/context/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/context/README.md -README.md: fa28751e548dc4aec8e2e2711508816729f5d407 -README.zh.md: 9d12c7a854c6cb008c3806ea246ec5b5667be941 +README.md: ac1f6c302ce557adb71974c4d80b3b61e27f049c +README.zh.md: 98cc4ab9b3a3f84e9f8dfff0a91025ee697ba240 diff --git a/packages/context/README.md b/packages/context/README.md index fa28751e54..ac1f6c302c 100644 --- a/packages/context/README.md +++ b/packages/context/README.md @@ -2,11 +2,13 @@ English | [中文](README.zh.md) -Product plugins that add model-visible request context without defining a tool. `agent-instructions` is included by the default `dsh-agent-spine-demo` bundle and can be disabled through bundle config; `time-context`, `tmux-context`, and `session-reference` are opt-in. +Product plugins that add model-visible request context without defining a tool. `agent-instructions` is included by the default `dsh-agent-spine-demo` bundle and can be disabled through bundle config; `time-context`, `tmux-context`, `session-reference`, `file-reference`, and `file-reference-local` are opt-in. | Package | Role | ctx key | |---|---|---| | [`session-reference/`](session-reference/README.md) | Bounded snapshots of other sessions | `ctx.sessionReferenceResolver` | +| [`file-reference/`](file-reference/README.md) | File-reference discovery seam and `@file` grammar | `ctx.fileReferences` | +| [`file-reference-local/`](file-reference-local/README.md) | Local-filesystem file-reference provider | — | | [`time-context/`](time-context/README.md) | Current-time and elapsed-time context | — | | [`tmux-context/`](tmux-context/README.md) | tmux location context | — | | [`agent-instructions/`](agent-instructions/README.md) | Workspace-instruction context | — | diff --git a/packages/context/README.zh.md b/packages/context/README.zh.md index 9d12c7a854..98cc4ab9b3 100644 --- a/packages/context/README.zh.md +++ b/packages/context/README.zh.md @@ -2,11 +2,13 @@ [English](README.md) | 中文 -在不定义工具的情况下添加模型可见的请求上下文的产品插件。`agent-instructions` 包含在默认 `dsh-agent-spine-demo` 组合包中,可通过组合包配置禁用;`time-context`、`tmux-context` 和 `session-reference` 需主动启用。 +在不定义工具的情况下添加模型可见的请求上下文的产品插件。`agent-instructions` 包含在默认 `dsh-agent-spine-demo` 组合包中,可通过组合包配置禁用;`time-context`、`tmux-context`、`session-reference`、`file-reference` 和 `file-reference-local` 需主动启用。 | 包 | 职责 | ctx key | |---|---|---| | [`session-reference/`](session-reference/README.md) | 其他会话的有界快照 | `ctx.sessionReferenceResolver` | +| [`file-reference/`](file-reference/README.md) | 文件引用发现 seam 与 `@file` 语法 | `ctx.fileReferences` | +| [`file-reference-local/`](file-reference-local/README.md) | 本地文件系统文件引用提供方 | — | | [`time-context/`](time-context/README.md) | 当前时间与耗时上下文 | — | | [`tmux-context/`](tmux-context/README.md) | tmux 位置上下文 | — | | [`agent-instructions/`](agent-instructions/README.md) | 工作区指令上下文 | — | diff --git a/packages/context/file-reference/README.i18n.yaml b/packages/context/file-reference/README.i18n.yaml index de92c41665..352a6747ad 100644 --- a/packages/context/file-reference/README.i18n.yaml +++ b/packages/context/file-reference/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/context/file-reference/README.md -README.md: c65c787c2143ba88f2ac9887065b537c23f67ca2 -README.zh.md: 4c0d955bd6804f17ee99a5a65138f39391adfc95 +README.md: ae0e1ef68f927890af78767660cac094d831380c +README.zh.md: 1645dd345cd52cf5d3e02270c1fbd4c4344ada37 diff --git a/packages/context/file-reference/README.md b/packages/context/file-reference/README.md index c65c787c21..ae0e1ef68f 100644 --- a/packages/context/file-reference/README.md +++ b/packages/context/file-reference/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -File-reference discovery seam and browser-safe `@file` grammar shared by host-backed user interfaces. `ctx.fileReferences.list(agent, query, signal)` returns path-only file or directory candidates for the addressed agent; concrete providers own namespace access, ranking, caching, and invalidation. +File-reference discovery seam and browser-safe `@file` grammar shared by host-backed user interfaces. `ctx.fileReferences.list(agent, query, signal)` returns path-only file or directory candidates for the addressed agent; concrete providers own namespace access, ranking, caching, and invalidation. The same contract is remotely callable as the unary `fileReferences/list` Remote method (`@Remote` on the Service Definition, cancelled through the reserved trailing signal), so browser consumers call `ctx.remote.fileReferences.list` without an API Proxy route. `activeAtToken()` recognizes an `@path` or open `@"path with spaces` token only at the start of input or after whitespace, so email-like text does not open completion. `formatFileMention()` emits the matching prompt spelling, appends `/` to directory candidates, preserves an explicitly opened quote, and rejects control characters or embedded quotes that the editor grammar cannot represent safely. diff --git a/packages/context/file-reference/README.zh.md b/packages/context/file-reference/README.zh.md index 4c0d955bd6..1645dd345c 100644 --- a/packages/context/file-reference/README.zh.md +++ b/packages/context/file-reference/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -文件引用发现 seam,以及供宿主驱动的用户界面共享、可在浏览器中安全使用的 `@file` 语法。`ctx.fileReferences.list(agent, query, signal)` 为指定 agent(智能体)返回仅含路径的文件或目录候选;具体提供方负责命名空间访问、排序、缓存和失效处理。 +文件引用发现 seam,以及供宿主驱动的用户界面共享、可在浏览器中安全使用的 `@file` 语法。`ctx.fileReferences.list(agent, query, signal)` 为指定 agent(智能体)返回仅含路径的文件或目录候选;具体提供方负责命名空间访问、排序、缓存和失效处理。同一契约以一元 `fileReferences/list` Remote 方法对外可调(`@Remote` 标注在 Service Definition 上,经保留的末位 signal 参数取消),浏览器消费方直接调用 `ctx.remote.fileReferences.list`,无需 API Proxy 路由。 `activeAtToken()` 只在输入开头或空白后识别 `@path` 或尚未闭合的 `@"path with spaces` token,因此类似电子邮件的文本不会打开补全。`formatFileMention()` 会生成与提示词匹配的写法,为目录候选追加 `/`,保留显式打开的引号,并拒绝编辑器语法无法安全表示的控制字符或内嵌引号。 diff --git a/packages/context/file-reference/package.json b/packages/context/file-reference/package.json index 0624bf331f..50ecf97ba1 100644 --- a/packages/context/file-reference/package.json +++ b/packages/context/file-reference/package.json @@ -26,6 +26,18 @@ "types": "./lib/types/invariant.d.ts", "default": "./lib/invariant.js" }, + "./types": { + "types": "./lib/types/types.d.ts", + "default": "./lib/types/types.js" + }, + "./typert": { + "types": "./lib/typert.host.d.ts", + "default": "./lib/typert.host.js" + }, + "./remote": { + "types": "./lib/typert.remote-client.d.ts", + "default": "./lib/typert.remote-client.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, @@ -33,17 +45,26 @@ "lib/index.js", "lib/invariant.js", "lib/types/**/*.js", - "lib/types/**/*.d.ts" + "lib/types/**/*.d.ts", + "lib/typert.host.js", + "lib/typert.host.d.ts", + "lib/typert.remote-client.js", + "lib/typert.remote-client.d.ts" ], "license": "MIT", "peerDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", + "@deepseek-ai/dsh-typert-protocol": "workspace:^", "@deepseek-ai/cordis": "workspace:^" }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", + "@deepseek-ai/dsh-typert-protocol": "workspace:^", "@deepseek-ai/cordis": "workspace:^" + }, + "dependencies": { + "zod": "^4.4.3" } } diff --git a/packages/context/file-reference/src/grammar.ts b/packages/context/file-reference/src/grammar.ts index 3e74248f32..86bb51a843 100644 --- a/packages/context/file-reference/src/grammar.ts +++ b/packages/context/file-reference/src/grammar.ts @@ -4,7 +4,7 @@ * @module @deepseek-ai/dsh-file-reference/grammar */ -import type { FileReferenceCandidate } from './index.ts' +import type { FileReferenceCandidate } from './types.ts' /** Active `@` token ending at the editor cursor. */ export interface ActiveAtToken { diff --git a/packages/context/file-reference/src/index.ts b/packages/context/file-reference/src/index.ts index f189416854..ca8c017236 100644 --- a/packages/context/file-reference/src/index.ts +++ b/packages/context/file-reference/src/index.ts @@ -4,24 +4,19 @@ * @module @deepseek-ai/dsh-file-reference */ -import { Service } from '@deepseek-ai/cordis' import type { Context } from '@deepseek-ai/cordis' import type { Agent } from '@deepseek-ai/dsh-agent' +import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol' + +import type { FileReferenceCandidate } from './types.ts' export { activeAtToken, formatFileMention } from './grammar.ts' export type { ActiveAtToken } from './grammar.ts' +export type { FileReferenceCandidate } from './types.ts' /** Model guidance for path-only references selected by a user interface. */ export const FILE_REFERENCE_PROMPT = 'Paths prefixed with @ are files explicitly referenced by the user. Use the read tool when their contents are needed; do not claim to have inspected a file before reading it.' -/** One path-only completion candidate inside the target session cwd. */ -export 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' -} - declare module '@deepseek-ai/cordis' { interface Context { fileReferences: FileReferenceService @@ -29,7 +24,7 @@ declare module '@deepseek-ai/cordis' { } /** Host capability for cancellable file-reference discovery. */ -export abstract class FileReferenceService extends Service { +export abstract class FileReferenceService extends TypertRemoteService { constructor(ctx: Context) { super(ctx, 'fileReferences') } @@ -46,6 +41,23 @@ export abstract class FileReferenceService extends Service { query: string, signal: AbortSignal, ): Promise + + /** + * 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 { + return this.list(agent, query, signal) + } } export default FileReferenceService diff --git a/packages/context/file-reference/src/types.ts b/packages/context/file-reference/src/types.ts new file mode 100644 index 0000000000..3637b2b2c4 --- /dev/null +++ b/packages/context/file-reference/src/types.ts @@ -0,0 +1,13 @@ +/** + * Public file-reference discovery records. This module contains types only so + * generated Remote clients can consume it without Host runtime code. + * @module @deepseek-ai/dsh-file-reference/types + */ + +/** One path-only completion candidate inside the target session cwd. */ +export 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' +} diff --git a/packages/context/file-reference/tests/service.spec.ts b/packages/context/file-reference/tests/service.spec.ts new file mode 100644 index 0000000000..1f41ad6f79 --- /dev/null +++ b/packages/context/file-reference/tests/service.spec.ts @@ -0,0 +1,21 @@ +/** The Remote face delegates to the provider's discovery contract unchanged. */ +import { Context } from '@deepseek-ai/cordis' +import { describe, expect, it, vi } from 'vitest' +import type { Agent } from '@deepseek-ai/dsh-agent' +import { FileReferenceService } from '../src/index.ts' +import type { FileReferenceCandidate } from '../src/types.ts' + +describe('FileReferenceService', () => { + it('serves the Remote face through the abstract discovery member', async () => { + const candidates: FileReferenceCandidate[] = [{ path: 'src', kind: 'directory' }] + const list = vi.fn((_agent: Agent, _query: string, _signal: AbortSignal) => Promise.resolve(candidates)) + class StubProvider extends FileReferenceService { + list = list + } + const provider = new StubProvider(new Context()) + const agent = { id: 'target' } as unknown as Agent + const signal = new AbortController().signal + await expect(provider.remoteExportList(agent, 'sr', signal)).resolves.toBe(candidates) + expect(list).toHaveBeenCalledWith(agent, 'sr', signal) + }) +}) diff --git a/packages/context/session-reference/README.i18n.yaml b/packages/context/session-reference/README.i18n.yaml index 2cffa62cc9..5a56be0add 100644 --- a/packages/context/session-reference/README.i18n.yaml +++ b/packages/context/session-reference/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/context/session-reference/README.md -README.md: cce2e19b949cba92dd58c31c9af0110d61fc66c1 -README.zh.md: 45deab310590909b239a92a63c049a5574c8b919 +README.md: 706838bfd475c320b589e091467b2974c9cc43ab +README.zh.md: 71e5fdca0755ea481b19aec79ad1e5ec9cf067fb diff --git a/packages/context/session-reference/README.md b/packages/context/session-reference/README.md index cce2e19b94..706838bfd4 100644 --- a/packages/context/session-reference/README.md +++ b/packages/context/session-reference/README.md @@ -6,7 +6,7 @@ English | [中文](README.zh.md) ## Public API -- `listCandidates(agent, query?, limit?)` lists sessions other than `agent.id`, filters case-insensitively by id, cwd, or the latest log-backed title, and ranks same-cwd, cwd-less, then other-cwd records while preserving `listSessions()` creation order within each group. Each selected candidate uses that title as the mention label and falls back to the session id when the title is absent or unreadable; message bodies are not searched. +- `listCandidates(agent, query?, limit?)` lists sessions other than `agent.id`, filters case-insensitively by id, cwd, or the latest log-backed title, and ranks same-cwd, cwd-less, then other-cwd records while preserving `listSessions()` creation order within each group. Each selected candidate uses that title as the mention label and falls back to the session id when the title is absent or unreadable; message bodies are not searched. The unary `sessionReferenceResolver/candidates` Remote method serves the same discovery under the configured candidate limit and attaches each candidate's canonical mention, so browser consumers call `ctx.remote.sessionReferenceResolver.candidates` without an API Proxy route. - `prepare(agent, content, references, signal?)` preserves first-mention order, deduplicates ids, rejects self-reference and more than the configured distinct-source limit, reads every source in parallel, and returns detached content plus zero or one aggregated, identified `UserMessage` context. Any invalid reference, failed read, cancellation, or budget failure rejects before the host calls `followup()` or `steer()`. - `encodeSessionReferenceUri()` and `decodeSessionReferenceUri()` implement `dsh-session:` so every JavaScript string id round-trips exactly. `formatSessionReferenceMention()` emits `@[label](uri)`, and `parseSessionReferenceText()` replaces Markdown mentions or bare canonical URIs with readable `@label` text while returning structured references. Explicit Markdown mentions reject every malformed URI; bare text is considered a reference only when a non-empty base64url-shaped payload follows the scheme, and a matching noncanonical candidate still fails. Empty or punctuation-only scheme mentions remain ordinary discussion text. diff --git a/packages/context/session-reference/README.zh.md b/packages/context/session-reference/README.zh.md index 45deab3105..71e5fdca07 100644 --- a/packages/context/session-reference/README.zh.md +++ b/packages/context/session-reference/README.zh.md @@ -6,7 +6,7 @@ ## 公开 API -- `listCandidates(agent, query?, limit?)` 会列出 `agent.id` 之外的会话,按 id、cwd 或以日志为依据的最新标题进行不区分大小写的筛选,再按同 cwd、无 cwd、其他 cwd 记录排序,同时保持每组内的 `listSessions()` 创建顺序。每个已选候选会话都使用该标题作为 mention label;标题不存在或无法读取时回退到会话 id。不搜索消息主体。 +- `listCandidates(agent, query?, limit?)` 会列出 `agent.id` 之外的会话,按 id、cwd 或以日志为依据的最新标题进行不区分大小写的筛选,再按同 cwd、无 cwd、其他 cwd 记录排序,同时保持每组内的 `listSessions()` 创建顺序。每个已选候选会话都使用该标题作为 mention label;标题不存在或无法读取时回退到会话 id。不搜索消息主体。一元 `sessionReferenceResolver/candidates` Remote 方法在配置的候选上限内提供同一发现能力,并为每个候选附上规范 mention,浏览器消费方直接调用 `ctx.remote.sessionReferenceResolver.candidates`,无需 API Proxy 路由。 - `prepare(agent, content, references, signal?)` 会保留首次 mention 顺序、对 id 去重,并拒绝自引用或超过已配置不同源上限的情况。它会并行读取所有源,返回与输入脱离的内容,外加零个或一个聚合且带标识的 `UserMessage` 上下文。任何无效引用、读取失败、取消或预算失败,都会使准备操作在宿主调用 `followup()` 或 `steer()` 之前失败。 - `encodeSessionReferenceUri()` 与 `decodeSessionReferenceUri()` 实现 `dsh-session:`,因此每个 JavaScript 字符串 id 都能精确往返。`formatSessionReferenceMention()` 发出 `@[label](uri)`,`parseSessionReferenceText()` 将 Markdown mention 或裸规范 URI 替换为可读的 `@label` 文本,并返回结构化引用。解析器会拒绝显式 Markdown mention 中任何格式错误的 URI;只当 scheme 后跟非空、符合 base64url 形状的 payload 时,裸文本才被视为引用,匹配但非规范的候选项仍会失败。空 scheme mention 或只含标点符号的 scheme mention 仍是普通讨论文本。 diff --git a/packages/context/session-reference/package.json b/packages/context/session-reference/package.json index 8d3e0ec487..e92a0204e8 100644 --- a/packages/context/session-reference/package.json +++ b/packages/context/session-reference/package.json @@ -22,17 +22,35 @@ "types": "./lib/types/invariant.d.ts", "default": "./lib/invariant.js" }, + "./types": { + "types": "./lib/types/types.d.ts", + "default": "./lib/types/types.js" + }, + "./typert": { + "types": "./lib/typert.host.d.ts", + "default": "./lib/typert.host.js" + }, + "./remote": { + "types": "./lib/typert.remote-client.d.ts", + "default": "./lib/typert.remote-client.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", "lib/invariant.js", - "lib/types/**/*.d.ts" + "lib/types/**/*.js", + "lib/types/**/*.d.ts", + "lib/typert.host.js", + "lib/typert.host.d.ts", + "lib/typert.remote-client.js", + "lib/typert.remote-client.d.ts" ], "license": "MIT", "dependencies": { - "@deepseek-ai/schemastery": "workspace:^" + "@deepseek-ai/schemastery": "workspace:^", + "zod": "^4.4.3" }, "peerDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", @@ -42,6 +60,7 @@ "@deepseek-ai/dsh-output-retention": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-session-query": "workspace:^", + "@deepseek-ai/dsh-typert-protocol": "workspace:^", "@deepseek-ai/cordis": "workspace:^" }, "devDependencies": { @@ -52,6 +71,7 @@ "@deepseek-ai/dsh-output-retention": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-session-query": "workspace:^", + "@deepseek-ai/dsh-typert-protocol": "workspace:^", "@deepseek-ai/cordis": "workspace:^" } } diff --git a/packages/context/session-reference/src/index.ts b/packages/context/session-reference/src/index.ts index 43bed833ef..b77ba8ce16 100644 --- a/packages/context/session-reference/src/index.ts +++ b/packages/context/session-reference/src/index.ts @@ -5,9 +5,10 @@ * @module @deepseek-ai/dsh-session-reference */ -import { Context, Service } from '@deepseek-ai/cordis' +import { Context } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' import type { Agent } from '@deepseek-ai/dsh-agent' +import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol' import { createUserMessage } from '@deepseek-ai/dsh-llm' import type { ContentBlock, UserMessage } from '@deepseek-ai/dsh-llm' import type { SessionId } from '@deepseek-ai/dsh-session' @@ -21,7 +22,11 @@ import { } from './config.ts' import { retainReferencedSession, type ReferenceRetentionStats, type ReferencedSessionData } from './projection.ts' import { stringifyTagSafeJson } from './serialization.ts' -import type { PreparedReferencedMessage, SessionReferenceCandidate, SessionReferenceInput, SessionReferenceSource } from './types.ts' +import type { + PreparedReferencedMessage, SessionReferenceCandidate, SessionReferenceInput, + SessionReferenceMentionCandidate, SessionReferenceSource, +} from './types.ts' +import { formatSessionReferenceMention } from './uri.ts' export type * from './types.ts' export type { Config, SessionReferenceErrorCode } from './config.ts' @@ -67,7 +72,7 @@ interface RenderedSource { } /** Exact-read consumer that prepares immutable cross-session message context. */ -export class SessionReferenceResolver extends Service { +export class SessionReferenceResolver extends TypertRemoteService { static inject = ['sessionQuery'] static Config: z = z.object({ maxReferences: z.number().step(1).min(1).max(MAX_REFERENCES).default(MAX_REFERENCES), @@ -158,6 +163,28 @@ export class SessionReferenceResolver extends Service { })) } + /** + * 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 { + const candidates = await this.listCandidates(agent, query, this.config.candidateLimit, signal) + return candidates.map(candidate => ({ + ...candidate, + mention: formatSessionReferenceMention({ sessionId: candidate.sessionId, label: candidate.label }), + })) + } + /** * Snapshot all references before enqueue and return one aggregated durable context. * @param agent - target agent; references to it are rejected. diff --git a/packages/context/session-reference/src/types.ts b/packages/context/session-reference/src/types.ts index e1eff84244..6d908f34b9 100644 --- a/packages/context/session-reference/src/types.ts +++ b/packages/context/session-reference/src/types.ts @@ -1,7 +1,13 @@ -/** Public session-reference request, candidate, and preparation records. */ +/** + * Public session-reference request, candidate, and preparation records. + * Imports stay on type-only subpaths so generated Remote clients can consume + * this module without Host runtime code. + * @module @deepseek-ai/dsh-session-reference/types + */ -import type { ContentBlock } from '@deepseek-ai/dsh-llm' -import type { SessionId, UserMessage } from '@deepseek-ai/dsh-session' +import type { UserMessage } from '@deepseek-ai/dsh-llm/message' +import type { ContentBlock } from '@deepseek-ai/dsh-llm/types' +import type { SessionId } from '@deepseek-ai/dsh-session/types' /** Durable source session, cited event seqs, and snapshot facts for prepared cross-session context. */ export interface SessionReferenceSource { @@ -49,6 +55,12 @@ export interface SessionReferenceCandidate { createdAt: number } +/** One discovery candidate carrying its canonical prompt mention. */ +export interface SessionReferenceMentionCandidate extends SessionReferenceCandidate { + /** Canonical `@[label](dsh-session:…)` mention serialized into the prompt draft. */ + mention: string +} + /** Direct message content and optional referenced-session context. */ export interface PreparedReferencedMessage { /** Readable message content after host mention tokens are removed. */ diff --git a/packages/context/session-reference/tests/session-reference.spec.ts b/packages/context/session-reference/tests/session-reference.spec.ts index a103554f23..d08b7da413 100644 --- a/packages/context/session-reference/tests/session-reference.spec.ts +++ b/packages/context/session-reference/tests/session-reference.spec.ts @@ -283,6 +283,24 @@ describe('session reference discovery and preparation', () => { listSessions.mockRestore() }) + it('serves the Remote face with the configured limit and canonical mentions', async () => { + const ctx = await harness() + const target = ctx.sessions.create(SessionId('target'), { meta: { cwd: '/same', createdAt: 10 } }) + ctx.sessions.create(SessionId('source]'), { meta: { cwd: '/same', createdAt: 20 } }) + const candidates = await ctx.sessionReferenceResolver.remoteExportCandidates( + fakeAgent(target), + '', + new AbortController().signal, + ) + expect(candidates).toEqual([{ + sessionId: SessionId('source]'), + label: 'source]', + cwd: '/same', + createdAt: 20, + mention: formatSessionReferenceMention({ sessionId: SessionId('source]'), label: 'source]' }), + }]) + }) + it('keeps metadata matches when one title observation fails and cancels a stalled title batch', async () => { const ctx = await harness() const target = ctx.sessions.create(SessionId('target')) diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index 6200e431f4..3f51251b5f 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -573,6 +573,12 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ parameters: [{ name: 'agent', description: 'target agent whose session cwd bounds discovery.' }, { name: 'query', description: 'path text following `@` or `@"`.' }, { name: 'signal', description: 'caller cancellation.' }], returns: 'deterministic path-only candidates.', }, + { + signature: '@Remote(\'list\') remoteExportList( agent: Agent, query: string, signal: AbortSignal, ): Promise', + description: 'Remote face of list; the decorator cannot mark the abstract member, so this concrete adapter carries the identical contract.', + parameters: [{ name: 'agent', description: 'target agent whose session cwd bounds discovery.' }, { name: 'query', description: 'path text following `@` or `@"`.' }, { name: 'signal', description: 'caller cancellation.' }], + returns: 'deterministic path-only candidates.', + }, ], }, { @@ -1269,6 +1275,12 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ parameters: [{ name: 'agent', description: 'target agent; self is excluded and its cwd drives ranking.' }, { name: 'query', description: 'optional case-insensitive session-id/cwd/title substring.' }, { name: 'limit', description: 'optional positive result cap.' }, { name: 'signal', description: 'optional cancellation boundary for host autocomplete teardown.' }], returns: 'candidates labeled by latest title or, when absent, session id.', }, + { + signature: '@Remote(\'candidates\') async remoteExportCandidates( agent: Agent, query: string, signal: AbortSignal, ): Promise', + description: 'Remote face of listCandidates: the configured candidate limit applies, and every candidate carries the canonical mention a host inserts into the prompt draft.', + parameters: [{ name: 'agent', description: 'target agent; self is excluded and its cwd drives ranking.' }, { name: 'query', description: 'optional case-insensitive session-id/cwd/title substring.' }, { name: 'signal', description: 'caller cancellation.' }], + returns: 'mention-carrying candidates in rank order.', + }, { signature: 'async prepare( agent: Agent, content: ContentBlock[], references: SessionReferenceInput[], signal?: AbortSignal, ): Promise', description: 'Snapshot all references before enqueue and return one aggregated durable context.', @@ -3878,6 +3890,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'SessionReferenceInput', declaration: 'export interface SessionReferenceInput {\n sessionId: SessionId;\n label?: string;\n}', }, + { + name: 'SessionReferenceMentionCandidate', + declaration: 'export interface SessionReferenceMentionCandidate extends SessionReferenceCandidate {\n mention: string;\n}', + }, { name: 'SessionResultFilter', declaration: 'export type SessionResultFilter = {\n kind: \'id\';\n values: readonly SessionId[];\n} | {\n kind: \'cwd\';\n values: readonly (string | null)[];\n} | ({\n kind: \'created-at\';\n} & SessionResultRange) | {\n kind: \'parent\';\n values: readonly (SessionId | null)[];\n} | {\n kind: \'availability\';\n values: readonly SessionAvailability[];\n};', diff --git a/packages/host/apiproxy/README.i18n.yaml b/packages/host/apiproxy/README.i18n.yaml index 1629b11286..22fb19c837 100644 --- a/packages/host/apiproxy/README.i18n.yaml +++ b/packages/host/apiproxy/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/host/apiproxy/README.md -README.md: d03714140589d5a10d2fcb308f1b1ad88d29d85e -README.zh.md: c2cd11abbb22540e14e3c7aa28acc2fc91c33466 +README.md: 69f10f1ed22d95e7c4601754957218df374a66cc +README.zh.md: 40179d51f8024105beafdc7c49e6eb8e70c7a224 diff --git a/packages/host/apiproxy/README.md b/packages/host/apiproxy/README.md index d037141405..69f10f1ed2 100644 --- a/packages/host/apiproxy/README.md +++ b/packages/host/apiproxy/README.md @@ -38,8 +38,6 @@ Session model selection is a session-domain contract. `session.models` returns t `session.prompt` and `subagent.prompt` accept optional request-local `clientTimeZone` provenance. When present, the Host validates and canonicalizes `UTC` or an IANA Area/Location before Agent entry, rejects invalid input with `invalid-time-zone`, and records the canonical value on that exact `user-rpc` message beside its `rpcId`. The value is not Session, connection, create, resume, or fork state; non-browser callers may omit it. -The `reference.*` domain addresses an ordinary session through the same ownership-fenced Agent lookup as other session controls. `reference.files` delegates cancellable path discovery to `ctx.fileReferences`; `reference.sessions` delegates metadata-only candidate ranking and Host-side canonical mention creation to `ctx.sessionReferenceResolver`. A missing capability returns `reference-unavailable`, while cancellation and provider failures remain distinguishable instead of becoming an authoritative empty list. - `session.prompt` parses canonical session mentions from normalized text blocks and asks `ctx.sessionReferenceResolver` to prepare every snapshot before enqueue. Parsing, cancellation, validation, reads, and budget enforcement are one admission transaction: failure enqueues no message, while success registers the separately sourced context against the readable prompt's exact message id for insertion immediately before it at pre-step entry. Ordinary discard removes the registration, and moving a queued prompt to steering preserves the association. Pending queued input is a live control-plane contract, not conversation history. The gateway derives the complete `next-turn` queue from durable `agent/inbox/spliced` mutations and broadcasts authoritative `session/queue` snapshots after each change and on reconnect; pending `next-step` steering stays outside this Web projection. Within `next-step`, user-origin messages carry the `steering` placement while injected context (approval notices, task completion, attached snapshots) carries `context` and is not surfaced until claimed. The message-local `agent/inbox/inserted`, `claimed`, and `discarded` notifications remain available to lifecycle observers but do not build the queue view. `session.updateQueue` addresses one `MessageId`; edit and remove mutate the attached Agent through `Inbox.splice()`. A claim's pure deletion splice wins races before pre-step admission, so a later operation returns `queue-item-not-found`. `session.cancel` aborts only the active turn and preserves pending inbox work; after cancellation reaches quiescence and the closing turn flushes, AgentLoop claims the next waking message in FIFO order, and the browser never resends or promotes it. Queue operations never resume a cold session, and the client never infers retirement from turn or status events. diff --git a/packages/host/apiproxy/README.zh.md b/packages/host/apiproxy/README.zh.md index c2cd11abbb..40179d51f8 100644 --- a/packages/host/apiproxy/README.zh.md +++ b/packages/host/apiproxy/README.zh.md @@ -38,8 +38,6 @@ Settings 分节中的 `reasoningEffort` 在 agent-default-model 插件配置中 `session.prompt` 和 `subagent.prompt` 接受可选的请求本地 `clientTimeZone` 来源信息。若提供该值,Host 会在进入 Agent 前校验 `UTC` 或 IANA Area/Location 并将其规范化;无效输入以 `invalid-time-zone` 拒绝,规范值则与 `rpcId` 一起记录在这条确切的 `user-rpc` 消息上。该值不属于 Session、连接、create、resume 或 fork 状态;非浏览器调用方可以省略它。 -`reference.*` 领域通过与其他会话控制相同、带所有权栅栏的 Agent 查找来寻址普通会话。`reference.files` 把可取消的路径发现委托给 `ctx.fileReferences`;`reference.sessions` 把仅基于元数据的候选排序与 Host 侧规范提及标记创建委托给 `ctx.sessionReferenceResolver`。缺少能力时返回 `reference-unavailable`;取消与提供方失败保持可区分,不会被折叠成看似权威的空列表。 - `session.prompt` 从规范化文本块中解析规范会话提及标记,并要求 `ctx.sessionReferenceResolver` 在入队前准备每个快照。解析、取消、校验、读取和预算约束共同构成一个准入事务:失败时不会有消息入队;成功时,会以可读提示词的确切消息 id 注册独立来源上下文,等待 pre-step 进入时紧邻插入到它之前。普通丢弃会移除该注册,把 queued 提示词移动到 steering(中途引导)时仍会保留两者的关联。 待处理的 queued 输入属于实时控制平面约定,而非对话历史。网关根据持久 `agent/inbox/spliced` 变更派生完整的 `next-turn` 队列,并在每次变更后及重连时广播权威 `session/queue` 快照;待处理的 `next-step` steering(中途引导)不进入此 Web 投影。在 `next-step` 内,用户来源的消息携带 `steering` placement,而注入上下文(审批通知、任务完成、附加快照)携带 `context`,领取前不对外呈现。面向单条消息的 `agent/inbox/inserted`、`claimed` 与 `discarded` 通知仍供生命周期观察方使用,但不用于构建队列视图。`session.updateQueue` 通过 `MessageId` 寻址单个项;编辑和移除经已挂载 Agent 的 `Inbox.splice()` 修改队列。认领操作的纯删除 splice 会在 pre-step 准入前赢得竞态,因此之后的操作返回 `queue-item-not-found`。`session.cancel` 仅中止活动轮次并保留待处理 inbox 工作;取消达到完全停稳且结束中的轮次完成 flush 后,AgentLoop 按 FIFO 顺序认领下一条可唤醒消息,浏览器绝不重发或提升它。队列操作绝不恢复冷会话,客户端也绝不根据轮次或状态事件推断某项已退出队列。 diff --git a/packages/host/apiproxy/package.json b/packages/host/apiproxy/package.json index 43fd5fae1a..e3850b596a 100644 --- a/packages/host/apiproxy/package.json +++ b/packages/host/apiproxy/package.json @@ -51,7 +51,6 @@ "@deepseek-ai/dsh-api-remotes": "workspace:^", "@deepseek-ai/dsh-brand": "workspace:^", "@deepseek-ai/dsh-commands": "workspace:^", - "@deepseek-ai/dsh-file-reference": "workspace:^", "@deepseek-ai/dsh-credentials": "workspace:^", "@deepseek-ai/dsh-goal": "workspace:^", "@deepseek-ai/dsh-host-directory-picker": "workspace:^", @@ -90,7 +89,6 @@ "@deepseek-ai/dsh-storage": "workspace:^", "@deepseek-ai/dsh-storage-domain": "workspace:^", "@deepseek-ai/dsh-typert-protocol": "workspace:^", - "@deepseek-ai/dsh-typert-registry": "workspace:^", - "@deepseek-ai/cordis": "workspace:^" + "@deepseek-ai/dsh-typert-registry": "workspace:^" } } diff --git a/packages/host/apiproxy/src/api-proxy.ts b/packages/host/apiproxy/src/api-proxy.ts index a4265a9c74..037aa5d06c 100644 --- a/packages/host/apiproxy/src/api-proxy.ts +++ b/packages/host/apiproxy/src/api-proxy.ts @@ -10,7 +10,6 @@ import type { Context } from '@deepseek-ai/cordis' import { installModelSelection } from '@deepseek-ai/dsh-agent' import type { Agent, ModelSelection, ModelSelectionRef, AgentOptions, AgentStatus, PreStepDecision } from '@deepseek-ai/dsh-agent' import type {} from '@deepseek-ai/dsh-agent-presets/types' -import type {} from '@deepseek-ai/dsh-file-reference' import { AttachmentError } from '@deepseek-ai/dsh-attachment' import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment' import { contentHasImage, createUserMessage, freezeMessage, ReasoningEffortId } from '@deepseek-ai/dsh-llm' @@ -20,7 +19,6 @@ import { isAppendSurfaceEvent, isJsonValue } from '@deepseek-ai/dsh-session' import type { JsonValue, Session, SessionEvent, SessionEventMap, SessionHeader, SessionId, UserMessage } from '@deepseek-ai/dsh-session' import type { SessionPersistence } from '@deepseek-ai/dsh-session-persistence' import { - formatSessionReferenceMention, parseSessionReferenceText, type SessionReferenceInput, } from '@deepseek-ai/dsh-session-reference' @@ -1121,6 +1119,7 @@ function deliverPrompt( let cleanedUp = false let detachPreStep = (): void => {} let detachDiscard = (): void => {} + let detachDisposed = (): void => {} const cleanup = (): void => { /* v8 ignore next -- all settlement paths share this idempotent release. */ if (cleanedUp) return @@ -1128,8 +1127,14 @@ function deliverPrompt( ownership.cleanups.delete(message.id) detachPreStep() detachDiscard() + detachDisposed() } ownership.cleanups.set(message.id, cleanup) + // An agent retired with the prepared prompt still pending must not leave + // these listeners on the Host root context for the process lifetime. + detachDisposed = ctx.on('agent/disposed', ({ agent: subject }) => { + if (subject === agent) cleanup() + }) detachPreStep = ctx.on('agent/pre-step', async ({ agent: subject, messages }, next): Promise => { if (subject !== agent || !messages.some(candidate => candidate.id === message.id)) return next() cleanup() @@ -3352,85 +3357,6 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro }, }, - references: { - async files(request, signal) { - const { sessionId, query } = request.payload - const found = await agentFor(sessionId) - if ('error' in found) return err(request, found.error) - const fileReferences = ctx.get('fileReferences') - if (fileReferences === undefined) { - return err(request, { - code: 'reference-unavailable', - message: 'file reference capability unavailable', - details: { kind: 'file' }, - }) - } - const effectiveSignal = signal ?? new AbortController().signal - try { - return ok(request, { - items: await fileReferences.list(found.agent, query, effectiveSignal), - }) - } catch (error: unknown) { - if (effectiveSignal.aborted) { - return err(request, { - code: 'cancelled', - message: 'file reference listing was aborted', - details: {}, - }) - } - return err(request, { - code: 'reference-failed', - message: 'file reference listing failed', - details: { reason: String(error) }, - }) - } - }, - - async sessions(request, signal) { - const { sessionId, query } = request.payload - const found = await agentFor(sessionId) - if ('error' in found) return err(request, found.error) - const sessionReferences = ctx.get('sessionReferenceResolver') - if (sessionReferences === undefined) { - return err(request, { - code: 'reference-unavailable', - message: 'session reference capability unavailable', - details: { kind: 'session' }, - }) - } - try { - const candidates = await sessionReferences.listCandidates( - found.agent, - query, - undefined, - signal, - ) - return ok(request, { - items: candidates.map(candidate => ({ - ...candidate, - mention: formatSessionReferenceMention({ - sessionId: candidate.sessionId, - label: candidate.label, - }), - })), - }) - } catch (error: unknown) { - if (signal?.aborted === true) { - return err(request, { - code: 'cancelled', - message: 'session reference listing was aborted', - details: {}, - }) - } - return err(request, { - code: 'reference-failed', - message: 'session reference listing failed', - details: { reason: String(error) }, - }) - } - }, - }, - settings: { describe(request) { const settings = ctx.get('settings') diff --git a/packages/host/apiproxy/src/api/index.ts b/packages/host/apiproxy/src/api/index.ts index 875e597e7c..b5e1d1ffd9 100644 --- a/packages/host/apiproxy/src/api/index.ts +++ b/packages/host/apiproxy/src/api/index.ts @@ -9,7 +9,6 @@ import type { HostApi } from './host.ts' import type { WorkspaceApi } from './workspace.ts' import type { AgentPresetsApi } from './agent-presets.ts' import type { SkillsApi } from './skills.ts' -import type { ReferencesApi } from './references.ts' import type { SubagentsApi } from './subagents.ts' import type { EventsApi } from './events.ts' import type { GoalsApi } from './goals.ts' @@ -26,7 +25,6 @@ export interface ApiProxy { host: HostApi workspace: WorkspaceApi skills: SkillsApi - references: ReferencesApi agentPresets: AgentPresetsApi events: EventsApi goals: GoalsApi @@ -57,7 +55,6 @@ export type { export type { JobView } from './jobs.ts' export type { WorkspaceApi, WorkspaceId, WorkspaceView } from './workspace.ts' export type { SkillsApi, SkillEntry } from './skills.ts' -export type { FileReferenceItem, ReferencesApi, SessionReferenceItem } from './references.ts' export type { AgentPresetsApi, AgentPresetEntry } from './agent-presets.ts' export type { EventsApi, MuxFrame, HostFrame, QueuedInboxItem, ToolCallView, ToolEventView, ToolResultView } from './events.ts' export type { GoalsApi, GoalId, GoalRef } from './goals.ts' diff --git a/packages/host/apiproxy/src/api/references.schema.ts b/packages/host/apiproxy/src/api/references.schema.ts deleted file mode 100644 index 59bee90e09..0000000000 --- a/packages/host/apiproxy/src/api/references.schema.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Reference-domain wire schemas. - * - * @module @deepseek-ai/dsh-host-apiproxy/api/references.schema - */ - -import { z } from 'zod' -import type { RequestPayload, ResponseValue } from './rpc-map.ts' -import type { Wire } from './rpc.schema.ts' -import { sessionIdSchema } from './sessions.schema.ts' - -const referenceRequestSchema = z.object({ - sessionId: sessionIdSchema, - query: z.string(), -}) - -/** reference.files request payload. */ -export const referenceFilesRequestSchema = referenceRequestSchema satisfies - z.ZodType>> - -/** reference.files response value. */ -export const referenceFilesValueSchema = z.object({ - items: z.array(z.object({ - path: z.string(), - kind: z.union([z.literal('file'), z.literal('directory')]), - })), -}) satisfies z.ZodType>> - -/** reference.sessions request payload. */ -export const referenceSessionsRequestSchema = referenceRequestSchema satisfies - z.ZodType>> - -/** reference.sessions response value. */ -export const referenceSessionsValueSchema = z.object({ - items: z.array(z.object({ - sessionId: sessionIdSchema, - label: z.string(), - cwd: z.string().optional(), - createdAt: z.number(), - mention: z.string(), - })), -}) satisfies z.ZodType>> diff --git a/packages/host/apiproxy/src/api/references.ts b/packages/host/apiproxy/src/api/references.ts deleted file mode 100644 index 13471f8d46..0000000000 --- a/packages/host/apiproxy/src/api/references.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Reference autocomplete domain contract. - * - * @module @deepseek-ai/dsh-host-apiproxy/api/references - */ - -import type { SessionId } from '@deepseek-ai/dsh-session/types' -import type { RpcRequest, RpcResponse } from './rpc.ts' - -/** One file or directory candidate inside the target session workspace. */ -export interface FileReferenceItem { - path: string - kind: 'file' | 'directory' -} - -/** One metadata-only cross-session reference candidate. */ -export interface SessionReferenceItem { - sessionId: SessionId - label: string - cwd?: string - createdAt: number - /** Canonical opaque mention serialized into the prompt draft. */ - mention: string -} - -/** Host-backed file and session reference discovery. */ -export interface ReferencesApi { - /** List path candidates using the target agent's workspace boundary. */ - files( - request: RpcRequest<{ sessionId: SessionId; query: string }>, - signal?: AbortSignal, - ): Promise> - - /** List cross-session candidates excluding the target session itself. */ - sessions( - request: RpcRequest<{ sessionId: SessionId; query: string }>, - signal?: AbortSignal, - ): Promise> -} diff --git a/packages/host/apiproxy/src/api/rpc-map.ts b/packages/host/apiproxy/src/api/rpc-map.ts index e34a7701df..80dede1799 100644 --- a/packages/host/apiproxy/src/api/rpc-map.ts +++ b/packages/host/apiproxy/src/api/rpc-map.ts @@ -9,7 +9,6 @@ import type { HostApi } from './host.ts' import type { WorkspaceApi } from './workspace.ts' import type { AgentPresetsApi } from './agent-presets.ts' import type { SkillsApi } from './skills.ts' -import type { ReferencesApi } from './references.ts' import type { GoalsApi } from './goals.ts' import type { SettingsApi } from './settings.ts' import type { CredentialsApi } from './credentials.ts' @@ -52,8 +51,6 @@ export interface RpcMethodMap { 'workspace.insertSessionBefore': WorkspaceApi['insertSessionBefore'] 'workspace.archiveSession': WorkspaceApi['archiveSession'] 'skill.list': SkillsApi['list'] - 'reference.files': ReferencesApi['files'] - 'reference.sessions': ReferencesApi['sessions'] 'agentPreset.list': AgentPresetsApi['list'] 'agentPreset.select': AgentPresetsApi['select'] 'agentPreset.read': AgentPresetsApi['read'] diff --git a/packages/host/apiproxy/src/fetch/client.ts b/packages/host/apiproxy/src/fetch/client.ts index 3fd94ffd4f..70e3ece58f 100644 --- a/packages/host/apiproxy/src/fetch/client.ts +++ b/packages/host/apiproxy/src/fetch/client.ts @@ -41,10 +41,6 @@ import { workspaceRenameValueSchema, } from '../api/workspace.schema.ts' import { skillListValueSchema } from '../api/skills.schema.ts' -import { - referenceFilesValueSchema, - referenceSessionsValueSchema, -} from '../api/references.schema.ts' import { agentPresetCopyValueSchema, agentPresetListValueSchema, agentPresetOpenDocumentValueSchema, agentPresetReadValueSchema, agentPresetRemoveValueSchema, agentPresetSelectValueSchema, @@ -128,10 +124,6 @@ export interface IApiClient { skills: { list(payload: RequestPayload<'skill.list'>, signal?: AbortSignal): Promise>> } - references: { - files(payload: RequestPayload<'reference.files'>, signal?: AbortSignal): Promise>> - sessions(payload: RequestPayload<'reference.sessions'>, signal?: AbortSignal): Promise>> - } agentPresets: { list(payload: RequestPayload<'agentPreset.list'>, signal?: AbortSignal): Promise>> select(payload: RequestPayload<'agentPreset.select'>, signal?: AbortSignal): Promise>> @@ -207,8 +199,6 @@ const UNARY_VALUE_SCHEMAS: { [K in keyof RpcMethodMap]: z.ZodType this.callUnary('skill.list', payload, signal), } - readonly references: IApiClient['references'] = { - files: (payload, signal) => this.callUnary('reference.files', payload, signal), - sessions: (payload, signal) => this.callUnary('reference.sessions', payload, signal), - } - // Annotated like every sibling, and load-bearing rather than cosmetic: // inferring this member inlines `AgentPresetEntry` into the emitted // declaration by the specifier TS picks — the host `index.ts` — which drags diff --git a/packages/host/apiproxy/src/fetch/handler.ts b/packages/host/apiproxy/src/fetch/handler.ts index 43643b2be1..d33e8dc55b 100644 --- a/packages/host/apiproxy/src/fetch/handler.ts +++ b/packages/host/apiproxy/src/fetch/handler.ts @@ -44,10 +44,6 @@ import { workspaceRenameRequestSchema, } from '../api/workspace.schema.ts' import { skillListRequestSchema } from '../api/skills.schema.ts' -import { - referenceFilesRequestSchema, - referenceSessionsRequestSchema, -} from '../api/references.schema.ts' import { agentPresetCopyRequestSchema, agentPresetListRequestSchema, agentPresetOpenDocumentRequestSchema, agentPresetReadRequestSchema, agentPresetRemoveRequestSchema, agentPresetSelectRequestSchema, @@ -121,8 +117,6 @@ const UNARY_ROUTES: UnaryRoutes = { 'workspace.insertSessionBefore': { schema: workspaceInsertSessionBeforeRequestSchema, invoke: (api, r) => api.workspace.insertSessionBefore(r) }, 'workspace.archiveSession': { schema: workspaceArchiveSessionRequestSchema, invoke: (api, r) => api.workspace.archiveSession(r) }, 'skill.list': { schema: skillListRequestSchema, invoke: (api, r) => api.skills.list(r) }, - 'reference.files': { schema: referenceFilesRequestSchema, invoke: (api, r, signal) => api.references.files(r, signal) }, - 'reference.sessions': { schema: referenceSessionsRequestSchema, invoke: (api, r, signal) => api.references.sessions(r, signal) }, 'agentPreset.list': { schema: agentPresetListRequestSchema, invoke: (api, r) => api.agentPresets.list(r) }, 'agentPreset.select': { schema: agentPresetSelectRequestSchema, invoke: (api, r) => api.agentPresets.select(r) }, 'agentPreset.read': { schema: agentPresetReadRequestSchema, invoke: (api, r) => api.agentPresets.read(r) }, @@ -178,9 +172,6 @@ function fullResponse(narrow: RpcResponse): Response { * Wire<> widening back to the exact payload (undefined-valued properties and * absent ones are indistinguishable after JSON transport). */ -// K appears once in the signature but ties the UNARY_ROUTES[K] row lookup to its own -// schema/invoke pairing; a union parameter degrades the row to an uninvokable intersection. -// oxlint-disable-next-line typescript/no-unnecessary-type-parameters async function handleUnary( api: ApiProxy, method: K, message: ClientRequest, signal: AbortSignal, ): Promise { diff --git a/packages/host/apiproxy/src/index.ts b/packages/host/apiproxy/src/index.ts index af8e064592..ac6c770801 100644 --- a/packages/host/apiproxy/src/index.ts +++ b/packages/host/apiproxy/src/index.ts @@ -85,7 +85,6 @@ export class ApiProxyService extends Service implements ApiProxy { readonly host: ApiProxy['host'] readonly goals: ApiProxy['goals'] readonly skills: ApiProxy['skills'] - readonly references: ApiProxy['references'] readonly agentPresets: ApiProxy['agentPresets'] readonly settings: ApiProxy['settings'] readonly credentials: ApiProxy['credentials'] @@ -114,7 +113,6 @@ export class ApiProxyService extends Service implements ApiProxy { this.host = api.host this.goals = api.goals this.skills = api.skills - this.references = api.references this.agentPresets = api.agentPresets this.settings = api.settings this.credentials = api.credentials diff --git a/packages/host/apiproxy/tests/api-proxy-references.spec.ts b/packages/host/apiproxy/tests/api-proxy-references.spec.ts index 408584e26c..921440d7f2 100644 --- a/packages/host/apiproxy/tests/api-proxy-references.spec.ts +++ b/packages/host/apiproxy/tests/api-proxy-references.spec.ts @@ -1,7 +1,7 @@ /** - * Reference RPC coverage over the real ApiProxy: addressed Host discovery, - * canonical session mentions, atomic snapshot preparation before enqueue, - * and error/cancellation behavior. + * Referenced prompt coverage over the real ApiProxy: atomic snapshot + * preparation before enqueue and error/cancellation behavior. Discovery lives + * on the owning services' Remote faces, tested in their packages. */ import { Context } from '@deepseek-ai/cordis' import { describe, expect, it, vi } from 'vitest' @@ -86,67 +86,6 @@ function stubAgent(ctx: Context, status: Agent['status'] = 'idle') { return agent } -describe('reference discovery', () => { - it('addresses the target agent and returns file candidates unchanged', async () => { - const ctx = await harness() - const agent = stubAgent(ctx) - const list = vi.fn(() => Promise.resolve([ - { path: 'src', kind: 'directory' as const }, - { path: 'src/index.ts', kind: 'file' as const }, - ])) - ctx.provide('fileReferences', { list } as never) - const api = createApiProxy(ctx, DEFAULTS) - const signal = new AbortController().signal - const value = expectOk(await api.references.files( - request({ sessionId: agent.id, query: 'sr' }), - signal, - )) - expect(value.items).toEqual([ - { path: 'src', kind: 'directory' }, - { path: 'src/index.ts', kind: 'file' }, - ]) - expect(list).toHaveBeenCalledWith(agent, 'sr', signal) - }) - - it('formats metadata candidates as opaque canonical mentions', async () => { - const ctx = await harness() - const agent = stubAgent(ctx) - const source = 'source-session' as SessionId - const listCandidates = vi.fn(() => Promise.resolve([{ - sessionId: source, - label: 'Research]', - cwd: '/project', - createdAt: 42, - }])) - ctx.provide('sessionReferenceResolver', { listCandidates } as never) - const api = createApiProxy(ctx, DEFAULTS) - const value = expectOk(await api.references.sessions( - request({ sessionId: agent.id, query: 'res' }), - new AbortController().signal, - )) - expect(value.items).toEqual([{ - sessionId: source, - label: 'Research]', - cwd: '/project', - createdAt: 42, - mention: formatSessionReferenceMention({ sessionId: source, label: 'Research]' }), - }]) - expect(listCandidates).toHaveBeenCalledWith(agent, 'res', undefined, expect.any(AbortSignal)) - }) - - it('fails explicitly when a reference capability is not composed', async () => { - const ctx = await harness() - const agent = stubAgent(ctx) - const api = createApiProxy(ctx, DEFAULTS) - expect(expectErr(await api.references.files( - request({ sessionId: agent.id, query: '' }), - )).code).toBe('reference-unavailable') - expect(expectErr(await api.references.sessions( - request({ sessionId: agent.id, query: '' }), - )).code).toBe('reference-unavailable') - }) -}) - describe('referenced prompt preparation', () => { it('normalizes the visible mention and waits for all context preparation before enqueue', async () => { const ctx = await harness() @@ -292,6 +231,44 @@ describe('referenced prompt preparation', () => { expect(agent.inject).not.toHaveBeenCalled() }) + it('releases the admission listeners when the agent is disposed with the prompt pending', async () => { + const ctx = await harness() + const agent = stubAgent(ctx) + const source = 'source-session' as SessionId + const context = createUserMessage({ + source: { kind: 'plugin' as const, plugin: 'session-reference' }, + content: [{ type: 'text' as const, text: 'snapshot' }], + }) + ctx.provide('sessionReferenceResolver', { + prepare: () => Promise.resolve({ + content: [{ type: 'text' as const, text: 'continue @Research' }], + additionalContext: context, + }), + } as never) + const api = createApiProxy(ctx, DEFAULTS) + expectOk(await api.sessions.prompt(request({ + sessionId: agent.id, + content: [{ + type: 'text' as const, + text: `continue ${formatSessionReferenceMention({ sessionId: source, label: 'Research' })}`, + }], + mode: 'queue' as const, + }))) + const queued = agent.followup.mock.calls[0]?.[0] + if (queued === undefined) throw new Error('expected queued prompt') + + agentEvents(ctx, agent).emit('agent/disposed', { agent }) + + const signal = new AbortController().signal + const batch = agent.inbox.claim('next-turn', 1) + const decision = await agentEvents(ctx, agent).waterfall( + 'agent/pre-step', + { messages: batch, turn: 1, step: 1, signal }, + () => Promise.resolve({ kind: 'enter' as const, messages: batch }), + ) + expect(decision).toEqual({ kind: 'enter', messages: [queued] }) + }) + it('keeps prepared context paired when a queued prompt moves to steering', async () => { const ctx = await harness() const agent = stubAgent(ctx, 'running') diff --git a/packages/host/apiproxy/tests/client-handler.spec.ts b/packages/host/apiproxy/tests/client-handler.spec.ts index 7d1afc3650..4130d8f210 100644 --- a/packages/host/apiproxy/tests/client-handler.spec.ts +++ b/packages/host/apiproxy/tests/client-handler.spec.ts @@ -22,7 +22,6 @@ function scriptedApi(overrides: { subagents?: Partial host?: Partial skills?: Partial - references?: Partial agentPresets?: Partial events?: Partial goals?: Partial @@ -91,11 +90,6 @@ function scriptedApi(overrides: { archiveSession: r => ok(r, { archivedSessionIds: [r.payload.sessionId] }), }, skills: { list: r => ok(r, { skills: [] }), ...overrides.skills }, - references: { - files: r => ok(r, { items: [] }), - sessions: r => ok(r, { items: [] }), - ...overrides.references, - }, agentPresets: { list: r => ok(r, { presets: [], authorable: false, hasDocument: false }), select: r => ok(r, { agentPreset: r.payload.agentPreset }), diff --git a/packages/host/apiproxy/tests/fetch-carrier.spec.ts b/packages/host/apiproxy/tests/fetch-carrier.spec.ts index 47d9382371..2000f708ba 100644 --- a/packages/host/apiproxy/tests/fetch-carrier.spec.ts +++ b/packages/host/apiproxy/tests/fetch-carrier.spec.ts @@ -223,14 +223,6 @@ function fakeApi(overrides: Partial<{ muxFrames: MuxFrame[]; hostFrames: HostFra return { rpcId: request.rpcId, result: { ok: true, value: { skills: [{ name: 'commit-helper', description: 'Git commits', modelInvocable: true }] } } } }, }, - references: { - async files(request) { - return { rpcId: request.rpcId, result: { ok: true, value: { items: [] } } } - }, - async sessions(request) { - return { rpcId: request.rpcId, result: { ok: true, value: { items: [] } } } - }, - }, goals: { async create(request) { return { rpcId: request.rpcId, result: { ok: false, error: { code: 'internal', message: 'stub', details: {} } } } diff --git a/packages/host/apiproxy/tests/rpc-schemas.spec.ts b/packages/host/apiproxy/tests/rpc-schemas.spec.ts index 66ae52f783..8902715c6b 100644 --- a/packages/host/apiproxy/tests/rpc-schemas.spec.ts +++ b/packages/host/apiproxy/tests/rpc-schemas.spec.ts @@ -73,6 +73,9 @@ describe('rpcErrorSchema', () => { details: { provider: 'p', model: 'm' }, }).code).toBe('model-unavailable') expect(rpcErrorSchema.parse({ code: 'agent-busy', message: 'm', details: { reason: 'r' } }).code).toBe('agent-busy') + expect(rpcErrorSchema.parse({ code: 'reference-unavailable', message: 'm', details: { kind: 'session' } }).code).toBe('reference-unavailable') + expect(rpcErrorSchema.parse({ code: 'reference-invalid', message: 'm', details: { reason: 'r' } }).code).toBe('reference-invalid') + expect(rpcErrorSchema.parse({ code: 'reference-failed', message: 'm', details: { reason: 'r' } }).code).toBe('reference-failed') expect(rpcErrorSchema.parse({ code: 'queue-item-not-found', message: 'm', details: { itemId: 'i' } }).code).toBe('queue-item-not-found') expect(rpcErrorSchema.parse({ code: 'command-error', message: 'm', details: {} }).code).toBe('command-error') expect(rpcErrorSchema.parse({ code: 'unknown-command', message: 'm', details: {} }).code).toBe('unknown-command') @@ -84,6 +87,8 @@ describe('rpcErrorSchema', () => { it('rejects a known code with missing details', () => { expect(() => rpcErrorSchema.parse({ code: 'agent-busy', message: 'm', details: {} })).toThrow() + expect(() => rpcErrorSchema.parse({ code: 'reference-unavailable', message: 'm', details: { kind: 'disk' } })).toThrow() + expect(() => rpcErrorSchema.parse({ code: 'reference-failed', message: 'm', details: {} })).toThrow() expect(() => rpcErrorSchema.parse({ code: 'title-invalid', message: 'm', details: {} })).toThrow() expect(() => rpcErrorSchema.parse({ code: 'command-error', message: 'm' })).toThrow() expect(() => rpcErrorSchema.parse({ code: 'nope', message: 'm', details: {} })).toThrow() diff --git a/packages/host/apiproxy/tsconfig.json b/packages/host/apiproxy/tsconfig.json index 9dc2ac1b78..6abd799c0e 100644 --- a/packages/host/apiproxy/tsconfig.json +++ b/packages/host/apiproxy/tsconfig.json @@ -62,9 +62,6 @@ { "path": "../../session-query/session-query" }, - { - "path": "../../context/file-reference" - }, { "path": "../../context/session-reference" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b862c8fcf2..0ce2a4ce80 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -858,6 +858,9 @@ importers: '@deepseek-ai/dsh-credentials': specifier: workspace:^ version: link:../../credentials/credentials + '@deepseek-ai/dsh-file-reference': + specifier: workspace:^ + version: link:../../context/file-reference '@deepseek-ai/dsh-goal': specifier: workspace:^ version: link:../../goal/goal @@ -879,6 +882,9 @@ importers: '@deepseek-ai/dsh-session-persistence': specifier: workspace:^ version: link:../../session/session-persistence + '@deepseek-ai/dsh-session-reference': + specifier: workspace:^ + version: link:../../context/session-reference '@deepseek-ai/dsh-settings': specifier: workspace:^ version: link:../../settings/settings @@ -2424,18 +2430,30 @@ importers: '@deepseek-ai/dsh-api-remotes': specifier: workspace:^ version: link:../../api/remotes + '@deepseek-ai/dsh-client-locale': + specifier: workspace:^ + version: link:../locale '@deepseek-ai/dsh-client-runtime': specifier: workspace:^ version: link:../runtime '@deepseek-ai/dsh-client-ui-input-trigger': specifier: workspace:^ version: link:../ui-input-trigger + '@deepseek-ai/dsh-client-ui-slots': + specifier: workspace:^ + version: link:../ui-slots '@deepseek-ai/dsh-file-reference': specifier: workspace:^ version: link:../../context/file-reference '@deepseek-ai/dsh-invariants': specifier: workspace:^ version: link:../../runtime-diagnostics/invariants + '@deepseek-ai/dsh-session-reference': + specifier: workspace:^ + version: link:../../context/session-reference + '@deepseek-ai/dsh-typert-protocol': + specifier: workspace:^ + version: link:../../typert/protocol packages/client/ui-settings: dependencies: @@ -3385,6 +3403,10 @@ importers: version: link:../../core/tools packages/context/file-reference: + dependencies: + zod: + specifier: ^4.4.3 + version: 4.4.3 devDependencies: '@deepseek-ai/cordis': specifier: workspace:^ @@ -3395,6 +3417,9 @@ importers: '@deepseek-ai/dsh-invariants': specifier: workspace:^ version: link:../../runtime-diagnostics/invariants + '@deepseek-ai/dsh-typert-protocol': + specifier: workspace:^ + version: link:../../typert/protocol packages/context/file-reference-local: dependencies: @@ -3426,6 +3451,9 @@ importers: '@deepseek-ai/schemastery': specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery + zod: + specifier: ^4.4.3 + version: 4.4.3 devDependencies: '@deepseek-ai/cordis': specifier: workspace:^ @@ -3451,6 +3479,9 @@ importers: '@deepseek-ai/dsh-session-query': specifier: workspace:^ version: link:../../session-query/session-query + '@deepseek-ai/dsh-typert-protocol': + specifier: workspace:^ + version: link:../../typert/protocol packages/context/time-context: dependencies: @@ -4789,9 +4820,6 @@ importers: '@deepseek-ai/dsh-credentials': specifier: workspace:^ version: link:../../credentials/credentials - '@deepseek-ai/dsh-file-reference': - specifier: workspace:^ - version: link:../../context/file-reference '@deepseek-ai/dsh-goal': specifier: workspace:^ version: link:../../goal/goal diff --git a/scripts/gen-cordis-catalog.ts b/scripts/gen-cordis-catalog.ts index 4e2ca42b8d..0c56dd9953 100644 --- a/scripts/gen-cordis-catalog.ts +++ b/scripts/gen-cordis-catalog.ts @@ -271,6 +271,7 @@ export const LINK_MAP: Readonly> = { PreparedReferencedMessage: 'session-reference.md', FileReferenceCandidate: 'session-reference.md', SessionReferenceCandidate: 'session-reference.md', + SessionReferenceMentionCandidate: 'session-reference.md', SessionReferenceInput: 'session-reference.md', SessionEvent: 'session.md', SessionId: 'core.md', diff --git a/scripts/gen-doc-graphs.ts b/scripts/gen-doc-graphs.ts index 57d1f77609..e839ad0052 100644 --- a/scripts/gen-doc-graphs.ts +++ b/scripts/gen-doc-graphs.ts @@ -244,8 +244,7 @@ const SERVICE_ROLES: ServiceRole[] = [ title: 'File reference discovery', mode: 'seam', implementations: ['file-reference-local'], - consumers: ['apiproxy'], - note: 'The interface returns path-only completion candidates within the addressed Agent cwd; providers own namespace access and ranking without reading file contents.', + note: '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.', }, { key: 'sessionReferenceResolver', diff --git a/scripts/type-equiv.manifest.json b/scripts/type-equiv.manifest.json index 423a099544..cbd01a6fc8 100644 --- a/scripts/type-equiv.manifest.json +++ b/scripts/type-equiv.manifest.json @@ -668,13 +668,18 @@ { "doc": "docs/subsystems/session-reference.md", "symbol": "FileReferenceCandidate", - "source": "packages/context/file-reference/src/index.ts" + "source": "packages/context/file-reference/src/types.ts" }, { "doc": "docs/subsystems/session-reference.md", "symbol": "SessionReferenceCandidate", "source": "packages/context/session-reference/src/types.ts" }, + { + "doc": "docs/subsystems/session-reference.md", + "symbol": "SessionReferenceMentionCandidate", + "source": "packages/context/session-reference/src/types.ts" + }, { "doc": "docs/subsystems/session-reference.md", "symbol": "PreparedReferencedMessage", diff --git a/tsconfig.base.json b/tsconfig.base.json index ba80a3286f..5eae861962 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -87,6 +87,9 @@ "@deepseek-ai/dsh-api-remotes/invariant": ["./packages/api/remotes/src/invariant.ts"], "@deepseek-ai/dsh-compaction/checkpoint": ["./packages/compaction/compaction/src/checkpoint.ts"], "@deepseek-ai/dsh-compaction/types": ["./packages/compaction/compaction/src/types.ts"], + "@deepseek-ai/dsh-file-reference/grammar": ["./packages/context/file-reference/src/grammar.ts"], + "@deepseek-ai/dsh-file-reference/types": ["./packages/context/file-reference/src/types.ts"], + "@deepseek-ai/dsh-session-reference/types": ["./packages/context/session-reference/src/types.ts"], "@deepseek-ai/dsh-tools/presentation": ["./packages/core/tools/src/presentation.ts"], "@deepseek-ai/dsh-tools/types": ["./packages/core/tools/src/types.ts"], "@deepseek-ai/dsh-tool-subagent-control/list-agents": ["./packages/subagent/tool-subagent-control/src/list-agents.ts"],