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,920 @@
1
+ ---
2
+ editSource: "docs/subsystems/llm-streaming.md"
3
+ outline: [2,3]
4
+ ---
5
+
6
+ # LLM Streaming
7
+
8
+ The conversation and streaming types from [`packages/llm`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/llm/README.md): the `Message`/`ContentBlock` variants every request and durable history share, the fully assembled model request, the raw `StreamChunk` protocol, the adapter contract every adapter must implement, and the shared assembler. The [core packages](./core.md) hold and log these values on every turn; this page declares them.
9
+
10
+ Source: [`packages/llm/llm/src/types.ts`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/llm/llm/src/types.ts)
11
+
12
+ <a id="content-blocks-and-messages"></a>
13
+
14
+ ## Content blocks and messages
15
+
16
+ A conversation is `Message`s; a message is an array of typed **content blocks**. The block union derives from `ContentBlockMap`.
17
+
18
+ Source: [`packages/llm/llm/src/types.ts`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/llm/llm/src/types.ts)
19
+
20
+ ```ts type-equiv
21
+ /**
22
+ * Merge-extensible content blocks keyed by `type`. New core blocks must land
23
+ * with adapter, UI, and compaction support.
24
+ */
25
+ interface ContentBlockMap {
26
+ 'text': TextBlock
27
+ 'reasoning': ReasoningBlock
28
+ 'image': ImageBlock
29
+ 'tool-call': ToolCallBlock
30
+ 'tool-result': ToolResultBlock
31
+ }
32
+ ```
33
+
34
+ The block interfaces (full fields in source): `TextBlock` (`text`), `ReasoningBlock` (thinking, distinct from visible text), `ImageBlock` (a durable [image attachment](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/subsystems/attachment.md)), `ToolCallBlock` (`id: CallId`, `name`, raw-JSON `arguments`), and `ToolResultBlock` (`toolCallId`, nested `content: ContentBlock[]`, `isError?`). `ContentBlock = ContentBlockMap[ContentBlockType]`. A new modality belongs in the merge-extensible map only when its adapter, UI, compaction, and durable replay paths honor it.
35
+
36
+ Source: [`packages/llm/llm/src/message.ts`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/llm/llm/src/message.ts)
37
+
38
+ A `Message` is one identified, immutable role/source/content value. Model-produced assistant messages name the provider and model that produced them and carry optional adapter-private replay data in their source:
39
+
40
+ ```ts type-equiv
41
+ /** Provider/model identity and adapter-private replay data for an assistant message. */
42
+ interface AssistantProvenance {
43
+ /** Provider route that produced the message. */
44
+ provider: string
45
+ /** Provider model id that produced the message. */
46
+ model: string
47
+ /**
48
+ * Lossless-JSON adapter state needed to replay the provider response.
49
+ * `LlmRuntime` exposes it to a target adapter only when that adapter instance
50
+ * currently owns both this historical provider and the target provider.
51
+ */
52
+ replayState?: unknown
53
+ }
54
+ ```
55
+
56
+ ```ts type-equiv
57
+ /** One immutable message representation shared by delivery, durable history, and model requests. */
58
+ interface Message {
59
+ /** Stable identity preserved across every representation boundary. */
60
+ readonly id: MessageId
61
+ /** Provider-neutral conversation role. */
62
+ readonly role: 'system' | 'user' | 'assistant'
63
+ /** Exact model-facing blocks. */
64
+ readonly content: ContentBlock[]
65
+ /** Required source fields supplied by the producer. */
66
+ readonly source: MessageSource
67
+ }
68
+ ```
69
+
70
+ Where a message came from is itself a merge-extensible sum type:
71
+
72
+ ```ts type-equiv
73
+ /**
74
+ * Where a message (or injected content) came from.
75
+ * Merge-extensible sum type — plugins add their own `kind`s.
76
+ */
77
+ interface MessageSourceMap {
78
+ user: { kind: 'user' }
79
+ plugin: { kind: 'plugin'; plugin: string } & ContextFormed
80
+ model: ModelMessageSource
81
+ tool: ToolMessageSource
82
+ }
83
+ ```
84
+
85
+ Producer identity and presentation form are independent. `kind` answers *who produced this*; the optional `form` answers *what kind of information this is*, and consumers decide how to present it. Several producers may share one form, and one producer may emit more than one form over a session. The values are semantic and grow one at a time; an absent or unrecognized value uses the documented default and is presented as opaque content:
86
+
87
+ ```ts type-equiv
88
+ /**
89
+ * The kind of information in producer-supplied context, declared by the
90
+ * producer beside its provenance.
91
+ *
92
+ * `MessageSource.kind` answers *who produced this*; `form` answers *what kind
93
+ * of thing it is*, and the two axes are deliberately independent — several
94
+ * producers share one form, and one producer may emit more than one form over
95
+ * a session.
96
+ *
97
+ * The vocabulary is SEMANTIC, never visual: a value states that the content is
98
+ * a file's instructions or a catalog of available items, and a consumer decides
99
+ * what that looks like. Colors, icons, ordering, and collapse defaults are the
100
+ * consumer's business and must not enter this union. It grows one value at a
101
+ * time as producers gain the structured fields their form needs; an absent or
102
+ * unknown value is the documented default, presented as opaque content.
103
+ */
104
+ type ContextForm =
105
+ /** Instructions read out of workspace files the model is expected to follow. */
106
+ | 'instructions'
107
+ /** A catalog of items available in this session, republished as it changes. */
108
+ | 'catalog'
109
+ /** Current state, where a later snapshot from the same producer supersedes an earlier one. */
110
+ | 'snapshot'
111
+ /** A one-off account of something that just happened; it supersedes nothing. */
112
+ | 'notice'
113
+ /** A message another agent addressed to this one. */
114
+ | 'relay'
115
+ /** Material lifted out of another session's log, possibly reduced on the way in. */
116
+ | 'recall'
117
+ ```
118
+
119
+ ```ts type-equiv
120
+ /** One named contribution to a `snapshot`-form context, in assembly order. */
121
+ interface ContextSnapshotSection {
122
+ /** The contributing subsystem's name. */
123
+ readonly name: string
124
+ /** That contribution's model-facing text, exactly as assembled. */
125
+ readonly text: string
126
+ }
127
+ ```
128
+
129
+ ```ts type-equiv
130
+ /**
131
+ * Producer-declared {@link ContextForm} and the fields that form requires,
132
+ * mixed into the source types that carry one.
133
+ *
134
+ * Discriminated by `form` so a producer cannot select a form without the
135
+ * fields needed to present it: a `notice` must record its one-line
136
+ * account, a `snapshot` its sections. Omitting `form` stays valid — an
137
+ * undeclared context is the documented default.
138
+ */
139
+ type ContextFormed =
140
+ | { readonly form?: never }
141
+ | { readonly form: 'instructions' }
142
+ | { readonly form: 'catalog' }
143
+ | {
144
+ readonly form: 'snapshot'
145
+ /** The named contributions this snapshot assembled, in order. */
146
+ readonly sections: readonly ContextSnapshotSection[]
147
+ }
148
+ | {
149
+ readonly form: 'notice'
150
+ /** One-line account of what happened, shown without expanding the row. */
151
+ readonly summary: string
152
+ }
153
+ | { readonly form: 'relay' }
154
+ | { readonly form: 'recall' }
155
+ ```
156
+
157
+ <a id="streamchunk--the-raw-protocol"></a>
158
+
159
+ ## `StreamChunk` — the raw protocol
160
+
161
+ A streaming response interleaves several typed blocks (text, reasoning, multiple tool calls). `index` ties each delta to its block; `block-end` carries the fully-assembled `ContentBlock` so consumers don't have to re-assemble deltas themselves. It is a **closed** discriminated union — a `switch` over `type` ends with `assertNever`, so adding a variant breaks compilation at every consumer that must handle it.
162
+
163
+ ```ts type-equiv
164
+ /**
165
+ * Adapter-private lossless-JSON state for replaying a successful response,
166
+ * carried by a terminal `finish` chunk and stored on the assembled assistant
167
+ * message's model source. Both halves stay opaque to the harness; only the
168
+ * split is shared vocabulary, so assembly can keep stored metadata aligned
169
+ * with stored content without reading either half.
170
+ */
171
+ interface ReplayEnvelope {
172
+ /** Response-level adapter-private metadata (ids, native stop reason). */
173
+ response: unknown
174
+ /**
175
+ * Per-block adapter-private metadata, one entry per emitted block in
176
+ * first-seen stream order. When assembly drops a block it drops the entry at
177
+ * the same position; entries whose length does not match the emitted block
178
+ * count discard the whole envelope. An adapter whose metadata is independent
179
+ * of block structure omits this field and the envelope passes through
180
+ * assembly unchanged.
181
+ */
182
+ blocks?: readonly unknown[]
183
+ }
184
+ ```
185
+
186
+ ```ts type-equiv
187
+ /**
188
+ * Raw streaming protocol emitted by adapters.
189
+ * Block indexes correlate interleaved deltas, and `block-end` carries the
190
+ * assembled block. Adapters emit usage before the terminal finish and nothing
191
+ * afterward; tool arguments remain raw JSON strings. An adapter implementation
192
+ * may throw, but `LlmRuntime.stream()` normalizes that failure to a terminal
193
+ * `error` or `aborted` finish before exposing it to consumers.
194
+ */
195
+ type StreamChunk =
196
+ | { type: 'block-start'; index: number; blockType: ContentBlockType }
197
+ | { type: 'text-delta'; index: number; text: string }
198
+ | { type: 'reasoning-delta'; index: number; text: string }
199
+ | { type: 'tool-call-delta'; index: number; id: CallId; name?: string; argumentsDelta: string }
200
+ | { type: 'block-end'; index: number; block: ContentBlock }
201
+ | { type: 'usage'; usage: TokenUsage }
202
+ | {
203
+ type: 'finish'
204
+ reason: FinishReason
205
+ /** Replay metadata for a successful response; see {@link ReplayEnvelope}. */
206
+ replayState?: ReplayEnvelope
207
+ }
208
+ ```
209
+
210
+ ## `LlmFailure`
211
+
212
+ Every thrown or in-band final-adapter failure normalizes to one serializable provider-neutral payload. `providerRetryAfterMs` is a validated positive delay requested by the provider, not a retry decision; `ProviderRequestId` is an opaque branded string for diagnostics.
213
+
214
+ ```ts type-equiv
215
+ /** Serializable provider or transport failure facts; policy decides whether they are retryable. */
216
+ interface LlmFailure {
217
+ /** Human-readable provider or transport failure. */
218
+ readonly message: string
219
+ /** Stable provider-neutral machine-routing code. */
220
+ readonly code: string
221
+ /** HTTP status returned by the provider, when available. */
222
+ readonly status?: number
223
+ /** Provider-requested delay in milliseconds, when valid and available. */
224
+ readonly providerRetryAfterMs?: number
225
+ /** Opaque provider-issued request identifier for diagnostics. */
226
+ readonly requestId?: ProviderRequestId
227
+ }
228
+ ```
229
+
230
+ ## The adapter contract
231
+
232
+ Every adapter MUST obey these, and every consumer may rely on them:
233
+
234
+ - **`usage` before `finish`, nothing after `finish`.** Defer both to the provider's end-of-stream marker so a trailing usage-only chunk can't violate the ordering.
235
+ - **Tool-call `arguments` stay raw JSON strings end-to-end.** Partial fragments stream via `argumentsDelta`; a provider that hands back parsed objects re-stringifies at `block-end`.
236
+ - **Two sanctioned error paths, one `LlmFailure` type.** A failure may either THROW from `stream()` (transport/protocol errors) **or** end the stream with `finish {kind:'error'|'aborted', failure}` (provider in-band errors, for adapters that can't throw mid-stream). `LlmError.failure` carries the same `LlmFailure`. After the call selects its adapter, the stream preserves the exact thrown `Error` object and associates immutable facts plus the serving registration's immutable retry policy with that call; the agent loop closes the failed step and offers the error, facts, immutable prior-retried facts, serving policy, and turn signal to `agent/request-error`. A handling listener returns `{ kind: 'retry' }` after its awaited repair; absent recovery the structured failure becomes the turn error, and no normal assistant message or tool side effect is committed for that attempt.
237
+ - **One adapter call is one provider attempt.** Adapters disable library retries. Agent-level recovery opens another durable numbered turn; direct `ctx.llm.stream()` callers remain single-attempt.
238
+ - **Provider stalls are bounded at the transport.** Both shipping remote adapters expose positive finite `streamIdleTimeoutMs` with a five-minute default. The watchdog arms only while iterator `next()` is outstanding, uses one stable signal for the whole request, maps its own expiry to `TIMEOUT`, and keeps an earlier caller abort as `ABORTED`.
239
+ - **Context overflow has one canonical code.** Both DeepSeek adapters classify explicit provider detail through `isContextWindowExceededError()` and surface `CONTEXT_WINDOW_EXCEEDED`, whether the failure arrives as a thrown HTTP `LlmError` or an in-band finish error. Consumers route on the code, never provider text.
240
+ - **An empty completion is a retryable error, not a silent success.** Both adapters map a terminal `stop` finish that carried no content blocks to `finish {kind:'error'}` with the canonical `EMPTY_RESPONSE` code, and `dsh-llm-retry` retries it by default; see [empty model responses are retryable](https://github.com/deepseek-ai/deepseek-harness/blob/master/.agents/notes/implemented/bug-fix/2026-07-24-empty-model-response-is-retryable.md).
241
+ - **Every provider HTTP request carries the app-attribution header.** Adapters send `attributionHeaders()` (below) - the `User-Agent` baseline - and prove it with a wire-level test.
242
+ - **Replay state is adapter-owned; its split is shared.** A successful `finish` may carry a `ReplayEnvelope`: opaque response-level metadata plus optional per-block entries aligned with the emitted block sequence. The alignment is the harness's vocabulary — when assembly drops a block it drops the entry at the same position, so stored metadata always describes stored content. The loop stores the pruned envelope with the assembled assistant message. On a later request, `LlmRuntime` passes the state only when the historical provider and target provider are currently registered to the exact same adapter instance. That adapter validates the state and owns any cross-model or cross-provider conversion; other adapters receive the provider-neutral content plus provider/model fields without the private state. Durable content stays authoritative: a stored state the reading adapter cannot use degrades that one message to provider-neutral conversion with a diagnostic instead of failing the request.
243
+
244
+ ## `ResolvedRetryPolicy`
245
+
246
+ Provider configuration resolves before route registration into an immutable discriminated union. Normal mode carries `mode: 'normal'`, finite `maxRetries`, `retryableCodes`, and required `initialDelayMs`, `maxDelayMs`, and `jitterRatio`; always mode carries `mode: 'always'` and the same required backoff fields without a finite maximum. `LlmRuntime.providerRetryPolicy(provider)` returns the currently registered value and supplies normal defaults when the adapter omits one; `llmRetryPolicyOf(stream)` returns the value captured from the serving registration after the call selects that registration, so later route disposal or replacement cannot change an in-flight failure's recovery policy. The [generated config catalog](../config-catalog.md) lists the optional input fields.
247
+
248
+ ## `AppIdentity` — app attribution
249
+
250
+ The static public application identity every adapter sends to providers ([`packages/llm/llm/src/attribution.ts`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/llm/llm/src/attribution.ts)). `attributionHeaders(identity?)` maps it to the standard `User-Agent` header only; OpenRouter-specific app attribution headers are intentionally not supported by this contract. The default `APP_IDENTITY` sources its version from the package manifest; every field is a public product fact - no secrets, paths, session ids, or per-user identifiers, and nothing per-request may influence the values. Rationale: [Mandatory `User-Agent` attribution](https://github.com/deepseek-ai/deepseek-harness/blob/master/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md).
251
+
252
+ ```ts type-equiv
253
+ /**
254
+ * Static public application identity sent to LLM providers.
255
+ *
256
+ * Every field is a public product fact, safe on every request: no secrets,
257
+ * local paths, session ids, prompt text, or per-user identifiers belong here,
258
+ * and nothing per-request may influence the values.
259
+ */
260
+ interface AppIdentity {
261
+ /** `User-Agent` product token (lowercase, hyphenated). */
262
+ product: string
263
+ /** Product version; sourced from package metadata, never hand-copied. */
264
+ version: string
265
+ /** Repository home URL of the app, used as the `User-Agent` comment. */
266
+ url: string
267
+ }
268
+ ```
269
+
270
+ ## `TokenUsage`
271
+
272
+ Per-call token accounting. Counts are **disjoint**: `inputTokens` is uncached input only; cached input is reported separately, and billed input is the sum of the three. Adapters whose providers fold cache hits into a single prompt total (DeepSeek's `prompt_tokens`) subtract them back out. `reasoningTokens`, when present, is informational detail already included in `outputTokens`; totals must not add it again.
273
+
274
+ ```ts type-equiv
275
+ /**
276
+ * Token accounting for one model call (cache fields are optional).
277
+ *
278
+ * Counts are DISJOINT: `inputTokens` is uncached input only; cached input is
279
+ * reported separately as `cacheReadTokens`/`cacheWriteTokens` (billed input =
280
+ * sum of the three). Adapters whose providers fold cache hits into a total
281
+ * prompt count (DeepSeek's `prompt_tokens`) subtract them out.
282
+ */
283
+ interface TokenUsage {
284
+ inputTokens: number
285
+ outputTokens: number
286
+ cacheReadTokens?: number
287
+ cacheWriteTokens?: number
288
+ reasoningTokens?: number
289
+ }
290
+ ```
291
+
292
+ ## `BlockAssembler`
293
+
294
+ `BlockAssembler` ([`packages/llm/llm/src/assembler.ts`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/llm/llm/src/assembler.ts)) is the single shared implementation that folds a `StreamChunk` stream back into `ContentBlock`s, usage, finish reason, and replay state. The loop logs the raw chunks while feeding the same chunks through an assembler, then stores the assembled assistant content with the provider and model that produced it. A consumer that needs the assembled result without re-implementing the fold uses this.
295
+
296
+ One keep/drop decision covers content and metadata together: a `max-tokens` finish drops every tool call because a truncated call is unsafe to execute, and the same decision prunes the replay envelope's per-block entry at each dropped position. `blocks()` and `replayState` therefore cannot disagree, whatever assembly removes.
297
+
298
+ ```ts public-api
299
+ /**
300
+ * Incrementally assembles raw {@link StreamChunk}s into complete
301
+ * {@link ContentBlock}s and a final assistant {@link Message}.
302
+ *
303
+ * The agent loop feeds it while logging raw chunks for replay fidelity, then
304
+ * reads `blocks()` / `message()` / `usage` / `finish` once the stream ends.
305
+ *
306
+ * Tolerant of delta-only protocols (no block-start/end); deltas arriving for
307
+ * an index already closed by `block-end` are ignored (malformed stream) so a
308
+ * misbehaving adapter cannot grow memory or corrupt a completed block.
309
+ */
310
+ declare class BlockAssembler {
311
+ /**
312
+ * Feed one chunk into the assembly state.
313
+ * @param chunk - the next raw chunk, in stream order.
314
+ */
315
+ push(chunk: StreamChunk): void;
316
+ /**
317
+ * Assemble all blocks seen so far, in stream order.
318
+ * @returns one block per seen index, except that max-token truncation drops
319
+ * tool calls that cannot be executed safely; an open block assembles from
320
+ * its accumulated deltas (an unknown block type never closed by `block-end` throws).
321
+ */
322
+ blocks(): ContentBlock[];
323
+ /** Usage from the `usage` chunk; undefined until one arrives. */
324
+ get usage(): TokenUsage | undefined;
325
+ /** Finish reason from the `finish` chunk; `{kind: 'stop'}` when the stream ended without one. */
326
+ get finish(): FinishReason;
327
+ /**
328
+ * Replay metadata from the terminal finish chunk, if any, with per-block
329
+ * entries pruned in step with {@link blocks}. Undefined when the envelope's
330
+ * entries do not align with the emitted blocks.
331
+ */
332
+ get replayState(): ReplayEnvelope | undefined;
333
+ /**
334
+ * The assembled assistant message.
335
+ * @param source - producer attribution for the assembled message.
336
+ * @returns a frozen assistant-role message over `blocks()` (same open-block assembly rules).
337
+ */
338
+ message(source: MessageSource = { kind: 'plugin', plugin: 'dsh-llm/assembler' }): Message;
339
+ }
340
+ ```
341
+
342
+ <a id="the-model-request-and-result"></a>
343
+
344
+ ## The model request
345
+
346
+ One model call is a fully-assembled `GenerateOptions`. The adapter answers with a raw [`StreamChunk`](#streamchunk--the-raw-protocol) stream; the consumer assembles it with [`BlockAssembler`](#blockassembler).
347
+
348
+ Source: [`packages/llm/llm/src/types.ts`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/llm/llm/src/types.ts)
349
+
350
+ Provider and model discovery uses small provider-neutral descriptors. A model catalog is advisory: routing still keys on a registered provider, and an adapter may accept unlisted model ids.
351
+
352
+ Registering an adapter returns a handle: the disposer, plus the atomic route replacement a plugin whose route set is user-configurable needs.
353
+
354
+ ```ts type-equiv
355
+ /**
356
+ * What {@link LlmRuntime.registerAdapter} returns: the disposer, plus an
357
+ * atomic route replacement for the same adapter instance.
358
+ */
359
+ interface AdapterRegistrationHandle {
360
+ /** Release every route this registration currently holds. */
361
+ (): void
362
+ /**
363
+ * Replace this registration's routes with `providers`, keeping the same
364
+ * adapter instance. The candidate set is validated in full first — a
365
+ * conflict with another adapter, an invalid name, or bad provider metadata
366
+ * throws and leaves the current routes untouched — and the swap itself is
367
+ * one synchronous section, so no request can observe a gap. An empty array
368
+ * is legal here (a settings section that emptied holds zero routes while
369
+ * staying registered), unlike an empty initial registration.
370
+ *
371
+ * Throws `LlmError` with code `REGISTRATION_DISPOSED` once the registration
372
+ * has been released: its routes are gone and its disposer has already run,
373
+ * so anything registered afterwards would have no owner left to release it.
374
+ * @param providers - the complete next route set for this registration.
375
+ */
376
+ replace(providers: string[]): void
377
+ }
378
+ ```
379
+
380
+ ```ts type-equiv
381
+ /** Display metadata for one registered provider route. */
382
+ interface LlmProviderInfo {
383
+ /** Provider route key used by {@link GenerateOptions.provider}. */
384
+ id: string
385
+ /** Human-readable provider name for selectors and diagnostics. */
386
+ name: string
387
+ }
388
+ ```
389
+
390
+ Adapter plugins additionally declare which routes *could* run through `registerConfigurableProviders()`, addressing each one's user-settings section, so configuration surfaces can offer dormant providers before any route registers.
391
+
392
+ ```ts type-equiv
393
+ /**
394
+ * One provider route an adapter plugin can activate through configuration,
395
+ * whether or not the route is currently registered. Configuration surfaces
396
+ * merge this directory with `listProviders()` to offer every configurable
397
+ * provider alongside its live/dormant state.
398
+ */
399
+ interface LlmConfigurableProvider {
400
+ /** Provider route key this entry activates when configured. */
401
+ provider: string
402
+ /** Human-readable provider name for configuration surfaces. */
403
+ displayName: string
404
+ /** User-settings namespace whose section configures this provider. */
405
+ settingsNs: string
406
+ /**
407
+ * Path from that namespace's section root to this provider's profile
408
+ * object; empty when the whole section is the profile.
409
+ */
410
+ settingsPath: readonly string[]
411
+ /**
412
+ * Whether the owning adapter knows this route only because configuration
413
+ * declared it — a gateway or self-hosted server it ships nothing about.
414
+ * Absent means the adapter draws no such distinction; false means it does
415
+ * and this route is one of its own. Only the adapter can answer: a stored
416
+ * profile is how a user-added route AND a corrected shipped one both look
417
+ * from outside.
418
+ */
419
+ declared?: boolean
420
+ }
421
+ ```
422
+
423
+ ```ts type-equiv
424
+ /** One adapter-discovered model; catalog membership is advisory, not request validation. */
425
+ interface LlmModelInfo {
426
+ /** Provider route that owns this model entry. */
427
+ provider: string
428
+ /** Model id passed to {@link GenerateOptions.model}. */
429
+ id: string
430
+ /** Human-readable model name for selectors. */
431
+ name: string
432
+ /** Optional user-facing distinction from otherwise similar models. */
433
+ description?: string
434
+ /** Accepted request modalities; absent means unknown, while an explicit omission is negative capability. */
435
+ inputModalities?: readonly ModelModality[]
436
+ }
437
+ ```
438
+
439
+ Correctness-sensitive metadata is resolved separately from the advisory catalog and is owned by the adapter serving the exact route. Context capacity, adapter call defaults, and reasoning choices share one exact-model result so consumers do not repeat authoritative model resolution.
440
+
441
+ ```ts type-equiv
442
+ /** Provider-owned context capacity for one exact provider/model route. */
443
+ interface LlmModelContext {
444
+ /** Maximum combined request and response context in tokens. */
445
+ contextWindow: number
446
+ }
447
+ ```
448
+
449
+ Reasoning effort is another exact-route capability. The core brands identifiers but does not enumerate their values; each adapter owns the ordered set, display names, and optional deployment default.
450
+
451
+ ```ts type-equiv
452
+ /** Adapter-owned identifier for one model's selectable reasoning effort. */
453
+ type ReasoningEffortId = Branded<'ReasoningEffortId'>
454
+ ```
455
+
456
+ ```ts type-equiv
457
+ /** Display metadata for one adapter-owned reasoning effort. */
458
+ interface LlmReasoningEffortInfo {
459
+ /** Opaque stable value accepted by {@link GenerateOptions.reasoningEffort}. */
460
+ id: ReasoningEffortId
461
+ /** Human-readable effort name for selectors and diagnostics. */
462
+ name: string
463
+ /** Optional user-facing distinction from otherwise similar efforts. */
464
+ description?: string
465
+ }
466
+ ```
467
+
468
+ ```ts type-equiv
469
+ /** Selectable reasoning efforts for one exact provider/model route. */
470
+ interface LlmModelReasoningInfo {
471
+ /** Supported efforts in adapter-preferred display order. */
472
+ efforts: readonly LlmReasoningEffortInfo[]
473
+ /**
474
+ * Adapter-configured default materialized into requests when callers omit
475
+ * an effort. Absence preserves the provider's own default.
476
+ */
477
+ defaultEffort?: ReasoningEffortId
478
+ }
479
+ ```
480
+
481
+ ```ts type-equiv
482
+ /** Exact-route model metadata resolved by its owning adapter. */
483
+ interface LlmResolvedModelInfo extends LlmModelInfo {
484
+ /** Provider-owned context capacity when known. */
485
+ context?: LlmModelContext
486
+ /** Adapter-configured per-request output cap materialized when callers omit one. */
487
+ defaultMaxTokens?: number
488
+ /** Adapter-owned selectable reasoning levels when exposed. */
489
+ reasoning?: LlmModelReasoningInfo
490
+ }
491
+ ```
492
+
493
+ ```ts type-equiv
494
+ /** A single model request, fully assembled. */
495
+ interface GenerateOptions {
496
+ /** Registered provider route selecting the adapter instance. */
497
+ provider: string
498
+ model: string
499
+ /** Adapter-owned reasoning effort selected for this exact model. */
500
+ reasoningEffort?: ReasoningEffortId
501
+ /**
502
+ * Ordered conversation messages, exactly as the provider sees them (after
503
+ * the `system` slot). A loop-built request assembles them as
504
+ * the derived history (dsh-agent-loop); a hand-built one-shot passes any list.
505
+ */
506
+ messages: Message[]
507
+ /** System prompt text (adapters map to the provider's system slot). */
508
+ system?: string
509
+ /** Tool schemas (adapters map to the provider's `tools` field). */
510
+ tools?: ToolSchema[]
511
+ temperature?: number
512
+ maxTokens?: number
513
+ /**
514
+ * Stop sequences: generation halts as soon as the model produces any one of
515
+ * these strings (adapters map to the provider's stop field, e.g. OpenAI
516
+ * `stop`). The stop string itself is not included in the output.
517
+ */
518
+ stop?: string[]
519
+ signal?: AbortSignal
520
+ /**
521
+ * Session identity stamped by the loop for request routing. Replay uses it
522
+ * to separate cursors; adapters may map it to model-hidden transport metadata.
523
+ */
524
+ sessionId?: Branded<'SessionId'>
525
+ /**
526
+ * Provider-neutral classification for an auxiliary model call. Adapters may
527
+ * map the purpose to model-hidden transport metadata or purpose-specific
528
+ * generation policy. Ordinary conversation requests leave it unset.
529
+ */
530
+ purpose?: 'compaction' | 'session-title'
531
+ }
532
+ ```
533
+
534
+ Why a model response stopped is a merge-extensible reason. Terminal provider failures carry the streaming contract's [`LlmFailure`](#llmfailure):
535
+
536
+ ```ts type-equiv
537
+ /**
538
+ * Why a model response stopped.
539
+ * Merge-extensible so adapters can surface provider-specific reasons.
540
+ */
541
+ interface FinishReasonMap {
542
+ 'stop': { kind: 'stop' }
543
+ 'tool-calls': { kind: 'tool-calls' }
544
+ 'max-tokens': { kind: 'max-tokens' }
545
+ 'aborted': { kind: 'aborted'; failure: LlmFailure }
546
+ 'error': { kind: 'error'; failure: LlmFailure }
547
+ }
548
+ ```
549
+
550
+ `FinishReason = FinishReasonMap[keyof FinishReasonMap]`. `TokenUsage` (per-call accounting with disjoint cache fields) is detailed [below](#tokenusage).
551
+
552
+ `GenerateOptions.tools` carries `ToolSchema` — the JSON-schema description of a tool, as sent to the model. It is declared in dsh-llm (not dsh-tools) precisely because it is part of the request the loop assembles every step:
553
+
554
+ ```ts type-equiv
555
+ /**
556
+ * JSON-schema description of a tool, as sent to the model.
557
+ *
558
+ * Declared here (not in dsh-tools) because it is part of {@link GenerateOptions};
559
+ * dsh-tools' ToolDefinition and dsh-system-prompt's PromptAssembly both import
560
+ * it from this package.
561
+ */
562
+ interface ToolSchema {
563
+ name: string
564
+ description: string
565
+ /** JSON Schema object for the arguments. */
566
+ parameters: Record<string, unknown>
567
+ }
568
+ ```
569
+
570
+ The model-facing `ToolSchema` is the wire type; the registered `ToolDefinition` that produces it (schema + `execute`) is on [tools.md](./tools.md).
571
+
572
+ A provider a surface is still drafting has no route and no catalog, so interrogation is described separately: the request carries the draft the user is editing, and the reply is candidates a surface may adopt rather than a catalog it must serve.
573
+
574
+ ```ts type-equiv
575
+ /**
576
+ * One interrogation of a provider endpoint that configuration has not stored
577
+ * yet. Configuration surfaces send the draft a user is still editing, so the
578
+ * request carries the endpoint and credential directly instead of naming a
579
+ * route: a provider being added has no route to name.
580
+ */
581
+ interface LlmModelDiscoveryRequest {
582
+ /**
583
+ * Route the draft is editing, when it edits an existing one. A route whose
584
+ * adapter already knows its models answers from that knowledge instead of
585
+ * asking the endpoint — the adapter's own registry is the better answer, and
586
+ * it costs no network call.
587
+ */
588
+ provider?: string
589
+ /**
590
+ * Endpoint to interrogate. Optional because a route the adapter already
591
+ * describes needs none; a route it does not must supply one.
592
+ */
593
+ baseURL?: string
594
+ /** Wire protocol the endpoint speaks, when the draft names one. */
595
+ api?: string
596
+ /** Credential for this interrogation alone; the harness never stores it. */
597
+ apiKey?: string
598
+ /** Caller cancellation; implementations must settle promptly after it aborts. */
599
+ signal?: AbortSignal
600
+ }
601
+ ```
602
+
603
+ ```ts type-equiv
604
+ /**
605
+ * One model an endpoint reports about itself. Every field but the id is
606
+ * optional because most provider listings disclose an id and nothing else;
607
+ * a surface adopting one of these still owes the capacities its adapter needs.
608
+ */
609
+ interface LlmDiscoveredModel {
610
+ /** Model id the endpoint accepts. */
611
+ id: string
612
+ /** Human-readable name when the endpoint supplies one. */
613
+ name?: string
614
+ /** Maximum combined request and response context, when disclosed. */
615
+ contextWindow?: number
616
+ /** Maximum output tokens, when disclosed. */
617
+ maxTokens?: number
618
+ }
619
+ ```
620
+
621
+ ### The request envelope: `LlmCallConfig` and the logged header
622
+
623
+ The loop builds each request from logged state. `EpochHeader` records call config, marks the fields supplied by adapter defaults, and records the rendered prompt and authoritative returned tool order (configured by `toolOrder`, or lexicographic when unset) through full `request/header` snapshots. Together with derived history, this makes the request reconstructable from the session log. See [session.md](./session.md#the-request-header-event-requestheader) and the [reconstructability Agent Note](https://github.com/deepseek-ai/deepseek-harness/blob/master/.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md).
624
+
625
+ `agent/request` receives a frozen call-config seed and may return a replacement to switch provider, model, reasoning effort, or sampling. Before the waterfall, the loop removes values marked as adapter defaults so exact-model preparation materializes the selected route's current values; unmarked explicit settings remain in the proposal. After the waterfall, preparation rejects unsupported explicit effort ids without clamping and logs the effective config plus the fields supplied by adapter defaults under the turn signal. The prepared call keeps one adapter registration through dispatch. Requests reaching `llm/stream` are deep-frozen, so mutation throws, and carry a process-local loop identity so observers do not confuse separately logged frozen auxiliary calls with conversation requests.
626
+
627
+ On the wire, a loop-built request reads the `system` slot (the rendered prompt assembly) followed by the derived history. The logged request snapshot ends with the newest `user/message` on a turn's first step and the previous step's tool results on later steps. The dev invariant recomputes exactly this equation against every loop-built request.
628
+
629
+ FIXME(call-config-shape): revisit which remaining fields are genuinely epoch-level for cache purposes (`model` and the model-owned reasoning effort are explicit; the sampling scalars sit here out of caution).
630
+
631
+ ```ts type-equiv
632
+ /**
633
+ * Provider, model, reasoning effort, and sampling scalars of one conversation's
634
+ * requests. Every field maps 1:1 onto the same-named `GenerateOptions` field;
635
+ * the loop builds requests from the logged header rather than accepting these
636
+ * per call.
637
+ */
638
+ interface LlmCallConfig {
639
+ provider: string
640
+ model: string
641
+ reasoningEffort?: ReasoningEffortId
642
+ temperature?: number
643
+ maxTokens?: number
644
+ stop?: string[]
645
+ }
646
+ ```
647
+
648
+ ```ts type-equiv
649
+ /**
650
+ * Effective config fields supplied by exact-model adapter resolution rather
651
+ * than by the caller's request proposal.
652
+ */
653
+ interface LlmCallConfigAdapterDefaults {
654
+ reasoningEffort?: true
655
+ maxTokens?: true
656
+ }
657
+ ```
658
+
659
+ ## Service and provider contracts
660
+
661
+ `LlmAdapter` is the provider contract: subclass, implement `stream()`, and register one adapter instance with `ctx.llm.registerAdapter(providers, adapter)`. `GenerateOptions.provider` selects the registered adapter; `GenerateOptions.model` is passed to that adapter and need not be registered at lifecycle start. Duplicate provider routes fail atomically. Optional `providerRetryPolicy()` is captured per route with normal defaults, while `providerInfo()` and asynchronous `listModels()` feed `LlmRuntime.listProviders()` / `listModels()` with detached selector metadata. That catalog is advisory rather than a request whitelist: the adapter remains authoritative and may accept unlisted model ids. One asynchronous `resolveModel()` query returns exact model identity plus optional correctness-sensitive context capacity, an adapter-configured `defaultMaxTokens`, and ordered model-owned reasoning ids with an optional deployment default; absent fields mean unavailable metadata or provider-owned behavior, not invalid catalog membership. The resolver receives optional cancellation and must settle promptly after abort. `LlmRuntime.resolveModelInfo()` validates and detaches the aggregate. At the final adapter boundary, `resolveCallConfig()` materializes the output default only when `maxTokens` is absent and validates and materializes reasoning, so direct calls cannot bypass either configured behavior; direct dispatch captures one registration before awaiting that resolution. The agent loop instead uses `prepareCall()` to keep the same registration across model resolution, durable header logging, and dispatch, retain detached context metadata from that exact lookup, and report which config fields the adapter defaulted. Adapter lookup happens at the terminal continuation of the `llm/stream` waterfall, so a listener may short-circuit the call or route a mutable one-shot request before lookup. AgentLoop observes a request attempt once the outer waterfall returns a stream handle; that limited boundary does not prove a lazy terminal adapter was constructed or began provider I/O. The `block-start` / `block-end` `index` correlation and the assembler together mean an adapter only has to emit well-formed chunks — block reassembly is not each adapter's problem. [architecture.md](../index.md#turn-flow) shows where `ctx.llm.stream()` and the `llm/stream` waterfall sit in one turn.
662
+
663
+ ```ts type-equiv
664
+ /** One model call whose config and adapter registration were resolved together. */
665
+ interface PreparedLlmCall {
666
+ /** Detached, deep-frozen config with any adapter-owned default materialized. */
667
+ readonly config: LlmCallConfig
668
+ /** Immutable retry policy captured with the adapter registration. */
669
+ readonly retryPolicy: ResolvedRetryPolicy
670
+ /** Detached context metadata resolved with the registration-bound call. */
671
+ readonly context?: LlmModelContext
672
+ /** Config fields materialized by the captured adapter rather than proposed by the caller. */
673
+ readonly adapterDefaults: LlmCallConfigAdapterDefaults
674
+ /**
675
+ * Dispatch this call once through the registration captured during
676
+ * preparation. The request's call-config fields must match {@link config};
677
+ * reuse or mismatch fails with `INVALID_PREPARED_CALL`.
678
+ * @param options - fully assembled request carrying the prepared config.
679
+ * @returns the chunk stream, including the `llm/stream` waterfall.
680
+ */
681
+ stream(options: GenerateOptions): AsyncIterable<StreamChunk>
682
+ }
683
+ ```
684
+
685
+ ```ts public-api
686
+ /**
687
+ * Provider-wire adapter for the harness message and stream vocabulary. Register implementations
688
+ * with `ctx.llm.registerAdapter(providers, adapter)`. Every provider HTTP request must include
689
+ * `attributionHeaders()`; prove the headers are added in the wire request or library header hook. The direct-fetch
690
+ * DeepSeek and library-backed pi-ai adapters meet this contract through different internals.
691
+ */
692
+ declare abstract class LlmAdapter {
693
+ /**
694
+ * Describe one provider route owned by this adapter.
695
+ * @param provider - a route passed to `registerAdapter()` for this instance.
696
+ * @returns detached display metadata whose id must equal `provider`.
697
+ */
698
+ providerInfo(provider: string): LlmProviderInfo;
699
+ /**
700
+ * Return the provider-owned retry policy captured with this route.
701
+ * @param _provider - a route passed to `registerAdapter()` for this instance.
702
+ * @returns a resolved policy, or `undefined` to use the normal defaults.
703
+ */
704
+ providerRetryPolicy(_provider: string): ResolvedRetryPolicy | undefined;
705
+ /**
706
+ * List models this adapter can currently advertise for one owned provider.
707
+ * The result is advisory: an adapter may accept unlisted model ids, and
708
+ * consumers must not turn absence into request rejection.
709
+ * @param _provider - one provider route owned by this adapter.
710
+ * @returns discoverable models in adapter-preferred order.
711
+ */
712
+ listModels(_provider: string): Promise<readonly LlmModelInfo[]>;
713
+ /**
714
+ * Resolve all metadata available for one exact model. This query is
715
+ * independent of the advisory catalog and does not validate request routing.
716
+ * @param provider - one provider route owned by this adapter.
717
+ * @param model - exact model id passed to {@link GenerateOptions.model}.
718
+ * @param _signal - cancellation for this exact-model lookup; asynchronous
719
+ * implementations must settle promptly after it aborts.
720
+ * @returns provider/model identity plus any context, call-default, and reasoning metadata.
721
+ */
722
+ resolveModel(
723
+ provider: string,
724
+ model: string,
725
+ _signal?: AbortSignal,
726
+ ): Promise<LlmResolvedModelInfo>;
727
+ /**
728
+ * Stream one model call as raw chunks. The only required method.
729
+ * @param options - the fully-assembled request; implementations must honor `options.signal`.
730
+ * @returns the chunk stream, obeying the adapter contract documented on `StreamChunk`.
731
+ */
732
+ abstract stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
733
+ }
734
+ ```
735
+
736
+ `ContentBlockType` (the key set the `index`-correlated blocks carry) derives from [`ContentBlockMap`](#content-blocks-and-messages) above.
737
+
738
+ <!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
739
+
740
+ <a id="cordis-surface"></a>
741
+
742
+ ## Cordis API
743
+
744
+ Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
745
+
746
+ <a id="ctxllm--llmruntime"></a>
747
+
748
+ ### `ctx.llm` — `LlmRuntime`
749
+
750
+ The abstract `llm` service: an adapter registry plus a streaming model-call API, interceptable via the `llm/stream` waterfall.
751
+
752
+ ```ts cordis-catalog
753
+ /**
754
+ * Register an adapter for the given provider routes. Throws `LlmError` with code
755
+ * `DUPLICATE_ADAPTER` if any provider already has an adapter (all-or-nothing).
756
+ * Disposed with the fiber.
757
+ * @param providers - every provider route this adapter should serve.
758
+ * @param adapter - the adapter that streams calls for those providers.
759
+ * @returns the disposer, carrying {@link AdapterRegistrationHandle.replace}.
760
+ */
761
+ registerAdapter(providers: string[], adapter: LlmAdapter): AdapterRegistrationHandle
762
+
763
+ /**
764
+ * Describe provider routes with a registered adapter.
765
+ * @returns detached provider metadata in registration order.
766
+ */
767
+ listProviders(): LlmProviderInfo[]
768
+
769
+ /**
770
+ * Declare provider routes an adapter plugin can activate through
771
+ * configuration. Registration is all-or-nothing: an empty list, invalid
772
+ * entry, or a provider already declared by any registration throws
773
+ * `LlmError` without registering the rest. Disposed with the fiber.
774
+ * @param entries - every configurable provider this plugin owns.
775
+ * @returns a handle that withdraws all of them, and can atomically replace them.
776
+ */
777
+ registerConfigurableProviders(entries: readonly LlmConfigurableProvider[]): DirectoryRegistrationHandle
778
+
779
+ /**
780
+ * List every declared configurable provider, registered or dormant.
781
+ * @returns detached directory entries in declaration order.
782
+ */
783
+ listConfigurableProviders(): LlmConfigurableProvider[]
784
+
785
+ /**
786
+ * Offer to interrogate provider endpoints on behalf of the settings
787
+ * namespace this plugin owns. The namespace is the key because that is what
788
+ * a configuration surface already holds from the configurable-provider
789
+ * directory, and because a provider being *added* has no route to name yet.
790
+ * Disposed with the fiber.
791
+ * @param settingsNs - the namespace whose profiles this discovery serves.
792
+ * @param discover - interrogates one endpoint; must honor `request.signal`.
793
+ * @returns the disposer that withdraws the offer.
794
+ */
795
+ registerModelDiscovery( settingsNs: string, discover: (request: LlmModelDiscoveryRequest) => Promise<readonly LlmDiscoveredModel[]>, ): () => void
796
+
797
+ /**
798
+ * Interrogate one provider endpoint for the models it advertises. The
799
+ * request describes a draft, not a stored route, so nothing here reads or
800
+ * writes settings or credentials — the caller owns both, and the reply is
801
+ * candidate metadata a surface may offer for adoption.
802
+ * @param settingsNs - namespace whose registered discovery serves this draft.
803
+ * @param request - the endpoint, protocol, and one-shot credential to use.
804
+ * @returns the advertised models, deduplicated in endpoint order.
805
+ */
806
+ async discoverModels( settingsNs: string, request: LlmModelDiscoveryRequest, ): Promise<LlmDiscoveredModel[]>
807
+
808
+ /**
809
+ * Resolve the retry policy captured when one provider route was registered.
810
+ * @param provider - registered provider route to inspect.
811
+ * @returns the provider-owned policy, with normal defaults already resolved.
812
+ */
813
+ providerRetryPolicy(provider: string): ResolvedRetryPolicy
814
+
815
+ /**
816
+ * Discover models advertised by one registered provider. Catalog membership
817
+ * is advisory and never changes routing or request validation.
818
+ * @param provider - registered provider route to inspect.
819
+ * @returns detached model metadata in adapter-preferred order.
820
+ */
821
+ async listModels(provider: string): Promise<LlmModelInfo[]>
822
+
823
+ /**
824
+ * Resolve and validate all metadata from the adapter that owns one exact
825
+ * route. The result is detached from adapter-owned objects; catalog
826
+ * membership remains advisory and does not control request routing.
827
+ * @param provider - registered provider route to inspect.
828
+ * @param model - exact model id passed to the adapter.
829
+ * @param signal - optional cancellation for adapter-owned asynchronous lookup.
830
+ * @returns exact model identity plus available context and reasoning metadata.
831
+ */
832
+ async resolveModelInfo( provider: string, model: string, signal?: AbortSignal, ): Promise<LlmResolvedModelInfo>
833
+
834
+ /**
835
+ * Validate a conversation call config against its exact model capability and
836
+ * materialize adapter-configured defaults. Unsupported explicit efforts
837
+ * reject before provider I/O; no clamping or aliasing is performed. This
838
+ * standalone query does not bind a later dispatch; use {@link prepareCall}
839
+ * when logging and streaming must share one adapter registration.
840
+ * @param config - provider/model route and optional request controls.
841
+ * @param signal - optional cancellation for adapter-owned capability lookup.
842
+ * @returns a detached config only when a default must be materialized.
843
+ */
844
+ async resolveCallConfig(config: LlmCallConfig, signal?: AbortSignal): Promise<LlmCallConfig>
845
+
846
+ /**
847
+ * Resolve one call under its current adapter registration. The returned
848
+ * one-shot handle keeps that registration across header logging and dispatch,
849
+ * so HMR cannot combine one adapter's capability result with another adapter.
850
+ * @param config - provider/model route and optional request controls.
851
+ * @param signal - optional cancellation for adapter-owned capability lookup.
852
+ * @returns a prepared config and its registration-bound stream entry point.
853
+ */
854
+ async prepareCall(config: LlmCallConfig, signal?: AbortSignal): Promise<PreparedLlmCall>
855
+
856
+ /**
857
+ * Stream one model call as raw chunks (token-level deltas). Replay state is
858
+ * retained only when the same adapter instance owns its historical provider
859
+ * and the target provider. Final adapter selection remains fixed through
860
+ * asynchronous exact-model resolution and dispatch. Adapter selection,
861
+ * dispatch, and iteration failures become terminal `error` or `aborted`
862
+ * finish chunks; middleware, nested-call, cleanup, and consumer failures
863
+ * remain thrown.
864
+ * @param options - the full request; `options.provider` selects the adapter.
865
+ * @returns the chunk stream, possibly wrapped by `llm/stream` listeners.
866
+ */
867
+ stream(options: GenerateOptions): AsyncIterable<StreamChunk>
868
+ ```
869
+
870
+ Source: [`packages/llm/llm/src/index.ts:284`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/llm/llm/src/index.ts)
871
+
872
+ <a id="llm-events"></a>
873
+
874
+ ### `llm/*` events
875
+
876
+ <a id="llmadapters-updated--emit"></a>
877
+
878
+ #### `llm/adapters-updated` — emit
879
+
880
+ The provider topology changed: an adapter registered or unregistered routes, or the configurable-provider directory gained or lost entries. This payload-free registry notification fires at each commit point (including registration disposal); consumers re-read `listProviders()`, `listModels()`, or `listConfigurableProviders()` for the new state. Observer failures are contained and cannot veto the registry mutation.
881
+
882
+ ```ts cordis-catalog
883
+ /**
884
+ * The provider topology changed: an adapter registered or unregistered
885
+ * routes, or the configurable-provider directory gained or lost entries.
886
+ * This payload-free registry notification fires at each commit point
887
+ * (including registration disposal); consumers re-read `listProviders()`,
888
+ * `listModels()`, or `listConfigurableProviders()` for the new state.
889
+ * Observer failures are contained and cannot veto the registry mutation.
890
+ * @mode emit
891
+ */
892
+ 'llm/adapters-updated'(): void
893
+ ```
894
+
895
+ Source: [`packages/llm/llm/src/types.ts:23`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/llm/llm/src/types.ts)
896
+
897
+ <a id="llmstream--waterfall"></a>
898
+
899
+ #### `llm/stream` — waterfall
900
+
901
+ Waterfall around every streaming model call (retry, replay, routing). Bound to the LlmRuntime; call `next()` to reach the resolved adapter's stream, or yield your own chunks to short-circuit.
902
+
903
+ ```ts cordis-catalog
904
+ /**
905
+ * Waterfall around every streaming model call (retry, replay, routing).
906
+ * Bound to the {@link LlmRuntime}; call `next()` to reach the resolved
907
+ * adapter's stream, or yield your own chunks to short-circuit.
908
+ * @param options - the full request. A LOOP-built request carries the
909
+ * process-local {@link markAgentLoopRequest} identity and arrives deep-frozen
910
+ * (mutation throws): its content is a pure function of the session log (the
911
+ * reconstructability Agent Note), so listeners read it, never rewrite it.
912
+ * Hand-built calls do not carry that marker; their messages already obey
913
+ * the immutable creation contract.
914
+ * @mode waterfall
915
+ */
916
+ 'llm/stream'(this: LlmRuntime, options: GenerateOptions, next: () => AsyncIterable<StreamChunk>): AsyncIterable<StreamChunk>
917
+ ```
918
+
919
+ Source: [`packages/llm/llm/src/index.ts:64`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/llm/llm/src/index.ts)
920
+ <!-- END GENERATED cordis-surface -->