dsh-plugin-dev-kb 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +56 -0
  3. package/cordis.patch.yml +12 -0
  4. package/kb/INDEX.md +210 -0
  5. package/kb/README.md +69 -0
  6. package/kb/extra/AGENTS.md +75 -0
  7. package/kb/extra/api-gateway.md +164 -0
  8. package/kb/extra/api-gateway.zh.md +164 -0
  9. package/kb/extra/cookbook/adding-a-vendored-package.md +59 -0
  10. package/kb/extra/cookbook/adding-a-vendored-package.zh.md +59 -0
  11. package/kb/extra/cookbook/maintaining-dsh-code-review.md +64 -0
  12. package/kb/extra/cookbook/maintaining-dsh-code-review.zh.md +64 -0
  13. package/kb/extra/cookbook/responding-to-pr-review-on-a-stack.md +32 -0
  14. package/kb/extra/cookbook/responding-to-pr-review-on-a-stack.zh.md +32 -0
  15. package/kb/extra/defensive-patterns.md +33 -0
  16. package/kb/extra/defensive-patterns.zh.md +33 -0
  17. package/kb/extra/development.md +171 -0
  18. package/kb/extra/development.zh.md +171 -0
  19. package/kb/extra/event-producer-consumer.md +76 -0
  20. package/kb/extra/event-producer-consumer.zh.md +78 -0
  21. package/kb/extra/glossary.md +45 -0
  22. package/kb/extra/glossary.zh.md +45 -0
  23. package/kb/extra/graph-atlas.md +24 -0
  24. package/kb/extra/graph-atlas.zh.md +26 -0
  25. package/kb/extra/i18n/README.md +60 -0
  26. package/kb/extra/i18n/README.zh.md +60 -0
  27. package/kb/extra/i18n/style-samples.md +87 -0
  28. package/kb/extra/i18n/terminology.md +214 -0
  29. package/kb/extra/i18n/translation-prompt.md +263 -0
  30. package/kb/extra/i18n/translation-rules.md +69 -0
  31. package/kb/extra/i18n/translation-rules.zh.md +69 -0
  32. package/kb/extra/module-graph.md +1641 -0
  33. package/kb/extra/module-graph.zh.md +1643 -0
  34. package/kb/extra/postmortem/0001-acp-default-export-drops-inject.md +113 -0
  35. package/kb/extra/postmortem/0001-acp-default-export-drops-inject.zh.md +113 -0
  36. package/kb/extra/postmortem/0002-js-expression-disabled-filesystem-tools.md +47 -0
  37. package/kb/extra/postmortem/0002-js-expression-disabled-filesystem-tools.zh.md +47 -0
  38. package/kb/extra/postmortem/0003-web-agent-gui-feedback-loop.md +53 -0
  39. package/kb/extra/postmortem/0003-web-agent-gui-feedback-loop.zh.md +53 -0
  40. package/kb/extra/postmortem/0004-landlock-partial-notice-misclassified-child-failures.md +55 -0
  41. package/kb/extra/postmortem/0004-landlock-partial-notice-misclassified-child-failures.zh.md +55 -0
  42. package/kb/extra/postmortem/README.md +18 -0
  43. package/kb/extra/postmortem/README.zh.md +18 -0
  44. package/kb/extra/rescope.md +53 -0
  45. package/kb/extra/rescope.zh.md +53 -0
  46. package/kb/extra/subsystems/attachment.md +125 -0
  47. package/kb/extra/subsystems/attachment.zh.md +125 -0
  48. package/kb/extra/subsystems/extensions.md +364 -0
  49. package/kb/extra/subsystems/extensions.zh.md +364 -0
  50. package/kb/extra/subsystems/feedback.md +266 -0
  51. package/kb/extra/subsystems/feedback.zh.md +266 -0
  52. package/kb/extra/testing.md +49 -0
  53. package/kb/extra/testing.zh.md +49 -0
  54. package/kb/extra/web-styling.md +25 -0
  55. package/kb/extra/web-styling.zh.md +25 -0
  56. package/kb/meta/search-index.json +1328 -0
  57. package/kb/meta/site-pages.txt +168 -0
  58. package/kb/meta/source.json +13 -0
  59. package/kb/meta/topics.md +75 -0
  60. package/kb/site/develop/basic/config.md +108 -0
  61. package/kb/site/develop/basic/index.md +146 -0
  62. package/kb/site/develop/basic/publish.md +185 -0
  63. package/kb/site/develop/basic/tool.md +54 -0
  64. package/kb/site/develop/cordis-tutorial/01-first-plugin.md +95 -0
  65. package/kb/site/develop/cordis-tutorial/02-lifecycle-and-effects.md +98 -0
  66. package/kb/site/develop/cordis-tutorial/03-services.md +98 -0
  67. package/kb/site/develop/cordis-tutorial/04-events.md +144 -0
  68. package/kb/site/develop/cordis-tutorial/05-config.md +84 -0
  69. package/kb/site/develop/cordis-tutorial/06-composition-and-hmr.md +113 -0
  70. package/kb/site/develop/cordis-tutorial/07-into-the-harness.md +107 -0
  71. package/kb/site/develop/cordis-tutorial/index.md +62 -0
  72. package/kb/site/develop/framework/events.md +145 -0
  73. package/kb/site/develop/framework/index.md +139 -0
  74. package/kb/site/develop/framework/service.md +152 -0
  75. package/kb/site/develop/practice/index.md +157 -0
  76. package/kb/site/develop/practice/llm-adapter.md +190 -0
  77. package/kb/site/en/develop/basic/config.md +108 -0
  78. package/kb/site/en/develop/basic/index.md +146 -0
  79. package/kb/site/en/develop/basic/publish.md +185 -0
  80. package/kb/site/en/develop/basic/tool.md +54 -0
  81. package/kb/site/en/develop/cordis-tutorial/01-first-plugin.md +95 -0
  82. package/kb/site/en/develop/cordis-tutorial/02-lifecycle-and-effects.md +98 -0
  83. package/kb/site/en/develop/cordis-tutorial/03-services.md +98 -0
  84. package/kb/site/en/develop/cordis-tutorial/04-events.md +144 -0
  85. package/kb/site/en/develop/cordis-tutorial/05-config.md +84 -0
  86. package/kb/site/en/develop/cordis-tutorial/06-composition-and-hmr.md +113 -0
  87. package/kb/site/en/develop/cordis-tutorial/07-into-the-harness.md +107 -0
  88. package/kb/site/en/develop/cordis-tutorial/index.md +60 -0
  89. package/kb/site/en/develop/framework/events.md +145 -0
  90. package/kb/site/en/develop/framework/index.md +139 -0
  91. package/kb/site/en/develop/framework/service.md +150 -0
  92. package/kb/site/en/develop/practice/index.md +157 -0
  93. package/kb/site/en/develop/practice/llm-adapter.md +190 -0
  94. package/kb/site/en/guide/providers-custom-form.png +0 -0
  95. package/kb/site/en/guide/providers-models-page.png +0 -0
  96. package/kb/site/en/guide/providers.md +100 -0
  97. package/kb/site/en/guide/python-sdk.md +106 -0
  98. package/kb/site/en/guide/quickstart.md +32 -0
  99. package/kb/site/en/index.md +8 -0
  100. package/kb/site/en/reference/agent-lifecycle.md +86 -0
  101. package/kb/site/en/reference/capability-seams.md +475 -0
  102. package/kb/site/en/reference/config-catalog.md +3155 -0
  103. package/kb/site/en/reference/cookbook/adding-a-conversation-node.md +235 -0
  104. package/kb/site/en/reference/cookbook/adding-a-package.md +120 -0
  105. package/kb/site/en/reference/cookbook/adding-a-settings-card.md +102 -0
  106. package/kb/site/en/reference/cookbook/adding-a-tool.md +96 -0
  107. package/kb/site/en/reference/cookbook/adding-an-llm-adapter.md +45 -0
  108. package/kb/site/en/reference/cookbook/extension-cookbook.md +131 -0
  109. package/kb/site/en/reference/cordis-api/context.md +368 -0
  110. package/kb/site/en/reference/cordis-api/events.md +211 -0
  111. package/kb/site/en/reference/cordis-api/fiber.md +379 -0
  112. package/kb/site/en/reference/cordis-api/inherited.md +43 -0
  113. package/kb/site/en/reference/cordis-api/registry.md +156 -0
  114. package/kb/site/en/reference/cordis-api/service.md +106 -0
  115. package/kb/site/en/reference/cordis-primer.md +46 -0
  116. package/kb/site/en/reference/index.md +131 -0
  117. package/kb/site/en/reference/persistence-catalog.md +949 -0
  118. package/kb/site/en/reference/subsystems/approval.md +173 -0
  119. package/kb/site/en/reference/subsystems/client-modules.md +121 -0
  120. package/kb/site/en/reference/subsystems/code-runtime.md +194 -0
  121. package/kb/site/en/reference/subsystems/commands.md +190 -0
  122. package/kb/site/en/reference/subsystems/compaction.md +241 -0
  123. package/kb/site/en/reference/subsystems/core.md +1073 -0
  124. package/kb/site/en/reference/subsystems/credentials.md +136 -0
  125. package/kb/site/en/reference/subsystems/filesystem.md +498 -0
  126. package/kb/site/en/reference/subsystems/goal.md +280 -0
  127. package/kb/site/en/reference/subsystems/index.md +58 -0
  128. package/kb/site/en/reference/subsystems/invariants.md +91 -0
  129. package/kb/site/en/reference/subsystems/jobs.md +293 -0
  130. package/kb/site/en/reference/subsystems/llm-streaming.md +920 -0
  131. package/kb/site/en/reference/subsystems/lsp.md +205 -0
  132. package/kb/site/en/reference/subsystems/permission-presets.md +134 -0
  133. package/kb/site/en/reference/subsystems/persistence.md +388 -0
  134. package/kb/site/en/reference/subsystems/plan.md +90 -0
  135. package/kb/site/en/reference/subsystems/sandbox.md +221 -0
  136. package/kb/site/en/reference/subsystems/schedule.md +189 -0
  137. package/kb/site/en/reference/subsystems/scope.md +62 -0
  138. package/kb/site/en/reference/subsystems/session-projection.md +265 -0
  139. package/kb/site/en/reference/subsystems/session-query.md +498 -0
  140. package/kb/site/en/reference/subsystems/session-reference.md +111 -0
  141. package/kb/site/en/reference/subsystems/session-telemetry.md +197 -0
  142. package/kb/site/en/reference/subsystems/session-title.md +207 -0
  143. package/kb/site/en/reference/subsystems/session.md +852 -0
  144. package/kb/site/en/reference/subsystems/settings.md +313 -0
  145. package/kb/site/en/reference/subsystems/shell.md +306 -0
  146. package/kb/site/en/reference/subsystems/skills.md +334 -0
  147. package/kb/site/en/reference/subsystems/spill.md +120 -0
  148. package/kb/site/en/reference/subsystems/storage.md +232 -0
  149. package/kb/site/en/reference/subsystems/subagent.md +737 -0
  150. package/kb/site/en/reference/subsystems/subprocess.md +327 -0
  151. package/kb/site/en/reference/subsystems/system-prompt.md +210 -0
  152. package/kb/site/en/reference/subsystems/terminal.md +187 -0
  153. package/kb/site/en/reference/subsystems/token-meter.md +93 -0
  154. package/kb/site/en/reference/subsystems/tools.md +723 -0
  155. package/kb/site/en/reference/subsystems/typert.md +339 -0
  156. package/kb/site/en/reference/subsystems/user-questions.md +181 -0
  157. package/kb/site/en/reference/subsystems/web-server.md +111 -0
  158. package/kb/site/en/reference/subsystems/web.md +202 -0
  159. package/kb/site/en/reference/subsystems/workflow.md +281 -0
  160. package/kb/site/en/reference/subsystems/workspace.md +231 -0
  161. package/kb/site/en/reference/tool-catalog.md +1877 -0
  162. package/kb/site/en/reference/tool-execution-pipeline.md +66 -0
  163. package/kb/site/guide/providers-custom-form.zh.png +0 -0
  164. package/kb/site/guide/providers-models-page.zh.png +0 -0
  165. package/kb/site/guide/providers.md +100 -0
  166. package/kb/site/guide/python-sdk.md +106 -0
  167. package/kb/site/guide/quickstart.md +32 -0
  168. package/kb/site/index.md +8 -0
  169. package/kb/site/reference/agent-lifecycle.md +86 -0
  170. package/kb/site/reference/capability-seams.md +475 -0
  171. package/kb/site/reference/config-catalog.md +3154 -0
  172. package/kb/site/reference/cookbook/adding-a-conversation-node.md +235 -0
  173. package/kb/site/reference/cookbook/adding-a-package.md +120 -0
  174. package/kb/site/reference/cookbook/adding-a-settings-card.md +102 -0
  175. package/kb/site/reference/cookbook/adding-a-tool.md +98 -0
  176. package/kb/site/reference/cookbook/adding-an-llm-adapter.md +45 -0
  177. package/kb/site/reference/cookbook/extension-cookbook.md +133 -0
  178. package/kb/site/reference/cordis-api/context.md +368 -0
  179. package/kb/site/reference/cordis-api/events.md +211 -0
  180. package/kb/site/reference/cordis-api/fiber.md +379 -0
  181. package/kb/site/reference/cordis-api/inherited.md +43 -0
  182. package/kb/site/reference/cordis-api/registry.md +156 -0
  183. package/kb/site/reference/cordis-api/service.md +106 -0
  184. package/kb/site/reference/cordis-primer.md +52 -0
  185. package/kb/site/reference/index.md +135 -0
  186. package/kb/site/reference/persistence-catalog.md +949 -0
  187. package/kb/site/reference/subsystems/approval.md +173 -0
  188. package/kb/site/reference/subsystems/client-modules.md +121 -0
  189. package/kb/site/reference/subsystems/code-runtime.md +194 -0
  190. package/kb/site/reference/subsystems/commands.md +190 -0
  191. package/kb/site/reference/subsystems/compaction.md +241 -0
  192. package/kb/site/reference/subsystems/core.md +1081 -0
  193. package/kb/site/reference/subsystems/credentials.md +136 -0
  194. package/kb/site/reference/subsystems/filesystem.md +498 -0
  195. package/kb/site/reference/subsystems/goal.md +280 -0
  196. package/kb/site/reference/subsystems/index.md +58 -0
  197. package/kb/site/reference/subsystems/invariants.md +91 -0
  198. package/kb/site/reference/subsystems/jobs.md +293 -0
  199. package/kb/site/reference/subsystems/llm-streaming.md +926 -0
  200. package/kb/site/reference/subsystems/lsp.md +205 -0
  201. package/kb/site/reference/subsystems/permission-presets.md +134 -0
  202. package/kb/site/reference/subsystems/persistence.md +388 -0
  203. package/kb/site/reference/subsystems/plan.md +90 -0
  204. package/kb/site/reference/subsystems/sandbox.md +221 -0
  205. package/kb/site/reference/subsystems/schedule.md +189 -0
  206. package/kb/site/reference/subsystems/scope.md +62 -0
  207. package/kb/site/reference/subsystems/session-projection.md +265 -0
  208. package/kb/site/reference/subsystems/session-query.md +498 -0
  209. package/kb/site/reference/subsystems/session-reference.md +111 -0
  210. package/kb/site/reference/subsystems/session-telemetry.md +197 -0
  211. package/kb/site/reference/subsystems/session-title.md +207 -0
  212. package/kb/site/reference/subsystems/session.md +854 -0
  213. package/kb/site/reference/subsystems/settings.md +313 -0
  214. package/kb/site/reference/subsystems/shell.md +306 -0
  215. package/kb/site/reference/subsystems/skills.md +334 -0
  216. package/kb/site/reference/subsystems/spill.md +120 -0
  217. package/kb/site/reference/subsystems/storage.md +232 -0
  218. package/kb/site/reference/subsystems/subagent.md +739 -0
  219. package/kb/site/reference/subsystems/subprocess.md +327 -0
  220. package/kb/site/reference/subsystems/system-prompt.md +210 -0
  221. package/kb/site/reference/subsystems/terminal.md +187 -0
  222. package/kb/site/reference/subsystems/token-meter.md +93 -0
  223. package/kb/site/reference/subsystems/tools.md +723 -0
  224. package/kb/site/reference/subsystems/typert.md +339 -0
  225. package/kb/site/reference/subsystems/user-questions.md +181 -0
  226. package/kb/site/reference/subsystems/web-server.md +111 -0
  227. package/kb/site/reference/subsystems/web.md +202 -0
  228. package/kb/site/reference/subsystems/workflow.md +281 -0
  229. package/kb/site/reference/subsystems/workspace.md +231 -0
  230. package/kb/site/reference/tool-catalog.md +1880 -0
  231. package/kb/site/reference/tool-execution-pipeline.md +66 -0
  232. package/package.json +40 -0
  233. package/scripts/rebuild-index.mjs +88 -0
  234. package/skills/dsh-plugin-dev-kb.md +66 -0
