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.md"
3
+ ---
4
+
5
+ # Add a Web Client conversation node
6
+
7
+ This tutorial adds one business-owned row to the Web Client Chat view. The finished plugin correlates a durable Session event family into one Context, incrementally builds business State, publishes typed Step data, and renders a keyed Chat Node without scanning the Session window or other rendered nodes. It assumes the Host already records the events and the client plugin is composed into the Web bundle; external Host-side UIs and additional view targets such as Trajectory are outside this tutorial.
8
+
9
+ The [Conversation Node assembly decision](https://github.com/deepseek-ai/deepseek-harness/blob/master/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.md) owns the rationale and complete engine model. This guide covers the implementation path.
10
+
11
+ ## 1. Design a replayable event family
12
+
13
+ Choose one stable business id before writing the Definition. Every event that contributes to the same Node must carry that id or derive it independently from its own payload; the client must never assign an update to “the latest unfinished” Context.
14
+
15
+ For a review job, the event contract could be:
16
+
17
+ | Event | Role | Required durable facts |
18
+ |---|---|---|
19
+ | `review/start` | unique start | `reviewId`, Turn/Step coordinates, title |
20
+ | `review/progress` | update | the same `reviewId`, coordinates, replayable progress |
21
+ | `review/end` | update | the same `reviewId`, coordinates, final summary |
22
+
23
+ Use the producer-owned branded id type across the process boundary. Put the `SessionEventMap` merge and payload types on the producer's type-only export, then import that export for side effects from the client package. Each `(kind, id)` may have at most one start event. A single-event business can use the event's stable identity, such as `event.seq`, as its Definition-local id.
24
+
25
+ Incremental events are supported. Prefer whole-value checkpoints when the producer can emit them cheaply, because they remain useful when the start is outside the loaded window. Each delta must carry the stable id and produce deterministic State when replayed in ascending log `seq`; it must not depend on live-only memory. If the current history window contains only updates, the assembler keeps a pending Context and builds no State until an older page supplies the start. If the product must render before the start is loaded, a terminal or checkpoint event must carry enough whole fallback state for the Definition to build that result directly; do not recover it by scanning unrelated events.
26
+
27
+ ## 2. Implement the Definition and typed Chat payload
28
+
29
+ The example keeps the producer declarations and client contribution in one block so the complete relationship is visible. In a package family, keep the branded id and `SessionEventMap` declaration with the event producer, and keep the Definition, Chat data merge, and renderer in the client plugin.
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)` is an identity extractor, not a fold: it receives only the current event and returns the Definition-local id and lifecycle role. After a match, the assembler locates the Context by `(kind, id)` and calls `start` once or `update` with the current State. Both functions return the State that the engine adopts; returning a new immutable value is preferred, but a function that mutates and returns the same object has the same adoption semantics.
199
+
200
+ `buildLocationData(context, scope)` optionally publishes Definition-owned data onto an engine-owned Turn or Step. Use declaration merging to give each key a precise value type. Another Node in the same Location can consume that value through its constrained slot hook, such as `useTurnData(key)`, without receiving the Session or scanning `snapshot.chat.nodes`.
201
+
202
+ `target` and `buildViewNode(context)` declare one target-owned rendering contribution and must appear together. Preserve `context.key` as the React-facing identity, choose `anchorSeq` from durable ordering evidence, and return only renderer-ready data. Once a target Node has been published, keep returning the same key; use `visibility: 'hidden'` when it must temporarily leave the visible flow rather than withdrawing it with `null`.
203
+
204
+ ## 3. Query an earlier business Context only at start
205
+
206
+ Some Definitions need the latest earlier State of another business kind. `start` receives a `ConversationContextReader`; call `reader.previous<State>(kind)` there instead of accepting a Context collection or scanning events. The reader returns the nearest started Context before the current start `seq` as read-only data.
207
+
208
+ The assembler records that dependency. If an older prepend later supplies a nearer predecessor, closes a previously unknown window gap, or revises the predecessor State, it reruns the dependent Context from `start` and replays its updates in ascending `seq`. The queried Definition remains responsible for writing useful State; the reader exposes no business-specific query methods and grants no mutation authority over another Context.
209
+
210
+ ## 4. Understand the three ingestion paths
211
+
212
+ History may be requested from the tail backward one page at a time, but every accepted page is normalized into ascending `seq` before State replay.
213
+
214
+ | Path | Engine work | Definition-visible behavior |
215
+ |---|---|---|
216
+ | Replace on open, resync, or gap repair | Rebuild the loaded window, match every event once per Definition, then replay each started Context | `start`, followed by its updates in ascending `seq`; pending update-only Contexts remain without State |
217
+ | Prepend one older page | Match only fresh older events, merge them into Contexts by `(kind, id)`, preserve existing keyed nodes, and replay only affected Contexts and dependencies | A newly found start activates its collected updates; a changed Location or predecessor may rerun the Context |
218
+ | Append one live event | Call each Definition's `match` once, look up the matched Context by key, and update only that Context | One `update` and one requested publication for a matching post-start event; no existing Context scan |
219
+
220
+ With `D` registered Definitions, one incoming event performs `D` current-event matches and constant-time Context-key lookup after a match. Definition code must preserve that property: do not traverse the complete event window, every Context, `context.matches`, or the rendered Node collection on the normal append path. Use State for accumulated facts, Location data for same-Turn/Step sharing, and `reader.previous()` for indexed predecessor dependencies.
221
+
222
+ `publication` controls when changed State is materialized. Use `immediate` for structural or terminal changes, `animation-frame` for high-frequency visible deltas, and `none` when the State change feeds only a later publication. The engine still applies every update in log order; cadence only coalesces view publication.
223
+
224
+ ## 5. Verify replay, pagination, and rendering
225
+
226
+ Add focused tests that establish these outcomes:
227
+
228
+ 1. A complete window passed through replace produces the expected final State, Location data, Node payload, and `anchorSeq`.
229
+ 2. An update-only tail stays pending; prepending the unique start produces the same result as a complete replace.
230
+ 3. Initial history followed by live append produces the same result as replaying the combined window.
231
+ 4. Prepending an older page adds earlier rows without replacing existing keyed Node values whose data did not change.
232
+ 5. Repeated visible deltas preserve `context.key` and publish at most once per animation frame when requested.
233
+ 6. The keyed renderer consumes `node.data` and constrained Location hooks only; it does not scan the Session event window, Contexts, or Chat Nodes.
234
+
235
+ Use [`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) for streaming and interruption, [`inbox.ts`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/ui-conversation/src/client/conversation-nodes/inbox.ts) plus [`message.ts`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/ui-conversation/src/client/conversation-nodes/message.ts) for predecessor queries, and [`packages/client/ui-deliverables`](https://github.com/deepseek-ai/deepseek-harness/tree/master/packages/client/ui-deliverables) for a Definition that publishes Turn data without creating its own Node.
@@ -0,0 +1,120 @@
1
+ ---
2
+ editSource: "docs/cookbook/adding-a-package.md"
3
+ ---
4
+
5
+ # Cookbook: adding a workspace package
6
+
7
+ The file-by-file checklist for a new `@deepseek-ai/dsh-<name>` package. This checklist is validated against the bash and adapter packages as templates; if it drifts from them, fix it here.
8
+
9
+ ## 1. Create the package
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
+ Choose an existing group when one matches the package's role (`core`, `llm`, `bash`, `compact`, `subagent`, `todo`, `session-persistence`, `ui`, `util`, or `support`). A new group is allowed, but it is a pure container: no `package.json`, no source files, and packages still sit exactly one level below it.
26
+
27
+ package.json invariants (enforced by `pnpm run constraints` / `scripts/check-workspace-constraints.ts`): `private: true`, a `version` matching the root `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` in BOTH peerDependencies and devDependencies (same range). Mirror every dsh peer dependency in devDependencies. `@deepseek-ai/schemastery` goes in `dependencies` (it is a runtime validator), matching agent-loop. The `files` list contains exactly `lib/index.js`, `lib/invariant.js`, `lib/types/**/*.d.ts`, and package-specific runtime artifacts recognized by the gate; a package whose runtime export points into the emitted tree also includes `lib/types/**/*.js`. Do not publish `src`, declaration maps, JS maps, or stale root declaration files. CLI app packages with a package `bin` include `lib/bin.js` immediately after `lib/index.js` in `files`.
28
+
29
+ In-package relative imports use explicit `.ts` specifiers in source (for example, `export * from './types.ts'`). The compiler rewrites those to `.js` in emitted JS and leaves explicit `.ts` specifiers in declarations, which standard NodeNext/Node16 TypeScript consumers resolve to the sibling `.d.ts` files.
30
+
31
+ ## 2. Register it in the root configs
32
+
33
+ | File | Change |
34
+ |---|---|
35
+ | `tsconfig.base.json` | no edit for an existing group; for a new group, add a `./packages/<group>/*/src` candidate to the `@deepseek-ai/dsh-*` wildcard |
36
+ | `tsconfig.host.json` (Host package) or `tsconfig.client.json` (Client package) | add `{ "path": "./packages/<group>/<pkg>" }` to `references` — an ordinary package belongs to exactly one aggregate, never both. `api/remotes` uses a repository-specific split because the Host generates a contract that the Client consumes in a later phase; new packages must not copy it ([layout](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/development.md#typescript-project-layout)) |
37
+ | `knip.json` | only if the package has entrypoints that repository discovery does not already cover |
38
+
39
+ A `packages/client/*` package additionally extends `tsconfig.base.client.json` instead of `tsconfig.base.json`, and a client plugin package declares `dsh.client` in package.json, exports `./client`, and calls the shared tsdown preset (`packages/client/tsdown.client.ts`) — see [packages/client/AGENTS.md](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/AGENTS.md) for the client-side contract.
40
+
41
+ Covered automatically by globs or package-manifest discovery — no edits needed: root `package.json` workspaces, `scripts/publint-all.ts`, `tsdown.config.ts`, `.oxlintrc.json`, `scripts/check-workspace-constraints.ts`.
42
+
43
+ ## 3. Decide the package topology
44
+
45
+ For a swappable capability, separate Service Definition / Service Provider / Consumer roles into packages when they evolve independently (see docs/architecture.md § "Capability seams" — the shell trio is the template). A single-purpose plugin stays one package.
46
+
47
+ ### Name the role that exists
48
+
49
+ Name the stable current responsibility. Do not name the first implementation, a possible future expansion, or the Cordis base class. An interface package names the capability. An implementation package adds the mechanism, protocol, environment, or vendor that distinguishes it. Use `local` only when same-host execution is part of the contract.
50
+
51
+ Use a singular `ctx` key for one engine, runtime, policy, controller, resolver, store, or current configuration. Use a plural key for a registry or a service that owns multiple named members. The class role and key number must agree. Do not reuse one Cordis `Context` key for incompatible host and client declarations. TypeScript declaration merging sees both faces even when they use separate runtime contexts. Add the role suffix when the natural plural already belongs to another face.
52
+
53
+ | Word | Use it when | Do not use it when |
54
+ |---|---|---|
55
+ | `Controller` | It accepts commands or user intent and changes one existing domain or presentation state. | It executes arbitrary work, owns a provider fleet, or only converts values for display. |
56
+ | `Store` | It owns one data set and mainly offers CRUD, snapshot, or subscription operations for that data. | It validates a state machine, arbitrates authority, dispatches work, or owns provider precedence. A map does not make a class a store. |
57
+ | `Directory` | It exposes entries and metadata for discovery or selection. | Producers register arbitrary implementations into it, or callers execute work through it. |
58
+ | `Presenter` | It is a pure conversion from domain values or tool arguments to render intent. | It performs I/O, subscribes, mutates state, or owns lifecycle. |
59
+ | `Registry` | It owns a dynamic set of named registrations, including lookup, duplicate or precedence rules, lifetime, and disposal. | Its main contract is dispatch, execution, cancellation, policy, or orchestration. |
60
+ | `Runtime` | It runs live work and owns dispatch, cancellation, provider coordination, or operation lifecycle across calls. | It only stores records, returns a catalog, resolves one value, or holds configuration. |
61
+ | `Resolver` | It computes or locates one answer from supplied inputs without owning that answer's lifecycle. | It owns a mutable collection or long-running execution. |
62
+ | `Binder` | It attaches one declared interface to a caller context or lifecycle and returns the bound value. | It owns the value as a collection, controls its domain state, or only converts data. |
63
+ | `Engine` | It implements a domain algorithm or stateful execution model. | It only selects a provider or forwards across a protocol boundary. |
64
+ | `Policy` | It decides what is allowed, selected, limited, or observed. | It performs the mechanism that the decision permits. |
65
+ | `Executor` | It runs one explicit request or resolved specification in one capability. | It owns a broad application lifecycle or provider catalog. |
66
+ | `Gateway` | It adapts a process, network, RPC, or API boundary. | It only registers same-process services or stores metadata. |
67
+ | `Provider` | It supplies one implementation of a capability definition. Add a mechanism or vendor qualifier when several can exist. | It is the capability definition, provider registry, or consumer runtime. |
68
+ | `Backend` | It implements replaceable lower-level persistence, transport, or execution behind a defined interface. | It is a user-facing service or one returned live-resource reference. |
69
+ | `Handle` | It refers to one live resource and controls or observes that resource. | It creates and manages the complete resource pool. |
70
+ | `Config` | It owns one resolved configuration value or one tightly bounded record and its update contract. | It stores a general collection, executes work, or exposes unrelated settings. |
71
+ | `Service` | It owns a cohesive domain service that no sharper role above states honestly. | The name exists only because the class extends Cordis `Service`. |
72
+
73
+ Use `SDK` only for the JSON-RPC client/server protocol used by the supported Python and TypeScript SDKs. DeepSeek Harness itself is an agent harness, not an SDK project. Use the canonical product spelling `Typert`, never `TypeRT` or `typeRT`.
74
+
75
+ ## 4. Write the package README
76
+
77
+ Keep package-specific service API, config, events, extension points, and design notes first. The limitations section records durable consumer gaps and non-obvious maintainer constraints owned by this package; ordinary cleanup stays in its source TODO or Agent Note. An indirect Model Experience sentence may name the consumer that surfaces this package's contribution, but it does not restate that consumer's implementation. End a package README with this canonical sequence:
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
+ Fill Model Experience from the implementation. Use one H3 per direct, conditional, capped, lifetime, or auxiliary model-context entry, with the three ordered H4 fields shown above and one prose paragraph under each. Quote stable text owned by the package: system-prompt prose goes in a titled H5 plus `markdown` fence under the field that introduces it—normally `What the model sees`—other short literals stay inline with named placeholders, and other long literals use the same nested form. Summarize only data-dependent or provider-owned text. A tool-schema entry links its anchored section in the generated [tool catalog](../tool-catalog.md) and states only deltas absent there. Keep prompt and schema entries separate when scoping can hide one without the other. In `KV Cache effect`, distinguish append-only growth, a stable repeated prefix, replacement of earlier request tokens, and an independent model request, then name the package-owned changes that can invalidate reuse. “Does not invalidate” means the package preserves an already-reusable prefix; provider cache availability and eviction remain outside the package contract. The [prose standard](https://github.com/deepseek-ai/deepseek-harness/blob/master/.agents/skills/dsh-prose-standard/SKILL.md) governs completeness and ownership; the verifier enforces the required section structure.
108
+
109
+ A package with no context effect or one consumer-owned path uses the audited `None, as ` or `Indirectly, through ` sentence in [`SENTENCE_MODEL_EXPERIENCE`](https://github.com/deepseek-ai/deepseek-harness/blob/master/scripts/verify-package-readme-model-experience.ts), followed by a `KV Cache effect` H4 and one non-empty paragraph; a model-agnostic generic package may instead join `NO_MODEL_EXPERIENCE_SECTION`. Do not expand either case into a description of another package's work. The limitations [allowlist](https://github.com/deepseek-ai/deepseek-harness/blob/master/scripts/verify-package-readme-limitations.ts) is independent. The [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) records the rationale.
110
+
111
+ ## 5. Verify
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
+ Follow the [repository testing policy](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/testing.md) for the behavior-specific checks and coverage required by the new package.
@@ -0,0 +1,102 @@
1
+ ---
2
+ editSource: "docs/cookbook/adding-a-settings-card.md"
3
+ ---
4
+
5
+ # Cookbook: adding a settings card
6
+
7
+ How a plugin puts its own configuration on the web settings page. Nothing in this path needs a change inside this repository: the Host serves every registered settings namespace, and the **Plugins** section keys its cards on the namespace they edit, so a plugin that registers both halves is paired up automatically.
8
+
9
+ The two halves live in one package — the Host half under `src/`, the browser half under `src/client/`, exported as `./client` and declared with `dsh.client`. [`packages/client/ui-theme`](https://github.com/deepseek-ai/deepseek-harness/tree/master/packages/client/ui-theme) is a worked example of that packaging; the cards this section ships live in [`packages/client/ui-settings-plugins`](https://github.com/deepseek-ai/deepseek-harness/tree/master/packages/client/ui-settings-plugins).
10
+
11
+ ## 1. Register the namespace (Host half)
12
+
13
+ The namespace is the join key, so pick it once and spell it in both halves. A consumer that already has a `cordis.yml` entry should register through `installSettingsSection`, which layers the entry under the user document and keeps working when no settings provider is mounted:
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')` on a field keeps its value off every response; the card writes such a field into an `update`/`mutate` payload, or addresses a credential reference through the `credentials` domain instead. `applies: 'restart'` tells a configuration surface the owner acts on a change only at the next start.
47
+
48
+ ## 2. Register the card (browser half)
49
+
50
+ The card registers into `settings.plugin.item` under its namespace and owns everything inside it — chrome, controls, and copy. It reads and writes through `ctx.settingsScope`, which fences each write with the revision it read:
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
+ The scope snapshot carries what a form needs: the resolved `value`, the composition `base`, and the raw `user` layer, whose key **presence** — not its value — is what marks a field overridden. `scope.set(field, value)` stores one field and `scope.unset(field)` clears it back to the composition layer.
73
+
74
+ ## 3. What the tab does with it
75
+
76
+ The **Plugin configuration** tab reads which namespaces the Host serves and dispatches one slot key per namespace. A card is rendered when the Host serves its key and skipped when it does not, so a deployment that never composed the Host half shows no trace of the card. A served namespace no card claims renders nothing — that is how the namespaces owned by other pages (`ui-theme`, `permission`, `llm-*`) stay off this tab.
77
+
78
+ Cards appear in the order they registered into the slot; a keyed entry declares no `order` of its own.
79
+
80
+ ## Packaging
81
+
82
+ The browser half is served to the page by the [client module system](https://github.com/deepseek-ai/deepseek-harness/tree/master/packages/client/modules), which scans the enabled Loader entries for packages declaring `dsh.client` and serves each one's built `./client` export. So the plugin appears on the page as soon as a `cordis.yml` mounts it — no rebuild of the web application.
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
+ The bundle must be the loader's lazy-CJS factory artifact. Inside this repository `tsdown.config.ts` is three lines over the shared preset:
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
+ That preset is not published today, so a package outside this repository has to reproduce the same output format itself. The bundle-purity gate also rejects value imports across plugins, so a card cannot import this section's card chrome or its staged-form model — it renders its own, and owns its own staging and revision fencing. Both limits are recorded under [the section's known limitations](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/ui-settings-plugins/README.md#known-limitations-and-deferred-work).
@@ -0,0 +1,96 @@
1
+ ---
2
+ editSource: "docs/cookbook/adding-a-tool.md"
3
+ ---
4
+
5
+ # Tool authoring reference
6
+
7
+ Reference for the contracts a model-facing tool must satisfy. For an ordered first tool, follow [Build a tool](../../develop/basic/tool.md). `packages/shell/tool-bash` is the production-grade three-package example.
8
+
9
+ ## The minimal shape
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
+ Registration is effect-based: disposing the plugin fiber unregisters the tool. Schemas flow into the system-prompt assembly automatically.
41
+
42
+ ## Rules of the execute() contract
43
+
44
+ - **Args are validated for you.** `defineTool` validates model-generated `arguments` against the unified `ParameterSchemaSpec` before `execute` runs (types, required keys, literal constraints, exact-one unions, and nested values — [runtime arg validation](https://github.com/deepseek-ai/deepseek-harness/blob/master/.agents/notes/implemented/architecture/2026-06-11-runtime-arg-validation.md)), so inside `execute` the args match `InferArgs`. Explicit object nodes declare `additionalProperties: true | false`; the implicit parameter root stays open. You still hand-check constraints the DSL does not express, such as non-empty strings, positive numbers, or cross-field rules. Raw JSON-Schema tools registered directly own their input validation.
45
+ - **Registration borrows your readonly definition.** A typed same-process contribution is not a serialization boundary; do not mutate its schema or replace callbacks after registration. `schemas()` materializes only the explicit model-facing projection. To hot-swap a tool, dispose its owning effect and register the replacement; mutable state inside the callback's closure remains ordinary plugin state.
46
+ - **Execution identity is protected.** The registry materializes `arguments` as detached lossless JSON in one recursive pass, freezes that value before policy starts, and assigns an opaque `exec.token`; `callId`, `name`, `arguments`, `agent`, `token`, the required caller-owned `signal`, and an optional enclosing-transport `parent` token stay immutable through dispatch. `parent` is identity-only and exposes no live outer execution. Treat `args` as readonly input. Only an around-dispatch wrapper receives a mutable view, and it may replace and restore the required `exec.signal` to impose a deadline but cannot remove it.
47
+ - **Declare and return one canonical JSON value.** `output.schema` uses `ValueSchemaSpec` and may have an object, array, scalar, or null root. `execute` returns only the inferred value; the registry snapshots it as lossless JSON, validates it, freezes it, and passes it to `output.render(args, value)`. Do not return content blocks from the body or make callers parse prose for ids and fields.
48
+ - **Throwing or returning an invalid value means `isError`.** The registry catches throws and contains schema, renderer, metadata-projector, and lossless-JSON failures before observers run. Throw for infrastructure failures. Represent a successful domain outcome in the canonical value even when its Native renderer explains a non-ideal state, such as a non-zero process exit.
49
+ - **Honor `exec.signal`.** Cancel in-flight work when it fires.
50
+ - **Project durable card data with `presentationMeta` (optional).** `output.presentationMeta(args, value)` derives replayable JSON from the same canonical value. The core persists it on `tool/result` and hands it to `presentResult`, so a card that needs result-time facts—such as `write`/`edit` applied hunks—survives replay without persisting the canonical value. The projector is skipped for nested Code dispatches because they have no cards.
51
+ - **Use `exec.agent` for async notifications.** `agent.inject({ content, source: { kind: 'plugin', plugin: '<name>' } })` appends durable context the NEXT model request sees — it is not a wake-up (an idle agent stays idle). Guard against disposed agents (try/catch).
52
+
53
+ ## Long-running work
54
+
55
+ Gate `run_in_background` with producer config, then register through `ctx.jobs.start({ kind, label, owner: exec.agent, run })`. The registry rejects a pre-aborted invocation before the producer body; the runtime validates ownership and task-controller availability before `run()` starts work, then supplies the id, session fence, generic control tools, notices, and owner cleanup. A successful background branch returns a typed canonical handle such as `{ kind: 'background', jobId }`; its Native renderer may keep human prose such as `started background job bash-1`, but Code Mode must never parse that prose to recover the id.
56
+
57
+ The producer supplies synchronous `cancel`, non-rejecting `done` that settles after resource cleanup, and optional consuming `readOutput` with bounded-output formatting. A pre-aborted call is a failure because no task exists whose id could satisfy the successful output schema. Once `ctx.jobs.start()` publishes the id, use a task-owned cancellation signal rather than `exec.signal`: later outer-call cancellation stops waiting for the call but does not kill published work; `job_kill`, owner disposal, and service teardown own that lifetime. Foreground work remains coupled to `exec.signal`. See the [background job runtime Agent Note](https://github.com/deepseek-ai/deepseek-harness/blob/master/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md) and `dsh-tool-bash` for a stream producer.
58
+
59
+ ## Execution policy and observation
60
+
61
+ Prefer not to build deployment policy into the tool. Use `tools/pre-execute` for extensible allow/deny/ask policy (the [permission-gate example](./extension-cookbook.md#a-hook-plugin-permission-gate-example)), `ctx.tools.guard()` for a final monotonic deny that later listeners cannot undo, `tools/execute` to wrap dispatch with a deadline, retry, or metrics collection, `tools/post-execute` to replace presentation content or the returned value, block the result, or attach model-facing context, and `tools/result` to observe the immutable normalized outcome. A content replacement leaves programmatic access to `value` intact; confidentiality policy blocks or replaces the value. A sandboxing implementation can also run inside the tool's executor implementation; the [`dsh-tools` README](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/core/tools/README.md#extension-points) defines each extension point's inputs, order, return values, and failure behavior.
62
+
63
+ ## Code Mode reaches your tool for free
64
+
65
+ In [Code Mode](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/core/tools/README.md), every visible registered tool is available as `await tools.<name>(args)` without extra integration. The generated `ToolArgsMap` and `ToolOutputMap` derive exact argument and canonical-return types from the same schemas, and calls re-enter the normal execution pipeline. A successful call resolves to the final canonical JSON value after policy, not to rendered Native content. A failed call rejects with the real `ToolCallError`; programs can inspect only its `name`, `toolName`, and human-readable `message`, not internal error codes or a failure union.
66
+
67
+ Design `output.schema` as a useful programmatic API: return handles and fields directly, allow scalar/array/null roots when they are the honest value, and keep human explanation in `output.render`. Intermediate values are execution-local, are not persisted or prompt-truncated, and have no byte cap, so the producer's truthful acquisition bounds and process memory still matter. Only the outer `run_code` logs/result cross the configurable output cap and model-facing spill pipeline.
68
+
69
+ ## How your tool renders in a UI
70
+
71
+ Your tool's `output.render` returns model-facing content; its **UI card** is a separate concern declared through pure presentation projections and optional `presentCall` / `presentResult` methods. Design these alongside the canonical value. A tool with no UI presentation falls back to a generic card (title = tool name, raw args as input).
72
+
73
+ Both methods return a **`card`-tagged render intent** — pick the card kind that matches what your tool does:
74
+
75
+ - `presentCall(args)` → a `ToolCallView` (the PENDING card):
76
+ - `{ card: 'generic', title, kind?, rawInput?, content?, locations? }` — the default. Set `kind` for an icon (`read`/`search`/…); set `locations: [{ path, line? }]` for any file your tool touches so a capable editor follows along / jumps to it.
77
+ - `{ card: 'terminal', title, description?, cwd? }` — your call IS a shell command. `title` is the command, `description` renders above the terminal card. (tool-bash.)
78
+ - `{ card: 'diff', title, diffs, locations? }` — your call creates or modifies a file. `diffs: [{ path, oldText, newText }]` (`oldText: null` for a new file) renders as an inline diff card. (tool-fs `write`/`edit`.)
79
+ - `presentResult(args, { content, isError, meta? })` returns the completed card:
80
+ - `generic` supplies an optional title and content.
81
+ - `terminal` supplies raw output and optional exit metadata; each UI renders its capable or fallback view.
82
+ - `diff` supplies applied hunks, often derived by `output.presentationMeta` and carried in persisted `result.meta` so replay reproduces them. Mutation tools keep a diff result because the completed view replaces the pending card.
83
+ - `search` supplies a discovery result reconstructed from persisted `result.meta`: grouped-by-file matches (`shape: 'matches'`, grep) or a flat path list (`shape: 'paths'`, glob), plus `truncated`/`total` so a UI never presents a capped result as complete. The view carries no result text (a UI without a search card falls back to the raw result content), and there is no `search` call view — a discovery call's pending state stays a generic card, since matches exist only after `execute`. (tool-fs-search `grep`/`glob`.)
84
+ - `web` supplies a completed web retrieval, discriminated by `kind: 'search' | 'fetch'` (the structured search sources or the fetch summary), derived from `result.meta`; it carries no body copy, so a UI without the `web` capability falls back to the raw result content. (tool-web `web_search`/`web_fetch`.)
85
+
86
+ Hard rules (they bite if broken):
87
+
88
+ - **Purity.** These run on live streaming AND on session-log REPLAY, so they must be pure functions of `args` (+ the result) — NO I/O, NO reading session state, NO clock/random. A diff is derived from the args (`write` uses `oldText: null` because a call-time presenter has no prior file content); the UI adapter, not the tool, supplies session context. If you find yourself wanting the file's old content or the working directory inside `presentCall`, stop — that belongs in durable result metadata or the adapter, not the presenter.
89
+ - **UI-only formatting stays out of the model result.** A fenced ` ```console ` block, a diff, a relativized path—none of these belongs in the canonical value or Native content merely to serve a UI. `output.render` owns model-facing prose; `presentationMeta` plus the card presenters own replayable UI state. A `terminal` result view carries raw output and the adapter adds any fallback framing.
90
+ - **`defineTool` soft-validates the display path.** Malformed or older logged arguments make the wrapper return `undefined` (a generic fallback) rather than throw — display must never crash a replay.
91
+
92
+ The neutral vocabulary lives in `dsh-tools`; tools never import a UI or transport type. Host/client runtimes map each `card` into their own view. The design and the why are in [the render-intent-union 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) and `dsh-tool-bash` (terminal) are the reference implementations.
93
+
94
+ ## Verification
95
+
96
+ Follow the [repository testing policy](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/testing.md) and the owning package's test documentation. A shipped model- or UI-visible change requires the assembled coverage specified there.
@@ -0,0 +1,45 @@
1
+ ---
2
+ editSource: "docs/cookbook/adding-an-llm-adapter.md"
3
+ ---
4
+
5
+ # Cookbook: adding an LLM adapter
6
+
7
+ How to connect a new model provider. Reference implementations: `packages/llm/llm-deepseek` (direct HTTP, SSE framed by `eventsource-parser`) and `packages/llm/llm-pi-ai` (wrapping an LLM library). Read the `StreamChunk` doc in `packages/llm/llm/src/types.ts` first — it records the protocol conventions both adapters were verified against.
8
+
9
+ ## The shape
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
+ Registration is effect-based (HMR-safe); one adapter per provider route — duplicates throw, and multi-route registration is all-or-nothing. `options.provider` selects the adapter and `options.model` is the provider model id, so a dynamic catalog adapter can serve new models without lifecycle reconfiguration. Secrets are cordis-native: schemastery Config with env fallbacks, fed from cordis.yml via `!!js process.env.MY_KEY`. Never read ad-hoc key files in code.
26
+
27
+ ## Protocol obligations (the contract two implementations verified)
28
+
29
+ - Emit `usage` BEFORE `finish`; emit NOTHING after `finish`. The robust way: buffer finish/usage until the provider's end-of-stream marker, then flush (handles providers that send trailing usage-only chunks).
30
+ - Tool-call `arguments` are RAW JSON strings end-to-end; stream fragments as `argumentsDelta`. If your provider hands back parsed objects, re-stringify at `block-end`.
31
+ - Allocate block `index`es in first-seen stream order; reuse the index for every delta of the same block.
32
+ - Errors have exactly two sanctioned paths: THROW from `stream()` (transport and protocol failures — use `LlmError` with a stable code), or end the stream with `finish {kind: 'error' | 'aborted'}` (provider in-band failures). Consumers handle both; pick per failure class and document it.
33
+ - Honor `options.signal` (pass it to fetch / your SDK).
34
+ - A `GenerateOptions` field your provider cannot honor (e.g. a `stop` list on a provider without stop sequences): throw `LlmError(..., 'UNSUPPORTED')` rather than silently dropping it.
35
+ - If the provider requires response ids, signatures, or other native metadata on follow-up calls, emit the minimal lossless-JSON projection as `finish.replayState`. Validate it when rebuilding history. `LlmRuntime` passes it only when the historical provider route and target provider route are currently owned by the exact same adapter instance; your adapter decides whether same-model, cross-model, or cross-provider restoration is legal. Never infer native replay from provider/model names alone when state is absent.
36
+
37
+ Provider-specific thinking-mode toggles remain in the adapter's Config. Exact model metadata uses one provider-neutral capability seam: implement `resolveModel()` with provider/model identity and optional `context` and `reasoning` fields, declare a configured `defaultEffort` only when one exists, and honor the resolver's optional `AbortSignal`. Reasoning efforts are ordered opaque ids mapped to provider requests by the adapter. Preserve the adapter's authoritative selectable list, including an adapter-defined `off` when supported, without exposing final wire spellings or clamping unsupported values; an id need not equal its wire representation.
38
+
39
+ ## Implementation structure
40
+
41
+ Keep wire types, request serialization, transport parsing, chunk translation, and the adapter class as separate responsibilities; [`llm-deepseek`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/llm/llm-deepseek/README.md) is the reference layout.
42
+
43
+ ## Verification
44
+
45
+ Follow the [repository testing policy](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/testing.md), which owns adapter coverage, real-provider checks, and published-entry requirements.