@kodax-ai/kodax 0.7.66 → 0.7.68

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