@@ -0,0 +1,235 @@
1
+ ---
2
+ editSource: "docs/cookbook/adding-a-conversation-node.zh.md"
3
+ ---
4
+
5
+ # 添加 Web Client Conversation Node
6
+
7
+ 本教程为 Web Client Chat 视图添加一行由业务自行拥有的内容。完成后的插件会把一个持久 Session 事件族关联成一个 Context,增量构造业务 State,发布类型化 Step 数据,再渲染 keyed Chat Node;整个过程不扫描 Session 窗口或其他已渲染节点。本教程假设 Host 已经记录这些事件,且该 Client 插件已组装进 Web bundle;Host 侧外部 UI 和 Trajectory 等额外视图目标不在本文范围内。
8
+
9
+ [Conversation Node 组装决策](https://github.com/deepseek-ai/deepseek-harness/blob/master/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.md)记录完整的引擎模型和设计理由;本文只说明实现路径。
10
+
11
+ ## 1. 设计可回放的事件族
12
+
13
+ 编写 Definition 前先选定稳定的业务 id。构成同一个 Node 的每条事件都必须携带该 id,或只凭自身 payload 独立推导出该 id;Client 绝不能把 update 猜测为属于“最近一个未完成”的 Context。
14
+
15
+ 以一个 review job 为例,事件约定可以是:
16
+
17
+ | 事件 | 角色 | 必须持久化的事实 |
18
+ |---|---|---|
19
+ | `review/start` | 唯一 start | `reviewId`、Turn/Step 坐标、标题 |
20
+ | `review/progress` | update | 相同的 `reviewId`、坐标、可回放进度 |
21
+ | `review/end` | update | 相同的 `reviewId`、坐标、最终摘要 |
22
+
23
+ 跨进程边界使用生产方拥有的 branded id 类型。把 `SessionEventMap` 合并和 payload 类型放在生产方的纯类型导出中,再由 Client 包通过仅类型副作用导入该导出。每个 `(kind, id)` 最多只能有一条 start 事件。单事件业务可以把事件自身的稳定身份(例如 `event.seq`)作为 Definition 内部 id。
24
+
25
+ 系统支持增量事件。如果生产方能以较低成本发出 whole-value checkpoint,应优先采用,因为 start 位于已加载窗口之外时它仍可直接使用。每条 delta 都必须携带稳定 id,并且按照日志 `seq` 升序回放时能够确定性地产生 State;它不能依赖只存在于实时内存中的状态。如果当前历史窗口只有 update,Assembler 会保留一个 pending Context,并在更早分页补齐 start 前不构造 State。如果产品必须在 start 尚未加载时渲染,terminal 或 checkpoint 事件就必须携带足够的完整 fallback 状态,让 Definition 能直接构造结果;不要通过扫描无关事件恢复它。
26
+
27
+ ## 2. 实现 Definition 与类型化 Chat payload
28
+
29
+ 为了完整展示关联关系,下面把生产方声明和 Client 贡献写在同一个代码块里。实际的包族中,branded id 与 `SessionEventMap` 声明留在事件生产方,Definition、Chat data 合并与 renderer 留在 Client 插件。
30
+
31
+ ```ts ignore-check
32
+ import { createElement } from 'react'
33
+ import type { Branded } from '@deepseek-ai/dsh-brand'
34
+ import type {
35
+ ClientContext, ConversationLocation, ConversationNodeContext,
36
+ ConversationNodeDefinition,
37
+ } from '@deepseek-ai/dsh-client-runtime/client'
38
+ import type { ChatNodeViewProps } from '@deepseek-ai/dsh-client-ui-conversation/client'
39
+
40
+ type ReviewId = Branded<'ReviewId'>
41
+
42
+ interface ReviewStartData {
43
+ readonly reviewId: ReviewId
44
+ readonly turn: number
45
+ readonly step: number
46
+ readonly title: string
47
+ }
48
+
49
+ interface ReviewProgressData {
50
+ readonly reviewId: ReviewId
51
+ readonly turn: number
52
+ readonly step: number
53
+ readonly completed: number
54
+ }
55
+
56
+ interface ReviewEndData {
57
+ readonly reviewId: ReviewId
58
+ readonly turn: number
59
+ readonly step: number
60
+ readonly summary: string
61
+ }
62
+
63
+ declare module '@deepseek-ai/dsh-session/types' {
64
+ interface SessionEventMap {
65
+ /**
66
+ * Opens one durable review job.
67
+ * @mode emit
68
+ * @param data - stable identity, location, and initial display state.
69
+ */
70
+ 'review/start': ReviewStartData
71
+ /**
72
+ * Records replayable progress for one review job.
73
+ * @mode emit
74
+ * @param data - stable identity, location, and latest progress.
75
+ */
76
+ 'review/progress': ReviewProgressData
77
+ /**
78
+ * Closes one review job with its final summary.
79
+ * @mode emit
80
+ * @param data - stable identity, location, and final display state.
81
+ */
82
+ 'review/end': ReviewEndData
83
+ }
84
+ }
85
+
86
+ interface ReviewChatData {
87
+ readonly title: string
88
+ readonly completed: number
89
+ readonly status: 'running' | 'completed'
90
+ readonly summary?: string
91
+ }
92
+
93
+ declare module '@deepseek-ai/dsh-client-ui-conversation/client' {
94
+ interface ChatNodeDataMap {
95
+ 'review-job': ReviewChatData
96
+ }
97
+ }
98
+
99
+ declare module '@deepseek-ai/dsh-client-runtime/client' {
100
+ interface ConversationStepDataMap {
101
+ 'review-job': ReviewChatData
102
+ }
103
+ }
104
+
105
+ interface ReviewState extends ReviewChatData {
106
+ readonly turn: number
107
+ readonly step: number
108
+ }
109
+
110
+ function locationOf(context: ConversationNodeContext): ConversationLocation {
111
+ return context.start?.location ?? context.matches[0]?.location ?? { kind: 'unresolved' }
112
+ }
113
+
114
+ function viewData(state: ReviewState): ReviewChatData {
115
+ return {
116
+ title: state.title,
117
+ completed: state.completed,
118
+ status: state.status,
119
+ ...state.summary === undefined ? {} : { summary: state.summary },
120
+ }
121
+ }
122
+
123
+ const reviewDefinition: ConversationNodeDefinition<ReviewState> = {
124
+ kind: 'review-job',
125
+ target: 'chat',
126
+ match: (event) => {
127
+ if (event.type === 'review/start') {
128
+ return { id: String(event.data.reviewId), role: 'start' }
129
+ }
130
+ if (event.type === 'review/progress' || event.type === 'review/end') {
131
+ return { id: String(event.data.reviewId), role: 'update' }
132
+ }
133
+ return null
134
+ },
135
+ start: (_context, match) => {
136
+ if (match.event.type !== 'review/start') throw new Error('review-job requires review/start')
137
+ return {
138
+ turn: match.event.data.turn,
139
+ step: match.event.data.step,
140
+ title: match.event.data.title,
141
+ completed: 0,
142
+ status: 'running',
143
+ }
144
+ },
145
+ update: (context, match) => {
146
+ if (match.event.type === 'review/progress') {
147
+ return { ...context.state, completed: match.event.data.completed }
148
+ }
149
+ if (match.event.type === 'review/end') {
150
+ return { ...context.state, completed: 100, status: 'completed', summary: match.event.data.summary }
151
+ }
152
+ return context.state
153
+ },
154
+ publication: match => match.event.type === 'review/progress'
155
+ ? 'animation-frame'
156
+ : 'immediate',
157
+ buildLocationData: (context, scope) => {
158
+ if (scope !== 'step' || context.state === undefined) return null
159
+ return {
160
+ kind: 'step',
161
+ turn: context.state.turn,
162
+ step: context.state.step,
163
+ key: 'review-job',
164
+ value: viewData(context.state),
165
+ }
166
+ },
167
+ buildViewNode: (context) => {
168
+ if (context.state === undefined) return null
169
+ return {
170
+ key: context.key,
171
+ kind: 'review-job',
172
+ id: context.id,
173
+ target: 'chat',
174
+ anchorSeq: context.start?.event.seq ?? context.matches[0]?.event.seq ?? 0,
175
+ location: locationOf(context),
176
+ visibility: 'visible',
177
+ data: viewData(context.state),
178
+ }
179
+ },
180
+ }
181
+
182
+ function ReviewNodeView({ node }: ChatNodeViewProps<'review-job'>) {
183
+ const text = node.data.summary ?? `${node.data.title}: ${node.data.completed}%`
184
+ return createElement('p', null, text)
185
+ }
186
+
187
+ export const inject = ['conversationEvents', 'slots']
188
+
189
+ export function apply(ctx: ClientContext): void {
190
+ ctx.conversationEvents.register(reviewDefinition)
191
+ ctx.slots.inject('conversation.chat.node', () => ctx.slots.register({
192
+ name: 'conversation.chat.node',
193
+ key: 'review-job',
194
+ }, ReviewNodeView))
195
+ }
196
+ ```
197
+
198
+ `match(event)` 是身份提取器,不是 fold:它只能收到当前事件,并返回 Definition 内部 id 与生命周期角色。命中后,Assembler 通过 `(kind, id)` 定位 Context,再调用一次 `start`,或把当前 State 交给 `update`。两个函数都必须返回引擎随后采用的 State;推荐返回新的 immutable value,但函数原地修改后返回同一对象时,采用语义也相同。
199
+
200
+ `buildLocationData(context, scope)` 可以把 Definition 拥有的数据发布到引擎拥有的 Turn 或 Step 上。通过 declaration merging 为每个 key 指定精确 value 类型。同一 Location 内的另一个 Node 可以使用受限 slot hook(例如 `useTurnData(key)`)读取该值,无须取得 Session,也无须扫描 `snapshot.chat.nodes`。
201
+
202
+ `target` 与 `buildViewNode(context)` 必须同时声明一项由 target 拥有的渲染贡献。把 `context.key` 保留为 React 侧身份,根据持久排序证据选择 `anchorSeq`,并且只返回 renderer 可以直接使用的数据。某个 target Node 一旦发布,就要继续返回同一个 key;需要暂时离开可见流时使用 `visibility: 'hidden'`,不要改为返回 `null` 撤回它。
203
+
204
+ ## 3. 只在 start 时查询更早的业务 Context
205
+
206
+ 有些 Definition 需要另一个业务 kind 在当前位置之前的最新 State。`start` 会收到 `ConversationContextReader`;应在这里调用 `reader.previous<State>(kind)`,不要接收 Context 集合或扫描事件。Reader 返回当前 start `seq` 之前最近一个已启动 Context 的只读数据。
207
+
208
+ Assembler 会记录这项依赖。如果后续 older prepend 带来了更近的前序 Context、补齐了原先未知的窗口缺口,或者前序 State 被修订,引擎会从 `start` 重新运行依赖方 Context,并按 `seq` 升序回放其 update。被查询的 Definition 仍负责把有用信息写入自身 State;Reader 不提供业务专用查询方法,也不授予修改其他 Context 的权限。
209
+
210
+ ## 4. 理解三条摄入路径
211
+
212
+ 历史可能从尾部开始一页一页向前请求,但每个已接收分页都会先按 `seq` 升序归一化,再进入 State 回放。
213
+
214
+ | 路径 | 引擎工作 | Definition 可观察到的行为 |
215
+ |---|---|---|
216
+ | open、resync 或 gap repair 时 replace | 重建已加载窗口,每条事件对每个 Definition 匹配一次,再回放每个已有 start 的 Context | 先执行 `start`,再按 `seq` 升序执行其 update;只有 update 的 pending Context 仍没有 State |
217
+ | prepend 一页更早历史 | 只匹配新增的更早事件,按 `(kind, id)` 合并进 Context,保留现有 keyed node,并只重放受影响的 Context 与依赖 | 新发现的 start 会激活已收集 update;Location 或前序依赖变化也可能重跑 Context |
218
+ | append 一条实时事件 | 每个 Definition 各调用一次 `match`,按 key 查找命中的 Context,只更新该 Context | 对 start 之后的匹配事件执行一次 `update` 并请求一次发布;不扫描已有 Context |
219
+
220
+ 注册 `D` 个 Definition 时,一条新事件会进行 `D` 次仅当前事件匹配;命中后的 Context key 查询是常数时间。Definition 代码必须维持这个性质:正常 append 热路径不得遍历完整事件窗口、所有 Context、`context.matches` 或已渲染 Node 集合。累计事实放进 State,同 Turn/Step 共享信息放进 Location data,有索引的前序依赖使用 `reader.previous()`。
221
+
222
+ `publication` 控制发生 State 变更后何时物化。结构或 terminal 变化使用 `immediate`,高频可见 delta 使用 `animation-frame`,只为后续发布积累 State 时使用 `none`。引擎仍会按日志顺序应用每条 update;该选项只合并视图发布频率。
223
+
224
+ ## 5. 验证回放、分页与渲染
225
+
226
+ 添加聚焦测试,证明以下结果:
227
+
228
+ 1. 完整窗口通过 replace 后产生预期的最终 State、Location data、Node payload 与 `anchorSeq`。
229
+ 2. 只有 update 的尾部窗口保持 pending;prepend 唯一 start 后,结果与完整 replace 相同。
230
+ 3. 初始历史后继续实时 append,与回放合并后的完整窗口得到相同结果。
231
+ 4. prepend 更早分页只增加更早的行;数据未变化的既有 keyed Node value 不被替换。
232
+ 5. 重复的可见 delta 保持 `context.key`,并在请求 `animation-frame` 时每帧最多发布一次。
233
+ 6. keyed renderer 只消费 `node.data` 与受限 Location hook,不扫描 Session 事件窗口、Context 或 Chat Node。
234
+
235
+ 流式与中断处理可参考 [`packages/client/ui-conversation/src/client/conversation-nodes/assistant.ts`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/ui-conversation/src/client/conversation-nodes/assistant.ts),前序查询可参考 [`inbox.ts`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/ui-conversation/src/client/conversation-nodes/inbox.ts) 与 [`message.ts`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/ui-conversation/src/client/conversation-nodes/message.ts),只发布 Turn data 而不创建自有 Node 的例子见 [`packages/client/ui-deliverables`](https://github.com/deepseek-ai/deepseek-harness/tree/master/packages/client/ui-deliverables)。
@@ -0,0 +1,120 @@
1
+ ---
2
+ editSource: "docs/cookbook/adding-a-package.zh.md"
3
+ ---
4
+
5
+ # 实操手册:添加 workspace 包
6
+
7
+ 为新建 `@deepseek-ai/dsh-<name>` 包提供的逐文件清单。本清单以 bash 和适配器这两个包为模板进行验证;如果清单与模板有出入,请在此修正。
8
+
9
+ ## 1. 创建包
10
+
11
+ ```
12
+ packages/<group>/<pkg>/
13
+ package.json # copy from packages/core/tools, adjust name/description/deps
14
+ tsconfig.json # extends ../../../tsconfig.base.json, rootDir src,
15
+ # outDir lib/types, references: ../../../vendor/cosmokit,
16
+ # ../../../vendor/cordis (+ ../../../vendor/schemastery if
17
+ # you use Config, + ../../<group>/<dep> for each dsh dep)
18
+ src/index.ts # service default export or plugin (name/inject/apply/Config)
19
+ README.md # service API, events, extension points, design notes,
20
+ # + gated Model Experience context blocks or short form
21
+ # + the gated "Known Limitations and Deferred Work" section
22
+ # (or a whitelist entry in scripts/verify-package-readme-limitations.ts)
23
+ ```
24
+
25
+ 当已有分组与包的角色匹配时,选择该分组(`core`、`llm`、`bash`、`compact`、`subagent`、`todo`、`session-persistence`、`ui`、`util` 或 `support`)。允许新建分组,但分组只是纯容器:没有 `package.json`,没有源文件,包仍然恰好位于其下一层。
26
+
27
+ package.json 不变式(由 `pnpm run constraints` / `scripts/check-workspace-constraints.ts` 强制执行):`private: true`,`version` 与根 `package.json` 一致,`type: module`,`main: "lib/index.js"`,`types: "lib/types/index.d.ts"`,`exports["."].types: "./lib/types/index.d.ts"`,`exports["."].default: "./lib/index.js"`,`@deepseek-ai/cordis` 同时出现在 peerDependencies 和 devDependencies 中(相同范围)。每个 dsh 对等依赖(peer dependency)都要在 devDependencies 中镜像。`@deepseek-ai/schemastery` 放在 `dependencies` 中(它是运行时校验器),与 agent-loop 保持一致。`files` 列表精确包含 `lib/index.js`、`lib/invariant.js`、`lib/types/**/*.d.ts` 以及门禁认可的包专用运行时产物;如果包的运行时 export 指向输出树,还要包含 `lib/types/**/*.js`。不要发布 `src`、声明映射、JS map 或陈旧的根声明文件。带有 `bin` 的 CLI 应用包在 `files` 中将 `lib/bin.js` 紧跟在 `lib/index.js` 之后。
28
+
29
+ 包内的相对导入在源码中使用显式 `.ts` 后缀(例如 `export * from './types.ts'`)。编译器在输出的 JS 中将其重写为 `.js`,在声明文件中保留显式 `.ts` 后缀;标准的 NodeNext/Node16 TypeScript 消费方会将其解析到同目录的 `.d.ts` 文件。
30
+
31
+ ## 2. 在根配置中注册
32
+
33
+ | 文件 | 变更 |
34
+ |---|---|
35
+ | `tsconfig.base.json` | 已有分组无需编辑;新分组需为 `@deepseek-ai/dsh-*` 通配符添加 `./packages/<group>/*/src` 候选路径 |
36
+ | `tsconfig.host.json`(Host 包)或 `tsconfig.client.json`(Client 包) | 在 `references` 中添加 `{ "path": "./packages/<group>/<pkg>" }`——普通包恰好属于一个 aggregate,绝不两个都加。`api/remotes` 因 Host 生成约定与 Client 消费约定之间存在顺序依赖而使用仓库专属拆分,新增包不得仿照([布局](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/development.md#typescript-project-layout)) |
37
+ | `knip.json` | 仅当包有仓库发现机制尚未覆盖的入口时需要 |
38
+
39
+ `packages/client/*` 包改为 extends `tsconfig.base.client.json`(而非 `tsconfig.base.json`);client 插件包还需在 package.json 声明 `dsh.client`、导出 `./client`、调用共享 tsdown preset(`packages/client/tsdown.client.ts`)——client 侧见 [packages/client/AGENTS.md](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/AGENTS.md)。
40
+
41
+ 以下内容由 glob 或包 manifest(元数据清单)发现机制自动覆盖,无需手动编辑:根 `package.json` workspaces、`scripts/publint-all.ts`、`tsdown.config.ts`、`.oxlintrc.json`、`scripts/check-workspace-constraints.ts`。
42
+
43
+ ## 3. 确定包拓扑
44
+
45
+ 对于可替换的能力,当 Service Definition/Service Provider/Consumer 角色需要独立演进时,将它们拆分到不同包中(见 docs/architecture.md § "Capability seams"——shell 三组件是模板)。单一用途的插件保持为一个包。
46
+
47
+ ### 使用符合实际的角色名称
48
+
49
+ 名称必须描述当前稳定职责。不要用首个实现、可能的未来扩展或 Cordis 基类命名。接口包使用能力名称。实现包加上能够区分实现的机制、协议、环境或厂商限定词。只有同主机执行属于约定时,才使用 `local`。
50
+
51
+ 一个 engine、runtime、policy、controller、resolver、store 或当前配置使用单数 `ctx` key。registry 或拥有多个具名成员的服务使用复数 key。类的角色与 key 的单复数必须一致。不得让不兼容的 host 与 client 声明复用同一个 Cordis `Context` key。即使二者使用独立的运行时 context,TypeScript 声明合并仍会同时看到两种类型。如果自然复数已经属于另一个端面,就增加职责后缀。
52
+
53
+ | 词 | 适用条件 | 不适用条件 |
54
+ |---|---|---|
55
+ | `Controller` | 接受命令或用户意图,并改变一项既有领域状态或展示状态。 | 执行任意工作、拥有一组 provider,或只把值转换为展示形式。 |
56
+ | `Store` | 拥有一组数据,主要提供该数据的 CRUD、snapshot 或 subscription 操作。 | 校验状态机、裁决权限、分派工作或拥有 provider 优先级。类中有 map 不等于 store。 |
57
+ | `Directory` | 暴露供发现或选择的条目及其元数据。 | producer 向其中注册任意实现,或调用方通过它执行工作。 |
58
+ | `Presenter` | 将领域值或工具参数纯转换为渲染意图。 | 执行 I/O、订阅、修改状态或拥有生命周期。 |
59
+ | `Registry` | 拥有一组动态具名注册,以及查询、重复项或优先级规则、生命周期和释放。 | 主要约定是分派、执行、取消、策略或编排。 |
60
+ | `Runtime` | 运行实时工作,并跨调用拥有分派、取消、provider 协调或操作生命周期。 | 只存储记录、返回目录、解析一个值或保存配置。 |
61
+ | `Resolver` | 根据输入计算或定位一个答案,但不拥有该答案的生命周期。 | 拥有可变集合或长时间运行的执行过程。 |
62
+ | `Binder` | 把一个已声明接口绑定到调用方的 context 或生命周期,并返回绑定值。 | 把该值作为集合持有、控制其领域状态,或只转换数据。 |
63
+ | `Engine` | 实现领域算法或有状态执行模型。 | 只选择 provider 或跨协议边界转发请求。 |
64
+ | `Policy` | 决定允许、选择、限制或观察什么。 | 执行该决定所允许的机制。 |
65
+ | `Executor` | 在一项能力中运行一个明确请求或已解析 spec。 | 拥有广泛应用生命周期或 provider 目录。 |
66
+ | `Gateway` | 适配进程、网络、RPC 或 API 边界。 | 只注册同进程服务或存储元数据。 |
67
+ | `Provider` | 提供一项能力定义的一个实现。存在多个实现时,加上机制或厂商限定词。 | 表示能力定义、provider registry 或消费方 runtime。 |
68
+ | `Backend` | 在已定义接口之后实现可替换的底层持久化、传输或执行。 | 表示面向用户的服务或一个已返回的实时资源引用。 |
69
+ | `Handle` | 引用一个实时资源,并控制或观察该资源。 | 创建并管理完整资源池。 |
70
+ | `Config` | 拥有一个已解析配置值,或一项边界严格的配置记录及其更新约定。 | 存储通用集合、执行工作或暴露无关设置。 |
71
+ | `Service` | 拥有一项无法用以上更精确角色诚实描述的内聚领域服务。 | 只因为类继承 Cordis `Service` 而使用该名称。 |
72
+
73
+ 只对受支持的 Python 与 TypeScript SDK 所使用的 JSON-RPC 客户端/服务器协议使用 `SDK`。DeepSeek Harness 本身是 agent harness,不是 SDK 项目。产品拼写统一使用 `Typert`,不得使用 `TypeRT` 或 `typeRT`。
74
+
75
+ ## 4. 编写包 README
76
+
77
+ 将包特有的服务 API、配置、事件、扩展点和设计说明放在前面。limitations 部分记录持久的消费方缺口和本包拥有的非显而易见的维护者约束;日常清理事项留在源码 TODO 或 Agent Note 中。间接的 Model Experience 语句可以点名暴露本包贡献的消费方,但不重述该消费方的实现。包 README 以如下规范序列结尾:
78
+
79
+ ````markdown
80
+ ## Model Experience
81
+
82
+ ### Request context and condition
83
+
84
+ #### What the model sees
85
+
86
+ The exact data-dependent fields, an anchored generated-catalog link, or an introduction to the verbatim literal below.
87
+
88
+ ##### Verbatim text for this field, when needed
89
+
90
+ ```markdown
91
+ Stable system-prompt prose of any length, or another long non-generated literal, copied exactly from source.
92
+ ```
93
+
94
+ #### Token effect
95
+
96
+ Fixed, conditional, retained, replaced, capped, or zero-direct token effect.
97
+
98
+ #### KV Cache effect
99
+
100
+ Append-only, prefix-stable, replacing, or independent behavior, including the exact conditions that may invalidate reuse.
101
+
102
+ ## Known Limitations and Deferred Work
103
+
104
+ - **Consumer-visible gap** — exact missing operation or case, its consequence, and any maintainer constraint.
105
+ ````
106
+
107
+ 根据实现填写 Model Experience。每个直接、条件、上限、生命周期或辅助的模型上下文条目使用一个 H3,包含上述三个有序 H4 字段,每个字段下有一个正文段落。引用包拥有的稳定文本:系统提示词放在引出它的字段下,用带标题的 H5 加 `markdown` 围栏表示,通常归入 `What the model sees`;其他短文本以命名占位符内联,其他长文本使用相同的嵌套形式。仅概述数据依赖或提供方拥有的文本。工具 schema 条目链接到生成的[工具目录](../tool-catalog.md)中对应的锚定章节,仅说明该处缺失的差异。当作用域可以隐藏 prompt 或 schema 其中之一而不影响另一个时,将二者分开。填写 `KV Cache effect` 时,应区分仅追加增长、稳定重复的前缀、替换既有请求 token 和独立模型请求,并列出会使缓存复用失效、且由本包拥有的变化。“不使缓存失效”仅表示本包保留了已有的可复用前缀;缓存是否可用以及何时淘汰不属于本包约定。[行文标准](https://github.com/deepseek-ai/deepseek-harness/blob/master/.agents/skills/dsh-prose-standard/SKILL.md)约束完整性与归属;验证器强制执行所需章节结构。
108
+
109
+ 没有上下文效果或仅有消费方拥有路径的包使用 [`SENTENCE_MODEL_EXPERIENCE`](https://github.com/deepseek-ai/deepseek-harness/blob/master/scripts/verify-package-readme-model-experience.ts) 中经过审计的 `None, as ` 或 `Indirectly, through ` 语句,随后添加 `KV Cache effect` H4 和一个非空正文段落;与模型无关的通用包可以改为加入 `NO_MODEL_EXPERIENCE_SECTION`。两种情况都不要展开为对另一个包工作的描述。limitations [allowlist](https://github.com/deepseek-ai/deepseek-harness/blob/master/scripts/verify-package-readme-limitations.ts) 独立管理。[Model Experience Agent Note](https://github.com/deepseek-ai/deepseek-harness/blob/master/.agents/notes/implemented/process/2026-07-12-package-model-experience-contract.md) 记录了设计动机。
110
+
111
+ ## 5. 验证
112
+
113
+ ```sh
114
+ pnpm install # registers the workspace
115
+ pnpm run doc-sync
116
+ pnpm run constraints && pnpm run typecheck && pnpm run lint
117
+ pnpm run build && pnpm run hygiene
118
+ ```
119
+
120
+ 请遵循[仓库测试政策](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/testing.md),执行新包所需的行为专项检查并达到相应覆盖率。
@@ -0,0 +1,102 @@
1
+ ---
2
+ editSource: "docs/cookbook/adding-a-settings-card.zh.md"
3
+ ---
4
+
5
+ # Cookbook: 新增设置卡片
6
+
7
+ 插件如何把自己的配置放上 Web 设置页。这条路径上没有任何一步需要改动本仓库:Host 服务每一个已注册的 settings 命名空间,而**插件配置**分区以卡片所编辑的命名空间为键,因此同时注册了两个半侧的插件会被自动配对。
8
+
9
+ 两个半侧住在同一个包里——Host 半侧在 `src/`,浏览器半侧在 `src/client/`,以 `./client` 导出并用 `dsh.client` 声明。[`packages/client/ui-theme`](https://github.com/deepseek-ai/deepseek-harness/tree/master/packages/client/ui-theme) 是这种打包方式的现成例子;本分区自带的卡片在 [`packages/client/ui-settings-plugins`](https://github.com/deepseek-ai/deepseek-harness/tree/master/packages/client/ui-settings-plugins)。
10
+
11
+ ## 1. 注册命名空间(Host 半侧)
12
+
13
+ 命名空间就是配对用的键,所以只挑一次,并在两个半侧都写出它。已经有 `cordis.yml` entry 的消费方应通过 `installSettingsSection` 注册——它把 entry 层叠在用户文档之下,并在没有挂载 settings provider 时照常工作:
14
+
15
+ ```ts
16
+ import type { Context } from '@deepseek-ai/cordis'
17
+ import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings'
18
+ import z from '@deepseek-ai/schemastery'
19
+
20
+ declare function assertReachable(endpoint: string | undefined): void
21
+ declare function rebuildFromSettings(config: Config): void
22
+
23
+ export const MY_PLUGIN_NS = settingsNamespace('my-plugin')
24
+
25
+ export interface Config {
26
+ endpoint?: string
27
+ retries?: number
28
+ }
29
+
30
+ export const Config: z<Config> = z.object({
31
+ endpoint: z.string(),
32
+ retries: z.number().step(1).min(0).default(3),
33
+ })
34
+
35
+ export function apply(ctx: Context, config: Config) {
36
+ let source = () => config
37
+ installSettingsSection(ctx, MY_PLUGIN_NS, Config, config, {
38
+ // Constraints the schema cannot express refuse the write, not the next use.
39
+ validate: value => void assertReachable(value.endpoint),
40
+ setSource: (current) => { source = current },
41
+ onChange: () => { rebuildFromSettings(source()) },
42
+ })
43
+ }
44
+ ```
45
+
46
+ 字段上的 `role('secret')` 让它的值不出现在任何响应里;卡片把这类字段写进 `update`/`mutate` 载荷,或改为经 `credentials` 领域寻址一个凭据引用。`applies: 'restart'` 告诉配置表层:拥有方要到下次启动才会对变更生效。
47
+
48
+ ## 2. 注册卡片(浏览器半侧)
49
+
50
+ 卡片以自己的命名空间为键注册进 `settings.plugin.item`,并拥有其中的一切——外观、控件与文案。它通过 `ctx.settingsScope` 读写,后者用读取时的 revision 为每次写入设栅:
51
+
52
+ ```ts ignore-check
53
+ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
54
+ // Type-only: the keyed slot's declaration. Cross-plugin collaboration goes
55
+ // through cordis services; a value import fails the client bundle-purity gate.
56
+ import type {} from '@deepseek-ai/dsh-client-ui-settings-plugins/client'
57
+
58
+ export const inject = ['slots', 'locale', 'connection', 'remote', 'settingsScope']
59
+
60
+ export function apply(ctx: ClientContext): void {
61
+ const card = new MyPluginCardController(ctx.settingsScope.bind({ namespace: 'my-plugin' }))
62
+ ctx.slots.inject('settings.plugin.item', () => ctx.slots.register({
63
+ name: 'settings.plugin.item',
64
+ key: 'my-plugin',
65
+ locale: 'settings.myPlugin',
66
+ inject: () => card.inject(),
67
+ }, MyPluginCard),
68
+ )
69
+ }
70
+ ```
71
+
72
+ scope 快照携带表单所需的一切:解析后的 `value`、组装层 `base`,以及原始的 `user` 层——字段是否被覆盖,取决于它在 `user` 层中是否**出现**,而非它的值。`scope.set(field, value)` 存一个字段,`scope.unset(field)` 把它清回组装层。
73
+
74
+ ## 3. 标签页拿它做什么
75
+
76
+ **插件配置**标签页读取 Host 服务了哪些命名空间,并为每个命名空间派发一个 slot 键。当 Host 服务了某卡片的键时它被渲染,否则被跳过,因此从未组装过 Host 半侧的部署不会留下这张卡片的任何痕迹。被服务却无人认领的命名空间什么都不渲染——归其他页面所有的那些命名空间(`ui-theme`、`permission`、`llm-*`)正是这样留在本标签页之外的。
77
+
78
+ 卡片按其注册进该 slot 的顺序出现;keyed entry 不声明自己的 `order`。
79
+
80
+ ## 打包
81
+
82
+ 浏览器半侧由[客户端模块系统](https://github.com/deepseek-ai/deepseek-harness/tree/master/packages/client/modules)提供给页面:它扫描已启用的 Loader entries 中声明了 `dsh.client` 的包,并提供每个包构建出的 `./client` 导出。因此只要 `cordis.yml` 挂载了该插件,它就会出现在页面上——无需重新构建 Web 应用。
83
+
84
+ ```jsonc
85
+ {
86
+ "exports": {
87
+ ".": { "types": "./lib/types/index.d.ts", "default": "./lib/index.js" },
88
+ "./client": { "types": "./lib/types/client/index.d.ts", "default": "./lib/client.js" }
89
+ },
90
+ "dsh": { "client": { "platform": "web", "inject": ["@deepseek-ai/dsh-client-ui-settings-plugins"] } }
91
+ }
92
+ ```
93
+
94
+ bundle 必须是 loader 的 lazy-CJS factory 产物。在本仓库内,`tsdown.config.ts` 就是基于共享预设的三行:
95
+
96
+ ```ts ignore-check
97
+ import { clientBundle } from '../tsdown.client.ts'
98
+
99
+ export default clientBundle('@deepseek-ai/dsh-client-my-plugin', ['lib/types/index.js', 'lib/types/invariant.js'])
100
+ ```
101
+
102
+ 该预设目前未发布,因此本仓库之外的包得自行复刻同样的输出格式。bundle 纯净度门禁同时拒绝跨插件的值导入,所以卡片无法导入本分区的卡片外观或其暂存表单模型——它渲染自己的那一份,并自行拥有暂存与 revision 设栅。这两条限制都记在[本分区的已知限制](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/ui-settings-plugins/README.md#known-limitations-and-deferred-work)里。
@@ -0,0 +1,98 @@
1
+ ---
2
+ editSource: "docs/cookbook/adding-a-tool.zh.md"
3
+ ---
4
+
5
+ # 工具编写参考
6
+
7
+ 面向模型的工具必须满足哪些约定,均以本文为准。如需按步骤构建第一个工具,请阅读[构建工具](../../develop/basic/tool.md)。`packages/shell/tool-bash` 是生产级的三包示例。
8
+
9
+ ## 最小形态
10
+
11
+ ```ts
12
+ import { readFile } from 'node:fs/promises'
13
+ import type { Context } from '@deepseek-ai/cordis'
14
+ import { defineTool } from '@deepseek-ai/dsh-tools'
15
+
16
+ export const name = 'my-tool'
17
+ export const inject = ['tools']
18
+
19
+ export function apply(ctx: Context) {
20
+ ctx.tools.register(defineTool({
21
+ name: 'read_file',
22
+ description: 'Read a file from disk.', // what the model sees
23
+ parameters: {
24
+ path: { type: 'string', required: true, description: 'Absolute path' },
25
+ limit: { type: 'number' }, // optional by default
26
+ },
27
+ output: {
28
+ schema: { type: 'string' },
29
+ render: (_args, value) => [{ type: 'text', text: value }],
30
+ },
31
+ async execute(args, exec) {
32
+ // args is TYPED from the schema: { path: string; limit?: number }
33
+ // exec carries immutable identity + token; signal is the operational field
34
+ return readFile(args.path, { encoding: 'utf8', signal: exec.signal })
35
+ },
36
+ }))
37
+ }
38
+ ```
39
+
40
+ 注册基于副作用:dispose(资源释放)插件 fiber 即注销该工具。schema 会自动流入系统提示词的组装过程。
41
+
42
+ ## execute() 约定的规则
43
+
44
+ - **参数已为你校验。** `defineTool` 在 `execute` 运行前,会根据统一的 `ParameterSchemaSpec` 校验模型生成的 `arguments`(类型、必填键、字面量约束、恰好匹配一个分支的联合以及嵌套值——见[运行时参数校验](https://github.com/deepseek-ai/deepseek-harness/blob/master/.agents/notes/implemented/architecture/2026-06-11-runtime-arg-validation.md)),因此 `execute` 内的 args 会匹配 `InferArgs`。显式对象节点必须声明 `additionalProperties: true | false`;隐式参数根对象保持开放。你仍需手动检查 schema DSL 无法表达的约束,例如非空字符串、正数或跨字段规则。直接注册的原始 JSON Schema 工具自行负责输入校验。
45
+ - **注册借用你的只读定义。** 类型化的同进程贡献不是序列化边界;注册后不要修改其 schema 或替换回调。`schemas()` 只物化显式的模型可见投影。如需热替换工具,请 dispose 其所属副作用并注册替代品;回调闭包内的可变状态仍是普通的插件状态。
46
+ - **执行身份受保护。** 注册表在一次递归遍历中将 `arguments` 物化为分离的无损 JSON,在策略开始前冻结该值,并分配一个不透明的 `exec.token`;`callId`、`name`、`arguments`、`agent`、`token`、必填且由调用方持有的 `signal`,以及可选的外层传输 `parent` token 在整个分发过程中保持不可变。`parent` 仅用于身份标识,不暴露活跃的外层执行。请将 `args` 视为只读输入。只有 around-dispatch 包装器会收到可变视图;它可以替换并恢复必填的 `exec.signal` 以施加截止时间,但不能移除该信号。
47
+ - **声明并返回一个规范 JSON 值。** `output.schema` 使用 `ValueSchemaSpec`,根可以是对象、数组、标量或 null。`execute` 只返回推导出的值;注册表将其快照为无损 JSON,完成校验和冻结后,再传给 `output.render(args, value)`。工具主体不要返回内容块,也不要迫使调用方从自然语言中解析 id 和字段。
48
+ - **抛出异常或返回无效值意味着 `isError`。** 注册表会捕获异常,并在观察者运行前收敛 schema、渲染器、元数据投影器和无损 JSON 失败。基础设施故障请抛异常。成功的领域结果即使表示不理想的状态,也应写入规范值;其 Native 渲染器可以解释该状态,例如进程以非零状态退出。
49
+ - **遵守 `exec.signal`。** 信号触发时取消进行中的工作。
50
+ - **使用 `presentationMeta` 投影持久化的卡片数据(可选)。** `output.presentationMeta(args, value)` 从同一个规范值派生可回放的 JSON。核心将其持久化在 `tool/result` 上并传给 `presentResult`,因此需要结果期事实的卡片——例如 `write`/`edit` 的已应用 hunk——无需持久化规范值也能在回放中重现。嵌套 Code 分发没有卡片,因此会跳过该投影器。
51
+ - **使用 `exec.agent` 发送异步通知。** `agent.inject({ content, source: { kind: 'plugin', plugin: '<name>' } })` 追加持久化上下文,下一次模型请求会看到它——这不是唤醒(空闲的 agent(智能体)保持空闲)。请防范已 dispose 的 agent(try/catch)。
52
+
53
+ ## 长时间运行的工作
54
+
55
+ 通过 producer 配置控制 `run_in_background`,然后使用 `ctx.jobs.start({ kind, label, owner: exec.agent, run })` 注册任务。注册表会在进入 producer 主体前将已预先中止的调用判为失败;运行时会在 `run()` 启动工作前校验 owner 和任务控制器是否可用,随后提供 id、会话围栏、通用控制工具、通知和 owner cleanup。成功的后台分支会返回类型化的规范句柄,如 `{ kind: 'background', jobId }`;其 Native 渲染器可以保留 `started background job bash-1` 这类供人阅读的自然语言,但 Code Mode 绝不能通过解析该文本取得 id。
56
+
57
+ producer 提供同步的 `cancel`、在资源清理后 settle 且不 reject 的 `done`,以及可选的消费式 `readOutput`(负责有界输出的格式化)。预先中止的调用属于失败,因为此时没有任务,其 id 无法满足成功输出 schema。`ctx.jobs.start()` 发布 id 后,应使用任务自有的取消信号,而不是 `exec.signal`:之后取消外层调用只会停止等待本次调用,不会终止已经发布的工作;该生命周期归 `job_kill`、owner dispose 和服务 teardown 所有。前台工作仍与 `exec.signal` 耦合。流式 producer 的示例和完整约定见[后台任务运行时 Agent Note](https://github.com/deepseek-ai/deepseek-harness/blob/master/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md)与 `dsh-tool-bash`。
58
+
59
+ <a id="execution-policy-and-observation"></a>
60
+
61
+ ## 执行策略与观测
62
+
63
+ 尽量不要把部署策略内建到工具中。使用 `tools/pre-execute` 实现可扩展的允许/拒绝/询问策略(见[权限门禁示例](./extension-cookbook.md#a-hook-plugin-permission-gate-example));使用 `ctx.tools.guard()` 设置最终的单调拒绝,后续监听器无法撤销;使用 `tools/execute` 为分发添加截止时间、重试或指标收集;使用 `tools/post-execute` 替换展示内容或返回值、阻止结果,或附加模型可见上下文;使用 `tools/result` 观测不可变的归一化结果而不改变它。替换内容不会阻止程序化访问 `value`;保密策略会屏蔽或替换该值。沙箱实现也可以在工具的执行器实现中运行;[`dsh-tools` README](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/core/tools/README.md#extension-points) 定义每个扩展点的输入、顺序、返回值和失败行为。
64
+
65
+ ## Code Mode 自动触达你的工具
66
+
67
+ 在 [Code Mode](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/core/tools/README.md) 中,每个可见的已注册工具都可通过 `await tools.<name>(args)` 调用,无需额外集成。生成的 `ToolArgsMap` 和 `ToolOutputMap` 会根据同一组 schema 分别派生精确的参数类型与规范返回类型,调用则重新进入正常的执行流水线。成功调用会解析为策略处理后的最终规范 JSON 值,而不是渲染后的 Native 内容。失败调用会以真正的 `ToolCallError` reject;程序只能检查其 `name`、`toolName` 和可供人阅读的 `message`,无法取得内部错误代码或失败联合。
68
+
69
+ 请把 `output.schema` 设计为实用的程序化 API:直接返回句柄与字段;当标量、数组或 null 确实就是结果时,允许采用相应的根类型;将面向人类的解释放入 `output.render`。中间值只存在于执行期间,不会被持久化或按提示词上限截断,也不设字节上限,因此生产方如实声明的采集边界和进程内存仍然重要。只有外层 `run_code` 日志/结果会受到可配置输出上限和面向模型的 spill 流水线约束。
70
+
71
+ ## 工具在 UI 中的渲染方式
72
+
73
+ 工具的 `output.render` 返回模型可见的内容;其 **UI 卡片** 是另一项独立关注点,通过纯展示投影以及可选的 `presentCall`/`presentResult` 方法声明。请将这些内容与规范值一并设计。没有 UI 展示方法的工具会回退到通用卡片(标题 = 工具名,原始 args 作为输入)。
74
+
75
+ 两个方法都返回一个 **`card` 标签的渲染意图**——选择与你的工具行为匹配的卡片类型:
76
+
77
+ - `presentCall(args)` → 一个 `ToolCallView`(PENDING 卡片):
78
+ - `{ card: 'generic', title, kind?, rawInput?, content?, locations? }`——默认。设置 `kind` 获取图标(`read`/`search`/…);设置 `locations: [{ path, line? }]` 标注工具涉及的文件,使有能力的编辑器跟随/跳转。
79
+ - `{ card: 'terminal', title, description?, cwd? }`——你的调用本身就是 shell 命令。`title` 是命令,`description` 渲染在终端卡片上方。(tool-bash。)
80
+ - `{ card: 'diff', title, diffs, locations? }`——你的调用创建或修改文件。`diffs: [{ path, oldText, newText }]`(新文件时 `oldText: null`)渲染为内联 diff 卡片。(tool-fs `write`/`edit`。)
81
+ - `presentResult(args, { content, isError, meta? })` 返回完成后的卡片:
82
+ - `generic` 提供可选的标题和内容。
83
+ - `terminal` 提供原始输出和可选的退出元数据;各 UI 根据自身能力渲染对应视图或回退视图。
84
+ - `diff` 提供已应用的 hunk,通常由 `output.presentationMeta` 派生并通过持久化的 `result.meta` 携带,使回放能重现它们。变更类工具保留 diff 结果,因为完成后的视图会替换 pending 卡片。
85
+ - `search` 提供从持久化 `result.meta` 重建的发现型结果:按文件分组的匹配(`shape: 'matches'`,grep)或扁平路径列表(`shape: 'paths'`,glob),外加 `truncated`/`total` 使 UI 永不把被截断的结果当作完整结果呈现。该视图不携带结果文本(无 search 卡片的 UI 回退到原始结果内容),也没有 `search` 调用视图——发现型调用的 pending 状态保持为 generic 卡片,因为匹配只在 `execute` 之后才存在。(tool-fs-search 的 `grep`/`glob`。)
86
+ - `web` 提供已完成的 web 检索,以 `kind: 'search' | 'fetch'` 区分(结构化的搜索来源或抓取摘要),由 `result.meta` 派生;它不携带正文副本,因此不具备 `web` 能力的 UI 回退到原始结果内容。(tool-web `web_search`/`web_fetch`。)
87
+
88
+ 硬性规则(违反会出问题):
89
+
90
+ - **纯函数。** 这些方法在实时流式输出和会话日志回放时都会运行,因此必须是 `args`(加 result)的纯函数——不做 I/O、不读会话状态、不用时钟/随机数。diff 从 args 派生(`write` 使用 `oldText: null`,因为调用时的展示器没有文件先前内容);会话上下文由 UI 适配器而非工具提供。如果你发现自己想在 `presentCall` 内获取文件旧内容或工作目录,请停下:那属于持久结果元数据或适配器,不属于展示器。
91
+ - **UI 格式不进入模型结果。** 围栏 ` ```console ` 块、diff、相对化路径均不应仅为服务 UI 而进入规范值或 Native 内容。`output.render` 负责模型可见的自然语言;`presentationMeta` 和卡片展示器负责可回放的 UI 状态。`terminal` 结果视图携带原始输出,由适配器按需添加回退格式。
92
+ - **`defineTool` 对展示路径做软校验。** 格式错误或旧版日志中的参数会使包装器返回 `undefined`(通用回退)而非抛异常——展示绝不能导致回放崩溃。
93
+
94
+ 中性词汇定义在 `dsh-tools` 中;工具绝不导入 UI 或传输类型。host/client 运行时将每个 `card` 映射到各自的视图。设计与原因见[渲染意图联合体 Agent Note](https://github.com/deepseek-ai/deepseek-harness/blob/master/.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md);`dsh-tool-fs`(generic/diff)和 `dsh-tool-bash`(terminal)是参考实现。
95
+
96
+ ## 验证
97
+
98
+ 遵循[仓库测试策略](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/testing.md)和所属包的测试文档。已交付且面向模型或 UI 的变更必须提供其中规定的组装覆盖。
@@ -0,0 +1,45 @@
1
+ ---
2
+ editSource: "docs/cookbook/adding-an-llm-adapter.zh.md"
3
+ ---
4
+
5
+ # 实操手册:添加 LLM(大语言模型)适配器
6
+
7
+ 如何接入一个新的模型提供方。参考实现:`packages/llm/llm-deepseek`(直接 HTTP,SSE(Server-Sent Events)由 `eventsource-parser` 分帧)与 `packages/llm/llm-pi-ai`(封装 LLM 库)。请先阅读 `packages/llm/llm/src/types.ts` 中的 `StreamChunk` 文档——它记录了两个适配器都经过验证的协议约定。
8
+
9
+ ## 基本形态
10
+
11
+ ```ts ignore-check
12
+ class MyAdapter extends LlmAdapter {
13
+ async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> { … }
14
+ }
15
+
16
+ export const name = 'llm-myprovider'
17
+ export const inject = ['llm']
18
+ export const Config: z<Config> = z.object({ apiKey: z.string(), … })
19
+
20
+ export function apply(ctx: Context, config: Config) {
21
+ ctx.llm.registerAdapter(['my-provider'], new MyAdapter(…))
22
+ }
23
+ ```
24
+
25
+ 注册基于副作用,可安全支持 HMR(热模块替换);每个提供方路由仅对应一个适配器,重复注册会抛出异常,多路由注册要么全部成功,要么全部失败。`options.provider` 用于选择适配器,`options.model` 是提供方模型 ID,因此动态模型目录适配器无需重新配置生命周期即可提供新模型。密钥采用 Cordis 原生方式管理:schemastery Config 带环境变量回退,通过 cordis.yml 的 `!!js process.env.MY_KEY` 注入。切勿在代码中读取自行约定的密钥文件。
26
+
27
+ ## 协议义务(两个实现共同验证的约定)
28
+
29
+ - 在 `finish` **之前**发出 `usage`;`finish` 之后**不再发出任何内容**。稳健做法:缓冲 finish/usage 直到提供方的流结束标记,再统一 flush(可处理提供方在末尾发送仅含 usage 的分片的情况)。
30
+ - 工具调用的 `arguments` 全程为原始 JSON 字符串;流式片段以 `argumentsDelta` 发送。如果你的提供方返回已解析的对象,请在 `block-end` 时重新 stringify。
31
+ - 按首次出现的流顺序分配块 `index`;同一个块的每次 delta 复用该 index。
32
+ - 错误有且仅有两条合法路径:从 `stream()` **抛出**(传输与协议故障——使用带稳定 code 的 `LlmError`),或以 `finish {kind: 'error' | 'aborted'}` 结束流(提供方带内故障)。消费方两者都处理;按故障类别选择路径并加以文档化。
33
+ - 遵守 `options.signal`(将其传递给 fetch 或你的 SDK)。
34
+ - 如果 `GenerateOptions` 中某个字段你的提供方无法支持(例如提供方不支持 stop sequences 时收到 `stop` 列表):抛出 `LlmError(..., 'UNSUPPORTED')`,而非静默丢弃。
35
+ - 如果提供方在后续调用中需要响应 ID、签名或其他原生元数据,请将其最小无损 JSON 投影作为 `finish.replayState` 发出。重建历史时验证该状态。只有历史提供方路由和目标提供方路由当前由完全相同的适配器实例拥有时,`LlmRuntime` 才会传递该状态;由适配器决定同模型、跨模型或跨提供方恢复是否合法。状态缺失时,切勿仅根据提供方/模型名称推断原生回放。
36
+
37
+ 提供方特有的思考模式开关仍放在适配器的 Config 中。确切模型元数据使用一处提供方无关的能力 seam:实现 `resolveModel()`,返回提供方/模型身份以及可选的 `context` 和 `reasoning` 字段;仅当存在配置指定的默认值时才声明 `defaultEffort`;遵守解析模型时传入的可选 `AbortSignal`。推理(reasoning)强度是由适配器映射到提供方请求的有序不透明 ID。请保留适配器给出的权威可选列表,包括适配器在支持时定义的 `off`;不得暴露最终协议值的具体拼写,也不得自动调整不支持的值。ID 无需与其协议表示相同。
38
+
39
+ ## 实现结构
40
+
41
+ 让协议格式(wire format)类型、请求序列化、传输解析、分片转换和适配器类分别承担独立职责;[`llm-deepseek`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/llm/llm-deepseek/README.md) 是参考布局。
42
+
43
+ ## 验证
44
+
45
+ 遵循[仓库测试策略](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/testing.md),该策略负责适配器覆盖、真实提供方检查和已发布入口要求。