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