mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
Merge commit '7ef7175ff0c16623ab4e43187d1ecc29820b3909' into codex/acp-minimal-diagnostics
This commit is contained in:
+2
-2
@@ -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/bug-fix/2026-08-24-system-prompt-section-order-ties.md
|
||||
2026-08-24-system-prompt-section-order-ties.md: 673c3b3cd668115ead9f9b5478c2bc432b78f930
|
||||
2026-08-24-system-prompt-section-order-ties.zh.md: 96a6843a0db48e52a2132ad9f8caf6243dfbbcd2
|
||||
2026-08-24-system-prompt-section-order-ties.md: d92756e751e893b1d03b8892ef71ff9faac9d2c6
|
||||
2026-08-24-system-prompt-section-order-ties.zh.md: 4a822b7925a38feb254dbc534fc6153c76a93e19
|
||||
+1
@@ -1,6 +1,7 @@
|
||||
# Agent Note: Equal-order system-prompt sections render in activation order
|
||||
|
||||
Status: implemented
|
||||
Archived: 2026-08-25
|
||||
|
||||
English | [中文](2026-08-24-system-prompt-section-order-ties.zh.md)
|
||||
|
||||
+1
@@ -1,6 +1,7 @@
|
||||
# Agent Note: 等序系统提示词分段按激活顺序渲染
|
||||
|
||||
Status: implemented
|
||||
Archived: 2026-08-25
|
||||
|
||||
[English](2026-08-24-system-prompt-section-order-ties.md) | 中文
|
||||
|
||||
@@ -112,6 +112,9 @@
|
||||
"bug-fix/2026-08-12-collapsed-sidebar-shared-entry-motion.i18n.yaml": "sha256:3ce4f6e39e173fc304bf64deca9c95bcddc1dbb492e065ca8c267a7a40788588",
|
||||
"bug-fix/2026-08-12-collapsed-sidebar-shared-entry-motion.md": "sha256:7b169aa4543edfc965de5a8b7b9e60aa9d9d5218693cd0b57908e2d482280723",
|
||||
"bug-fix/2026-08-12-collapsed-sidebar-shared-entry-motion.zh.md": "sha256:88db36c698800bf55c3c7531d6f92665576d978c29c15ff7d74215fb93376cb1",
|
||||
"bug-fix/2026-08-24-system-prompt-section-order-ties.i18n.yaml": "sha256:f7a20bddd4544738ec0dbbfc52ea931f42317defa1674beb9a3c0daebd52fc2d",
|
||||
"bug-fix/2026-08-24-system-prompt-section-order-ties.md": "sha256:108a97346eb7a62f1ab01f48dbb9fdd965e8991f53e382b0f501b916af0e9e23",
|
||||
"bug-fix/2026-08-24-system-prompt-section-order-ties.zh.md": "sha256:3deaddfcf9736b3ff8d61b51093d7e46fdcc86103705033e4aa4c9d043794b16",
|
||||
"feature/2026-06-14-acp-agent-client-protocol.i18n.yaml": "sha256:006795baa43ae962a8d125cc0f1e9f134bc2ee9fb758b6e7669e3fa0126e1918",
|
||||
"feature/2026-06-14-acp-agent-client-protocol.md": "sha256:6828c0af74bb3fb96206ca6b21c0e56a000b50e4744aad4bc2c05092f3a5a31b",
|
||||
"feature/2026-06-14-acp-agent-client-protocol.zh.md": "sha256:ba104e841a1fb84edbd3b6c8119d50445b7785255a7a8d13bb9ac8a2cb4d2e69",
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md
|
||||
2026-07-05-prompt-variables-and-tool-guidance-ownership.md: 9a53619d9510e3f4fa561f8420b2da3bedbbf4bb
|
||||
2026-07-05-prompt-variables-and-tool-guidance-ownership.zh.md: dc1164198df0f92b843c75b618f140d8aef86e4f
|
||||
2026-07-05-prompt-variables-and-tool-guidance-ownership.md: 361184fa7dbdaccd49ac19235c016daf5eb5ca53
|
||||
2026-07-05-prompt-variables-and-tool-guidance-ownership.zh.md: 6b462572883fb69ca64f2babf28974ae59e7bd74
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ Plugins register `{{name}}` values through `ctx.systemPrompt.variable(name, prov
|
||||
|
||||
### Persona as the order-0 section
|
||||
|
||||
`dsh-system-prompt` owns `harness:identity` at order `-100` and the configured `deployment:persona` at order 0, so both survive a replacement loop. Prompt rendering has one path, `renderPrompt(assembly)`, and the routed request header therefore records the exact prompt later replayed by `ctx.tokenMeter` for compaction pressure. An agent-scoped `deployment:persona` shadows the global default and lets subagent providers install a persona before publication. The conventional order bands are identity `-100`, persona `0`, and tool guidance `100–199`.
|
||||
`dsh-system-prompt` owns `harness:identity` at first-party order `-1000` and the configured `deployment:persona` at order 0, so both survive a replacement loop. Prompt rendering has one path, `renderPrompt(assembly)`, and the routed request header therefore records the exact prompt later replayed by `ctx.tokenMeter` for compaction pressure. An agent-scoped `deployment:persona` shadows the global default and lets subagent providers install a persona before publication. The [first-party order allocation](2026-08-25-sparse-first-party-prompt-section-orders.md) owns the sparse named placements for identity, policy, tool guidance, generated protocol, and final-output obligations.
|
||||
|
||||
### Tool guidance ownership
|
||||
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ Status: implemented
|
||||
|
||||
### Persona 作为 order-0 section
|
||||
|
||||
`dsh-system-prompt` 拥有 order 为 `-100` 的 `harness:identity` 和 order 为 0 的配置 `deployment:persona`,因此两者在循环被替换时仍然存活。提示词渲染只有一条路径 `renderPrompt(assembly)`,已路由请求 header 因此会记录准确的提示词,稍后由 `ctx.tokenMeter` 为压缩(compaction)压力回放。agent 作用域的 `deployment:persona` 遮蔽全局默认值,允许 subagent 提供方在发布前安装 persona。约定的 order 区间为:identity `-100`、persona `0`、工具指导 `100–199`。
|
||||
`dsh-system-prompt` 拥有 first-party order 为 `-1000` 的 `harness:identity` 和 order 为 0 的配置 `deployment:persona`,因此两者在循环被替换时仍然存活。提示词渲染只有一条路径 `renderPrompt(assembly)`,已路由请求 header 因此会记录准确的提示词,稍后由 `ctx.tokenMeter` 为压缩(compaction)压力回放。agent 作用域的 `deployment:persona` 遮蔽全局默认值,允许 subagent 提供方在发布前安装 persona。[first-party 顺序分配](2026-08-25-sparse-first-party-prompt-section-orders.zh.md)规定身份、策略、工具指导、生成协议和最终输出义务的稀疏具名位置。
|
||||
|
||||
### 工具指导归属
|
||||
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-25-sparse-first-party-prompt-section-orders.md
|
||||
2026-08-25-sparse-first-party-prompt-section-orders.md: 2bf2e7441b449a6cfbd5b845f1d7e97b3fab09ae
|
||||
2026-08-25-sparse-first-party-prompt-section-orders.zh.md: 624ed51f4d40848c72091097900ef05ec35fdd2e
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
# Agent Note: Centralize sparse first-party prompt-section orders
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-25-sparse-first-party-prompt-section-orders.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Repository-owned system-prompt sections declared unrelated numeric literals across more than twenty packages. The main tool sequence occupied consecutive values from 100 through 117 and then used half-step values for insertions. A later change could therefore collide with an existing section without seeing the complete allocation.
|
||||
|
||||
Equal orders used stable JavaScript sort behavior, which made plugin activation order the effective tie-breaker. The [Cordis/workflow prompt-order fix](../../archived/bug-fix/2026-08-24-system-prompt-section-order-ties.md) showed that clean compositions can activate the same plugins in different orders and produce different request headers and snapshot results. Fixing one collision locally did not prevent another package from reusing that value.
|
||||
|
||||
The shell guidance also followed filesystem guidance even though shell commands have the broadest execution and failure semantics. A model should read the shell result obligation before the narrower instructions that route file work to dedicated tools.
|
||||
|
||||
## Decision
|
||||
|
||||
`@deepseek-ai/dsh-system-prompt` exports `FIRST_PARTY_SECTION_ORDER` as the single allocation for repository-owned sections. Every first-party contributor imports its named placement instead of declaring a numeric literal. Values are unique integers, and adjacent allocated values differ by at least ten.
|
||||
|
||||
The allocation preserves the established first-party sequence except for two deliberate changes: Bash, or PowerShell in the Windows composition, leads per-tool guidance; and sections that shared an order receive an explicit sequence. The groups are:
|
||||
|
||||
| Group | Entries |
|
||||
|---|---|
|
||||
| Product opening | `harness:identity` −1000, `harness:source` −900, `app:web-surface` −800, `deployment:persona` 0 |
|
||||
| Work modes | `plan:policy` 500, `team:policy` 600 |
|
||||
| Invocation prelude | `tools:code-only` 800, `context:file-reference` 900 |
|
||||
| Local tools | `tool:bash` 1000, `tool:pwsh` 1010, `tool:read` 1100, `tool:write` 1200, `tool:edit` 1300, `tool:glob` 1400, `tool:grep` 1500, `tool:jobs` 1600, `tool:pty` 1700 |
|
||||
| Higher-level tools | `tool:web_search` 2000, `tool:web_fetch` 2100, `tool:lsp` 2200, `tool:session-query` 2300, `tool:goal` 2400, `tool:cordis` 2500, `tool:workflow` 2600, `tool:ralph` 2700, continuable-subagent guidance 2800, `tool:report` 2900 |
|
||||
| Generated protocol | `tools:sdk` 5000 |
|
||||
| Final-output obligations | deliverable file references 9000, `tool:structured_output` 9900 |
|
||||
|
||||
`SystemPrompt.assemble()` sorts equal-order sections by code-unit section name after comparing `order`. This makes third-party collisions deterministic without locale-sensitive comparison. First-party contributors still receive distinct ranks so their intended sequence remains explicit rather than depending on the fallback.
|
||||
|
||||
Dynamic `PromptContext` order and tool-schema `toolOrder` are separate sequences and remain unchanged. A scoped `deployment:persona` continues to shadow the global section by name before section sorting, so it shares `PERSONA_ORDER` rather than consuming another placement.
|
||||
|
||||
## Verification
|
||||
|
||||
The system-prompt unit suite verifies that every exported first-party value is an integer, every value is unique, adjacent values differ by at least ten, and opposite registration permutations produce the same code-unit name order for a tie. Real-composition snapshots pin the model-visible ordering change, including Bash before filesystem guidance and the explicit Cordis, workflow, Ralph, subagent, and report sequence.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep package-local numeric literals and review collisions manually.** Rejected because a contributor cannot see the complete allocation locally, and the collision that motivated the earlier fix recurred after that fix merged.
|
||||
|
||||
**Continue inserting fractional values.** Rejected because fractions provide no durable spacing rule, obscure the semantic groups, and still permit unrelated packages to choose the same value.
|
||||
|
||||
**Normalize only snapshot comparisons.** Rejected because the runtime request header and model prompt would remain activation-order dependent while the test hid the difference.
|
||||
|
||||
**Preserve activation order for equal ranks.** Rejected because activation order is not a prompt-order decision and varies across valid compositions. Name order is deterministic for external collisions; explicit named placements carry first-party intent.
|
||||
|
||||
**Renumber dynamic contexts and tool schemas in the same allocation.** Rejected because they are independently assembled sequences. Combining them would imply cross-sequence ordering that the runtime does not perform.
|
||||
|
||||
## Consequences
|
||||
|
||||
Numeric ranks are not rendered, so the renumbering alone does not change model text. Bash or PowerShell moves before other per-tool guidance, and previously tied sections acquire deterministic order; those model-visible changes update request-header snapshots and may invalidate provider prefix reuse from the first moved paragraph.
|
||||
|
||||
An external plugin that chose a raw number specifically to sit between old first-party values may move relative to repository sections. This repository is pre-release and provides no compatibility shim for the old allocation; extensions can select positions from the exported current allocation. Equal external ranks remain supported and deterministic by name.
|
||||
|
||||
The system-prompt package now knows the names and relative placement of repository features. That centralized coupling is deliberate: the registry already owns the ordering semantics, while distributed numeric literals made the same relationship implicit and uncheckable.
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
# Agent Note: 集中管理稀疏的 first-party 提示词段顺序
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-25-sparse-first-party-prompt-section-orders.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
仓库自带的系统提示词段分散在二十多个包中,各自声明互不关联的数字字面量。主要工具序列连续占用 100 到 117,后续插入还使用半步数值。因此,后续更改可能在无法看到完整分配表的情况下与已有段发生冲突。
|
||||
|
||||
相同 order 依赖 JavaScript 稳定排序,使插件激活顺序成为实际的平局规则。[Cordis/workflow 提示词顺序修复](../../archived/bug-fix/2026-08-24-system-prompt-section-order-ties.md)表明,完整且有效的组合可能按不同顺序激活同一组插件,进而产生不同的请求 header 和快照结果。局部修复一次冲突,无法阻止另一个包再次使用同一数值。
|
||||
|
||||
此外,shell 指导位于文件系统指导之后,但 shell 命令具有最广泛的执行和失败语义。模型应先读到 shell 结果义务,再阅读将文件操作分流到专用工具的更窄指令。
|
||||
|
||||
## 决策
|
||||
|
||||
`@deepseek-ai/dsh-system-prompt` 导出 `FIRST_PARTY_SECTION_ORDER`,作为仓库自带提示词段的唯一分配表。每个 first-party 贡献方都导入具名位置,不再声明数字字面量。所有值都是互不相同的整数,相邻已分配值之差至少为十。
|
||||
|
||||
除两项有意调整外,该分配保留既有 first-party 顺序:Bash,或 Windows 组合中的 PowerShell,位于逐工具指导的首位;原先共享 order 的段获得明确顺序。分组如下:
|
||||
|
||||
| 分组 | 条目 |
|
||||
|---|---|
|
||||
| 产品开场 | `harness:identity` −1000、`harness:source` −900、`app:web-surface` −800、`deployment:persona` 0 |
|
||||
| 工作模式 | `plan:policy` 500、`team:policy` 600 |
|
||||
| 调用前置说明 | `tools:code-only` 800、`context:file-reference` 900 |
|
||||
| 本地工具 | `tool:bash` 1000、`tool:pwsh` 1010、`tool:read` 1100、`tool:write` 1200、`tool:edit` 1300、`tool:glob` 1400、`tool:grep` 1500、`tool:jobs` 1600、`tool:pty` 1700 |
|
||||
| 高层工具 | `tool:web_search` 2000、`tool:web_fetch` 2100、`tool:lsp` 2200、`tool:session-query` 2300、`tool:goal` 2400、`tool:cordis` 2500、`tool:workflow` 2600、`tool:ralph` 2700、可继续运行的 subagent 指导 2800、`tool:report` 2900 |
|
||||
| 生成协议 | `tools:sdk` 5000 |
|
||||
| 最终输出义务 | 可交付文件引用 9000、`tool:structured_output` 9900 |
|
||||
|
||||
`SystemPrompt.assemble()` 比较 `order` 后,按提示词段名称的代码单元顺序排列同号项。这样无需使用受区域设置影响的比较,也能让第三方冲突产生确定结果。first-party 贡献方仍使用不同 rank,其预期顺序由分配表明确表达,而不依赖兜底规则。
|
||||
|
||||
动态 `PromptContext` 顺序和工具 schema 的 `toolOrder` 是独立序列,保持不变。带作用域的 `deployment:persona` 仍会在段排序之前按名称遮蔽全局段,因此共享 `PERSONA_ORDER`,而不占用另一个位置。
|
||||
|
||||
## 验证
|
||||
|
||||
系统提示词单元测试验证:导出的每个 first-party 值都是整数、所有值互不重复、相邻值之差至少为十,并且顺序相反的两种注册排列会对同号项产生相同的代码单元名称顺序。真实组合快照固定面向模型的顺序变化,包括 Bash 位于文件系统指导之前,以及 Cordis、workflow、Ralph、subagent 和 report 的明确序列。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**保留包内数字字面量并通过评审人工检查冲突。**未采用,因为贡献方无法在局部看到完整分配表,而且早期修复合入后,触发该修复的同类冲突再次出现。
|
||||
|
||||
**继续插入小数值。**未采用,因为小数没有持久的间距规则,难以表达语义分组,也无法阻止无关包选择同一数值。
|
||||
|
||||
**只规范化快照比较。**未采用,因为运行时请求 header 和模型提示词仍依赖激活顺序,测试只会隐藏差异。
|
||||
|
||||
**同 rank 时保留激活顺序。**未采用,因为激活顺序不是提示词顺序决策,并且会在有效组合之间变化。名称顺序为外部冲突提供确定结果;具名位置负责表达 first-party 意图。
|
||||
|
||||
**在同一分配表中重新编号动态上下文和工具 schema。**未采用,因为运行时独立组装这些序列。合并分配会暗示运行时并不执行的跨序列顺序。
|
||||
|
||||
## 后果
|
||||
|
||||
数字 rank 不会被渲染,因此单纯重新编号不会改变模型文本。Bash 或 PowerShell 会移到其他逐工具指导之前,原先同号的段会获得确定顺序;这些面向模型的变化会更新请求 header 快照,并可能从第一个移动的段落起使提供方前缀复用失效。
|
||||
|
||||
如果外部插件专门选择一个原始数字以插入旧 first-party 数值之间,它相对仓库段的位置可能改变。本仓库处于预发布阶段,不为旧分配提供兼容层;扩展可以根据当前导出的分配表选择位置。外部段仍可使用相同 rank,并会按名称获得确定顺序。
|
||||
|
||||
系统提示词包现在了解仓库功能的名称和相对位置。这种集中耦合是有意的:注册表本就拥有排序语义,而分散的数字字面量只是让同一关系变得隐式且无法检查。
|
||||
+2
-2
@@ -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/bug-fix/2026-07-30-source-checkout-workdir-distinction.md
|
||||
2026-07-30-source-checkout-workdir-distinction.md: ba6d9dd12b55a54d4ae8d2e91ad83ac3c1dc47fd
|
||||
2026-07-30-source-checkout-workdir-distinction.zh.md: 06b09f82f294a70795090a9aec1b2220bd6e7512
|
||||
2026-07-30-source-checkout-workdir-distinction.md: 407d11bd2d0bb27bf1953f1a3a84848480e03328
|
||||
2026-07-30-source-checkout-workdir-distinction.zh.md: 91251aa040226d72cf431101aa0d064ba107823e
|
||||
|
||||
@@ -14,7 +14,7 @@ A blanket statement that the checkout is not the working directory would also be
|
||||
|
||||
The section identifies the path as the “DeepSeek Harness implementation checkout.” It says that the checkout location and current working directory are separate values that may differ, forbids inferring the working directory from the checkout path, directs the model to use `pwd`, and limits the checkout's purpose to inspecting or extending DSH itself.
|
||||
|
||||
The path derivation, global `harness:source` ownership, and `-99` ordering remain unchanged. Describing the values as conceptually separate rather than always unequal keeps the instruction accurate in both ordinary project sessions and `dsh meta`.
|
||||
The path derivation and global `harness:source` ownership remain unchanged. The section uses first-party order −900, immediately after `harness:identity`. Describing the values as conceptually separate rather than always unequal keeps the instruction accurate in both ordinary project sessions and `dsh meta`.
|
||||
|
||||
## Verification
|
||||
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ Status: implemented
|
||||
|
||||
该提示词段将路径标识为「DeepSeek Harness implementation checkout」。它说明 checkout 位置与当前工作目录是两个可能不同的值,禁止从 checkout 路径推断工作目录,指示模型使用 `pwd`,并限定该 checkout 只用于检查或扩展 DSH 自身。
|
||||
|
||||
路径推导方式、全局 `harness:source` 所有权和 `-99` 顺序均保持不变。将两者描述为概念上独立、而不是始终不相等,使这条指令在普通项目会话和 `dsh meta` 中都准确。
|
||||
路径推导方式与全局 `harness:source` 所有权保持不变。该段使用 first-party 顺序 −900,紧随 `harness:identity`。将两者描述为概念上独立、而不是始终不相等,使这条指令在普通项目会话和 `dsh meta` 中都准确。
|
||||
|
||||
## 验证
|
||||
|
||||
|
||||
@@ -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/bug-fix/2026-08-07-code-mode-executor-collapse.md
|
||||
2026-08-07-code-mode-executor-collapse.md: abfba369a2f6482f72d7224f6762f3ac75c8234e
|
||||
2026-08-07-code-mode-executor-collapse.zh.md: 8bfdc8be6189d12b32fc260f2ab9637b8e2ba8c9
|
||||
2026-08-07-code-mode-executor-collapse.md: b19d0842d8b58b170a0f6839dcb32b9390211ca0
|
||||
2026-08-07-code-mode-executor-collapse.zh.md: 38bb319c8ce8fd01ab42ad29a6b3748c5d0f7925
|
||||
|
||||
@@ -42,5 +42,5 @@ No provider guarantees interception of unadvertised names; the reported session
|
||||
- `both` and `native` behavior is unchanged; SDK sub-dispatches are unchanged (the `parent` token is the discriminator).
|
||||
- A collapsed call is rejected at `prepare`, BEFORE the extensible policy pipeline: pre-execute listeners, approval `ask`, and guards never observe it. `executionMode` also fails closed (`exclusive`), so scheduling has no observable difference.
|
||||
- Native-tool guidance sections (`tool:read`, `tool:write`, `tool:bash`, etc.) remain in the system prompt because they describe capabilities available through the generated SDK as well as native function calls, and several carry cross-tool routing policy (`read` over `bash cat`, `read` before `write` for the default fs-observation-policy, `subagent` over `workflow`) that no single tool description can hold. The executor collapse, not prompt filtering, prevents model-direct native calls.
|
||||
- The prompt STATES the collapse, in the `tools:code-only` section ordered ahead of the 100-199 guidance band. Those sections name their tool without qualifying how it is reached, so a model that read only them emitted a native call, received `UNKNOWN_TOOL` for a tool the same prompt declared, and concluded the deployment was inconsistent rather than correcting itself. The denial carries the route for the same reason. `both` renders the rule empty: its native calls do execute, so stating it there would be false — which is why `both-mode-turn` no longer shares `code-mode-turn`'s expected prompt.
|
||||
- The prompt STATES the collapse, in the `tools:code-only` section ordered ahead of first-party per-tool guidance. Those sections name their tool without qualifying how it is reached, so a model that read only them emitted a native call, received `UNKNOWN_TOOL` for a tool the same prompt declared, and concluded the deployment was inconsistent rather than correcting itself. The denial carries the route for the same reason. `both` renders the rule empty: its native calls do execute, so stating it there would be false — which is why `both-mode-turn` no longer shares `code-mode-turn`'s expected prompt.
|
||||
- Any future composite transport that sets a `parent` token opts its sub-dispatches into the full table, matching the nested-call semantics the token already documents.
|
||||
|
||||
@@ -42,5 +42,5 @@ guard 是可选的插件扩展;安全不变量不能依赖部署恰好组装
|
||||
- `both` 与 `native` 行为不变;SDK 子调用不变(判别信号是 `parent` token)。
|
||||
- 被塌缩的调用在 `prepare` 阶段即被拒绝——在可扩展策略流水线之前:pre-execute 监听器、approval `ask` 与 guard 永远不会观察到它。`executionMode` 同样 fail-closed(`exclusive`),调度无可观察差异。
|
||||
- 原生工具指引段(`tool:read`、`tool:write`、`tool:bash` 等)保留在系统提示词中,因为它们同时描述了通过生成 SDK 及原生函数调用可用的能力,其中若干段还承载着任何单个工具描述都装不下的跨工具路由策略(`read` 优先于 `bash cat`、默认 fs-observation-policy 要求先 `read` 再 `write`、一两个委派用 `subagent` 而非 `workflow`)。防止模型直呼原生工具的是执行器塌缩,而非提示词过滤。
|
||||
- 提示词会**声明**这条塌缩,位于排在 100–199 指导段之前的 `tools:code-only` 段。那些段只写出工具名而不限定其可达方式,因此只读到它们的模型会发出原生调用,为一个同一份提示词刚刚声明过的工具收到 `UNKNOWN_TOOL`,进而判定部署不一致,而不是自行纠正。拒绝信息给出正确路径也是同一原因。`both` 下该规则渲染为空:它的原生调用确实会执行,在那里声明就是假话——这也是 `both-mode-turn` 不再与 `code-mode-turn` 共用期望提示词的原因。
|
||||
- 提示词会**声明**这条塌缩,位于 first-party 逐工具指导之前的 `tools:code-only` 段。那些段只写出工具名而不限定其可达方式,因此只读到它们的模型会发出原生调用,为一个同一份提示词刚刚声明过的工具收到 `UNKNOWN_TOOL`,进而判定部署不一致,而不是自行纠正。拒绝信息给出正确路径也是同一原因。`both` 下该规则渲染为空:它的原生调用确实会执行,在那里声明就是假话——这也是 `both-mode-turn` 不再与 `code-mode-turn` 共用期望提示词的原因。
|
||||
- 未来任何设置 `parent` token 的组合传输,其子调用自动走全表,与该 token 已有的嵌套调用语义一致。
|
||||
|
||||
@@ -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-23-web-assistant-markdown.md
|
||||
2026-07-23-web-assistant-markdown.md: 0ad74546b9a54d5eadfc3e7991efa4e0d9b7bc77
|
||||
2026-07-23-web-assistant-markdown.zh.md: 9349847228b10c13ecead4374ad2fc9210f5e9f1
|
||||
2026-07-23-web-assistant-markdown.md: d2b8e30d779656636f70b05524c96796a254b57b
|
||||
2026-07-23-web-assistant-markdown.zh.md: c1542d75faf1b484160f98b4217164b5df4e4b99
|
||||
|
||||
@@ -12,7 +12,7 @@ The Web conversation preserves assistant Markdown source through session events,
|
||||
|
||||
`@deepseek-ai/dsh-client-ui-primitives` exports `MarkdownText` as the untrusted assistant-text renderer, and `ui-conversation` selects it only for assistant `text` blocks. Finalized history, the streaming tail, and interrupted partials already share `AssistantMarkdown`, so they receive the same renderer without changing events or snapshots. User and steering messages keep `MessageText` and remain literal.
|
||||
|
||||
`MarkdownText` parses with `mdast-util-from-markdown` plus the GFM micromark extensions and renders the mdast tree through the package's own renderer, parsing incrementally while a turn streams (the [incremental AST renderer note](../architecture/2026-08-06-web-markdown-incremental-ast-renderer.md) owns that mechanism and its DOM-parity contract). It covers CommonMark blocks plus GFM tables, task lists, strikethrough, and autolinks without raw-HTML parsing. A micromark attention extension reuses the CommonMark resolver while letting runs of at least two asterisks close after Unicode punctuation when followed immediately by CJK text. This exception covers punctuation-terminated strong emphasis in whitespace-free CJK prose during streaming and after settlement; single-asterisk emphasis, non-CJK adjacency, escaped source, code, and math retain upstream parsing. Fenced code routes through the shared `CodeBlock`, which highlights registered grammars with the client's shiki singleton (`--shiki-*` tokens) and falls back to plain monospace otherwise. While a turn streams, fences stay on the plain arm so growing fences are not retokenized every chunk.
|
||||
`MarkdownText` parses with `mdast-util-from-markdown` plus the GFM micromark extensions and renders the mdast tree through the package's own renderer, parsing incrementally while a turn streams (the [incremental AST renderer note](../architecture/2026-08-06-web-markdown-incremental-ast-renderer.md) owns that mechanism and its DOM-parity contract). It covers CommonMark blocks plus GFM tables, task lists, strikethrough, and autolinks without raw-HTML parsing. A micromark attention extension reuses the CommonMark resolver while letting runs of at least two asterisks close after Unicode punctuation when followed immediately by CJK text. This exception covers punctuation-terminated strong emphasis in whitespace-free CJK prose during streaming and after settlement; single-asterisk emphasis, non-CJK adjacency, escaped source, code, and math retain upstream parsing. Fenced code routes through the shared `CodeBlock`, which highlights registered grammars with the client's shiki singleton (`--shiki-*` tokens) and falls back to plain monospace otherwise. While a turn streams, fences highlight incrementally: each chunk tokenizes newly completed text from a saved grammar state plus the still-growing last line, excluding the completed prefix from repeated work (the [streaming fence-highlight note](2026-08-20-web-streaming-fence-highlight.md) owns that mechanism).
|
||||
|
||||
Visual spacing, tables, links, blockquotes, inline code, and code-block chrome follow deepsuite `@deepseek/md` (`markdown.css` / `code-block.css`) and the same `--dsw-alias-markdown-*`, `--dsw-font-markdown-*`, `--dsw-alias-border-l*`, and `--dsw-alias-label-*` tokens. Links use `--dsw-alias-state-business-primary` (deepsuite's sheet uses `--dsw-alias-brand-text`, which is blue only under newDesign; design-platform keeps brand-text near-black and is not retuned here). When one inline-code token consists entirely of an absolute HTTP(S) URL, its code chrome contains the same keyboard-focusable safe external anchor as an ordinary link; port, path, and query text remain unchanged, while commands, partial URLs, other schemes, and fenced code stay inert. `CodeBlock` ships a language banner and a copy control (`复制` / `复制成功`). Finalized text renders KaTeX through the settled grammar's math extensions; `mathCompatibility` maps `\(...\)`, `\[...\]`, and block-level same-line `$$...$$` to the same standard math AST nodes. This is a narrow parser compatibility layer, not a regex rewrite or malformed-model-output repair. Streaming stays literal until finalization so incomplete formulae do not flash errors. Citation pills, heading anchors, the thinking-small markdown variant, and custom □/☑ task markers remain out of scope; GFM task lists keep native checkboxes.
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Web 对话通过会话事件、历史回放与流式累积保留 assistant Markd
|
||||
|
||||
`@deepseek-ai/dsh-client-ui-primitives` 导出 `MarkdownText`,用作不受信任的 assistant 文本渲染器;`ui-conversation` 仅为 assistant `text` 块选择该渲染器。已完成的历史消息、流式输出尾部与被中断的部分输出已经共用 `AssistantMarkdown`,因此无需更改事件或快照,它们便会采用同一渲染器。用户消息与 steering 消息继续使用 `MessageText`,并保持按字面渲染。
|
||||
|
||||
`MarkdownText` 以 `mdast-util-from-markdown` 加 GFM micromark 扩展解析,并经包内自有渲染器渲染 mdast 树,轮次流式输出期间增量解析([增量 AST 渲染器 Note](../architecture/2026-08-06-web-markdown-incremental-ast-renderer.zh.md) 拥有该机制及其 DOM 一致性约定)。它覆盖 CommonMark 块,以及 GFM 表格、任务列表、删除线与自动链接,且不解析原始 HTML。一个 micromark attention 扩展复用 CommonMark resolver,同时允许至少两个星号组成的连续序列在 Unicode 标点后闭合,前提是其后紧邻 CJK 文本。这一例外涵盖流式输出期间与完成后无空格 CJK 文本中以标点结尾的粗体;单星号强调、紧邻非 CJK 文本的情况、已转义源文本、代码与数学公式仍沿用上游解析行为。围栏代码经共享的 `CodeBlock` 路由;该组件用客户端的 shiki 单例(`--shiki-*` token)高亮已注册语法,否则回退为纯等宽文本。轮次流式输出期间,围栏停留在纯文本分支,以免每收到一个分片就对增长中的围栏重新分词。
|
||||
`MarkdownText` 以 `mdast-util-from-markdown` 加 GFM micromark 扩展解析,并经包内自有渲染器渲染 mdast 树,轮次流式输出期间增量解析([增量 AST 渲染器 Note](../architecture/2026-08-06-web-markdown-incremental-ast-renderer.zh.md) 拥有该机制及其 DOM 一致性约定)。它覆盖 CommonMark 块,以及 GFM 表格、任务列表、删除线与自动链接,且不解析原始 HTML。一个 micromark attention 扩展复用 CommonMark resolver,同时允许至少两个星号组成的连续序列在 Unicode 标点后闭合,前提是其后紧邻 CJK 文本。这一例外涵盖流式输出期间与完成后无空格 CJK 文本中以标点结尾的粗体;单星号强调、紧邻非 CJK 文本的情况、已转义源文本、代码与数学公式仍沿用上游解析行为。围栏代码经共享的 `CodeBlock` 路由;该组件用客户端的 shiki 单例(`--shiki-*` token)高亮已注册语法,否则回退为纯等宽文本。轮次流式输出期间,围栏增量高亮:每个分片从保存的 grammar state 出发 tokenize 新完成的文本以及仍在增长的最后一行,不重复处理已完成的前缀([流式围栏高亮 Note](2026-08-20-web-streaming-fence-highlight.zh.md) 拥有该机制)。
|
||||
|
||||
视觉间距、表格、链接、引用块、行内代码与代码块外框遵循 deepsuite `@deepseek/md`(`markdown.css` / `code-block.css`),并使用同一套 `--dsw-alias-markdown-*`、`--dsw-font-markdown-*`、`--dsw-alias-border-l*` 与 `--dsw-alias-label-*` token。链接使用 `--dsw-alias-state-business-primary`(deepsuite 的样式表使用 `--dsw-alias-brand-text`,仅在 newDesign 下为蓝色;design-platform 将 brand-text 保持为近黑色,此处不做重新调色)。当单个行内代码 token 完全由绝对 HTTP(S) URL 构成时,其代码外框会包含一个与普通链接相同、可通过键盘聚焦的安全外链锚点;端口、路径与查询文本保持不变,而命令、非完整 URL、其他 scheme 与围栏代码仍不会成为链接。`CodeBlock` 提供语言横幅与复制控件(`复制` / `复制成功`)。已完成的文本通过定稿语法的数学扩展渲染 KaTeX;`mathCompatibility` 将 `\(...\)`、`\[...\]` 和块级同一行 `$$...$$` 映射为同一套标准数学 AST 节点。这是一层小范围的解析器兼容层,不是正则重写,也不修复格式错误的模型输出。流式输出在完成前保持按字面渲染,避免不完整公式闪现错误。引用胶囊、标题锚点、thinking-small markdown 变体,以及自定义 □/☑ 任务标记仍不在范围内;GFM 任务列表继续使用原生复选框。
|
||||
|
||||
|
||||
+2
-2
@@ -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-08-06-continuable-child-report-obligation.md
|
||||
2026-08-06-continuable-child-report-obligation.md: e771e81831147dd02a6c32a543c8d8944c2ec2f4
|
||||
2026-08-06-continuable-child-report-obligation.zh.md: e1cbb38644889459d8b8785382e1923a80c81957
|
||||
2026-08-06-continuable-child-report-obligation.md: 422d3ab74389e084becb75a145a82139dda38ac7
|
||||
2026-08-06-continuable-child-report-obligation.zh.md: ec7e19885204ddc99b2640407c605288f1e9c045
|
||||
|
||||
@@ -15,7 +15,7 @@ Each of those choices is defensible alone. Together they made the return channel
|
||||
The return channel is an instruction the child receives, not a capability it may discover. The report package installs two scope-local registrations into every continuable in-process child, and one disposer revokes both:
|
||||
|
||||
- the `report` tool, whose description now states that the child calls it once before finishing with a self-contained final result, and earlier for progress that changes what the parent should do next;
|
||||
- a `tool:report` system-prompt section at order 117 carrying the same obligation in the child's own voice, so a child that never reads tool descriptions closely still receives it.
|
||||
- a `tool:report` system-prompt section at first-party order 2900 carrying the same obligation in the child's own voice, so a child that never reads tool descriptions closely still receives it.
|
||||
|
||||
`reportDelivery` defaults to `next-step`. An accepted report wakes a parked parent driver or joins a running parent's nearest step boundary, matching the instruction to report findings that change the parent's next action. `quiet` remains available for deployments that prefer unread reports over model-work amplification. The [report/settlement ordering decision](../bug-fix/2026-08-17-subagent-report-settlement-ordering.md) owns the scheduling rationale.
|
||||
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ Status: implemented
|
||||
返回通道是 child 收到的一条指令,而不是它需要自行发现的能力。report 包会向每个可继续进程内 child 安装两项作用域局部注册,并由同一个 disposer 撤销两者:
|
||||
|
||||
- `report` 工具,其描述现在说明 child 要在结束前调用一次并给出自足的最终结果,并在部分进展会改变 parent 下一步动作时提前调用;
|
||||
- 一个 order 为 117 的 `tool:report` 系统提示词 section,用 child 自己的语气承载同一条义务,使从不细读工具描述的 child 仍能收到它。
|
||||
- 一个 first-party order 为 2900 的 `tool:report` 系统提示词 section,用 child 自己的语气承载同一条义务,使从不细读工具描述的 child 仍能收到它。
|
||||
|
||||
`reportDelivery` 的默认值为 `next-step`。一条被接受的报告会唤醒停驻的 parent driver,或加入运行中 parent 最近的 step 边界,与发现会改变 parent 下一步动作时上报的指令一致。对于宁可让报告无人阅读也要避免模型工作量放大的部署,`quiet` 依旧可用。[报告与结算顺序决策](../bug-fix/2026-08-17-subagent-report-settlement-ordering.zh.md)负责调度理由。
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-20-web-streaming-fence-highlight.md
|
||||
2026-08-20-web-streaming-fence-highlight.md: ccc961da1febba3611e3e558087b586c6f1f474b
|
||||
2026-08-20-web-streaming-fence-highlight.zh.md: e5d29545bf659ed572d052f1212d68d344d49b5a
|
||||
@@ -0,0 +1,37 @@
|
||||
# Agent Note: Streaming fences highlight incrementally
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-20-web-streaming-fence-highlight.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
While a reply streamed, `MarkdownText` stripped the fence language before `CodeBlock` saw it, so code rendered as plain monospace with an empty language banner until the finalize swap recolored the whole reply at once ([#1499](https://github.com/deepseek-harness/deepseek-harness/issues/1499)). The plain arm was a deliberate cost guard, recorded in the [assistant-markdown note](2026-07-23-web-assistant-markdown.md): shiki tokenizes a document from the top, so highlighting a growing fence naively re-tokenizes the whole fence on every chunk — quadratic in fence length over the stream, the same cost class the [incremental markdown parser](../architecture/2026-08-06-web-markdown-incremental-ast-renderer.md) removes for block parsing. The fix has to deliver highlighting during streaming without reintroducing that cost, without transiently coloring under a wrong grammar while the info string is still mid-chunk, and without changing the settled render.
|
||||
|
||||
## Decision
|
||||
|
||||
Streaming fences highlight incrementally through grammar-state resumption; the settled arm is unchanged.
|
||||
|
||||
- **`StreamingHighlightSession`** (`packages/client/ui-primitives/src/markdown/highlight.ts`) exploits that TextMate tokenization is line-based and forward-only: a line's tokens depend only on its own text and the grammar state entering it, so appended text never changes a completed line's tokens. The session caches completed lines' spans plus shiki's `GrammarState` after them (`getLastGrammarState`), and each update tokenizes newly completed text via `codeToTokensBase(…, { grammarState })` plus the still-growing last line. Per-chunk cost excludes the completed prefix; the result is token-identical to a from-scratch tokenization. Non-append input and a resolved-grammar change reset the cache and re-tokenize fully. Each run carries the style shiki's HTML arm would assign it — the css-variables color plus the markup font-style bits the theme lets through (bold/italic/underline; markdown fences carry them); whitespace-only runs fold into their following token as shiki's default `mergeWhitespaces` does (its underlined/struck-whitespace exemption cannot occur under this theme, whose only underline rule styles inline-link scopes that tokenize spaced text as one run); and a CRLF cut never leaks its `\r` into the last completed line, matching shiki's own line splitting — so the streaming spans and the settled `codeToHtml` swap render one identical span tree.
|
||||
- **`CodeBlock`** gains a `streaming` prop: it renders the session's spans as a `pre.shiki.css-variables` React tree with the same attributes shiki's HTML emits, holds the session and per-line elements in refs, and reuses a retained line's element identity so React leaves that line's DOM untouched. Unknown or absent languages keep the identical-geometry plain arm; a lazy grammar renders plain until it registers, then the existing `useSyncExternalStore` load signal re-renders into highlight — one plain→highlighted transition, no flicker back.
|
||||
- **`render.tsx`** passes `lang` and `context.streaming` to fences. Wrong-grammar transients are structurally impossible: a fence whose info string is still mid-chunk (`` ```py `` completing to `` ```python ``) has no content yet — content only exists after the info line's newline, which finalizes the language — and the empty-value fence keeps the stock `<pre>`. The streaming CodeBlock instance survives every chunk because streaming render keys are source offsets. `` ```math `` fences and TeX stay literal until the settled pass; the language banner shows the fence language during streaming.
|
||||
|
||||
The settle swap re-renders through `highlightToHtml`: same tokens, same span tree, so the swap is visually invisible and never touches the code content.
|
||||
|
||||
## Testing
|
||||
|
||||
Package tests cover incremental/from-scratch equivalence across multiline grammar state, blank lines, CRLF, and markup styles; cache identity and reset/lazy paths; streaming/settled token-tree parity; DOM retention; and plain or math fallbacks. The assembled Web browser snapshot boots the real Web composition, streams a TypeScript fence through the Host and SSE path, pauses the deterministic LLM adapter while the reply is still active, and snapshots Chromium's Shiki token tree before verifying that settlement preserves it. The `tests/fixtures/markdown-dom/*.streaming.txt` fixtures pin the intentional streaming divergence from their react-markdown origin: the Shiki span tree and visible language banner replace the plain arm.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Pass `lang` through and re-tokenize the whole fence per chunk.** One-line fix, but it reverses the recorded plain-arm rationale without addressing it: a long streaming fence pays quadratic tokenization over the stream, janking exactly on the replies where highlighting matters most.
|
||||
|
||||
**Highlight only frozen (closed, settled-position) fences during streaming.** Bounded cost, but an unclosed fence pins the incremental parser's tail, so the actively growing fence — the one on screen — would stay plain until the reply finishes, failing the issue's "识别语言后即可增量高亮".
|
||||
|
||||
**Move highlighting to a worker or async pass.** Rejected when shiki was adopted ([synchronous highlighting note](../process/2026-07-26-web-syntax-highlighting-shiki.md)); an async swap also reintroduces the plain→colored→plain flicker class this change must avoid.
|
||||
|
||||
**Build the settled HTML string incrementally and keep `dangerouslySetInnerHTML`.** Exact settled parity for free, but React replaces the whole `innerHTML` per chunk, so the browser re-parses and rebuilds every line's DOM each time — O(fence) DOM churn that forfeits the token-level win the session provides.
|
||||
|
||||
## Consequences
|
||||
|
||||
Streaming code is readable as it arrives: tokens color as soon as the language is known, completed lines never re-tokenize or re-render, and the finalize swap is invisible for fences. The package owns a small mirror of shiki's HTML-arm conventions — the `pre` attributes and the whitespace fold — pinned by the arm-parity test, so a shiki upgrade that changes either fails loud there instead of drifting the two arms apart. The streaming DOM-parity fixtures pin Shiki span trees as an intentional divergence from their react-markdown origin. The still-growing last line re-tokenizes per chunk (bounded by one line), and a pathological single-line fence still degrades to full re-tokenization per chunk — the same degradation class the incremental block parser accepts for a single giant block.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Agent Note: 流式围栏代码增量高亮
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-20-web-streaming-fence-highlight.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
回复流式输出期间,`MarkdownText` 在 `CodeBlock` 看到围栏语言之前就把它剥掉,代码因此以无高亮的等宽纯文本呈现、语言横幅为空,直到定稿切换一次性重新着色整个回复([#1499](https://github.com/deepseek-harness/deepseek-harness/issues/1499))。纯文本臂是一道刻意的成本防线,记录于 [assistant-markdown 笔记](2026-07-23-web-assistant-markdown.zh.md):shiki 从文档顶部开始 tokenize,朴素地高亮一个增长中的围栏意味着每个分片都重新 tokenize 整个围栏——随流式过程对围栏长度呈平方级,与[增量 markdown 解析器](../architecture/2026-08-06-web-markdown-incremental-ast-renderer.zh.md)为块解析消除的是同一类成本。修复必须在流式期间给出高亮,同时不重新引入该成本、不在 info string 尚在分片中途时以错误语法短暂着色、也不改变定稿渲染。
|
||||
|
||||
## Decision
|
||||
|
||||
流式围栏通过 grammar state 续接实现增量高亮;定稿臂保持不变。
|
||||
|
||||
- **`StreamingHighlightSession`**(`packages/client/ui-primitives/src/markdown/highlight.ts`)利用 TextMate tokenize 按行、且只向前推进的性质:一行的 token 只取决于该行文本与进入该行时的 grammar state,因此追加的文本永远不会改变已完成行的 token。会话缓存已完成行的 span 以及其后的 shiki `GrammarState`(`getLastGrammarState`),每次更新通过 `codeToTokensBase(…, { grammarState })` tokenize 新完成的文本,外加仍在增长的最后一行。每分片成本不包含已完成的前缀;结果与从头 tokenize 逐 token 一致。非追加输入与解析后语法变化会重置缓存并完整重新 tokenize。每个 run 携带 shiki HTML 臂会赋予它的样式——css-variables 颜色加上主题放行的 markup 字体位(bold/italic/underline;markdown 围栏会携带它们);纯空白 run 并入其后的 token,与 shiki 默认的 `mergeWhitespaces` 一致(其对带下划线/删除线空白的豁免在该主题下不可能出现:主题唯一的 underline 规则作用于 inline-link scope,其含空格文本整体成一个 run);CRLF 切割点的 `\r` 绝不进入最后一个已完成行,与 shiki 自身的行切分一致——因此流式 span 与定稿 `codeToHtml` 换入的 span 树完全一致。
|
||||
- **`CodeBlock`** 新增 `streaming` prop:把会话的 span 渲染为带有 shiki HTML 同款属性的 `pre.shiki.css-variables` React 树,用 ref 持有会话与逐行元素,并复用保留行的元素标识,让 React 完全不触碰该行的 DOM。未知或缺失语言保持几何一致的纯文本臂;懒加载语法在注册前渲染纯文本,注册后由既有的 `useSyncExternalStore` 加载信号触发重渲染进入高亮——只有一次纯文本→高亮的转换,不会闪回。
|
||||
- **`render.tsx`** 向围栏传递 `lang` 与 `context.streaming`。错误语法的瞬时着色在结构上不可能出现:info string 尚在分片中途的围栏(`` ```py `` 补全为 `` ```python ``)还没有内容——内容只在 info 行的换行之后才存在,而该换行恰恰定格了语言——空值围栏保持原生 `<pre>`。流式渲染 key 是源偏移,围栏的 CodeBlock 实例因此跨分片存活。`` ```math `` 围栏与 TeX 在定稿前保持字面量;语言横幅在流式期间显示围栏语言。
|
||||
|
||||
定稿切换经 `highlightToHtml` 重渲染:token 相同、span 树相同,切换在视觉上不可见,也绝不触碰代码内容。
|
||||
|
||||
## Testing
|
||||
|
||||
包测试覆盖跨多行 grammar state、空行、CRLF 与 markup 样式的增量/从头等价性,缓存标识与重置/懒加载路径,流式/定稿 token 树一致性,DOM 保留,以及纯文本和 math 回退。组装后的 Web 浏览器快照会启动真实 Web 组合,让 TypeScript 围栏经过 Host 与 SSE 路径流式传输,在回复仍活跃时暂停确定性 LLM 适配器并对 Chromium 中的 Shiki token 树做快照,然后验证定稿保留该 token 树。`tests/fixtures/markdown-dom/*.streaming.txt` fixture 锁定相对 react-markdown 来源的一项有意分叉:Shiki span 树与可见语言横幅取代纯文本臂。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**直接透传 `lang`,每个分片重新 tokenize 整个围栏。** 一行改动,但在不回应的情况下推翻了已记录的纯文本臂理由:长流式围栏在整个流式过程付出平方级 tokenize 成本,恰恰在高亮最有价值的长代码回复上产生卡顿。
|
||||
|
||||
**流式期间只高亮已冻结(闭合且位置定格)的围栏。** 成本有界,但未闭合围栏会钉住增量解析器的尾部,于是正在增长的围栏——屏幕上的那个——要等回复结束才高亮,不满足 issue 的"识别语言后即可增量高亮"。
|
||||
|
||||
**把高亮移到 worker 或异步流程。** 采纳 shiki 时已否决([同步高亮笔记](../process/2026-07-26-web-syntax-highlighting-shiki.zh.md));异步换入还会重新引入本变更必须避免的纯文本→彩色→纯文本闪烁类问题。
|
||||
|
||||
**增量拼接定稿 HTML 字符串并继续使用 `dangerouslySetInnerHTML`。** 白得定稿一致性,但 React 每个分片都会整体替换 `innerHTML`,浏览器每次重新解析并重建所有行的 DOM——O(围栏) 的 DOM 翻搅,抵消了会话在 token 层的收益。
|
||||
|
||||
## Consequences
|
||||
|
||||
流式代码随到达即可读:语言一经识别 token 即着色,已完成行绝不重新 tokenize 或重渲染,定稿切换对围栏而言不可见。该包持有一小份 shiki HTML 臂约定的镜像——`pre` 属性与空白折叠——由双臂一致性测试锁定,shiki 升级若改变任一处会在该测试处响亮失败,而不是让两臂悄然漂移。流式 DOM 一致性 fixture 锁定 Shiki span 树,这是相对其 react-markdown 来源的一项有意分叉。仍在增长的最后一行每分片重新 tokenize(以一行为界);病态的单行超长围栏仍退化为每分片完整重新 tokenize——与增量块解析器对单个巨型块接受的是同一退化类。
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-07-22-plan-specific-collaboration-state.md
|
||||
2026-07-22-plan-specific-collaboration-state.md: c2b882bd6f686662a2369d1b76d6e1611caee9b9
|
||||
2026-07-22-plan-specific-collaboration-state.zh.md: 1fdb4b260853be7480f1575d6ded86d036b7d746
|
||||
2026-07-22-plan-specific-collaboration-state.md: 20c363d00cfe2bcc2f01af101370f5214c948361
|
||||
2026-07-22-plan-specific-collaboration-state.zh.md: 63157b1959236e728e94a38d7ca6397a975595ef
|
||||
|
||||
+2
-2
@@ -26,7 +26,7 @@ Sandbox mode and approval policy remain separate enforcement axes. Plan mode nei
|
||||
|
||||
`plan/mode` is log-only and non-surface, so resume, fork, and compaction recover the state without a live mirror. A spawned agent begins inactive because there is no creation-time plan option. Pending user selections flush before the affected request assembly at initial or continuation pre-step, or on a request-recovery retry; a failed durable append leaves the intent pending for a later boundary.
|
||||
|
||||
The active state contributes the deployment's section at prompt order 50. Inactive state contributes no section, while `exit_plan_mode` remains registered in both states, so a transition changes the logged request header but not native tool schemas or the Code Mode SDK. A user-driven transition appends one plugin-sourced notice only when the last request header described the opposite state; a pre-first-request or net-zero selection adds none, and an approved tool exit relies on its tool result instead of a second notice.
|
||||
The active state contributes the deployment's section at first-party prompt order 500. Inactive state contributes no section, while `exit_plan_mode` remains registered in both states, so a transition changes the logged request header but not native tool schemas or the Code Mode SDK. A user-driven transition appends one plugin-sourced notice only when the last request header described the opposite state; a pre-first-request or net-zero selection adds none, and an approved tool exit relies on its tool result instead of a second notice.
|
||||
|
||||
### Reviewed exit
|
||||
|
||||
@@ -68,4 +68,4 @@ The tool renders the submitted plan as a generic card titled by its first headin
|
||||
|
||||
The implementation has one vocabulary for one shipped feature. Adding another collaboration stance is an explicit design decision instead of a config entry, and automation clients do not acquire human mode controls through ACP. The migration intentionally rejects old `mode/set` logs and old `modes.plan.section` configuration under the repository's pre-release format policy.
|
||||
|
||||
Plan state remains reconstructable and tool schemas remain stable, but an idle pending selection is lost if the process exits before the next boundary. Entering or leaving plan mode changes the prompt from order 50 onward, and a model that ignores the guidance can still mutate unless the deployment independently configures sandbox, approval, or filesystem policy.
|
||||
Plan state remains reconstructable and tool schemas remain stable, but an idle pending selection is lost if the process exits before the next boundary. Entering or leaving plan mode changes the prompt from first-party order 500 onward, and a model that ignores the guidance can still mutate unless the deployment independently configures sandbox, approval, or filesystem policy.
|
||||
|
||||
+2
-2
@@ -26,7 +26,7 @@ Plan mode 拥有一个 plan 专用产品包:位于 `packages/plan/plan-mode/`
|
||||
|
||||
`plan/mode` 仅记录到日志且不进入表层,因此恢复、fork 和压缩(compaction)都能恢复该状态,无需实时镜像。spawn 出的 agent(智能体)初始处于未激活状态,因为创建时没有 plan 选项。待生效的用户选择会在初始或续步 pre-step 时,或在请求恢复重试时,于受影响的请求组装前写入日志;持久追加失败会让意图保持待定,留到后续边界处理。
|
||||
|
||||
激活状态在提示词顺序 50 处贡献部署提供的区段。未激活状态不贡献区段,但 `exit_plan_mode` 在两种状态下都保持注册,因此状态转换会改变已记录的请求头,却不改变原生工具 schema 或 Code Mode SDK。用户发起的转换只会在上一条请求头描述相反状态时追加一条来源为插件的通知;第一次请求前的选择或最终状态未变化的选择不会追加通知,经批准的工具退出则依赖其工具结果,不再追加第二条通知。
|
||||
激活状态在 first-party 提示词顺序 500 处贡献部署提供的区段。未激活状态不贡献区段,但 `exit_plan_mode` 在两种状态下都保持注册,因此状态转换会改变已记录的请求头,却不改变原生工具 schema 或 Code Mode SDK。用户发起的转换只会在上一条请求头描述相反状态时追加一条来源为插件的通知;第一次请求前的选择或最终状态未变化的选择不会追加通知,经批准的工具退出则依赖其工具结果,不再追加第二条通知。
|
||||
|
||||
### 经评审的退出
|
||||
|
||||
@@ -68,4 +68,4 @@ Plan mode 拥有一个 plan 专用产品包:位于 `packages/plan/plan-mode/`
|
||||
|
||||
该实现只用一套词汇描述一项已交付功能。若要添加另一种协作方式,必须显式作出设计决策,而不能只增加配置项;自动化客户端不会通过 ACP 获得面向人类的模式控制。根据仓库的预发布格式策略,本次迁移有意拒绝旧的 `mode/set` 日志与 `modes.plan.section` 配置。
|
||||
|
||||
Plan 状态仍可重建,工具 schema 仍保持稳定,但如果进程在下一边界前退出,空闲状态下待生效的选择会丢失。进入或离开 plan mode 会改变提示词顺序 50 处及其后的内容;如果模型忽略引导,仍可能执行修改,除非部署另行配置沙箱、审批或文件系统策略。
|
||||
Plan 状态仍可重建,工具 schema 仍保持稳定,但如果进程在下一边界前退出,空闲状态下待生效的选择会丢失。进入或离开 plan mode 会改变 first-party 提示词顺序 500 处及其后的内容;如果模型忽略引导,仍可能执行修改,除非部署另行配置沙箱、审批或文件系统策略。
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- button "Stream one TypeScript fence for" [disabled]
|
||||
- img
|
||||
- text: Standard mode
|
||||
- button "Session log":
|
||||
- text: Session log
|
||||
- img
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- text: Stream one TypeScript fence for the highlighting snapshot. {{clock}}
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "Context injection @deepseek-ai/dsh-system-prompt":
|
||||
- img
|
||||
- img
|
||||
- text: Context injection @deepseek-ai/dsh-system-prompt
|
||||
- text: ts
|
||||
- button "Copy"
|
||||
- code: "const first: number = 1 const second = \"two\" let tail"
|
||||
- status: Deep diving...
|
||||
- textbox "Message the agent"
|
||||
- button "Commands":
|
||||
- img
|
||||
- 'button "Access mode, current: Workspace Write"': Workspace Write
|
||||
- button "Select model, current streaming-fence-highlight-test/streaming-fence":
|
||||
- text: streaming-fence-highlight-test/streaming-fence
|
||||
- img
|
||||
- button "Stop generating"
|
||||
|
||||
---
|
||||
|
||||
{
|
||||
"language": "ts",
|
||||
"pre": {
|
||||
"className": "shiki css-variables",
|
||||
"style": "background-color: var(--shiki-background); color: var(--shiki-foreground);",
|
||||
"tabIndex": "0"
|
||||
},
|
||||
"lines": [
|
||||
[
|
||||
{
|
||||
"text": "const",
|
||||
"style": "color: var(--shiki-token-keyword);"
|
||||
},
|
||||
{
|
||||
"text": " first",
|
||||
"style": "color: var(--shiki-token-constant);"
|
||||
},
|
||||
{
|
||||
"text": ":",
|
||||
"style": "color: var(--shiki-token-keyword);"
|
||||
},
|
||||
{
|
||||
"text": " number",
|
||||
"style": "color: var(--shiki-token-constant);"
|
||||
},
|
||||
{
|
||||
"text": " =",
|
||||
"style": "color: var(--shiki-token-keyword);"
|
||||
},
|
||||
{
|
||||
"text": " 1",
|
||||
"style": "color: var(--shiki-token-constant);"
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"text": "const",
|
||||
"style": "color: var(--shiki-token-keyword);"
|
||||
},
|
||||
{
|
||||
"text": " second",
|
||||
"style": "color: var(--shiki-token-constant);"
|
||||
},
|
||||
{
|
||||
"text": " =",
|
||||
"style": "color: var(--shiki-token-keyword);"
|
||||
},
|
||||
{
|
||||
"text": " \"two\"",
|
||||
"style": "color: var(--shiki-token-string-expression);"
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"text": "let",
|
||||
"style": "color: var(--shiki-token-keyword);"
|
||||
},
|
||||
{
|
||||
"text": " tail",
|
||||
"style": "color: var(--shiki-foreground);"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/** Keyless assembled-Web evidence for syntax highlighting during a streamed code fence. */
|
||||
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import type { Browser, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import { LlmAdapter } from '@deepseek-ai/dsh-llm'
|
||||
import type {} from '@deepseek-ai/dsh-agent-default-model'
|
||||
import {
|
||||
assertFixtureInventory,
|
||||
captureStableAria,
|
||||
compareOrRefreshGolden,
|
||||
launchWebScaffold,
|
||||
watchConsole,
|
||||
webSnapshotMode,
|
||||
type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/streaming-fence-highlight', import.meta.url))
|
||||
const MID_EXPECTED = fileURLToPath(new URL('./snapshots/streaming-fence-highlight/mid-stream.expected.md', import.meta.url))
|
||||
const MODE = webSnapshotMode()
|
||||
const PROVIDER = 'streaming-fence-highlight-test'
|
||||
const MODEL = 'streaming-fence'
|
||||
const PROMPT = 'Stream one TypeScript fence for the highlighting snapshot.'
|
||||
const OPEN_REPLY = '```ts\nconst first: number = 1\nconst second = "two"\nlet tail'
|
||||
const REPLY = `${OPEN_REPLY}\n\`\`\``
|
||||
|
||||
/** Deterministic model response held after the visible fence body arrives. */
|
||||
class StreamingFenceAdapter extends LlmAdapter {
|
||||
private resolvePaused!: () => void
|
||||
private resolveContinuation!: () => void
|
||||
private continued = false
|
||||
readonly paused = new Promise<void>((resolve) => { this.resolvePaused = resolve })
|
||||
private readonly continuation = new Promise<void>((resolve) => { this.resolveContinuation = resolve })
|
||||
|
||||
continue(): void {
|
||||
if (this.continued) return
|
||||
this.continued = true
|
||||
this.resolveContinuation()
|
||||
}
|
||||
|
||||
override async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
|
||||
yield { type: 'block-start', index: 0, blockType: 'text' }
|
||||
yield { type: 'text-delta', index: 0, text: OPEN_REPLY }
|
||||
this.resolvePaused()
|
||||
await this.continuation
|
||||
if (options.signal?.aborted === true) throw options.signal.reason
|
||||
yield { type: 'text-delta', index: 0, text: '\n```' }
|
||||
yield { type: 'block-end', index: 0, block: { type: 'text', text: REPLY } }
|
||||
yield { type: 'finish', reason: { kind: 'stop' } }
|
||||
}
|
||||
}
|
||||
|
||||
interface FenceTree {
|
||||
language: string
|
||||
pre: { className: string; style: string | null; tabIndex: string | null }
|
||||
lines: { text: string; style: string | null }[][]
|
||||
}
|
||||
|
||||
/** Read the stable, user-visible subset of one rendered code fence. */
|
||||
async function fenceTree(block: ReturnType<Page['locator']>): Promise<FenceTree> {
|
||||
return await block.evaluate((element) => {
|
||||
const pre = element.querySelector<HTMLPreElement>('pre.shiki')
|
||||
if (pre === null) throw new Error('streaming fence did not render through the shiki arm')
|
||||
return {
|
||||
language: element.querySelector('[class*="infostring"]')?.textContent ?? '',
|
||||
pre: {
|
||||
className: pre.className,
|
||||
style: pre.style.cssText,
|
||||
tabIndex: pre.getAttribute('tabindex'),
|
||||
},
|
||||
lines: [...pre.querySelectorAll('.line')].map(line =>
|
||||
[...line.querySelectorAll('span')].map(span => ({
|
||||
text: span.textContent ?? '',
|
||||
style: span.style.cssText,
|
||||
})),
|
||||
),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
describe.skipIf(MODE === 'record')('web e2e: streaming code-fence highlighting', () => {
|
||||
let scaffold: WebScaffold
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
const adapter = new StreamingFenceAdapter()
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold()
|
||||
scaffold.ctx.effect(
|
||||
() => scaffold.ctx.llm.registerAdapter([PROVIDER], adapter),
|
||||
'streaming fence highlight adapter',
|
||||
)
|
||||
await scaffold.ctx.agentDefaultModel.saveSelection({ provider: PROVIDER, model: MODEL })
|
||||
browser = await chromium.launch()
|
||||
page = await newEnglishPage(browser)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
await connectFreshWorkspace(page, scaffold.workspaceCwd)
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
adapter.continue()
|
||||
await browser?.close()
|
||||
await scaffold?.close()
|
||||
})
|
||||
|
||||
it('renders the growing fence through shiki and preserves its token tree when the turn settles', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-streaming-fence-highlight'))
|
||||
const input = page.locator('textarea').first()
|
||||
const settled = scaffold.whenTurnSettled(30_000)
|
||||
await input.fill(PROMPT)
|
||||
await input.press('Enter')
|
||||
await adapter.paused
|
||||
|
||||
const streaming = page.locator('[data-streaming="true"]')
|
||||
await streaming.waitFor({ timeout: 10_000 })
|
||||
const block = streaming.locator('.md-code-block').filter({ hasText: 'const first' })
|
||||
await block.locator('pre.shiki span[style]').first().waitFor({ timeout: 10_000 })
|
||||
const midTree = await fenceTree(block)
|
||||
expect(midTree.language).toBe('ts')
|
||||
expect(midTree.lines).toHaveLength(3)
|
||||
expect(midTree.lines.flat().map(span => span.style)).toContain('color: var(--shiki-token-keyword);')
|
||||
|
||||
const aria = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(
|
||||
MID_EXPECTED,
|
||||
`${aria}\n\n---\n\n${JSON.stringify(midTree, null, 2)}`,
|
||||
MODE,
|
||||
)
|
||||
|
||||
adapter.continue()
|
||||
await settled
|
||||
await expect.poll(() => page.locator('[data-streaming="true"]').count(), { timeout: 10_000 }).toBe(0)
|
||||
const settledBlock = page.locator('.md-code-block').filter({ hasText: 'const first' })
|
||||
await settledBlock.locator('pre.shiki').waitFor({ timeout: 10_000 })
|
||||
expect(await fenceTree(settledBlock)).toEqual(midTree)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, ['mid-stream.expected.md'])
|
||||
}, 60_000)
|
||||
})
|
||||
@@ -43,6 +43,7 @@
|
||||
"tests/models-settings.e2e.ts",
|
||||
"tests/default-model.e2e.ts",
|
||||
"tests/github-ready-review.e2e.ts",
|
||||
"tests/streaming-fence-highlight.e2e.ts",
|
||||
"tests/declared-reasoning.e2e.ts",
|
||||
"tests/onboarding-deepseek-config.e2e.ts",
|
||||
"tests/onboarding-usable-provider.e2e.ts",
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/config-catalog.md
|
||||
config-catalog.md: 57c92b35e24af4fd0d95865b47fdf8bb493960a4
|
||||
config-catalog.zh.md: d84c2e876072207630c6e512347ca61fa04587ff
|
||||
config-catalog.md: cf7be82f0cfeb393da8e44fb5ab503496419507c
|
||||
config-catalog.zh.md: c860ed9517ce9786603b8b50d870d1289fdfba8c
|
||||
|
||||
@@ -610,7 +610,7 @@ export interface Config {
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/experimental/tool-agent-team/src/index.ts:17`](../packages/experimental/tool-agent-team/src/index.ts)
|
||||
Source: [`packages/experimental/tool-agent-team/src/index.ts:18`](../packages/experimental/tool-agent-team/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-file-reference-local"></a>
|
||||
|
||||
@@ -2457,7 +2457,7 @@ export interface Config {
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/core/system-prompt/src/index.ts:186`](../packages/core/system-prompt/src/index.ts)
|
||||
Source: [`packages/core/system-prompt/src/index.ts:237`](../packages/core/system-prompt/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-terminal-bash"></a>
|
||||
|
||||
@@ -2777,7 +2777,7 @@ export interface Config {
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/workflow/tool-ralph/src/index.ts:23`](../packages/workflow/tool-ralph/src/index.ts)
|
||||
Source: [`packages/workflow/tool-ralph/src/index.ts:22`](../packages/workflow/tool-ralph/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-tool-session-query"></a>
|
||||
|
||||
@@ -2942,7 +2942,7 @@ export interface Config {
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/terminal/tool-terminal/src/index.ts:35`](../packages/terminal/tool-terminal/src/index.ts)
|
||||
Source: [`packages/terminal/tool-terminal/src/index.ts:36`](../packages/terminal/tool-terminal/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-tool-todo"></a>
|
||||
|
||||
@@ -3010,7 +3010,7 @@ export interface Config {
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/workflow/tool-workflow/src/index.ts:33`](../packages/workflow/tool-workflow/src/index.ts)
|
||||
Source: [`packages/workflow/tool-workflow/src/index.ts:32`](../packages/workflow/tool-workflow/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-tools"></a>
|
||||
|
||||
|
||||
@@ -612,7 +612,7 @@ export interface Config {
|
||||
}
|
||||
```
|
||||
|
||||
来源:[`packages/experimental/tool-agent-team/src/index.ts:17`](../packages/experimental/tool-agent-team/src/index.ts)
|
||||
来源:[`packages/experimental/tool-agent-team/src/index.ts:18`](../packages/experimental/tool-agent-team/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-file-reference-local"></a>
|
||||
|
||||
@@ -2459,7 +2459,7 @@ export interface Config {
|
||||
}
|
||||
```
|
||||
|
||||
来源:[`packages/core/system-prompt/src/index.ts:186`](../packages/core/system-prompt/src/index.ts)
|
||||
来源:[`packages/core/system-prompt/src/index.ts:237`](../packages/core/system-prompt/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-terminal-bash"></a>
|
||||
|
||||
@@ -2779,7 +2779,7 @@ export interface Config {
|
||||
}
|
||||
```
|
||||
|
||||
来源:[`packages/workflow/tool-ralph/src/index.ts:23`](../packages/workflow/tool-ralph/src/index.ts)
|
||||
来源:[`packages/workflow/tool-ralph/src/index.ts:22`](../packages/workflow/tool-ralph/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-tool-session-query"></a>
|
||||
|
||||
@@ -2944,7 +2944,7 @@ export interface Config {
|
||||
}
|
||||
```
|
||||
|
||||
来源:[`packages/terminal/tool-terminal/src/index.ts:35`](../packages/terminal/tool-terminal/src/index.ts)
|
||||
来源:[`packages/terminal/tool-terminal/src/index.ts:36`](../packages/terminal/tool-terminal/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-tool-todo"></a>
|
||||
|
||||
@@ -3012,7 +3012,7 @@ export interface Config {
|
||||
}
|
||||
```
|
||||
|
||||
来源:[`packages/workflow/tool-workflow/src/index.ts:33`](../packages/workflow/tool-workflow/src/index.ts)
|
||||
来源:[`packages/workflow/tool-workflow/src/index.ts:32`](../packages/workflow/tool-workflow/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-tools"></a>
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/module-graph.md
|
||||
module-graph.md: 1cca77ee09ce2a47e732893d814531daac3c07ad
|
||||
module-graph.zh.md: 46390d0c897f55c79bb183ea56deb2843fd80b94
|
||||
module-graph.md: 3fce90069441b683f449b4b16def95d8d9acc323
|
||||
module-graph.zh.md: f7431bd5a417ff6c5de4efe22a1d711611b1ffee
|
||||
|
||||
@@ -1060,6 +1060,7 @@ flowchart TD
|
||||
pkg_subagent --> pkg_session_projection
|
||||
pkg_subagent --> pkg_session_projection_cache
|
||||
pkg_subagent --> pkg_session_query
|
||||
pkg_subagent --> pkg_system_prompt
|
||||
pkg_subagent --> pkg_tools
|
||||
pkg_subagent --> pkg_user_approval
|
||||
pkg_session_query_sqlite --> pkg_invariants
|
||||
@@ -1825,7 +1826,7 @@ flowchart TD
|
||||
| [`tool-bash`](../packages/shell/tool-bash) | `shell` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| [`tool-pwsh`](../packages/shell/tool-pwsh) | `shell` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| [`webhook`](../packages/webhook/webhook) | `webhook` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-presets`](../packages/preset/agent-presets), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`workspace`](../packages/workspace/workspace) |
|
||||
| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query) |
|
||||
| [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
|
||||
| [`client-connection`](../packages/client/connection) | `client` | [`attachment`](../packages/attachment/attachment), [`commands`](../packages/interaction/commands), [`host-apiproxy`](../packages/host/apiproxy), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tool-todo`](../packages/todo/tool-todo) |
|
||||
|
||||
@@ -1062,6 +1062,7 @@ flowchart TD
|
||||
pkg_subagent --> pkg_session_projection
|
||||
pkg_subagent --> pkg_session_projection_cache
|
||||
pkg_subagent --> pkg_session_query
|
||||
pkg_subagent --> pkg_system_prompt
|
||||
pkg_subagent --> pkg_tools
|
||||
pkg_subagent --> pkg_user_approval
|
||||
pkg_session_query_sqlite --> pkg_invariants
|
||||
@@ -1827,7 +1828,7 @@ flowchart TD
|
||||
| [`tool-bash`](../packages/shell/tool-bash) | `shell` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| [`tool-pwsh`](../packages/shell/tool-pwsh) | `shell` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| [`webhook`](../packages/webhook/webhook) | `webhook` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-presets`](../packages/preset/agent-presets), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`workspace`](../packages/workspace/workspace) |
|
||||
| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query) |
|
||||
| [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
|
||||
| [`client-connection`](../packages/client/connection) | `client` | [`attachment`](../packages/attachment/attachment), [`commands`](../packages/interaction/commands), [`host-apiproxy`](../packages/host/apiproxy), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tool-todo`](../packages/todo/tool-todo) |
|
||||
|
||||
@@ -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/plan.md
|
||||
plan.md: a4332ba97667f125b0996f305e7e2a2a36005ddf
|
||||
plan.zh.md: e8536d873e92f9afa0482a1268a66e5379f65453
|
||||
plan.md: a1e29982507940f58d458e48789dc3259bee3a2e
|
||||
plan.zh.md: 8df3f5f48ba2813b8c35e362324a71eb8cad763b
|
||||
|
||||
@@ -26,7 +26,7 @@ interface PlanModeConfig {
|
||||
}
|
||||
```
|
||||
|
||||
A missing, blank, or non-string `section` and any unknown key fail at plugin load rather than being ignored. While plan mode is active, the exact `section` text renders as the `plan:policy` [system-prompt section](system-prompt.md) at order 50; inactive plan mode contributes no text.
|
||||
A missing, blank, or non-string `section` and any unknown key fail at plugin load rather than being ignored. While plan mode is active, the exact `section` text renders as the `plan:policy` [system-prompt section](system-prompt.md) at first-party order 500; inactive plan mode contributes no text.
|
||||
|
||||
## The exit tool and the `/plan` command
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ interface PlanModeConfig {
|
||||
}
|
||||
```
|
||||
|
||||
`section` 缺失、为空白或不是字符串,以及任何未知键,都会在插件加载时失败,而不是被忽略。计划模式激活期间,确切的 `section` 文本以 order 50 渲染为 `plan:policy` [系统提示词段落](system-prompt.zh.md);未激活的计划模式不贡献任何文本。
|
||||
`section` 缺失、为空白或不是字符串,以及任何未知键,都会在插件加载时失败,而不是被忽略。计划模式激活期间,确切的 `section` 文本以 first-party 顺序 500 渲染为 `plan:policy` [系统提示词段落](system-prompt.zh.md);未激活的计划模式不贡献任何文本。
|
||||
|
||||
## 退出工具与 `/plan` 命令
|
||||
|
||||
|
||||
@@ -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/system-prompt.md
|
||||
system-prompt.md: f4cdf40703ac4f4a6f87d525efb6010e45f4480b
|
||||
system-prompt.zh.md: d3eddc3c5e0df72546f80d953e7123fa0f795816
|
||||
system-prompt.md: 68eab4f5c93f14675d548c0fa635c5f3f3ce70a7
|
||||
system-prompt.zh.md: 086555593e507da03aea3e83a55017e0aa839fcb
|
||||
|
||||
@@ -39,7 +39,7 @@ interface ToolProviderResult {
|
||||
|
||||
## Prompt sections
|
||||
|
||||
`PromptSection` is a readonly same-process registration contract. Its text may be static or resolved from the current assembly context. One effective `complete` section becomes the sole prompt section after cooperative assembly.
|
||||
`PromptSection` is a readonly same-process registration contract. Its text may be static or resolved from the current assembly context. Sections sort by ascending order and then code-unit name; `FIRST_PARTY_SECTION_ORDER` publishes the sparse named allocation for repository-owned contributions. One effective `complete` section becomes the sole prompt section after cooperative assembly.
|
||||
|
||||
```ts type-equiv
|
||||
/** One contributed section of the system prompt (registry input). */
|
||||
@@ -47,9 +47,9 @@ interface PromptSection {
|
||||
/** Unique name — a duplicate registration throws (see {@link SystemPrompt.section}). */
|
||||
readonly name: string
|
||||
/**
|
||||
* Sections are concatenated in ascending order. Convention: `-100` is the
|
||||
* harness identity, `0` the deployment persona, tool guidance uses 100–199;
|
||||
* other negative orders also render before the persona.
|
||||
* Sections are concatenated in ascending order. Equal orders use code-unit
|
||||
* name order. Repository-owned placements use
|
||||
* {@link FIRST_PARTY_SECTION_ORDER}.
|
||||
*/
|
||||
readonly order: number
|
||||
/**
|
||||
|
||||
@@ -39,7 +39,7 @@ interface ToolProviderResult {
|
||||
|
||||
## 提示词段落
|
||||
|
||||
`PromptSection` 是一份只读的同进程注册约定。其文本可以是静态的,也可以从当前组装上下文动态解析。协作式组装完成后,一个有效的 `complete` 段会成为唯一的提示词段落。
|
||||
`PromptSection` 是一份只读的同进程注册约定。其文本可以是静态的,也可以从当前组装上下文动态解析。各段先按 order 升序排列,再按名称的代码单元顺序排列;`FIRST_PARTY_SECTION_ORDER` 公开仓库自带贡献的稀疏具名分配表。协作式组装完成后,一个有效的 `complete` 段会成为唯一的提示词段落。
|
||||
|
||||
```ts type-equiv
|
||||
/** One contributed section of the system prompt (registry input). */
|
||||
@@ -47,9 +47,9 @@ interface PromptSection {
|
||||
/** Unique name — a duplicate registration throws (see {@link SystemPrompt.section}). */
|
||||
readonly name: string
|
||||
/**
|
||||
* Sections are concatenated in ascending order. Convention: `-100` is the
|
||||
* harness identity, `0` the deployment persona, tool guidance uses 100–199;
|
||||
* other negative orders also render before the persona.
|
||||
* Sections are concatenated in ascending order. Equal orders use code-unit
|
||||
* name order. Repository-owned placements use
|
||||
* {@link FIRST_PARTY_SECTION_ORDER}.
|
||||
*/
|
||||
readonly order: number
|
||||
/**
|
||||
|
||||
@@ -18,8 +18,7 @@ import Group from '@deepseek-ai/cordis-plugin-group'
|
||||
import { dshHomePath, resolveDshHome } from '@deepseek-ai/dsh-home-paths'
|
||||
import { createLaunchEnvironmentSnapshot, type LaunchEnvironmentSnapshot } from '@deepseek-ai/dsh-launch-environment'
|
||||
import type {} from '@deepseek-ai/cordis-plugin-hmr'
|
||||
// Side-effect type import: resolves `ctx.get('systemPrompt')` to the service.
|
||||
import type {} from '@deepseek-ai/dsh-system-prompt'
|
||||
import { FIRST_PARTY_SECTION_ORDER } from '@deepseek-ai/dsh-system-prompt'
|
||||
|
||||
declare module '@deepseek-ai/cordis' {
|
||||
interface Context {
|
||||
@@ -826,8 +825,8 @@ export const HARNESS_SOURCE_SECTION = 'harness:source'
|
||||
* explicitly distinguishing it from the task workspace and current working
|
||||
* directory. The self-referential `dsh-tool-cordis` toolset reads and edits this
|
||||
* checkout. Call once on the settled boot context ({@link boot}); the section
|
||||
* orders just after the harness identity opener (`-100`) and before the deployment
|
||||
* persona (`0`). A booted tree with no `systemPrompt` service has no prompt to
|
||||
* uses the shared first-party placement just after the harness identity opener
|
||||
* and before the deployment persona. A booted tree with no `systemPrompt` service has no prompt to
|
||||
* augment, so this is then a no-op that returns `undefined`. The section is
|
||||
* registered against the `systemPrompt` service's fiber, so a dev HMR reload of
|
||||
* that plugin drops it until the next boot.
|
||||
@@ -840,7 +839,7 @@ export function addHarnessSourceSection(ctx: Context, sourceRoot: string): (() =
|
||||
if (systemPrompt === undefined) return undefined
|
||||
return systemPrompt.section({
|
||||
name: HARNESS_SOURCE_SECTION,
|
||||
order: -99,
|
||||
order: FIRST_PARTY_SECTION_ORDER.HARNESS_SOURCE,
|
||||
text: `The DeepSeek Harness implementation checkout is at ${sourceRoot}. The checkout location and current working directory are separate values and may differ; never infer the working directory from this path. Use pwd to determine the current working directory. Use this checkout only to inspect or extend DSH itself.`,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -817,8 +817,8 @@ describe('addHarnessSourceSection', () => {
|
||||
const systemPrompt = ctx.get('systemPrompt')!
|
||||
const rendered = renderPrompt(await systemPrompt.assemble())
|
||||
expect(rendered).toContain(EXPECTED)
|
||||
// Harness-owned opener (-100) → source (-99) → persona (0). The >= 0 guards
|
||||
// keep a drifted opener/persona string from a false pass through `-1 < n`.
|
||||
// The >= 0 guards keep a drifted opener/persona string from a false pass
|
||||
// through `-1 < n`.
|
||||
const identityAt = rendered.indexOf('You are an AI agent powered by DeepSeek Harness.')
|
||||
const sourceAt = rendered.indexOf(EXPECTED)
|
||||
const personaAt = rendered.indexOf('You are a coding agent.')
|
||||
|
||||
@@ -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/bundle/web-app/README.md
|
||||
README.md: 4092cf4fd2985027f3c7e59909f58a5dc1ef4244
|
||||
README.zh.md: 5c0f3a109b8d5261ab2a5e2ae0219cb06d493c04
|
||||
README.md: c428bbd352f7f58fe4b76cd078e2c1a1993b422a
|
||||
README.zh.md: 0d5a33fe356d749fa619ad980ab9ee13ed9ba3fc
|
||||
|
||||
@@ -16,7 +16,7 @@ Web uses the shared bounded normal default of five eligible retries after the in
|
||||
|
||||
#### What the model sees
|
||||
|
||||
When `surfaceContext` is true, the `harness:source` section identifies the on-disk Harness implementation without claiming it is the working directory, and the `app:web-surface` global section (order −98) orients the model to the GUI: the canonical local URL, the "this page" referent, the update contract (the reload receiver is always on; no-refresh reloads additionally need the `pnpm run dev:web` watcher), and the instruction not to start replacement servers. `DSH_WEB_URL` additionally appears in the managed bash environment with its description, resolved per invocation from the live server. When it is false, neither section nor the variable is registered.
|
||||
When `surfaceContext` is true, the `harness:source` section identifies the on-disk Harness implementation without claiming it is the working directory, and the `app:web-surface` global section (first-party order −800) orients the model to the GUI: the canonical local URL, the "this page" referent, the update contract (the reload receiver is always on; no-refresh reloads additionally need the `pnpm run dev:web` watcher), and the instruction not to start replacement servers. `DSH_WEB_URL` additionally appears in the managed bash environment with its description, resolved per invocation from the live server. When it is false, neither section nor the variable is registered.
|
||||
|
||||
#### Token effect
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ Web 使用共享的有界 normal 默认值,在首次请求后最多再重试
|
||||
|
||||
#### 模型看到的内容
|
||||
|
||||
当 `surfaceContext` 为 true 时,`harness:source` 段落标明磁盘上的 Harness 实现,但不会声称它就是工作目录;全局段落 `app:web-surface`(顺序 −98)则向模型说明 GUI:规范的本地 URL、「this page」指代什么、更新约定(重载接收端始终开启;无刷新重载还需要 `pnpm run dev:web` watcher),以及不要启动替代服务器的指令。`DSH_WEB_URL` 还会连同描述出现在受管 bash 环境中,每次调用时从运行中的服务器解析。当它为 false 时,这两个段落和该变量都不会注册。
|
||||
当 `surfaceContext` 为 true 时,`harness:source` 段落标明磁盘上的 Harness 实现,但不会声称它就是工作目录;全局段落 `app:web-surface`(first-party 顺序 −800)则向模型说明 GUI:规范的本地 URL、「this page」指代什么、更新约定(重载接收端始终开启;无刷新重载还需要 `pnpm run dev:web` watcher),以及不要启动替代服务器的指令。`DSH_WEB_URL` 还会连同描述出现在受管 bash 环境中,每次调用时从运行中的服务器解析。当它为 false 时,这两个段落和该变量都不会注册。
|
||||
|
||||
#### Token 影响
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment'
|
||||
import { scrubbedParentEnv } from '@deepseek-ai/dsh-subprocess'
|
||||
import type {} from '@deepseek-ai/cordis-plugin-loader'
|
||||
import type {} from '@deepseek-ai/dsh-host-webserver'
|
||||
import type {} from '@deepseek-ai/dsh-system-prompt'
|
||||
import { FIRST_PARTY_SECTION_ORDER } from '@deepseek-ai/dsh-system-prompt'
|
||||
import type {} from '@deepseek-ai/dsh-shell-env'
|
||||
|
||||
/** Stable Cordis plugin name. */
|
||||
@@ -243,7 +243,7 @@ export function apply(ctx: Context, config: Config): void {
|
||||
addHarnessSourceSection(promptCtx, SOURCE_ROOT)
|
||||
promptCtx.systemPrompt.section({
|
||||
name: 'app:web-surface',
|
||||
order: -98,
|
||||
order: FIRST_PARTY_SECTION_ORDER.WEB_SURFACE,
|
||||
text: () => webSurfacePrompt(localWebUrl(promptCtx)),
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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-deliverables/README.md
|
||||
README.md: ace08fae3a001080918973c23aa362080cd69066
|
||||
README.zh.md: 9df1664bc42c39012c2eebf397cb368b3b5c6260
|
||||
README.md: 58d68746c9d34f05c103430f330a042626213509
|
||||
README.zh.md: a62421eef8d524e0f5b65aa6d11d6db0c42c02da
|
||||
|
||||
@@ -26,7 +26,7 @@ One fixed prompt paragraph whenever this package is loaded; no tool schema, tool
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
The section is static at order 190 for the lifetime of the package mount, so it remains in the reusable prompt prefix and does not change across Turns.
|
||||
The section is static at first-party order 9000 for the lifetime of the package mount, so it remains in the reusable prompt prefix and does not change across Turns.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ Node 侧注册静态系统提示词段落 `ui:deliverable-file-references`。它
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
该段落在本包加载期间始终以顺序 190 保持静态,因此留在可复用的提示词前缀中,不会随 Turn 改变。
|
||||
该段落在本包加载期间始终以 first-party 顺序 9000 保持静态,因此留在可复用的提示词前缀中,不会随 Turn 改变。
|
||||
|
||||
## 已知限制与暂缓事项
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type {} from '@deepseek-ai/dsh-system-prompt'
|
||||
import { FIRST_PARTY_SECTION_ORDER } from '@deepseek-ai/dsh-system-prompt'
|
||||
|
||||
/** Services required for the model guidance paired with the browser renderer. */
|
||||
export const inject = ['systemPrompt']
|
||||
@@ -22,7 +22,7 @@ const FILE_REFERENCE_PROMPT = 'When you successfully create or modify files, men
|
||||
export function apply(ctx: Context): void {
|
||||
ctx.systemPrompt.section({
|
||||
name: 'ui:deliverable-file-references',
|
||||
order: 190,
|
||||
order: FIRST_PARTY_SECTION_ORDER.DELIVERABLE_FILE_REFERENCES,
|
||||
text: FILE_REFERENCE_PROMPT,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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-primitives/README.md
|
||||
README.md: c1c40e39710d46fae240f0b3281c36d660855010
|
||||
README.zh.md: 631936ad658c147923e5f80f2d7445cac93d6b36
|
||||
README.md: 3b7b37cf59aafc7292f75e9e4fb1513b93b32c1c
|
||||
README.zh.md: aee1a2ac842b34b284a0be5589b6dd801498e76b
|
||||
|
||||
@@ -14,7 +14,7 @@ Pure React atoms (zero cordis): StateDot, DisclosureRow, ic_ds_* icons, Button/P
|
||||
|
||||
## Markdown rendering
|
||||
|
||||
`MarkdownText` renders GFM and `$…$`, `$$…$$`, `\(…\)`, and `\[…\]` TeX math from untrusted assistant output through React elements, with math typeset by KaTeX and trusted commands disabled; block-level same-line `$$…$$` is display math, including `\tag{}`. A narrow micromark extension lets asterisk strong emphasis ending in punctuation close before adjacent CJK text, where prose normally omits the whitespace CommonMark requires; single-asterisk emphasis, non-CJK adjacency, escapes, code, and math retain upstream parsing. It omits raw HTML, neutralizes relative and non-HTTP(S)/mailto links, opens HTTP(S) links with safe external-link attributes, and renders absolute HTTP(S) images without a referrer; relative paths, absolute local paths, `file:` URLs, and unsupported schemes retain their alt text. Inline code whose complete value is an absolute HTTP(S) URL keeps its code styling and gains the same safe external anchor; commands, partial URLs, other schemes, and fenced code remain inert. An optional `fileMentions` resolver lets the owning view link inline code that names a real file: the token keeps code styling and gains a button wired to the resolved opener, with the resolver's accessible label and full-path `title`. The renderer never guesses at what looks like a path — an unresolved token stays inert, mentions apply to settled renders only (the streaming cache must not bake in handlers that could go stale), and a token inside an anchor stays inert because a button cannot nest there. While a reply streams, `MarkdownText` parses incrementally: all but the trailing two blocks freeze as cached React elements and only the source tail behind them re-parses per chunk, so per-chunk work tracks the tail instead of the whole reply ([mechanism and DOM-parity contract](../../../.agents/notes/implemented/architecture/2026-08-06-web-markdown-incremental-ast-renderer.md)). Tables size by column count (deepsuite chat parity): under four columns — or inside a blockquote — a table fills its column and wraps cell text down to the cells' minimum readable width, while four-or-more-column tables keep their natural width, scroll horizontally inside their wrapper, and carry the stable `md-table-wide` class so a hosting layout can widen the wrapper past its column (the chat transcript's container-query breakout in `dsh-client-ui-conversation`); a wide table's horizontal bar reveals on hover or keyboard focus (the wrapper carries `tabindex="0"`) instead of staying painted ([decision record](../../../.agents/notes/implemented/feature/2026-08-19-web-markdown-wide-table-view.md)). `MessageText` remains the literal-text primitive for user-authored content. `extractMarkdownPlainText` removes Markdown presentation markup for compact labels while preserving raw HTML as literal text. Element spacing, responsive images, tables, links, and inline code use the same `--dsw-alias-markdown-*` / `--dsw-font-markdown-*` tokens as deepsuite `@deepseek/md`. Fenced blocks render through `CodeBlock` (language banner, copy control, shiki for the registered grammars).
|
||||
`MarkdownText` renders GFM and `$…$`, `$$…$$`, `\(…\)`, and `\[…\]` TeX math from untrusted assistant output through React elements, with math typeset by KaTeX and trusted commands disabled; block-level same-line `$$…$$` is display math, including `\tag{}`. A narrow micromark extension lets asterisk strong emphasis ending in punctuation close before adjacent CJK text, where prose normally omits the whitespace CommonMark requires; single-asterisk emphasis, non-CJK adjacency, escapes, code, and math retain upstream parsing. It omits raw HTML, neutralizes relative and non-HTTP(S)/mailto links, opens HTTP(S) links with safe external-link attributes, and renders absolute HTTP(S) images without a referrer; relative paths, absolute local paths, `file:` URLs, and unsupported schemes retain their alt text. Inline code whose complete value is an absolute HTTP(S) URL keeps its code styling and gains the same safe external anchor; commands, partial URLs, other schemes, and fenced code remain inert. An optional `fileMentions` resolver lets the owning view link inline code that names a real file: the token keeps code styling and gains a button wired to the resolved opener, with the resolver's accessible label and full-path `title`. The renderer never guesses at what looks like a path — an unresolved token stays inert, mentions apply to settled renders only (the streaming cache must not bake in handlers that could go stale), and a token inside an anchor stays inert because a button cannot nest there. While a reply streams, `MarkdownText` parses incrementally: all but the trailing two blocks freeze as cached React elements and only the source tail behind them re-parses per chunk, so per-chunk work tracks the tail instead of the whole reply ([mechanism and DOM-parity contract](../../../.agents/notes/implemented/architecture/2026-08-06-web-markdown-incremental-ast-renderer.md)). Tables size by column count (deepsuite chat parity): under four columns — or inside a blockquote — a table fills its column and wraps cell text down to the cells' minimum readable width, while four-or-more-column tables keep their natural width, scroll horizontally inside their wrapper, and carry the stable `md-table-wide` class so a hosting layout can widen the wrapper past its column (the chat transcript's container-query breakout in `dsh-client-ui-conversation`); a wide table's horizontal bar reveals on hover or keyboard focus (the wrapper carries `tabindex="0"`) instead of staying painted ([decision record](../../../.agents/notes/implemented/feature/2026-08-19-web-markdown-wide-table-view.md)). `MessageText` remains the literal-text primitive for user-authored content. `extractMarkdownPlainText` removes Markdown presentation markup for compact labels while preserving raw HTML as literal text. Element spacing, responsive images, tables, links, and inline code use the same `--dsw-alias-markdown-*` / `--dsw-font-markdown-*` tokens as deepsuite `@deepseek/md`. Fenced blocks render through `CodeBlock` (language banner, copy control, shiki for the registered grammars); while a reply streams, a fence highlights incrementally as it grows — each chunk tokenizes newly completed text from the saved grammar state plus the still-growing last line, while completed lines keep their DOM — and the settled render swaps in shiki's HTML with an identical span tree ([decision record](../../../.agents/notes/implemented/feature/2026-08-20-web-streaming-fence-highlight.md)).
|
||||
|
||||
## Terminal output
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
## Markdown 渲染
|
||||
|
||||
`MarkdownText` 通过 React 元素渲染来自不受信任 assistant 输出的 GFM 与 `$…$`、`$$…$$`、`\(…\)` 和 `\[…\]` TeX 公式,公式由 KaTeX 排版并禁用受信任命令;块级同一行 `$$…$$` 是显示公式并支持 `\tag{}`。一个小范围的 micromark 扩展允许由星号标记、以标点结尾的粗体在紧邻的 CJK 文本前闭合,以适应 CJK 文本通常省略 CommonMark 所要求空格的写法;单星号强调、紧邻非 CJK 文本的情况、转义、代码与数学公式仍沿用上游解析行为。它会省略原始 HTML,使相对链接及非 HTTP(S)/mailto 链接失效,以安全的外部链接属性打开 HTTP(S) 链接,并在不发送 referrer 的情况下渲染采用绝对 HTTP(S) URL 的图片;相对路径、绝对本地路径、`file:` URL 与不受支持的 scheme 会保留其 alt 文本。完整内容为绝对 HTTP(S) URL 的行内代码会保留代码样式,并获得同样安全的外部链接;命令、非完整 URL、其他 scheme 与围栏代码仍不会成为链接。可选的 `fileMentions` 解析器让持有该组件的视图为命名真实文件的行内代码添加可点击入口:token 保留代码样式,并获得一个连接到解析所得 opener 的按钮,按钮带有解析器提供的无障碍标签和以完整路径为值的 `title`。渲染器绝不猜测哪些内容像路径:未解析的 token 保持不可交互;文件提及仅应用于已定稿的渲染(流式缓存不得固化可能过期的 handler);锚点内的 token 也保持不可交互,因为按钮不能嵌套其中。回复流式输出期间,`MarkdownText` 增量解析:除末尾两个块外全部冻结为缓存的 React 元素,每个分片只重新解析其后的源文本尾部,因此每分片的工作量跟随尾部而非整个回复([机制与 DOM 一致性约定](../../../.agents/notes/implemented/architecture/2026-08-06-web-markdown-incremental-ast-renderer.zh.md))。表格按列数决定尺寸(对齐 deepsuite chat):不足四列——或位于 blockquote 内——的表格填满所在列,单元格文本换行收缩至最小可读列宽;四列及以上的表格保持自然宽度、在包裹层内横向滚动,并携带稳定的 `md-table-wide` 类,供宿主布局把包裹层加宽到所在列之外(`dsh-client-ui-conversation` 中聊天转录区的容器查询突破样式);宽表的横向滚动条在悬停或键盘聚焦(包裹层带 `tabindex="0"`)时才出现、不再常驻([决策记录](../../../.agents/notes/implemented/feature/2026-08-19-web-markdown-wide-table-view.zh.md))。`MessageText` 仍是用户创作内容使用的字面文本原语。`extractMarkdownPlainText` 会移除 Markdown 呈现标记以用于紧凑标签,同时将原始 HTML 保留为字面文本。元素间距、响应式图片、表格、链接与行内代码使用与 deepsuite `@deepseek/md` 相同的 `--dsw-alias-markdown-*` / `--dsw-font-markdown-*` token。围栏代码块通过 `CodeBlock` 渲染(语言横幅、复制控件,以及对已注册语法使用 shiki)。
|
||||
`MarkdownText` 通过 React 元素渲染来自不受信任 assistant 输出的 GFM 与 `$…$`、`$$…$$`、`\(…\)` 和 `\[…\]` TeX 公式,公式由 KaTeX 排版并禁用受信任命令;块级同一行 `$$…$$` 是显示公式并支持 `\tag{}`。一个小范围的 micromark 扩展允许由星号标记、以标点结尾的粗体在紧邻的 CJK 文本前闭合,以适应 CJK 文本通常省略 CommonMark 所要求空格的写法;单星号强调、紧邻非 CJK 文本的情况、转义、代码与数学公式仍沿用上游解析行为。它会省略原始 HTML,使相对链接及非 HTTP(S)/mailto 链接失效,以安全的外部链接属性打开 HTTP(S) 链接,并在不发送 referrer 的情况下渲染采用绝对 HTTP(S) URL 的图片;相对路径、绝对本地路径、`file:` URL 与不受支持的 scheme 会保留其 alt 文本。完整内容为绝对 HTTP(S) URL 的行内代码会保留代码样式,并获得同样安全的外部链接;命令、非完整 URL、其他 scheme 与围栏代码仍不会成为链接。可选的 `fileMentions` 解析器让持有该组件的视图为命名真实文件的行内代码添加可点击入口:token 保留代码样式,并获得一个连接到解析所得 opener 的按钮,按钮带有解析器提供的无障碍标签和以完整路径为值的 `title`。渲染器绝不猜测哪些内容像路径:未解析的 token 保持不可交互;文件提及仅应用于已定稿的渲染(流式缓存不得固化可能过期的 handler);锚点内的 token 也保持不可交互,因为按钮不能嵌套其中。回复流式输出期间,`MarkdownText` 增量解析:除末尾两个块外全部冻结为缓存的 React 元素,每个分片只重新解析其后的源文本尾部,因此每分片的工作量跟随尾部而非整个回复([机制与 DOM 一致性约定](../../../.agents/notes/implemented/architecture/2026-08-06-web-markdown-incremental-ast-renderer.zh.md))。表格按列数决定尺寸(对齐 deepsuite chat):不足四列——或位于 blockquote 内——的表格填满所在列,单元格文本换行收缩至最小可读列宽;四列及以上的表格保持自然宽度、在包裹层内横向滚动,并携带稳定的 `md-table-wide` 类,供宿主布局把包裹层加宽到所在列之外(`dsh-client-ui-conversation` 中聊天转录区的容器查询突破样式);宽表的横向滚动条在悬停或键盘聚焦(包裹层带 `tabindex="0"`)时才出现、不再常驻([决策记录](../../../.agents/notes/implemented/feature/2026-08-19-web-markdown-wide-table-view.zh.md))。`MessageText` 仍是用户创作内容使用的字面文本原语。`extractMarkdownPlainText` 会移除 Markdown 呈现标记以用于紧凑标签,同时将原始 HTML 保留为字面文本。元素间距、响应式图片、表格、链接与行内代码使用与 deepsuite `@deepseek/md` 相同的 `--dsw-alias-markdown-*` / `--dsw-font-markdown-*` token。围栏代码块通过 `CodeBlock` 渲染(语言横幅、复制控件,以及对已注册语法使用 shiki);回复流式输出期间,围栏随内容增长而增量高亮——每个分片从保存的 grammar state 出发 tokenize 新完成的文本以及仍在增长的最后一行,已完成的行保持其 DOM 不变——定稿渲染换入的 shiki HTML 具有完全一致的 span 树([决策记录](../../../.agents/notes/implemented/feature/2026-08-20-web-streaming-fence-highlight.zh.md))。
|
||||
|
||||
## 终端输出
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { useCallback, useMemo, useRef, useState, useSyncExternalStore } from 'react'
|
||||
import { Fragment, useCallback, useMemo, useRef, useState, useSyncExternalStore } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { writeClipboard } from '../clipboard.ts'
|
||||
import { grammarLoadCount, highlightToHtml, subscribeGrammarLoaded } from './highlight.ts'
|
||||
import {
|
||||
StreamingHighlightSession, grammarLoadCount, highlightToHtml, subscribeGrammarLoaded,
|
||||
} from './highlight.ts'
|
||||
import type { HighlightSpan } from './highlight.ts'
|
||||
import css from './CodeBlock.module.css'
|
||||
|
||||
export interface CodeBlockProps {
|
||||
@@ -9,6 +13,14 @@ export interface CodeBlockProps {
|
||||
code: string
|
||||
/** Grammar hint (markdown fence info string or a fixed caller id); unknown = plain. */
|
||||
lang?: string | undefined
|
||||
/**
|
||||
* The code is still growing (a streaming markdown fence): highlight through
|
||||
* a per-instance {@link StreamingHighlightSession}, which re-tokenizes only
|
||||
* appended text and keeps completed lines' elements (and DOM) untouched.
|
||||
* The caller must keep the component instance stable across growth (a
|
||||
* stream-stable React key); settled callers omit this and get shiki's HTML.
|
||||
*/
|
||||
streaming?: boolean | undefined
|
||||
/** Extra class merged onto the wrapper (callers position; this component draws). */
|
||||
className?: string | undefined
|
||||
/** Copy-button idle label; the owner passes localized copy (this package is cordis-free, so copy arrives via props). */
|
||||
@@ -17,13 +29,61 @@ export interface CodeBlockProps {
|
||||
copiedLabel: string
|
||||
}
|
||||
|
||||
export function CodeBlock({ code, lang, className, copyLabel, copiedLabel }: CodeBlockProps) {
|
||||
/**
|
||||
* The `pre` attributes shiki's HTML arm emits for the css-variables theme,
|
||||
* mirrored so the streaming arm's tree is interchangeable with the settled
|
||||
* swap (`tests/streaming-code-block.client.spec.tsx` pins the two arms'
|
||||
* parity).
|
||||
*/
|
||||
const SHIKI_PRE_PROPS = {
|
||||
className: 'shiki css-variables',
|
||||
style: { backgroundColor: 'var(--shiki-background)', color: 'var(--shiki-foreground)' },
|
||||
tabIndex: 0,
|
||||
} as const
|
||||
|
||||
export function CodeBlock({ code, lang, streaming, className, copyLabel, copiedLabel }: CodeBlockProps) {
|
||||
const trimmed = code.endsWith('\n') ? code.slice(0, -1) : code
|
||||
// Re-render when a lazy grammar finishes loading, so a fence that showed plain
|
||||
// text while its language's grammar imported picks up highlighting. The
|
||||
// snapshot value is opaque; only its change across renders drives the memo.
|
||||
const loaded = useSyncExternalStore(subscribeGrammarLoaded, grammarLoadCount, grammarLoadCount)
|
||||
const html = useMemo(() => highlightToHtml(trimmed, lang), [trimmed, lang, loaded])
|
||||
const html = useMemo(
|
||||
() => (streaming === true ? undefined : highlightToHtml(trimmed, lang)),
|
||||
[streaming, trimmed, lang, loaded],
|
||||
)
|
||||
// Streaming state lives in refs mutated inside the memo (the MarkdownText
|
||||
// streaming-cache pattern): the session's caches carry across chunks only
|
||||
// because the owner keys this instance stably while the fence grows.
|
||||
const sessionRef = useRef<StreamingHighlightSession | null>(null)
|
||||
const lineCacheRef = useRef<{ lines: readonly HighlightSpan[][]; elements: ReactNode[] } | null>(null)
|
||||
const streamedBody = useMemo(() => {
|
||||
if (streaming !== true) {
|
||||
sessionRef.current = null
|
||||
lineCacheRef.current = null
|
||||
return undefined
|
||||
}
|
||||
sessionRef.current ??= new StreamingHighlightSession()
|
||||
const lines = sessionRef.current.update(trimmed, lang)
|
||||
if (lines === undefined) {
|
||||
lineCacheRef.current = null
|
||||
return undefined
|
||||
}
|
||||
// A retained line keeps its span-array identity across chunks, so its
|
||||
// cached element is reused and React leaves that line's DOM untouched.
|
||||
const previous = lineCacheRef.current
|
||||
const elements = lines.map((line, index) => previous !== null && previous.lines[index] === line
|
||||
? previous.elements[index]
|
||||
: (
|
||||
<Fragment key={index}>
|
||||
{index > 0 && '\n'}
|
||||
<span className="line">
|
||||
{line.map((span, spanIndex) => <span key={spanIndex} style={span.style}>{span.text}</span>)}
|
||||
</span>
|
||||
</Fragment>
|
||||
))
|
||||
lineCacheRef.current = { lines, elements }
|
||||
return <pre {...SHIKI_PRE_PROPS}><code>{elements}</code></pre>
|
||||
}, [streaming, trimmed, lang, loaded])
|
||||
const rootRef = useRef<HTMLDivElement>(null)
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
@@ -39,16 +99,18 @@ export function CodeBlock({ code, lang, className, copyLabel, copiedLabel }: Cod
|
||||
})
|
||||
}, [copied, trimmed])
|
||||
|
||||
const body = html === undefined
|
||||
? (
|
||||
<pre className={css.plain}><code>{trimmed}</code></pre>
|
||||
)
|
||||
: (
|
||||
// shiki's output is a static span tree it generated from `code` (no user
|
||||
// HTML passes through), the sanctioned innerHTML consumption path per
|
||||
// shiki's HTML output is a static span tree it generated from `code` (no
|
||||
// user HTML passes through), the sanctioned innerHTML consumption path per
|
||||
// shiki's own docs.
|
||||
<div dangerouslySetInnerHTML={{ __html: html }} />
|
||||
)
|
||||
const body = streamedBody !== undefined
|
||||
? streamedBody
|
||||
: html === undefined
|
||||
? (
|
||||
<pre className={css.plain}><code>{trimmed}</code></pre>
|
||||
)
|
||||
: (
|
||||
<div dangerouslySetInnerHTML={{ __html: html }} />
|
||||
)
|
||||
|
||||
return (
|
||||
<div ref={rootRef} className={clsx(css.block, 'md-code-block', className)}>
|
||||
|
||||
@@ -140,9 +140,10 @@ class StreamingRenderer {
|
||||
/**
|
||||
* Render untrusted assistant-authored Markdown as semantic React elements.
|
||||
* @param props - Markdown source text preserved by the session projection;
|
||||
* `streaming` renders fences and TeX plain (highlighting and KaTeX land on
|
||||
* the finalize swap) and parses incrementally across chunks; `labels`
|
||||
* forwards localized fence and footnote chrome — pass a
|
||||
* `streaming` parses incrementally across chunks and highlights fences as
|
||||
* they grow (each fence re-tokenizes only appended text; TeX stays literal
|
||||
* until the finalize swap so incomplete formulae never flash errors);
|
||||
* `labels` forwards localized fence and footnote chrome — pass a
|
||||
* reference-stable object (memoized per locale revision), because a new
|
||||
* identity discards the streaming render cache mid-message. `fileMentions`
|
||||
* links inline-code tokens its resolver recognizes as real files; this is
|
||||
|
||||
@@ -23,7 +23,7 @@ import { createJavaScriptRegexEngine, defaultJavaScriptRegexConstructor } from '
|
||||
import langTs from '@shikijs/langs/typescript'
|
||||
import langBash from '@shikijs/langs/shellscript'
|
||||
import langJson from '@shikijs/langs/json'
|
||||
import type { HighlighterCore } from 'shiki/core'
|
||||
import type { GrammarState, HighlighterCore, ThemedToken } from 'shiki/core'
|
||||
import type { CSSProperties } from 'react'
|
||||
|
||||
/** A shiki grammar module's default export (a `LanguageRegistration[]`), taken
|
||||
@@ -278,6 +278,146 @@ export interface HighlightSpan {
|
||||
style: CSSProperties
|
||||
}
|
||||
|
||||
/** vscode-textmate FontStyle bits shiki folds into `text-decoration` values. */
|
||||
const DECORATION_BITS: readonly (readonly [number, string])[] = [[4, 'underline'], [8, 'line-through']]
|
||||
|
||||
/**
|
||||
* The inline style shiki's HTML arm assigns one token (`getTokenStyleObject`
|
||||
* mirrored onto React style keys): the css-variables color plus the
|
||||
* vscode-textmate font-style bits the theme lets through — italic (1), bold
|
||||
* (2), and the {@link DECORATION_BITS} decorations (the theme injects bold,
|
||||
* italic, and underline rules for markup scopes, so markdown fences carry
|
||||
* them). The theme has no per-scope backgrounds, so `background-color` never
|
||||
* occurs; the arm-parity tests fail loud if a shiki upgrade changes that.
|
||||
*/
|
||||
function spanStyle(token: ThemedToken): CSSProperties {
|
||||
const style: CSSProperties = { color: token.color }
|
||||
/* v8 ignore next -- fontStyle is optional in ThemedToken's type; tokenizeWithTheme always stamps it. */
|
||||
const bits = token.fontStyle ?? 0
|
||||
if ((bits & 1) !== 0) style.fontStyle = 'italic'
|
||||
if ((bits & 2) !== 0) style.fontWeight = 'bold'
|
||||
const decorations = DECORATION_BITS.filter(([bit]) => (bits & bit) !== 0)
|
||||
if (decorations.length > 0) style.textDecoration = decorations.map(([, value]) => value).join(' ')
|
||||
return style
|
||||
}
|
||||
|
||||
/**
|
||||
* Narrow one tokenized line to the runs a `<span style>` renders, folding a
|
||||
* whitespace-only run into the token that follows it — shiki's default
|
||||
* `mergeWhitespaces` HTML behavior — with each run styled through
|
||||
* {@link spanStyle}, so the streaming spans and the settled `codeToHtml`
|
||||
* swap render one identical span tree. shiki exempts underlined/struck
|
||||
* whitespace from the fold; under the css-variables theme that case cannot
|
||||
* occur — its only underline rule styles inline-link scopes, whose spaced
|
||||
* text tokenizes as one run, and it injects no strikethrough rule — so the
|
||||
* unconditional fold here stays equivalent (the markdown arm-parity test
|
||||
* pins it). A line-trailing whitespace-only run has no follower and keeps
|
||||
* its own span, as in shiki.
|
||||
*/
|
||||
function lineSpans(line: ThemedToken[]): HighlightSpan[] {
|
||||
const spans: HighlightSpan[] = []
|
||||
let pendingWhitespace = ''
|
||||
for (const [index, token] of line.entries()) {
|
||||
if (/^\s+$/.test(token.content) && index + 1 < line.length) {
|
||||
pendingWhitespace += token.content
|
||||
continue
|
||||
}
|
||||
spans.push({ text: pendingWhitespace + token.content, style: spanStyle(token) })
|
||||
pendingWhitespace = ''
|
||||
}
|
||||
return spans
|
||||
}
|
||||
|
||||
/**
|
||||
* Incremental highlighter for one growing streaming fence. TextMate
|
||||
* tokenization is line-based and forward-only — a line's tokens depend only on
|
||||
* its own text and the grammar state entering it — so appended text never
|
||||
* changes a completed line's tokens. The session caches the spans of every
|
||||
* completed line together with the grammar state after them; each
|
||||
* {@link update} tokenizes newly completed text from that state, plus the
|
||||
* still-growing last line. Per-call cost therefore excludes the completed
|
||||
* prefix, and the result equals a from-scratch tokenization of the same code.
|
||||
* Non-append input and a change of resolved grammar reset the cache and
|
||||
* re-tokenize fully, so any input stays correct.
|
||||
*/
|
||||
export class StreamingHighlightSession {
|
||||
/** Grammar id the cache was built with; a different resolution resets it. */
|
||||
private resolved: string | undefined
|
||||
/** Newline-terminated source prefix covered by {@link spans}. */
|
||||
private prefix = ''
|
||||
/** Cached spans, one entry per completed line of {@link prefix}. */
|
||||
private spans: HighlightSpan[][] = []
|
||||
/** Grammar state after {@link prefix}; undefined = the grammar's initial state. */
|
||||
private state: GrammarState | undefined
|
||||
private lastCode: string | undefined
|
||||
private lastLang: string | undefined
|
||||
private lastResult: HighlightSpan[][] | undefined
|
||||
|
||||
private reset(resolved: string | undefined): void {
|
||||
this.resolved = resolved
|
||||
this.prefix = ''
|
||||
this.spans = []
|
||||
this.state = undefined
|
||||
}
|
||||
|
||||
/** Tokenize `text` with `resolved`, resuming from the cached grammar state when one exists. */
|
||||
private tokenize(resolved: string, text: string): ThemedToken[][] {
|
||||
return highlighter().codeToTokensBase(text, {
|
||||
lang: resolved,
|
||||
theme: 'css-variables',
|
||||
...(this.state === undefined ? {} : { grammarState: this.state }),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Tokenize the fence's current text into per-line highlighted runs;
|
||||
* `undefined` means the caller renders its plain fallback. Idempotent per
|
||||
* (`code`, `lang`) input — repeated calls return the identical result array —
|
||||
* and a retained line keeps its span-array identity across growing calls, so
|
||||
* a React caller can reuse cached line elements. A lazy grammar not yet
|
||||
* loaded returns `undefined` and loads in the background exactly as
|
||||
* {@link highlightToHtml} does; the next call after it registers highlights.
|
||||
* @param code - the fence text accumulated so far (display-trimmed, no synthetic trailing newline).
|
||||
* @param lang - the language hint (a markdown fence info string).
|
||||
* @returns one entry per line of `code` (each an array of runs), or `undefined` for unknown or not-yet-loaded languages.
|
||||
*/
|
||||
update(code: string, lang: string | undefined): readonly HighlightSpan[][] | undefined {
|
||||
if (code === this.lastCode && lang === this.lastLang && this.lastResult !== undefined) {
|
||||
return this.lastResult
|
||||
}
|
||||
this.lastCode = code
|
||||
this.lastLang = lang
|
||||
const resolved = lang === undefined ? undefined : LANG_ALIASES.get(lang.toLowerCase())
|
||||
if (resolved === undefined || !ensureGrammar(resolved)) {
|
||||
this.reset(undefined)
|
||||
this.lastResult = undefined
|
||||
return undefined
|
||||
}
|
||||
if (resolved !== this.resolved || !code.startsWith(this.prefix)) this.reset(resolved)
|
||||
const rest = code.slice(this.prefix.length)
|
||||
const lastNewline = rest.lastIndexOf('\n')
|
||||
// Everything before the last newline is newly completed lines: tokenize
|
||||
// them once from the cached state and retain their spans. What follows is
|
||||
// the still-growing line, re-tokenized per call but never retained.
|
||||
if (lastNewline >= 0) {
|
||||
// Tokenize what shiki's own line splitting would see: splitLines strips
|
||||
// the \r of a \r\n terminator (interior pairs are shiki's to split), so
|
||||
// a CRLF cut must not leak its \r into the last completed line — a bash
|
||||
// continuation's grammar state, for example, differs with it.
|
||||
const grownEnd = rest[lastNewline - 1] === '\r' ? lastNewline - 1 : lastNewline
|
||||
const tokens = this.tokenize(resolved, rest.slice(0, grownEnd))
|
||||
// Per-line push, not one spread call: a reconnect can deliver the whole
|
||||
// accumulated fence as one update, and spreading tens of thousands of
|
||||
// lines into arguments can exceed the engine's argument limit.
|
||||
for (const line of tokens) this.spans.push(lineSpans(line))
|
||||
this.state = highlighter().getLastGrammarState(tokens)
|
||||
this.prefix = code.slice(0, this.prefix.length + lastNewline + 1)
|
||||
}
|
||||
this.lastResult = [...this.spans, ...this.tokenize(resolved, rest.slice(lastNewline + 1)).map(lineSpans)]
|
||||
return this.lastResult
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tokenize `code` into per-line highlighted runs when `lang` maps to a
|
||||
* registered grammar; `undefined` means the caller renders its plain fallback.
|
||||
@@ -286,8 +426,9 @@ export interface HighlightSpan {
|
||||
* so this returns shiki's own 2D line/token structure narrowed to what a run
|
||||
* renders. Each run's color is a `--shiki-*` custom property, keeping token
|
||||
* colors on the theme package's sheets exactly as the HTML path does; the
|
||||
* css-variables theme carries no font-style bits, matching that path's
|
||||
* color-only output. The trailing newline shiki appends as a final empty line
|
||||
* markup font-style bits the theme lets through (bold/italic/underline in
|
||||
* markdown scopes) are dropped — the line-numbered file view renders
|
||||
* color-only runs. The trailing newline shiki appends as a final empty line
|
||||
* is dropped so the run count matches the caller's own line array.
|
||||
* @param code - the source text.
|
||||
* @param lang - the language hint (a file-extension-derived language id).
|
||||
|
||||
@@ -126,7 +126,7 @@ export interface MarkdownFileMentions {
|
||||
* numbering accumulated in document order while references render.
|
||||
*/
|
||||
export interface MarkdownRenderContext {
|
||||
/** Streaming arm: fences render plain and TeX stays literal. */
|
||||
/** Streaming arm: fences highlight incrementally as they grow; TeX (including ```math fences) stays literal until the settled pass. */
|
||||
readonly streaming: boolean
|
||||
/** Localized fence copy-button labels. */
|
||||
readonly labels: MarkdownLabels
|
||||
@@ -334,7 +334,13 @@ function renderCode(node: Md.Code, key: Key, context: MarkdownRenderContext): Re
|
||||
// CodeBlock's display trim removes; feeding the bare value would make
|
||||
// that trim eat a REAL trailing blank line inside the fence instead.
|
||||
code={`${node.value}\n`}
|
||||
lang={context.streaming ? undefined : lang}
|
||||
lang={lang}
|
||||
// Streaming keys are source offsets, stable while the fence grows, so
|
||||
// the CodeBlock instance (and its incremental highlight session)
|
||||
// survives every chunk. A fence whose info string is still mid-chunk
|
||||
// has no content yet and took the empty-fence arm above, so `lang`
|
||||
// here is final: it can never re-resolve to a different grammar.
|
||||
streaming={context.streaming}
|
||||
copyLabel={context.labels.code.copyLabel}
|
||||
copiedLabel={context.labels.code.copiedLabel}
|
||||
/>
|
||||
|
||||
+27
-4
@@ -3,12 +3,25 @@
|
||||
<div class="_bannerWrap_9aea57">
|
||||
<div class="_banner_9aea57">
|
||||
<div class="_infostring_9aea57">
|
||||
#text "ts"
|
||||
<div class="_action_9aea57">
|
||||
<button class="_copyButton_9aea57" type="button">
|
||||
#text "复制"
|
||||
<pre class="_plain_9aea57">
|
||||
<pre class="shiki css-variables" style="background-color: var(--shiki-background); color: var(--shiki-foreground);" tabindex="0">
|
||||
<code>
|
||||
#text "const answer: number = 42"
|
||||
<span class="line">
|
||||
<span style="color: var(--shiki-token-keyword);">
|
||||
#text "const"
|
||||
<span style="color: var(--shiki-token-constant);">
|
||||
#text " answer"
|
||||
<span style="color: var(--shiki-token-keyword);">
|
||||
#text ":"
|
||||
<span style="color: var(--shiki-token-constant);">
|
||||
#text " number"
|
||||
<span style="color: var(--shiki-token-keyword);">
|
||||
#text " ="
|
||||
<span style="color: var(--shiki-token-constant);">
|
||||
#text " 42"
|
||||
<div class="_block_9aea57 md-code-block">
|
||||
<div class="_bannerWrap_9aea57">
|
||||
<div class="_banner_9aea57">
|
||||
@@ -23,6 +36,7 @@
|
||||
<div class="_bannerWrap_9aea57">
|
||||
<div class="_banner_9aea57">
|
||||
<div class="_infostring_9aea57">
|
||||
#text "unknown-lang"
|
||||
<div class="_action_9aea57">
|
||||
<button class="_copyButton_9aea57" type="button">
|
||||
#text "复制"
|
||||
@@ -33,12 +47,21 @@
|
||||
<div class="_bannerWrap_9aea57">
|
||||
<div class="_banner_9aea57">
|
||||
<div class="_infostring_9aea57">
|
||||
#text "ts"
|
||||
<div class="_action_9aea57">
|
||||
<button class="_copyButton_9aea57" type="button">
|
||||
#text "复制"
|
||||
<pre class="_plain_9aea57">
|
||||
<pre class="shiki css-variables" style="background-color: var(--shiki-background); color: var(--shiki-foreground);" tabindex="0">
|
||||
<code>
|
||||
#text "const withMeta = true"
|
||||
<span class="line">
|
||||
<span style="color: var(--shiki-token-keyword);">
|
||||
#text "const"
|
||||
<span style="color: var(--shiki-token-constant);">
|
||||
#text " withMeta"
|
||||
<span style="color: var(--shiki-token-keyword);">
|
||||
#text " ="
|
||||
<span style="color: var(--shiki-token-constant);">
|
||||
#text " true"
|
||||
<pre>
|
||||
<code>
|
||||
<div class="_block_9aea57 md-code-block">
|
||||
|
||||
Vendored
+15
-2
@@ -13,11 +13,24 @@
|
||||
<div class="_bannerWrap_9aea57">
|
||||
<div class="_banner_9aea57">
|
||||
<div class="_infostring_9aea57">
|
||||
#text "ts"
|
||||
<div class="_action_9aea57">
|
||||
<button class="_copyButton_9aea57" type="button">
|
||||
#text "复制"
|
||||
<pre class="_plain_9aea57">
|
||||
<pre class="shiki css-variables" style="background-color: var(--shiki-background); color: var(--shiki-foreground);" tabindex="0">
|
||||
<code>
|
||||
#text "const doubled = true\n\n"
|
||||
<span class="line">
|
||||
<span style="color: var(--shiki-token-keyword);">
|
||||
#text "const"
|
||||
<span style="color: var(--shiki-token-constant);">
|
||||
#text " doubled"
|
||||
<span style="color: var(--shiki-token-keyword);">
|
||||
#text " ="
|
||||
<span style="color: var(--shiki-token-constant);">
|
||||
#text " true"
|
||||
#text "\n"
|
||||
<span class="line">
|
||||
#text "\n"
|
||||
<span class="line">
|
||||
<p>
|
||||
#text "after"
|
||||
|
||||
+1
@@ -21,6 +21,7 @@
|
||||
<div class="_bannerWrap_9aea57">
|
||||
<div class="_banner_9aea57">
|
||||
<div class="_infostring_9aea57">
|
||||
#text "math"
|
||||
<div class="_action_9aea57">
|
||||
<button class="_copyButton_9aea57" type="button">
|
||||
#text "复制"
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
// @vitest-environment jsdom
|
||||
// The fixture corpus is a DOM compatibility baseline; review diffs as
|
||||
// user-visible Markdown changes rather than regenerating them for refactors.
|
||||
// One intentional divergence from the original react-markdown recording:
|
||||
// streaming fences highlight (with their banner language visible) since the
|
||||
// incremental fence-highlight decision, so `*.streaming.txt` fixtures with
|
||||
// fenced code pin shiki span trees where react-markdown had the plain arm.
|
||||
import { cleanup, render } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { MarkdownText } from './markdown-test-components.tsx'
|
||||
|
||||
@@ -224,14 +224,46 @@ describe('MarkdownText', () => {
|
||||
expect(plain.container.querySelector('pre code')?.textContent).toContain('no language here')
|
||||
})
|
||||
|
||||
it('streaming renders fences plain; the finalize swap highlights them', () => {
|
||||
it('streaming highlights a registered-grammar fence; the finalize swap keeps it highlighted', () => {
|
||||
const fence = '```ts\nconst answer = 42\n```'
|
||||
const live = render(<MarkdownText text={fence} streaming />)
|
||||
const pre = live.container.querySelector('pre.shiki')
|
||||
expect(pre).not.toBeNull()
|
||||
expect(pre?.textContent).toContain('const answer = 42')
|
||||
expect(pre?.querySelectorAll('span[style]').length).toBeGreaterThan(1)
|
||||
live.rerender(<MarkdownText text={fence} />)
|
||||
expect(live.container.querySelector('pre.shiki')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('a growing unclosed fence extends highlighting; completed lines keep their DOM nodes', () => {
|
||||
const live = render(<MarkdownText text={'```ts\nconst a = 1\nlet partial'} streaming />)
|
||||
const lines = live.container.querySelectorAll('pre.shiki .line')
|
||||
expect(lines).toHaveLength(2)
|
||||
const firstLine = lines[0]
|
||||
live.rerender(<MarkdownText text={'```ts\nconst a = 1\nlet partial = 2\n// tail\n```'} streaming />)
|
||||
const grown = live.container.querySelectorAll('pre.shiki .line')
|
||||
expect(grown).toHaveLength(3)
|
||||
expect(grown[0]).toBe(firstLine)
|
||||
expect(grown[2]?.textContent).toBe('// tail')
|
||||
expect(live.container.querySelector('pre.shiki')?.textContent).toBe('const a = 1\nlet partial = 2\n// tail')
|
||||
})
|
||||
|
||||
it('a fence whose info string is still mid-chunk has no content to color, so no wrong grammar ever paints', () => {
|
||||
// '```t' could complete to ts, toml, … — but until its line ends, the
|
||||
// fence has no content and keeps the stock empty pre.
|
||||
const live = render(<MarkdownText text={'```t'} streaming />)
|
||||
expect(live.container.querySelector('pre')?.outerHTML).toBe('<pre><code class="language-t"></code></pre>')
|
||||
live.rerender(<MarkdownText text={'```ts\nconst answer = 42'} streaming />)
|
||||
expect(live.container.querySelector('pre.shiki')?.textContent).toBe('const answer = 42')
|
||||
})
|
||||
|
||||
it('streaming keeps unknown and language-less fences on the plain arm, and ```math literal until settle', () => {
|
||||
const live = render(
|
||||
<MarkdownText text={'```cobol\nDISPLAY "X".\n```\n\n```\nno language\n```\n\n```math\n\\sqrt{2}\n```'} streaming />,
|
||||
)
|
||||
expect(live.container.querySelector('pre.shiki')).toBeNull()
|
||||
expect(live.container.querySelector('pre code')?.textContent).toContain('const answer = 42')
|
||||
live.unmount()
|
||||
const done = render(<MarkdownText text={fence} />)
|
||||
expect(done.container.querySelector('pre.shiki')).not.toBeNull()
|
||||
expect(live.container.querySelector('.katex')).toBeNull()
|
||||
expect(live.container.textContent).toContain('\\sqrt{2}')
|
||||
})
|
||||
|
||||
it('forwards localized labels to fenced code blocks', () => {
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
// @vitest-environment jsdom
|
||||
// The streaming fence arm: StreamingHighlightSession's incremental
|
||||
// tokenization equals from-scratch tokenization at every appended prefix, and
|
||||
// CodeBlock's `streaming` arm renders the same token tree as the settled
|
||||
// shiki-HTML swap while keeping completed lines' DOM nodes untouched. Lives
|
||||
// apart from code-block.client.spec.tsx so its lazy-grammar timing cannot
|
||||
// race that file's first-touch assertions (files run isolated).
|
||||
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { cleanup, render } from '@testing-library/react'
|
||||
import { CodeBlock } from '../src/markdown/CodeBlock.tsx'
|
||||
import { StreamingHighlightSession } from '../src/markdown/highlight.ts'
|
||||
import { markdownLabels } from './labels.client.ts'
|
||||
|
||||
const LABELS = markdownLabels.code
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
/**
|
||||
* One arm's rendered token tree, with every style channel the settled shiki
|
||||
* HTML emits (color plus the markup font-style bits), so equality between the
|
||||
* streaming spans and the settled `codeToHtml` swap pins full visual parity.
|
||||
*/
|
||||
function readPre(root: HTMLElement) {
|
||||
const pre = root.querySelector('pre.shiki')
|
||||
expect(pre).not.toBeNull()
|
||||
return {
|
||||
classes: [...pre!.classList].sort().join(' '),
|
||||
tabIndex: pre!.getAttribute('tabindex'),
|
||||
text: pre!.textContent,
|
||||
lines: [...pre!.querySelectorAll('.line')].map(line =>
|
||||
[...line.querySelectorAll('span[style]')].map((span) => {
|
||||
const style = (span as HTMLElement).style
|
||||
return `${span.textContent ?? ''}|${style.color}|${style.fontStyle}|${style.fontWeight}|${style.textDecoration}`
|
||||
}),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
describe('StreamingHighlightSession', () => {
|
||||
it('reconstructs the code verbatim and colors tokens through --shiki-* properties', () => {
|
||||
const code = 'const a = 1\n// note\nconst b = "x"'
|
||||
const lines = new StreamingHighlightSession().update(code, 'ts')
|
||||
expect(lines?.map(line => line.map(span => span.text).join('')).join('\n')).toBe(code)
|
||||
expect(lines?.[0]?.[0]).toEqual({ text: 'const', style: { color: 'var(--shiki-token-keyword)' } })
|
||||
expect(lines?.[1]?.[0]?.style.color).toBe('var(--shiki-token-comment)')
|
||||
})
|
||||
|
||||
it('incremental growth equals a fresh from-scratch tokenization at every prefix', () => {
|
||||
// The template literal spans lines, so mid-stream states leave the
|
||||
// grammar inside a multi-line construct — the case where a stale saved
|
||||
// state would color the continuation wrong.
|
||||
const code = 'const s = `template\nline ${x} mid\n` // done\nconst t: number = 42'
|
||||
const session = new StreamingHighlightSession()
|
||||
for (let end = 1; end <= code.length; end++) {
|
||||
const slice = code.slice(0, end)
|
||||
expect(session.update(slice, 'ts')).toEqual(new StreamingHighlightSession().update(slice, 'ts'))
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps completed lines\' span arrays identical across growth and re-tokenizes only the tail', () => {
|
||||
const session = new StreamingHighlightSession()
|
||||
const first = session.update('const a = 1\nlet', 'ts')
|
||||
expect(first).toBeDefined()
|
||||
const second = session.update('const a = 1\nlet b = 2', 'ts')
|
||||
expect(second?.[0]).toBe(first?.[0])
|
||||
expect(second?.[1]).not.toBe(first?.[1])
|
||||
})
|
||||
|
||||
it('is idempotent per input: repeated calls return the identical result array', () => {
|
||||
const session = new StreamingHighlightSession()
|
||||
const result = session.update('const a = 1', 'ts')
|
||||
expect(result).toBeDefined()
|
||||
expect(session.update('const a = 1', 'ts')).toBe(result)
|
||||
})
|
||||
|
||||
it('an alias switch onto the same grammar keeps the cache; a different grammar re-tokenizes correctly', () => {
|
||||
const session = new StreamingHighlightSession()
|
||||
const first = session.update('const a = 1\nlet', 'ts')
|
||||
expect(first).toBeDefined()
|
||||
// Same code under a different alias of the same grammar: recomputed
|
||||
// (the idempotence key is the raw input) but the line cache is kept.
|
||||
const aliased = session.update('const a = 1\nlet', 'typescript')
|
||||
expect(aliased?.[0]).toBe(first?.[0])
|
||||
const json = session.update('{"a": 1}', 'json')
|
||||
expect(json).toEqual(new StreamingHighlightSession().update('{"a": 1}', 'json'))
|
||||
})
|
||||
|
||||
it('non-append input re-tokenizes from scratch', () => {
|
||||
const session = new StreamingHighlightSession()
|
||||
session.update('const a = 1\nconst b = 2', 'ts')
|
||||
const replaced = session.update('let c = 3', 'ts')
|
||||
expect(replaced).toEqual(new StreamingHighlightSession().update('let c = 3', 'ts'))
|
||||
})
|
||||
|
||||
it('returns undefined for unknown or absent languages, then recovers when a known one arrives', () => {
|
||||
const session = new StreamingHighlightSession()
|
||||
expect(session.update('x', 'cobol')).toBeUndefined()
|
||||
expect(session.update('x', undefined)).toBeUndefined()
|
||||
expect(session.update('const x = 1', 'ts')).toEqual(new StreamingHighlightSession().update('const x = 1', 'ts'))
|
||||
})
|
||||
|
||||
it('a lazy grammar reports plain until it registers, then highlights on the next update', async () => {
|
||||
const session = new StreamingHighlightSession()
|
||||
expect(session.update('print(1)', 'python')).toBeUndefined()
|
||||
await vi.waitFor(() => {
|
||||
const lines = session.update('print(1)', 'python')
|
||||
expect(lines?.[0]?.map(span => span.text).join('')).toBe('print(1)')
|
||||
expect(lines?.[0]?.length).toBeGreaterThan(1)
|
||||
}, { timeout: 5_000 })
|
||||
})
|
||||
|
||||
it('a trailing newline renders as a real empty last line (settled-arm parity)', () => {
|
||||
const lines = new StreamingHighlightSession().update('const a = 1\n', 'ts')
|
||||
expect(lines).toHaveLength(2)
|
||||
expect(lines?.[1]).toEqual([])
|
||||
})
|
||||
|
||||
it('a blank line inside a multi-line construct keeps the saved grammar state', () => {
|
||||
// The empty completed segment tokenizes as [[]]; the state saved after it
|
||||
// must still be the inside-template state, so the continuation stays
|
||||
// string-colored (incremental equals from-scratch at every prefix).
|
||||
const code = 'const s = `a\n\nb` // done'
|
||||
const session = new StreamingHighlightSession()
|
||||
for (let end = 1; end <= code.length; end++) {
|
||||
const slice = code.slice(0, end)
|
||||
expect(session.update(slice, 'ts')).toEqual(new StreamingHighlightSession().update(slice, 'ts'))
|
||||
}
|
||||
const lines = session.update(code, 'ts')
|
||||
expect(lines?.[1]).toEqual([])
|
||||
expect(lines?.[2]?.[0]?.text).toBe('b`')
|
||||
expect(lines?.[2]?.[0]?.style.color).toBe('var(--shiki-token-string-expression)')
|
||||
})
|
||||
|
||||
it('a CRLF boundary never leaks its \\r into the grammar (shiki line-split parity)', () => {
|
||||
// bash: a backslash continuation only holds if the line ends at the
|
||||
// continuation — a leaked \r would break the saved state and recolor the
|
||||
// next line as a fresh command.
|
||||
const code = 'echo a \\\r\nb\r\nc'
|
||||
const session = new StreamingHighlightSession()
|
||||
for (let end = 1; end <= code.length; end++) {
|
||||
const slice = code.slice(0, end)
|
||||
expect(session.update(slice, 'bash')).toEqual(new StreamingHighlightSession().update(slice, 'bash'))
|
||||
}
|
||||
// Span text carries no \r for completed lines, exactly like the settled
|
||||
// arm's shiki output.
|
||||
const lines = session.update(code, 'bash')
|
||||
expect(lines?.map(line => line.map(span => span.text).join('')).join('\n')).toBe('echo a \\\nb\nc')
|
||||
})
|
||||
|
||||
it('markdown markup styles (bold/italic/underline) reach the spans once the grammar loads', async () => {
|
||||
const snippet = '# Heading\n**bold words** and *italic* and a [link with spaces](https://x.example) tail'
|
||||
await vi.waitFor(() => {
|
||||
expect(new StreamingHighlightSession().update('# x', 'md')).toBeDefined()
|
||||
}, { timeout: 5_000 })
|
||||
const session = new StreamingHighlightSession()
|
||||
for (let end = 1; end <= snippet.length; end++) {
|
||||
const slice = snippet.slice(0, end)
|
||||
expect(session.update(slice, 'md')).toEqual(new StreamingHighlightSession().update(slice, 'md'))
|
||||
}
|
||||
const lines = session.update(snippet, 'md')
|
||||
expect(lines?.[0]?.[0]?.style.fontWeight).toBe('bold')
|
||||
const spans = lines?.[1] ?? []
|
||||
expect(spans.some(span => span.style.fontWeight === 'bold')).toBe(true)
|
||||
expect(spans.some(span => span.style.fontStyle === 'italic')).toBe(true)
|
||||
expect(spans.some(span => span.style.textDecoration === 'underline')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('CodeBlock streaming arm', () => {
|
||||
it('renders the same token tree as the settled shiki HTML swap', () => {
|
||||
const code = 'const s = `tpl\nline ${x}\n`\n'
|
||||
const streamed = render(<CodeBlock code={code} lang="ts" streaming {...LABELS} />)
|
||||
const settled = render(<CodeBlock code={code} lang="ts" {...LABELS} />)
|
||||
expect(readPre(streamed.container)).toEqual(readPre(settled.container))
|
||||
})
|
||||
|
||||
it('a markdown fence matches the settled swap including font styles, and a CRLF fence matches too', async () => {
|
||||
// md is a lazy grammar: wait for it so both arms highlight.
|
||||
await vi.waitFor(() => {
|
||||
expect(new StreamingHighlightSession().update('# x', 'md')).toBeDefined()
|
||||
}, { timeout: 5_000 })
|
||||
const md = '# Heading\n**bold words** and *italic* and a [link with spaces](https://x.example) tail\n'
|
||||
const mdStreamed = render(<CodeBlock code={md} lang="md" streaming {...LABELS} />)
|
||||
const mdSettled = render(<CodeBlock code={md} lang="md" {...LABELS} />)
|
||||
const streamedTree = readPre(mdStreamed.container)
|
||||
expect(streamedTree).toEqual(readPre(mdSettled.container))
|
||||
// The settled arm really carries the styles, so the equality above cannot
|
||||
// pass by both arms dropping them.
|
||||
const flat = streamedTree.lines.flat().join(' ')
|
||||
expect(flat).toContain('|bold|')
|
||||
expect(flat).toContain('|italic|')
|
||||
expect(flat).toContain('|underline')
|
||||
const crlf = 'echo a \\\r\nb\r\nc\n'
|
||||
const crlfStreamed = render(<CodeBlock code={crlf} lang="bash" streaming {...LABELS} />)
|
||||
const crlfSettled = render(<CodeBlock code={crlf} lang="bash" {...LABELS} />)
|
||||
expect(readPre(crlfStreamed.container)).toEqual(readPre(crlfSettled.container))
|
||||
})
|
||||
|
||||
it('keeps completed lines\' DOM nodes as the code grows and appends the new ones', () => {
|
||||
const view = render(<CodeBlock code={'const a = 1\nlet partial\n'} lang="ts" streaming {...LABELS} />)
|
||||
const firstLine = view.container.querySelector('pre.shiki .line')
|
||||
expect(firstLine).not.toBeNull()
|
||||
view.rerender(<CodeBlock code={'const a = 1\nlet partial = 2\n// tail\n'} lang="ts" streaming {...LABELS} />)
|
||||
const lines = view.container.querySelectorAll('pre.shiki .line')
|
||||
expect(lines).toHaveLength(3)
|
||||
expect(lines[0]).toBe(firstLine)
|
||||
expect(lines[2]?.textContent).toBe('// tail')
|
||||
// Newlines separate the line spans, so pre textContent (the copy source)
|
||||
// stays the code verbatim.
|
||||
expect(view.container.querySelector('pre.shiki')?.textContent).toBe('const a = 1\nlet partial = 2\n// tail')
|
||||
})
|
||||
|
||||
it('streaming with an unknown language stays on the identical plain arm', () => {
|
||||
const view = render(<CodeBlock code={'IDENTIFICATION DIVISION.\n'} lang="cobol" streaming {...LABELS} />)
|
||||
expect(view.container.querySelector('pre.shiki')).toBeNull()
|
||||
expect(view.getByText('IDENTIFICATION DIVISION.')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('the settle swap (streaming to settled) preserves the code content', () => {
|
||||
const code = 'const answer = 42\n'
|
||||
const view = render(<CodeBlock code={code} lang="ts" streaming {...LABELS} />)
|
||||
const streamedText = view.container.querySelector('pre.shiki')?.textContent
|
||||
view.rerender(<CodeBlock code={code} lang="ts" {...LABELS} />)
|
||||
const settledText = view.container.querySelector('pre.shiki')?.textContent
|
||||
expect(streamedText).toBe('const answer = 42')
|
||||
expect(settledText).toBe(streamedText)
|
||||
})
|
||||
})
|
||||
@@ -11,7 +11,7 @@ import FileReferenceService, {
|
||||
FILE_REFERENCE_PROMPT,
|
||||
type FileReferenceCandidate,
|
||||
} from '@deepseek-ai/dsh-file-reference'
|
||||
import type {} from '@deepseek-ai/dsh-system-prompt'
|
||||
import { FIRST_PARTY_SECTION_ORDER } from '@deepseek-ai/dsh-system-prompt'
|
||||
import type {} from '@deepseek-ai/dsh-tools'
|
||||
import {
|
||||
DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES,
|
||||
@@ -69,7 +69,7 @@ export class LocalFileReferenceService extends FileReferenceService {
|
||||
const fiber = agent.ctx.inject(['systemPrompt', 'tools'], (scope) => {
|
||||
scope.systemPrompt.section({
|
||||
name: 'context:file-reference',
|
||||
order: 99,
|
||||
order: FIRST_PARTY_SECTION_ORDER.FILE_REFERENCE,
|
||||
text: () => agent.ctx.tools.get('read', agent) === undefined ? '' : FILE_REFERENCE_PROMPT,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/core/system-prompt/README.md
|
||||
README.md: d750a507e628e7609af542227e4528d4d4934ce8
|
||||
README.zh.md: ec5b32d742b96c8044a3707c35f15e30ba642b4f
|
||||
README.md: fde810374056bd701c69d0dcc29c35c97c53e99e
|
||||
README.zh.md: e97a1ea87696b3f3cbc3eb3c1db1397acbb5062f
|
||||
|
||||
@@ -8,7 +8,7 @@ System prompt assembly registry. Plugins contribute ordered sections, tool schem
|
||||
|
||||
| Key | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `includeHarnessIdentity` | `true` | Include the fixed `You are an AI agent powered by DeepSeek Harness.` order-−100 opener. Set false only when a compatibility deployment owns the complete system prompt. |
|
||||
| `includeHarnessIdentity` | `true` | Include the fixed `You are an AI agent powered by DeepSeek Harness.` first-party opener at order −1000. Set false only when a compatibility deployment owns the complete system prompt. |
|
||||
| `includeRuntimeContext` | `true` | Include ordered dynamic contexts in assembly. When false, context providers are not evaluated and contexts added by `system-prompt/assemble` listeners are discarded after the waterfall; other services and their enforcement remain active. |
|
||||
| `persona` | `''` | The global deployment-persona default: the ONE config-authored prompt fragment, rendered as the order-0 `deployment:persona` section unless an agent-scoped contribution shadows it. A template — complete `{{…}}` groups are interpreted strictly against the registered variables (the shipped loop registers `{{model}}`/`{{cwd}}`), with no escape syntax for literal braces yet. Empty ⇒ the section is dropped at render. |
|
||||
| `toolOrder` | — | Explicit model-facing tool order, as a list of `ToolSchema.name`s with one `'<unlisted-tools>'` rest entry (`TOOL_ORDER_REST`): listed tools take their listed position, unlisted tools land at the rest entry in lexicographic name order. Absent ⇒ plain lexicographic name order. Applied to the collected tools BEFORE the `system-prompt/assemble` waterfall — like the sections' `order` sort, it canonicalizes what the registry contributed (registration order is a plugin-load artifact), and a waterfall listener that mutates the list owns the determinism of what it emits. Misconfiguration fails loud: a list without exactly one rest entry, or with duplicates, throws at load; a listed name with no registered tool rejects every `assemble()`; a tool provider returning the reserved rest-entry name also rejects. Under the shipped loop the turn fails before any model request. Why a central list and not per-plugin weights: [Explicit model-facing tool order](../../../.agents/notes/implemented/feature/2026-07-06-explicit-tool-order.md). |
|
||||
@@ -17,7 +17,7 @@ System prompt assembly registry. Plugins contribute ordered sections, tool schem
|
||||
|
||||
### Public API
|
||||
|
||||
- `ctx.systemPrompt.section(section: PromptSection): () => void` Contribute a section. The layer is the calling context's scope: `agent.ctx` contributes to that agent alone, shadowing a same-named global section there. A `complete: true` section becomes the exact complete prompt after the assembly waterfall; more than one effective complete section rejects assembly. Duplicate names within one layer and non-finite orders throw. Disposed with the calling fiber.
|
||||
- `ctx.systemPrompt.section(section: PromptSection): () => void` Contribute a section. The layer is the calling context's scope: `agent.ctx` contributes to that agent alone, shadowing a same-named global section there. Sections sort by ascending order, then code-unit name for equal orders. A `complete: true` section becomes the exact complete prompt after the assembly waterfall; more than one effective complete section rejects assembly. Duplicate names within one layer and non-finite orders throw. Disposed with the calling fiber.
|
||||
- `ctx.systemPrompt.context(context: PromptContext): () => void` Contribute ordered dynamic context for the calling scope. Providers are evaluated for each eligible assembly and become a sourced runtime-context snapshot in model history under the shipped loop.
|
||||
- `ctx.systemPrompt.suppressRuntimeContext(): () => void` Suppress every dynamic-context contribution for the calling scope. Multiple registrations compose independently; disposing the returned effect restores context when no suppressor remains.
|
||||
- `ctx.systemPrompt.tools(provider: (context: AssembleContext) => ToolProviderResult): () => void` Contribute tool schemas, evaluated at each assembly with that assembly's context. `ToolProviderResult` = `{ schemas, knownNames? }`: `schemas` is the post-restriction visible set; `knownNames` is the pre-restriction universe used by `toolOrder`. A provider must not return a schema named `TOOL_ORDER_REST`. Scoped providers are consulted only for their scope's assemblies. Disposed with the calling fiber.
|
||||
@@ -31,7 +31,8 @@ System prompt assembly registry. Plugins contribute ordered sections, tool schem
|
||||
### Key types
|
||||
|
||||
- `AssembleContext` — what one `assemble()` call is FOR. Merge-extensible; declares `scope?: ScopeKey` (the layer selector) and `signal?: AbortSignal` (the explicit request control capability) here, while `dsh-agent` declares `agent?: Agent` (the typed DX field — never set without `scope`; use `assembleContextFor(agent, signal)`). Providers must tolerate absent fields because a bare `assemble()` carries an empty, scope-less, signal-less context. `signal` is a request value, not part of the ambient Agent execution frame.
|
||||
- `PromptSection` — `{ name, order, text, complete? }`. Sections are concatenated in ascending `order`. Order bands: `-100` is the harness identity, `0` the deployment persona, tool guidance uses `100–199`. One effective `complete` section suppresses all other sections after cooperative assembly.
|
||||
- `FIRST_PARTY_SECTION_ORDER` — the sparse named allocation for repository-owned sections. Values are unique integers whose adjacent allocated positions differ by at least ten; external sections may use any finite order.
|
||||
- `PromptSection` — `{ name, order, text, complete? }`. Sections are concatenated in ascending `order`, with equal values ordered by code-unit `name`. One effective `complete` section suppresses all other sections after cooperative assembly.
|
||||
- `PromptAssembly` — `{ sections: AssembledSection[], tools: ToolSchema[], variables: Record<string, string | undefined> }`. Section texts arrive resolved but not yet interpolated; `variables` holds every registered variable resolved against the context. Tool schemas are part of the assembly by design: "what the model is told it can do" is one coherent thing, even though adapters transmit schemas as a separate wire field.
|
||||
- `renderPrompt(assembly)` — interpolates `{{variable}}` references in each section, drops empty sections, joins with blank lines. STRICT: an unknown reference (`Object.hasOwn` lookup — prototype names like `{{constructor}}` are unknown), a registered-but-valueless reference, a malformed complete `{{…}}` group, or a `{{` that opens no complete group while a `}}` still follows (`{{{model}}}`) throws — fail loud beats shipping a malformed prompt. A lone `{{` with no `}}` anywhere after it passes through verbatim; substituted values are never re-scanned.
|
||||
|
||||
@@ -44,7 +45,7 @@ Merge-extensible: plugins can declare extra fields on `PromptAssembly` and `Asse
|
||||
- Tool schema providers: `ToolRuntime` registers itself as a tool provider automatically.
|
||||
- The [`system-prompt/assemble` waterfall](#live-events): cooperatively mutate or replace the assembly per caller before any complete-section constraint is enforced.
|
||||
|
||||
Design rationale: [the prompt-variables Agent Note](../../../.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md).
|
||||
Design rationale: [the prompt-variables Agent Note](../../../.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md) and [the first-party order allocation](../../../.agents/notes/implemented/architecture/2026-08-25-sparse-first-party-prompt-section-orders.md).
|
||||
|
||||
## Model Experience
|
||||
|
||||
@@ -87,4 +88,3 @@ Prefix-stable while the visible schema set, rendering, and order are unchanged.
|
||||
- **Deployment-authored prompt text is config/composition only** — this plugin owns the global persona default, creator plugins may register agent-scoped shadows, and other sections come from the plugin that owns the fact; there is no end-user prompt-editing API.
|
||||
- **No escape syntax for literal `{{…}}` braces** — every complete group is interpolated against registered variables; an escape is deferred until a real prompt needs one.
|
||||
- **`toolOrder` misconfiguration surfaces at prompt assembly (the first turn), not at boot** — only shape violations throw at config load.
|
||||
- **Sections sharing an `order` value tie-break by registration order** — a plugin-load artifact; determinism relies on the distinct-order band convention, unlike the canonicalized tool order.
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
| 键 | 默认值 | 含义 |
|
||||
|---|---|---|
|
||||
| `includeHarnessIdentity` | `true` | 是否包含顺序为 −100 的固定开场白 `You are an AI agent powered by DeepSeek Harness.`。仅当兼容性部署拥有完整系统提示词时设为 false。 |
|
||||
| `includeHarnessIdentity` | `true` | 是否包含顺序为 −1000 的 first-party 固定开场白 `You are an AI agent powered by DeepSeek Harness.`。仅当兼容性部署拥有完整系统提示词时设为 false。 |
|
||||
| `includeRuntimeContext` | `true` | 是否在组装中包含有序动态上下文。设为 false 时不会求值上下文提供方,并会在 waterfall 后丢弃 `system-prompt/assemble` 监听器添加的上下文;其他服务及其强制机制仍然生效。 |
|
||||
| `persona` | `''` | 全局部署 persona 默认值:唯一由配置提供的提示词片段,渲染为顺序为 0 的 `deployment:persona` 段,除非 agent 作用域的贡献将其遮蔽。它是模板,完整的 `{{…}}` 组会严格按已注册变量解释(随附循环注册 `{{model}}`/`{{cwd}}`),目前没有表达字面量花括号的转义语法。为空 ⇒ 渲染时删除该段。 |
|
||||
| `toolOrder` | 无 | 显式指定面向模型的工具顺序。该列表由 `ToolSchema.name` 组成,并且必须恰好包含一个 `'<unlisted-tools>'` 其余项标记(`TOOL_ORDER_REST`):已列工具按列表位置排列,未列工具则按名称字典序插入该标记所在的位置。缺席 ⇒ 直接按名称字典序排列。该顺序会在 `system-prompt/assemble` waterfall(瀑布式事件)之前应用于已收集的工具。与段的 `order` 排序一样,它会规范化注册表贡献的内容;注册顺序只是插件加载时序的产物。修改列表的 waterfall 监听器对其输出的确定性负责。配置错误会明确失败:列表没有恰好一个其余项或存在重复项,会在加载时抛出;已列名称没有对应已注册工具,会使每次 `assemble()` 被拒绝;工具提供方返回保留的其余项名称也会被拒绝。在随附循环下,轮次会在任何模型请求前失败。为何采用中心列表而非每插件权重,见[显式面向模型工具顺序](../../../.agents/notes/implemented/feature/2026-07-06-explicit-tool-order.zh.md)。 |
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
### 公开 API
|
||||
|
||||
- `ctx.systemPrompt.section(section: PromptSection): () => void`:贡献一个段。层由调用上下文的作用域决定:`agent.ctx` 只为该 agent 贡献,并在该处遮蔽同名全局段。一个 `complete: true` 段会在组装 waterfall 之后成为精确的完整提示词;有效 complete 段超过一个时,组装会被拒绝。同一层中的重复名称和非有限顺序会抛出。随调用 fiber 一并 dispose(资源释放)。
|
||||
- `ctx.systemPrompt.section(section: PromptSection): () => void`:贡献一个段。层由调用上下文的作用域决定:`agent.ctx` 只为该 agent 贡献,并在该处遮蔽同名全局段。各段先按 order 升序排列,同号时再按名称的代码单元顺序排列。一个 `complete: true` 段会在组装 waterfall 之后成为精确的完整提示词;有效 complete 段超过一个时,组装会被拒绝。同一层中的重复名称和非有限顺序会抛出。随调用 fiber 一并 dispose(资源释放)。
|
||||
- `ctx.systemPrompt.context(context: PromptContext): () => void`:为调用作用域贡献有序动态上下文。每次符合条件的组装都会求值提供方,并在随附循环下成为模型历史中带来源的 runtime-context 快照。
|
||||
- `ctx.systemPrompt.suppressRuntimeContext(): () => void`:抑制调用作用域的所有动态上下文贡献。多个注册会独立组合;只有当不再存在抑制器时,dispose 返回的 effect 才会恢复上下文。
|
||||
- `ctx.systemPrompt.tools(provider: (context: AssembleContext) => ToolProviderResult): () => void`:贡献工具 schema;每次组装时使用该次组装的上下文求值。`ToolProviderResult` = `{ schemas, knownNames? }`:`schemas` 是限制后的可见集合;`knownNames` 是限制前由 `toolOrder` 使用的全集。提供方不得返回名为 `TOOL_ORDER_REST` 的 schema。带作用域提供方只在其作用域的组装中查询。随调用 fiber 一并 dispose。
|
||||
@@ -33,7 +33,8 @@
|
||||
### 关键类型
|
||||
|
||||
- `AssembleContext`:说明一次 `assemble()` 调用的用途。它可通过合并扩展;此处声明 `scope?: ScopeKey`(层选择器)与 `signal?: AbortSignal`(显式请求控制能力),而 `dsh-agent` 声明 `agent?: Agent`(类型化 DX 字段;绝不能在没有 `scope` 时设置,应使用 `assembleContextFor(agent, signal)`)。提供方必须容忍字段缺席,因为裸 `assemble()` 携带的是无作用域、无信号的空上下文。`signal` 是请求值,不是环境 Agent 执行 frame 的一部分。
|
||||
- `PromptSection`:`{ name, order, text, complete? }`。各段按 `order` 升序拼接。顺序区间:`-100` 是 harness 身份,`0` 是部署 persona,工具引导使用 `100–199`。协作式组装完成后,一个有效的 `complete` 段会抑制其他所有段。
|
||||
- `FIRST_PARTY_SECTION_ORDER`:仓库自带提示词段的稀疏具名分配表。所有值都是互不相同的整数,相邻已分配位置之差至少为十;外部段可以使用任意有限 order。
|
||||
- `PromptSection`:`{ name, order, text, complete? }`。各段按 `order` 升序拼接,同号时按代码单元 `name` 排列。协作式组装完成后,一个有效的 `complete` 段会抑制其他所有段。
|
||||
- `PromptAssembly`:`{ sections: AssembledSection[], tools: ToolSchema[], variables: Record<string, string | undefined> }`。各段文本到达时已求值,但尚未插值;`variables` 保存所有已注册变量在当前上下文中求得的值。工具 schema 按设计属于组装结果:「模型获知自己能做什么」是一个连贯整体,尽管适配器把 schema 作为独立 wire 字段传输。
|
||||
- `renderPrompt(assembly)`:插值每个段中的 `{{variable}}` 引用,删除空段,并用空行连接。严格规则:未知引用(使用 `Object.hasOwn` 查找,因此 `{{constructor}}` 等原型名称未知)、已注册但无值的引用、格式错误的完整 `{{…}}` 组,或出现 `{{` 却没有形成完整组、而后文仍有 `}}`(`{{{model}}}`),都会抛出异常;明确失败胜过交付格式错误的提示词。孤立的 `{{` 如果后面任何位置都没有 `}}`,会按字面量通过;替换值绝不再次扫描。
|
||||
|
||||
@@ -46,7 +47,7 @@
|
||||
- 工具 schema 提供方:`ToolRuntime` 自动将自身注册为工具提供方。
|
||||
- [`system-prompt/assemble` waterfall](#live-events):按调用方协作式修改或替换组装结果,之后再实施 complete 段约束。
|
||||
|
||||
设计原理:[提示词变量 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.zh.md)。
|
||||
设计原理:[提示词变量 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.zh.md)与 [first-party 顺序分配](../../../.agents/notes/implemented/architecture/2026-08-25-sparse-first-party-prompt-section-orders.zh.md)。
|
||||
|
||||
## 模型体验
|
||||
|
||||
@@ -89,4 +90,3 @@ schema token 在每次请求中重复。限制工具会为该 agent 移除其全
|
||||
- **部署方编写的提示词文本只来自配置/组合**:此插件拥有全局 persona 默认值;创建方插件可以注册 agent 作用域的遮蔽项;其他段来自拥有相应事实的插件。不存在终端用户提示词编辑 API。
|
||||
- **没有表示字面量 `{{…}}` 花括号的转义语法**:每个完整组都会按已注册变量插值;只有实际提示词需要转义时才会实现。
|
||||
- **`toolOrder` 配置错误在提示词组装(首轮)时出现,而不是启动时**:只有形状违规会在配置加载时抛出。
|
||||
- **共享同一 `order` 值的段按注册顺序打破平局**:这是插件加载产物;确定性依赖在顺序分段内使用不同值的约定,与已规范化的工具顺序不同。
|
||||
|
||||
@@ -54,9 +54,9 @@ export interface PromptSection {
|
||||
/** Unique name — a duplicate registration throws (see {@link SystemPrompt.section}). */
|
||||
readonly name: string
|
||||
/**
|
||||
* Sections are concatenated in ascending order. Convention: `-100` is the
|
||||
* harness identity, `0` the deployment persona, tool guidance uses 100–199;
|
||||
* other negative orders also render before the persona.
|
||||
* Sections are concatenated in ascending order. Equal orders use code-unit
|
||||
* name order. Repository-owned placements use
|
||||
* {@link FIRST_PARTY_SECTION_ORDER}.
|
||||
*/
|
||||
readonly order: number
|
||||
/**
|
||||
@@ -119,6 +119,47 @@ export interface PromptAssembly {
|
||||
variables: Record<string, string | undefined>
|
||||
}
|
||||
|
||||
/**
|
||||
* Sparse integer placements for repository-owned prompt sections.
|
||||
*
|
||||
* Adjacent values differ by at least ten to keep the first-party groups sparse
|
||||
* and make accidental collisions mechanically detectable.
|
||||
* External plugins may use any finite order; equal orders are deterministic by
|
||||
* section name.
|
||||
*/
|
||||
export const FIRST_PARTY_SECTION_ORDER = {
|
||||
HARNESS_IDENTITY: -1000,
|
||||
HARNESS_SOURCE: -900,
|
||||
WEB_SURFACE: -800,
|
||||
DEPLOYMENT_PERSONA: 0,
|
||||
PLAN_POLICY: 500,
|
||||
TEAM_POLICY: 600,
|
||||
CODE_ONLY: 800,
|
||||
FILE_REFERENCE: 900,
|
||||
TOOL_BASH: 1000,
|
||||
TOOL_PWSH: 1010,
|
||||
TOOL_READ: 1100,
|
||||
TOOL_WRITE: 1200,
|
||||
TOOL_EDIT: 1300,
|
||||
TOOL_GLOB: 1400,
|
||||
TOOL_GREP: 1500,
|
||||
TOOL_JOBS: 1600,
|
||||
TOOL_PTY: 1700,
|
||||
TOOL_WEB_SEARCH: 2000,
|
||||
TOOL_WEB_FETCH: 2100,
|
||||
TOOL_LSP: 2200,
|
||||
TOOL_SESSION_QUERY: 2300,
|
||||
TOOL_GOAL: 2400,
|
||||
TOOL_CORDIS: 2500,
|
||||
TOOL_WORKFLOW: 2600,
|
||||
TOOL_RALPH: 2700,
|
||||
TOOL_SUBAGENT: 2800,
|
||||
TOOL_REPORT: 2900,
|
||||
TOOLS_SDK: 5000,
|
||||
DELIVERABLE_FILE_REFERENCES: 9000,
|
||||
STRUCTURED_OUTPUT: 9900,
|
||||
} as const
|
||||
|
||||
/**
|
||||
* The deployment persona's section name and order. Exported because a
|
||||
* composition can replace this slot — an agent preset shadows the
|
||||
@@ -127,8 +168,8 @@ export interface PromptAssembly {
|
||||
*/
|
||||
export const PERSONA_SECTION = 'deployment:persona'
|
||||
|
||||
/** Prompt order of the persona slot; the first section a model reads. */
|
||||
export const PERSONA_ORDER = 0
|
||||
/** Prompt order of the persona slot. */
|
||||
export const PERSONA_ORDER = FIRST_PARTY_SECTION_ORDER.DEPLOYMENT_PERSONA
|
||||
|
||||
/** Valid variable names: how they are written between the braces. */
|
||||
const VARIABLE_NAME = /^[a-z][a-z0-9_]*$/
|
||||
@@ -177,9 +218,19 @@ function orderTools(tools: ToolSchema[], toolOrder: string[] | undefined, knownN
|
||||
name === TOOL_ORDER_REST ? rest : tools.filter(tool => tool.name === name))
|
||||
}
|
||||
|
||||
/** Lexicographic (code-unit) name comparison — locale-independent, so the order is identical on every machine. */
|
||||
/** Code-unit name comparison — locale-independent, so the order is identical on every machine. */
|
||||
function compareNames(a: string, b: string): number {
|
||||
return a < b ? -1 : a > b ? 1 : 0
|
||||
}
|
||||
|
||||
/** Order prompt sections by their explicit placement, then deterministically by name. */
|
||||
function comparePromptSections(a: PromptSection, b: PromptSection): number {
|
||||
return a.order - b.order || compareNames(a.name, b.name)
|
||||
}
|
||||
|
||||
/** Order tool schemas lexicographically by name. */
|
||||
function compareToolNames(a: ToolSchema, b: ToolSchema): number {
|
||||
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0
|
||||
return compareNames(a.name, b.name)
|
||||
}
|
||||
|
||||
/** Plugin config: the deployment-authored fragment of the system prompt (see {@link Config.persona} for its contract). */
|
||||
@@ -357,7 +408,7 @@ export class SystemPrompt extends Service {
|
||||
if (config.includeHarnessIdentity ?? true) {
|
||||
this.section({
|
||||
name: 'harness:identity',
|
||||
order: -100,
|
||||
order: FIRST_PARTY_SECTION_ORDER.HARNESS_IDENTITY,
|
||||
text: 'You are an AI agent powered by DeepSeek Harness.',
|
||||
})
|
||||
}
|
||||
@@ -480,7 +531,7 @@ export class SystemPrompt extends Service {
|
||||
variables[name] = provider(context)
|
||||
}
|
||||
}
|
||||
// Scoped sections shadow globals before the stable order sort.
|
||||
// Scoped sections shadow globals before the deterministic order sort.
|
||||
const sectionByName = this.layers.merge(scope, layer => layer.sections)
|
||||
const contextByName = this.layers.merge(scope, layer => layer.contexts)
|
||||
// Validate order against pre-restriction names while collecting visible schemas.
|
||||
@@ -501,7 +552,7 @@ export class SystemPrompt extends Service {
|
||||
collected.push(...schemas)
|
||||
for (const name of acceptedKnownNames) knownNames.add(name)
|
||||
}
|
||||
const sectionDefinitions = [...sectionByName.values()].sort((a, b) => a.order - b.order)
|
||||
const sectionDefinitions = [...sectionByName.values()].sort(comparePromptSections)
|
||||
const completeSections = sectionDefinitions.filter(section => section.complete === true)
|
||||
if (completeSections.length > 1) {
|
||||
throw new Error(`multiple complete prompt sections are active: ${completeSections.map(section => JSON.stringify(section.name)).join(', ')}`)
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import SystemPrompt, { AssembleContext, PromptAssembly, renderContextSnapshot, renderPrompt } from '@deepseek-ai/dsh-system-prompt'
|
||||
import SystemPrompt, {
|
||||
AssembleContext, FIRST_PARTY_SECTION_ORDER, PromptAssembly, renderContextSnapshot, renderPrompt,
|
||||
} from '@deepseek-ai/dsh-system-prompt'
|
||||
|
||||
/**
|
||||
* Every assembly carries the plugin's own built-ins — `harness:identity`
|
||||
* (order −100) and `deployment:persona` (order 0, from config). Tests about
|
||||
* and `deployment:persona` (from config). Tests about
|
||||
* registry MECHANICS strip them with {@link contributed} to stay focused on
|
||||
* their own sections; the built-ins' behavior is pinned by its own describe.
|
||||
*/
|
||||
@@ -15,6 +17,14 @@ function contributed(assembly: PromptAssembly): PromptAssembly['sections'] {
|
||||
}
|
||||
|
||||
describe('SystemPrompt', () => {
|
||||
it('keeps first-party section placements unique, integral, and at least ten apart', () => {
|
||||
const orders = Object.values(FIRST_PARTY_SECTION_ORDER)
|
||||
expect(orders.every(Number.isInteger)).toBe(true)
|
||||
expect(new Set(orders).size).toBe(orders.length)
|
||||
const sorted = [...orders].sort((a, b) => a - b)
|
||||
expect(sorted.slice(1).every((order, index) => order - sorted[index]! >= 10)).toBe(true)
|
||||
})
|
||||
|
||||
describe('built-in sections', () => {
|
||||
it('registers the harness identity and the configured deployment persona', async () => {
|
||||
const ctx = new Context()
|
||||
@@ -100,6 +110,15 @@ describe('SystemPrompt', () => {
|
||||
expect(renderContextSnapshot(assembly)).toBe('Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\ncontext 1\n\ncontext 2')
|
||||
})
|
||||
|
||||
it('breaks equal section orders by code-unit name regardless of registration order', async () => {
|
||||
for (const names of [['äther', 'zeta'], ['zeta', 'äther']] as const) {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt)
|
||||
for (const name of names) ctx.systemPrompt.section({ name, order: 10, text: name })
|
||||
expect(contributed(await ctx.systemPrompt.assemble()).map(section => section.name)).toEqual(['zeta', 'äther'])
|
||||
}
|
||||
})
|
||||
|
||||
it('resolves section text providers against the assemble context, at each assemble call', async () => {
|
||||
// The context is HOW per-agent sections work (the loop passes { agent });
|
||||
// this spec stays agent-agnostic and smuggles a marker through a plain field.
|
||||
@@ -247,7 +266,7 @@ describe('SystemPrompt', () => {
|
||||
it('composes multiple system-prompt/assemble waterfall listeners in order, with the context', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt)
|
||||
ctx.systemPrompt.section({ name: 'base', order: 0, text: 'base' })
|
||||
ctx.systemPrompt.section({ name: 'base', order: 10, text: 'base' })
|
||||
|
||||
// Listener A appends a section, then delegates.
|
||||
const contexts: AssembleContext[] = []
|
||||
@@ -314,7 +333,7 @@ describe('SystemPrompt', () => {
|
||||
it('assembles snapshots so one-step mutations do not leak into future assemblies', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt)
|
||||
ctx.systemPrompt.section({ name: 'base', order: 0, text: 'base' })
|
||||
ctx.systemPrompt.section({ name: 'base', order: 10, text: 'base' })
|
||||
ctx.systemPrompt.tools(() => ({ schemas: [{ name: 't', description: 'tool', parameters: { type: 'object', properties: {} } }] }))
|
||||
|
||||
const first = await ctx.systemPrompt.assemble()
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/core/tools/README.md
|
||||
README.md: a140255a04187d4f2206df0f90c950d7608ee8ac
|
||||
README.zh.md: f519ba0a364f05ab63967c07ffd6681a5f62a70a
|
||||
README.md: 98f5d260e023e6b301590bbb4e4eda3beb7ee9ff
|
||||
README.zh.md: 98045426b4c18317cd7685a907ce7042ca322269
|
||||
|
||||
@@ -119,7 +119,7 @@ Under `code` or `both`, the registry exposes the reserved `run_code` transport a
|
||||
|
||||
Under `code` — not `both` — the transport is also the only entry the model may use: a model-direct call naming any other visible tool resolves to `UNKNOWN_TOOL` at execution creation, before `tools/pre-execute`, approval `ask`, and guards, so nothing observes or approves a call that can only fail. The denial names the route back (`only \`run_code\` is callable directly — call \`<name>\` from inside a \`run_code\` program instead`), because the same prompt declares that tool and a bare `unknown tool` reads as a broken deployment. SDK sub-dispatches carry the outer execution's `parent` token and are exempt, so programs keep every binding the SDK declared. See the [executor-collapse note](../../../.agents/notes/implemented/bug-fix/2026-08-07-code-mode-executor-collapse.md), the [Code Mode foundation](../../../.agents/notes/implemented/feature/2026-06-15-code-mode.md), [typed-return contract](../../../.agents/notes/implemented/feature/2026-07-20-code-mode-typed-tool-returns.md), and [code-runtime seam](../../code-runtime/README.md). Try `pnpm run demo:code-mode`.
|
||||
|
||||
- **The SDK section** (`tools:sdk`, order 150): a lazy prompt section regenerating the language-appropriate SDK text at each assembly. In the TypeScript flavor it emits `JsonValue`, exact `ToolArgsMap` / `ToolOutputMap`, `ToolName`, the `ToolCallError` declaration, and a mapped `tools` namespace for the calling scope's visible end capabilities (exotic names via quoted keys), plus fixed usage instructions; the Python flavor (`ctx.codeRuntime.language === 'python'`) emits the equivalent named `TypedDict`s and a `tools` object with matching usage instructions. Deterministic — lexicographic tool order, byte-identical text for an unchanged tool set (prefix-cache-friendly). Both codegens are exported and never throw during prompt assembly: `jsonSchemaToTs` handles every unified schema construct and degrades unsupported raw constructs to `unknown`; `jsonSchemaToPy` does the same, degrading to `Any` (and a whole object to `dict[str, Any]` when a field name is not a legal `TypedDict` attribute, or whenever it is called outside the SDK render, which supplies the naming context a `TypedDict` declaration needs).
|
||||
- **The SDK section** (`tools:sdk`, first-party order 5000): a lazy prompt section regenerating the language-appropriate SDK text at each assembly. In the TypeScript flavor it emits `JsonValue`, exact `ToolArgsMap` / `ToolOutputMap`, `ToolName`, the `ToolCallError` declaration, and a mapped `tools` namespace for the calling scope's visible end capabilities (exotic names via quoted keys), plus fixed usage instructions; the Python flavor (`ctx.codeRuntime.language === 'python'`) emits the equivalent named `TypedDict`s and a `tools` object with matching usage instructions. Deterministic — lexicographic tool order, byte-identical text for an unchanged tool set (prefix-cache-friendly). Both codegens are exported and never throw during prompt assembly: `jsonSchemaToTs` handles every unified schema construct and degrades unsupported raw constructs to `unknown`; `jsonSchemaToPy` does the same, degrading to `Any` (and a whole object to `dict[str, Any]` when a field name is not a legal `TypedDict` attribute, or whenever it is called outside the SDK render, which supplies the naming context a `TypedDict` declaration needs).
|
||||
- **The dispatch bridge** (`run_code`'s execute): every binding call is snapshotted as lossless JSON before dispatch, scheduled through a per-run pool that reuses the native concurrency contract — calls start strictly in submission order, consecutive `isConcurrencySafe` calls overlap up to the validated `maxParallelSubCalls` config (default 10; `1` restores serial dispatch), and an exclusive-classified call drains the pool, runs alone, and bars later calls — given the outer execution's opaque token as `parent`, and run through the complete pre-execute → guards → execute → post-execute → result pipeline. A success returns the final canonical value after policy; a failure reaches the worker as one message and becomes `ToolCallError(toolName, message)`. Each started sub-call logs a `tool/code-dispatch-start` event (deterministic id `<parent>:code:<n>`, numbered by submission) at pipeline entry and settles with one `tool/code-dispatch` event carrying the complete model-facing `content`/`isError` outcome (the `tool/result` vocabulary, so UIs render sub-calls through the native path — the pair's `time` fields carry per-sub-call timing); a queued call abandoned by run settlement logs neither. `deriveMessages()` surfaces neither event nor persists the canonical value. Token correlation lets commit-style observers defer an inner success until the final `run_code` result without exposing the live outer execution; ordinary tool side effects are not rolled back. Every sub-call `additionalContexts` entry and every successful final content sequence containing an image is deferred through the outer `ToolRunContext` in dispatch order; the loop appends those contexts only after the parent `run_code` result, preserving adjacency and source attribution even when the program later fails.
|
||||
- **Settlement discipline**: the bridge owns a run-scoped abort that follows the outer signal in and fires when the run settles for any reason, so a budget expiry aborts an in-flight sub-tool instead of orphaning it; the bridge then drains its queue BEFORE returning, so every `tool/code-dispatch` lands inside the open turn. A failed run throws `CodeRunFailedError` (`code: 'CODE_RUN_FAILED'`, message = the failure kind + captured logs), which the pipeline converts to a structured `isError` the model self-corrects from.
|
||||
- **Result size**: intermediate binding values cross the worker process whole and have no per-binding byte cap. `run_code` returns canonical `{ logs: string[], result?: JsonValue }`; strings render raw, every other present JSON root renders through a stack-safe pretty JSON traversal whose total indentation is capped at ten characters (deeper subtrees stay compact), `null` remains explicit, and absent `result` means the program returned `undefined`. The worker's configurable `maxOutputBytes` (default 64 MiB) applies only to the combined serialized outer log-array, completion-value, or failure-message payloads; fixed result-envelope syntax and presentation whitespace are outside that limit. Invalid and over-limit completions fail explicitly, and only this outer result is eligible for ordinary spill.
|
||||
|
||||
@@ -121,7 +121,7 @@ ctx.tools.register(defineTool({
|
||||
|
||||
在 `code`(而非 `both`)下,该传输同时也是模型唯一可用的入口:模型直呼其他任何可见工具名,都会在创建执行时、早于 `tools/pre-execute`、审批 `ask` 和 guards 解析为 `UNKNOWN_TOOL`,因此没有任何一方会观察或批准一个注定失败的调用。拒绝信息会给出正确路径(`only \`run_code\` is callable directly — call \`<name>\` from inside a \`run_code\` program instead`),因为同一份提示词刚刚声明过那个工具,只说 `unknown tool` 会被读成部署损坏。SDK 子分发携带外层执行的 `parent` token,不受此限制,因此程序保留 SDK 声明的全部绑定。参见[执行器塌缩 note](../../../.agents/notes/implemented/bug-fix/2026-08-07-code-mode-executor-collapse.md)、[Code Mode 基础](../../../.agents/notes/implemented/feature/2026-06-15-code-mode.md)、[类型化返回约定](../../../.agents/notes/implemented/feature/2026-07-20-code-mode-typed-tool-returns.md)和[代码运行时 seam](../../code-runtime/README.md)。可以运行 `pnpm run demo:code-mode` 试用。
|
||||
|
||||
- **SDK 段**(`tools:sdk`,顺序 150):一个在组装时求值的提示词段,每次组装都会重新生成与所加载运行时语言相符的 SDK 文本。TypeScript 形态会生成 `JsonValue`、精确的 `ToolArgsMap` / `ToolOutputMap`、`ToolName`、`ToolCallError` 声明,以及映射调用作用域最终可见工具的 `tools` 命名空间(特殊名称使用带引号的键),并附带固定的使用说明;Python 形态(`ctx.codeRuntime.language === 'python'`)发出等价的具名 `TypedDict` 与一个带相同用法说明的 `tools` 对象。其输出具有确定性:工具按字典序排列;工具集合不变时,文本逐字节相同(有利于前缀 cache)。两个代码生成器都已导出,且绝不会在提示词组装期间抛出:`jsonSchemaToTs` 处理统一 schema 的每种构造并将不受支持的原始构造降级为 `unknown`;`jsonSchemaToPy` 同理,降级为 `Any`(当某字段名不是合法的 `TypedDict` 属性时,或在 SDK 渲染之外被调用时——`TypedDict` 声明所需的命名上下文由该渲染提供——整个对象降级为 `dict[str, Any]`)。
|
||||
- **SDK 段**(`tools:sdk`,first-party 顺序 5000):一个在组装时求值的提示词段,每次组装都会重新生成与所加载运行时语言相符的 SDK 文本。TypeScript 形态会生成 `JsonValue`、精确的 `ToolArgsMap` / `ToolOutputMap`、`ToolName`、`ToolCallError` 声明,以及映射调用作用域最终可见工具的 `tools` 命名空间(特殊名称使用带引号的键),并附带固定的使用说明;Python 形态(`ctx.codeRuntime.language === 'python'`)发出等价的具名 `TypedDict` 与一个带相同用法说明的 `tools` 对象。其输出具有确定性:工具按字典序排列;工具集合不变时,文本逐字节相同(有利于前缀 cache)。两个代码生成器都已导出,且绝不会在提示词组装期间抛出:`jsonSchemaToTs` 处理统一 schema 的每种构造并将不受支持的原始构造降级为 `unknown`;`jsonSchemaToPy` 同理,降级为 `Any`(当某字段名不是合法的 `TypedDict` 属性时,或在 SDK 渲染之外被调用时——`TypedDict` 声明所需的命名上下文由该渲染提供——整个对象降级为 `dict[str, Any]`)。
|
||||
- **分发桥接层**(`run_code` 的 execute):每个绑定调用都会在分发前快照为无损 JSON,经由每次运行独有、复用原生并发约定的池调度——调用严格按提交顺序启动,连续的 `isConcurrencySafe` 调用最多可重叠经校验的 `maxParallelSubCalls` 配置个(默认 10;设为 `1` 即恢复串行分发),被分类为独占的调用先排空池、单独运行并阻挡其后的调用——以外层执行的不透明 token 作为 `parent`,并经过完整的 pre-execute → guards → execute → post-execute → result 流水线。成功会返回策略处理后的最终规范值;失败以一条消息到达 worker,并成为 `ToolCallError(toolName, message)`。每个已启动的子调用在进入流水线时记录一条 `tool/code-dispatch-start` 事件(确定性 id `<parent>:code:<n>`,按提交顺序编号),并以一条携带完整模型可见 `content`/`isError` 结果的 `tool/code-dispatch` 事件完结(采用 `tool/result` 词汇,因此 UI 会沿原生路径呈现子调用——这对事件的 `time` 字段承载每个子调用的计时);因 run 结算而被放弃的排队调用两者都不记录。`deriveMessages()` 既不公开这两个事件,也不持久化规范值。token 关联使按提交语义工作的观察器可以延后提交内部调用的成功结果,直到最终 `run_code` 结果确定,而无需暴露进行中的外层执行;普通工具副作用不会回滚。每个子调用的 `additionalContexts` 条目以及每份成功且含图片的最终内容序列都会按分发顺序通过外层 `ToolRunContext` 延迟;循环只在父级 `run_code` 结果之后追加这些上下文,从而保持相邻关系和来源归属,即使程序后来失败也不例外。
|
||||
- **结算纪律**:桥接层拥有一个运行作用域的中止机制;该中止会跟随传入的外层信号,并在运行因任何原因结算时触发,因此预算耗尽会中止正在运行的子工具,而不会将其遗留。桥接层随后会在返回之前排空队列,使每个 `tool/code-dispatch` 都落在仍打开的轮次内。失败的运行会抛出 `CodeRunFailedError`(`code: 'CODE_RUN_FAILED'`,message = 失败类型 + 已捕获日志),流水线会将其转换为模型可据以自我修正的结构化 `isError`。
|
||||
- **结果大小**:中间绑定值会完整传入 worker 进程,且没有逐绑定字节上限。`run_code` 返回规范的 `{ logs: string[], result?: JsonValue }`;字符串原样呈现,其他所有存在的 JSON 根都通过栈安全的美化 JSON 遍历呈现,总缩进最多为 10 个字符(更深的子树保持紧凑),`null` 保持显式,而缺少 `result` 表示程序返回 `undefined`。worker 可配置的 `maxOutputBytes`(默认 64 MiB)只应用于组合序列化后的外层日志数组、完成值或失败消息载荷;固定的结果封装语法和呈现空白不计入该上限。无效和超限的完成会明确失败,只有这个外层结果可以按常规 spill 机制处理。
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
||||
import type { CodeBindingFunction, CodeRunResult, CodeRuntime } from '@deepseek-ai/dsh-code-runtime'
|
||||
import { snapshotJsonValue } from '@deepseek-ai/dsh-session'
|
||||
import type { JsonValue } from '@deepseek-ai/dsh-session'
|
||||
import { FIRST_PARTY_SECTION_ORDER } from '@deepseek-ai/dsh-system-prompt'
|
||||
import { defineTool, parameterSchemaSpecToJsonSchema } from './schema.ts'
|
||||
import { TOOL_RUNTIME_SCHEDULER } from './index.ts'
|
||||
import type { CodeDispatchLog, ToolDefinition, ToolExecutionResult, ToolRuntime, ToolRunContext } from './index.ts'
|
||||
@@ -19,8 +20,8 @@ import type {} from './types.ts'
|
||||
/** The model-facing name of the Code Mode tool. */
|
||||
export const RUN_CODE_NAME = 'run_code'
|
||||
|
||||
/** The `tools:sdk` section order: inside the 100–199 tool-guidance band, after per-tool guidance sections. */
|
||||
export const SDK_SECTION_ORDER = 150
|
||||
/** The `tools:sdk` section order, after per-tool guidance sections. */
|
||||
export const SDK_SECTION_ORDER = FIRST_PARTY_SECTION_ORDER.TOOLS_SDK
|
||||
|
||||
/**
|
||||
* The language-specific `run_code` schema text: the tool `description` and its
|
||||
|
||||
@@ -13,7 +13,7 @@ import { assertNever, deepFreeze, HarnessError } from '@deepseek-ai/dsh-llm'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { snapshotJsonValue } from '@deepseek-ai/dsh-session'
|
||||
import type { JsonValue, UserMessage } from '@deepseek-ai/dsh-session'
|
||||
import type { ToolProviderResult } from '@deepseek-ai/dsh-system-prompt'
|
||||
import { FIRST_PARTY_SECTION_ORDER, type ToolProviderResult } from '@deepseek-ai/dsh-system-prompt'
|
||||
import type { CodeRuntime } from '@deepseek-ai/dsh-code-runtime'
|
||||
// Type-only: makes `ctx.get('approval')` resolve to the ApprovalService
|
||||
// augmentation. The seam stays optional at runtime — see `serviceAsk`.
|
||||
@@ -45,10 +45,10 @@ import { renderToolsSdkPy } from './py-types.ts'
|
||||
*/
|
||||
/**
|
||||
* Prompt order of the `code` collapse statement: after the persona and before
|
||||
* the 100-199 per-tool guidance band, so the model reads which tools it may
|
||||
* call before it reads what each one is for.
|
||||
* per-tool guidance, so the model reads which tools it may call before it
|
||||
* reads what each one is for.
|
||||
*/
|
||||
const COLLAPSE_SECTION_ORDER = 99
|
||||
const COLLAPSE_SECTION_ORDER = FIRST_PARTY_SECTION_ORDER.CODE_ONLY
|
||||
|
||||
/**
|
||||
* The model-facing statement of the `code` collapse. Names the consequence
|
||||
@@ -842,12 +842,11 @@ export class ToolRuntime extends Service {
|
||||
* {@link sdkSection} is and rendering empty outside an effective `code`.
|
||||
*
|
||||
* Every tool contributes its own guidance section naming its tool, none of
|
||||
* them qualify how that tool is reached, and they all render before the SDK
|
||||
* (orders 100-199 against {@link SDK_SECTION_ORDER}). Without this the model
|
||||
* reads a catalog of tools it is told to use and no statement that only
|
||||
* `run_code` may be called, so it emits a native call, receives
|
||||
* `UNKNOWN_TOOL` for a tool the prompt just declared, and concludes the
|
||||
* deployment is inconsistent. {@link COLLAPSE_SECTION_ORDER} places the rule
|
||||
* them qualify how that tool is reached, and they all render before the SDK.
|
||||
* Without this the model reads a catalog of tools it is told to use and no
|
||||
* statement that only `run_code` may be called, so it emits a native call,
|
||||
* receives `UNKNOWN_TOOL` for a tool the prompt just declared, and concludes
|
||||
* the deployment is inconsistent. {@link COLLAPSE_SECTION_ORDER} places the rule
|
||||
* before that guidance rather than after it.
|
||||
*
|
||||
* `both` renders empty: native calls do execute there, so the rule is false.
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Context } from '@deepseek-ai/cordis'
|
||||
import { createUserMessage, CallId } from '@deepseek-ai/dsh-llm'
|
||||
import { createScope } from '@deepseek-ai/dsh-scope'
|
||||
import type { Scope } from '@deepseek-ai/dsh-scope'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import SystemPrompt, { FIRST_PARTY_SECTION_ORDER } from '@deepseek-ai/dsh-system-prompt'
|
||||
import { CodeRuntime } from '@deepseek-ai/dsh-code-runtime'
|
||||
import type { CodeRunRequest, CodeRunResult } from '@deepseek-ai/dsh-code-runtime'
|
||||
import ToolRuntime, { CodeRunFailedError, RUN_CODE_NAME, TOOL_ABORTED_BEFORE_DISPATCH, defineContentToolFixture, defineTool } from '@deepseek-ai/dsh-tools'
|
||||
@@ -138,9 +138,13 @@ describe('mode-aware wire contribution', () => {
|
||||
it("mode 'code' states the run_code-only rule BEFORE the per-tool guidance that names each tool", async () => {
|
||||
const { ctx, systemPrompt } = await setup({ mode: 'code' })
|
||||
registerEcho(ctx)
|
||||
// Stand in for a real tool's guidance section, which sits in the 100-199
|
||||
// band and names its tool without saying how it is reached.
|
||||
ctx.systemPrompt.section({ name: 'tool:echo', order: 100, text: 'Use the echo tool.' })
|
||||
// Stand in for a real tool's guidance section, which names its tool without
|
||||
// saying how it is reached.
|
||||
ctx.systemPrompt.section({
|
||||
name: 'tool:echo',
|
||||
order: FIRST_PARTY_SECTION_ORDER.TOOL_READ,
|
||||
text: 'Use the echo tool.',
|
||||
})
|
||||
|
||||
const assembly = await systemPrompt.assemble()
|
||||
const names = assembly.sections.map(section => section.name)
|
||||
@@ -206,7 +210,11 @@ describe('mode-aware wire contribution', () => {
|
||||
const { ctx, systemPrompt } = await setup({ mode })
|
||||
registerEcho(ctx)
|
||||
const { scope, agent } = await mintAgentScope(ctx)
|
||||
scope.ctx.systemPrompt.section({ name: 'tools:sdk', order: 150, text: 'SCOPED SDK' })
|
||||
scope.ctx.systemPrompt.section({
|
||||
name: 'tools:sdk',
|
||||
order: FIRST_PARTY_SECTION_ORDER.TOOLS_SDK,
|
||||
text: 'SCOPED SDK',
|
||||
})
|
||||
|
||||
const scoped = await systemPrompt.assemble({ scope: agent })
|
||||
const global = await systemPrompt.assemble()
|
||||
@@ -290,7 +298,11 @@ describe('mode-aware wire contribution', () => {
|
||||
expect(() => ctx.tools.register(impostor)).toThrow(/reserved for the Code Mode presentation transport/)
|
||||
expect(() => scope.ctx.tools.restrict({ allow: [RUN_CODE_NAME] })).toThrow(/cannot name reserved Code Mode presentation transport/)
|
||||
expect(() => scope.ctx.tools.restrict({ deny: [RUN_CODE_NAME] })).toThrow(/cannot name reserved Code Mode presentation transport/)
|
||||
scope.ctx.systemPrompt.section({ name: 'scoped-note', order: 149, text: 'safe note' })
|
||||
scope.ctx.systemPrompt.section({
|
||||
name: 'scoped-note',
|
||||
order: FIRST_PARTY_SECTION_ORDER.TOOLS_SDK - 10,
|
||||
text: 'safe note',
|
||||
})
|
||||
scope.ctx.tools.register(defineContentToolFixture({
|
||||
name: 'scoped_safe',
|
||||
description: 'Safe scoped tool.',
|
||||
|
||||
@@ -5,6 +5,7 @@ import z from '@deepseek-ai/schemastery'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { TeamTaskId } from '@deepseek-ai/dsh-experimental-agent-team'
|
||||
import type { TeamMemberView } from '@deepseek-ai/dsh-experimental-agent-team'
|
||||
import { FIRST_PARTY_SECTION_ORDER } from '@deepseek-ai/dsh-system-prompt'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import type { InferValue, ValueSchemaSpec } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
@@ -163,7 +164,7 @@ function install(agent: Agent, ctx: Context, config: Required<Config>): () => vo
|
||||
try {
|
||||
register(scoped.systemPrompt.section({
|
||||
name: 'team:policy',
|
||||
order: 60,
|
||||
order: FIRST_PARTY_SECTION_ORDER.TEAM_POLICY,
|
||||
text: () => {
|
||||
const membership = ctx.agentTeams.membership(agent)
|
||||
return `${POLICY}\n\nYour Team role is ${membership.role}; your Team name is ${membership.name}; Team id is ${membership.id}.`
|
||||
|
||||
@@ -14,7 +14,7 @@ import type { JsonValue } from '@deepseek-ai/dsh-session'
|
||||
import type { UserMessage } from '@deepseek-ai/dsh-session'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import type { ToolExecution } from '@deepseek-ai/dsh-tools'
|
||||
import type {} from '@deepseek-ai/dsh-system-prompt'
|
||||
import { FIRST_PARTY_SECTION_ORDER } from '@deepseek-ai/dsh-system-prompt'
|
||||
import { missingServices, providedServices } from './inspect.ts'
|
||||
import {
|
||||
presentDefineCall, presentInspectListCall, presentInspectQueryCall, presentInspectSelfCall, presentRunCall,
|
||||
@@ -33,7 +33,11 @@ function requireAgent(exec: ToolExecution): Agent {
|
||||
|
||||
/** Register the Cordis tools and explicit `@pluginId` context injection. */
|
||||
export function apply(ctx: Context): void {
|
||||
ctx.systemPrompt.section({ name: 'tool:cordis', order: 115.5, text: CORDIS_SYSTEM_PROMPT })
|
||||
ctx.systemPrompt.section({
|
||||
name: 'tool:cordis',
|
||||
order: FIRST_PARTY_SECTION_ORDER.TOOL_CORDIS,
|
||||
text: CORDIS_SYSTEM_PROMPT,
|
||||
})
|
||||
for (const provider of hostInspectProviders(ctx)) {
|
||||
ctx.effect(() => ctx.cordisInspect.register(provider), `tool-cordis: inspect ${provider.manifest.id}`)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { sep } from 'node:path'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import type { GenericCallView, SearchResultView, ToolResult } from '@deepseek-ai/dsh-tools'
|
||||
import type { SpillRef } from '@deepseek-ai/dsh-spill'
|
||||
import type {} from '@deepseek-ai/dsh-system-prompt'
|
||||
import { FIRST_PARTY_SECTION_ORDER } from '@deepseek-ai/dsh-system-prompt'
|
||||
import { runRipgrep, toWorkdirRelative, trySaveFormattedResult } from './search-core.ts'
|
||||
import { globSearchMeta, searchViewFromMeta } from './presentation.ts'
|
||||
import { acceptedDirectCallValue } from './direct-call.ts'
|
||||
@@ -300,7 +300,7 @@ export function applyGlobTool(ctx: Context, caps: GlobToolCaps): void {
|
||||
: 'while a larger one keeps the modification-time-ordered head.'
|
||||
ctx.systemPrompt.section({
|
||||
name: 'tool:glob',
|
||||
order: 103,
|
||||
order: FIRST_PARTY_SECTION_ORDER.TOOL_GLOB,
|
||||
text: 'Use the glob tool — not shell find — to discover files by path pattern. A pattern with no "/" matches basenames at any depth, so "*" matches every file in the tree rather than its top level. '
|
||||
+ `Results are files only, never directories, and include hidden and ignored files: a result that fits comes back in modification-time order, ${overCapGuidance}`,
|
||||
})
|
||||
|
||||
@@ -16,7 +16,7 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import type { GenericCallView, SearchResultView, ToolResult } from '@deepseek-ai/dsh-tools'
|
||||
import type { RetainedItems } from '@deepseek-ai/dsh-output-retention'
|
||||
import type { SpillRef } from '@deepseek-ai/dsh-spill'
|
||||
import type {} from '@deepseek-ai/dsh-system-prompt'
|
||||
import { FIRST_PARTY_SECTION_ORDER } from '@deepseek-ai/dsh-system-prompt'
|
||||
import type { GrepMatch } from './search-core.ts'
|
||||
import { SearchError, previewLine, retainGrepMatches, runRipgrep, toWorkdirRelative, trySaveFormattedResult } from './search-core.ts'
|
||||
import { grepSearchMeta, searchViewFromMeta } from './presentation.ts'
|
||||
@@ -275,7 +275,7 @@ export function presentGrepResult(
|
||||
export function applyGrepTool(ctx: Context, caps: GrepToolCaps): void {
|
||||
ctx.systemPrompt.section({
|
||||
name: 'tool:grep',
|
||||
order: 104,
|
||||
order: FIRST_PARTY_SECTION_ORDER.TOOL_GREP,
|
||||
text: 'Use the grep tool — not shell grep or rg — to search file contents. Use read on a matched file when you need surrounding context.',
|
||||
})
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { Context } from '@deepseek-ai/cordis'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import type { DiffCallView, DiffResultView, ToolResult } from '@deepseek-ai/dsh-tools'
|
||||
import type {} from '@deepseek-ai/dsh-fs'
|
||||
import type {} from '@deepseek-ai/dsh-system-prompt'
|
||||
import { FIRST_PARTY_SECTION_ORDER } from '@deepseek-ai/dsh-system-prompt'
|
||||
import { computeHunkDiffs, diffsFromMeta } from './diff.ts'
|
||||
import { remediateFsError } from './error.ts'
|
||||
import { sessionResolveOptions } from './session-cwd.ts'
|
||||
@@ -76,7 +76,7 @@ export function formatEditOutput(displayPath: string, replaceAll: boolean): stri
|
||||
export function applyEditTool(ctx: Context, sandbox: FsSandboxController): void {
|
||||
ctx.systemPrompt.section({
|
||||
name: 'tool:edit',
|
||||
order: 102,
|
||||
order: FIRST_PARTY_SECTION_ORDER.TOOL_EDIT,
|
||||
text: 'Use the edit tool for targeted changes to existing UTF-8 text files. It replaces literal old_string with new_string; by default old_string must appear exactly once. If old_string appears multiple times, provide a more specific old_string or set replace_all to true. Read the file first (the default fs-observation-policy requires it), unless you just created or edited it in this session.',
|
||||
})
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { Context } from '@deepseek-ai/cordis'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import type { GenericCallView, ReadResultView, ToolResult } from '@deepseek-ai/dsh-tools'
|
||||
import type {} from '@deepseek-ai/dsh-fs'
|
||||
import type {} from '@deepseek-ai/dsh-system-prompt'
|
||||
import { FIRST_PARTY_SECTION_ORDER } from '@deepseek-ai/dsh-system-prompt'
|
||||
import { buildWindow, formatReadOutput, langFromPath, readMetaFromMeta } from './read-render.ts'
|
||||
import { resolveRegularReadTarget } from './read-target.ts'
|
||||
|
||||
@@ -69,7 +69,7 @@ export function parseReadArgs(args: { file_path: string; offset?: number; limit?
|
||||
export function applyReadTool(ctx: Context, caps: ReadToolCaps): void {
|
||||
ctx.systemPrompt.section({
|
||||
name: 'tool:read',
|
||||
order: 100,
|
||||
order: FIRST_PARTY_SECTION_ORDER.TOOL_READ,
|
||||
text: 'Use the read tool — not shell commands like cat — to inspect text files. Results include line numbers. Use offset and limit to continue reading large files.',
|
||||
})
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import type { DiffCallView, DiffResultView, ToolResult } from '@deepseek-ai/dsh-tools'
|
||||
import type { FsWriteOutcome } from '@deepseek-ai/dsh-fs'
|
||||
import type {} from '@deepseek-ai/dsh-fs'
|
||||
import type {} from '@deepseek-ai/dsh-system-prompt'
|
||||
import { FIRST_PARTY_SECTION_ORDER } from '@deepseek-ai/dsh-system-prompt'
|
||||
import { computeHunkDiffs, diffsFromMeta } from './diff.ts'
|
||||
import { remediateFsError } from './error.ts'
|
||||
import { sessionResolveOptions } from './session-cwd.ts'
|
||||
@@ -62,7 +62,7 @@ interface WriteToolArgs {
|
||||
export function applyWriteTool(ctx: Context, sandbox: FsSandboxController): void {
|
||||
ctx.systemPrompt.section({
|
||||
name: 'tool:write',
|
||||
order: 101,
|
||||
order: FIRST_PARTY_SECTION_ORDER.TOOL_WRITE,
|
||||
text: 'Use the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-observation-policy requires it) and prefer edit for targeted changes.',
|
||||
})
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { GoalRef, GoalView } from '@deepseek-ai/dsh-goal'
|
||||
import { boundContextSummary, createUserMessage, HarnessError } from '@deepseek-ai/dsh-llm'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import type { GenericCallView } from '@deepseek-ai/dsh-tools'
|
||||
import type {} from '@deepseek-ai/dsh-system-prompt'
|
||||
import { FIRST_PARTY_SECTION_ORDER } from '@deepseek-ai/dsh-system-prompt'
|
||||
import {
|
||||
completionAuthority,
|
||||
goalToolExecution,
|
||||
@@ -188,7 +188,7 @@ export function apply(ctx: Context, config: Config): void {
|
||||
const resolved = resolveConfig(config)
|
||||
ctx.systemPrompt.section({
|
||||
name: 'tool:goal',
|
||||
order: 114,
|
||||
order: FIRST_PARTY_SECTION_ORDER.TOOL_GOAL,
|
||||
text: guidance(resolved.blockedAfterConsecutiveRounds),
|
||||
})
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import type { GenericCallView, ToolDefinition, ToolExecution } from '@deepseek-ai/dsh-tools'
|
||||
import { JobId } from '@deepseek-ai/dsh-jobs'
|
||||
import type { JobSnapshot } from '@deepseek-ai/dsh-jobs'
|
||||
import type {} from '@deepseek-ai/dsh-system-prompt'
|
||||
import { FIRST_PARTY_SECTION_ORDER } from '@deepseek-ai/dsh-system-prompt'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
|
||||
export const name = 'tool-jobs'
|
||||
@@ -259,10 +259,10 @@ export function apply(ctx: Context, config: Config): void {
|
||||
// Producers may start work only while a controller is attached.
|
||||
ctx.jobs.attachController('tool-jobs')
|
||||
|
||||
// Cross-call guidance follows the bash section and precedes product sections.
|
||||
// Cross-call guidance follows the filesystem sections and precedes product sections.
|
||||
ctx.systemPrompt.section({
|
||||
name: 'tool:jobs',
|
||||
order: 106,
|
||||
order: FIRST_PARTY_SECTION_ORDER.TOOL_JOBS,
|
||||
text: 'Track every background job id you start. You are notified in-session when a job finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running job\'s work. Before giving a final answer, collect every still-relevant job with job_output (set wait: true only when you are genuinely blocked on it), and job_kill jobs that stopped mattering.',
|
||||
})
|
||||
|
||||
|
||||
@@ -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/lsp/tool-lsp/README.md
|
||||
README.md: e0fd5738cf114314a8483125d20142d5f1ffac40
|
||||
README.zh.md: 71d6a4b5479b2da187b78a388d617afe7214e78c
|
||||
README.md: 84ca94863758c2ee5bb23a458053cb5d4fefb777
|
||||
README.zh.md: 48eacc553337a852c4722931761590d84f44554d
|
||||
|
||||
@@ -26,7 +26,7 @@ The tool requires the workspace root from the session `header.cwd`, with no fall
|
||||
|
||||
#### What the model sees
|
||||
|
||||
One system-prompt section (order 112) positions LSP as a precision aid with the following text:
|
||||
One system-prompt section (first-party order 2200) positions LSP as a precision aid with the following text:
|
||||
|
||||
##### Verbatim guidance
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ Namespace 插件(`name`/`inject`/`Config`/`apply`,无默认导出)
|
||||
|
||||
#### 模型看到的内容
|
||||
|
||||
一个系统提示词区段(顺序 112)将 LSP 定位为精确辅助工具,文本如下:
|
||||
一个系统提示词区段(first-party 顺序 2200)将 LSP 定位为精确辅助工具,文本如下:
|
||||
|
||||
##### 逐字指引
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import { assertNever } from '@deepseek-ai/dsh-llm'
|
||||
import { LspError } from '@deepseek-ai/dsh-lsp'
|
||||
import type {} from '@deepseek-ai/dsh-lsp'
|
||||
import type {} from '@deepseek-ai/dsh-system-prompt'
|
||||
import { FIRST_PARTY_SECTION_ORDER } from '@deepseek-ai/dsh-system-prompt'
|
||||
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
|
||||
import {
|
||||
DEFAULT_MAX_LOCATIONS,
|
||||
@@ -101,7 +101,11 @@ export function apply(ctx: Context, config: Config): void {
|
||||
assertPositiveInteger('maxResultChars', resolved.maxResultChars)
|
||||
assertTimer('timeoutMs', resolved.timeoutMs)
|
||||
|
||||
ctx.systemPrompt.section({ name: 'tool:lsp', order: 112, text: LSP_PROMPT_TEXT })
|
||||
ctx.systemPrompt.section({
|
||||
name: 'tool:lsp',
|
||||
order: FIRST_PARTY_SECTION_ORDER.TOOL_LSP,
|
||||
text: LSP_PROMPT_TEXT,
|
||||
})
|
||||
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'lsp',
|
||||
|
||||
@@ -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/plan/plan-mode/README.md
|
||||
README.md: 3eabe2cb3f04b434b7f908f7beca869f1022a59e
|
||||
README.zh.md: b3548da880af5d0cacfebba4486294d9d5085626
|
||||
README.md: cc518d9854609362ad5e932dc1e819e9757ee5d6
|
||||
README.zh.md: cd17f216e914bbd37251cda22a1068e2b0c0ac9c
|
||||
|
||||
@@ -45,7 +45,7 @@ Design: [plan-specific collaboration state](../../../.agents/notes/implemented/s
|
||||
|
||||
#### What the model sees
|
||||
|
||||
While plan mode is active, the model sees the deployment's exact `section` text at prompt order 50; inactive mode contributes no text.
|
||||
While plan mode is active, the model sees the deployment's exact `section` text at first-party prompt order 500; inactive mode contributes no text.
|
||||
|
||||
##### Configuration example
|
||||
|
||||
@@ -59,7 +59,7 @@ Inactive mode adds no tokens; active mode adds the configured section to every r
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
The section is stable within plan mode, but entering or leaving changes the system prompt from order 50 onward.
|
||||
The section is stable within plan mode, but entering or leaving changes the system prompt from first-party order 500 onward.
|
||||
|
||||
### Human command
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ Web 客户端使用该插件提供的 `/plan` 命令;其他入口可以直接
|
||||
|
||||
#### 模型所见内容
|
||||
|
||||
Plan mode 激活时,模型会在提示词顺序 50 处看到部署方提供的原样 `section` 文本;未激活 mode 不贡献文本。
|
||||
Plan mode 激活时,模型会在 first-party 提示词顺序 500 处看到部署方提供的原样 `section` 文本;未激活 mode 不贡献文本。
|
||||
|
||||
##### 配置示例
|
||||
|
||||
@@ -61,7 +61,7 @@ You are in plan mode. Explore and design before presenting the complete plan thr
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
该段在 plan mode 内稳定,但进入或退出会从顺序 50 开始改变系统提示词。
|
||||
该段在 plan mode 内稳定,但进入或退出会从 first-party 顺序 500 开始改变系统提示词。
|
||||
|
||||
### 人类命令
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ import type { Agent, PreStepDecision } from '@deepseek-ai/dsh-agent'
|
||||
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
||||
import type { Session, SessionEvent, UserMessage } from '@deepseek-ai/dsh-session'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import type {} from '@deepseek-ai/dsh-system-prompt'
|
||||
import { FIRST_PARTY_SECTION_ORDER } from '@deepseek-ai/dsh-system-prompt'
|
||||
import { UserQuestionError } from '@deepseek-ai/dsh-user-questions'
|
||||
// Type-only edge: resolves `ctx.commands` for the optional command child.
|
||||
import type { CommandId } from '@deepseek-ai/dsh-commands'
|
||||
@@ -242,7 +242,7 @@ export class PlanModeController extends Service {
|
||||
|
||||
ctx.systemPrompt.section({
|
||||
name: 'plan:policy',
|
||||
order: 50,
|
||||
order: FIRST_PARTY_SECTION_ORDER.PLAN_POLICY,
|
||||
text: (context) => {
|
||||
if (context.agent === undefined) return ''
|
||||
const pending = this.pendingIntents.get(context.agent.session)
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { Context } from '@deepseek-ai/cordis'
|
||||
import z from '@deepseek-ai/schemastery'
|
||||
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import type {} from '@deepseek-ai/dsh-system-prompt'
|
||||
import { FIRST_PARTY_SECTION_ORDER } from '@deepseek-ai/dsh-system-prompt'
|
||||
import { toolInput } from './input.ts'
|
||||
import { operations } from './operations.ts'
|
||||
import { presentation } from './presentation.ts'
|
||||
@@ -59,7 +59,7 @@ export function apply(ctx: Context, config: Config): void {
|
||||
const resolved = resolveConfig(config)
|
||||
ctx.systemPrompt.section({
|
||||
name: 'tool:session-query',
|
||||
order: 113,
|
||||
order: FIRST_PARTY_SECTION_ORDER.TOOL_SESSION_QUERY,
|
||||
text: PROMPT_TEXT,
|
||||
})
|
||||
|
||||
|
||||
@@ -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/shell/tool-bash/README.md
|
||||
README.md: 21749bf0a1cd3cdc46257fb2a02ba6b4ab1f5ee6
|
||||
README.zh.md: 860db1a4c347d1aefe04f70e14a65e4b511ff7f6
|
||||
README.md: c684508ea50b80ff6f98b25cd4cd7ca0fd6012a5
|
||||
README.zh.md: 3459ed64964bf40b3b72530d7614ca27e45cafc6
|
||||
|
||||
@@ -8,7 +8,7 @@ Requires a loaded executor Service Provider (e.g. `@deepseek-ai/dsh-bash-local`)
|
||||
|
||||
The package root exposes only the Cordis plugin contract (`name`, `inject`, `Config`, `apply`); result rendering and background-process adaptation remain package-internal.
|
||||
|
||||
The plugin also contributes the `tool:bash` prompt section (order 105): check the `[exit code: N]` marker on every result and investigate failures before moving on.
|
||||
The plugin also contributes the `tool:bash` prompt section (first-party order 1000): check the `[exit code: N]` marker on every result and investigate failures before moving on.
|
||||
|
||||
## Tools
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
包根只公开 Cordis 插件约定(`name`、`inject`、`Config`、`apply`);结果渲染和后台进程适配仍保留在包内部。
|
||||
|
||||
插件还会提供 `tool:bash` 提示词段落(顺序 105):检查每个结果中的 `[exit code: N]` 标记,发现失败时先调查原因再继续。
|
||||
插件还会提供 `tool:bash` 提示词段落(first-party 顺序 1000):检查每个结果中的 `[exit code: N]` 标记,发现失败时先调查原因再继续。
|
||||
|
||||
## 工具
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import { defineTool, TOOL_ABORTED } from '@deepseek-ai/dsh-tools'
|
||||
import type { GenericCallView, TerminalCallView, ToolExecution, ToolResult, ToolResultView } from '@deepseek-ai/dsh-tools'
|
||||
import { HarnessError } from '@deepseek-ai/dsh-llm'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import type {} from '@deepseek-ai/dsh-system-prompt'
|
||||
import { FIRST_PARTY_SECTION_ORDER } from '@deepseek-ai/dsh-system-prompt'
|
||||
import type {} from '@deepseek-ai/dsh-jobs'
|
||||
import type {} from '@deepseek-ai/dsh-user-approval'
|
||||
import type {} from '@deepseek-ai/dsh-shell-env'
|
||||
@@ -235,7 +235,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
||||
// Cross-call guidance belongs in the prompt rather than one-call schema prose.
|
||||
ctx.systemPrompt.section({
|
||||
name: 'tool:bash',
|
||||
order: 105,
|
||||
order: FIRST_PARTY_SECTION_ORDER.TOOL_BASH,
|
||||
text: 'Check the [exit code: N] marker on every bash result; investigate failures before moving on.',
|
||||
})
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user