@kodax-ai/kodax 0.7.63 → 0.7.67

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 (84) hide show
  1. package/CHANGELOG.md +2390 -2189
  2. package/README.md +118 -16
  3. package/README_CN.md +78 -12
  4. package/dist/chunks/{agent-SL444XLN.js → agent-E4V5SLX6.js} +1 -1
  5. package/dist/chunks/argument-completer-N6RT5KYT.js +2 -0
  6. package/dist/chunks/{chunk-ARUWXX25.js → chunk-35P7QL2Q.js} +1 -1
  7. package/dist/chunks/chunk-AKMXJSGM.js +56 -0
  8. package/dist/chunks/{chunk-L3MF5V64.js → chunk-CW32NQFO.js} +1 -1
  9. package/dist/chunks/{chunk-KCFVGXVK.js → chunk-DEICMTHO.js} +9 -9
  10. package/dist/chunks/{chunk-7MPU7TP6.js → chunk-EN3O2C43.js} +270 -270
  11. package/dist/chunks/chunk-KBBGDRER.js +22 -0
  12. package/dist/chunks/chunk-NVDK375I.js +729 -0
  13. package/dist/chunks/{chunk-S4GVQO3W.js → chunk-O7N22GJ3.js} +1 -1
  14. package/dist/chunks/chunk-ONUPGMER.js +2 -0
  15. package/dist/chunks/{chunk-ZL5CEANW.js → chunk-PZTG2C33.js} +1 -1
  16. package/dist/chunks/chunk-QI6SQ2UQ.js +306 -0
  17. package/dist/chunks/{chunk-6OZ5KWG3.js → chunk-QJJKMNZF.js} +1 -1
  18. package/dist/chunks/chunk-RS2GAABJ.js +328 -0
  19. package/dist/chunks/chunk-W2XMNYLK.js +15 -0
  20. package/dist/chunks/chunk-WCSSXBHR.js +419 -0
  21. package/dist/chunks/chunk-YH5BTUQO.js +343 -0
  22. package/dist/chunks/{chunk-UA744TZM.js → chunk-YRBNXFHY.js} +1 -1
  23. package/dist/chunks/compaction-config-56NO42ZI.js +2 -0
  24. package/dist/chunks/{construction-bootstrap-EFZBONTK.js → construction-bootstrap-GLEHQ2KB.js} +1 -1
  25. package/dist/chunks/{devtools-4CRULTR2.js → devtools-CD6KSIHE.js} +1 -1
  26. package/dist/chunks/{devtools-YINBSZC7.js → devtools-XR5F2C6T.js} +1 -1
  27. package/dist/chunks/{dist-73L4OYPD.js → dist-4PXB7U22.js} +1 -1
  28. package/dist/chunks/dist-4UIRSSFS.js +2 -0
  29. package/dist/chunks/host-DMREFNSO.js +2 -0
  30. package/dist/chunks/{paste-FDYM7SZX.js → paste-C33GZQV5.js} +1 -1
  31. package/dist/chunks/run-manager-P5XMSGZ7.js +2 -0
  32. package/dist/chunks/utils-JENPFKAE.js +2 -0
  33. package/dist/constructed-handler-worker.js +2 -0
  34. package/dist/index.d.ts +25 -11
  35. package/dist/index.js +6 -6
  36. package/dist/kodax_cli.js +1282 -1232
  37. package/dist/runtime-worker.js +2872 -0
  38. package/dist/sdk-agent.d.ts +46 -10
  39. package/dist/sdk-agent.js +1 -1
  40. package/dist/sdk-coding.d.ts +337 -1020
  41. package/dist/sdk-coding.js +1 -1
  42. package/dist/sdk-llm.js +1 -1
  43. package/dist/sdk-mcp.d.ts +2 -1
  44. package/dist/sdk-mcp.js +1 -1
  45. package/dist/sdk-media.js +1 -1
  46. package/dist/sdk-repl.d.ts +208 -185
  47. package/dist/sdk-repl.js +2 -2
  48. package/dist/sdk-runtime.d.ts +1070 -0
  49. package/dist/sdk-runtime.js +2 -0
  50. package/dist/sdk-session.d.ts +4 -4
  51. package/dist/sdk-session.js +1 -1
  52. package/dist/sdk-skills.js +1 -1
  53. package/dist/semantic-worker.js +191 -10
  54. package/dist/types-chunks/{bash-prefix-extractor.d-CZW9fRoa.d.ts → bash-prefix-extractor.d-D57Gt6n8.d.ts} +186 -12
  55. package/dist/types-chunks/{run-manager.d-CYTnWhZY.d.ts → capsule.d-BZmqu_HL.d.ts} +5 -92
  56. package/dist/types-chunks/commands.d-Cg26ud-Y.d.ts +213 -0
  57. package/dist/types-chunks/{types.d-1CnTg7Sd.d.ts → guardrail.d-Ds7JHxR1.d.ts} +3 -128
  58. package/dist/types-chunks/{guardrail.d-MR6LwMB2.d.ts → guardrail.d-vSaBe64I.d.ts} +3 -3
  59. package/dist/types-chunks/{manager.d-DBD7SOTT.d.ts → manager.d-Zum9cGHU.d.ts} +3 -173
  60. package/dist/types-chunks/oauth-login.d-Bgb4rdLN.d.ts +174 -0
  61. package/dist/types-chunks/public-api.d-CPBMPDiU.d.ts +602 -0
  62. package/dist/types-chunks/run-manager.d-lnwnfyNC.d.ts +91 -0
  63. package/dist/types-chunks/{sdk-session-RBSBBKol.d.ts → sdk-session-ByoMLoUS.d.ts} +8 -306
  64. package/dist/types-chunks/{process.d-CY2g03Mb.d.ts → types.d-DG9B4SMm.d.ts} +369 -4
  65. package/dist/types-chunks/types.d-DU9lLkmE.d.ts +128 -0
  66. package/dist/types-chunks/types.d-DYv3a1ka.d.ts +975 -0
  67. package/dist/types-chunks/{utils.d-C14jZ9ZM.d.ts → utils.d-DMI-9GTR.d.ts} +88 -72
  68. package/docs/SDK_EMBEDDER_GUIDE.md +3079 -0
  69. package/package.json +8 -1
  70. package/dist/chunks/argument-completer-EN63ZVJK.js +0 -2
  71. package/dist/chunks/chunk-4MIHVNB7.js +0 -405
  72. package/dist/chunks/chunk-HP4FXMEM.js +0 -18
  73. package/dist/chunks/chunk-KHICMJIR.js +0 -310
  74. package/dist/chunks/chunk-LGR7ACBQ.js +0 -326
  75. package/dist/chunks/chunk-RR7W7UF6.js +0 -341
  76. package/dist/chunks/chunk-UGTK2JIJ.js +0 -731
  77. package/dist/chunks/chunk-V4WSBIXB.js +0 -2
  78. package/dist/chunks/chunk-VSWROENP.js +0 -52
  79. package/dist/chunks/compaction-config-4VIONMFB.js +0 -2
  80. package/dist/chunks/dist-EDF2YUF6.js +0 -2
  81. package/dist/chunks/host-J4HPDLFC.js +0 -2
  82. package/dist/chunks/run-manager-ZVSW2OWM.js +0 -2
  83. package/dist/chunks/utils-5HDLYTZJ.js +0 -2
  84. package/dist/types-chunks/storage.d-C6kkAEch.d.ts +0 -280
@@ -0,0 +1,3079 @@
1
+ # KodaX SDK — Embedder Integration Guide
2
+
3
+ > Audience: host applications embedding `@kodax-ai/kodax` (and its
4
+ > subpaths) as a substrate — e.g. KodaX Space's desktop wrapper, IDE
5
+ > extensions, custom CLIs. If you are an end-user running the `kodax`
6
+ > command-line tool, see the root [README.md](../README.md) instead.
7
+
8
+ This guide documents the SDK surfaces a host integrator needs that
9
+ are NOT obvious from inspecting the type definitions alone:
10
+
11
+ 1. [MCP server management — `McpManager` runtime API](#1-mcp-server-management--mcpmanager-runtime-api)
12
+ 2. [Skill `!`cmd`` dynamic-context resolution + `IVariableResolver`](#2-skill-cmd-dynamic-context-resolution--ivariableresolver)
13
+ 3. [Per-app data directory namespacing — `getAppDataDir`](#3-per-app-data-directory-namespacing--getappdatadir)
14
+ 4. [Cross-reference: other FEATURE_186 surfaces](#4-cross-reference-other-feature_186-surfaces)
15
+ 5. [Consuming from a CommonJS context (Electron main, CJS bundles)](#5-consuming-from-a-commonjs-context-electron-main-cjs-bundles)
16
+ 6. [Session persistence — wiring `runKodaX` to disk](#6-session-persistence--wiring-runkodax-to-disk)
17
+ 7. [Local development via `npm link` (iterating against in-tree KodaX)](#7-local-development-via-npm-link-iterating-against-in-tree-kodax)
18
+ 8. [User-authored agents — markdown loader + extension `registerAgent`](#8-user-authored-agents--markdown-loader--extension-registeragent-feature_191-v0743)
19
+ 9. [Electron + `stdio: 'inherit'` on Windows — PowerShell input hijack](#9-electron--stdio-inherit-on-windows--powershell-input-hijack)
20
+ 10. [Model capabilities — context window, reasoning, descriptors](#10-model-capabilities--context-window-reasoning-descriptors)
21
+ 11. [Workflow process events and lifecycle controls](#11-workflow-process-events-and-lifecycle-controls-feature_229-v0750)
22
+ 12. [Provider credential verification — `verifyProviderCredential`](#12-provider-credential-verification--verifyprovidercredential-feature_216-v0745)
23
+ 13. [Inject your product's manual — `selfManual`](#13-inject-your-products-manual--selfmanual-feature_221-v0747)
24
+ 14. [Media input artifacts — `@kodax-ai/kodax/media`](#14-media-input-artifacts--kodax-aikodaxmedia-feature_239-v0756)
25
+ 15. [Space v0.7.57 follow-up ledger](#15-space-v0757-follow-up-ledger)
26
+ 16. [SDK agent-profile surface — `KodaXAgentProfile`](#16-sdk-agent-profile-surface--kodaxagentprofile-feature_247-v0758)
27
+ 17. [Runtime SDK, Worker isolation, and local daemon](#17-runtime-sdk-worker-isolation-and-local-daemon-feature_253-feature_257)
28
+ 18. [External-agent executor plane](#18-external-agent-executor-plane-feature_258-v0767)
29
+ 19. [Session surface filtering and cursor pagination](#19-session-surface-filtering-and-cursor-pagination-feature_261-v0767)
30
+ 20. [Cost-disciplined workflow routing and telemetry](#20-cost-disciplined-workflow-routing-and-telemetry-feature_259-v0767)
31
+
32
+ §1–§3 (and the Phase-7/8 MCP-popout surface in §1) land in v0.7.42
33
+ under FEATURE_186 (see [ADR-032](ADR.md#adr-032-sdk-embedder-surface-closure-feature_186-v0742)).
34
+ §5 documents the ESM-only packaging contract and the canonical
35
+ `await import(...)` recipe for CJS / Electron main consumers.
36
+
37
+ > **Code examples in §1–§4 use static ESM `import`** — that's the
38
+ > shape ESM consumers (Node `"type": "module"`, Vite, modern Electron
39
+ > renderer with `nodeIntegration: true`+ESM) want. **If your host
40
+ > compiles to CJS** (Electron main process, legacy Webpack CJS bundle,
41
+ > `tsc --module commonjs`), every static `import` example becomes
42
+ > `await import(...)` — see §5 for the full recipe and bundler
43
+ > configuration.
44
+
45
+ ---
46
+
47
+ ## 1. MCP server management — `McpManager` runtime API
48
+
49
+ ### Why this exists
50
+
51
+ `@kodax-ai/kodax/mcp` re-exports `McpCapabilityProvider`, which is the
52
+ class KodaX uses internally to plug MCP into the agent runtime. Its
53
+ methods are capability-provider-shaped (`search` / `describe` /
54
+ `execute` / `read` / `getPrompt`) — that's the substrate-facing API,
55
+ not what a popout UI needs.
56
+
57
+ `McpManager` is a thin facade exposing the **popout-shape** API for
58
+ the lifecycle operations a host UI typically renders:
59
+
60
+ ```
61
+ +- MCP Servers (popout) -+
62
+ | filesystem [Ready] | <- listServers row
63
+ | git [Idle] |
64
+ | sqlite [Error] |
65
+ +------------------------+
66
+ | [Start] [Stop] [Logs] | <- startServer / stopServer / getServerLogs
67
+ | [View tools] | <- listTools / getCatalog
68
+ +------------------------+
69
+ ```
70
+
71
+ ### Quick start
72
+
73
+ ```ts
74
+ import { createMcpManager } from '@kodax-ai/kodax/mcp';
75
+ import { listMcpServers } from '@kodax-ai/kodax/repl';
76
+
77
+ // Build manager from the persisted ~/.kodax/config.json `mcpServers` section.
78
+ // listMcpServers reads the CRUD module's view of disk state.
79
+ const manager = createMcpManager(listMcpServers());
80
+
81
+ // Enumerate all configured servers (lazy + prewarm + disabled all included).
82
+ const rows = manager.listServers();
83
+ for (const row of rows) {
84
+ console.log(`${row.serverId}: status=${row.status}, tools=${row.tools}`);
85
+ }
86
+
87
+ // Force connect + catalog refresh for a single server.
88
+ const started = await manager.startServer('filesystem');
89
+ console.log(`filesystem now: ${started.status}, ${started.tools} tools`);
90
+
91
+ // Tools-only view (popout's "tools" tab).
92
+ const toolList = await manager.listTools('filesystem');
93
+ for (const tool of toolList.tools) {
94
+ console.log(` - ${tool.name}: ${tool.description ?? '(no description)'}`);
95
+ }
96
+
97
+ // Full catalog (tools + resources + prompts) — popout's "all capabilities" tab.
98
+ const catalog = await manager.getCatalog('filesystem');
99
+ for (const descriptor of catalog.descriptors) {
100
+ console.log(` [${descriptor.kind}] ${descriptor.id}`);
101
+ }
102
+
103
+ // Last error + status for the "Logs" pane.
104
+ const logs = manager.getServerLogs('filesystem');
105
+ console.log(`status=${logs.status}, lastError=${logs.lastError ?? '(none)'}`);
106
+
107
+ // Disconnect transport (server stays in config; status flips to idle).
108
+ await manager.stopServer('filesystem');
109
+
110
+ // Tear down all runtimes when the popout closes / app exits.
111
+ await manager.dispose();
112
+ ```
113
+
114
+ ### Method reference
115
+
116
+ | Method | Returns | Purpose |
117
+ |---|---|---|
118
+ | `listServers()` | `McpServerStatus[]` | One row per configured server: `status`, `tools`/`resources`/`prompts` counts, `lastError`, `cachedAt`, deep-cloned `config`. Synchronous (uses cached diagnostics). |
119
+ | `startServer(id)` | `Promise<McpServerStatus>` | Force `refreshCatalog(true)`. Connects (or reconnects), re-lists tools/resources/prompts, writes the disk cache. Throws on unknown id. |
120
+ | `stopServer(id)` | `Promise<McpServerStatus>` | Dispose transport. Server stays in config so a subsequent `startServer` / `listTools` reconnects cleanly. |
121
+ | `getServerLogs(id)` | `McpServerLogs` | `{ status, connect, lastError?, cachedAt? }`. Designed as the data source for a "Logs" pane. **v0.7.42 is intentionally conservative** — only last error + status are exposed. Future iterations may add a ring buffer; the field shape will extend, never break. |
122
+ | `listTools(id, { forceRefresh? })` | `Promise<McpServerToolList>` | Tools-only filtered descriptors. Triggers lazy connect on cold cache. |
123
+ | `getCatalog(id, { forceRefresh? })` | `Promise<McpServerCatalog>` | Full catalog: `items` (lightweight) + `descriptors` (full) for tools, resources, and prompts. |
124
+ | `dispose()` | `Promise<void>` | Dispose all runtimes. After this, build a fresh `McpManager` to reuse. |
125
+ | `provider()` | `McpCapabilityProvider` | Escape hatch — the underlying capability-provider for advanced uses. |
126
+ | `execute(id, input)` | `Promise<CapabilityResult>` | Invoke a tool by capability id (`mcp:<serverId>:tool:<name>`). |
127
+ | `describe(id)` | `Promise<McpCapabilityDescriptor \| undefined>` | Resolve a single descriptor by capability id. |
128
+ | `search(query, options?)` | `Promise<readonly McpCatalogItem[]>` | Cross-server catalog search. |
129
+ | `read(id, options?)` | `Promise<CapabilityResult>` | Read a resource by capability id. |
130
+
131
+ ### Server lifecycle states
132
+
133
+ `McpServerStatus.status` values:
134
+
135
+ | Status | Meaning |
136
+ |---|---|
137
+ | `idle` | Configured, not yet connected. Lazy default until first tool call. |
138
+ | `connecting` | Connection in progress (transient). |
139
+ | `ready` | Connected, catalog cached. |
140
+ | `error` | Last connect / refresh failed. `lastError` carries the message. |
141
+ | `disabled` | `connect: 'disabled'` in config — runtime exists but won't be used. |
142
+
143
+ ### Adding / removing servers
144
+
145
+ For server **config** CRUD (write to `~/.kodax/config.json`), use the
146
+ `@kodax-ai/kodax/repl` subpath — kept separate from `/mcp` to keep
147
+ the latter dependency-free:
148
+
149
+ ```ts
150
+ import { listMcpServers, upsertMcpServer, removeMcpServer } from '@kodax-ai/kodax/repl';
151
+
152
+ upsertMcpServer('filesystem', {
153
+ type: 'stdio',
154
+ command: 'mcp-server-filesystem',
155
+ args: ['/repo'],
156
+ connect: 'lazy',
157
+ });
158
+
159
+ // Remote servers: `type: 'streamable-http'` or `'sse'`. For ecosystem-config
160
+ // compatibility you may also use `type: 'http'` (v0.7.48+) — a config-layer
161
+ // alias that auto-detects Streamable HTTP first, then falls back to legacy
162
+ // HTTP+SSE. OAuth-protected servers are zero-config: omit `auth` endpoint
163
+ // fields and KodaX discovers + dynamically registers on the first 401.
164
+
165
+ // In-flight `McpManager` does NOT hot-pickup config changes. The
166
+ // standard pattern is: edit config, then construct a fresh manager
167
+ // (or call dispose() + createMcpManager again) before the next agent
168
+ // turn.
169
+ await manager.dispose();
170
+ const refreshed = createMcpManager(listMcpServers());
171
+ ```
172
+
173
+ ### Trust boundary
174
+
175
+ KodaX is a single-user CLI; the manager is last-write-wins. A popout
176
+ that swaps configs hot constructs a fresh manager — there is no
177
+ file-locking layer in v0.7.42. If your host needs multi-process
178
+ coordination (e.g. KodaX Space's popout window AND the main agent
179
+ both writing the same config), you mediate at your IPC layer.
180
+
181
+ ---
182
+
183
+ ## 2. Skill `!`cmd`` dynamic-context resolution + `IVariableResolver`
184
+
185
+ ### Why this exists
186
+
187
+ Skills (`SKILL.md` files in `~/.kodax/skills/<name>/` etc.) support
188
+ **dynamic context** — markdown can embed `!`shell-command`` tokens
189
+ that are replaced with the command's stdout at resolution time:
190
+
191
+ ```markdown
192
+ ---
193
+ name: incident-report
194
+ description: Generate an incident summary from current repo state.
195
+ ---
196
+
197
+ Current git status:
198
+ !`git status --short`
199
+
200
+ Recent commits:
201
+ !`git log --oneline -10`
202
+ ```
203
+
204
+ Default behavior (no host hook): `VariableResolver` directly
205
+ `execSync`s each command with an internal allowlist. **For host
206
+ applications** (KodaX Space, IDE extensions, sandboxed substrates)
207
+ this is rarely the right behavior — the host typically wants to
208
+ mediate every shell execution through its own permission broker,
209
+ audit trail, or policy gate.
210
+
211
+ v0.7.42 (FEATURE_186 Phase 3) added a host hook on `SkillContext`
212
+ that intercepts every `!`cmd`` execution.
213
+
214
+ ### Quick start
215
+
216
+ ```ts
217
+ import { createResolver, resolveSkillContent } from '@kodax-ai/kodax/skills';
218
+ import type { SkillContext } from '@kodax-ai/kodax/skills';
219
+
220
+ const context: SkillContext = {
221
+ workingDirectory: '/repo',
222
+ projectRoot: '/repo',
223
+ sessionId: 'session-1',
224
+ environment: process.env as Record<string, string>,
225
+
226
+ // v0.7.42 — host hook for !`cmd` execution.
227
+ executeDynamicContext: async (command, cwd) => {
228
+ // Route through the host's permission broker.
229
+ const approved = await brokerAskUser({
230
+ kind: 'skill-shell-command',
231
+ command,
232
+ cwd,
233
+ });
234
+ if (!approved) {
235
+ throw new Error('User denied shell command execution');
236
+ }
237
+ // Run via the host's audited shell wrapper.
238
+ const { stdout } = await brokerExecute(command, { cwd });
239
+ return stdout;
240
+ },
241
+ };
242
+
243
+ const resolved = await resolveSkillContent(skill.content, '', context);
244
+ // resolved.content has !`cmd` tokens replaced with stdout (or the
245
+ // command was rejected and the resolver substituted an error
246
+ // banner — depending on your hook's throw behavior).
247
+ ```
248
+
249
+ ### Disabling dynamic context entirely
250
+
251
+ For maximum-safety hosts (e.g. a security-audit popout), set
252
+ `disableDynamicContext: true` — every `!`cmd`` token is replaced with
253
+ a refusal banner regardless of any hook:
254
+
255
+ ```ts
256
+ const context: SkillContext = {
257
+ workingDirectory: '/repo',
258
+ projectRoot: '/repo',
259
+ sessionId: 'session-1',
260
+ environment: {},
261
+ disableDynamicContext: true,
262
+ };
263
+ ```
264
+
265
+ ### Resolution priority (3-tier dispatch)
266
+
267
+ ```
268
+ +- VariableResolver.executeDynamicCommand ------+
269
+ | |
270
+ | 1. context.disableDynamicContext === true |
271
+ | -> throw "Dynamic context disabled" |
272
+ | |
273
+ | 2. context.executeDynamicContext is set |
274
+ | -> await hook(command, cwd) |
275
+ | |
276
+ | 3. (legacy fallback) |
277
+ | -> isSafeDynamicContextCommand allowlist |
278
+ | -> execSync (with built-in restrictions) |
279
+ | |
280
+ +------------------------------------------------+
281
+ ```
282
+
283
+ **Hosts should always set `executeDynamicContext`** — the legacy
284
+ fallback exists for the standalone `kodax` CLI use case where the
285
+ end-user implicitly trusts their own machine's shell. Embedded
286
+ hosts have a different trust boundary (user trusts the host UI,
287
+ host UI mediates everything else).
288
+
289
+ ### The LLM-triggered `skill` tool path — `KodaXOptions.skillDynamicContext` (v0.7.58)
290
+
291
+ The `SkillContext` hook above covers skills your host expands itself via
292
+ `resolveSkillContent`. But a skill can also be **auto-triggered by the model**
293
+ through the built-in `skill` tool — and that path builds its own `SkillContext`
294
+ internally, so it does not see your `resolveSkillContent` hook. Without wiring,
295
+ an auto-triggered `SKILL.md` (including a cloned project-level
296
+ `.kodax/skills/*`) would run its `` !`cmd` `` blocks through the built-in
297
+ `execSync` allowlist, bypassing your permission broker.
298
+
299
+ Thread the same policy into the tool path via `runKodaX`/`startKodaX` options:
300
+
301
+ ```ts
302
+ import { runKodaX } from '@kodax-ai/kodax/coding';
303
+
304
+ await runKodaX(
305
+ {
306
+ provider: 'anthropic',
307
+ skillDynamicContext: {
308
+ // Same shape as SkillContext.executeDynamicContext — route through your broker.
309
+ execute: async (command, cwd) => brokerExecute(command, cwd),
310
+ // …or refuse all dynamic-context commands outright:
311
+ // disable: true,
312
+ },
313
+ },
314
+ prompt,
315
+ );
316
+ ```
317
+
318
+ Absent this option the tool path keeps the trusted-CLI `execSync` fallback
319
+ (unchanged), so setting `skillDynamicContext.execute` (or `disable: true`) is the
320
+ supported way for an embedder to bring the auto-triggered path under the same
321
+ policy as the manual one.
322
+
323
+ ### `IVariableResolver` entry points
324
+
325
+ | Symbol | Purpose | Source |
326
+ |---|---|---|
327
+ | `IVariableResolver` (interface) | Type contract — `resolve(content): Promise<string>` | `@kodax-ai/kodax/skills` |
328
+ | `VariableResolver` (class) | Stock implementation; reads `context.executeDynamicContext` if present, otherwise legacy `execSync` | `@kodax-ai/kodax/skills` |
329
+ | `createResolver(context)` (factory) | Returns a fresh `IVariableResolver` bound to the given context | `@kodax-ai/kodax/skills` |
330
+ | `resolveSkillContent(content, args, context)` (top-level) | High-level convenience — constructs resolver internally + resolves a single skill body | `@kodax-ai/kodax/skills` |
331
+
332
+ Hosts that want to ship a fully custom resolver (e.g. replace the
333
+ default with a JS-only sandbox) can implement `IVariableResolver`
334
+ directly and call `resolver.resolve(content)` themselves — KodaX's
335
+ own skill execution path calls through this interface.
336
+
337
+ ### Argument parsing
338
+
339
+ `parseArguments(args: string): string[]` is exported alongside the
340
+ resolver. Use it to convert the raw `$ARGS` string into a parsed
341
+ positional array matching the skill's `argumentHint`.
342
+
343
+ ---
344
+
345
+ ## 3. Per-app data directory namespacing — `getAppDataDir`
346
+
347
+ ### Why this exists
348
+
349
+ `~/.kodax/` is KodaX's own state directory — sessions, custom
350
+ providers, MCP servers, instances heartbeat, etc. all live under it.
351
+ Embedder hosts often need their **own** state directory adjacent to
352
+ KodaX's:
353
+
354
+ - IDE extension: store extension-local cache (`recents`, layout
355
+ prefs, popout snapshot).
356
+ - KodaX Space: store desktop-app preferences (window position,
357
+ theme, last-opened project).
358
+
359
+ Letting every host pick its own dir leads to multi-app conflict
360
+ (two extensions both writing `~/.kodax/cache/`) — and writing
361
+ inside `~/.kodax/` itself risks collision with KodaX's own keys.
362
+
363
+ `getAppDataDir(appId)` carves out a namespaced subdirectory under
364
+ `<KODAX_HOME>/apps/<appId>/` with reserved-name guards.
365
+
366
+ ### Quick start
367
+
368
+ ```ts
369
+ import { getAppDataDir } from '@kodax-ai/kodax/coding';
370
+
371
+ // Returns <KODAX_HOME>/apps/space/ (default: ~/.kodax/apps/space/)
372
+ // mkdirSync({recursive:true}) is called for you.
373
+ const spaceDir = getAppDataDir('space');
374
+
375
+ // Persist host-specific state inside that dir.
376
+ writeFileSync(path.join(spaceDir, 'window-state.json'), JSON.stringify(state));
377
+ ```
378
+
379
+ ### Namespace rules
380
+
381
+ `appId` is validated against `^[a-z][a-z0-9-]{1,31}$`:
382
+
383
+ - Must start with a lowercase letter.
384
+ - May contain lowercase letters, digits, `-`.
385
+ - 2-32 characters total.
386
+ - Reserved prefix: `kodax-*` (and the bare string `kodax`) — rejected
387
+ to prevent host apps from squatting on names that look like KodaX's
388
+ own subsystems.
389
+
390
+ | `appId` | Result |
391
+ |---|---|
392
+ | `space` | ✓ `<KODAX_HOME>/apps/space/` |
393
+ | `kodax-space-helper` | ✗ `Error: reserved appId prefix 'kodax-*'` |
394
+ | `MyApp` | ✗ `Error: appId must match /^[a-z][a-z0-9-]{1,31}$/` |
395
+ | `a` | ✗ `Error: appId must be 2-32 chars` |
396
+
397
+ ### Interaction with `setAgentConfigHome`
398
+
399
+ `getAppDataDir` resolves `<KODAX_HOME>` via `getAgentConfigHome()` on
400
+ every call — so a host that called `setAgentConfigHome('/custom')`
401
+ before the first `getAppDataDir('space')` lands at
402
+ `/custom/apps/space/`. Useful for:
403
+
404
+ - **Tests**: per-test temp dir override.
405
+ - **Multi-tenant**: route different tenants to different home dirs.
406
+
407
+ ```ts
408
+ import { setAgentConfigHome, getAppDataDir } from '@kodax-ai/kodax/coding';
409
+
410
+ setAgentConfigHome('/srv/tenant-42/.kodax');
411
+ const dir = getAppDataDir('space');
412
+ // dir === '/srv/tenant-42/.kodax/apps/space/'
413
+ ```
414
+
415
+ ---
416
+
417
+ ## 4. Cross-reference: other FEATURE_186 surfaces
418
+
419
+ The three surfaces above are the most "needs-a-guide" pieces.
420
+ FEATURE_186 ships several additional SDK exports that are self-
421
+ documenting from type signatures but are worth knowing:
422
+
423
+ | Subpath | Symbol | Purpose |
424
+ |---|---|---|
425
+ | `@kodax-ai/kodax` | `runKodaX(opts, prompt)` | Blocking `Promise<KodaXResult>` — the original entry. |
426
+ | `@kodax-ai/kodax/coding` | `startKodaX(opts, prompt): RunningSession` | Non-blocking handle. See [RunningSession docs](#running-session-quick-reference). |
427
+ | `@kodax-ai/kodax/coding` | `createSessionControl()` | Bare `KodaXSessionControl` for advanced wiring. |
428
+ | `@kodax-ai/kodax/coding` | `getAgentConfigPath(name)` | Resolves `<KODAX_HOME>/<name>` dynamically. |
429
+ | `@kodax-ai/kodax/coding` | `setAgentConfigHome(path \| undefined)` | Override KODAX_HOME for tests / multi-tenant. |
430
+ | `@kodax-ai/kodax/coding` | `validateCustomProviderConfig(config)` | Same validator the SDK uses internally — no parallel schemas. |
431
+ | `@kodax-ai/kodax/coding` | `ToolSideEffect` + 4 helpers | Tool metadata (`readonly` / `mutates-fs` / `mutates-shell` / `mutates-network` / `mutates-state`) for plan-mode gates + custom permission UIs. |
432
+ | `@kodax-ai/kodax/coding` | `loadAgentsFiles(opts?)` | Load `AGENTS.md` cascade (project + user + global) for prompt assembly. |
433
+ | `@kodax-ai/kodax/repl` | `bootstrapAutoMode(...)` | Bootstrap the auto-mode classifier guardrail. |
434
+ | `@kodax-ai/kodax/repl` | `loadCommands(...)` / `KODAX_COMMANDS_DIR` | Discover user-defined slash commands. |
435
+ | `@kodax-ai/kodax/repl` | `listCustomProviders` / `upsertCustomProvider` / `removeCustomProvider` | Custom LLM provider CRUD. |
436
+ | `@kodax-ai/kodax/repl` | `listMcpServers` / `upsertMcpServer` / `removeMcpServer` / `validateMcpServerConfig` | MCP server config CRUD. |
437
+ | `@kodax-ai/kodax/skills` | `IVariableResolver` / `VariableResolver` / `createResolver` | See [section 2](#2-skill-cmd-dynamic-context-resolution--ivariableresolver). |
438
+
439
+ ### RunningSession quick reference
440
+
441
+ ```ts
442
+ import { startKodaX } from '@kodax-ai/kodax/coding';
443
+
444
+ const session = startKodaX({ provider: 'anthropic', model: 'claude-sonnet-4-6' }, 'review this PR');
445
+ console.log(session.id); // freshly minted or echoes opts.session.id
446
+
447
+ // Mid-run mutators — applied on the next turn (CAP-055 per-turn re-resolution).
448
+ session.setProvider('zhipu');
449
+ session.setModel('glm-4.6');
450
+ session.setReasoning('balanced'); // KodaXReasoningMode: 'off' | 'auto' | 'quick' | 'balanced' | 'deep'
451
+
452
+ // Cooperative abort.
453
+ setTimeout(() => session.abort('user cancelled'), 30_000);
454
+
455
+ // Await the eventual result.
456
+ const result = await session.result;
457
+ console.log(result.finalMessage);
458
+ ```
459
+
460
+ Constructor-supplied `options.abortSignal` is forwarded into the
461
+ internal `AbortController`; calling either signal aborts the run.
462
+
463
+ ---
464
+
465
+ ## 5. Consuming from a CommonJS context (Electron main, CJS bundles)
466
+
467
+ ### TL;DR
468
+
469
+ `@kodax-ai/kodax` and **all** its subpaths are published **ESM-only**.
470
+ In a CommonJS context — Electron's main process (`vm.mainModule` runs
471
+ as CJS by default), legacy Webpack/esbuild configs with
472
+ `format: 'cjs'`, or any code path that ends up calling `require()` —
473
+ you must use dynamic `import()` instead of static `import` /
474
+ `require`. Both Node 22+ and Node 20.19+ support dynamic-importing
475
+ ESM from CJS without flags.
476
+
477
+ ```ts
478
+ // ❌ This breaks in CJS — esbuild / tsc transforms static `import`
479
+ // into `require('@kodax-ai/kodax/mcp')`, which Node then rejects
480
+ // with ERR_PACKAGE_PATH_NOT_EXPORTED because our `exports` only
481
+ // declares the `import` condition.
482
+ import { McpManager } from '@kodax-ai/kodax/mcp';
483
+
484
+ // ✅ Use dynamic import — Node resolves it via the ESM loader and
485
+ // matches our `import` condition.
486
+ const { McpManager } = await import('@kodax-ai/kodax/mcp');
487
+ ```
488
+
489
+ `import type { ... }` is **fine in CJS** — TypeScript / esbuild strip
490
+ type-only imports at compile time, so they never become runtime
491
+ `require()` calls:
492
+
493
+ ```ts
494
+ import type { McpManager, McpServerStatus } from '@kodax-ai/kodax/mcp';
495
+ // ↑ compiles to nothing at runtime
496
+ ```
497
+
498
+ ### Why we don't ship dual ESM/CJS bundles
499
+
500
+ The natural fix would be to add `"require": "./dist/sdk-*.cjs"` next
501
+ to the existing `"import"` condition in `package.json#exports`. We
502
+ investigated this for v0.7.42 and the technical reality blocks all
503
+ the subpaths embedders typically reach for:
504
+
505
+ | Subpath | ESM-only third-party deps inlined | Dual feasible? |
506
+ |---|---|---|
507
+ | `/agent` | 0 | ✅ feasible (no UI deps) |
508
+ | `/mcp` | 0 | ✅ feasible (no UI deps) |
509
+ | `/skills` | 1 (`yaml`) | ❌ |
510
+ | `/llm` | 2 (`@agentclientprotocol/sdk`, `partial-json`) | ❌ |
511
+ | `/coding` | 4 (`yaml`, `tsx`, …) | ❌ |
512
+ | `/repl` | **21** (`ink`, `chalk`, `react`, `ansi-escapes`, …) | ❌ |
513
+ | root | 21 (same as `/repl`) | ❌ |
514
+
515
+ Once a bundle inlines any ESM-only dependency, the bundle itself
516
+ cannot be a valid CJS module — `require()`ing it would synchronously
517
+ import an ESM dep, which Node refuses. `ink`, `chalk`, and most of
518
+ the modern terminal-UI ecosystem are ESM-only as of 2024–2026, with
519
+ no plans to dual-publish.
520
+
521
+ **Dynamic `import()` is the canonical fix**, not a workaround. It
522
+ is part of the ECMAScript standard, supported in CJS contexts by
523
+ spec, and is how Node itself recommends consuming ESM from CJS today.
524
+
525
+ ### Electron main process recipe
526
+
527
+ Electron's main process is CJS by default (`require('electron')`
528
+ works because main runs without `"type": "module"`). The pattern
529
+ that drops cleanly into existing Electron code:
530
+
531
+ ```ts
532
+ // main.ts (or main.cjs) — Electron main process
533
+ import type { McpManager } from '@kodax-ai/kodax/mcp'; // compile-time only
534
+ import type { RunningSession } from '@kodax-ai/kodax/coding'; // compile-time only
535
+
536
+ let mcpManager: McpManager | null = null;
537
+ let kodax: typeof import('@kodax-ai/kodax/coding') | null = null;
538
+
539
+ async function bootKodaX() {
540
+ // Bundle these dynamic imports as runtime-resolved (don't let
541
+ // esbuild rewrite them — see "Bundler config" below).
542
+ const { createMcpManager } = await import('@kodax-ai/kodax/mcp');
543
+ const { listMcpServers } = await import('@kodax-ai/kodax/repl');
544
+
545
+ kodax = await import('@kodax-ai/kodax/coding');
546
+ mcpManager = createMcpManager(listMcpServers());
547
+ }
548
+
549
+ app.whenReady().then(bootKodaX);
550
+
551
+ // Wire to IPC handlers — your popout UI calls these.
552
+ ipcMain.handle('mcp:listServers', () => mcpManager!.listServers());
553
+ ipcMain.handle('mcp:startServer', (_, id: string) => mcpManager!.startServer(id));
554
+ ipcMain.handle('mcp:listTools', (_, id: string) => mcpManager!.listTools(id));
555
+ ipcMain.handle('mcp:getServerLogs', (_, id: string) => mcpManager!.getServerLogs(id));
556
+
557
+ ipcMain.handle('kodax:run', async (_, prompt: string) => {
558
+ const session = kodax!.startKodaX({ provider: 'anthropic', model: 'claude-sonnet-4-6' }, prompt);
559
+ return session.result;
560
+ });
561
+ ```
562
+
563
+ ### Bundler configuration
564
+
565
+ Most bundlers (esbuild, Webpack, Vite, Rollup) will, by default,
566
+ transform `await import(x)` into `require(x)` when targeting CJS.
567
+ You must tell the bundler to **preserve** the dynamic import:
568
+
569
+ **esbuild**:
570
+
571
+ ```js
572
+ build({
573
+ format: 'cjs',
574
+ platform: 'node',
575
+ // Keep dynamic imports as-is so they resolve to the ESM bundle at runtime.
576
+ external: ['@kodax-ai/kodax', '@kodax-ai/kodax/*'],
577
+ })
578
+ ```
579
+
580
+ Alternatively, mark only `@kodax-ai/kodax` as external — bundlers
581
+ that respect `external` will not rewrite dynamic imports of external
582
+ modules.
583
+
584
+ **Webpack**: add `@kodax-ai/kodax` (and any subpaths you use) to
585
+ `externals`, e.g. `{ '@kodax-ai/kodax/mcp': 'commonjs2 @kodax-ai/kodax/mcp' }`.
586
+
587
+ **Vite**: in `vite.config.ts` SSR / Electron-main builds, add
588
+ `@kodax-ai/kodax` and subpaths to `ssr.external`.
589
+
590
+ If the bundler still rewrites the dynamic import, the symptom is a
591
+ synchronous `Error: ERR_REQUIRE_ESM` (Node ≤ 20) or
592
+ `ERR_PACKAGE_PATH_NOT_EXPORTED` (Node 22+). Fix the bundler config
593
+ before suspecting our package.
594
+
595
+ ### When you really need synchronous CJS
596
+
597
+ If your host environment cannot adopt `await import(...)` (rare —
598
+ even old Webpack supports it via `import()` syntax preserved through
599
+ to runtime), the two subpaths with zero ESM-only deps (`/agent` and
600
+ `/mcp`) are the **only** ones that could in principle ship a CJS
601
+ build. We have not productized this yet — file an issue with your
602
+ concrete blocker (sync popout startup, specific bundler limitation,
603
+ etc.) and we'll evaluate adding a partial-dual emit for those two
604
+ subpaths in a follow-up release.
605
+
606
+ ### Quick checklist before reporting "it doesn't work in CJS"
607
+
608
+ - [ ] `package.json#type` in **your** project — if it's `"module"`,
609
+ static `import` works; if absent / `"commonjs"`, you need
610
+ dynamic `await import()`.
611
+ - [ ] Your bundler emit format — `format: 'cjs'` + static `import`
612
+ from KodaX subpaths will always fail.
613
+ - [ ] You used `import type { ... }` not `import { ... }` for
614
+ type-only references (otherwise they survive as runtime
615
+ `require`).
616
+ - [ ] Bundler is set to **external** `@kodax-ai/kodax` (or skip
617
+ bundling subpath imports entirely).
618
+ - [ ] Node version ≥ 20.19 (or ≥ 22 for the more permissive
619
+ `--experimental-require-module` default).
620
+
621
+ ---
622
+
623
+ ## 6. Session persistence — wiring `runKodaX` to disk
624
+
625
+ ### The trap
626
+
627
+ ```ts
628
+ import { runKodaX } from '@kodax-ai/kodax/coding';
629
+
630
+ await runKodaX(
631
+ {
632
+ provider: 'zhipu-coding',
633
+ session: { id: 's_my_chat', scope: 'user' }, // ← session.id set
634
+ },
635
+ 'reply with: ok',
636
+ );
637
+ // ✗ Run completes, LLM streams, events fire — but
638
+ // ~/.kodax/sessions/s_my_chat.jsonl does NOT exist.
639
+ ```
640
+
641
+ `session.id` alone is **not** enough. The SDK's snapshot path is
642
+ gated on `options.session.storage`:
643
+
644
+ ```ts
645
+ // packages/coding/src/agent-runtime/middleware/session-snapshot.ts
646
+ if (!options.session?.storage) {
647
+ return; // silent no-op
648
+ }
649
+ ```
650
+
651
+ This is by design — the CLI ships its own storage wiring, and the
652
+ SDK doesn't want to force a disk-write side-effect onto every
653
+ embedder (some hosts persist to a DB / cloud / IndexedDB instead).
654
+ But the contract was previously **undocumented**, so SDK consumers
655
+ typically hit this once before learning the rule.
656
+
657
+ **v0.7.43 added a one-shot `console.warn` when `session.id` is set
658
+ but `session.storage` is missing** — it points at this section.
659
+
660
+ **v0.7.63 narrows that warning to caller-provided session IDs.** The
661
+ `startKodaX()` convenience wrapper may generate a handle ID for the returned
662
+ running-session object; that generated ID is threaded into the run only when it
663
+ will not override auto-resume/resume discovery, and it no longer triggers the
664
+ missing-storage warning by itself.
665
+
666
+ ### The canonical fix
667
+
668
+ ```ts
669
+ import { runKodaX } from '@kodax-ai/kodax/coding';
670
+ import { createSessionManager } from '@kodax-ai/kodax/session';
671
+
672
+ // One manager per host process; reuse across runs so the
673
+ // per-session write queue + append-watermark caches stay coherent.
674
+ const {
675
+ storage,
676
+ listSessions,
677
+ loadSession,
678
+ loadFullTranscript,
679
+ appendClientNotice,
680
+ compactSession,
681
+ } = createSessionManager();
682
+
683
+ await runKodaX(
684
+ {
685
+ provider: 'zhipu-coding',
686
+ session: {
687
+ id: 's_my_chat',
688
+ scope: 'user',
689
+ storage, // ← key — wire the storage instance
690
+ },
691
+ },
692
+ 'reply with: ok',
693
+ );
694
+ // ✓ ~/.kodax/sessions/s_my_chat.jsonl now exists after the run.
695
+
696
+ // Same `storage` instance reads back through SessionManager:
697
+ const recent = await listSessions({
698
+ scope: 'user',
699
+ surface: 'acp',
700
+ limit: 50,
701
+ });
702
+ const nextPage = recent.at(-1)?.cursor
703
+ ? await listSessions({
704
+ scope: 'user',
705
+ surface: 'acp',
706
+ limit: 50,
707
+ cursor: recent.at(-1)?.cursor,
708
+ })
709
+ : [];
710
+ const replay = await loadSession('s_my_chat');
711
+ const scrollback = await loadFullTranscript('s_my_chat');
712
+ const compacted = await compactSession('s_my_chat', { dryRun: true });
713
+
714
+ await appendClientNotice('s_my_chat', {
715
+ source: 'space',
716
+ content: '/doctor ok',
717
+ });
718
+ ```
719
+
720
+ ### What `createSessionManager()` returns (v0.7.43+)
721
+
722
+ ```ts
723
+ interface SessionManager {
724
+ // Read side (FEATURE_173 v0.7.42)
725
+ listSessions(...): Promise<SessionSummary[]>;
726
+ loadSession(id): Promise<...>;
727
+ loadFullTranscript(id): Promise<...>;
728
+ appendClientNotice(id, opts): Promise<SessionTranscriptEntry | null>;
729
+ compactSession(id, opts?): Promise<CompactSessionResult>;
730
+ forkSession(id, opts?): Promise<...>;
731
+ rewindSession(id, opts?): Promise<...>;
732
+ setActiveEntry(id, selector): Promise<void>;
733
+ deleteSession(id): Promise<void>;
734
+ listRunningSessions(): Promise<RunningSessionInfo[]>;
735
+ watchSessions(cb): () => void;
736
+ // Write side (v0.7.43 follow-up)
737
+ storage: FileSessionStorage; // ← NEW — pass into runKodaX
738
+ }
739
+ ```
740
+
741
+ `surface` is an exact filter applied before `limit`. Each returned summary may
742
+ include an opaque `cursor`; pass the last summary's cursor back unchanged to
743
+ continue the stable newest-first listing. Callers must not parse or construct
744
+ cursors themselves.
745
+
746
+ ### Active context vs full transcript vs UI replay
747
+
748
+ Session persistence exposes three related but different layers:
749
+
750
+ | Need | Use | Meaning |
751
+ |---|---|---|
752
+ | Continue a model turn | `loadSession(id)` | Active branch only. This is the context KodaX would resume from. |
753
+ | Render a host sidebar / scrollback | `loadFullTranscript(id)` | Append-order transcript entries, including entries no longer on the active branch when available. |
754
+ | Reuse TUI display projection | `SessionData.uiHistory` | Optional bounded replay cache. Interactive REPL sessions may write it; headless SDK sessions may not. |
755
+
756
+ For product UI, prefer `loadFullTranscript(id)` for conversation history and
757
+ treat `loadSession(id)` as the model-context API. Do not assume `uiHistory`
758
+ exists. It is intentionally a small, lossy replay cache; canonical facts remain
759
+ in `messages` / `lineage`.
760
+
761
+ `loadFullTranscript(id).transcriptEntries` is the structured host-facing
762
+ scrollback. Each entry has stable ownership and ordering fields:
763
+
764
+ ```ts
765
+ interface SessionTranscriptEntry {
766
+ entryId: string;
767
+ parentId: string | null;
768
+ logicalId: string;
769
+ sourceEntryId?: string;
770
+ timestamp: string;
771
+ type: 'message' | 'compaction' | 'branch_summary' | 'rewind_marker' | 'client_notice' | 'task_result';
772
+ source?: 'user' | 'assistant' | 'workflow' | 'child_task' | 'system' | 'client';
773
+ turnId?: string;
774
+ active: boolean;
775
+ message: KodaXMessage;
776
+ payload?: unknown;
777
+ taskResults?: readonly KodaXTaskResultMetadata[];
778
+ }
779
+ ```
780
+
781
+ `entryId` is the physical lineage node id. `logicalId` is stable across
782
+ forked/cloned copies of the same transcript item, and `sourceEntryId` is present
783
+ on cloned entries to point back to the root physical source entry. Hosts that
784
+ want to fold cloned history should group by `logicalId`, not by
785
+ `message.role`, content, timestamp, or `[compacted]` placeholders.
786
+ Legacy entries without persisted provenance use `logicalId === entryId` and omit
787
+ `sourceEntryId`; treat that as "unknown/not cloned", not as content-based proof
788
+ that no older clone exists.
789
+ `loadFullTranscript()` still returns raw append-order scrollback; it does not
790
+ hide compaction notices or silently merge branches.
791
+
792
+ Since v0.7.63, rewind audit markers are represented as
793
+ `type: 'rewind_marker'`. They are useful for host scrollback and audit UI, but
794
+ they do not enter model context: `loadSession()` omits them, and
795
+ `loadFullTranscript().messages` filters them out while
796
+ `loadFullTranscript().transcriptEntries` keeps the structured marker.
797
+
798
+ Use `type` / `source` / `timestamp` / `active` instead of parsing
799
+ `message.role`, synthetic wrapper text, or filesystem side stores. In
800
+ particular, workflow and child-task completions surface as
801
+ `type: 'task_result'` with `taskResults[]`:
802
+
803
+ ```ts
804
+ const full = await loadFullTranscript(sessionId);
805
+ for (const entry of full?.transcriptEntries ?? []) {
806
+ if (entry.type === 'task_result') {
807
+ for (const result of entry.taskResults ?? []) {
808
+ // result.source is 'workflow' or 'child_task'
809
+ // result.taskId / runId / status / title / summary are structured.
810
+ }
811
+ }
812
+ }
813
+ ```
814
+
815
+ For host-local output that should be visible in the transcript but must not
816
+ enter model context, call `appendClientNotice()`:
817
+
818
+ ```ts
819
+ await appendClientNotice(sessionId, {
820
+ source: 'space',
821
+ content: '/mcp status: 3 servers connected',
822
+ timestamp: new Date().toISOString(),
823
+ payload: { command: '/mcp status' },
824
+ });
825
+
826
+ const active = await loadSession(sessionId); // no client notice
827
+ const full = await loadFullTranscript(sessionId); // includes client_notice
828
+ ```
829
+
830
+ Client notices are persisted as lineage entries, not model messages. They are
831
+ returned from `loadFullTranscript()` with `type: 'client_notice'`,
832
+ `source: 'client'`, and `payload.entersModelContext === false`.
833
+
834
+ v0.7.51 extends the `uiHistory` schema so interactive sessions can persist
835
+ sanitized terminal tool cards. Headless SDK sessions can still reconstruct
836
+ tool-call display from canonical assistant `tool_use` and user `tool_result`
837
+ messages when no TUI replay cache exists. Workflow progress remains on the
838
+ `WorkflowProcessSnapshot` / lifecycle-controller surfaces from v0.7.50; session
839
+ history should replay durable child digests and final answers, not workflow live
840
+ process state. The neutral replay types live with the session data model and are
841
+ exported from both `@kodax-ai/kodax/agent` and `@kodax-ai/kodax/session`; use
842
+ `KodaXSessionUiHistoryItem` / `KodaXSessionUiToolCall` when a host needs to
843
+ type-check `SessionData.uiHistory`.
844
+
845
+ SDK hosts that want the same neutral replay projection as the TUI can call the
846
+ session helper directly without importing Ink/React:
847
+
848
+ ```ts
849
+ import {
850
+ loadSession,
851
+ restoreHistoryItemsFromSession,
852
+ type CreatableHistoryItem,
853
+ } from '@kodax-ai/kodax/session';
854
+
855
+ const session = await loadSession(sessionId);
856
+ const replayItems: CreatableHistoryItem[] = session
857
+ ? restoreHistoryItemsFromSession({
858
+ messages: session.messages,
859
+ uiHistory: session.uiHistory,
860
+ })
861
+ : [];
862
+ ```
863
+
864
+ `restoreHistoryItemsFromSession()` prefers persisted `uiHistory.tool_group`
865
+ items when present, and otherwise reconstructs sanitized terminal tool cards
866
+ from adjacent `tool_use` / `tool_result` message blocks. The lower-level
867
+ `extractHistorySeedsFromMessages()` helper is also exported from
868
+ `@kodax-ai/kodax/session` for hosts that want to apply their own projection.
869
+
870
+ ### Custom sessions directory (multi-tenant / tests)
871
+
872
+ ```ts
873
+ const { storage } = createSessionManager({
874
+ sessionsDir: '/srv/tenant-42/.kodax/sessions',
875
+ });
876
+ // `storage` writes there; matching listSessions/loadSession on the
877
+ // same manager read from the same dir.
878
+ ```
879
+
880
+ This is equivalent to constructing `FileSessionStorage` directly
881
+ but keeps read + write sharing one instance (so append-watermark
882
+ caches stay warm across mixed list / run operations).
883
+
884
+ ### Bring-your-own storage (database / cloud / IndexedDB)
885
+
886
+ `FileSessionStorage` implements `KodaXSessionStorage`. Any class
887
+ implementing the same interface can be passed in. Minimal contract:
888
+
889
+ ```ts
890
+ import type { KodaXSessionStorage, SessionData } from '@kodax-ai/kodax/coding';
891
+
892
+ class MyDbSessionStorage implements KodaXSessionStorage {
893
+ async save(id: string, data: SessionData): Promise<void> { /* ... */ }
894
+ async load(id: string): Promise<SessionData | null> { /* ... */ }
895
+ async list(opts?: ...): Promise<SessionSummary[]> { /* ... */ }
896
+ async delete(id: string): Promise<void> { /* ... */ }
897
+ // (other methods — see the @kodax-ai/kodax/coding type for the full surface)
898
+ }
899
+
900
+ const storage = new MyDbSessionStorage();
901
+ await runKodaX({ session: { id, storage }, ... }, prompt);
902
+ ```
903
+
904
+ The SA / AMA loops are storage-implementation-agnostic — they just
905
+ call `storage.save(...)` at the terminal sites (success / error /
906
+ mid-flow / limit-reached). Storage failures are swallowed locally
907
+ with a `[SessionSnapshot] storage.save failed` `console.error` —
908
+ they never propagate to the `runKodaX` caller.
909
+
910
+ ### Why isn't `storage` defaulted automatically?
911
+
912
+ Three reasons we **don't** auto-construct `FileSessionStorage` when
913
+ `session.id` is supplied:
914
+
915
+ 1. **Package boundary**: `FileSessionStorage` is implemented in
916
+ `@kodax-ai/repl` (it's >500 LoC of write-queue + watermark +
917
+ JSONL streaming logic). `@kodax-ai/coding` does not depend on
918
+ `/repl`, and reversing that direction breaks ADR-001 package
919
+ independence (you'd no longer be able to consume `/coding`
920
+ without dragging the Ink REPL bundle).
921
+ 2. **Pluggability**: hosts often want non-filesystem storage
922
+ (Electron IndexedDB, web app S3 bucket, server-side Postgres).
923
+ Defaulting to `FileSessionStorage` would force a `fs/promises`
924
+ side-effect on every embedder.
925
+ 3. **Explicit > implicit**: a silent default would hide the wiring
926
+ from new SDK consumers; the v0.7.43 `console.warn` makes the
927
+ missing wiring loud the first time it bites.
928
+
929
+ ### When to construct `FileSessionStorage` directly vs go through `createSessionManager`
930
+
931
+ | Use case | Construct directly | Use `createSessionManager` |
932
+ |---|---|---|
933
+ | Only need to save runs, no listing / reading UI | ✓ | (also fine — `storage` field gives you the same instance) |
934
+ | Need a popout / sidebar showing past sessions | | ✓ — pairs read + write through one instance |
935
+ | Custom sessions dir for tests / tenants | (works, but you must repeat the dir option for each call) | ✓ — `{ sessionsDir }` once, both sides honor it |
936
+ | Want to mock storage for unit tests | ✓ (inject mock implementing `KodaXSessionStorage`) | — |
937
+
938
+ ### Quick checklist before reporting "session.jsonl not appearing"
939
+
940
+ - [ ] Did you set `session.storage`? (Not just `session.id`.)
941
+ - [ ] Is the run actually reaching a terminal site?
942
+ `saveSessionSnapshot` fires from success / error / mid-flow /
943
+ limit-reached. A run that throws synchronously before
944
+ `runKodaX` enters the loop never saves.
945
+ - [ ] Did `storage.save` throw? Check `console.error` for
946
+ `[SessionSnapshot] storage.save failed`. Storage errors are
947
+ isolated by design (don't fail the run), but they leave the
948
+ session file unwritten.
949
+ - [ ] Is the `sessionsDir` the one you expect? Default is
950
+ `<KODAX_HOME>/sessions`; override via `setAgentConfigHome` or
951
+ `createSessionManager({ sessionsDir })`.
952
+ - [ ] Is the `session.id` filesystem-safe? KodaX accepts any string
953
+ but writes `<id>.jsonl` literally — IDs with `/`, `\`, or
954
+ control chars will be rejected by the OS.
955
+
956
+ ---
957
+
958
+ ## 7. Local development via `npm link` (iterating against in-tree KodaX)
959
+
960
+ If you maintain an SDK consumer (KodaX Space, an IDE extension, a custom
961
+ bundler integration) and need to iterate against an unreleased KodaX
962
+ build — verifying a bugfix, prototyping against an in-progress feature,
963
+ running your project's test suite against `main` — you can `npm link`
964
+ the in-tree KodaX checkout instead of waiting for a published version.
965
+
966
+ As of v0.7.43 the root `package.json` is in **already-published shape**:
967
+ `"name": "@kodax-ai/kodax"` is baked in along with all 8 SDK subpath
968
+ exports. `npm link` "just works" — no need to run `scripts/release.mjs`
969
+ first.
970
+
971
+ ### Recipe
972
+
973
+ ```bash
974
+ # In your local KodaX checkout
975
+ cd /path/to/KodaX
976
+ npm install # one-time (sets up workspaces)
977
+ npm run build # required — npm link resolves to dist/
978
+ npm link # exposes the dir as @kodax-ai/kodax globally
979
+
980
+ # In your SDK consumer project (e.g. KodaX Space)
981
+ cd /path/to/my-host-app
982
+ npm link @kodax-ai/kodax # consume the linked checkout
983
+ ```
984
+
985
+ After this, `import { ... } from '@kodax-ai/kodax/repl'` in your host
986
+ app resolves to `/path/to/KodaX/dist/sdk-repl.js`. Subsequent edits
987
+ inside KodaX require **re-running `npm run build`** — the link points
988
+ at the bundled output, not source.
989
+
990
+ ### Tearing down the link
991
+
992
+ ```bash
993
+ # In your SDK consumer project
994
+ npm unlink @kodax-ai/kodax # restore the published version (npm install runs again)
995
+
996
+ # In KodaX
997
+ npm unlink -g @kodax-ai/kodax # remove the global symlink
998
+ ```
999
+
1000
+ ### Why root stays `"private": true`
1001
+
1002
+ The dev `package.json` carries `"private": true` so a bare `npm publish`
1003
+ from the repo refuses — `scripts/release.mjs` is the only sanctioned
1004
+ publish path, and it briefly toggles `private: false` (via try/finally)
1005
+ just for the publish call. `"private"` does **not** block `npm link` —
1006
+ it only gates `npm publish` — so the linked-build flow is unaffected.
1007
+
1008
+ ### Alternative: tarball install
1009
+
1010
+ If you want a one-shot snapshot rather than a live symlink (e.g. for CI
1011
+ or for a teammate without write access to the KodaX checkout):
1012
+
1013
+ ```bash
1014
+ # In KodaX
1015
+ node scripts/release.mjs --pack-only # produces kodax-ai-kodax-<v>.tgz
1016
+
1017
+ # In your host app
1018
+ npm install /path/to/KodaX/kodax-ai-kodax-<version>.tgz
1019
+ ```
1020
+
1021
+ The tarball is byte-identical to what `npm publish` would ship, so it
1022
+ exercises exactly the published shape.
1023
+
1024
+ ---
1025
+
1026
+ ## 8. User-authored agents — markdown loader + extension `registerAgent` (FEATURE_191, v0.7.43)
1027
+
1028
+ ### Why this exists
1029
+
1030
+ KodaX's Self-Construction substrate (FEATURE_087-090 / 101) lets the
1031
+ LLM author + admit constructed agents at runtime. FEATURE_191 closes
1032
+ the loop for **human authors** and **SDK embedders**: ship an `<name>.md`
1033
+ file under `~/.kodax/agents/` (user-level) or `<repo>/.kodax/agents/`
1034
+ (project-level), or have an extension call `api.registerAgent(name, content)`
1035
+ at activate time. All three paths feed the same admission pipeline as
1036
+ the LLM-generation route and surface in the Worker SP's
1037
+ `=== Available specialist agents ===` block.
1038
+
1039
+ ### Markdown shape
1040
+
1041
+ ```markdown
1042
+ ---
1043
+ name: db-reviewer
1044
+ description: Reviews DB migrations for safety and best practices
1045
+ tools: [read, grep]
1046
+ model: claude-sonnet-4-6
1047
+ ---
1048
+ You are a DB migration reviewer. Focus on:
1049
+ - Locking behavior under concurrent writes
1050
+ - Default value backfill cost on large tables
1051
+ ```
1052
+
1053
+ `name` and `description` are required; missing/invalid `name` is a
1054
+ silent skip (claudecode-compatible: treats the file as a reference doc).
1055
+ `tools` accepts either a YAML array (`[read, grep]`) or a
1056
+ comma-separated string (`"read, grep"`); each entry maps to
1057
+ `builtin:<name>`. `mcpServers` / `hooks` / `memory` / `isolation` /
1058
+ `permissionMode` / `maxTurns` / `skills` frontmatter fields are
1059
+ silently ignored in v0.7.43 (forward-compat with future features).
1060
+
1061
+ Project agents (`<repo>/.kodax/agents/*.md`) shadow user-level agents
1062
+ of the same name (last-write-wins).
1063
+
1064
+ ### SDK API for extensions
1065
+
1066
+ ```ts
1067
+ // In an extension's activate function:
1068
+ export default async function activate(api: KodaXExtensionAPI) {
1069
+ const dispose = await api.registerAgent('python-reviewer', {
1070
+ instructions: 'You review Python code for PEP-8 + type hints.',
1071
+ description: 'Python code reviewer (PEP-8 + type hints)',
1072
+ tools: [{ ref: 'builtin:read' }, { ref: 'builtin:grep' }],
1073
+ });
1074
+
1075
+ // The returned dispose is auto-pushed onto the extension's
1076
+ // disposables list, so manual disposal is optional. Call it only
1077
+ // if you need to unregister the agent mid-session.
1078
+ return () => dispose();
1079
+ }
1080
+ ```
1081
+
1082
+ `api.registerAgent(name, content)` throws on admission rejection with
1083
+ the extension id + agent name + verdict reason — the embedder sees
1084
+ failures at activate time rather than silently dropped registrations.
1085
+
1086
+ ### Reading the agent registry (host code)
1087
+
1088
+ ```ts
1089
+ import {
1090
+ listConstructedAgents,
1091
+ resolveConstructedAgent,
1092
+ } from '@kodax-ai/kodax';
1093
+
1094
+ // All registered constructed agents (markdown + extension + LLM + CLI).
1095
+ const agents = listConstructedAgents();
1096
+
1097
+ // Resolve a specific agent by name.
1098
+ const agent = resolveConstructedAgent('db-reviewer');
1099
+ ```
1100
+
1101
+ > **Source-aware variants** — `listConstructedAgentsWithSource()` /
1102
+ > `resolveConstructedAgentSource(name)` expose the in-memory source
1103
+ > tag (`'built-in' | 'extension' | 'markdown:user' | 'markdown:project'
1104
+ > | 'constructed:cli' | 'constructed:llm'`). These are marked
1105
+ > `@internal` in v0.7.43 and exposed only via the construction
1106
+ > sub-barrel — they will be promoted to the top-level SDK entry when
1107
+ > the v0.7.46+ REPL `/agents list` command lands as their third
1108
+ > production consumer (current consumers: source-tag round-trip tests
1109
+ > + planned REPL). Embedders that need provenance today can read the
1110
+ > source via the existing `listConstructedAgents()` Agent shape and
1111
+ > cross-reference their own registration calls.
1112
+
1113
+ ### Wiring dispatch
1114
+
1115
+ Workers automatically see a registered agent through the SP block.
1116
+ Programmatic dispatch (e.g. in `runKodaX` SDK consumers) goes through
1117
+ the standard tool surface — pass `subagent_type` to
1118
+ `dispatch_child_task`:
1119
+
1120
+ ```ts
1121
+ // Inside a tool handler or eval driver:
1122
+ const result = await dispatchChildTask({
1123
+ id: 'child-1',
1124
+ objective: 'Review the migration in this PR',
1125
+ readOnly: true,
1126
+ subagent_type: 'db-reviewer',
1127
+ });
1128
+ ```
1129
+
1130
+ Unknown `subagent_type` returns a tool-result error listing available
1131
+ names (does NOT throw); write-capable specialists dispatched outside the
1132
+ Worker path are rejected at the dispatch layer. Older V1 docs may phrase
1133
+ this as "non-Worker/Generator"; Generator is historical after FEATURE_193.
1134
+
1135
+ ### See also
1136
+
1137
+ - [docs/test-guides/FEATURE_191_v0.7.43_TEST_GUIDE.md](test-guides/FEATURE_191_v0.7.43_TEST_GUIDE.md) — manual test recipes
1138
+ - [docs/features/v0.7.43.md FEATURE_191](features/v0.7.43.md#feature_191-user-authored-custom-agents--markdown-loader--extension-registeragent--dispatch_child_task-bridge) — design + acceptance gates
1139
+ - [docs/ADR.md ADR-035](ADR.md#adr-035-user-authored-custom-agents--markdown-loader--extension-registeragent--dispatch_child_task-bridge-feature_191-v0743) — architectural rationale
1140
+
1141
+ ---
1142
+
1143
+ ## 9. Electron + `stdio: 'inherit'` on Windows — PowerShell input hijack
1144
+
1145
+ ### Symptom
1146
+
1147
+ A host process (e.g. `scripts/dev.mjs`) spawns Electron with
1148
+ `stdio: 'inherit'`. After Electron's main process starts up and loads
1149
+ the KodaX SDK, the parent terminal (PowerShell, Windows Terminal,
1150
+ cmd.exe) stops responding to keyboard input — characters don't echo,
1151
+ Enter doesn't dispatch, Ctrl-C may not register.
1152
+
1153
+ ### What's actually happening
1154
+
1155
+ This is **not caused by KodaX hooking stdin**. The SDK has no
1156
+ module-level `process.stdin.on/setRawMode/resume/setEncoding` anywhere
1157
+ in the published code path. We verified this empirically with the
1158
+ following probe (Node 24, Windows, v0.7.43 dist):
1159
+
1160
+ | Probe step | stdin listeners delta | raw mode delta | signal listeners delta |
1161
+ |---|---|---|---|
1162
+ | `import('@kodax-ai/kodax')` (root) | 0 | none | 0 |
1163
+ | `import('@kodax-ai/kodax/agent')` | 0 | none | 0 |
1164
+ | `import('@kodax-ai/kodax/llm')` | 0 | none | 0 |
1165
+ | `import('@kodax-ai/kodax/coding')` | 0 | none | 0 |
1166
+ | `import('@kodax-ai/kodax/mcp')` | 0 | none | 0 |
1167
+ | `import('@kodax-ai/kodax/session')` | 0 | none | 0 |
1168
+ | `import('@kodax-ai/kodax/skills')` | 0 | none | 0 |
1169
+ | `import('@kodax-ai/kodax/repl')` | 0 | none | 0 |
1170
+ | `hydrateProcessEnvFromShell()` (Windows) | 0 (early return) | none | 0 |
1171
+ | `loadConfig()` + `listMcpServers()` | 0 | none | 0 |
1172
+
1173
+ `hydrateProcessEnvFromShell()` on Windows specifically returns `false`
1174
+ at [packages/repl/src/common/utils.ts](../packages/repl/src/common/utils.ts)
1175
+ line 151 **before** any `spawnSync`. Even on non-Windows where the
1176
+ spawn happens, it explicitly passes `stdio: ['ignore', 'pipe', 'pipe']`
1177
+ — the child shell never sees the parent stdin.
1178
+
1179
+ The root cause is an Electron + Windows ConPTY interaction: spawning
1180
+ an Electron child with `stdio: 'inherit'` from a Windows terminal
1181
+ makes the child process inherit a live handle to the parent's input
1182
+ stream. Even though nobody inside Electron's main process reads from
1183
+ it, the open handle alters how PowerShell / Windows Terminal route
1184
+ keystrokes — they cannot tell whether the upstream child has
1185
+ "consumed" them. This is a known Windows console quirk independent
1186
+ of any code Electron's main module runs.
1187
+
1188
+ ### Canonical fix (host-side)
1189
+
1190
+ Detach Electron's stdin in the spawn config:
1191
+
1192
+ ```js
1193
+ // scripts/dev.mjs
1194
+ import { spawn } from 'node:child_process';
1195
+
1196
+ const electron = spawn(electronBin, [appEntry], {
1197
+ stdio: ['ignore', 'inherit', 'inherit'], // ← stdin: 'ignore', NOT 'inherit'
1198
+ shell: false,
1199
+ });
1200
+ ```
1201
+
1202
+ `stdio: ['ignore', 'inherit', 'inherit']` keeps stdout/stderr piped
1203
+ to the host terminal for log visibility but prevents Electron from
1204
+ holding the parent's stdin. PowerShell / Windows Terminal regain
1205
+ full control of keyboard input. This is the canonical workaround
1206
+ Electron itself documents (the `electron/dev-tools` examples ship
1207
+ with this exact pattern).
1208
+
1209
+ ### Why we don't ship a SDK-side mitigation
1210
+
1211
+ There is nothing the SDK can do to release a stdin handle it never
1212
+ opened. Some hosts (CLI runners, headless servers) may legitimately
1213
+ want to pipe data into the Electron main process via stdin; pre-emptively
1214
+ closing or redirecting it from inside the SDK would break those.
1215
+ The spawn-time decision belongs to the host.
1216
+
1217
+ ### How to confirm whether your symptom matches this
1218
+
1219
+ Run [`scripts/probe-sdk-stdin.mjs`](../scripts/probe-sdk-stdin.mjs)
1220
+ against your in-tree or installed SDK dist:
1221
+
1222
+ ```bash
1223
+ # In-tree (this repo):
1224
+ node scripts/probe-sdk-stdin.mjs
1225
+
1226
+ # Against an installed @kodax-ai/kodax:
1227
+ node scripts/probe-sdk-stdin.mjs ./node_modules/@kodax-ai/kodax/dist
1228
+ ```
1229
+
1230
+ The probe imports every SDK subpath and runs the Space startup sequence
1231
+ (`hydrateProcessEnvFromShell` / typeof reads / `loadConfig` /
1232
+ `listMcpServers` / provider snapshots), reporting stdin listener delta /
1233
+ raw-mode delta / signal-handler delta at each step. If every step shows
1234
+ "no state change ✓", the issue is in your spawn config — not in KodaX.
1235
+ If any step shows a non-zero delta, file an issue with the probe output
1236
+ and your Node / OS / SDK version.
1237
+
1238
+ ---
1239
+
1240
+ ## 10. Model capabilities — context window, reasoning, descriptors
1241
+
1242
+ ### Why this exists
1243
+
1244
+ A popout-style UI typically wants to list every provider/model KodaX
1245
+ supports, with at minimum `context window` and `reasoning capability`
1246
+ shown next to each model — so the user can pick informed. Pre-v0.7.43,
1247
+ this metadata lived inside each `Provider` class's `config` field and
1248
+ was only readable via `provider.getContextWindow()` / `getModelDescriptor()`
1249
+ on an instantiated Provider. `getProvider(name)` instantiates which
1250
+ throws if the relevant API key env var is unset — meaning a UI couldn't
1251
+ show "Anthropic Sonnet 4.6 / 200K context" until the user had set
1252
+ `ANTHROPIC_API_KEY`. Capability metadata is **KodaX-maintained static
1253
+ data** (we know what context windows the upstream models advertise),
1254
+ so gating it on credentials is wrong.
1255
+
1256
+ v0.7.43 promoted this metadata into registry-layer snapshots and getters; the
1257
+ current implementation backs `KODAX_PROVIDER_SNAPSHOTS` with
1258
+ `provider-capabilities.json`. The getters still read without a provider
1259
+ instance, API key, or env var.
1260
+
1261
+ ### The new surface
1262
+
1263
+ All exports below come from `@kodax-ai/kodax/llm` (preferred) — also
1264
+ re-exported through `@kodax-ai/kodax` (root), `@kodax-ai/kodax/coding`,
1265
+ and `@kodax-ai/kodax/agent` for convenience.
1266
+
1267
+ ```ts
1268
+ import {
1269
+ // Built-in providers (anthropic / kimi / zhipu / deepseek / ark-coding / ...):
1270
+ getProviderModelDescriptors, // (name) => KodaXModelDescriptor[]
1271
+ getModelCapabilities, // (name, model) => KodaXModelCapabilities | undefined
1272
+ listBuiltinModelCapabilities, // () => KodaXModelCapabilities[] (all built-ins, default-first per provider)
1273
+
1274
+ // Custom providers (registered via `registerConfiguredCustomProviders`
1275
+ // from `~/.kodax/config.json#customProviders`):
1276
+ getCustomProviderModelDescriptors, // (name) => KodaXModelDescriptor[] | undefined
1277
+ getCustomModelCapabilities, // (name, model) => KodaXModelCapabilities | undefined
1278
+ listCustomProviderModelCapabilities, // () => KodaXModelCapabilities[]
1279
+
1280
+ // Unified dispatchers — built-in OR custom, transparent routing:
1281
+ resolveProviderModelDescriptors, // (name) => KodaXModelDescriptor[] (empty if unknown)
1282
+ resolveModelCapabilities, // (name, model) => KodaXModelCapabilities | undefined
1283
+ listAllModelCapabilities, // () => KodaXModelCapabilities[] (built-in + custom merged)
1284
+
1285
+ // Types:
1286
+ type KodaXModelCapabilities,
1287
+ type KodaXModelDescriptor,
1288
+ } from '@kodax-ai/kodax/llm';
1289
+ ```
1290
+
1291
+ ### Shape
1292
+
1293
+ ```ts
1294
+ interface KodaXModelCapabilities {
1295
+ provider: string; // 'anthropic' | 'kimi' | 'ark-coding' | <custom-name>
1296
+ model: string; // model id (e.g. 'claude-sonnet-4-6', 'kimi-k2.6')
1297
+ displayName: string; // human label — falls back to model id
1298
+ supportsThinking: boolean; // native reasoning is available?
1299
+ reasoningCapability: 'native-budget' | 'native-effort' | 'native-toggle' | 'prompt-only' | 'none' | 'unknown'; // legacy mechanism label
1300
+ reasoningProfile?: {
1301
+ defaultEffort?: string;
1302
+ supportedEfforts?: Array<{ value: string; isDefault?: boolean; isUserVisible?: boolean }>;
1303
+ };
1304
+ contextWindow?: number; // input tokens (provider default + per-model override cascade)
1305
+ maxOutputTokens?: number; // per-turn max_tokens KodaX requests — see note below
1306
+ thinkingBudgetCap?: number; // tokens (native-budget providers only)
1307
+ isDefault: boolean; // true for the provider's default model
1308
+ }
1309
+ ```
1310
+
1311
+ ### Recipes
1312
+
1313
+ **List every model KodaX supports (built-in + custom):**
1314
+
1315
+ ```ts
1316
+ import { listAllModelCapabilities } from '@kodax-ai/kodax/llm';
1317
+
1318
+ for (const caps of listAllModelCapabilities()) {
1319
+ console.log(`${caps.provider}/${caps.model}: ${caps.contextWindow ?? 'unknown'} tokens`);
1320
+ }
1321
+ ```
1322
+
1323
+ **Look up a single model:**
1324
+
1325
+ ```ts
1326
+ import { resolveModelCapabilities } from '@kodax-ai/kodax/llm';
1327
+
1328
+ const caps = resolveModelCapabilities('kimi', 'kimi-k2.6');
1329
+ // => { contextWindow: 256_000, supportsThinking: true, reasoningProfile: { defaultEffort: 'high', ... }, ... }
1330
+ ```
1331
+
1332
+ For picker/status UIs, use `reasoningProfile.supportedEfforts` and
1333
+ `defaultEffort`. The legacy `reasoningCapability` field describes the provider
1334
+ wire mechanism, not user-facing reasoning depth.
1335
+
1336
+ > **v0.7.58 fix — per-model overrides on a provider's DEFAULT model.**
1337
+ > `resolveModelCapabilities(provider, model)` previously dropped a model's own
1338
+ > `contextWindow` / `maxOutputTokens` / `reasoningProfile` override when that
1339
+ > model happened to be the provider's default (e.g. `zhipu-coding` / `zai-coding`
1340
+ > default to `glm-5.2`, which declares a 1M window — the resolver returned the
1341
+ > 200K provider default). It now merges the `models[]` override regardless of
1342
+ > default-model status, so `resolveModelCapabilities` agrees with the runtime
1343
+ > `provider.getEffectiveContextWindow()` / `getEffectiveMaxOutputTokens()`.
1344
+
1345
+ ### Resolving a wire-legal reasoning effort — `resolveWireEffort` (v0.7.58)
1346
+
1347
+ Mapping a user's desired reasoning strength to the actual wire `effort` value
1348
+ means composing the model's profile with its alias / disabled / ceiling / default
1349
+ rules AND any learned hard-rejections. `resolveWireEffort` (from
1350
+ `@kodax-ai/kodax/llm`) is the single host-facing entry so you don't re-assemble
1351
+ (and drift from) that logic:
1352
+
1353
+ ```ts
1354
+ import { resolveWireEffort } from '@kodax-ai/kodax/llm';
1355
+
1356
+ const { effort, adjusted } = resolveWireEffort({
1357
+ provider: 'zai-coding',
1358
+ model: 'glm-5.2',
1359
+ desiredEffort: 'low', // GLM-5.2 aliases low → high
1360
+ });
1361
+ // effort === 'high', adjusted === true
1362
+ ```
1363
+
1364
+ `effort` is `undefined` when the model omits a wire effort (e.g. anthropic
1365
+ adaptive) — send no `reasoning_effort` in that case; do **not** substitute a
1366
+ value. Pass `rejectedEfforts` (e.g. from the agent layer's
1367
+ `getCachedRejectedEfforts`) to fold learned rejections into the resolution.
1368
+
1369
+ ### Reasoning-effort rejection is self-healing at the runtime layer (v0.7.58)
1370
+
1371
+ When a provider hard-rejects a `reasoning_effort` (400/422), the coding runtime
1372
+ now **records** the rejection in the capability cache and **consults** it before
1373
+ building each subsequent turn's request — so the same rejected effort is not
1374
+ re-sent turn after turn. This happens whether or not a host wires
1375
+ `events.onReasoningEffortRejected` (that event is still delivered for hosts that
1376
+ want to surface it). Previously only the built-in REPL recorded rejections, so a
1377
+ headless SDK host silently re-issued a failing request every turn.
1378
+
1379
+ ### Passive effort capability learning
1380
+
1381
+ KodaX v0.7.57 treats `effort` as the primary reasoning-depth input. When a
1382
+ provider hard-rejects a requested effort value, the SDK emits
1383
+ `KodaXEvents.onReasoningEffortRejected` with provider/model/effort metadata.
1384
+ The LLM layer owns the pure learning semantics (`narrowReasoningProfile` and
1385
+ cache record helpers), while the agent layer provides the default
1386
+ `~/.kodax/capability-cache.json` store (`recordRejectedEffort`,
1387
+ `getCachedRejectedEfforts`, `clearCapabilityCache`). The built-in REPL is just
1388
+ one consumer: it records the event through the agent store and narrows future
1389
+ effort choices for the same provider/model.
1390
+
1391
+ Headless SDK hosts can use the same agent store, provide their own store around
1392
+ the LLM pure helpers, or ignore the event for deterministic no-learning runs.
1393
+ This keeps the mechanism reusable without forcing a cross-session disk cache on
1394
+ every embedded runtime.
1395
+
1396
+ **Group by provider for a picker UI:**
1397
+
1398
+ ```ts
1399
+ import {
1400
+ KODAX_PROVIDER_SNAPSHOTS,
1401
+ resolveProviderModelDescriptors,
1402
+ } from '@kodax-ai/kodax/llm';
1403
+
1404
+ for (const providerName of Object.keys(KODAX_PROVIDER_SNAPSHOTS)) {
1405
+ const descriptors = resolveProviderModelDescriptors(providerName);
1406
+ // descriptors[0] is the default model; descriptors.slice(1) are alternatives.
1407
+ }
1408
+ ```
1409
+
1410
+ ### A note on `maxOutputTokens`
1411
+
1412
+ `KodaXModelCapabilities.maxOutputTokens` is the **per-turn `max_tokens`
1413
+ KodaX requests**, NOT the upstream "theoretical maximum". The two
1414
+ diverge because:
1415
+
1416
+ - **What upstream advertises is often unreliable.** A 2026-05 probe
1417
+ against `zhipu-coding` / `kimi-code` / `minimax-coding` / `ark-coding`
1418
+ (Coding-Plan endpoint) / `deepseek` showed their `/v1/models`
1419
+ endpoints return only `{id, object, owned_by, created}` — no
1420
+ context-window, no max-output, no capabilities at all. Ark's
1421
+ pay-as-you-go `/v3/models` returns rich `token_limits` but none of
1422
+ the Coding-Plan models KodaX actually uses appear in that catalog.
1423
+ Even when upstream does advertise a number, stream behavior often
1424
+ deviates (the LLM stops early at unrelated stop conditions, or the
1425
+ server enforces a tighter kill window). Upstream `/models` data is
1426
+ not a substitute for KodaX-maintained metadata.
1427
+ - **What KodaX requests is the trustworthy number.** Values in
1428
+ `KODAX_PROVIDER_SNAPSHOTS` are bench-validated against each provider
1429
+ (kill-windows, decode-rate, cost-per-turn predictability). Examples:
1430
+ - DeepSeek V4 *advertises* 384K max output; KodaX requests
1431
+ `KODAX_ESCALATED_MAX_OUTPUT_TOKENS` (~32K) per turn so streams
1432
+ finish under server-side timeouts. Long generation flows through
1433
+ the L5 continuation meta path instead.
1434
+ - `zhipu-coding` has a ~308s server-side kill window; KodaX caps at
1435
+ 16K so typical tool_use turns complete within the window.
1436
+
1437
+ For a popout UI showing "expected output size for this model", use
1438
+ the KodaX value (`caps.maxOutputTokens`). It's exactly what KodaX
1439
+ asks the model for — i.e. the actual size budget your turn gets. If
1440
+ you also want to expose the model's *theoretical* max output, that
1441
+ comes from the upstream provider's own documentation; KodaX doesn't
1442
+ certify that number because we don't request it.
1443
+
1444
+ ### Why no instance methods touch this
1445
+
1446
+ The existing `provider.getContextWindow()` / `getEffectiveContextWindow()`
1447
+ / `getModelDescriptor()` instance methods still work — they're the
1448
+ runtime path the agent loop itself uses. The new getters layer above
1449
+ them at the **registry** layer so they don't need a Provider instance.
1450
+ A consumer that has a configured Provider and wants effective values
1451
+ in the four-step cascade (compactionConfig override → per-model →
1452
+ provider default → 200K fallback) should still use
1453
+ `resolveContextWindow(compactionConfig, provider, model)` from
1454
+ `@kodax-ai/kodax/agent`. The new registry-layer getters are for the
1455
+ *"list everything KodaX knows"* case — for picker UIs, comparison
1456
+ tables, capability-aware routing.
1457
+
1458
+ ### Confirming snapshot accuracy
1459
+
1460
+ Snapshot values are sourced from
1461
+ [`packages/llm/src/providers/provider-capabilities.json`](../packages/llm/src/providers/provider-capabilities.json)
1462
+ and loaded into the in-memory `KODAX_PROVIDER_SNAPSHOTS` export. When upstream
1463
+ providers publish a new model or change a context-window cap, the JSON file is
1464
+ the patch site — the new value flows to runtime (via `buildProviderConfig`) AND
1465
+ to SDK consumers (via the getters) in a single edit. The current snapshot is
1466
+ dated 2026-06-14 and includes the GPT-5.4, Kimi K2.7 Code, GLM-5.2, MiniMax
1467
+ M3/M2.7, DeepSeek V4, and Doubao Seed 2.0 route refreshes where supported. The
1468
+ test suite at
1469
+ [`packages/llm/src/providers/model-capabilities.test.ts`](../packages/llm/src/providers/model-capabilities.test.ts)
1470
+ locks in specific values (e.g. kimi-k2.6 at 256K, deepseek-v4-pro at 1M)
1471
+ so accidental drift is caught at PR time.
1472
+
1473
+ The probe scripts that surveyed upstream APIs live at
1474
+ [`scripts/probe-upstream-model-metadata.mjs`](../scripts/probe-upstream-model-metadata.mjs)
1475
+ and [`scripts/probe-ark-tokens.mjs`](../scripts/probe-ark-tokens.mjs) —
1476
+ re-run them periodically; if a provider starts returning richer model
1477
+ metadata, we can promote the snapshot to derive from it.
1478
+
1479
+ ---
1480
+
1481
+ ## 11. Workflow process events and lifecycle controls (FEATURE_229, v0.7.50)
1482
+
1483
+ FEATURE_229 makes dynamic workflow progress a reusable SDK process surface
1484
+ instead of terminal-only text. Hosts can observe and control workflows without
1485
+ parsing `/workflow` output, replaying slash commands, or depending on Ink view
1486
+ models.
1487
+
1488
+ Use the Agent subpath for neutral process types:
1489
+
1490
+ ```ts
1491
+ import type {
1492
+ WorkflowProcessEvent,
1493
+ WorkflowProcessSnapshot,
1494
+ } from '@kodax-ai/kodax/agent';
1495
+ ```
1496
+
1497
+ Use the Coding subpath for workflow execution, process subscription, and
1498
+ lifecycle control:
1499
+
1500
+ ```ts
1501
+ import {
1502
+ createWorkflowLifecycleController,
1503
+ createWorkflowRunManager,
1504
+ generateWorkflowFromOptions,
1505
+ } from '@kodax-ai/kodax/coding';
1506
+
1507
+ const runManager = createWorkflowRunManager();
1508
+ const unsubscribe = runManager.subscribeWorkflowProcess((event) => {
1509
+ renderWorkflowPanel(event.snapshot);
1510
+ });
1511
+
1512
+ const controller = createWorkflowLifecycleController({
1513
+ runManager,
1514
+ runBaseDir: '.kodax/workflows/runs',
1515
+ });
1516
+
1517
+ const generated = await generateWorkflowFromOptions({
1518
+ options,
1519
+ request: 'Review the payment flow',
1520
+ });
1521
+
1522
+ if (generated.kind !== 'generated') throw new Error(generated.reason);
1523
+
1524
+ const runId = makeRunId();
1525
+ const runDir = makeRunDir(runId);
1526
+ const run = runManager.startFromOptions({
1527
+ module: generated.module,
1528
+ args: { request: 'Review the payment flow' },
1529
+ options,
1530
+ runId,
1531
+ runDir,
1532
+ scriptSnapshot: generated.scriptSnapshot,
1533
+ onWorkflowProcessEvent: (event) => auditWorkflow(event.snapshot),
1534
+ });
1535
+
1536
+ await run.done;
1537
+ const snapshot = controller.getWorkflowProcessSnapshot(runId);
1538
+ const result = await controller.readWorkflowResult(runId);
1539
+ ```
1540
+
1541
+ `KodaXOptions.events.onWorkflowProcessEvent` receives the same events when a
1542
+ host runs normal coding tasks that enter workflow mode. `WorkflowProcessSnapshot`
1543
+ is intentionally ANSI-free and UI-neutral. It carries workflow status, phases,
1544
+ child item status, result-bearing child summaries, provider/model routing hints,
1545
+ and final `resultSummary`.
1546
+
1547
+ ### Two ways to author a workflow (v0.7.58)
1548
+
1549
+ There are two host-facing ways to turn a natural-language request into a workflow
1550
+ run — pick by whether you want the SDK to *orchestrate generation for you* or the
1551
+ *Worker to investigate and author it itself*:
1552
+
1553
+ | | `generateWorkflowFromOptions` (shown above) | `authorWorkflowViaWorker` |
1554
+ |---|---|---|
1555
+ | Who authors | A context-**blind** one-shot LLM call (`tools:[]`) | The **Worker agent** — scouts the repo with its own tools, then authors + runs `run_workflow` (ADR-047 scout-then-author) |
1556
+ | Host role | You call it, get a `module`, then `startFromOptions` | You submit one turn; the Worker does everything; you subscribe |
1557
+ | Quality | Generic (no repo investigation) | Grounded (real paths / sub-problems / `outputSchema` baked into child prompts) |
1558
+ | Use when | Non-interactive / CI / low-capability host, or you want to inspect the module before running | You want the same intelligence the REPL's `/workflow create` gets (recommended for interactive GUI hosts) |
1559
+
1560
+ `authorWorkflowViaWorker` is exactly what the REPL's `/workflow create` does
1561
+ internally (elevate one turn to `agentMode:'amaw'` so the Worker has
1562
+ `run_workflow`), exposed as a single call so a GUI host doesn't reimplement the
1563
+ turn-submission glue:
1564
+
1565
+ ```ts
1566
+ import { authorWorkflowViaWorker } from '@kodax-ai/kodax/coding';
1567
+
1568
+ const { session, workflowRunId } = authorWorkflowViaWorker({
1569
+ request: 'Review the payment flow end-to-end and fix any bugs you find',
1570
+ options: {
1571
+ provider: 'anthropic',
1572
+ workflowRunsBaseDir: '<your app data>/workflow-runs', // REQUIRED — else run_workflow can't wire (throws)
1573
+ events: {
1574
+ // Numeric, UI-neutral progress — same surface as everything else in §11.
1575
+ onWorkflowProcessEvent: (event) => renderWorkflowPanel(event.snapshot),
1576
+ },
1577
+ },
1578
+ });
1579
+
1580
+ // Resolves once the Worker actually launches a workflow (the run_workflow task),
1581
+ // or `undefined` if it judged a workflow unnecessary and answered inline.
1582
+ const runId = await workflowRunId;
1583
+
1584
+ // `session` is a normal RunningSession — await session.result, or session.abort().
1585
+ await session.result;
1586
+ ```
1587
+
1588
+ Notes:
1589
+ - `agentMode` is forced to `'amaw'` for the turn; the base `options.agentMode`
1590
+ is otherwise irrelevant here.
1591
+ - `workflowRunsBaseDir` is **mandatory** for this call (it gates `ctx.workflowHost`
1592
+ → the `run_workflow` tool). Omitting it throws immediately rather than silently
1593
+ producing a Worker that can't author.
1594
+ - The Worker retains judgment: for a request that doesn't warrant a multi-agent
1595
+ workflow it may just answer inline, in which case `workflowRunId` resolves
1596
+ `undefined`. There is no forced-tool guarantee (that would trade away the
1597
+ scout-then-author intelligence).
1598
+
1599
+ ### Resume replay telemetry (v0.7.58)
1600
+
1601
+ When a workflow run resumes a prior run (`run_workflow`'s `resumeFromRunId`),
1602
+ unchanged child agents replay instantly from the prior run's content-addressed
1603
+ cache. Three read-only fields let a host render "resumed, N/M replayed from
1604
+ cache" without changing any execution semantics:
1605
+
1606
+ - `WorkflowProcessSnapshot.resumedFromRunId?` — the prior run id this run resumed
1607
+ from (absent on a fresh run).
1608
+ - `WorkflowProcessItem.origin?` — `'ran'` (executed live this run) or
1609
+ `'replayed-from-cache'` (returned from the prior run's cache). Populated only
1610
+ on resumed runs; on a fresh run every item omits it (treat absent as `'ran'`).
1611
+ - `WorkflowProcessProgress.replayedAgents?` — count of replayed agents; present
1612
+ only when `> 0`. `spawnedAgents`/`finishedAgents` continue to count only agents
1613
+ that actually ran this turn.
1614
+
1615
+ All three are additive and absent on non-resumed runs, so existing renderers are
1616
+ unaffected. A resumed agent item is emitted with `status:'completed'` (a replay
1617
+ is instantaneous) and `origin:'replayed-from-cache'`.
1618
+
1619
+ ### Timeout configuration
1620
+
1621
+ SDK hosts can configure user-facing timeout budgets with seconds-based fields:
1622
+
1623
+ ```ts
1624
+ const options = {
1625
+ provider: 'anthropic',
1626
+ timeouts: {
1627
+ workflow: {
1628
+ generationTimeoutSec: 300,
1629
+ },
1630
+ llm: {
1631
+ requestTimeoutSec: 900,
1632
+ streamIdleTimeoutSec: 0,
1633
+ chunkTimeoutSec: 45,
1634
+ maxRetryDelaySec: 90,
1635
+ },
1636
+ },
1637
+ };
1638
+ ```
1639
+
1640
+ `timeouts.workflow.generationTimeoutSec` controls dynamic workflow harness
1641
+ generation. It replaces the legacy millisecond-only environment override for
1642
+ SDK callers while keeping `KODAX_WORKFLOW_GENERATION_TIMEOUT_MS` compatible.
1643
+ `timeouts.llm.*Sec` is normalized by the LLM-layer helper
1644
+ `resolveLlmTimeoutConfig()` from `@kodax-ai/kodax/llm`; the coding runtime then
1645
+ adapts the resolved millisecond values into provider resilience settings. Use
1646
+ the LLM helper directly when building a non-coding runner that still needs the
1647
+ same request/stream timeout semantics.
1648
+
1649
+ The public timeout config intentionally does not control internal cleanup or
1650
+ resource-protection watchdogs such as process kill probes, workflow stop
1651
+ cleanup, VM smoke checks, or daemon readiness checks.
1652
+
1653
+ ### Workflow run host attribution (v0.7.51)
1654
+
1655
+ Hosts that need to attach a workflow run back to an external session, surface,
1656
+ or tab can stamp an opaque string map on the run:
1657
+
1658
+ ```ts
1659
+ const run = runManager.startFromOptions({
1660
+ module: generated.module,
1661
+ args: { request: 'Review the payment flow' },
1662
+ options,
1663
+ runId,
1664
+ runDir,
1665
+ scriptSnapshot: generated.scriptSnapshot,
1666
+ processMetadata: {
1667
+ source: 'sdk',
1668
+ hostMetadata: {
1669
+ sessionId: 'space-session-123',
1670
+ tag: 'coder',
1671
+ },
1672
+ },
1673
+ });
1674
+
1675
+ runManager.subscribeWorkflowProcess((event) => {
1676
+ const owner = event.snapshot.hostMetadata;
1677
+ if (owner?.sessionId === 'space-session-123') {
1678
+ renderSessionWorkflow(event.snapshot);
1679
+ }
1680
+ });
1681
+ ```
1682
+
1683
+ `hostMetadata` is host-owned and KodaX does not interpret its keys. It is
1684
+ normalized as a small string-only map, persisted in `run.json`, and echoed on
1685
+ live and restored `WorkflowProcessSnapshot` values. Unstamped runs return
1686
+ `hostMetadata === undefined`; hosts should treat that as "no declared owner",
1687
+ not infer ownership from session replay text.
1688
+
1689
+ ### Live child-agent telemetry
1690
+
1691
+ F229 also preserves parent `KodaXEvents` callbacks for child-agent execution.
1692
+ Treat these callbacks as live telemetry, not canonical assistant messages.
1693
+ Tool callbacks, prompt callbacks, `onTextDelta`, `onThinkingDelta`,
1694
+ `onThinkingEnd`, and `onStreamEnd` can receive optional trailing metadata.
1695
+
1696
+ The child-agent event bridge is intentionally an allow-list. KodaX does not
1697
+ blindly clone the parent `KodaXEvents` object into child runs, because unscoped
1698
+ callbacks such as compaction, retry history, session start, and parent
1699
+ iteration start would otherwise mutate the parent host state. Child activity
1700
+ callbacks carry child metadata; child `onIterationEnd` events that are surfaced
1701
+ to a host are worker-scoped (`scope:'worker'`).
1702
+
1703
+ For workflow children, tool/progress/prompt callbacks can carry
1704
+ `workflowCorrelation` metadata that identifies the workflow run, child agent,
1705
+ and workflow item. Use that metadata to update a workflow panel or activity log.
1706
+ Keep `WorkflowProcessEvent` / `WorkflowProcessSnapshot` as the durable source of
1707
+ workflow state, summaries, terminal status, result reads, and artifact reads.
1708
+ Async digest failures are still summary-bearing: hosts should render
1709
+ `summaryStatus:'unavailable'` / `summaryKind:'digest-failed'` with the provided
1710
+ bounded fallback summary instead of treating the child as silent.
1711
+ KodaX gives async digest a longer best-effort window than blocking digest, so
1712
+ late `agent_summary_updated` messages can arrive noticeably after the child
1713
+ terminal event without restarting the workflow.
1714
+
1715
+ ### Collecting a child's result inside a workflow script (declare `outputSchema`)
1716
+
1717
+ When a workflow script aggregates child agents into a synthesis step, read the
1718
+ child's result from the **right field**. A `WorkflowTaskResult` (from
1719
+ `wf.runAgent` / `wf.wait`) carries several fields that are NOT all populated at
1720
+ the same instant:
1721
+
1722
+ | field | reliability at the moment `runAgent`/`wait` resolves |
1723
+ |---|---|
1724
+ | `structured` | **The reliable field.** Present + schema-validated (with one bounded, *awaited* repair turn) whenever the spawn declared an `outputSchema`. Resolved before the call returns. |
1725
+ | `finalText` | Always a string, but **may be empty or a "Let me start…" preamble** if the child ended its turn on a `tool_use`/handoff rather than a closing text block. Do NOT treat it as guaranteed content. |
1726
+ | `digest` / `digestPending` | The smart digest is delivered **asynchronously** via `agent_summary_updated` *after* the call resolves; at resolve time `digest` is usually absent and `digestPending` is `true`. It powers the live panel — it is NOT available to the script's return value. |
1727
+
1728
+ So a script that folds `finalText` straight into `wf.synthesize` can get **empty
1729
+ findings even though the per-agent digest is visible in the panel** (the digest
1730
+ arrived a moment later, asynchronously). The supported pattern:
1731
+
1732
+ ```ts
1733
+ const FINDING = {
1734
+ type: 'object', additionalProperties: false, required: ['finding'],
1735
+ properties: { finding: { type: 'string', description: 'Concrete findings with file:line evidence.' } },
1736
+ };
1737
+
1738
+ const result = await wf.runAgent({ name: 'review:auth', prompt, readOnly: true, outputSchema: FINDING });
1739
+ // Prefer the schema-validated structured finding; fall back to finalText only when non-empty.
1740
+ const text =
1741
+ (result?.structured as { finding?: string } | undefined)?.finding?.trim()
1742
+ || (result?.finalText?.trim() ? result.finalText : '[no finding returned]');
1743
+ ```
1744
+
1745
+ Declare an `outputSchema` on every child whose result feeds a downstream step,
1746
+ and read `result.structured`. `finalText` is a best-effort fallback, and the
1747
+ async `digest` is for live UI only — never rely on it in the script's own control
1748
+ flow. (The built-in `parallel-investigation` workflow follows exactly this
1749
+ pattern as the reference.)
1750
+
1751
+ For normal `dispatch_child_task` children, hosts should render child activity
1752
+ under the dispatch tool or a separate child-activity panel, while leaving the
1753
+ main TodoList/plan visible. A good default is:
1754
+
1755
+ - show the main agent plan as the work contract;
1756
+ - show child-agent tool/thinking/progress as bounded live-only activity;
1757
+ - persist only the child final summary, explicit approvals/audit records, and
1758
+ the parent assistant's final answer in the user-visible conversation history.
1759
+
1760
+ Callbacks use optional trailing metadata so existing consumers remain
1761
+ source-compatible:
1762
+
1763
+ ```ts
1764
+ events.onToolProgress = (update, meta) => {
1765
+ if (meta?.workflowCorrelation) {
1766
+ renderWorkflowToolProgress(meta.workflowCorrelation, update);
1767
+ return;
1768
+ }
1769
+ renderMainToolProgress(update);
1770
+ };
1771
+ ```
1772
+
1773
+ Hosts that want full child transcripts should put them in an explicit debug or
1774
+ trace drawer. Do not append raw child thinking/text/tool streams into the normal
1775
+ conversation by default; it makes the parent assistant appear to have authored
1776
+ every child step and can overwhelm users.
1777
+
1778
+ ### Sidecar verifier actionable messages
1779
+
1780
+ `KodaXEvents.onSidecarMessage` fires when the Sidecar Verifier produces an
1781
+ actionable `revise` or `blocked` verdict. `accept` remains silent because there
1782
+ is no message to deliver.
1783
+
1784
+ ```ts
1785
+ events.onSidecarMessage = (event) => {
1786
+ if (event.delivery === 'synthetic-user-message') {
1787
+ renderAudit(`Sidecar asked the main agent to revise: ${event.content}`);
1788
+ return;
1789
+ }
1790
+ if (event.delivery === 'budget-exhausted') {
1791
+ renderTerminalBlock(`Sidecar requested a revision, but the reanimate budget is exhausted: ${event.content}`);
1792
+ return;
1793
+ }
1794
+ renderTerminalBlock(event.content);
1795
+ };
1796
+ ```
1797
+
1798
+ The payload is:
1799
+
1800
+ ```ts
1801
+ interface KodaXSidecarMessageEvent {
1802
+ source: 'sidecar-verifier';
1803
+ verdict: 'revise' | 'blocked';
1804
+ recipient: 'main-agent' | 'user';
1805
+ delivery: 'synthetic-user-message' | 'budget-exhausted' | 'terminal-block';
1806
+ content: string;
1807
+ suggestedFix?: string;
1808
+ trace?: string;
1809
+ }
1810
+ ```
1811
+
1812
+ For `revise`, `content` is the exact synthetic user message injected back into
1813
+ the main agent. Treat it as sidecar-authored control text rather than a
1814
+ user-authored chat turn. When `delivery` is `budget-exhausted`, that same
1815
+ revise text was not injected because the runner is terminating instead. For
1816
+ `blocked`, `content` is terminal user-facing text. Headless JSONL output emits
1817
+ the same information as `{"type":"sidecar.message", ...}`.
1818
+
1819
+ The lifecycle controller also exposes terminal-run controls: stop, pause,
1820
+ resume, artifact reads, delete, prune, display-name changes, saved-capsule
1821
+ revision/replace provenance, and capsule preflight. Provenance fields such as
1822
+ `source`, `sourceRunId`, `sourceWorkflowName`, `savedWorkflowName`, and
1823
+ `revisionOf` let a host distinguish AMAW, `/workflow`, `/review --workflow`,
1824
+ saved-name reruns, and capsule revisions while still consuming one process
1825
+ contract.
1826
+
1827
+ ### Generated harness validation
1828
+
1829
+ Generated workflow source is treated as a restricted harness, not as trusted
1830
+ application code. `generateWorkflowFromOptions()` validates the source before a
1831
+ run is launched, including wrapped JavaScript compilation, the generated
1832
+ `async function run(wf, args)` contract, source-policy checks that ignore
1833
+ strings/comments, and a no-effect smoke execution with a fake `wf`
1834
+ implementation. Smoke validation catches common early harness defects such as
1835
+ malformed `wf.runAgent` inputs, wrong `wf.wait` arguments, startup
1836
+ `ReferenceError`, synchronous runaway startup code, and stalled startup awaits;
1837
+ those errors feed the generator repair loop instead of creating a doomed
1838
+ workflow run.
1839
+
1840
+ `preflightWorkflowCapsule()` also reports invalid restricted source as a
1841
+ `workflow:source` error. Hosts should show that as a capsule/harness problem
1842
+ before asking the user to approve a run. If a run still fails before launching
1843
+ any child agents, render it as a generated harness or saved capsule failure,
1844
+ not as a failed child-agent task. `/workflow rerun <runId>` repeats the saved
1845
+ script snapshot; it does not regenerate a broken generated harness.
1846
+
1847
+ Layer boundary:
1848
+
1849
+ - `@kodax-ai/kodax/agent` owns neutral workflow process/event/status types.
1850
+ - `@kodax-ai/kodax/coding` owns the coding backend, generated/saved workflow
1851
+ execution, run graph, host policy, lifecycle controller, result/artifact
1852
+ reads, and retention.
1853
+ - `@kodax-ai/kodax/repl` renders snapshots; it is not required for SDK workflow
1854
+ execution or progress UI.
1855
+
1856
+ ---
1857
+
1858
+ ## 12. Provider credential verification — `verifyProviderCredential` (FEATURE_216, v0.7.45)
1859
+
1860
+ ### Why
1861
+
1862
+ The original SDK exposed `provider.isConfigured()` (env-only check) and the full streaming surface (`provider.stream()` / `sideQuery()`). Neither fits the "test connection" UI use case: env check doesn't validate the key actually works against the upstream; streaming costs ~50–200 tokens and several seconds. KodaX Space (and any third-party SDK consumer building a provider-settings UI) needs a lightweight server-validated check.
1863
+
1864
+ FEATURE_216 ships **`verifyProviderCredential(name, opts?)`** — never-throws, lightweight, per-provider-strategy.
1865
+
1866
+ ### Quick start
1867
+
1868
+ ```typescript
1869
+ import { verifyProviderCredential } from '@kodax-ai/kodax/llm';
1870
+
1871
+ const result = await verifyProviderCredential('zhipu-coding', { timeoutMs: 8000 });
1872
+
1873
+ if (result.ok) {
1874
+ // Key works. result.durationMs is wall-clock; result.approxTokensSpent
1875
+ // is 0 for count-tokens/models-list strategies, ~6-7 for minimal-message.
1876
+ console.log(`✓ Verified in ${result.durationMs}ms (${result.approxTokensSpent} tokens)`);
1877
+ } else {
1878
+ switch (result.error) {
1879
+ case 'unauthorized': /* show "invalid key — auth failed" (covers 401/403 + kimi-code 400 special) */ break;
1880
+ case 'unconfigured': /* env var not set */ break;
1881
+ case 'network': /* show "check network" (DNS/conn/socket errors) */ break;
1882
+ case 'timeout': /* upstream didn't respond in time */ break;
1883
+ case 'server_error': /* upstream 5xx; transient */ break;
1884
+ case 'rate_limited': /* 429 — key is valid but throttled; suggest retry */ break;
1885
+ case 'unsupported': /* cli-bridge provider or unknown name */ break;
1886
+ case 'unknown': /* unexpected; surface result.message */ break;
1887
+ }
1888
+ }
1889
+ ```
1890
+
1891
+ ### Guarantees
1892
+
1893
+ - **Never throws** — every failure mode is captured in the returned `KodaXVerifyCredentialResult` envelope. Mirrors the `side-query.ts` pattern. Guarantee holds even for runtime-registered providers whose `verifyCredential()` override might throw (legacy 3rd-party extensions that predate FEATURE_216): the top-level helper wraps the call in try/catch and returns `error: 'unknown'`.
1894
+ - **No ctor throw on missing env** — the helper short-circuits to `error: 'unconfigured'` BEFORE attempting to instantiate the provider class (which would call `getApiKey()` and throw).
1895
+ - **Lightweight** — 9 of the 12 verifiable providers run a **zero-token** primitive; the remaining 3 cost ~6–7 tokens per call (~$0.00001 at typical rates).
1896
+ - **Cancellable** — pass `opts.signal` (any `AbortSignal`); the helper distinguishes timeout vs parent-abort in the result.
1897
+ - **Key redaction** — `result.message` redacts `sk-...` patterns before being surfaced, so an upstream error body that echoes the submitted key won't leak the fragment into a UI log or display.
1898
+
1899
+ ### How the strategy is chosen
1900
+
1901
+ Each provider has one `verifyStrategy` value baked into `provider-capabilities.json`. Three primitives, picked per-provider empirically:
1902
+
1903
+ | Strategy | What runs | Cost | Used by built-ins |
1904
+ |---|---|---|---|
1905
+ | `count-tokens` | `client.messages.countTokens({ messages: [{role:'user',content:'hi'}] })` | 0 token | `anthropic`, `zhipu-coding`, `kimi-code`, `minimax-coding`, `ark-coding` |
1906
+ | `models-list` | `client.models.list()` | 0 token | `openai`, `deepseek`, `kimi`, `qwen` |
1907
+ | `minimal-message` | `chat.completions.create({max_tokens:1, content:'hi'})` (or Anthropic equivalent) | ~6–7 token | `zhipu`, `mimo`, `mimo-coding` |
1908
+ | `unsupported` | nothing — short-circuits | — | `gemini-cli`, `codex-cli` (cli-bridge: credentials live in CLI binary) |
1909
+
1910
+ `models-list` is NOT used as a universal default because (a) some providers' `/v1/models` is publicly accessible (so a bad key returns 200 — false positive), and (b) some compat layers don't implement it (404) or 401 even for valid keys (false negative). The 2026-05-28 provider probe matrix captured these empirically (12 providers at the time; 15 built-in aliases as of 2026-06-28); opencode's `setup-recording-env.ts` makes the same per-provider decision across its 20+ providers.
1911
+
1912
+ ### Custom providers
1913
+
1914
+ Custom providers (`registerCustomProviders` / `~/.kodax/config.json`) inherit the verify primitive from their base class. The strategy default is derived from `protocol`:
1915
+
1916
+ - `protocol: 'anthropic'` → defaults to `count-tokens`
1917
+ - `protocol: 'openai'` → defaults to `models-list`
1918
+
1919
+ Override with explicit `verifyStrategy` when the upstream needs a different primitive (e.g. an openai-compat gateway whose `/v1/models` is public — set `verifyStrategy: 'minimal-message'`):
1920
+
1921
+ ```typescript
1922
+ registerCustomProviders([{
1923
+ name: 'my-gateway',
1924
+ protocol: 'openai',
1925
+ baseUrl: 'https://api.example.com/v1',
1926
+ apiKeyEnv: 'MY_GATEWAY_KEY',
1927
+ model: 'gpt-4-mini',
1928
+ verifyStrategy: 'minimal-message', // optional override
1929
+ }]);
1930
+ ```
1931
+
1932
+ The validator rejects illegal combinations:
1933
+ - `protocol: 'openai'` + `verifyStrategy: 'count-tokens'` → throws (OpenAI protocol has no count_tokens endpoint).
1934
+
1935
+ ### Model listing — `listProviderModels(name)`
1936
+
1937
+ Separate API for "model picker" UIs. Returns the static model list KodaX maintains in `provider-capabilities.json` (or the custom provider's `models` field). Always `source: 'static'` in v0.7.45 — KodaX's curated list is more reliable than upstream `/v1/models` (which is noisy, includes deprecated entries, or — in zhipu's case — is publicly served regardless of auth).
1938
+
1939
+ ```typescript
1940
+ import { listProviderModels } from '@kodax-ai/kodax/llm';
1941
+
1942
+ const r = await listProviderModels('ark-coding');
1943
+ if (r.ok) {
1944
+ // r.models is e.g. ['glm-5.1', 'glm-4.7', 'kimi-k2.6', ...]
1945
+ // r.source is 'static'; durationMs is 0 (no wire call)
1946
+ showModelPicker(r.models);
1947
+ }
1948
+ ```
1949
+
1950
+ Cli-bridge providers (`gemini-cli`, `codex-cli`) return their CLI binary's known model list — filled at SDK load via `cli-bridge-models.ts`.
1951
+
1952
+ ### Reference
1953
+
1954
+ - Source: `packages/llm/src/providers/verify-credential.ts` (orchestrator + classifier) + `verify-credential.test.ts` (19 unit tests) + `verify-credential-integration.test.ts` (10 real-key tests, gated on `KODAX_INTEGRATION_TEST=1`).
1955
+ - Data: `packages/llm/src/providers/provider-capabilities.json` `verifyStrategy` field per provider.
1956
+ - Design notes + probe matrix: [docs/features/v0.7.45.md FEATURE_216](features/v0.7.45.md#feature_216-provider-credential-verification-api).
1957
+
1958
+ ---
1959
+
1960
+ ## 13. Inject your product's manual — `selfManual` (FEATURE_221, v0.7.47)
1961
+
1962
+ ### Why
1963
+
1964
+ KodaX has a built-in self-knowledge manual + a read-only `kodax_manual` tool: when a user asks how to use / configure / troubleshoot KodaX, the model looks it up instead of guessing (and instead of mixing in Claude Code / Codex knowledge). If you embed KodaX in your own product (say **KodaX-Space**), your users ask about **your product** — and by default they'd get KodaX's internal manual. `selfManual` lets you inject your product's manual so those questions are answered correctly and on-brand.
1965
+
1966
+ ### Shape
1967
+
1968
+ ```ts
1969
+ import { runKodaX, type KodaXManualTopicInput } from '@kodax-ai/coding';
1970
+
1971
+ const SPACE_TOPICS: KodaXManualTopicInput[] = [
1972
+ { id: 'overview', title: 'KodaX-Space', summary: 'What KodaX-Space is.', body: 'A desktop coding app built on KodaX.' },
1973
+ { id: 'settings', title: 'KodaX-Space Settings', summary: 'Configure KodaX-Space.', body: 'Open Settings → Providers …', aliases: ['配置'] },
1974
+ ];
1975
+
1976
+ runKodaX({
1977
+ /* …your usual config… */
1978
+ selfManual: {
1979
+ productName: 'KodaX-Space', // re-brands the routing rule + every answer's scope anchor
1980
+ topics: SPACE_TOPICS, // extend the KodaX base; same id overrides a base topic
1981
+ },
1982
+ });
1983
+ ```
1984
+
1985
+ ### Semantics
1986
+
1987
+ - **Extend, not replace.** Your topics are merged on top of KodaX's base topics (same `id` overrides, new `id` adds). So a KodaX-Space user can ask about KodaX-Space *and* about the underlying provider/config (KodaX base topics).
1988
+ - **`productName` re-brands the prose**, not the tool. The routing rule and each answer's anti-confusion anchor say your product name; the tool stays `kodax_manual` (the model doesn't care about the tool name).
1989
+ - **Still tool-on-demand + bounded.** Nothing big is injected into the prompt — only the ≤250-token routing rule. Topics live in the registry and are returned one at a time when the model calls the tool, each capped at 4 KB. Drift-guarding your own topics (e.g. not referencing a removed setting) is your responsibility.
1990
+
1991
+ ### White-labeling further — `baseTopics` (v0.7.58, FEATURE_221)
1992
+
1993
+ By default your `topics` **extend** the KodaX base manual. v0.7.58 adds
1994
+ `selfManual.baseTopics` to control which base topics are present underneath:
1995
+
1996
+ - **omit** — all base topics (default; byte-identical to pre-v0.7.58).
1997
+ - **`[]`** — none: a full white-label replace where only your topics exist, so the
1998
+ model never surfaces a KodaX-branded mechanism topic.
1999
+ - **explicit subset** — seed only the base topic ids you name.
2000
+
2001
+ `KODAX_UNDERLYING_CAPABILITY_TOPICS` (exported) is the recommended mechanism-topic
2002
+ subset a product built on KodaX should keep even in a full replace — so your users
2003
+ still get correct answers about the underlying engine (providers / config /
2004
+ permissions / tools / skills / extensions / mcp / repo-intelligence / sessions /
2005
+ sdk / custom-providers) without the KodaX brand:
2006
+
2007
+ ```ts
2008
+ import {
2009
+ runKodaX,
2010
+ KODAX_UNDERLYING_CAPABILITY_TOPICS,
2011
+ MANUAL_REGISTRY,
2012
+ } from '@kodax-ai/coding';
2013
+
2014
+ runKodaX({
2015
+ selfManual: {
2016
+ productName: 'KodaX-Space',
2017
+ topics: SPACE_TOPICS,
2018
+ baseTopics: [...KODAX_UNDERLYING_CAPABILITY_TOPICS], // keep engine topics, drop KodaX-branded ones
2019
+ },
2020
+ });
2021
+ ```
2022
+
2023
+ `MANUAL_REGISTRY` (exported, keyed by `KodaXManualTopicId`) lets you read the base
2024
+ topic bodies at build time — e.g. to re-word them under your own brand.
2025
+
2026
+ ### Topic shape (`KodaXManualTopicInput`)
2027
+
2028
+ `{ id, title, summary, body }` required; `aliases?`, `nextTopics?`, `sources?` optional. Keep `body` short (a few lines) — it is a bounded on-demand answer, not a document.
2029
+
2030
+ ### Reference
2031
+
2032
+ - Types/exports: `KodaXManualTopicInput`, `KodaXSelfManualConfig`, `ResolveKodaXManualOptions`, `buildSelfKnowledgeRoutingRule` from `@kodax-ai/coding`.
2033
+ - Design: [docs/features/v0.7.47.md FEATURE_221](features/v0.7.47.md#feature_221-injectable-self-manual-for-sdk-consumers).
2034
+
2035
+ ---
2036
+
2037
+ ## 14. Media input artifacts — `@kodax-ai/kodax/media` (FEATURE_239, v0.7.56)
2038
+
2039
+ ### Why
2040
+
2041
+ Host apps such as KodaX Space own paste/drop UI, sandbox storage, and path
2042
+ authorization, but they should not import REPL-private files to normalize images
2043
+ or construct `runKodaX` artifacts. `@kodax-ai/kodax/media` is backed by the
2044
+ agent-layer `@kodax-ai/agent/media` implementation in v0.7.57, because input
2045
+ artifacts are an agent capability rather than a coding-only concept.
2046
+ `@kodax-ai/coding/media` remains as a compatibility re-export for existing
2047
+ source consumers.
2048
+
2049
+ ### Quick start
2050
+
2051
+ ```ts
2052
+ import {
2053
+ createFileArtifactFromPath,
2054
+ createImageArtifactFromPath,
2055
+ createVideoArtifactFromPath,
2056
+ enqueueWithArtifacts,
2057
+ getModelInputCapabilities,
2058
+ readAndNormalizeClipboardImage,
2059
+ validateInputArtifactsForModel,
2060
+ } from '@kodax-ai/kodax/media';
2061
+
2062
+ const image = await readAndNormalizeClipboardImage();
2063
+ if (!image) {
2064
+ // Clipboard did not provide a native image fallback. Continue normal text paste.
2065
+ return;
2066
+ }
2067
+
2068
+ const stored = await spaceImageStore.write({
2069
+ bytes: image.buffer,
2070
+ mediaType: image.mediaType,
2071
+ });
2072
+
2073
+ const artifact = createImageArtifactFromPath(stored.path, {
2074
+ mediaType: image.mediaType,
2075
+ source: 'clipboard',
2076
+ description: 'Clipboard image',
2077
+ });
2078
+
2079
+ validateInputArtifactsForModel([artifact], {
2080
+ provider: selectedProvider,
2081
+ model: selectedModel,
2082
+ });
2083
+
2084
+ // Pass the artifacts through `runKodaX` (or the `KodaXClient` constructor) —
2085
+ // `context.inputArtifacts` is the public entry point. `KodaXClient.send`
2086
+ // takes only a prompt string, so per-call artifacts go through `runKodaX`.
2087
+ import { runKodaX } from '@kodax-ai/kodax/coding';
2088
+
2089
+ await runKodaX(
2090
+ {
2091
+ provider: selectedProvider,
2092
+ model: selectedModel,
2093
+ context: { inputArtifacts: [artifact] },
2094
+ },
2095
+ promptText,
2096
+ );
2097
+ ```
2098
+
2099
+ ### Capability query
2100
+
2101
+ Use provider and model together. The same model name behind a gateway route is
2102
+ not assumed to support media unless that route is verified.
2103
+
2104
+ ```ts
2105
+ const caps = getModelInputCapabilities({
2106
+ provider: 'minimax-coding',
2107
+ model: 'MiniMax-M3',
2108
+ });
2109
+
2110
+ if (caps.image.sdkSupported) {
2111
+ enableImageDropZone();
2112
+ }
2113
+
2114
+ if (caps.video.status === 'provider-native-unwired') {
2115
+ showVideoComingSoonCopy();
2116
+ }
2117
+ ```
2118
+
2119
+ v0.7.57 can send image artifacts. File and video artifact shapes are stable, but
2120
+ the SDK runtime does not serialize them yet. Known native-video models report
2121
+ `video.status = 'provider-native-unwired'` so hosts can show accurate UI without
2122
+ enabling a send path KodaX cannot serialize yet.
2123
+
2124
+ ### Artifact contract
2125
+
2126
+ `KodaXInputArtifact` is a stable union:
2127
+
2128
+ ```ts
2129
+ type KodaXInputArtifact =
2130
+ | { kind: 'image'; path: string; mediaType?: KodaXImageMediaType; source?: KodaXInputArtifactSource; description?: string }
2131
+ | { kind: 'file'; path: string; mediaType?: string; mimeType?: string; name?: string; source?: KodaXInputArtifactSource; description?: string }
2132
+ | { kind: 'video'; path: string; mediaType: KodaXVideoMediaType; name?: string; source?: KodaXInputArtifactSource; description?: string };
2133
+ ```
2134
+
2135
+ Use `createFileArtifactFromPath()` for stable file metadata and
2136
+ `createVideoArtifactFromPath()` when a video path has a supported media type
2137
+ (`mp4`, `mpeg`, `mov`, `avi`, `flv`, `webm`, `wmv`, `3gp`). Video construction
2138
+ throws `KodaXMediaError('UNSUPPORTED_MEDIA_TYPE')` if the type cannot be
2139
+ inferred or supplied.
2140
+
2141
+ ### File/video downgrade strategy
2142
+
2143
+ `getModelInputCapabilities()` distinguishes native provider support from SDK
2144
+ runtime support:
2145
+
2146
+ - `image.status === 'supported'`: SDK can send image artifacts.
2147
+ - `video.status === 'provider-native-unwired'`: the selected provider/model is
2148
+ native-video capable, but KodaX SDK does not serialize video artifacts yet.
2149
+ - `file.status === 'unsupported'`: file artifacts are contract-stable, but KodaX
2150
+ SDK does not upload or extract files yet.
2151
+
2152
+ `validateInputArtifactsForModel()` enforces that policy before provider send.
2153
+ Hosts should use the thrown `KodaXMediaError.code` and `detail` to disable send
2154
+ or show downgrade UI. If Space wants to support files before SDK runtime wiring,
2155
+ Space should perform its own extraction and include the extracted text in the
2156
+ prompt rather than passing the file artifact through as sendable media.
2157
+
2158
+ ### Queued follow-ups with artifacts
2159
+
2160
+ For streaming follow-ups, use `enqueueWithArtifacts()` instead of the raw
2161
+ message queue:
2162
+
2163
+ ```ts
2164
+ enqueueWithArtifacts({
2165
+ provider: selectedProvider,
2166
+ model: selectedModel,
2167
+ content: followupText,
2168
+ inputArtifacts: [artifact],
2169
+ });
2170
+ ```
2171
+
2172
+ The helper validates first and then stores `inputArtifacts` on the queued prompt.
2173
+ Queued image follow-ups are rebuilt as multimodal content blocks on the next
2174
+ runner turn. Unsupported file/video attachments are rejected before enqueueing.
2175
+
2176
+ ### Boundaries
2177
+
2178
+ - `readAndNormalizeClipboardImage()` returns `null` when there is no clipboard
2179
+ image fallback; thrown `KodaXMediaError` values are stable enough for host copy.
2180
+ - Direct image path artifacts preserve `image/png`, `image/jpeg`, `image/webp`,
2181
+ and `image/gif`; clipboard normalization emits static PNG/JPEG bytes and may
2182
+ flatten animated GIFs before artifact creation. `image/gif` capability means
2183
+ SDK can pass the bytes and media type; provider animation semantics vary
2184
+ (for example, first-frame-only or non-animated GIF handling).
2185
+ - `persistImageAsBlock()` is a convenience helper. Embedded hosts should usually
2186
+ pass `directory` or store bytes in their own sandbox before constructing an
2187
+ artifact path.
2188
+ - `validateInputArtifactsForModel()` is pure shape/model validation. It does not
2189
+ probe host-owned sandbox paths.
2190
+
2191
+ ### Reference
2192
+
2193
+ - Public SDK entry: `src/sdk-media.ts`.
2194
+ - Shared implementation: `packages/agent/src/media/`.
2195
+ - Compatibility source re-export: `packages/coding/src/media/`.
2196
+ - Design: [docs/features/v0.7.56.md FEATURE_239](features/v0.7.56.md#feature_239-sdk-multimodal-input--clipboard-image-public-api).
2197
+
2198
+ ---
2199
+
2200
+ ## 15. Space v0.7.57 follow-up ledger
2201
+
2202
+ These are the remaining SDK-consumer integration decisions reported after the
2203
+ v0.7.57 source review. They are not all KodaX core regressions; most are Space
2204
+ UI/API follow-ups that should consume the SDK contracts already exposed here.
2205
+
2206
+ - **Custom provider reasoning form**: Space should expose the v0.7.57 custom
2207
+ provider shape `reasoning: { efforts, default }` or `"none"` instead of only
2208
+ legacy reasoning-mode inputs. Keep using the SDK validator from
2209
+ `@kodax-ai/kodax/llm` so Space does not maintain a parallel schema.
2210
+ - **Effort selector**: Space should build effort choices from
2211
+ `resolveModelCapabilities(provider, model)?.reasoningProfile.supportedEfforts`
2212
+ and `defaultEffort`. A fixed five-option selector will miss provider-specific
2213
+ values such as `xhigh`, `max`, or custom-provider effort names.
2214
+ - **Repo-intelligence prewarm**: `prewarmRepoIntelligenceCaches()` is currently a
2215
+ best-effort warmup call, not a progress/completion contract. Hosts can call it
2216
+ opportunistically; if Space needs visible progress or a completed state, the
2217
+ next SDK step should be a small handle/result API rather than inferring status
2218
+ from cache side effects.
2219
+ - **Relationship scan**: `relationship_scan` is a v0.7.57 agent/tool capability.
2220
+ It is intentionally model-facing today. Space can decide separately whether it
2221
+ deserves a top-level UI entry or remains available through normal agent turns.
2222
+ - **Quick Ask / `sideQuery`**: `sideQuery` is exported from
2223
+ `@kodax-ai/kodax/llm`, so the capability ledger can move from blocked to
2224
+ partial. Migrating Space Quick Ask still needs an application-level decision
2225
+ about transcript promotion and history semantics, because `sideQuery` is an
2226
+ isolated text-only one-shot call rather than a chat-session append.
2227
+
2228
+ ---
2229
+
2230
+ ## 16. SDK agent-profile surface — `KodaXAgentProfile` (FEATURE_247, v0.7.58)
2231
+
2232
+ ### Why
2233
+
2234
+ An SDK embedder (e.g. **KodaX-Space Partner**) often needs to run KodaX under a
2235
+ named product persona — its own identity + instructions, a narrowed tool surface,
2236
+ and a default verification standard — without forking the agent. `KodaXAgentProfile`
2237
+ provides this as one **opaque, profile-gated** object on `options.context`. With no
2238
+ `agentProfile` set the default Coding Agent is **byte-identical**; every path below
2239
+ is a no-op.
2240
+
2241
+ ### Shape
2242
+
2243
+ ```ts
2244
+ runKodaX({
2245
+ /* …your usual config… */
2246
+ context: {
2247
+ agentProfile: {
2248
+ surface: 'partner', // opaque label ('code' | 'partner' | …)
2249
+ name: 'KodaX-Space Partner', // opaque display name
2250
+ instructions: '…house rules injected into the AMA/AMAW Worker role prompt…',
2251
+ verification: { /* KodaXTaskVerificationContract — profile-default standard */ },
2252
+ },
2253
+ // R2 — narrow the model-visible tool list (applied on top of excludeTools):
2254
+ toolVisibilityPolicy: (tool) => tool !== 'web_search',
2255
+ },
2256
+ });
2257
+ ```
2258
+
2259
+ ### What each field / companion gates
2260
+
2261
+ - **R1 — identity + instructions.** `agentProfile.instructions` is prepended to the
2262
+ AMA/AMAW Worker role prompt; the SA path uses `context.systemPromptOverride`
2263
+ (mapped from `instructions` by `startKodaX`), so a profile behaves consistently
2264
+ across both execution modes.
2265
+ - **R2 — `context.toolVisibilityPolicy`.** A predicate applied when the
2266
+ model-visible tool list is built (in addition to `excludeTools`); tools it returns
2267
+ `false` for are hidden from the model.
2268
+ - **R3 — `agentProfile.verification`.** A profile-default `KodaXTaskVerificationContract`
2269
+ merged with per-task `context.taskVerification` (per-task fields win) before it
2270
+ reaches the Sidecar Verifier; each verdict is attributed to the profile.
2271
+ - **R4 — `KodaXEvents.onEffectiveConfig`.** Reports the effective agentMode / tool
2272
+ scope / verification / resolved verifier at run start, so a host can reflect what
2273
+ the profile actually resolved to.
2274
+ - **R5 — metadata across `fork()`.** Structured `profile` + `runtimeInfo` metadata
2275
+ ride on results and are inherited by forked sessions.
2276
+ - **R6 — `compactSession(id, options)`** from `@kodax-ai/kodax/session` — an
2277
+ imperative session compaction the host can trigger directly.
2278
+ - **R7 / R8 — attribution.** Session / profile / toolCall attribution is threaded
2279
+ into the tool execution context and onto inline-workflow process events + AMA tool
2280
+ events.
2281
+ - **R9 — `reads-network` side-effect class** (`isToolNetworkRead`) tags read-only
2282
+ network tools (`web_search`, MCP read / prompt) so a profile can allow network
2283
+ reads without granting mutation.
2284
+
2285
+ ### Reference
2286
+
2287
+ - Types: `KodaXAgentProfile`, `KodaXToolVisibilityPolicy`, `KodaXEffectiveTaskConfig`
2288
+ from `@kodax-ai/coding`; `compactSession` from `@kodax-ai/kodax/session`.
2289
+ - Design: [docs/features/v0.7.58.md](features/v0.7.58.md) FEATURE_247.
2290
+
2291
+ ---
2292
+
2293
+ ## 17. Runtime SDK, Worker isolation, and local daemon (FEATURE_253-FEATURE_257)
2294
+
2295
+ `@kodax-ai/kodax/runtime` is the stable host-facing runtime facade for
2296
+ applications that want KodaX as a substrate instead of only as a terminal CLI.
2297
+ It wraps the same coding/session engine used by the REPL and exposes it through
2298
+ one interface in three deployment shapes:
2299
+
2300
+ - **embedded / inline**: in-process runtime owned by the caller;
2301
+ - **embedded / worker**: private caller-owned runtime in a disposable V8 Worker;
2302
+ - **daemon**: local-only runtime owner reached through a named pipe on Windows or
2303
+ a Unix domain socket on Linux/macOS.
2304
+
2305
+ The daemon is not a separate product engine. It hosts the embedded runtime behind
2306
+ a process boundary, so REPL, Space, IDE adapters, ACP, and custom SDK clients can
2307
+ share the same profile runtime without reimplementing sessions, permissions,
2308
+ events, config, MCP, catalogs, artifacts, or diagnostics.
2309
+
2310
+ ### Which shape to use
2311
+
2312
+ | Host scenario | Recommended shape | Why |
2313
+ |---|---|---|
2314
+ | Unit tests, one-off scripts, short-lived SDK tools | `createKodaXRuntime()` | No daemon lifecycle; easiest cleanup. |
2315
+ | A single app owns all KodaX state in one process | `createKodaXRuntime({ mode: 'embedded' })` | Direct in-process calls and no IPC. |
2316
+ | A single app needs private state plus hard V8 disposal | `createKodaXRuntime({ mode: 'embedded', isolation: 'worker' })` | Same services over MessagePort; `close()` escalates to Worker termination. |
2317
+ | REPL + Space + IDE should share sessions/status/permissions | `createKodaXRuntime({ mode: 'daemon' })` | Starts or reuses the local profile daemon. |
2318
+ | Attach to an already-started daemon only | `connectKodaXRuntime({ profile, homeDir })` | Attach-only by default; fails if no daemon is ready. |
2319
+ | Test/CI isolated daemon namespace | pass `homeDir` and `profile` | Keeps state/config/sessions out of the user's home daemon. |
2320
+
2321
+ ### Public construction contract
2322
+
2323
+ Import the Runtime facade from the dedicated subpath:
2324
+
2325
+ ```ts
2326
+ import {
2327
+ createKodaXRuntime,
2328
+ connectKodaXRuntime,
2329
+ type CreateKodaXRuntimeOptions,
2330
+ type KodaXRuntime,
2331
+ } from '@kodax-ai/kodax/runtime';
2332
+ ```
2333
+
2334
+ The important creation options are:
2335
+
2336
+ | Option | Default | Contract |
2337
+ |---|---|---|
2338
+ | `mode` | `'embedded'` | Chooses private ownership or a shared daemon process. |
2339
+ | `isolation` | `'inline'` | Embedded-only. `'worker'` creates a private Runtime Worker; daemon rejects any explicit isolation because it is already process-isolated. |
2340
+ | `worker.resourceLimits` | unset | Optional V8 heap/stack limits; requires `isolation: 'worker'`. |
2341
+ | `worker.shutdownTimeoutMs` | `2000` | Grace before the parent terminates the Runtime Worker. |
2342
+ | `requirements.hardDispose` | `false` | Rejects inline and daemon forms; prevents an accidental weaker ownership form. |
2343
+ | `homeDir` | OS user home | Root for `.kodax` config/state and default sessions. Use a private value in tests. |
2344
+ | `profile` | `'default'` | Daemon uniqueness and runtime configuration namespace. |
2345
+ | `sessionsDir` | `<homeDir>/.kodax/sessions` | Explicit session storage override. |
2346
+ | `daemonStartupTimeoutMs` | `60000` | Total cold-start/concurrent-owner wait budget. |
2347
+ | `daemonConnectTimeoutMs` | `2000` | Per-socket connection timeout. |
2348
+ | `autoStartDaemon` | conditional | For `createKodaXRuntime({mode:'daemon'})`, true only when no explicit endpoint/transport is supplied. |
2349
+ | `externalAgents` | unset | Host-installed executor factories, dispatch policy, optional credential/artifact policies, and default dispatch context. Inline owner only; see §18. |
2350
+ | `requirements.externalAgents` | `false` | Reject a Runtime/daemon connection that does not advertise an installed external-agent plane. |
2351
+
2352
+ KodaX rejects contradictory options. Worker settings without Worker isolation,
2353
+ `requirements.hardDispose` on inline/daemon forms, and any explicit isolation
2354
+ on daemon mode are errors. Options are never silently ignored to select a
2355
+ weaker isolation form.
2356
+
2357
+ ### Basic embedded usage
2358
+
2359
+ ```ts
2360
+ import { createKodaXRuntime } from '@kodax-ai/kodax/runtime';
2361
+
2362
+ const runtime = await createKodaXRuntime({
2363
+ mode: 'embedded',
2364
+ homeDir: '/tmp/my-host-kodax',
2365
+ defaultProvider: 'zai-coding',
2366
+ });
2367
+
2368
+ try {
2369
+ const session = await runtime.sessions.create({
2370
+ title: 'SDK embedded session',
2371
+ projectPath: process.cwd(),
2372
+ surface: 'my-host',
2373
+ });
2374
+ const handle = await runtime.runs.start({
2375
+ sessionId: session.id,
2376
+ prompt: 'Read package.json and summarize this project.',
2377
+ });
2378
+ const result = await handle.result;
2379
+ console.log(result.phase);
2380
+ } finally {
2381
+ await runtime.close();
2382
+ }
2383
+ ```
2384
+
2385
+ ### Basic daemon usage
2386
+
2387
+ ```ts
2388
+ import { createKodaXRuntime, connectKodaXRuntime } from '@kodax-ai/kodax/runtime';
2389
+
2390
+ const replLikeClient = await createKodaXRuntime({
2391
+ mode: 'daemon',
2392
+ profile: 'default',
2393
+ clientInfo: { name: 'my-repl', title: 'My REPL', version: '1.0.0' },
2394
+ capabilities: {
2395
+ richEvents: true,
2396
+ permissionPrompts: true,
2397
+ contextDiagnostics: true,
2398
+ },
2399
+ });
2400
+
2401
+ const spaceLikeClient = await connectKodaXRuntime({
2402
+ profile: 'default',
2403
+ clientInfo: { name: 'my-space', title: 'My Space', version: '1.0.0' },
2404
+ capabilities: {
2405
+ richEvents: true,
2406
+ permissionPrompts: true,
2407
+ contextDiagnostics: true,
2408
+ },
2409
+ });
2410
+
2411
+ try {
2412
+ console.log(replLikeClient.identity.runtimeId === spaceLikeClient.identity.runtimeId);
2413
+ } finally {
2414
+ await spaceLikeClient.close();
2415
+ await replLikeClient.close();
2416
+ }
2417
+ ```
2418
+
2419
+ `createKodaXRuntime({ mode: 'daemon' })` is the high-level convenience API: when
2420
+ no explicit `daemonEndpoint` or `daemonTransport` is supplied it starts or reuses
2421
+ the local profile daemon. `connectKodaXRuntime()` is attach-only unless
2422
+ `autoStart: true` is passed.
2423
+
2424
+ SDK auto-start allows `daemonStartupTimeoutMs` (default 60 seconds) and
2425
+ `daemonConnectTimeoutMs`. The longer startup budget covers cold machines and
2426
+ concurrent test/desktop startup without weakening PID, endpoint, token, or
2427
+ runtime-identity validation.
2428
+
2429
+ ### Worker-hosted embedded usage
2430
+
2431
+ ```ts
2432
+ import { createKodaXRuntime } from '@kodax-ai/kodax/runtime';
2433
+
2434
+ const runtime = await createKodaXRuntime({
2435
+ mode: 'embedded',
2436
+ isolation: 'worker',
2437
+ worker: {
2438
+ resourceLimits: { maxOldGenerationSizeMb: 1024 },
2439
+ shutdownTimeoutMs: 2000,
2440
+ },
2441
+ });
2442
+
2443
+ try {
2444
+ console.log(runtime.identity.isolation); // 'worker'
2445
+ console.log(runtime.identity.workerThreadId); // Node Worker thread id
2446
+ // runtime.sessions/runs/events/... are identical to inline and daemon.
2447
+ } finally {
2448
+ await runtime.close();
2449
+ }
2450
+ ```
2451
+
2452
+ `mode` describes ownership and sharing; `isolation` describes where a private
2453
+ embedded owner executes. Inline is the lowest-latency default. Worker is useful
2454
+ for Electron/Space-style hosts that need private state and deterministic V8
2455
+ disposal. Daemon is for durable multi-client sharing and already uses an OS
2456
+ process, so daemon + worker is rejected.
2457
+
2458
+ Worker `resourceLimits` bound parts of the V8 heap only. They do not cover every
2459
+ kind of native/external memory and do not make Node code safe to treat as
2460
+ untrusted. Worker isolation is a fault boundary, not a security sandbox.
2461
+
2462
+ Callers that cannot accept a silent fallback can require the capability:
2463
+
2464
+ ```ts
2465
+ await createKodaXRuntime({
2466
+ mode: 'embedded',
2467
+ isolation: 'worker',
2468
+ requirements: { hardDispose: true },
2469
+ });
2470
+ ```
2471
+
2472
+ Daemon hosts advertise `hardDispose: false`; Worker hosts advertise true.
2473
+
2474
+ ### Close, abort, and ownership semantics
2475
+
2476
+ The same method name deliberately has deployment-specific ownership effects:
2477
+
2478
+ | Form | `runtime.close()` | Run abort | After owner crash/termination |
2479
+ |---|---|---|---|
2480
+ | embedded / inline | Cancels owned runs and permissions and closes private runtime state. It cannot recover a host event loop blocked by arbitrary inline code. | `runs.abort(runId)` settles the Runtime handle and forwards cancellation to coding. | The embedding process owns recovery. |
2481
+ | embedded / Worker | Requests shutdown, waits up to `shutdownTimeoutMs`, then always terminates the Worker. | Same Runtime abort API; closing the Runtime is the hard-disposal escalation for the whole isolate. | Pending transport requests reject; create a new Runtime explicitly. |
2482
+ | daemon client | Detaches only this client transport. Other REPL/Space/SDK clients and runs remain owned by the daemon. | Aborts only the addressed run. | The client connection rejects; reconnect explicitly after the daemon is healthy. |
2483
+
2484
+ `close()` is idempotent. It is not a shared-daemon stop command. Use
2485
+ `kodax daemon stop`, `kodax daemon restart`, or an authenticated low-level
2486
+ `runtime.shutdown` request for administrative shutdown. KodaX does not
2487
+ automatically retry or replay an in-flight run after a Worker/daemon owner dies,
2488
+ because provider and tool side effects may already have happened.
2489
+
2490
+ ### Daemon ownership and state
2491
+
2492
+ Daemon ownership is scoped by `homeDir + profile`.
2493
+
2494
+ - Default `homeDir` is the OS user home directory.
2495
+ - Default `profile` is `default`.
2496
+ - State lives under `.kodax/runtime/daemon/{profile}/`.
2497
+ - Default runtime session storage is also scoped under `<homeDir>/.kodax/sessions`
2498
+ when `sessionsDir` is omitted.
2499
+ - Windows uses a named pipe; Linux/macOS use a Unix domain socket.
2500
+ - The daemon opens no public TCP listener.
2501
+
2502
+ For a given profile, one owner wins an atomic lock. Concurrent starters wait for
2503
+ the winner and connect once it is ready. Stale state is cleaned only after pid,
2504
+ endpoint, token, and runtime identity checks. If ownership cannot be verified,
2505
+ KodaX reports the daemon as unhealthy instead of killing an arbitrary process.
2506
+ SDK auto-start launches a detached `kodax daemon serve` process; it never treats
2507
+ an in-process socket listener as daemon mode. Closing the SDK client detaches
2508
+ without stopping that shared process. Use `kodax daemon stop` or the explicit
2509
+ runtime shutdown protocol to stop the owner.
2510
+
2511
+ ### Daemon startup, conflict, and recovery behavior
2512
+
2513
+ REPL, Space, and SDK callers using the same resolved `homeDir + profile` target
2514
+ the same daemon. Simultaneous startup is expected: candidates race only for the
2515
+ atomic owner lock, then non-owners wait for and attach to the verified winner.
2516
+ A client never owns a daemon merely because it caused auto-start.
2517
+
2518
+ On abnormal exit, the next start validates the saved PID, endpoint, token, and
2519
+ runtime id before removing stale state. KodaX will not kill a process whose
2520
+ ownership cannot be proven. Persisted queued/running/waiting-permission runs are
2521
+ recovered as `interrupted` with a runtime event; they are not resumed
2522
+ automatically. Session and bounded event records remain available for explicit
2523
+ reconnect/retry decisions.
2524
+
2525
+ Operational guidance:
2526
+
2527
+ - use one stable profile for cooperating desktop clients;
2528
+ - use a separate `homeDir` or profile for tests, previews, and incompatible
2529
+ configurations;
2530
+ - test harnesses that auto-start a process daemon must send authenticated
2531
+ `runtime.shutdown` (or run `kodax daemon stop --home <dir> --profile <name>`)
2532
+ before deleting their temporary home; `runtime.close()` only detaches;
2533
+ - query `kodax daemon status --json` before deciding to restart;
2534
+ - inspect `kodax daemon logs --lines 100` when startup times out;
2535
+ - custom endpoints and injected transports are attach-only unless the caller
2536
+ implements their owner lifecycle explicitly.
2537
+
2538
+ The matching CLI surface is:
2539
+
2540
+ ```bash
2541
+ kodax daemon start --profile default
2542
+ kodax daemon status --profile default --json
2543
+ kodax daemon logs --profile default --lines 100
2544
+ kodax daemon stop --profile default --json
2545
+ kodax daemon restart --profile default
2546
+ kodax --runtime-mode daemon
2547
+ ```
2548
+
2549
+ Inside the REPL, `/status runtime` reports embedded/daemon mode, profile,
2550
+ runtime id, endpoint/health when applicable, and active/queued counters.
2551
+
2552
+ ### Runtime services
2553
+
2554
+ Every `KodaXRuntime` exposes the same service set in inline, Worker, and daemon mode:
2555
+
2556
+ | Service | Purpose |
2557
+ |---|---|
2558
+ | `identity` | Runtime id, mode, isolation, profile, started time, package version, optional Worker thread id. |
2559
+ | `sessions` | Create/load/list/fork/transcript/settings/notice/rewind/compact/archive/delete. |
2560
+ | `runs` | Start/await/get/list/abort runs; update provider/model/reasoning for supported phases. |
2561
+ | `events` | Subscribe to live events and replay persisted bounded events. |
2562
+ | `permissions` | Request/list/respond to tool permissions across clients. |
2563
+ | `workflows` | Observe workflow process snapshots/events and lifecycle controls. |
2564
+ | `config` | Read/patch/reload daemon or embedded profile config. |
2565
+ | `catalog` | Providers, models, commands, skills, custom providers, extensions. |
2566
+ | `mcp` | MCP server CRUD, validation, reload, and tool catalog listing. |
2567
+ | `artifacts` | Create/get/delete runtime artifact references for file/image/video inputs. |
2568
+ | `status` | Runtime snapshot with sessions, runs, permissions, workflows, and daemon counters. |
2569
+ | `diagnostics` | Latest context-budget and tool-exposure decisions for GUI/debug surfaces. |
2570
+ | `admin.agentRegistrations` | List/upsert/remove redacted external-agent registrations. With no plane, list is empty and mutations fail clearly. |
2571
+ | `agents` | Check `enabled`, list/describe policy-filtered dispatchable agents, and preflight a selected route. |
2572
+ | `agentTasks` | Start/list/get/wait/continue/cancel/reconcile durable external-agent tasks and read their ordered event stream. |
2573
+
2574
+ ### Sessions, runs, and queueing
2575
+
2576
+ Runs are serialized per session and can run concurrently across different
2577
+ sessions. Same-session prompts are FIFO queued. `runs.start()` returns a handle
2578
+ with a `result` promise that always settles on completion, failure, abort, or
2579
+ runtime close.
2580
+
2581
+ ```ts
2582
+ const session = await runtime.sessions.create({ title: 'Shared session' });
2583
+
2584
+ const sub = runtime.events.subscribe({ sessionId: session.id }, (event) => {
2585
+ if (event.type === 'assistant.delta') {
2586
+ // Render streaming text in the host UI.
2587
+ }
2588
+ });
2589
+
2590
+ const artifact = await runtime.artifacts.create({
2591
+ kind: 'image',
2592
+ path: '/tmp/screenshot.png',
2593
+ mediaType: 'image/png',
2594
+ });
2595
+
2596
+ const handle = await runtime.runs.start({
2597
+ sessionId: session.id,
2598
+ input: [
2599
+ { type: 'text', text: 'Review this screenshot.' },
2600
+ { type: 'artifact_ref', artifactId: artifact.id },
2601
+ ],
2602
+ options: {
2603
+ provider: 'zai-coding',
2604
+ effort: 'high',
2605
+ },
2606
+ });
2607
+
2608
+ const result = await handle.result;
2609
+ sub.close();
2610
+ ```
2611
+
2612
+ ### Run options across Worker/daemon boundaries
2613
+
2614
+ `runs.start({ options })` is a DTO boundary in Worker and daemon forms. Do not
2615
+ pass process-local objects such as `extensionRuntime`, callbacks, `AbortSignal`,
2616
+ LSP services, class instances, or cyclic structures. KodaX rejects them before
2617
+ transport instead of silently dropping fields.
2618
+
2619
+ Use the runtime APIs for cross-boundary behavior:
2620
+
2621
+ - cancellation: `runtime.runs.abort(runId)`;
2622
+ - output/progress: `runtime.events.subscribe(...)`;
2623
+ - approval: `runtime.permissions.respond(...)`;
2624
+ - session defaults: `runtime.sessions.updateSettings(...)`;
2625
+ - config/MCP/extensions: configure or reload them in the Runtime owner.
2626
+
2627
+ Host-bound extensions or callbacks that cannot be represented as owner-loaded
2628
+ module/config descriptors require inline embedded mode. There is no fallback to
2629
+ executing those objects in the client process.
2630
+
2631
+ For daemon mode, extension and MCP ownership follows daemon configuration.
2632
+ Configure extensions in the daemon profile and call
2633
+ `runtime.catalog.reloadExtensions()` or the matching config service. A CLI
2634
+ `--extension <path>` is intentionally rejected in daemon mode because that
2635
+ process-local object cannot become part of a durable shared owner. Worker mode
2636
+ has the same DTO rule; use owner-readable config/module descriptors or inline
2637
+ mode for host-created extension objects.
2638
+
2639
+ ### Permissions across clients
2640
+
2641
+ Pending permission requests live in the runtime/daemon, not in one UI client. A
2642
+ Space-style client can subscribe to permission events and answer a request
2643
+ created by a REPL-style client.
2644
+
2645
+ ```ts
2646
+ const sub = runtime.events.subscribe({ type: 'permission.requested' }, (event) => {
2647
+ const payload = event.payload;
2648
+ if (!payload || typeof payload !== 'object' || typeof payload.id !== 'string') {
2649
+ return;
2650
+ }
2651
+ void runtime.permissions.respond(
2652
+ payload.id,
2653
+ { type: 'allow_once' },
2654
+ { runId: payload.runId },
2655
+ );
2656
+ });
2657
+ ```
2658
+
2659
+ Only the first valid response wins. Wrong-run or stale responses are rejected.
2660
+ Abort, runtime close, daemon stop, and timeout reject unresolved permission
2661
+ requests so tool approval promises do not hang forever.
2662
+
2663
+ ### Config, catalogs, MCP, and Space-style admin APIs
2664
+
2665
+ Daemon-connected clients should not edit KodaX config files directly. Use the
2666
+ runtime services instead:
2667
+
2668
+ ```ts
2669
+ await runtime.config.patch({ provider: 'zai-coding', model: 'glm-5.2' });
2670
+ await runtime.config.reload();
2671
+
2672
+ await runtime.catalog.upsertCustomProvider({
2673
+ name: 'my-openai-compatible',
2674
+ protocol: 'openai',
2675
+ baseUrl: 'https://example.com/v1',
2676
+ apiKeyEnv: 'MY_LLM_API_KEY',
2677
+ model: 'my-model',
2678
+ });
2679
+
2680
+ await runtime.mcp.upsertServer('filesystem', {
2681
+ command: 'npx',
2682
+ args: ['-y', '@modelcontextprotocol/server-filesystem', process.cwd()],
2683
+ });
2684
+ await runtime.mcp.reloadServers();
2685
+
2686
+ const commands = await runtime.catalog.commands(process.cwd());
2687
+ const skills = await runtime.catalog.skills({ projectRoot: process.cwd() });
2688
+ ```
2689
+
2690
+ This is the intended path for KodaX Space, IDE adapters, and settings UIs:
2691
+ session defaults go through `sessions.updateSettings()`, one-turn overrides go
2692
+ through `runs.start({ options })`, and daemon/profile config goes through
2693
+ `config`, `catalog`, and `mcp`.
2694
+
2695
+ ### Context optimization and diagnostics
2696
+
2697
+ The runtime carries the Hermes-inspired context-efficiency plane from the coding
2698
+ engine:
2699
+
2700
+ - small-window schema pruning hides non-core deferred tool schemas while keeping
2701
+ bridge discovery resident;
2702
+ - tool-search/describe/call-style bridge semantics keep tools reachable;
2703
+ - repo-intelligence schemas remain discoverable under pressure;
2704
+ - context-aware tool result budgets and compaction pressure events are surfaced
2705
+ as bounded diagnostics.
2706
+
2707
+ Hosts that set `capabilities.contextDiagnostics: true` can read:
2708
+
2709
+ ```ts
2710
+ const budget = await runtime.diagnostics.latestContextBudget({ sessionId });
2711
+ const exposure = await runtime.diagnostics.latestToolExposure({ sessionId });
2712
+ ```
2713
+
2714
+ These diagnostics are designed for status panels and debugging. They should not
2715
+ contain raw sensitive tool input/output.
2716
+
2717
+ ### Protocol schema and versioning
2718
+
2719
+ The runtime subpath also exports daemon protocol metadata for clients that need
2720
+ schema-aware IPC validation:
2721
+
2722
+ ```ts
2723
+ import {
2724
+ KODAX_DAEMON_PROTOCOL,
2725
+ KODAX_DAEMON_PROTOCOL_VERSION,
2726
+ RUNTIME_DAEMON_PROTOCOL_SCHEMA,
2727
+ listRuntimeDaemonSchemaMethods,
2728
+ } from '@kodax-ai/kodax/runtime';
2729
+ ```
2730
+
2731
+ The schema is additive within this patch line. Removing or changing required
2732
+ fields requires a protocol version bump.
2733
+
2734
+ ### Current verification status
2735
+
2736
+ The v0.7.67 release validation covers the runtime migration, the Worker
2737
+ isolation follow-ups delivered ahead of their original v0.7.71/v0.7.72
2738
+ planning slots, and the v0.7.67 external-agent/session additions:
2739
+
2740
+ - complete repository test suite on Node 20 and Node 22;
2741
+ - package builds, Worker sidecar builds, and self-contained
2742
+ `dist/sdk-runtime.d.ts` generation;
2743
+ - runtime/daemon/SDK/ACP/REPL integration tests, including process-distinct SDK
2744
+ auto-start and multi-client sessions/permissions;
2745
+ - Worker Runtime identity, service parity, hard close, capability requirements,
2746
+ and contradictory-option rejection;
2747
+ - constructed-handler reverse tool RPC, abort bridging, CPU-loop termination,
2748
+ respawn, and revoke/dispose queue drainage;
2749
+ - context/tool-exposure eval gate;
2750
+ - embedded and hosted-daemon external-agent catalog/task parity, durable task
2751
+ recovery, policy/credential/artifact gates, and disabled-plane failure;
2752
+ - exact session `surface` filtering plus opaque cursor continuation through the
2753
+ narrow `/session` API, embedded Runtime, and daemon transport;
2754
+ - external fresh npm consumer installation of the `0.7.67` tarball, proving
2755
+ Worker isolation and a distinct daemon PID through the published subpath;
2756
+ - Ubuntu Node 22 Unix-domain-socket daemon gate, including two clients sharing
2757
+ one runtime and cross-client permission resolution.
2758
+
2759
+ The portable manual gates remain in
2760
+ `docs/test-guides/FEATURE_255_v0.7.66_TEST_GUIDE.md`,
2761
+ `FEATURE_256_v0.7.71_TEST_GUIDE.md`, and
2762
+ `FEATURE_257_v0.7.72_TEST_GUIDE.md` for release-machine verification; the latter
2763
+ two filenames retain their original planning slots while their content records
2764
+ the v0.7.66 delivery. v0.7.67 adds
2765
+ `FEATURE_258_v0.7.67_TEST_GUIDE.md`, `FEATURE_259_v0.7.67_TEST_GUIDE.md`, and
2766
+ `FEATURE_261_v0.7.67_TEST_GUIDE.md`. The final GitHub Actions run is recorded in
2767
+ the v0.7.67 release after the release commit is pushed.
2768
+
2769
+ ---
2770
+
2771
+ ## 18. External-agent executor plane (FEATURE_258, v0.7.67)
2772
+
2773
+ FEATURE_258 lets an SDK host register remote agents without teaching the coding
2774
+ runtime a specific A2A, MCP, or HTTP client. The public contracts live in
2775
+ `@kodax-ai/kodax/agent`; the host-facing catalog and task services live on
2776
+ `@kodax-ai/kodax/runtime`.
2777
+
2778
+ ### Ownership rule
2779
+
2780
+ An `AgentExecutorFactory` contains functions, so it cannot cross a Worker or
2781
+ daemon DTO boundary. Install factories where the Runtime owner executes:
2782
+
2783
+ | Desired owner | Supported construction |
2784
+ |---|---|
2785
+ | Private in-process owner | `createKodaXRuntime({ mode: 'embedded', isolation: 'inline', externalAgents })` |
2786
+ | New locally hosted daemon owner | `createKodaXRuntime({ mode: 'daemon', profile: '<unique>', externalAgents })` |
2787
+ | Existing daemon | Configure its owner, then attach with `connectKodaXRuntime({ requirements: { externalAgents: true } })`; a client cannot inject factories. |
2788
+ | Runtime Worker | The Worker owner must install factories itself; passing `externalAgents` from the parent is rejected. |
2789
+
2790
+ When `mode: 'daemon'` and `externalAgents` are supplied, the caller must win a
2791
+ new in-process daemon lease. KodaX rejects an already-running profile instead of
2792
+ silently replacing its executor configuration. Closing that owner facade shuts
2793
+ down the host it created; closing an ordinary attached client only detaches.
2794
+
2795
+ ### Minimal owner and task flow
2796
+
2797
+ The reference executor below is a contract/conformance adapter. Replace it with
2798
+ your own `AgentExecutorFactory` for a real remote protocol.
2799
+
2800
+ ```ts
2801
+ import {
2802
+ createReferenceAgentExecutorFactory,
2803
+ type ExternalAgentRegistration,
2804
+ } from '@kodax-ai/kodax/agent';
2805
+ import { createKodaXRuntime } from '@kodax-ai/kodax/runtime';
2806
+
2807
+ const runtime = await createKodaXRuntime({
2808
+ mode: 'embedded',
2809
+ isolation: 'inline',
2810
+ externalAgents: {
2811
+ factories: [createReferenceAgentExecutorFactory({
2812
+ executorId: 'example-http',
2813
+ protocol: 'http',
2814
+ })],
2815
+ policy: ({ registration, query }) => ({
2816
+ allowed: registration.enabled && query.readOnly === true,
2817
+ reasons: query.readOnly === true ? [] : ['This host allows read-only dispatch only.'],
2818
+ }),
2819
+ defaultContext: { actorId: 'desktop-host' },
2820
+ },
2821
+ });
2822
+
2823
+ const registration: ExternalAgentRegistration = {
2824
+ agentId: 'external:reviewer',
2825
+ displayName: 'Remote Reviewer',
2826
+ enabled: true,
2827
+ executorId: 'example-http',
2828
+ protocol: 'http',
2829
+ configurationRevision: 'reviewer-config-v1',
2830
+ endpointIdentityHash: 'sha256:replace-with-stable-endpoint-identity',
2831
+ skills: ['code-review'],
2832
+ inputModalities: ['text'],
2833
+ outputModalities: ['text'],
2834
+ capabilities: {
2835
+ streaming: 'supported',
2836
+ durableTasks: 'supported',
2837
+ inputRequired: 'conditional',
2838
+ cancellation: 'supported',
2839
+ artifacts: 'unsupported',
2840
+ },
2841
+ effects: { remote: 'read', workspace: 'proposal' },
2842
+ maxConcurrency: 1,
2843
+ };
2844
+
2845
+ try {
2846
+ await runtime.admin.agentRegistrations.upsert(registration);
2847
+
2848
+ const query = {
2849
+ actorId: 'desktop-host',
2850
+ requiredSkills: ['code-review'],
2851
+ readOnly: true,
2852
+ } as const;
2853
+ const available = await runtime.agents.listDispatchable(query);
2854
+ const preflight = await runtime.agents.preflight({
2855
+ agentId: 'external:reviewer',
2856
+ query,
2857
+ expectedConfigurationRevision: registration.configurationRevision,
2858
+ });
2859
+ if (!preflight.ok) throw new Error(preflight.reasons.join('; '));
2860
+
2861
+ const started = await runtime.agentTasks.start({
2862
+ agentId: 'external:reviewer',
2863
+ objective: 'Review the supplied immutable patch and return cited findings.',
2864
+ context: { actorId: 'desktop-host', runId: 'host-run-42' },
2865
+ readOnly: true,
2866
+ requiredSkills: ['code-review'],
2867
+ expectedConfigurationRevision: registration.configurationRevision,
2868
+ });
2869
+ const terminal = await runtime.agentTasks.wait(started.taskId, 60_000);
2870
+ console.log(available, terminal.state, terminal.output, terminal.usage);
2871
+ } finally {
2872
+ await runtime.close();
2873
+ }
2874
+ ```
2875
+
2876
+ `runtime.agents.enabled` is the cheap external-plane feature check. If it is
2877
+ false, catalog queries can still return built-in local agents but no external
2878
+ agents; registration and task lists are empty, while point reads and mutations
2879
+ fail clearly. Set
2880
+ `requirements.externalAgents: true` when absence must abort connection.
2881
+
2882
+ ### Service reference
2883
+
2884
+ | Surface | Methods | Contract |
2885
+ |---|---|---|
2886
+ | `runtime.admin.agentRegistrations` | `list`, `upsert`, `remove` | Durable owner configuration. List results expose `credentialConfigured`, never a credential value. With no plane, `list()` is empty and mutations fail clearly. |
2887
+ | `runtime.agents` | `enabled`, `listDispatchable`, `describe`, `preflight` | Applies health, capability, effect, concurrency, credential-presence, configuration-revision, and host-policy checks before dispatch. |
2888
+ | `runtime.agentTasks` | `start`, `list`, `get`, `events`, `wait`, `sendInput`, `cancel`, `reconcile` | Durable snapshots and append-only events for external tasks. The task keeps the immutable registration/executor binding captured at start. |
2889
+
2890
+ `agentTasks.events(taskId, cursor)` uses the last seen numeric event `seq` as its
2891
+ cursor and returns events with a greater sequence. `wait()` resolves only at a
2892
+ terminal task state and rejects on a positive `timeoutMs` expiry. `sendInput()`
2893
+ is valid only while the task reports `input-required` or `auth-required`.
2894
+ `reconcile()` asks the bound executor for authoritative remote state after an
2895
+ owner restart or uncertain failure.
2896
+
2897
+ The owner plane has a terminal close contract. Closing it rejects every pending
2898
+ `wait()` (including a wait without `timeoutMs`), disposes its executor instances,
2899
+ and makes subsequent registration, catalog, preflight, and task calls reject
2900
+ with `Agent executor plane is closed.` Repeated `close()` calls are safe. SDK
2901
+ hosts should stop accepting work before closing the owner and must not retain a
2902
+ plane service as a reusable handle after Runtime shutdown.
2903
+
2904
+ Restricted Workflow scripts use the same route as direct SDK calls. Both
2905
+ `wf.spawnAgent()` and `wf.runAgent()` validate and forward
2906
+ `target: { agentId, expectedConfigurationRevision? }`; `phase` is forwarded as
2907
+ well. A blank ID or revision is rejected at the script boundary instead of
2908
+ silently falling back to the native child backend.
2909
+
2910
+ ### Credential, artifact, and failure boundaries
2911
+
2912
+ - Put only a `credentialRef` in a registration. Resolve secret material through
2913
+ `AgentCredentialBroker.withCredential()`; do not place tokens in
2914
+ `executorConfig`, events, diagnostics, or task output.
2915
+ - Remote artifacts are denied by default. Supply `artifactPolicy` to authorize
2916
+ each artifact before it materializes in the host boundary.
2917
+ - External agents may declare workspace effect `none` or `proposal`; direct
2918
+ workspace mutation is intentionally not a valid external registration.
2919
+ - Use `expectedConfigurationRevision` to prevent dispatching against a stale
2920
+ endpoint/configuration seen by an earlier catalog read.
2921
+ - A remote start followed by uncertain local persistence is recorded as
2922
+ `unknown` with its executor reference preserved; reconcile it rather than
2923
+ blindly starting a duplicate. Stable idempotency keys protect retries.
2924
+ - The durable plane records provider-reported usage when available. It never
2925
+ invents missing token or cost fields.
2926
+
2927
+ For a production adapter, implement `preflight?`, `start`, `events`, `get`,
2928
+ `sendInput`, `cancel`, `reconcile`, and `dispose` on `AgentExecutor`. The factory
2929
+ receives `withCredential()` and `authorizeArtifact()` callbacks so protocol code
2930
+ cannot bypass the host's secret/artifact policies.
2931
+
2932
+ ---
2933
+
2934
+ ## 19. Session surface filtering and cursor pagination (FEATURE_261, v0.7.67)
2935
+
2936
+ The narrow session SDK and Runtime facade now share the same listing semantics:
2937
+ `surface` is an exact filter applied before `limit`, and `cursor` is an opaque
2938
+ continuation token carried by each returned summary.
2939
+
2940
+ ### Narrow `/session` API
2941
+
2942
+ ```ts
2943
+ import { listSessions, type SessionSummary } from '@kodax-ai/kodax/session';
2944
+
2945
+ const all: SessionSummary[] = [];
2946
+ let cursor: string | undefined;
2947
+ do {
2948
+ const page = await listSessions({
2949
+ scope: 'user',
2950
+ surface: 'partner',
2951
+ limit: 50,
2952
+ ...(cursor ? { cursor } : {}),
2953
+ });
2954
+ all.push(...page);
2955
+ cursor = page.length === 50 ? page.at(-1)?.cursor : undefined;
2956
+ } while (cursor);
2957
+ ```
2958
+
2959
+ ### Embedded/daemon Runtime API
2960
+
2961
+ ```ts
2962
+ const first = await runtime.sessions.list({ surface: 'acp', limit: 25 });
2963
+ const nextCursor = first.at(-1)?.cursor;
2964
+ const second = nextCursor
2965
+ ? await runtime.sessions.list({ surface: 'acp', limit: 25, cursor: nextCursor })
2966
+ : [];
2967
+ ```
2968
+
2969
+ The filter also composes with `projectRoot`, `scope`, `includeArchived`, `before`,
2970
+ and `tag`. Treat cursors as opaque: do not parse them, compare them to session
2971
+ IDs, or manufacture them. An invalid cursor produces an empty page on the narrow
2972
+ session API. A page shorter than the requested limit is terminal; a full page
2973
+ may continue with its last item's cursor.
2974
+
2975
+ The interactive `kodax -r` picker is a consumer of these session semantics, not
2976
+ a separate SDK API. Headless hosts should build their own UI on `listSessions()`
2977
+ or `runtime.sessions.list()` and resume by the selected full session ID.
2978
+
2979
+ ---
2980
+
2981
+ ## 20. Cost-disciplined workflow routing and telemetry (FEATURE_259, v0.7.67)
2982
+
2983
+ FEATURE_259 adds a public cost/quality contract without making the authoring LLM
2984
+ choose provider-specific model names. The SDK host maps semantic tiers to routes;
2985
+ workflow/child briefs request intent.
2986
+
2987
+ ### Configure tiers and bounded concurrency
2988
+
2989
+ ```ts
2990
+ import { join } from 'node:path';
2991
+ import { runKodaX } from '@kodax-ai/kodax/coding';
2992
+
2993
+ const workflowRunsBaseDir = join(process.cwd(), '.kodax-host', 'workflow-runs');
2994
+ const result = await runKodaX({
2995
+ provider: 'zai-coding',
2996
+ model: 'glm-5.2',
2997
+ agentMode: 'amaw',
2998
+ workflowRunsBaseDir,
2999
+ modelTiers: {
3000
+ fast: { provider: 'deepseek', model: 'deepseek-v4-flash' },
3001
+ deep: { provider: 'zai-coding', model: 'glm-5.2' },
3002
+ },
3003
+ workflow: { maxConcurrency: 3 },
3004
+ events: {
3005
+ onWorkflowAgentDigest: ({ runId, event }) => {
3006
+ if (event.type === 'agent_completed' || event.type === 'agent_unverified'
3007
+ || event.type === 'agent_failed') {
3008
+ console.log(runId, event.data);
3009
+ }
3010
+ },
3011
+ },
3012
+ }, 'Review this change using scoped evidence packets.');
3013
+
3014
+ console.log(result.lastText);
3015
+ ```
3016
+
3017
+ Tier rules are deliberately small:
3018
+
3019
+ - `fast`: mechanical read-only lookup. A write child is ineligible and safely
3020
+ inherits the parent route.
3021
+ - `balanced`: ordinary implementation/investigation/review; uses the parent
3022
+ route, so there is no separate `balanced` mapping.
3023
+ - `deep`: architecture, adversarial verification, severity calibration, and
3024
+ final synthesis.
3025
+ - An unconfigured or selector-shadowed tier inherits the appropriate explicit,
3026
+ specialist, or parent route and records why; it does not silently claim the
3027
+ requested route was applied.
3028
+
3029
+ The workflow authoring contract also supports `scopeSummary`, `constraints`,
3030
+ `evidenceRefs`, `verification`, `readOnly`, `outputSchema`, and `terseResult`.
3031
+ Use those fields to transfer a compact immutable packet instead of asking every
3032
+ child to rediscover the same repository context.
3033
+
3034
+ ### Live route facts
3035
+
3036
+ Terminal `WorkflowEvent.data` may include `requestedTier`, `tierOutcome`,
3037
+ `providerSource`, `modelSource`, initial/final provider and model,
3038
+ `fallbackReason`, `iterations`, `durationMs`, `usage`, and `digestUsage`.
3039
+ Treat absent fields as unknown. In particular, KodaX does not fabricate usage
3040
+ for external executors that did not report it.
3041
+
3042
+ Direct child-dispatch consumers receive the typed `KodaXChildAgentResult.routeFacts`
3043
+ surface, including resolved effort and input/cache-read/output/digest token
3044
+ breakdown when known. Inline workflow consumers can subscribe to the raw
3045
+ `onWorkflowAgentDigest` event as above; GUI progress remains available through
3046
+ `onWorkflowProcessEvent` / `runtime.workflows`.
3047
+
3048
+ ### Durable efficiency report
3049
+
3050
+ When `workflowRunsBaseDir` is supplied, every terminal workflow writes:
3051
+
3052
+ ```text
3053
+ <workflowRunsBaseDir>/<runId>/run.json
3054
+ <workflowRunsBaseDir>/<runId>/events.jsonl
3055
+ <workflowRunsBaseDir>/<runId>/artifacts/*.json
3056
+ ```
3057
+
3058
+ `run.json.efficiencyReport` includes:
3059
+
3060
+ - total/input/cache-read/output/digest tokens and wall-clock duration;
3061
+ - total starts, child turns, starts by `role/tier`, and primary-review starts;
3062
+ - duplicate primary packet reads plus verification/synthesis packet reads;
3063
+ - review/fix/re-review waves and structured review quality-gate outcomes;
3064
+ - `tokenCoverage.ok` plus missing local task IDs;
3065
+ - `excludedExternalTaskIds` for external tasks whose executor reported no usage.
3066
+
3067
+ Do not interpret `totalModelTokens: 0` as free execution unless
3068
+ `tokenCoverage.ok` is true and the relevant external task IDs are not excluded.
3069
+ The report is an audit/optimization artifact; correctness still comes from the
3070
+ workflow's structured findings, verification results, and quality gates.
3071
+
3072
+ ---
3073
+
3074
+ ## See also
3075
+
3076
+ - [README.md](../README.md) — end-user CLI quick start
3077
+ - [docs/ADR.md ADR-024](ADR.md#adr-024-npm-发布物正名-kodax-aikodax--sdk-subpath-exports-形式化-v0739) — SDK subpath architecture rationale
3078
+ - [docs/ADR.md ADR-032](ADR.md#adr-032-sdk-embedder-surface-closure-feature_186-v0742) — FEATURE_186 design record (all 8 phases)
3079
+ - [docs/features/v0.7.42.md FEATURE_186](features/v0.7.42.md#feature_186-sdk-embedder-surface-closure--kodax-space-gap-list--mcp-popout) — gap-by-gap landing matrix