@kodax-ai/kodax 0.7.73 → 0.7.74
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 +134 -0
- package/README.md +26 -6
- package/README_CN.md +23 -5
- package/config-templates/config.example.jsonc +7 -0
- package/dist/chunks/{agent-T6PGT6AU.js → agent-2ABIG5NW.js} +1 -1
- package/dist/chunks/argument-completer-C45DEO34.js +2 -0
- package/dist/chunks/chunk-5GH3SVI3.js +74 -0
- package/dist/chunks/{chunk-6JTWXGVU.js → chunk-7W6KYJU2.js} +225 -225
- package/dist/chunks/chunk-C7TZAT3S.js +369 -0
- package/dist/chunks/chunk-KPGVY6PF.js +321 -0
- package/dist/chunks/{chunk-6RI2RBTF.js → chunk-MVCAFD6M.js} +1 -1
- package/dist/chunks/{chunk-BUPJGG3C.js → chunk-PP53SIIL.js} +1 -1
- package/dist/chunks/chunk-QH56WEHK.js +2 -0
- package/dist/chunks/chunk-UJS7JAO6.js +770 -0
- package/dist/chunks/{chunk-M7FXNPWV.js → chunk-VK5QTHS6.js} +6 -6
- package/dist/chunks/chunk-VPURXE2F.js +427 -0
- package/dist/chunks/chunk-XM724XAA.js +329 -0
- package/dist/chunks/{chunk-MLEAQTCD.js → chunk-XMP66YWG.js} +11 -4
- package/dist/chunks/{compaction-config-IHJBP4GO.js → compaction-config-QYCDOW3Q.js} +1 -1
- package/dist/chunks/{construction-bootstrap-EGRUHKLP.js → construction-bootstrap-BKNPJI7O.js} +1 -1
- package/dist/chunks/dist-6EK4H7TP.js +2 -0
- package/dist/chunks/host-KS4TJKWQ.js +2 -0
- package/dist/chunks/{run-manager-AY7RQOWM.js → run-manager-L7MKGQNX.js} +1 -1
- package/dist/chunks/{utils-2GAE4XIM.js → utils-GTJLL4GT.js} +1 -1
- package/dist/index.d.ts +12 -11
- package/dist/index.js +5 -5
- package/dist/kodax_cli.js +1295 -1253
- package/dist/runtime-worker.js +1208 -1166
- package/dist/sdk-a2a.d.ts +9 -8
- package/dist/sdk-a2a.js +1 -1
- package/dist/sdk-agent.d.ts +107 -59
- package/dist/sdk-agent.js +1 -1
- package/dist/sdk-coding.d.ts +32 -16
- package/dist/sdk-coding.js +1 -1
- package/dist/sdk-mcp.js +1 -1
- package/dist/sdk-media.js +1 -1
- package/dist/sdk-repl.d.ts +27 -20
- package/dist/sdk-repl.js +2 -2
- package/dist/sdk-runtime.d.ts +151 -14
- package/dist/sdk-runtime.js +1 -1
- 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 +16 -14
- package/dist/types-chunks/{bash-prefix-extractor.d-bApf5nGw.d.ts → bash-prefix-extractor.d-r1beOESM.d.ts} +48 -12
- package/dist/types-chunks/{capsule.d-CGWltwno.d.ts → capsule.d-zeqV4IQX.d.ts} +10 -2
- package/dist/types-chunks/{commands.d-CEHGwRok.d.ts → commands.d-DUxnK2TU.d.ts} +10 -7
- package/dist/types-chunks/{guardrail.d-CC4vAQl7.d.ts → guardrail.d-CWYD1bdL.d.ts} +1 -1
- package/dist/types-chunks/{guardrail.d-CXIPlWP3.d.ts → guardrail.d-qjuKJZ31.d.ts} +72 -11
- package/dist/types-chunks/history-retrieval.d-BKTJIrVd.d.ts +57 -0
- package/dist/types-chunks/{public-api.d-FBmX0kxo.d.ts → public-api.d-CX4B11qY.d.ts} +17 -4
- package/dist/types-chunks/{run-manager.d-DUNnJcpE.d.ts → run-manager.d-B9fEIjZk.d.ts} +1 -1
- package/dist/types-chunks/{sdk-session-XqacVOs8.d.ts → sdk-session-B0fhAOPa.d.ts} +2 -2
- package/dist/types-chunks/{types.d-DKr6TCXL.d.ts → types.d-Bm_y6YuM.d.ts} +2 -2
- package/dist/types-chunks/{types.d-DgciGHoG.d.ts → types.d-DEctY20M.d.ts} +9 -2
- package/dist/types-chunks/{types.d-BnPhGmar.d.ts → types.d-sRLugmjy.d.ts} +61 -14
- package/dist/types-chunks/{utils.d-BPYX7-KN.d.ts → utils.d-D0wPxz8y.d.ts} +5 -5
- package/docs/SDK_EMBEDDER_GUIDE.md +1956 -1684
- package/package.json +8 -1
- package/dist/chunks/argument-completer-QGMWA2C3.js +0 -2
- package/dist/chunks/chunk-AGDDM5AW.js +0 -329
- package/dist/chunks/chunk-EAD4SF3Z.js +0 -413
- package/dist/chunks/chunk-HVCNAZ52.js +0 -365
- package/dist/chunks/chunk-OG6DRNKV.js +0 -755
- package/dist/chunks/chunk-PFH53O4M.js +0 -2
- package/dist/chunks/chunk-V4F6IYNK.js +0 -73
- package/dist/chunks/chunk-WQ6PWGK5.js +0 -320
- package/dist/chunks/dist-5N3I2RTW.js +0 -2
- package/dist/chunks/host-MECITNE3.js +0 -2
|
@@ -22,15 +22,18 @@ are NOT obvious from inspecting the type definitions alone:
|
|
|
22
22
|
12. [Provider credential verification — `verifyProviderCredential`](#12-provider-credential-verification--verifyprovidercredential-feature_216-v0745)
|
|
23
23
|
13. [Inject your product's manual — `selfManual`](#13-inject-your-products-manual--selfmanual-feature_221-v0747)
|
|
24
24
|
14. [Media input artifacts — `@kodax-ai/kodax/media`](#14-media-input-artifacts--kodax-aikodaxmedia-feature_239-v0756)
|
|
25
|
-
15. [Space v0.7.57 follow-up ledger](#15-space-v0757-follow-up-ledger)
|
|
26
|
-
16. [SDK agent-profile surface — `KodaXAgentProfile`](#16-sdk-agent-profile-surface--kodaxagentprofile-feature_247-v0758)
|
|
27
|
-
17. [Runtime SDK, Worker isolation, and local daemon](#17-runtime-sdk-worker-isolation-and-local-daemon-feature_253-feature_257)
|
|
28
|
-
18. [External-agent executor plane](#18-external-agent-executor-plane-feature_258-v0767)
|
|
29
|
-
19. [Session surface filtering and cursor pagination](#19-session-surface-filtering-and-cursor-pagination-feature_261-v0767)
|
|
30
|
-
20. [Cost-disciplined workflow routing and telemetry](#20-cost-disciplined-workflow-routing-and-telemetry-feature_259-v0767)
|
|
31
|
-
21. [Experimental governed memory — `/experimental-memory`](#21-experimental-governed-memory--experimental-memory-feature_260-v0768)
|
|
25
|
+
15. [Space v0.7.57 follow-up ledger](#15-space-v0757-follow-up-ledger)
|
|
26
|
+
16. [SDK agent-profile surface — `KodaXAgentProfile`](#16-sdk-agent-profile-surface--kodaxagentprofile-feature_247-v0758)
|
|
27
|
+
17. [Runtime SDK, Worker isolation, and local daemon](#17-runtime-sdk-worker-isolation-and-local-daemon-feature_253-feature_257)
|
|
28
|
+
18. [External-agent executor plane](#18-external-agent-executor-plane-feature_258-v0767)
|
|
29
|
+
19. [Session surface filtering and cursor pagination](#19-session-surface-filtering-and-cursor-pagination-feature_261-v0767)
|
|
30
|
+
20. [Cost-disciplined workflow routing and telemetry](#20-cost-disciplined-workflow-routing-and-telemetry-feature_259-v0767)
|
|
31
|
+
21. [Experimental governed memory — `/experimental-memory`](#21-experimental-governed-memory--experimental-memory-feature_260-v0768)
|
|
32
32
|
22. [Bidirectional A2A 1.0 — `/a2a`](#22-bidirectional-a2a-10--a2a-feature_267-v0769)
|
|
33
33
|
23. [Shared Coder daemon for Space and IDE hosts](#23-shared-coder-daemon-for-space-and-ide-hosts-feature_269-v0769)
|
|
34
|
+
24. [Runtime-owned Auto Mode and plan-approval bridges](#24-runtime-owned-auto-mode-and-plan-approval-bridges-v0772v0773)
|
|
35
|
+
25. [Always-on context compaction and bounded transcript recovery](#25-always-on-context-compaction-and-bounded-transcript-recovery-v0774)
|
|
36
|
+
26. [Agent mailbox control versus SDK event telemetry](#26-agent-mailbox-control-versus-sdk-event-telemetry-v0774)
|
|
34
37
|
|
|
35
38
|
§1–§3 (and the Phase-7/8 MCP-popout surface in §1) land in v0.7.42
|
|
36
39
|
under FEATURE_186 (see [ADR-032](ADR.md#adr-032-sdk-embedder-surface-closure-feature_186-v0742)).
|
|
@@ -697,33 +700,48 @@ await runKodaX(
|
|
|
697
700
|
// ✓ ~/.kodax/sessions/s_my_chat.jsonl now exists after the run.
|
|
698
701
|
|
|
699
702
|
// Same `storage` instance reads back through SessionManager:
|
|
700
|
-
const recent = await listSessions({
|
|
701
|
-
scope: 'user',
|
|
702
|
-
surface: 'acp',
|
|
703
|
-
limit: 50,
|
|
704
|
-
});
|
|
705
|
-
const nextPage = recent.at(-1)?.cursor
|
|
706
|
-
? await listSessions({
|
|
707
|
-
scope: 'user',
|
|
708
|
-
surface: 'acp',
|
|
709
|
-
limit: 50,
|
|
710
|
-
cursor: recent.at(-1)?.cursor,
|
|
711
|
-
})
|
|
712
|
-
: [];
|
|
703
|
+
const recent = await listSessions({
|
|
704
|
+
scope: 'user',
|
|
705
|
+
surface: 'acp',
|
|
706
|
+
limit: 50,
|
|
707
|
+
});
|
|
708
|
+
const nextPage = recent.at(-1)?.cursor
|
|
709
|
+
? await listSessions({
|
|
710
|
+
scope: 'user',
|
|
711
|
+
surface: 'acp',
|
|
712
|
+
limit: 50,
|
|
713
|
+
cursor: recent.at(-1)?.cursor,
|
|
714
|
+
})
|
|
715
|
+
: [];
|
|
713
716
|
const replay = await loadSession('s_my_chat');
|
|
714
717
|
const scrollback = await loadFullTranscript('s_my_chat');
|
|
715
718
|
const compacted = await compactSession('s_my_chat', { dryRun: true });
|
|
716
719
|
|
|
717
|
-
await appendClientNotice('s_my_chat', {
|
|
718
|
-
source: 'space',
|
|
719
|
-
content: '/doctor ok',
|
|
720
|
-
});
|
|
721
|
-
```
|
|
722
|
-
|
|
723
|
-
###
|
|
720
|
+
await appendClientNotice('s_my_chat', {
|
|
721
|
+
source: 'space',
|
|
722
|
+
content: '/doctor ok',
|
|
723
|
+
});
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
### Auto-resume selection in v0.7.74
|
|
727
|
+
|
|
728
|
+
With `session.autoResume: true` (or `resume: true`) and no explicit ID, KodaX
|
|
729
|
+
calls `storage.list(context.gitRoot, { limit: 1000 })` and chooses the first
|
|
730
|
+
newest-first summary whose `msgCount > 0`. This prevents newer zero-message
|
|
731
|
+
ACP/bootstrap placeholders from shadowing a real conversation. An explicit
|
|
732
|
+
`session.id` always wins. Custom `KodaXSessionStorage` implementations should
|
|
733
|
+
therefore honor the optional `limit` argument and return `msgCount` accurately;
|
|
734
|
+
they may return fewer than 1000 records.
|
|
735
|
+
|
|
736
|
+
The standalone interactive CLI additionally restores the persisted
|
|
737
|
+
workspace/runtime identity before the next turn. SDK embedders still own
|
|
738
|
+
`context.gitRoot`, `context.executionCwd`, and storage construction; do not rely
|
|
739
|
+
on process cwd as a substitute for host-owned runtime context.
|
|
740
|
+
|
|
741
|
+
### What `createSessionManager()` returns (v0.7.43+)
|
|
724
742
|
|
|
725
743
|
```ts
|
|
726
|
-
interface SessionManager {
|
|
744
|
+
interface SessionManager {
|
|
727
745
|
// Read side (FEATURE_173 v0.7.42)
|
|
728
746
|
listSessions(...): Promise<SessionSummary[]>;
|
|
729
747
|
loadSession(id): Promise<...>;
|
|
@@ -738,13 +756,13 @@ interface SessionManager {
|
|
|
738
756
|
watchSessions(cb): () => void;
|
|
739
757
|
// Write side (v0.7.43 follow-up)
|
|
740
758
|
storage: FileSessionStorage; // ← NEW — pass into runKodaX
|
|
741
|
-
}
|
|
742
|
-
```
|
|
743
|
-
|
|
744
|
-
`surface` is an exact filter applied before `limit`. Each returned summary may
|
|
745
|
-
include an opaque `cursor`; pass the last summary's cursor back unchanged to
|
|
746
|
-
continue the stable newest-first listing. Callers must not parse or construct
|
|
747
|
-
cursors themselves.
|
|
759
|
+
}
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
`surface` is an exact filter applied before `limit`. Each returned summary may
|
|
763
|
+
include an opaque `cursor`; pass the last summary's cursor back unchanged to
|
|
764
|
+
continue the stable newest-first listing. Callers must not parse or construct
|
|
765
|
+
cursors themselves.
|
|
748
766
|
|
|
749
767
|
### Active context vs full transcript vs UI replay
|
|
750
768
|
|
|
@@ -967,7 +985,7 @@ running your project's test suite against `main` — you can `npm link`
|
|
|
967
985
|
the in-tree KodaX checkout instead of waiting for a published version.
|
|
968
986
|
|
|
969
987
|
As of v0.7.43 the root `package.json` is in **already-published shape**:
|
|
970
|
-
`"name": "@kodax-ai/kodax"` is baked in along with all 10 SDK subpath
|
|
988
|
+
`"name": "@kodax-ai/kodax"` is baked in along with all 10 SDK subpath
|
|
971
989
|
exports. `npm link` "just works" — no need to run `scripts/release.mjs`
|
|
972
990
|
first.
|
|
973
991
|
|
|
@@ -1296,7 +1314,7 @@ import {
|
|
|
1296
1314
|
```ts
|
|
1297
1315
|
interface KodaXModelCapabilities {
|
|
1298
1316
|
provider: string; // 'anthropic' | 'kimi' | 'ark-coding' | <custom-name>
|
|
1299
|
-
model: string; // model id (e.g. 'claude-sonnet-4-6', 'kimi-k2.7-code')
|
|
1317
|
+
model: string; // model id (e.g. 'claude-sonnet-4-6', 'kimi-k2.7-code')
|
|
1300
1318
|
displayName: string; // human label — falls back to model id
|
|
1301
1319
|
supportsThinking: boolean; // native reasoning is available?
|
|
1302
1320
|
reasoningCapability: 'native-budget' | 'native-effort' | 'native-toggle' | 'prompt-only' | 'none' | 'unknown'; // legacy mechanism label
|
|
@@ -1328,8 +1346,8 @@ for (const caps of listAllModelCapabilities()) {
|
|
|
1328
1346
|
```ts
|
|
1329
1347
|
import { resolveModelCapabilities } from '@kodax-ai/kodax/llm';
|
|
1330
1348
|
|
|
1331
|
-
const caps = resolveModelCapabilities('kimi', 'kimi-k2.7-code');
|
|
1332
|
-
// => { contextWindow: 262_144, supportsThinking: true, reasoningProfile: { defaultEffort: 'high', ... }, ... }
|
|
1349
|
+
const caps = resolveModelCapabilities('kimi', 'kimi-k2.7-code');
|
|
1350
|
+
// => { contextWindow: 262_144, supportsThinking: true, reasoningProfile: { defaultEffort: 'high', ... }, ... }
|
|
1333
1351
|
```
|
|
1334
1352
|
|
|
1335
1353
|
For picker/status UIs, use `reasoningProfile.supportedEfforts` and
|
|
@@ -1466,11 +1484,11 @@ and loaded into the in-memory `KODAX_PROVIDER_SNAPSHOTS` export. When upstream
|
|
|
1466
1484
|
providers publish a new model or change a context-window cap, the JSON file is
|
|
1467
1485
|
the patch site — the new value flows to runtime (via `buildProviderConfig`) AND
|
|
1468
1486
|
to SDK consumers (via the getters) in a single edit. The current snapshot is
|
|
1469
|
-
dated 2026-07-16 and includes the GPT-5.4, Kimi K2.7 Code / HighSpeed, GLM-5.2, MiniMax
|
|
1487
|
+
dated 2026-07-16 and includes the GPT-5.4, Kimi K2.7 Code / HighSpeed, GLM-5.2, MiniMax
|
|
1470
1488
|
M3/M2.7, DeepSeek V4, and Doubao Seed 2.0 route refreshes where supported. The
|
|
1471
1489
|
test suite at
|
|
1472
1490
|
[`packages/llm/src/providers/model-capabilities.test.ts`](../packages/llm/src/providers/model-capabilities.test.ts)
|
|
1473
|
-
locks in specific values (e.g. the public Kimi lineup at 262,144 tokens, deepseek-v4-pro at 1M)
|
|
1491
|
+
locks in specific values (e.g. the public Kimi lineup at 262,144 tokens, deepseek-v4-pro at 1M)
|
|
1474
1492
|
so accidental drift is caught at PR time.
|
|
1475
1493
|
|
|
1476
1494
|
The probe scripts that surveyed upstream APIs live at
|
|
@@ -1905,12 +1923,12 @@ Each provider has one `verifyStrategy` value baked into `provider-capabilities.j
|
|
|
1905
1923
|
|
|
1906
1924
|
| Strategy | What runs | Cost | Used by built-ins |
|
|
1907
1925
|
|---|---|---|---|
|
|
1908
|
-
| `count-tokens` | `client.messages.countTokens({ messages: [{role:'user',content:'hi'}] })` | 0 token | `anthropic`, `kimi-code`, `qwen-token-plan`, `zhipu-coding`, `zai-coding`, `minimax-coding`, `ark-coding` |
|
|
1926
|
+
| `count-tokens` | `client.messages.countTokens({ messages: [{role:'user',content:'hi'}] })` | 0 token | `anthropic`, `kimi-code`, `qwen-token-plan`, `zhipu-coding`, `zai-coding`, `minimax-coding`, `ark-coding` |
|
|
1909
1927
|
| `models-list` | `client.models.list()` | 0 token | `openai`, `deepseek`, `kimi`, `qwen` |
|
|
1910
1928
|
| `minimal-message` | `chat.completions.create({max_tokens:1, content:'hi'})` (or Anthropic equivalent) | ~6–7 token | `zhipu`, `mimo`, `mimo-coding` |
|
|
1911
1929
|
| `unsupported` | nothing — short-circuits | — | `gemini-cli`, `codex-cli` (cli-bridge: credentials live in CLI binary) |
|
|
1912
1930
|
|
|
1913
|
-
`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 the original per-provider evidence (12 providers at the time; 15 built-in aliases as of 2026-06-28). The current capability catalog has 16 aliases, including `qwen-token-plan` with `count-tokens`; opencode's `setup-recording-env.ts` makes the same per-provider decision across its 20+ providers.
|
|
1931
|
+
`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 the original per-provider evidence (12 providers at the time; 15 built-in aliases as of 2026-06-28). The current capability catalog has 16 aliases, including `qwen-token-plan` with `count-tokens`; opencode's `setup-recording-env.ts` makes the same per-provider decision across its 20+ providers.
|
|
1914
1932
|
|
|
1915
1933
|
### Custom providers
|
|
1916
1934
|
|
|
@@ -1954,7 +1972,7 @@ Cli-bridge providers (`gemini-cli`, `codex-cli`) return their CLI binary's known
|
|
|
1954
1972
|
|
|
1955
1973
|
### Reference
|
|
1956
1974
|
|
|
1957
|
-
- Source: `packages/llm/src/providers/verify-credential.ts` (orchestrator + classifier) + `verify-credential.test.ts` (27 unit tests) + `verify-credential-integration.test.ts` (12 gated real-key/fake-key tests, enabled by `KODAX_INTEGRATION_TEST=1`).
|
|
1975
|
+
- Source: `packages/llm/src/providers/verify-credential.ts` (orchestrator + classifier) + `verify-credential.test.ts` (27 unit tests) + `verify-credential-integration.test.ts` (12 gated real-key/fake-key tests, enabled by `KODAX_INTEGRATION_TEST=1`).
|
|
1958
1976
|
- Data: `packages/llm/src/providers/provider-capabilities.json` `verifyStrategy` field per provider.
|
|
1959
1977
|
- Design notes + probe matrix: [docs/features/v0.7.45.md FEATURE_216](features/v0.7.45.md#feature_216-provider-credential-verification-api).
|
|
1960
1978
|
|
|
@@ -2003,14 +2021,14 @@ By default your `topics` **extend** the KodaX base manual. v0.7.58 adds
|
|
|
2003
2021
|
|
|
2004
2022
|
`KODAX_UNDERLYING_CAPABILITY_TOPICS` (exported) is the recommended mechanism-topic
|
|
2005
2023
|
subset a product built on KodaX should keep even in a full replace — so your users
|
|
2006
|
-
still get correct answers about the underlying engine (providers / config /
|
|
2007
|
-
permissions / tools / skills / extensions / mcp / repo-intelligence / sessions /
|
|
2008
|
-
sdk / custom-providers) without the KodaX brand:
|
|
2009
|
-
|
|
2010
|
-
The default full manual also includes the `memory` topic. It is intentionally
|
|
2011
|
-
not part of `KODAX_UNDERLYING_CAPABILITY_TOPICS` because `/experimental-memory`
|
|
2012
|
-
is opt-in; hosts that expose it should add `memory` explicitly or provide a
|
|
2013
|
-
product-specific override.
|
|
2024
|
+
still get correct answers about the underlying engine (providers / config /
|
|
2025
|
+
permissions / tools / skills / extensions / mcp / repo-intelligence / sessions /
|
|
2026
|
+
sdk / custom-providers) without the KodaX brand:
|
|
2027
|
+
|
|
2028
|
+
The default full manual also includes the `memory` topic. It is intentionally
|
|
2029
|
+
not part of `KODAX_UNDERLYING_CAPABILITY_TOPICS` because `/experimental-memory`
|
|
2030
|
+
is opt-in; hosts that expose it should add `memory` explicitly or provide a
|
|
2031
|
+
product-specific override.
|
|
2014
2032
|
|
|
2015
2033
|
```ts
|
|
2016
2034
|
import {
|
|
@@ -2169,30 +2187,32 @@ For streaming follow-ups, use `enqueueWithArtifacts()` instead of the raw
|
|
|
2169
2187
|
message queue:
|
|
2170
2188
|
|
|
2171
2189
|
```ts
|
|
2172
|
-
enqueueWithArtifacts({
|
|
2173
|
-
provider: selectedProvider,
|
|
2174
|
-
model: selectedModel,
|
|
2175
|
-
sessionId: activeSessionId,
|
|
2176
|
-
content: followupText,
|
|
2177
|
-
inputArtifacts: [artifact],
|
|
2178
|
-
});
|
|
2179
|
-
```
|
|
2180
|
-
|
|
2181
|
-
The helper validates first and then stores `inputArtifacts` on the queued prompt.
|
|
2182
|
-
Queued image follow-ups are rebuilt as multimodal content blocks on the next
|
|
2183
|
-
runner turn. Unsupported file/video attachments are rejected before enqueueing.
|
|
2184
|
-
Pass `sessionId` whenever the host can run more than one session concurrently;
|
|
2185
|
-
it targets that Actor session's root queue without exposing Actor paths. For
|
|
2186
|
-
backward compatibility, omitting both `sessionId` and `agentId` still binds to
|
|
2187
|
-
the sole active Actor root, or uses the legacy unscoped SA queue when no Actor
|
|
2188
|
-
run is active. If multiple Actor roots are active, the helper rejects the
|
|
2189
|
-
ambiguous call instead of risking cross-session delivery. Low-level child-Actor
|
|
2190
|
-
producers may continue to pass an explicit `agentId`.
|
|
2191
|
-
|
|
2192
|
-
`enqueueWithArtifacts()` is an in-process queue helper for direct/inline runs.
|
|
2193
|
-
Runtime Worker and daemon clients must use `runtime.runs.submitInput(...)` (with
|
|
2194
|
-
the same `sessionId
|
|
2195
|
-
|
|
2190
|
+
enqueueWithArtifacts({
|
|
2191
|
+
provider: selectedProvider,
|
|
2192
|
+
model: selectedModel,
|
|
2193
|
+
sessionId: activeSessionId,
|
|
2194
|
+
content: followupText,
|
|
2195
|
+
inputArtifacts: [artifact],
|
|
2196
|
+
});
|
|
2197
|
+
```
|
|
2198
|
+
|
|
2199
|
+
The helper validates first and then stores `inputArtifacts` on the queued prompt.
|
|
2200
|
+
Queued image follow-ups are rebuilt as multimodal content blocks on the next
|
|
2201
|
+
runner turn. Unsupported file/video attachments are rejected before enqueueing.
|
|
2202
|
+
Pass `sessionId` whenever the host can run more than one session concurrently;
|
|
2203
|
+
it targets that Actor session's root queue without exposing Actor paths. For
|
|
2204
|
+
backward compatibility, omitting both `sessionId` and `agentId` still binds to
|
|
2205
|
+
the sole active Actor root, or uses the legacy unscoped SA queue when no Actor
|
|
2206
|
+
run is active. If multiple Actor roots are active, the helper rejects the
|
|
2207
|
+
ambiguous call instead of risking cross-session delivery. Low-level child-Actor
|
|
2208
|
+
producers may continue to pass an explicit `agentId`.
|
|
2209
|
+
|
|
2210
|
+
`enqueueWithArtifacts()` is an in-process queue helper for direct/inline runs.
|
|
2211
|
+
Runtime Worker and daemon clients must use `runtime.runs.submitInput(...)` (with
|
|
2212
|
+
the same `sessionId` and `afterRunId`) because a process-local MessageQueue
|
|
2213
|
+
cannot cross those transport boundaries. Use `delivery:'after_turn'` to create
|
|
2214
|
+
a continuation Run after the current Run ends, or `delivery:'interrupt'` to
|
|
2215
|
+
inject into the current active Actor Run at its next safe Runner boundary.
|
|
2196
2216
|
|
|
2197
2217
|
### Boundaries
|
|
2198
2218
|
|
|
@@ -2361,14 +2381,14 @@ The important creation options are:
|
|
|
2361
2381
|
| `worker.resourceLimits` | unset | Optional V8 heap/stack limits; requires `isolation: 'worker'`. |
|
|
2362
2382
|
| `worker.shutdownTimeoutMs` | `2000` | Grace before the parent terminates the Runtime Worker. |
|
|
2363
2383
|
| `requirements.hardDispose` | `false` | Rejects inline and daemon forms; prevents an accidental weaker ownership form. |
|
|
2364
|
-
| `homeDir` | unset | When omitted, use the exact resolved `KODAX_HOME`. When set, this is the base directory that owns `.kodax`, with the same meaning as CLI `daemon --home`; daemon state/config live under `<homeDir>/.kodax`. |
|
|
2384
|
+
| `homeDir` | unset | When omitted, use the exact resolved `KODAX_HOME`. When set, this is the base directory that owns `.kodax`, with the same meaning as CLI `daemon --home`; daemon state/config live under `<homeDir>/.kodax`. |
|
|
2365
2385
|
| `profile` | `'default'` | Daemon uniqueness and runtime configuration namespace. |
|
|
2366
2386
|
| `sessionsDir` | `<homeDir>/.kodax/sessions` | Explicit session storage override. |
|
|
2367
2387
|
| `daemonStartupTimeoutMs` | `60000` | Total cold-start/concurrent-owner wait budget. |
|
|
2368
|
-
| `daemonConnectTimeoutMs` | `2000` | Per-socket connection timeout. |
|
|
2369
|
-
| `autoStartDaemon` | conditional | For `createKodaXRuntime({mode:'daemon'})`, true only when no explicit endpoint/transport is supplied. |
|
|
2370
|
-
| `externalAgents` | unset | Host-installed executor factories, dispatch policy, optional credential/artifact policies, and default dispatch context. Inline owner only; see §18. |
|
|
2371
|
-
| `requirements.externalAgents` | `false` | Reject a Runtime/daemon connection that does not advertise an installed external-agent plane. |
|
|
2388
|
+
| `daemonConnectTimeoutMs` | `2000` | Per-socket connection timeout. |
|
|
2389
|
+
| `autoStartDaemon` | conditional | For `createKodaXRuntime({mode:'daemon'})`, true only when no explicit endpoint/transport is supplied. |
|
|
2390
|
+
| `externalAgents` | unset | Host-installed executor factories, dispatch policy, optional credential/artifact policies, and default dispatch context. Inline owner only; see §18. |
|
|
2391
|
+
| `requirements.externalAgents` | `false` | Reject a Runtime/daemon connection that does not advertise an installed external-agent plane. |
|
|
2372
2392
|
|
|
2373
2393
|
KodaX rejects contradictory options. Worker settings without Worker isolation,
|
|
2374
2394
|
`requirements.hardDispose` on inline/daemon forms, and any explicit isolation
|
|
@@ -2442,20 +2462,20 @@ no explicit `daemonEndpoint` or `daemonTransport` is supplied it starts or reuse
|
|
|
2442
2462
|
the local profile daemon. `connectKodaXRuntime()` is attach-only unless
|
|
2443
2463
|
`autoStart: true` is passed.
|
|
2444
2464
|
|
|
2445
|
-
SDK auto-start allows `daemonStartupTimeoutMs` (default 60 seconds) and
|
|
2446
|
-
`daemonConnectTimeoutMs`. The longer startup budget covers cold machines and
|
|
2447
|
-
concurrent test/desktop startup without weakening PID, endpoint, token, or
|
|
2448
|
-
runtime-identity validation.
|
|
2449
|
-
|
|
2450
|
-
`homeDir` and `KODAX_HOME` deliberately name different levels. Runtime SDK and
|
|
2451
|
-
CLI daemon `--home` accept the **base directory that contains `.kodax`**;
|
|
2452
|
-
lower-level `KODAX_HOME` points at the **data directory itself** and need not be
|
|
2453
|
-
named `.kodax`. To share the default CLI daemon, omit `homeDir`; this honors the
|
|
2454
|
-
exact resolved `KODAX_HOME`. Passing `os.homedir()` explicitly instead selects
|
|
2455
|
-
`<os.homedir()>/.kodax`, regardless of an ambient custom `KODAX_HOME`. For an
|
|
2456
|
-
isolated embedder namespace, pass a private base directory and expect data at
|
|
2457
|
-
`<homeDir>/.kodax`. Passing `~/.kodax` as `homeDir` would instead select
|
|
2458
|
-
`~/.kodax/.kodax` and a different daemon namespace.
|
|
2465
|
+
SDK auto-start allows `daemonStartupTimeoutMs` (default 60 seconds) and
|
|
2466
|
+
`daemonConnectTimeoutMs`. The longer startup budget covers cold machines and
|
|
2467
|
+
concurrent test/desktop startup without weakening PID, endpoint, token, or
|
|
2468
|
+
runtime-identity validation.
|
|
2469
|
+
|
|
2470
|
+
`homeDir` and `KODAX_HOME` deliberately name different levels. Runtime SDK and
|
|
2471
|
+
CLI daemon `--home` accept the **base directory that contains `.kodax`**;
|
|
2472
|
+
lower-level `KODAX_HOME` points at the **data directory itself** and need not be
|
|
2473
|
+
named `.kodax`. To share the default CLI daemon, omit `homeDir`; this honors the
|
|
2474
|
+
exact resolved `KODAX_HOME`. Passing `os.homedir()` explicitly instead selects
|
|
2475
|
+
`<os.homedir()>/.kodax`, regardless of an ambient custom `KODAX_HOME`. For an
|
|
2476
|
+
isolated embedder namespace, pass a private base directory and expect data at
|
|
2477
|
+
`<homeDir>/.kodax`. Passing `~/.kodax` as `homeDir` would instead select
|
|
2478
|
+
`~/.kodax/.kodax` and a different daemon namespace.
|
|
2459
2479
|
|
|
2460
2480
|
### Worker-hosted embedded usage
|
|
2461
2481
|
|
|
@@ -2534,16 +2554,16 @@ For a given profile, one owner wins an atomic lock. Concurrent starters wait for
|
|
|
2534
2554
|
the winner and connect once it is ready. Stale state is cleaned only after pid,
|
|
2535
2555
|
endpoint, token, and runtime identity checks. If ownership cannot be verified,
|
|
2536
2556
|
KodaX reports the daemon as unhealthy instead of killing an arbitrary process.
|
|
2537
|
-
SDK auto-start launches a detached `kodax daemon serve` process; it never treats
|
|
2538
|
-
an in-process socket listener as daemon mode. Closing the SDK client detaches
|
|
2539
|
-
without stopping that shared process. Use `kodax daemon stop` or the explicit
|
|
2540
|
-
runtime shutdown protocol to stop the owner.
|
|
2541
|
-
|
|
2542
|
-
CLI and SDK startup retain the exact spawned candidate until health confirms
|
|
2543
|
-
that candidate PID. Early exit, timeout, identity mismatch, cancellation, or a
|
|
2544
|
-
different owner winning the race reclaims only the unsuccessful candidate
|
|
2545
|
-
process tree. Once healthy, the owner detaches normally and is not tied to the
|
|
2546
|
-
creating client process.
|
|
2557
|
+
SDK auto-start launches a detached `kodax daemon serve` process; it never treats
|
|
2558
|
+
an in-process socket listener as daemon mode. Closing the SDK client detaches
|
|
2559
|
+
without stopping that shared process. Use `kodax daemon stop` or the explicit
|
|
2560
|
+
runtime shutdown protocol to stop the owner.
|
|
2561
|
+
|
|
2562
|
+
CLI and SDK startup retain the exact spawned candidate until health confirms
|
|
2563
|
+
that candidate PID. Early exit, timeout, identity mismatch, cancellation, or a
|
|
2564
|
+
different owner winning the race reclaims only the unsuccessful candidate
|
|
2565
|
+
process tree. Once healthy, the owner detaches normally and is not tied to the
|
|
2566
|
+
creating client process.
|
|
2547
2567
|
|
|
2548
2568
|
### Daemon startup, conflict, and recovery behavior
|
|
2549
2569
|
|
|
@@ -2559,18 +2579,18 @@ recovered as `interrupted` with a runtime event; they are not resumed
|
|
|
2559
2579
|
automatically. Session and bounded event records remain available for explicit
|
|
2560
2580
|
reconnect/retry decisions.
|
|
2561
2581
|
|
|
2562
|
-
Operational guidance:
|
|
2563
|
-
|
|
2564
|
-
- use one stable profile for cooperating desktop clients;
|
|
2565
|
-
- use a separate `homeDir` or profile for tests, previews, and incompatible
|
|
2566
|
-
configurations;
|
|
2567
|
-
- test harnesses that auto-start a process daemon must send authenticated
|
|
2568
|
-
`runtime.shutdown` (or run `kodax daemon stop --home <dir> --profile <name>`)
|
|
2569
|
-
before deleting their temporary home; `runtime.close()` only detaches;
|
|
2570
|
-
- KodaX's own Vitest harness also supplies an internal worker-PID marker so a
|
|
2571
|
-
forcibly terminated worker cannot strand its test daemon; this is a test-only
|
|
2572
|
-
fallback, not a public SDK option or a production idle-shutdown policy;
|
|
2573
|
-
- query `kodax daemon status --json` before deciding to restart;
|
|
2582
|
+
Operational guidance:
|
|
2583
|
+
|
|
2584
|
+
- use one stable profile for cooperating desktop clients;
|
|
2585
|
+
- use a separate `homeDir` or profile for tests, previews, and incompatible
|
|
2586
|
+
configurations;
|
|
2587
|
+
- test harnesses that auto-start a process daemon must send authenticated
|
|
2588
|
+
`runtime.shutdown` (or run `kodax daemon stop --home <dir> --profile <name>`)
|
|
2589
|
+
before deleting their temporary home; `runtime.close()` only detaches;
|
|
2590
|
+
- KodaX's own Vitest harness also supplies an internal worker-PID marker so a
|
|
2591
|
+
forcibly terminated worker cannot strand its test daemon; this is a test-only
|
|
2592
|
+
fallback, not a public SDK option or a production idle-shutdown policy;
|
|
2593
|
+
- query `kodax daemon status --json` before deciding to restart;
|
|
2574
2594
|
- inspect `kodax daemon logs --lines 100` when startup times out;
|
|
2575
2595
|
- custom endpoints and injected transports are attach-only unless the caller
|
|
2576
2596
|
implements their owner lifecycle explicitly.
|
|
@@ -2606,10 +2626,10 @@ Every `KodaXRuntime` exposes the same service set in inline, Worker, and daemon
|
|
|
2606
2626
|
| `mcp` | MCP server CRUD, validation, reload, and tool catalog listing. |
|
|
2607
2627
|
| `artifacts` | Create/get/delete runtime artifact references for file/image/video inputs. |
|
|
2608
2628
|
| `status` | Runtime snapshot with sessions, runs, permissions, workflows, and daemon counters. |
|
|
2609
|
-
| `diagnostics` | Latest context-budget and tool-exposure decisions for GUI/debug surfaces. |
|
|
2610
|
-
| `admin.agentRegistrations` | List/upsert, atomically set `enabled` while preserving the full registration, or remove redacted external-agent registrations. Owner/revision-conditional mutation prevents a stale manager from changing a same-ID replacement. With no plane, list is empty and mutations fail clearly. |
|
|
2611
|
-
| `agents` | Check `enabled`, list/describe policy-filtered dispatchable agents, and preflight a selected route. |
|
|
2612
|
-
| `agentTasks` | Start/list/get/wait/continue/cancel/reconcile durable external-agent tasks and read their ordered event stream. |
|
|
2629
|
+
| `diagnostics` | Latest context-budget and tool-exposure decisions for GUI/debug surfaces. |
|
|
2630
|
+
| `admin.agentRegistrations` | List/upsert, atomically set `enabled` while preserving the full registration, or remove redacted external-agent registrations. Owner/revision-conditional mutation prevents a stale manager from changing a same-ID replacement. With no plane, list is empty and mutations fail clearly. |
|
|
2631
|
+
| `agents` | Check `enabled`, list/describe policy-filtered dispatchable agents, and preflight a selected route. |
|
|
2632
|
+
| `agentTasks` | Start/list/get/wait/continue/cancel/reconcile durable external-agent tasks and read their ordered event stream. |
|
|
2613
2633
|
|
|
2614
2634
|
### Sessions, runs, and queueing
|
|
2615
2635
|
|
|
@@ -2621,14 +2641,14 @@ runtime close.
|
|
|
2621
2641
|
```ts
|
|
2622
2642
|
const session = await runtime.sessions.create({ title: 'Shared session' });
|
|
2623
2643
|
|
|
2624
|
-
const sub = runtime.events.subscribe({ sessionId: session.id }, (event) => {
|
|
2625
|
-
if (event.type === 'assistant.delta') {
|
|
2626
|
-
// Render streaming text in the host UI.
|
|
2627
|
-
}
|
|
2628
|
-
});
|
|
2629
|
-
// Daemon subscriptions establish a remote handshake. Await this before
|
|
2630
|
-
// starting work whose first event must not be missed; local subscriptions omit it.
|
|
2631
|
-
await sub.ready;
|
|
2644
|
+
const sub = runtime.events.subscribe({ sessionId: session.id }, (event) => {
|
|
2645
|
+
if (event.type === 'assistant.delta') {
|
|
2646
|
+
// Render streaming text in the host UI.
|
|
2647
|
+
}
|
|
2648
|
+
});
|
|
2649
|
+
// Daemon subscriptions establish a remote handshake. Await this before
|
|
2650
|
+
// starting work whose first event must not be missed; local subscriptions omit it.
|
|
2651
|
+
await sub.ready;
|
|
2632
2652
|
|
|
2633
2653
|
const artifact = await runtime.artifacts.create({
|
|
2634
2654
|
kind: 'image',
|
|
@@ -2656,14 +2676,14 @@ sub.close();
|
|
|
2656
2676
|
|
|
2657
2677
|
`runs.start({ options })` is a DTO boundary in Worker and daemon forms. Do not
|
|
2658
2678
|
pass process-local objects such as `extensionRuntime`, callbacks, `AbortSignal`,
|
|
2659
|
-
LSP services, class instances, or cyclic structures. KodaX rejects them before
|
|
2660
|
-
transport instead of silently dropping fields.
|
|
2661
|
-
|
|
2662
|
-
`events.beforeToolExecute` is an executable policy hook, not an observation
|
|
2663
|
-
callback. It is preserved in embedded mode and rejected in daemon mode; install
|
|
2664
|
-
the equivalent policy in the daemon owner instead. The KodaX REPL explicitly
|
|
2665
|
-
marks and removes only its own legacy approval hook because Runtime-owned
|
|
2666
|
-
permission brokering replaces that hook.
|
|
2679
|
+
LSP services, class instances, or cyclic structures. KodaX rejects them before
|
|
2680
|
+
transport instead of silently dropping fields.
|
|
2681
|
+
|
|
2682
|
+
`events.beforeToolExecute` is an executable policy hook, not an observation
|
|
2683
|
+
callback. It is preserved in embedded mode and rejected in daemon mode; install
|
|
2684
|
+
the equivalent policy in the daemon owner instead. The KodaX REPL explicitly
|
|
2685
|
+
marks and removes only its own legacy approval hook because Runtime-owned
|
|
2686
|
+
permission brokering replaces that hook.
|
|
2667
2687
|
|
|
2668
2688
|
Use the runtime APIs for cross-boundary behavior:
|
|
2669
2689
|
|
|
@@ -2692,7 +2712,7 @@ Space-style client can subscribe to permission events and answer a request
|
|
|
2692
2712
|
created by a REPL-style client.
|
|
2693
2713
|
|
|
2694
2714
|
```ts
|
|
2695
|
-
const sub = runtime.events.subscribe({ type: 'permission.requested' }, (event) => {
|
|
2715
|
+
const sub = runtime.events.subscribe({ type: 'permission.requested' }, (event) => {
|
|
2696
2716
|
const payload = event.payload;
|
|
2697
2717
|
if (!payload || typeof payload !== 'object' || typeof payload.id !== 'string') {
|
|
2698
2718
|
return;
|
|
@@ -2701,35 +2721,35 @@ const sub = runtime.events.subscribe({ type: 'permission.requested' }, (event) =
|
|
|
2701
2721
|
payload.id,
|
|
2702
2722
|
{ type: 'allow_once' },
|
|
2703
2723
|
{ runId: payload.runId },
|
|
2704
|
-
);
|
|
2705
|
-
});
|
|
2706
|
-
await sub.ready;
|
|
2707
|
-
```
|
|
2708
|
-
|
|
2709
|
-
Await `ready` before another client starts work that may request permission;
|
|
2710
|
-
this creates an explicit cross-connection ordering boundary. Only the first
|
|
2711
|
-
valid response wins. Wrong-run or stale responses are rejected.
|
|
2712
|
-
Abort, runtime close, daemon stop, and timeout reject unresolved permission
|
|
2713
|
-
requests so tool approval promises do not hang forever.
|
|
2714
|
-
|
|
2715
|
-
SDK clients that create a concrete request may pass `toolInput` and
|
|
2716
|
-
`executionCwd` to `runtime.permissions.request(...)` in both embedded and
|
|
2717
|
-
daemon mode. The Runtime removes raw `toolInput` before publishing the pending
|
|
2718
|
-
request, derives the bounded/redacted preview from that concrete input,
|
|
2719
|
-
canonicalizes it into a matcher, and returns only opaque `grantSuggestions`.
|
|
2720
|
-
A caller-supplied `inputPreview` cannot override this trusted summary.
|
|
2721
|
-
`projectRoot`, classifier signals, and other owner-only safety context are
|
|
2722
|
-
deliberately not client request fields.
|
|
2723
|
-
|
|
2724
|
-
Grant administration is typed through `RuntimePermissionScope` and the
|
|
2725
|
-
exported `RuntimePermissionMatcher` union (`exact-command`, `exact-path`, and
|
|
2726
|
-
`exact-call`). These types are available from both the package root and
|
|
2727
|
-
`@kodax-ai/kodax/runtime`; matcher construction remains Runtime-owned.
|
|
2728
|
-
Legacy `allow_always.scope` responses remain accepted for 0.7.x clients, but
|
|
2729
|
-
the Runtime narrows them to its concrete candidate and never persists the
|
|
2730
|
-
client-provided coarse scope. Legacy persisted grants that lack a Runtime
|
|
2731
|
-
matcher remain inspectable and revocable, but never authorize a concrete tool
|
|
2732
|
-
call; the user must approve a fresh Runtime-issued exact suggestion.
|
|
2724
|
+
);
|
|
2725
|
+
});
|
|
2726
|
+
await sub.ready;
|
|
2727
|
+
```
|
|
2728
|
+
|
|
2729
|
+
Await `ready` before another client starts work that may request permission;
|
|
2730
|
+
this creates an explicit cross-connection ordering boundary. Only the first
|
|
2731
|
+
valid response wins. Wrong-run or stale responses are rejected.
|
|
2732
|
+
Abort, runtime close, daemon stop, and timeout reject unresolved permission
|
|
2733
|
+
requests so tool approval promises do not hang forever.
|
|
2734
|
+
|
|
2735
|
+
SDK clients that create a concrete request may pass `toolInput` and
|
|
2736
|
+
`executionCwd` to `runtime.permissions.request(...)` in both embedded and
|
|
2737
|
+
daemon mode. The Runtime removes raw `toolInput` before publishing the pending
|
|
2738
|
+
request, derives the bounded/redacted preview from that concrete input,
|
|
2739
|
+
canonicalizes it into a matcher, and returns only opaque `grantSuggestions`.
|
|
2740
|
+
A caller-supplied `inputPreview` cannot override this trusted summary.
|
|
2741
|
+
`projectRoot`, classifier signals, and other owner-only safety context are
|
|
2742
|
+
deliberately not client request fields.
|
|
2743
|
+
|
|
2744
|
+
Grant administration is typed through `RuntimePermissionScope` and the
|
|
2745
|
+
exported `RuntimePermissionMatcher` union (`exact-command`, `exact-path`, and
|
|
2746
|
+
`exact-call`). These types are available from both the package root and
|
|
2747
|
+
`@kodax-ai/kodax/runtime`; matcher construction remains Runtime-owned.
|
|
2748
|
+
Legacy `allow_always.scope` responses remain accepted for 0.7.x clients, but
|
|
2749
|
+
the Runtime narrows them to its concrete candidate and never persists the
|
|
2750
|
+
client-provided coarse scope. Legacy persisted grants that lack a Runtime
|
|
2751
|
+
matcher remain inspectable and revocable, but never authorize a concrete tool
|
|
2752
|
+
call; the user must approve a fresh Runtime-issued exact suggestion.
|
|
2733
2753
|
|
|
2734
2754
|
### Config, catalogs, MCP, and Space-style admin APIs
|
|
2735
2755
|
|
|
@@ -2802,18 +2822,18 @@ import {
|
|
|
2802
2822
|
The schema is additive within this patch line. Removing or changing required
|
|
2803
2823
|
fields requires a protocol version bump.
|
|
2804
2824
|
|
|
2805
|
-
### Current verification status
|
|
2806
|
-
|
|
2807
|
-
The v0.7.69 release validation covers the runtime migration, the Worker
|
|
2808
|
-
isolation follow-ups delivered ahead of their original v0.7.71/v0.7.72
|
|
2809
|
-
planning slots, the v0.7.67 external-agent/session additions, the v0.7.68
|
|
2810
|
-
experimental Memory Agent surface, and the v0.7.69 A2A/integration/shared-
|
|
2811
|
-
daemon delivery:
|
|
2812
|
-
|
|
2813
|
-
- Node 20 and Node 22 post-review regression for process cleanup, lock
|
|
2814
|
-
ownership, frozen eval inputs, A2A transport, and the built-in manual;
|
|
2815
|
-
- root `tsc --noEmit`, package builds, bundle builds, and all 12 public DTS
|
|
2816
|
-
entries on Node 20 and Node 22;
|
|
2825
|
+
### Current verification status
|
|
2826
|
+
|
|
2827
|
+
The v0.7.69 release validation covers the runtime migration, the Worker
|
|
2828
|
+
isolation follow-ups delivered ahead of their original v0.7.71/v0.7.72
|
|
2829
|
+
planning slots, the v0.7.67 external-agent/session additions, the v0.7.68
|
|
2830
|
+
experimental Memory Agent surface, and the v0.7.69 A2A/integration/shared-
|
|
2831
|
+
daemon delivery:
|
|
2832
|
+
|
|
2833
|
+
- Node 20 and Node 22 post-review regression for process cleanup, lock
|
|
2834
|
+
ownership, frozen eval inputs, A2A transport, and the built-in manual;
|
|
2835
|
+
- root `tsc --noEmit`, package builds, bundle builds, and all 12 public DTS
|
|
2836
|
+
entries on Node 20 and Node 22;
|
|
2817
2837
|
- runtime/daemon/SDK/ACP/REPL integration tests, including process-distinct SDK
|
|
2818
2838
|
auto-start and multi-client sessions/permissions;
|
|
2819
2839
|
- Worker Runtime identity, service parity, hard close, capability requirements,
|
|
@@ -2821,1525 +2841,1774 @@ daemon delivery:
|
|
|
2821
2841
|
- constructed-handler reverse tool RPC, abort bridging, CPU-loop termination,
|
|
2822
2842
|
respawn, and revoke/dispose queue drainage;
|
|
2823
2843
|
- context/tool-exposure eval gate;
|
|
2824
|
-
- embedded and hosted-daemon external-agent catalog/task parity, durable task
|
|
2825
|
-
recovery, policy/credential/artifact gates, and disabled-plane failure;
|
|
2826
|
-
- exact session `surface` filtering plus opaque cursor continuation through the
|
|
2827
|
-
narrow `/session` API, embedded Runtime, and daemon transport;
|
|
2828
|
-
- scoped zero-wait memory recall, read-only deliberate query, trace-only
|
|
2829
|
-
receipts, bounded episode review, governed consult-before-write promotion,
|
|
2830
|
-
and the self-contained `/experimental-memory` bundle/DTS surface;
|
|
2831
|
-
- bidirectional A2A 1.0 discovery/call/serve, durable task replay, no-code
|
|
2832
|
-
configuration, local-Agent binding, exact Skill-script admission, SSRF/
|
|
2833
|
-
credential boundaries, and the self-contained `/a2a` bundle/DTS surface;
|
|
2834
|
-
- split MCP/A2A/Extension configuration, lossless migration, canonical-template
|
|
2835
|
-
drift checks, last-known-good hot reload, draining, and restart-required
|
|
2836
|
-
classification;
|
|
2837
|
-
- atomic shared-daemon observation/resync, durable operations and same-session
|
|
2838
|
-
ordering, settings/grant CAS, AskUser/permission transport, credential/Host
|
|
2839
|
-
Tool reverse bridges, owner fencing, restart outcomes, and process-distinct
|
|
2840
|
-
client/daemon smokes with credential-canary scans;
|
|
2841
|
-
- a fresh `0.7.69` tarball consumer importing all 11 public subpaths, creating a
|
|
2842
|
-
Worker-hosted session, running the packaged CLI, and checking packaged DTS and
|
|
2843
|
-
Worker sidecars; plus a Windows x64 binary/version/sidecar smoke;
|
|
2844
|
-
- external fresh npm consumer installation of the `0.7.67` tarball, proving
|
|
2845
|
-
Worker isolation and a distinct daemon PID through the published subpath;
|
|
2844
|
+
- embedded and hosted-daemon external-agent catalog/task parity, durable task
|
|
2845
|
+
recovery, policy/credential/artifact gates, and disabled-plane failure;
|
|
2846
|
+
- exact session `surface` filtering plus opaque cursor continuation through the
|
|
2847
|
+
narrow `/session` API, embedded Runtime, and daemon transport;
|
|
2848
|
+
- scoped zero-wait memory recall, read-only deliberate query, trace-only
|
|
2849
|
+
receipts, bounded episode review, governed consult-before-write promotion,
|
|
2850
|
+
and the self-contained `/experimental-memory` bundle/DTS surface;
|
|
2851
|
+
- bidirectional A2A 1.0 discovery/call/serve, durable task replay, no-code
|
|
2852
|
+
configuration, local-Agent binding, exact Skill-script admission, SSRF/
|
|
2853
|
+
credential boundaries, and the self-contained `/a2a` bundle/DTS surface;
|
|
2854
|
+
- split MCP/A2A/Extension configuration, lossless migration, canonical-template
|
|
2855
|
+
drift checks, last-known-good hot reload, draining, and restart-required
|
|
2856
|
+
classification;
|
|
2857
|
+
- atomic shared-daemon observation/resync, durable operations and same-session
|
|
2858
|
+
ordering, settings/grant CAS, AskUser/permission transport, credential/Host
|
|
2859
|
+
Tool reverse bridges, owner fencing, restart outcomes, and process-distinct
|
|
2860
|
+
client/daemon smokes with credential-canary scans;
|
|
2861
|
+
- a fresh `0.7.69` tarball consumer importing all 11 public subpaths, creating a
|
|
2862
|
+
Worker-hosted session, running the packaged CLI, and checking packaged DTS and
|
|
2863
|
+
Worker sidecars; plus a Windows x64 binary/version/sidecar smoke;
|
|
2864
|
+
- external fresh npm consumer installation of the `0.7.67` tarball, proving
|
|
2865
|
+
Worker isolation and a distinct daemon PID through the published subpath;
|
|
2846
2866
|
- Ubuntu Node 22 Unix-domain-socket daemon gate, including two clients sharing
|
|
2847
2867
|
one runtime and cross-client permission resolution.
|
|
2848
2868
|
|
|
2849
|
-
The portable manual gates remain in
|
|
2850
|
-
`docs/test-guides/FEATURE_255_v0.7.66_TEST_GUIDE.md`,
|
|
2851
|
-
`FEATURE_256_v0.7.71_TEST_GUIDE.md`, and
|
|
2852
|
-
`FEATURE_257_v0.7.72_TEST_GUIDE.md` for release-machine verification; the latter
|
|
2853
|
-
two filenames retain their original planning slots while their content records
|
|
2854
|
-
the v0.7.66 delivery. v0.7.67 adds
|
|
2855
|
-
`FEATURE_258_v0.7.67_TEST_GUIDE.md`, `FEATURE_259_v0.7.67_TEST_GUIDE.md`, and
|
|
2856
|
-
`FEATURE_261_v0.7.67_TEST_GUIDE.md`; v0.7.69 adds
|
|
2857
|
-
`FEATURE_267_v0.7.69_TEST_GUIDE.md`, `FEATURE_268_v0.7.69_TEST_GUIDE.md`, and
|
|
2858
|
-
`FEATURE_269_v0.7.69_TEST_GUIDE.md`. The earlier release-preparation Actions
|
|
2859
|
-
run is not reused after the severe-fix review. Fresh GitHub Actions run
|
|
2860
|
-
`29385073422` passes the Node 20/22 build, bundle, DTS, and full-test matrix plus
|
|
2861
|
-
the Node 22 Unix-domain-socket daemon gate. npm publication remains a
|
|
2862
|
-
maintainer-owned manual step.
|
|
2863
|
-
|
|
2864
|
-
---
|
|
2865
|
-
|
|
2866
|
-
## 18. External-agent executor plane (FEATURE_258, v0.7.67)
|
|
2867
|
-
|
|
2868
|
-
FEATURE_258 lets an SDK host register remote agents without teaching the coding
|
|
2869
|
-
runtime a specific A2A, MCP, or HTTP client. The public contracts live in
|
|
2870
|
-
`@kodax-ai/kodax/agent`; the host-facing catalog and task services live on
|
|
2871
|
-
`@kodax-ai/kodax/runtime`.
|
|
2872
|
-
|
|
2873
|
-
### Ownership rule
|
|
2874
|
-
|
|
2875
|
-
An `AgentExecutorFactory` contains functions, so it cannot cross a Worker or
|
|
2876
|
-
daemon DTO boundary. Install factories where the Runtime owner executes:
|
|
2877
|
-
|
|
2878
|
-
| Desired owner | Supported construction |
|
|
2879
|
-
|---|---|
|
|
2880
|
-
| Private in-process owner | `createKodaXRuntime({ mode: 'embedded', isolation: 'inline', externalAgents })` |
|
|
2881
|
-
| New locally hosted daemon owner | `createKodaXRuntime({ mode: 'daemon', profile: '<unique>', externalAgents })` |
|
|
2882
|
-
| Existing daemon | Configure its owner, then attach with `connectKodaXRuntime({ requirements: { externalAgents: true } })`; a client cannot inject factories. |
|
|
2883
|
-
| Runtime Worker | The Worker owner must install factories itself; passing `externalAgents` from the parent is rejected. |
|
|
2884
|
-
|
|
2885
|
-
When `mode: 'daemon'` and `externalAgents` are supplied, the caller must win a
|
|
2886
|
-
new in-process daemon lease. KodaX rejects an already-running profile instead of
|
|
2887
|
-
silently replacing its executor configuration. Closing that owner facade shuts
|
|
2888
|
-
down the host it created; closing an ordinary attached client only detaches.
|
|
2889
|
-
|
|
2890
|
-
### Minimal owner and task flow
|
|
2891
|
-
|
|
2892
|
-
The reference executor below is a contract/conformance adapter. Replace it with
|
|
2893
|
-
your own `AgentExecutorFactory` for a real remote protocol.
|
|
2894
|
-
|
|
2895
|
-
```ts
|
|
2896
|
-
import {
|
|
2897
|
-
createReferenceAgentExecutorFactory,
|
|
2898
|
-
type ExternalAgentRegistration,
|
|
2899
|
-
} from '@kodax-ai/kodax/agent';
|
|
2900
|
-
import { createKodaXRuntime } from '@kodax-ai/kodax/runtime';
|
|
2901
|
-
|
|
2902
|
-
const runtime = await createKodaXRuntime({
|
|
2903
|
-
mode: 'embedded',
|
|
2904
|
-
isolation: 'inline',
|
|
2905
|
-
externalAgents: {
|
|
2906
|
-
factories: [createReferenceAgentExecutorFactory({
|
|
2907
|
-
executorId: 'example-http',
|
|
2908
|
-
protocol: 'http',
|
|
2909
|
-
})],
|
|
2910
|
-
policy: ({ registration, query }) => ({
|
|
2911
|
-
allowed: registration.enabled && query.readOnly === true,
|
|
2912
|
-
reasons: query.readOnly === true ? [] : ['This host allows read-only dispatch only.'],
|
|
2913
|
-
}),
|
|
2914
|
-
defaultContext: { actorId: 'desktop-host' },
|
|
2915
|
-
},
|
|
2916
|
-
});
|
|
2917
|
-
|
|
2918
|
-
const registration: ExternalAgentRegistration = {
|
|
2919
|
-
agentId: 'external:reviewer',
|
|
2920
|
-
displayName: 'Remote Reviewer',
|
|
2921
|
-
enabled: true,
|
|
2922
|
-
executorId: 'example-http',
|
|
2923
|
-
protocol: 'http',
|
|
2924
|
-
configurationRevision: 'reviewer-config-v1',
|
|
2925
|
-
endpointIdentityHash: 'sha256:replace-with-stable-endpoint-identity',
|
|
2926
|
-
skills: ['code-review'],
|
|
2927
|
-
inputModalities: ['text'],
|
|
2928
|
-
outputModalities: ['text'],
|
|
2929
|
-
capabilities: {
|
|
2930
|
-
streaming: 'supported',
|
|
2931
|
-
durableTasks: 'supported',
|
|
2932
|
-
inputRequired: 'conditional',
|
|
2933
|
-
cancellation: 'supported',
|
|
2934
|
-
artifacts: 'unsupported',
|
|
2935
|
-
},
|
|
2936
|
-
effects: { remote: 'read', workspace: 'proposal' },
|
|
2937
|
-
maxConcurrency: 1,
|
|
2938
|
-
};
|
|
2939
|
-
|
|
2940
|
-
try {
|
|
2941
|
-
await runtime.admin.agentRegistrations.upsert(registration);
|
|
2942
|
-
|
|
2943
|
-
const query = {
|
|
2944
|
-
actorId: 'desktop-host',
|
|
2945
|
-
requiredSkills: ['code-review'],
|
|
2946
|
-
readOnly: true,
|
|
2947
|
-
} as const;
|
|
2948
|
-
const available = await runtime.agents.listDispatchable(query);
|
|
2949
|
-
const preflight = await runtime.agents.preflight({
|
|
2950
|
-
agentId: 'external:reviewer',
|
|
2951
|
-
query,
|
|
2952
|
-
expectedConfigurationRevision: registration.configurationRevision,
|
|
2953
|
-
});
|
|
2954
|
-
if (!preflight.ok) throw new Error(preflight.reasons.join('; '));
|
|
2955
|
-
|
|
2956
|
-
const started = await runtime.agentTasks.start({
|
|
2957
|
-
agentId: 'external:reviewer',
|
|
2958
|
-
objective: 'Review the supplied immutable patch and return cited findings.',
|
|
2959
|
-
context: { actorId: 'desktop-host', runId: 'host-run-42' },
|
|
2960
|
-
readOnly: true,
|
|
2961
|
-
requiredSkills: ['code-review'],
|
|
2962
|
-
expectedConfigurationRevision: registration.configurationRevision,
|
|
2963
|
-
});
|
|
2964
|
-
const terminal = await runtime.agentTasks.wait(started.taskId, 60_000);
|
|
2965
|
-
console.log(available, terminal.state, terminal.output, terminal.usage);
|
|
2966
|
-
} finally {
|
|
2967
|
-
await runtime.close();
|
|
2968
|
-
}
|
|
2969
|
-
```
|
|
2970
|
-
|
|
2971
|
-
`runtime.agents.enabled` is the cheap external-plane feature check. If it is
|
|
2972
|
-
false, catalog queries can still return built-in local agents but no external
|
|
2973
|
-
agents; registration and task lists are empty, while point reads and mutations
|
|
2974
|
-
fail clearly. Set
|
|
2975
|
-
`requirements.externalAgents: true` when absence must abort connection.
|
|
2976
|
-
|
|
2977
|
-
### Service reference
|
|
2978
|
-
|
|
2979
|
-
| Surface | Methods | Contract |
|
|
2980
|
-
|---|---|---|
|
|
2981
|
-
| `runtime.admin.agentRegistrations` | `list`, `upsert`, `setEnabled`, `remove` | Durable owner configuration. `setEnabled` preserves the complete captured executor registration while changing admission. Mutations accept both `expectedConfigurationRevision` and `expectedManagementOwner`; `setEnabled` can also atomically `claimOwner` on an unowned registration and rejects another owner. List results expose `managementOwner` and `credentialConfigured`, never a credential value. The same contract is carried across the daemon transport. With no plane, `list()` is empty and mutations fail clearly. |
|
|
2982
|
-
| `runtime.agents` | `enabled`, `listDispatchable`, `describe`, `preflight` | Applies health, capability, effect, concurrency, credential-presence, configuration-revision, and host-policy checks before dispatch. |
|
|
2983
|
-
| `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. |
|
|
2984
|
-
|
|
2985
|
-
`agentTasks.events(taskId, cursor)` uses the last seen numeric event `seq` as its
|
|
2986
|
-
cursor and returns events with a greater sequence. `wait()` resolves only at a
|
|
2987
|
-
terminal task state and rejects on a positive `timeoutMs` expiry. `sendInput()`
|
|
2988
|
-
is valid only while the task reports `input-required` or `auth-required`.
|
|
2989
|
-
`reconcile()` asks the bound executor for authoritative remote state after an
|
|
2990
|
-
owner restart or uncertain failure.
|
|
2991
|
-
|
|
2992
|
-
For external tasks, the built-in stores persist an internal full registration
|
|
2993
|
-
snapshot before the public task ledger. It is keyed by Agent ID and revision,
|
|
2994
|
-
is never returned by task or daemon APIs, and lets an admitted task keep using
|
|
2995
|
-
its original executor route after registration update/removal and Runtime
|
|
2996
|
-
restart. The internal form fixes `enabled: true` and omits management ownership
|
|
2997
|
-
and health diagnostics. The task's public route summary is validated against that internal
|
|
2998
|
-
snapshot before recovery. Terminal task state is durable before the last
|
|
2999
|
-
unreferenced snapshot is removed; startup cleans crash-window orphans.
|
|
3000
|
-
|
|
3001
|
-
Custom `AgentExecutorPlaneStore` implementations should implement
|
|
3002
|
-
`loadTaskRegistrationSnapshots()` and `saveTaskRegistrationSnapshots()` as a
|
|
3003
|
-
pair and give one Runtime exclusive write ownership of that store. Omitting
|
|
3004
|
-
both remains compatible, but restart recovery then succeeds only while the
|
|
3005
|
-
exact current registration still exists. Store only non-secret executor config
|
|
3006
|
-
or secret references in `executorConfig`/`credentialRef`; the broker resolves
|
|
3007
|
-
the current referenced credential just in time, so removing a registration is
|
|
3008
|
-
not equivalent to revoking that credential at its issuer.
|
|
3009
|
-
|
|
3010
|
-
The owner plane has a terminal close contract. Closing it rejects every pending
|
|
3011
|
-
`wait()` (including a wait without `timeoutMs`), disposes its executor instances,
|
|
3012
|
-
and makes subsequent registration, catalog, preflight, and task calls reject
|
|
3013
|
-
with `Agent executor plane is closed.` One overall deadline covers admitted
|
|
3014
|
-
work plus executor disposal: the default is 30 seconds, and direct
|
|
3015
|
-
`createAgentExecutorPlane()` hosts may supply a positive finite
|
|
3016
|
-
`closeTimeoutMs`. A timeout rejects visibly even though already-admitted cleanup
|
|
3017
|
-
may finish in the background. Repeated `close()` calls are safe. SDK hosts
|
|
3018
|
-
should stop accepting work before closing the owner and must not retain a plane
|
|
3019
|
-
service as a reusable handle after Runtime shutdown.
|
|
3020
|
-
|
|
3021
|
-
Restricted Workflow scripts use the same route as direct SDK calls. Both
|
|
3022
|
-
`wf.spawnAgent()` and `wf.runAgent()` validate and forward
|
|
3023
|
-
`target: { agentId, expectedConfigurationRevision? }`; `phase` is forwarded as
|
|
3024
|
-
well. A blank ID or revision is rejected at the script boundary instead of
|
|
3025
|
-
silently falling back to the native child backend.
|
|
3026
|
-
|
|
3027
|
-
### Credential, artifact, and failure boundaries
|
|
3028
|
-
|
|
3029
|
-
- Put only a `credentialRef` in a registration. Resolve secret material through
|
|
3030
|
-
`AgentCredentialBroker.withCredential()`; do not place tokens in
|
|
3031
|
-
`executorConfig`, events, diagnostics, or task output.
|
|
3032
|
-
- Remote artifacts are denied by default. Supply `artifactPolicy` to authorize
|
|
3033
|
-
each artifact before it materializes in the host boundary.
|
|
3034
|
-
- External agents may declare workspace effect `none` or `proposal`; direct
|
|
3035
|
-
workspace mutation is intentionally not a valid external registration.
|
|
3036
|
-
- Use `expectedConfigurationRevision` for dispatch. For registration mutations,
|
|
3037
|
-
compare both it and `expectedManagementOwner` so a same-revision ownership
|
|
3038
|
-
change cannot be overwritten from an earlier catalog read. Config managers
|
|
3039
|
-
should set a stable `managementOwner`; they may atomically claim an unowned
|
|
3040
|
-
legacy registration while disabling it, but must not mutate a registration
|
|
3041
|
-
owned by another manager.
|
|
3042
|
-
- Treat `configurationRevision` as the stable identity of immutable execution
|
|
3043
|
-
content, not as a small counter. The same content may deterministically reuse
|
|
3044
|
-
the same revision across remove/re-add or Runtime restart, but different
|
|
3045
|
-
endpoint, protocol, executor/auth config, capabilities, effects, Skills,
|
|
3046
|
-
modalities, or resource limits must never reuse it. Built-in A2A
|
|
3047
|
-
configuration derives it from content.
|
|
3048
|
-
- A remote start followed by uncertain local persistence is recorded as
|
|
3049
|
-
`unknown` with its executor reference preserved; reconcile it rather than
|
|
3050
|
-
blindly starting a duplicate. Stable idempotency keys protect retries.
|
|
3051
|
-
- The durable plane records provider-reported usage when available. It never
|
|
3052
|
-
invents missing token or cost fields.
|
|
3053
|
-
|
|
3054
|
-
For a production adapter, implement `preflight?`, `start`, `events`, `get`,
|
|
3055
|
-
`sendInput`, `cancel`, `reconcile`, and `dispose` on `AgentExecutor`. The factory
|
|
3056
|
-
receives `withCredential()` and `authorizeArtifact()` callbacks so protocol code
|
|
3057
|
-
cannot bypass the host's secret/artifact policies.
|
|
3058
|
-
|
|
3059
|
-
---
|
|
3060
|
-
|
|
3061
|
-
## 19. Session surface filtering and cursor pagination (FEATURE_261, v0.7.67)
|
|
3062
|
-
|
|
3063
|
-
The narrow session SDK and Runtime facade now share the same listing semantics:
|
|
3064
|
-
`surface` is an exact filter applied before `limit`, and `cursor` is an opaque
|
|
3065
|
-
continuation token carried by each returned summary.
|
|
3066
|
-
|
|
3067
|
-
### Narrow `/session` API
|
|
3068
|
-
|
|
3069
|
-
```ts
|
|
3070
|
-
import { listSessions, type SessionSummary } from '@kodax-ai/kodax/session';
|
|
3071
|
-
|
|
3072
|
-
const all: SessionSummary[] = [];
|
|
3073
|
-
let cursor: string | undefined;
|
|
3074
|
-
do {
|
|
3075
|
-
const page = await listSessions({
|
|
3076
|
-
scope: 'user',
|
|
3077
|
-
surface: 'partner',
|
|
3078
|
-
limit: 50,
|
|
3079
|
-
...(cursor ? { cursor } : {}),
|
|
3080
|
-
});
|
|
3081
|
-
all.push(...page);
|
|
3082
|
-
cursor = page.length === 50 ? page.at(-1)?.cursor : undefined;
|
|
3083
|
-
} while (cursor);
|
|
3084
|
-
```
|
|
3085
|
-
|
|
3086
|
-
### Embedded/daemon Runtime API
|
|
3087
|
-
|
|
3088
|
-
```ts
|
|
3089
|
-
const first = await runtime.sessions.list({ surface: 'acp', limit: 25 });
|
|
3090
|
-
const nextCursor = first.at(-1)?.cursor;
|
|
3091
|
-
const second = nextCursor
|
|
3092
|
-
? await runtime.sessions.list({ surface: 'acp', limit: 25, cursor: nextCursor })
|
|
3093
|
-
: [];
|
|
3094
|
-
```
|
|
3095
|
-
|
|
3096
|
-
The filter also composes with `projectRoot`, `scope`, `includeArchived`, `before`,
|
|
3097
|
-
and `tag`. Treat cursors as opaque: do not parse them, compare them to session
|
|
3098
|
-
IDs, or manufacture them. An invalid cursor produces an empty page on the narrow
|
|
3099
|
-
session API. A page shorter than the requested limit is terminal; a full page
|
|
3100
|
-
may continue with its last item's cursor.
|
|
3101
|
-
|
|
3102
|
-
The interactive `kodax -r` picker is a consumer of these session semantics, not
|
|
3103
|
-
a separate SDK API. Headless hosts should build their own UI on `listSessions()`
|
|
3104
|
-
or `runtime.sessions.list()` and resume by the selected full session ID.
|
|
3105
|
-
|
|
3106
|
-
---
|
|
3107
|
-
|
|
3108
|
-
## 20. Cost-disciplined workflow routing and telemetry (FEATURE_259, v0.7.67)
|
|
3109
|
-
|
|
3110
|
-
FEATURE_259 adds a public cost/quality contract without making the authoring LLM
|
|
3111
|
-
choose provider-specific model names. The SDK host maps semantic tiers to routes;
|
|
3112
|
-
workflow/child briefs request intent.
|
|
3113
|
-
|
|
3114
|
-
### Configure tiers and bounded concurrency
|
|
3115
|
-
|
|
3116
|
-
```ts
|
|
3117
|
-
import { join } from 'node:path';
|
|
3118
|
-
import { runKodaX } from '@kodax-ai/kodax/coding';
|
|
3119
|
-
|
|
3120
|
-
const workflowRunsBaseDir = join(process.cwd(), '.kodax-host', 'workflow-runs');
|
|
3121
|
-
const result = await runKodaX({
|
|
3122
|
-
provider: 'zai-coding',
|
|
3123
|
-
model: 'glm-5.2',
|
|
3124
|
-
agentMode: 'amaw',
|
|
3125
|
-
workflowRunsBaseDir,
|
|
3126
|
-
modelTiers: {
|
|
3127
|
-
fast: { provider: 'deepseek', model: 'deepseek-v4-flash' },
|
|
3128
|
-
deep: { provider: 'zai-coding', model: 'glm-5.2' },
|
|
3129
|
-
},
|
|
3130
|
-
workflow: { maxConcurrency: 3 },
|
|
3131
|
-
events: {
|
|
3132
|
-
onWorkflowAgentDigest: ({ runId, event }) => {
|
|
3133
|
-
if (event.type === 'agent_completed' || event.type === 'agent_unverified'
|
|
3134
|
-
|| event.type === 'agent_failed') {
|
|
3135
|
-
console.log(runId, event.data);
|
|
3136
|
-
}
|
|
3137
|
-
},
|
|
3138
|
-
},
|
|
3139
|
-
}, 'Review this change using scoped evidence packets.');
|
|
3140
|
-
|
|
3141
|
-
console.log(result.lastText);
|
|
3142
|
-
```
|
|
3143
|
-
|
|
3144
|
-
Tier rules are deliberately small:
|
|
3145
|
-
|
|
3146
|
-
- `fast`: mechanical read-only lookup. A write child is ineligible and safely
|
|
3147
|
-
inherits the parent route.
|
|
3148
|
-
- `balanced`: ordinary implementation/investigation/review; uses the parent
|
|
3149
|
-
route, so there is no separate `balanced` mapping.
|
|
3150
|
-
- `deep`: architecture, adversarial verification, severity calibration, and
|
|
3151
|
-
final synthesis.
|
|
3152
|
-
- An unconfigured or selector-shadowed tier inherits the appropriate explicit,
|
|
3153
|
-
specialist, or parent route and records why; it does not silently claim the
|
|
3154
|
-
requested route was applied.
|
|
3155
|
-
|
|
3156
|
-
The workflow authoring contract also supports `scopeSummary`, `constraints`,
|
|
3157
|
-
`evidenceRefs`, `verification`, `readOnly`, `outputSchema`, and `terseResult`.
|
|
3158
|
-
Use those fields to transfer a compact immutable packet instead of asking every
|
|
3159
|
-
child to rediscover the same repository context.
|
|
3160
|
-
|
|
3161
|
-
### Live route facts
|
|
3162
|
-
|
|
3163
|
-
Terminal `WorkflowEvent.data` may include `requestedTier`, `tierOutcome`,
|
|
3164
|
-
`providerSource`, `modelSource`, initial/final provider and model,
|
|
3165
|
-
`fallbackReason`, `iterations`, `durationMs`, `usage`, and `digestUsage`.
|
|
3166
|
-
Treat absent fields as unknown. In particular, KodaX does not fabricate usage
|
|
3167
|
-
for external executors that did not report it.
|
|
3168
|
-
|
|
3169
|
-
Direct child-dispatch consumers receive the typed `KodaXChildAgentResult.routeFacts`
|
|
3170
|
-
surface, including resolved effort and input/cache-read/output/digest token
|
|
3171
|
-
breakdown when known. Inline workflow consumers can subscribe to the raw
|
|
3172
|
-
`onWorkflowAgentDigest` event as above; GUI progress remains available through
|
|
3173
|
-
`onWorkflowProcessEvent` / `runtime.workflows`.
|
|
3174
|
-
|
|
3175
|
-
### Durable efficiency report
|
|
3176
|
-
|
|
3177
|
-
When `workflowRunsBaseDir` is supplied, every terminal workflow writes:
|
|
3178
|
-
|
|
3179
|
-
```text
|
|
3180
|
-
<workflowRunsBaseDir>/<runId>/run.json
|
|
3181
|
-
<workflowRunsBaseDir>/<runId>/events.jsonl
|
|
3182
|
-
<workflowRunsBaseDir>/<runId>/artifacts/*.json
|
|
3183
|
-
```
|
|
3184
|
-
|
|
3185
|
-
`run.json.efficiencyReport` includes:
|
|
3186
|
-
|
|
3187
|
-
- total/input/cache-read/output/digest tokens and wall-clock duration;
|
|
3188
|
-
- total starts, child turns, starts by `role/tier`, and primary-review starts;
|
|
3189
|
-
- duplicate primary packet reads plus verification/synthesis packet reads;
|
|
3190
|
-
- review/fix/re-review waves and structured review quality-gate outcomes;
|
|
3191
|
-
- `tokenCoverage.ok` plus missing local task IDs;
|
|
3192
|
-
- `excludedExternalTaskIds` for external tasks whose executor reported no usage.
|
|
3193
|
-
|
|
3194
|
-
Do not interpret `totalModelTokens: 0` as free execution unless
|
|
3195
|
-
`tokenCoverage.ok` is true and the relevant external task IDs are not excluded.
|
|
3196
|
-
The report is an audit/optimization artifact; correctness still comes from the
|
|
3197
|
-
workflow's structured findings, verification results, and quality gates.
|
|
3198
|
-
|
|
3199
|
-
---
|
|
3200
|
-
|
|
3201
|
-
## 21. Experimental governed memory — `/experimental-memory` (FEATURE_260, v0.7.68)
|
|
3202
|
-
|
|
3203
|
-
KodaX has one durable memory plane: the F228 Memory Control Plane. FEATURE_260
|
|
3204
|
-
adds a thin, opt-in agent/session API over that plane; it does not add a second
|
|
3205
|
-
database, filesystem memory actions, a resident memory specialist, or online
|
|
3206
|
-
self-modification.
|
|
3207
|
-
|
|
3208
|
-
Top-level `runKodaX()` coding runs wire this lifecycle automatically. They build
|
|
3209
|
-
an exact-scoped memory pack at session start, keep passive recall off the
|
|
3210
|
-
blocking hot path, expose `memory_recall` only when the memory session starts,
|
|
3211
|
-
record bounded observations/outcomes, and close the session at the run boundary.
|
|
3212
|
-
Use the direct SDK below when a custom host needs to own those boundaries.
|
|
3213
|
-
|
|
3214
|
-
### Minimal direct session
|
|
3215
|
-
|
|
3216
|
-
```ts
|
|
3217
|
-
import { createMemoryControlPlane } from '@kodax-ai/kodax/agent';
|
|
3218
|
-
import { createMemoryAgent } from '@kodax-ai/kodax/experimental-memory';
|
|
3219
|
-
|
|
3220
|
-
const identity = {
|
|
3221
|
-
tenantId: 'tenant:acme',
|
|
3222
|
-
workspaceId: 'workspace:desktop',
|
|
3223
|
-
userId: 'user:42',
|
|
3224
|
-
agentId: 'agent:reviewer',
|
|
3225
|
-
projectId: 'project:kodax',
|
|
3226
|
-
sessionId: 'session:20260712',
|
|
3227
|
-
};
|
|
3228
|
-
|
|
3229
|
-
const controlPlane = createMemoryControlPlane({
|
|
3230
|
-
cwd: process.cwd(),
|
|
3231
|
-
identity,
|
|
3232
|
-
projectDocs: [],
|
|
3233
|
-
discoverSkills: false,
|
|
3234
|
-
});
|
|
3235
|
-
const memory = createMemoryAgent({ controlPlane });
|
|
3236
|
-
const session = await memory.startSession({
|
|
3237
|
-
identity,
|
|
3238
|
-
objective: 'Review the runtime shutdown change',
|
|
3239
|
-
});
|
|
3240
|
-
|
|
3241
|
-
const immediate = session.recall({
|
|
3242
|
-
decisionRevision: 'decision:1',
|
|
3243
|
-
objective: 'Review the runtime shutdown change',
|
|
3244
|
-
decisionContext: 'Choosing the first verification step',
|
|
3245
|
-
decisionIntent: 'runtime shutdown regression',
|
|
3246
|
-
throughSequence: 0,
|
|
3247
|
-
});
|
|
3248
|
-
const deliberate = await session.query({
|
|
3249
|
-
decisionRevision: 'decision:2',
|
|
3250
|
-
need: 'What uncommon daemon cleanup failure happened before?',
|
|
3251
|
-
throughSequence: 0,
|
|
3252
|
-
});
|
|
3253
|
-
|
|
3254
|
-
await session.complete({
|
|
3255
|
-
status: 'succeeded',
|
|
3256
|
-
summary: 'Shutdown state replacement verified',
|
|
3257
|
-
evidence: [],
|
|
3258
|
-
});
|
|
3259
|
-
await session.close();
|
|
3260
|
-
```
|
|
3261
|
-
|
|
3262
|
-
`recall()` is synchronous: exact observations/hints can be returned immediately,
|
|
3263
|
-
while an optional semantic prefetch finishes for a later matching decision.
|
|
3264
|
-
`query()` is deliberate and read-only; one distinct query is admitted per
|
|
3265
|
-
decision epoch, and the result is bounded to at most three prompt-safe hints and
|
|
3266
|
-
512 estimated tokens. `undefined` means there is no governed reminder to inject.
|
|
3267
|
-
|
|
3268
|
-
### Evidence, tracing, and persistence boundaries
|
|
3269
|
-
|
|
3270
|
-
- Recalled content is low-authority evidence. Current repository/config/runtime
|
|
3271
|
-
facts must still come from current tools or host state.
|
|
3272
|
-
- `observe()` accepts monotonic, evidence-linked observations and rejects secret
|
|
3273
|
-
material. `rewind()` removes observations after a sequence boundary.
|
|
3274
|
-
- `complete()` can emit an Outcome Digest through `persistOutcomeDigest` and run
|
|
3275
|
-
bounded episode review through `reviewEpisode`; cancellation creates neither.
|
|
3276
|
-
- `onTrace` receives policy-versioned `MemoryDecisionReceipt` metadata that links
|
|
3277
|
-
candidates, selection, injection, and later outcome influence. Receipts are
|
|
3278
|
-
trace-only and contain no hidden reasoning.
|
|
3279
|
-
- Durable memory mutation remains owned by F228's
|
|
3280
|
-
proposal/preview/fingerprint/apply flow. `/memory` is the CLI governance
|
|
3281
|
-
surface; direct file/shell writes to managed memory roots are denied.
|
|
3282
|
-
- Identity/applicability matching is exact and fail-closed. Hosts should supply
|
|
3283
|
-
stable tenant/workspace/user/agent/project/session identifiers and must not put
|
|
3284
|
-
credentials into those identifiers.
|
|
3285
|
-
|
|
3286
|
-
The subpath is experimental and ESM-only. Treat its exported types as opt-in
|
|
3287
|
-
v0.7.x contracts; keep persistence and product policy behind your own adapter.
|
|
3288
|
-
|
|
3289
|
-
---
|
|
3290
|
-
|
|
3291
|
-
## 22. Bidirectional A2A 1.0 — `/a2a` (FEATURE_267, v0.7.69)
|
|
3292
|
-
|
|
3293
|
-
`@kodax-ai/kodax/a2a` is the protocol edge for the A2A 1.0 JSON-RPC/SSE
|
|
3294
|
-
profile. It composes the protocol-neutral F258 executor plane with the Runtime
|
|
3295
|
-
facade; `/agent` and `/coding` remain free of A2A wire types and dependencies.
|
|
3296
|
-
|
|
3297
|
-
### Call an A2A Agent through F258
|
|
3298
|
-
|
|
3299
|
-
Discovery is an explicit host action. The URL must match `allowedOrigins`, and
|
|
3300
|
-
the default safe transport pins the validated DNS address for each connection
|
|
3301
|
-
and independently revalidates redirects, rejects public plain HTTP, bounds
|
|
3302
|
-
time/body/redirects, and strips authorization on a cross-origin redirect. A
|
|
3303
|
-
custom `fetch` option is a trusted transport override: the embedder then owns
|
|
3304
|
-
equivalent DNS-to-connection binding in that transport or proxy.
|
|
3305
|
-
|
|
3306
|
-
The selected interface must remain on the Card's trusted origin. KodaX parses
|
|
3307
|
-
typed Card-level and Skill-level security declarations: requirement objects are
|
|
3308
|
-
alternatives (OR), every scheme inside one object is conjunctive (AND), and an
|
|
3309
|
-
empty requirement is anonymous. A configured credential is used only when one
|
|
3310
|
-
complete requirement is satisfiable; protected Skills that the configured
|
|
3311
|
-
profile cannot satisfy are not advertised to the Runtime catalog.
|
|
3312
|
-
|
|
3313
|
-
The built-in profiles are HTTP Bearer and OAuth 2.0 Client Credentials. The
|
|
3314
|
-
OAuth profile pins the Card scheme, issuer, exact token endpoint, client ID,
|
|
3315
|
-
secret reference, scopes, optional RFC 8707 resource, and client authentication
|
|
3316
|
-
method. The external Authorization Server—not the Agent and not KodaX—issues
|
|
3317
|
-
the access token. KodaX resolves the client secret only for refresh, keeps an
|
|
3318
|
-
expiring token in process memory, coalesces refreshes, and retries one RPC once
|
|
3319
|
-
with a fresh token after `401`. Card, Agent RPC, and token endpoints remain
|
|
3320
|
-
separate safe-fetch trust boundaries, so a remote Agent cannot redirect a task
|
|
3321
|
-
payload to the token origin. API key, Basic, interactive OAuth, OIDC, mTLS, and
|
|
3322
|
-
multi-scheme AND requirements fail explicitly in the built-in client.
|
|
3323
|
-
|
|
3324
|
-
```ts
|
|
3325
|
-
import {
|
|
3326
|
-
createA2AAgentExecutorFactory,
|
|
3327
|
-
discoverA2ARegistration,
|
|
3328
|
-
} from '@kodax-ai/kodax/a2a';
|
|
3329
|
-
import { createKodaXRuntime } from '@kodax-ai/kodax/runtime';
|
|
3330
|
-
|
|
3331
|
-
const client = {
|
|
3332
|
-
networkPolicy: {
|
|
3333
|
-
// Card/RPC and OAuth token endpoints are separate trust boundaries.
|
|
3334
|
-
allowedOrigins: ['https://reviewer.example', 'https://identity.example'],
|
|
3335
|
-
allowPrivateAddresses: false,
|
|
3336
|
-
requestTimeoutMs: 10_000,
|
|
3337
|
-
maxResponseBytes: 1_048_576,
|
|
3338
|
-
maxRedirects: 2,
|
|
3339
|
-
},
|
|
3340
|
-
pollIntervalMs: 500,
|
|
3341
|
-
} as const;
|
|
3342
|
-
|
|
3343
|
-
const discovered = await discoverA2ARegistration({
|
|
3344
|
-
agentId: 'external:a2a-reviewer',
|
|
3345
|
-
agentCardUrl: 'https://reviewer.example/.well-known/agent-card.json',
|
|
3346
|
-
credentialRef: 'a2a/reviewer',
|
|
3347
|
-
effects: { remote: 'read' },
|
|
3348
|
-
}, client);
|
|
3349
|
-
|
|
3350
|
-
const runtime = await createKodaXRuntime({
|
|
3351
|
-
mode: 'embedded',
|
|
3352
|
-
isolation: 'inline',
|
|
3353
|
-
externalAgents: {
|
|
3354
|
-
factories: [createA2AAgentExecutorFactory(client)],
|
|
3355
|
-
credentialBroker: {
|
|
3356
|
-
async withCredential(ref, use) {
|
|
3357
|
-
const value = ref === 'a2a/reviewer'
|
|
3358
|
-
? process.env.A2A_REVIEWER_TOKEN
|
|
3359
|
-
: ref === 'a2a/reviewer-client-secret'
|
|
3360
|
-
? process.env.A2A_REVIEWER_CLIENT_SECRET
|
|
3361
|
-
: undefined;
|
|
3362
|
-
if (!value) throw new Error(`Missing credential for reference: ${ref}.`);
|
|
3363
|
-
return use(value);
|
|
3364
|
-
},
|
|
3365
|
-
},
|
|
3366
|
-
policy: ({ registration }) => ({ allowed: registration.effects.remote === 'read' }),
|
|
3367
|
-
defaultContext: { actorId: 'a2a-host' },
|
|
3368
|
-
},
|
|
3369
|
-
});
|
|
3370
|
-
|
|
3371
|
-
await runtime.admin.agentRegistrations.upsert(discovered.registration);
|
|
3372
|
-
const started = await runtime.agentTasks.start({
|
|
3373
|
-
agentId: discovered.registration.agentId,
|
|
3374
|
-
objective: 'Review this change and return cited findings.',
|
|
3375
|
-
context: { actorId: 'a2a-host' },
|
|
3376
|
-
readOnly: true,
|
|
3377
|
-
expectedConfigurationRevision: discovered.registration.configurationRevision,
|
|
3378
|
-
});
|
|
3379
|
-
const terminal = await runtime.agentTasks.wait(started.taskId, 60_000);
|
|
3380
|
-
```
|
|
3381
|
-
|
|
3382
|
-
For OAuth, replace the legacy `credentialRef` input with the structured form;
|
|
3383
|
-
the same F258 `credentialBroker` must resolve `clientSecretRef`. The shared
|
|
3384
|
-
network policy must admit both origins, while each Card, RPC, and token request
|
|
3385
|
-
is still narrowed to its own exact origin:
|
|
3386
|
-
|
|
3387
|
-
```ts
|
|
3388
|
-
const discovered = await discoverA2ARegistration({
|
|
3389
|
-
agentId: 'external:a2a-reviewer',
|
|
3390
|
-
agentCardUrl: 'https://reviewer.example/.well-known/agent-card.json',
|
|
3391
|
-
authentication: {
|
|
3392
|
-
type: 'oauth2-client-credentials',
|
|
3393
|
-
scheme: 'enterprise-oauth',
|
|
3394
|
-
issuer: 'https://identity.example/',
|
|
3395
|
-
tokenUrl: 'https://identity.example/oauth/token',
|
|
3396
|
-
clientId: 'kodax-reviewer',
|
|
3397
|
-
clientSecretRef: 'a2a/reviewer-client-secret',
|
|
3398
|
-
scopes: ['a2a.invoke'],
|
|
3399
|
-
resource: 'https://reviewer.example/',
|
|
3400
|
-
clientAuthentication: 'client-secret-basic',
|
|
3401
|
-
},
|
|
3402
|
-
effects: { remote: 'read' },
|
|
3403
|
-
}, client);
|
|
3404
|
-
```
|
|
3405
|
-
|
|
3406
|
-
The executor supports durable task start/get, input continuation, cancel,
|
|
3407
|
-
reconcile, SSE events, and polling fallback. An ambiguous start is not retried
|
|
3408
|
-
automatically. A `credentialRef` is resolved just in time by the F258 broker;
|
|
3409
|
-
the registration, task store, and diagnostics never contain the credential.
|
|
3410
|
-
Authenticated SSE uses that same broker. JSON-RPC ID/version and task/context
|
|
3411
|
-
correlation are validated before an event is accepted; if a stream ends
|
|
3412
|
-
normally before a terminal snapshot, the executor resumes bounded polling.
|
|
3413
|
-
Streamed `artifactUpdate` chunks are accumulated by artifact ID according to
|
|
3414
|
-
`append`, and direct Message file Parts are preserved as authorized artifact
|
|
3415
|
-
references.
|
|
3416
|
-
|
|
3417
|
-
### Built-in configured path (no host code)
|
|
3418
|
-
|
|
3419
|
-
The CLI product path stores one user document at
|
|
3420
|
-
`~/.kodax/integrations/a2a.json` and uses the same F258 plane:
|
|
3421
|
-
|
|
3422
|
-
```bash
|
|
3423
|
-
kodax a2a add reviewer https://reviewer.example/.well-known/agent-card.json \
|
|
3424
|
-
--credential-env A2A_REVIEWER_TOKEN --effect read
|
|
3425
|
-
kodax a2a test reviewer
|
|
3426
|
-
kodax a2a call reviewer "Review this document"
|
|
3427
|
-
```
|
|
3428
|
-
|
|
3429
|
-
The no-code OAuth path stores only the environment-variable name for the client
|
|
3430
|
-
secret. It can be staged disabled and hot-activated later:
|
|
3431
|
-
|
|
3432
|
-
```bash
|
|
3433
|
-
export A2A_REVIEWER_CLIENT_SECRET='provisioned-out-of-band'
|
|
3434
|
-
# PowerShell: $env:A2A_REVIEWER_CLIENT_SECRET='provisioned-out-of-band'
|
|
3435
|
-
# PowerShell: use one line or replace each trailing \ with a backtick.
|
|
3436
|
-
kodax a2a add reviewer https://reviewer.example/.well-known/agent-card.json \
|
|
3437
|
-
--disabled --effect read --oauth-scheme enterprise-oauth \
|
|
3438
|
-
--oauth-issuer https://identity.example/ \
|
|
3439
|
-
--oauth-token-url https://identity.example/oauth/token \
|
|
3440
|
-
--oauth-client-id kodax-reviewer \
|
|
3441
|
-
--oauth-client-secret-env A2A_REVIEWER_CLIENT_SECRET \
|
|
3442
|
-
--oauth-scope a2a.invoke --oauth-resource https://reviewer.example/
|
|
3443
|
-
kodax a2a enable reviewer
|
|
3444
|
-
kodax a2a disable reviewer
|
|
3445
|
-
```
|
|
3446
|
-
|
|
3447
|
-
Embedded CLI Runtimes and the user-owned daemon automatically reconcile these
|
|
3448
|
-
entries as `external:<name>`. Discovery/update failure retains that entry's
|
|
3449
|
-
last-known-good registration; another entry can still update. The environment
|
|
3450
|
-
broker resolves `credentialEnv` only at call time. Automatic Runtime
|
|
3451
|
-
registration accepts public HTTPS and exact loopback targets; explicit private
|
|
3452
|
-
network access remains an operator action on the direct CLI/SDK path.
|
|
3453
|
-
|
|
3454
|
-
`enabled` is desired state in `a2a.json`, not a fabricated cross-process live
|
|
3455
|
-
flag. `a2a list` reports configured entries and that desired state. The owning
|
|
3456
|
-
Runtime's `admin.agentRegistrations.list()` is authoritative for applied
|
|
3457
|
-
registrations. Automatic reconciliation handles disables/removals first,
|
|
3458
|
-
skips unchanged peers, performs no Card or token request for disabled entries,
|
|
3459
|
-
and rediscovers before re-enable. Once the owning Runtime observes and applies
|
|
3460
|
-
the revision, disable blocks all new starts, including an explicit
|
|
3461
|
-
`external:<name>` target, but does not cancel or break an already admitted task.
|
|
3462
|
-
The CLI mutation returning is not cross-process acknowledgement. A failed
|
|
3463
|
-
activation remains retryable through the owning
|
|
3464
|
-
`ConfiguredA2ARuntimeHandle.reload()` even when the disk revision is unchanged;
|
|
3465
|
-
the passive `kodax integrations reload` command validates only its own process.
|
|
3466
|
-
|
|
3467
|
-
`kodax a2a test` performs Card discovery and security planning only. It never
|
|
3468
|
-
requests an OAuth access token; token acquisition starts at `a2a call` or the
|
|
3469
|
-
first Runtime dispatch.
|
|
3470
|
-
|
|
3471
|
-
Inbound publication is also no-code:
|
|
3472
|
-
|
|
3473
|
-
```bash
|
|
3474
|
-
export KODAX_A2A_TOKEN='replace-with-a-long-random-token'
|
|
3475
|
-
# PowerShell: $env:KODAX_A2A_TOKEN='replace-with-a-long-random-token'
|
|
3476
|
-
kodax a2a expose # Runtime default Agent
|
|
3477
|
-
kodax a2a expose document-agent # ~/.kodax/agents/document-agent.md
|
|
3478
|
-
kodax a2a serve --port 8765
|
|
3479
|
-
```
|
|
3480
|
-
|
|
3481
|
-
The fixed token above is the compatibility profile. For dynamic production
|
|
3482
|
-
tokens, configure KodaX as an OAuth Resource Server and point it at an external
|
|
3483
|
-
issuer:
|
|
3484
|
-
|
|
3485
|
-
```bash
|
|
3486
|
-
kodax a2a expose document-agent --auth oauth2-jwt \
|
|
3487
|
-
--oauth-scheme enterprise-oauth \
|
|
3488
|
-
--oauth-issuer https://identity.example/ \
|
|
3489
|
-
--oauth-audience https://kodax.example/a2a \
|
|
3490
|
-
--oauth-jwks-url https://identity.example/.well-known/jwks.json \
|
|
3491
|
-
--oauth-token-url https://identity.example/oauth/token \
|
|
3492
|
-
--oauth-metadata-url https://identity.example/.well-known/oauth-authorization-server \
|
|
3493
|
-
--required-scope a2a.invoke
|
|
3494
|
-
kodax a2a serve --port 8765
|
|
3495
|
-
```
|
|
3496
|
-
|
|
3497
|
-
The Authorization Server authenticates clients, provisions client IDs/secrets,
|
|
3498
|
-
issues/rotates/revokes tokens and, for JWT access tokens, signs them and
|
|
3499
|
-
publishes metadata/JWKS. The calling A2A
|
|
3500
|
-
client obtains a token out of band or with Client Credentials and sends it in
|
|
3501
|
-
the Bearer header. KodaX validates JWT type, asymmetric signature, issuer,
|
|
3502
|
-
audience, lifetime, subject, and required scopes before task lookup, then maps
|
|
3503
|
-
`sub` to the A2A principal. Missing/invalid credentials return `401`; a valid
|
|
3504
|
-
token without the required scope returns `403 insufficient_scope`. KodaX does
|
|
3505
|
-
not hold the issuer signing key or expose token, refresh, client-registration,
|
|
3506
|
-
login, or consent endpoints. Opaque-token introspection and mTLS deployments
|
|
3507
|
-
must use a host authentication adapter or reverse proxy. Offline JWT/JWKS
|
|
3508
|
-
validation also cannot observe immediate per-token revocation: use short access
|
|
3509
|
-
token lifetimes, signing-key rotation, or an introspecting proxy/adapter when
|
|
3510
|
-
that property is required.
|
|
3511
|
-
|
|
3512
|
-
#### Upgrade retained pre-realm tasks
|
|
3513
|
-
|
|
3514
|
-
Realm-aware task ownership intentionally has no normal-request legacy fallback:
|
|
3515
|
-
an authority switch must never adopt tasks merely because it reuses a subject.
|
|
3516
|
-
If a v0.7.70 task store must remain addressable after upgrading, stop the A2A
|
|
3517
|
-
server and first inspect an exact-owner migration plan:
|
|
3518
|
-
|
|
3519
|
-
```bash
|
|
3520
|
-
kodax a2a migrate-tasks
|
|
3521
|
-
kodax a2a migrate-tasks --apply --confirm-server-stopped
|
|
3522
|
-
|
|
3523
|
-
# OAuth identity is token-specific, so provide the known historical subject.
|
|
3524
|
-
kodax a2a migrate-tasks --subject trusted-orchestrator
|
|
3525
|
-
```
|
|
3526
|
-
|
|
3527
|
-
The configured Bearer profile supplies its fixed `principalId`; OAuth requires
|
|
3528
|
-
`--subject`. Dry-run does not rewrite `tasks.json`. Apply rekeys only exact
|
|
3529
|
-
matches, preserves unmatched records, and refuses a live task-store owner.
|
|
3530
|
-
Custom SDK hosts can plan multiple known owners without exposing raw tokens:
|
|
3531
|
-
|
|
3532
|
-
```ts
|
|
3533
|
-
import { migrateA2ALegacyTaskOwners } from '@kodax-ai/kodax/a2a';
|
|
3534
|
-
|
|
3535
|
-
const mappings = [{
|
|
3536
|
-
securityRealm: 'oauth2-jwt:https://identity.example/',
|
|
3537
|
-
subject: 'trusted-orchestrator',
|
|
3538
|
-
}] as const;
|
|
3539
|
-
const plan = migrateA2ALegacyTaskOwners({
|
|
3540
|
-
dataDir: '/var/lib/kodax/a2a', mappings, apply: false,
|
|
3541
|
-
});
|
|
3542
|
-
|
|
3543
|
-
// After the host/operator verifies the plan:
|
|
3544
|
-
if (plan.matchedLegacyTaskCount > 0) {
|
|
3545
|
-
migrateA2ALegacyTaskOwners({
|
|
3546
|
-
dataDir: '/var/lib/kodax/a2a', mappings, apply: true,
|
|
3547
|
-
});
|
|
3548
|
-
}
|
|
3549
|
-
```
|
|
3550
|
-
|
|
3551
|
-
The SDK also accepts `tenant` when a custom authentication adapter historically
|
|
3552
|
-
returned one. Two mappings that claim the same legacy owner for different
|
|
3553
|
-
realms are ambiguous and rejected; split or guessed ownership is never applied.
|
|
3554
|
-
|
|
3555
|
-
`a2a serve` resolves its Runtime provider in this order: explicit CLI option,
|
|
3556
|
-
environment, core configuration, then the built-in default. Provider-compatible
|
|
3557
|
-
model selection follows the normal hosted Runtime rule. A selected Markdown
|
|
3558
|
-
Agent may declare its own validated `provider`; remote A2A input cannot choose
|
|
3559
|
-
or override provider, model, reasoning, profile, workspace, or tools.
|
|
3560
|
-
|
|
3561
|
-
`expose` validates a named user Markdown Agent before writing its reference.
|
|
3562
|
-
`serve` loads configured MCP and Extensions before it resolves the execution
|
|
3563
|
-
binding or opens a socket. Native workspace read tools are admitted by
|
|
3564
|
-
workspace access; writes, narrow Extension Tools, MCP capabilities, subagents,
|
|
3565
|
-
and isolated Skill scripts require their corresponding exact `toolPolicy`
|
|
3566
|
-
authority. Internal Skills come from `~/.kodax/skills`, `~/.agents/skills`,
|
|
3567
|
-
plugins, and built-ins; public Agent Card skills are a separate explicit
|
|
3568
|
-
projection and never reveal the private Skill inventory.
|
|
3569
|
-
|
|
3570
|
-
The running server pins Agent, Skill, workspace, tool registration, process and
|
|
3571
|
-
store revisions. Card/auth/limits can hot reload; execution-authority changes
|
|
3572
|
-
require an explicit restart. Managed contexts live below
|
|
3573
|
-
`~/kodax_a2a_server_workspace/<runtime-profile>/contexts/<context-key>/`. Exact Skill scripts require
|
|
3574
|
-
`process: isolated`, an admitted `scripts/...` path, and a passing
|
|
3575
|
-
`kodax sandbox doctor`; KodaX never falls back to an unsandboxed shell.
|
|
3576
|
-
|
|
3577
|
-
Every concrete file reached by `read`, `grep`, or `glob` is checked against the
|
|
3578
|
-
bound workspace. Child runs inherit ceilings for native reads, tools, Skills,
|
|
3579
|
-
and Skill scripts; they cannot expand the parent's admitted authority.
|
|
3580
|
-
|
|
3581
|
-
### Publish one KodaX Agent
|
|
3582
|
-
|
|
3583
|
-
Publication is host-owned and opt-in. The public card describes only the
|
|
3584
|
-
configured Agent, media types, and skills. Authentication runs before task
|
|
3585
|
-
lookup; authorization runs per operation; task visibility is principal-scoped.
|
|
3586
|
-
|
|
3587
|
-
```ts
|
|
3588
|
-
import {
|
|
3589
|
-
createBearerEnvA2AAuthentication,
|
|
3590
|
-
createKodaXA2AServer,
|
|
3591
|
-
} from '@kodax-ai/kodax/a2a';
|
|
3592
|
-
import { createKodaXRuntime } from '@kodax-ai/kodax/runtime';
|
|
3593
|
-
|
|
3594
|
-
const runtime = await createKodaXRuntime({ mode: 'embedded', isolation: 'inline' });
|
|
3595
|
-
const server = createKodaXA2AServer({
|
|
3596
|
-
runtime,
|
|
3597
|
-
dataDir: '/var/lib/kodax/a2a',
|
|
3598
|
-
agent: {
|
|
3599
|
-
name: 'KodaX Reviewer',
|
|
3600
|
-
description: 'Reviews bounded code changes.',
|
|
3601
|
-
version: '1.0.0',
|
|
3602
|
-
publicBaseUrl: 'https://kodax.example',
|
|
3603
|
-
skills: [{ id: 'review', name: 'Review', description: 'Review code.', tags: ['code'] }],
|
|
3604
|
-
inputModes: ['text/plain'],
|
|
3605
|
-
outputModes: ['text/plain'],
|
|
3606
|
-
},
|
|
3607
|
-
authentication: createBearerEnvA2AAuthentication({
|
|
3608
|
-
type: 'bearer-env',
|
|
3609
|
-
tokenEnv: 'KODAX_A2A_TOKEN',
|
|
3610
|
-
principalId: 'trusted-orchestrator',
|
|
3611
|
-
}),
|
|
3612
|
-
async authorize({ principal }) { return principal.scopes.includes('a2a:invoke'); },
|
|
3613
|
-
limits: {
|
|
3614
|
-
maxRequestBytes: 1_048_576,
|
|
3615
|
-
maxPartBytes: 524_288,
|
|
3616
|
-
maxConcurrentTasks: 8,
|
|
3617
|
-
maxTaskWaitMs: 30_000,
|
|
3618
|
-
maxActiveTasksPerPrincipal: 8,
|
|
3619
|
-
maxRetainedTasksPerPrincipal: 64,
|
|
3620
|
-
maxEventsPerTask: 1_000,
|
|
3621
|
-
maxEventBytesPerTask: 16_777_216,
|
|
3622
|
-
maxWorkspaceBytesPerContext: 1_073_741_824,
|
|
3623
|
-
},
|
|
3624
|
-
});
|
|
3625
|
-
|
|
3626
|
-
// Development only: the built-in listener refuses non-loopback hosts.
|
|
3627
|
-
const localBaseUrl = await server.listen({ hostname: '127.0.0.1', port: 0 });
|
|
3628
|
-
```
|
|
3629
|
-
|
|
3630
|
-
Production hosts route `GET /.well-known/agent-card.json` and canonical
|
|
3631
|
-
JSON-RPC `POST /a2a` to `server.handle(request)` behind their own TLS
|
|
3632
|
-
terminator. `POST /` remains an accepted compatibility alias. `listen()` waits for durable recovery before
|
|
3633
|
-
it resolves. A host that wires `handle()` directly may explicitly await
|
|
3634
|
-
`server.whenReady()` before it starts accepting traffic; `handle()` also waits
|
|
3635
|
-
for the same recovery promise. The durable edge store supports get/list,
|
|
3636
|
-
continuation, cancellation, ordered SSE subscription, and surviving Runtime-run
|
|
3637
|
-
reattachment after an edge restart. Push notifications, A2A 0.3, gRPC, and
|
|
3638
|
-
HTTP+JSON are not advertised; unsupported push methods return the standard
|
|
3639
|
-
`PushNotificationNotSupportedError`.
|
|
3640
|
-
|
|
3641
|
-
Non-streaming `SendMessage` waits at most `maxTaskWaitMs` (30 seconds by
|
|
3642
|
-
default). When that bound is reached the response contains the current working
|
|
3643
|
-
task; it does not cancel the Runtime run, and clients can continue with
|
|
3644
|
-
`GetTask` or `SubscribeToTask`.
|
|
3645
|
-
|
|
3646
|
-
When a task enters `INPUT_REQUIRED`, the next accepted input answers the pending
|
|
3647
|
-
interaction on the original Runtime run; it does not start a replacement run.
|
|
3648
|
-
History length and list filters are validated and bounded. Task listing uses a
|
|
3649
|
-
stable opaque cursor, while per-principal retention prunes only the oldest
|
|
3650
|
-
terminal records. Terminal subscriptions and failed-start resources are closed
|
|
3651
|
-
by their owning lifecycle.
|
|
3652
|
-
|
|
3653
|
-
Remote messages are ordinary user inputs. They cannot select provider, model,
|
|
3654
|
-
profile, tools, working directory, permission mode, or Runtime configuration.
|
|
3655
|
-
URL parts are rejected; inline raw/data parts are bounded and materialized under
|
|
3656
|
-
the server-owned data directory. Responses expose final approved output only,
|
|
3657
|
-
not system prompts, reasoning deltas, tool payloads, credentials, or local paths.
|
|
3658
|
-
|
|
3659
|
-
Generated files are published only through the trusted output broker: a normal
|
|
3660
|
-
tool or Extension stages a file in the context's `.kodax-a2a-staging` area, or
|
|
3661
|
-
a successfully admitted `run_skill_script` promotes one of its declared
|
|
3662
|
-
outputs. The server rechecks that the result is a regular non-symlink file in
|
|
3663
|
-
the real bound workspace and applies part-size/output-mode limits before
|
|
3664
|
-
inlining it. A declaration from a failed Skill run, an ordinary `write`/`edit`
|
|
3665
|
-
elsewhere in the workspace, and a local path in model text never become A2A
|
|
3666
|
-
artifacts implicitly.
|
|
3667
|
-
|
|
3668
|
-
The normative baseline is A2A repository commit
|
|
3669
|
-
`2183794bfb9b67af4aee1be0a0ef726050642873`, protocol `1.0`, with
|
|
3670
|
-
`specification/a2a.proto` SHA-256
|
|
3671
|
-
`e195bf96ab630c69797851970203e1b2b6b19528f2e9803b7d904b91a5104016`.
|
|
3672
|
-
|
|
3673
|
-
---
|
|
3674
|
-
|
|
3675
|
-
## 23. Shared Coder daemon for Space and IDE hosts (FEATURE_269, v0.7.69)
|
|
3676
|
-
|
|
3677
|
-
FEATURE_269 makes one local daemon the source of truth for a Coder profile.
|
|
3678
|
-
CLI, Space, IDE, and SDK clients can observe and control the same sessions and
|
|
3679
|
-
runs. The transport remains local to the current OS user; it is not a remote
|
|
3680
|
-
collaboration protocol. Closing a client detaches that client and does not stop
|
|
3681
|
-
the daemon or another client's run.
|
|
3682
|
-
|
|
3683
|
-
Partner is deliberately outside this migration. Keep Partner on its existing
|
|
3684
|
-
inline callbacks and give it a distinct product data root and sessions root.
|
|
3685
|
-
Do not point a Partner inline Runtime at the Coder daemon profile or the Coder
|
|
3686
|
-
data root.
|
|
3687
|
-
|
|
3688
|
-
### Connect and fail closed on required capabilities
|
|
3689
|
-
|
|
3690
|
-
Space should own the daemon SDK client in Electron Main. Persist a random,
|
|
3691
|
-
stable `instanceId` and a separate 32+ character `instanceSecret` per Space
|
|
3692
|
-
installation. Store the secret in the OS keychain; never accept either value
|
|
3693
|
-
from renderer or model output. `connectKodaXRuntime()` is attach-only unless `autoStart: true`.
|
|
3694
|
-
An explicit inline rollback policy blocks auto-start until the owner policy is
|
|
3695
|
-
explicitly changed back to daemon.
|
|
3696
|
-
|
|
3697
|
-
For Electron, `homeDir` is still the CLI-style base directory, not
|
|
3698
|
-
`process.env.KODAX_HOME`. Packaged/asar applications may use `autoStart: true`
|
|
3699
|
-
directly; the SDK launches only the daemon child in Electron's Node execution
|
|
3700
|
-
mode and does not mutate the application's environment or start a second GUI
|
|
3701
|
-
instance. `ELECTRON_RUN_AS_NODE` exists only at the child exec boundary and is
|
|
3702
|
-
removed before daemon application code loads, so Bash, MCP, LSP, sandboxed
|
|
3703
|
-
commands, and ordinary external processes do not inherit Electron Node mode.
|
|
3704
|
-
|
|
3705
|
-
Packaged auto-start requires Electron's `RunAsNode` fuse, which Electron enables
|
|
3706
|
-
by default. If an embedder deliberately disables that fuse, the packaged
|
|
3707
|
-
executable cannot serve as a detached Node host: start the daemon with an
|
|
3708
|
-
ordinary Node/CLI process and use attach-only mode instead. A packaged
|
|
3709
|
-
`autoStart: true` timeout includes this fuse requirement in its diagnostic; the
|
|
3710
|
-
SDK does not relaunch the GUI or silently fall back to an inline Runtime.
|
|
3711
|
-
|
|
3712
|
-
```ts
|
|
3713
|
-
import { connectKodaXRuntime } from '@kodax-ai/kodax/runtime';
|
|
3714
|
-
|
|
3715
|
-
const runtime = await connectKodaXRuntime({
|
|
3716
|
-
profile: 'coder',
|
|
3717
|
-
autoStart: true,
|
|
3718
|
-
homeDir: coderRuntimeBaseDir, // owns <coderRuntimeBaseDir>/.kodax
|
|
3719
|
-
clientInfo: {
|
|
3720
|
-
name: 'kodax-space',
|
|
3721
|
-
version: '0.1.32',
|
|
3722
|
-
instanceId: spaceInstallationId,
|
|
3723
|
-
instanceSecret: await spaceKeychain.readRuntimeClientSecret(),
|
|
3724
|
-
},
|
|
3725
|
-
capabilities: {
|
|
3726
|
-
richEvents: true,
|
|
3727
|
-
permissionPrompts: true,
|
|
3728
|
-
operationDeduplication: true,
|
|
3729
|
-
},
|
|
3730
|
-
requirements: {
|
|
3731
|
-
operationDeduplication: 1,
|
|
3732
|
-
sessionObservation: 1,
|
|
3733
|
-
afterTurnInput: 1,
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
}
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
`
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
}
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
}
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
`
|
|
3899
|
-
`
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
}
|
|
3909
|
-
|
|
2869
|
+
The portable manual gates remain in
|
|
2870
|
+
`docs/test-guides/FEATURE_255_v0.7.66_TEST_GUIDE.md`,
|
|
2871
|
+
`FEATURE_256_v0.7.71_TEST_GUIDE.md`, and
|
|
2872
|
+
`FEATURE_257_v0.7.72_TEST_GUIDE.md` for release-machine verification; the latter
|
|
2873
|
+
two filenames retain their original planning slots while their content records
|
|
2874
|
+
the v0.7.66 delivery. v0.7.67 adds
|
|
2875
|
+
`FEATURE_258_v0.7.67_TEST_GUIDE.md`, `FEATURE_259_v0.7.67_TEST_GUIDE.md`, and
|
|
2876
|
+
`FEATURE_261_v0.7.67_TEST_GUIDE.md`; v0.7.69 adds
|
|
2877
|
+
`FEATURE_267_v0.7.69_TEST_GUIDE.md`, `FEATURE_268_v0.7.69_TEST_GUIDE.md`, and
|
|
2878
|
+
`FEATURE_269_v0.7.69_TEST_GUIDE.md`. The earlier release-preparation Actions
|
|
2879
|
+
run is not reused after the severe-fix review. Fresh GitHub Actions run
|
|
2880
|
+
`29385073422` passes the Node 20/22 build, bundle, DTS, and full-test matrix plus
|
|
2881
|
+
the Node 22 Unix-domain-socket daemon gate. npm publication remains a
|
|
2882
|
+
maintainer-owned manual step.
|
|
2883
|
+
|
|
2884
|
+
---
|
|
2885
|
+
|
|
2886
|
+
## 18. External-agent executor plane (FEATURE_258, v0.7.67)
|
|
2887
|
+
|
|
2888
|
+
FEATURE_258 lets an SDK host register remote agents without teaching the coding
|
|
2889
|
+
runtime a specific A2A, MCP, or HTTP client. The public contracts live in
|
|
2890
|
+
`@kodax-ai/kodax/agent`; the host-facing catalog and task services live on
|
|
2891
|
+
`@kodax-ai/kodax/runtime`.
|
|
2892
|
+
|
|
2893
|
+
### Ownership rule
|
|
2894
|
+
|
|
2895
|
+
An `AgentExecutorFactory` contains functions, so it cannot cross a Worker or
|
|
2896
|
+
daemon DTO boundary. Install factories where the Runtime owner executes:
|
|
2897
|
+
|
|
2898
|
+
| Desired owner | Supported construction |
|
|
2899
|
+
|---|---|
|
|
2900
|
+
| Private in-process owner | `createKodaXRuntime({ mode: 'embedded', isolation: 'inline', externalAgents })` |
|
|
2901
|
+
| New locally hosted daemon owner | `createKodaXRuntime({ mode: 'daemon', profile: '<unique>', externalAgents })` |
|
|
2902
|
+
| Existing daemon | Configure its owner, then attach with `connectKodaXRuntime({ requirements: { externalAgents: true } })`; a client cannot inject factories. |
|
|
2903
|
+
| Runtime Worker | The Worker owner must install factories itself; passing `externalAgents` from the parent is rejected. |
|
|
2904
|
+
|
|
2905
|
+
When `mode: 'daemon'` and `externalAgents` are supplied, the caller must win a
|
|
2906
|
+
new in-process daemon lease. KodaX rejects an already-running profile instead of
|
|
2907
|
+
silently replacing its executor configuration. Closing that owner facade shuts
|
|
2908
|
+
down the host it created; closing an ordinary attached client only detaches.
|
|
2909
|
+
|
|
2910
|
+
### Minimal owner and task flow
|
|
2911
|
+
|
|
2912
|
+
The reference executor below is a contract/conformance adapter. Replace it with
|
|
2913
|
+
your own `AgentExecutorFactory` for a real remote protocol.
|
|
2914
|
+
|
|
2915
|
+
```ts
|
|
2916
|
+
import {
|
|
2917
|
+
createReferenceAgentExecutorFactory,
|
|
2918
|
+
type ExternalAgentRegistration,
|
|
2919
|
+
} from '@kodax-ai/kodax/agent';
|
|
2920
|
+
import { createKodaXRuntime } from '@kodax-ai/kodax/runtime';
|
|
2921
|
+
|
|
2922
|
+
const runtime = await createKodaXRuntime({
|
|
2923
|
+
mode: 'embedded',
|
|
2924
|
+
isolation: 'inline',
|
|
2925
|
+
externalAgents: {
|
|
2926
|
+
factories: [createReferenceAgentExecutorFactory({
|
|
2927
|
+
executorId: 'example-http',
|
|
2928
|
+
protocol: 'http',
|
|
2929
|
+
})],
|
|
2930
|
+
policy: ({ registration, query }) => ({
|
|
2931
|
+
allowed: registration.enabled && query.readOnly === true,
|
|
2932
|
+
reasons: query.readOnly === true ? [] : ['This host allows read-only dispatch only.'],
|
|
2933
|
+
}),
|
|
2934
|
+
defaultContext: { actorId: 'desktop-host' },
|
|
2935
|
+
},
|
|
2936
|
+
});
|
|
2937
|
+
|
|
2938
|
+
const registration: ExternalAgentRegistration = {
|
|
2939
|
+
agentId: 'external:reviewer',
|
|
2940
|
+
displayName: 'Remote Reviewer',
|
|
2941
|
+
enabled: true,
|
|
2942
|
+
executorId: 'example-http',
|
|
2943
|
+
protocol: 'http',
|
|
2944
|
+
configurationRevision: 'reviewer-config-v1',
|
|
2945
|
+
endpointIdentityHash: 'sha256:replace-with-stable-endpoint-identity',
|
|
2946
|
+
skills: ['code-review'],
|
|
2947
|
+
inputModalities: ['text'],
|
|
2948
|
+
outputModalities: ['text'],
|
|
2949
|
+
capabilities: {
|
|
2950
|
+
streaming: 'supported',
|
|
2951
|
+
durableTasks: 'supported',
|
|
2952
|
+
inputRequired: 'conditional',
|
|
2953
|
+
cancellation: 'supported',
|
|
2954
|
+
artifacts: 'unsupported',
|
|
2955
|
+
},
|
|
2956
|
+
effects: { remote: 'read', workspace: 'proposal' },
|
|
2957
|
+
maxConcurrency: 1,
|
|
2958
|
+
};
|
|
2959
|
+
|
|
2960
|
+
try {
|
|
2961
|
+
await runtime.admin.agentRegistrations.upsert(registration);
|
|
2962
|
+
|
|
2963
|
+
const query = {
|
|
2964
|
+
actorId: 'desktop-host',
|
|
2965
|
+
requiredSkills: ['code-review'],
|
|
2966
|
+
readOnly: true,
|
|
2967
|
+
} as const;
|
|
2968
|
+
const available = await runtime.agents.listDispatchable(query);
|
|
2969
|
+
const preflight = await runtime.agents.preflight({
|
|
2970
|
+
agentId: 'external:reviewer',
|
|
2971
|
+
query,
|
|
2972
|
+
expectedConfigurationRevision: registration.configurationRevision,
|
|
2973
|
+
});
|
|
2974
|
+
if (!preflight.ok) throw new Error(preflight.reasons.join('; '));
|
|
2975
|
+
|
|
2976
|
+
const started = await runtime.agentTasks.start({
|
|
2977
|
+
agentId: 'external:reviewer',
|
|
2978
|
+
objective: 'Review the supplied immutable patch and return cited findings.',
|
|
2979
|
+
context: { actorId: 'desktop-host', runId: 'host-run-42' },
|
|
2980
|
+
readOnly: true,
|
|
2981
|
+
requiredSkills: ['code-review'],
|
|
2982
|
+
expectedConfigurationRevision: registration.configurationRevision,
|
|
2983
|
+
});
|
|
2984
|
+
const terminal = await runtime.agentTasks.wait(started.taskId, 60_000);
|
|
2985
|
+
console.log(available, terminal.state, terminal.output, terminal.usage);
|
|
2986
|
+
} finally {
|
|
2987
|
+
await runtime.close();
|
|
2988
|
+
}
|
|
2989
|
+
```
|
|
2990
|
+
|
|
2991
|
+
`runtime.agents.enabled` is the cheap external-plane feature check. If it is
|
|
2992
|
+
false, catalog queries can still return built-in local agents but no external
|
|
2993
|
+
agents; registration and task lists are empty, while point reads and mutations
|
|
2994
|
+
fail clearly. Set
|
|
2995
|
+
`requirements.externalAgents: true` when absence must abort connection.
|
|
2996
|
+
|
|
2997
|
+
### Service reference
|
|
2998
|
+
|
|
2999
|
+
| Surface | Methods | Contract |
|
|
3000
|
+
|---|---|---|
|
|
3001
|
+
| `runtime.admin.agentRegistrations` | `list`, `upsert`, `setEnabled`, `remove` | Durable owner configuration. `setEnabled` preserves the complete captured executor registration while changing admission. Mutations accept both `expectedConfigurationRevision` and `expectedManagementOwner`; `setEnabled` can also atomically `claimOwner` on an unowned registration and rejects another owner. List results expose `managementOwner` and `credentialConfigured`, never a credential value. The same contract is carried across the daemon transport. With no plane, `list()` is empty and mutations fail clearly. |
|
|
3002
|
+
| `runtime.agents` | `enabled`, `listDispatchable`, `describe`, `preflight` | Applies health, capability, effect, concurrency, credential-presence, configuration-revision, and host-policy checks before dispatch. |
|
|
3003
|
+
| `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. |
|
|
3004
|
+
|
|
3005
|
+
`agentTasks.events(taskId, cursor)` uses the last seen numeric event `seq` as its
|
|
3006
|
+
cursor and returns events with a greater sequence. `wait()` resolves only at a
|
|
3007
|
+
terminal task state and rejects on a positive `timeoutMs` expiry. `sendInput()`
|
|
3008
|
+
is valid only while the task reports `input-required` or `auth-required`.
|
|
3009
|
+
`reconcile()` asks the bound executor for authoritative remote state after an
|
|
3010
|
+
owner restart or uncertain failure.
|
|
3011
|
+
|
|
3012
|
+
For external tasks, the built-in stores persist an internal full registration
|
|
3013
|
+
snapshot before the public task ledger. It is keyed by Agent ID and revision,
|
|
3014
|
+
is never returned by task or daemon APIs, and lets an admitted task keep using
|
|
3015
|
+
its original executor route after registration update/removal and Runtime
|
|
3016
|
+
restart. The internal form fixes `enabled: true` and omits management ownership
|
|
3017
|
+
and health diagnostics. The task's public route summary is validated against that internal
|
|
3018
|
+
snapshot before recovery. Terminal task state is durable before the last
|
|
3019
|
+
unreferenced snapshot is removed; startup cleans crash-window orphans.
|
|
3020
|
+
|
|
3021
|
+
Custom `AgentExecutorPlaneStore` implementations should implement
|
|
3022
|
+
`loadTaskRegistrationSnapshots()` and `saveTaskRegistrationSnapshots()` as a
|
|
3023
|
+
pair and give one Runtime exclusive write ownership of that store. Omitting
|
|
3024
|
+
both remains compatible, but restart recovery then succeeds only while the
|
|
3025
|
+
exact current registration still exists. Store only non-secret executor config
|
|
3026
|
+
or secret references in `executorConfig`/`credentialRef`; the broker resolves
|
|
3027
|
+
the current referenced credential just in time, so removing a registration is
|
|
3028
|
+
not equivalent to revoking that credential at its issuer.
|
|
3029
|
+
|
|
3030
|
+
The owner plane has a terminal close contract. Closing it rejects every pending
|
|
3031
|
+
`wait()` (including a wait without `timeoutMs`), disposes its executor instances,
|
|
3032
|
+
and makes subsequent registration, catalog, preflight, and task calls reject
|
|
3033
|
+
with `Agent executor plane is closed.` One overall deadline covers admitted
|
|
3034
|
+
work plus executor disposal: the default is 30 seconds, and direct
|
|
3035
|
+
`createAgentExecutorPlane()` hosts may supply a positive finite
|
|
3036
|
+
`closeTimeoutMs`. A timeout rejects visibly even though already-admitted cleanup
|
|
3037
|
+
may finish in the background. Repeated `close()` calls are safe. SDK hosts
|
|
3038
|
+
should stop accepting work before closing the owner and must not retain a plane
|
|
3039
|
+
service as a reusable handle after Runtime shutdown.
|
|
3040
|
+
|
|
3041
|
+
Restricted Workflow scripts use the same route as direct SDK calls. Both
|
|
3042
|
+
`wf.spawnAgent()` and `wf.runAgent()` validate and forward
|
|
3043
|
+
`target: { agentId, expectedConfigurationRevision? }`; `phase` is forwarded as
|
|
3044
|
+
well. A blank ID or revision is rejected at the script boundary instead of
|
|
3045
|
+
silently falling back to the native child backend.
|
|
3046
|
+
|
|
3047
|
+
### Credential, artifact, and failure boundaries
|
|
3048
|
+
|
|
3049
|
+
- Put only a `credentialRef` in a registration. Resolve secret material through
|
|
3050
|
+
`AgentCredentialBroker.withCredential()`; do not place tokens in
|
|
3051
|
+
`executorConfig`, events, diagnostics, or task output.
|
|
3052
|
+
- Remote artifacts are denied by default. Supply `artifactPolicy` to authorize
|
|
3053
|
+
each artifact before it materializes in the host boundary.
|
|
3054
|
+
- External agents may declare workspace effect `none` or `proposal`; direct
|
|
3055
|
+
workspace mutation is intentionally not a valid external registration.
|
|
3056
|
+
- Use `expectedConfigurationRevision` for dispatch. For registration mutations,
|
|
3057
|
+
compare both it and `expectedManagementOwner` so a same-revision ownership
|
|
3058
|
+
change cannot be overwritten from an earlier catalog read. Config managers
|
|
3059
|
+
should set a stable `managementOwner`; they may atomically claim an unowned
|
|
3060
|
+
legacy registration while disabling it, but must not mutate a registration
|
|
3061
|
+
owned by another manager.
|
|
3062
|
+
- Treat `configurationRevision` as the stable identity of immutable execution
|
|
3063
|
+
content, not as a small counter. The same content may deterministically reuse
|
|
3064
|
+
the same revision across remove/re-add or Runtime restart, but different
|
|
3065
|
+
endpoint, protocol, executor/auth config, capabilities, effects, Skills,
|
|
3066
|
+
modalities, or resource limits must never reuse it. Built-in A2A
|
|
3067
|
+
configuration derives it from content.
|
|
3068
|
+
- A remote start followed by uncertain local persistence is recorded as
|
|
3069
|
+
`unknown` with its executor reference preserved; reconcile it rather than
|
|
3070
|
+
blindly starting a duplicate. Stable idempotency keys protect retries.
|
|
3071
|
+
- The durable plane records provider-reported usage when available. It never
|
|
3072
|
+
invents missing token or cost fields.
|
|
3073
|
+
|
|
3074
|
+
For a production adapter, implement `preflight?`, `start`, `events`, `get`,
|
|
3075
|
+
`sendInput`, `cancel`, `reconcile`, and `dispose` on `AgentExecutor`. The factory
|
|
3076
|
+
receives `withCredential()` and `authorizeArtifact()` callbacks so protocol code
|
|
3077
|
+
cannot bypass the host's secret/artifact policies.
|
|
3078
|
+
|
|
3079
|
+
---
|
|
3080
|
+
|
|
3081
|
+
## 19. Session surface filtering and cursor pagination (FEATURE_261, v0.7.67)
|
|
3082
|
+
|
|
3083
|
+
The narrow session SDK and Runtime facade now share the same listing semantics:
|
|
3084
|
+
`surface` is an exact filter applied before `limit`, and `cursor` is an opaque
|
|
3085
|
+
continuation token carried by each returned summary.
|
|
3086
|
+
|
|
3087
|
+
### Narrow `/session` API
|
|
3088
|
+
|
|
3089
|
+
```ts
|
|
3090
|
+
import { listSessions, type SessionSummary } from '@kodax-ai/kodax/session';
|
|
3091
|
+
|
|
3092
|
+
const all: SessionSummary[] = [];
|
|
3093
|
+
let cursor: string | undefined;
|
|
3094
|
+
do {
|
|
3095
|
+
const page = await listSessions({
|
|
3096
|
+
scope: 'user',
|
|
3097
|
+
surface: 'partner',
|
|
3098
|
+
limit: 50,
|
|
3099
|
+
...(cursor ? { cursor } : {}),
|
|
3100
|
+
});
|
|
3101
|
+
all.push(...page);
|
|
3102
|
+
cursor = page.length === 50 ? page.at(-1)?.cursor : undefined;
|
|
3103
|
+
} while (cursor);
|
|
3104
|
+
```
|
|
3105
|
+
|
|
3106
|
+
### Embedded/daemon Runtime API
|
|
3107
|
+
|
|
3108
|
+
```ts
|
|
3109
|
+
const first = await runtime.sessions.list({ surface: 'acp', limit: 25 });
|
|
3110
|
+
const nextCursor = first.at(-1)?.cursor;
|
|
3111
|
+
const second = nextCursor
|
|
3112
|
+
? await runtime.sessions.list({ surface: 'acp', limit: 25, cursor: nextCursor })
|
|
3113
|
+
: [];
|
|
3114
|
+
```
|
|
3115
|
+
|
|
3116
|
+
The filter also composes with `projectRoot`, `scope`, `includeArchived`, `before`,
|
|
3117
|
+
and `tag`. Treat cursors as opaque: do not parse them, compare them to session
|
|
3118
|
+
IDs, or manufacture them. An invalid cursor produces an empty page on the narrow
|
|
3119
|
+
session API. A page shorter than the requested limit is terminal; a full page
|
|
3120
|
+
may continue with its last item's cursor.
|
|
3121
|
+
|
|
3122
|
+
The interactive `kodax -r` picker is a consumer of these session semantics, not
|
|
3123
|
+
a separate SDK API. Headless hosts should build their own UI on `listSessions()`
|
|
3124
|
+
or `runtime.sessions.list()` and resume by the selected full session ID.
|
|
3125
|
+
|
|
3126
|
+
---
|
|
3127
|
+
|
|
3128
|
+
## 20. Cost-disciplined workflow routing and telemetry (FEATURE_259, v0.7.67)
|
|
3129
|
+
|
|
3130
|
+
FEATURE_259 adds a public cost/quality contract without making the authoring LLM
|
|
3131
|
+
choose provider-specific model names. The SDK host maps semantic tiers to routes;
|
|
3132
|
+
workflow/child briefs request intent.
|
|
3133
|
+
|
|
3134
|
+
### Configure tiers and bounded concurrency
|
|
3135
|
+
|
|
3136
|
+
```ts
|
|
3137
|
+
import { join } from 'node:path';
|
|
3138
|
+
import { runKodaX } from '@kodax-ai/kodax/coding';
|
|
3139
|
+
|
|
3140
|
+
const workflowRunsBaseDir = join(process.cwd(), '.kodax-host', 'workflow-runs');
|
|
3141
|
+
const result = await runKodaX({
|
|
3142
|
+
provider: 'zai-coding',
|
|
3143
|
+
model: 'glm-5.2',
|
|
3144
|
+
agentMode: 'amaw',
|
|
3145
|
+
workflowRunsBaseDir,
|
|
3146
|
+
modelTiers: {
|
|
3147
|
+
fast: { provider: 'deepseek', model: 'deepseek-v4-flash' },
|
|
3148
|
+
deep: { provider: 'zai-coding', model: 'glm-5.2' },
|
|
3149
|
+
},
|
|
3150
|
+
workflow: { maxConcurrency: 3 },
|
|
3151
|
+
events: {
|
|
3152
|
+
onWorkflowAgentDigest: ({ runId, event }) => {
|
|
3153
|
+
if (event.type === 'agent_completed' || event.type === 'agent_unverified'
|
|
3154
|
+
|| event.type === 'agent_failed') {
|
|
3155
|
+
console.log(runId, event.data);
|
|
3156
|
+
}
|
|
3157
|
+
},
|
|
3158
|
+
},
|
|
3159
|
+
}, 'Review this change using scoped evidence packets.');
|
|
3160
|
+
|
|
3161
|
+
console.log(result.lastText);
|
|
3162
|
+
```
|
|
3163
|
+
|
|
3164
|
+
Tier rules are deliberately small:
|
|
3165
|
+
|
|
3166
|
+
- `fast`: mechanical read-only lookup. A write child is ineligible and safely
|
|
3167
|
+
inherits the parent route.
|
|
3168
|
+
- `balanced`: ordinary implementation/investigation/review; uses the parent
|
|
3169
|
+
route, so there is no separate `balanced` mapping.
|
|
3170
|
+
- `deep`: architecture, adversarial verification, severity calibration, and
|
|
3171
|
+
final synthesis.
|
|
3172
|
+
- An unconfigured or selector-shadowed tier inherits the appropriate explicit,
|
|
3173
|
+
specialist, or parent route and records why; it does not silently claim the
|
|
3174
|
+
requested route was applied.
|
|
3175
|
+
|
|
3176
|
+
The workflow authoring contract also supports `scopeSummary`, `constraints`,
|
|
3177
|
+
`evidenceRefs`, `verification`, `readOnly`, `outputSchema`, and `terseResult`.
|
|
3178
|
+
Use those fields to transfer a compact immutable packet instead of asking every
|
|
3179
|
+
child to rediscover the same repository context.
|
|
3180
|
+
|
|
3181
|
+
### Live route facts
|
|
3182
|
+
|
|
3183
|
+
Terminal `WorkflowEvent.data` may include `requestedTier`, `tierOutcome`,
|
|
3184
|
+
`providerSource`, `modelSource`, initial/final provider and model,
|
|
3185
|
+
`fallbackReason`, `iterations`, `durationMs`, `usage`, and `digestUsage`.
|
|
3186
|
+
Treat absent fields as unknown. In particular, KodaX does not fabricate usage
|
|
3187
|
+
for external executors that did not report it.
|
|
3188
|
+
|
|
3189
|
+
Direct child-dispatch consumers receive the typed `KodaXChildAgentResult.routeFacts`
|
|
3190
|
+
surface, including resolved effort and input/cache-read/output/digest token
|
|
3191
|
+
breakdown when known. Inline workflow consumers can subscribe to the raw
|
|
3192
|
+
`onWorkflowAgentDigest` event as above; GUI progress remains available through
|
|
3193
|
+
`onWorkflowProcessEvent` / `runtime.workflows`.
|
|
3194
|
+
|
|
3195
|
+
### Durable efficiency report
|
|
3196
|
+
|
|
3197
|
+
When `workflowRunsBaseDir` is supplied, every terminal workflow writes:
|
|
3198
|
+
|
|
3199
|
+
```text
|
|
3200
|
+
<workflowRunsBaseDir>/<runId>/run.json
|
|
3201
|
+
<workflowRunsBaseDir>/<runId>/events.jsonl
|
|
3202
|
+
<workflowRunsBaseDir>/<runId>/artifacts/*.json
|
|
3203
|
+
```
|
|
3204
|
+
|
|
3205
|
+
`run.json.efficiencyReport` includes:
|
|
3206
|
+
|
|
3207
|
+
- total/input/cache-read/output/digest tokens and wall-clock duration;
|
|
3208
|
+
- total starts, child turns, starts by `role/tier`, and primary-review starts;
|
|
3209
|
+
- duplicate primary packet reads plus verification/synthesis packet reads;
|
|
3210
|
+
- review/fix/re-review waves and structured review quality-gate outcomes;
|
|
3211
|
+
- `tokenCoverage.ok` plus missing local task IDs;
|
|
3212
|
+
- `excludedExternalTaskIds` for external tasks whose executor reported no usage.
|
|
3213
|
+
|
|
3214
|
+
Do not interpret `totalModelTokens: 0` as free execution unless
|
|
3215
|
+
`tokenCoverage.ok` is true and the relevant external task IDs are not excluded.
|
|
3216
|
+
The report is an audit/optimization artifact; correctness still comes from the
|
|
3217
|
+
workflow's structured findings, verification results, and quality gates.
|
|
3218
|
+
|
|
3219
|
+
---
|
|
3220
|
+
|
|
3221
|
+
## 21. Experimental governed memory — `/experimental-memory` (FEATURE_260, v0.7.68)
|
|
3222
|
+
|
|
3223
|
+
KodaX has one durable memory plane: the F228 Memory Control Plane. FEATURE_260
|
|
3224
|
+
adds a thin, opt-in agent/session API over that plane; it does not add a second
|
|
3225
|
+
database, filesystem memory actions, a resident memory specialist, or online
|
|
3226
|
+
self-modification.
|
|
3227
|
+
|
|
3228
|
+
Top-level `runKodaX()` coding runs wire this lifecycle automatically. They build
|
|
3229
|
+
an exact-scoped memory pack at session start, keep passive recall off the
|
|
3230
|
+
blocking hot path, expose `memory_recall` only when the memory session starts,
|
|
3231
|
+
record bounded observations/outcomes, and close the session at the run boundary.
|
|
3232
|
+
Use the direct SDK below when a custom host needs to own those boundaries.
|
|
3233
|
+
|
|
3234
|
+
### Minimal direct session
|
|
3235
|
+
|
|
3236
|
+
```ts
|
|
3237
|
+
import { createMemoryControlPlane } from '@kodax-ai/kodax/agent';
|
|
3238
|
+
import { createMemoryAgent } from '@kodax-ai/kodax/experimental-memory';
|
|
3239
|
+
|
|
3240
|
+
const identity = {
|
|
3241
|
+
tenantId: 'tenant:acme',
|
|
3242
|
+
workspaceId: 'workspace:desktop',
|
|
3243
|
+
userId: 'user:42',
|
|
3244
|
+
agentId: 'agent:reviewer',
|
|
3245
|
+
projectId: 'project:kodax',
|
|
3246
|
+
sessionId: 'session:20260712',
|
|
3247
|
+
};
|
|
3248
|
+
|
|
3249
|
+
const controlPlane = createMemoryControlPlane({
|
|
3250
|
+
cwd: process.cwd(),
|
|
3251
|
+
identity,
|
|
3252
|
+
projectDocs: [],
|
|
3253
|
+
discoverSkills: false,
|
|
3254
|
+
});
|
|
3255
|
+
const memory = createMemoryAgent({ controlPlane });
|
|
3256
|
+
const session = await memory.startSession({
|
|
3257
|
+
identity,
|
|
3258
|
+
objective: 'Review the runtime shutdown change',
|
|
3259
|
+
});
|
|
3260
|
+
|
|
3261
|
+
const immediate = session.recall({
|
|
3262
|
+
decisionRevision: 'decision:1',
|
|
3263
|
+
objective: 'Review the runtime shutdown change',
|
|
3264
|
+
decisionContext: 'Choosing the first verification step',
|
|
3265
|
+
decisionIntent: 'runtime shutdown regression',
|
|
3266
|
+
throughSequence: 0,
|
|
3267
|
+
});
|
|
3268
|
+
const deliberate = await session.query({
|
|
3269
|
+
decisionRevision: 'decision:2',
|
|
3270
|
+
need: 'What uncommon daemon cleanup failure happened before?',
|
|
3271
|
+
throughSequence: 0,
|
|
3272
|
+
});
|
|
3273
|
+
|
|
3274
|
+
await session.complete({
|
|
3275
|
+
status: 'succeeded',
|
|
3276
|
+
summary: 'Shutdown state replacement verified',
|
|
3277
|
+
evidence: [],
|
|
3278
|
+
});
|
|
3279
|
+
await session.close();
|
|
3280
|
+
```
|
|
3281
|
+
|
|
3282
|
+
`recall()` is synchronous: exact observations/hints can be returned immediately,
|
|
3283
|
+
while an optional semantic prefetch finishes for a later matching decision.
|
|
3284
|
+
`query()` is deliberate and read-only; one distinct query is admitted per
|
|
3285
|
+
decision epoch, and the result is bounded to at most three prompt-safe hints and
|
|
3286
|
+
512 estimated tokens. `undefined` means there is no governed reminder to inject.
|
|
3287
|
+
|
|
3288
|
+
### Evidence, tracing, and persistence boundaries
|
|
3289
|
+
|
|
3290
|
+
- Recalled content is low-authority evidence. Current repository/config/runtime
|
|
3291
|
+
facts must still come from current tools or host state.
|
|
3292
|
+
- `observe()` accepts monotonic, evidence-linked observations and rejects secret
|
|
3293
|
+
material. `rewind()` removes observations after a sequence boundary.
|
|
3294
|
+
- `complete()` can emit an Outcome Digest through `persistOutcomeDigest` and run
|
|
3295
|
+
bounded episode review through `reviewEpisode`; cancellation creates neither.
|
|
3296
|
+
- `onTrace` receives policy-versioned `MemoryDecisionReceipt` metadata that links
|
|
3297
|
+
candidates, selection, injection, and later outcome influence. Receipts are
|
|
3298
|
+
trace-only and contain no hidden reasoning.
|
|
3299
|
+
- Durable memory mutation remains owned by F228's
|
|
3300
|
+
proposal/preview/fingerprint/apply flow. `/memory` is the CLI governance
|
|
3301
|
+
surface; direct file/shell writes to managed memory roots are denied.
|
|
3302
|
+
- Identity/applicability matching is exact and fail-closed. Hosts should supply
|
|
3303
|
+
stable tenant/workspace/user/agent/project/session identifiers and must not put
|
|
3304
|
+
credentials into those identifiers.
|
|
3305
|
+
|
|
3306
|
+
The subpath is experimental and ESM-only. Treat its exported types as opt-in
|
|
3307
|
+
v0.7.x contracts; keep persistence and product policy behind your own adapter.
|
|
3308
|
+
|
|
3309
|
+
---
|
|
3310
|
+
|
|
3311
|
+
## 22. Bidirectional A2A 1.0 — `/a2a` (FEATURE_267, v0.7.69)
|
|
3312
|
+
|
|
3313
|
+
`@kodax-ai/kodax/a2a` is the protocol edge for the A2A 1.0 JSON-RPC/SSE
|
|
3314
|
+
profile. It composes the protocol-neutral F258 executor plane with the Runtime
|
|
3315
|
+
facade; `/agent` and `/coding` remain free of A2A wire types and dependencies.
|
|
3316
|
+
|
|
3317
|
+
### Call an A2A Agent through F258
|
|
3318
|
+
|
|
3319
|
+
Discovery is an explicit host action. The URL must match `allowedOrigins`, and
|
|
3320
|
+
the default safe transport pins the validated DNS address for each connection
|
|
3321
|
+
and independently revalidates redirects, rejects public plain HTTP, bounds
|
|
3322
|
+
time/body/redirects, and strips authorization on a cross-origin redirect. A
|
|
3323
|
+
custom `fetch` option is a trusted transport override: the embedder then owns
|
|
3324
|
+
equivalent DNS-to-connection binding in that transport or proxy.
|
|
3325
|
+
|
|
3326
|
+
The selected interface must remain on the Card's trusted origin. KodaX parses
|
|
3327
|
+
typed Card-level and Skill-level security declarations: requirement objects are
|
|
3328
|
+
alternatives (OR), every scheme inside one object is conjunctive (AND), and an
|
|
3329
|
+
empty requirement is anonymous. A configured credential is used only when one
|
|
3330
|
+
complete requirement is satisfiable; protected Skills that the configured
|
|
3331
|
+
profile cannot satisfy are not advertised to the Runtime catalog.
|
|
3332
|
+
|
|
3333
|
+
The built-in profiles are HTTP Bearer and OAuth 2.0 Client Credentials. The
|
|
3334
|
+
OAuth profile pins the Card scheme, issuer, exact token endpoint, client ID,
|
|
3335
|
+
secret reference, scopes, optional RFC 8707 resource, and client authentication
|
|
3336
|
+
method. The external Authorization Server—not the Agent and not KodaX—issues
|
|
3337
|
+
the access token. KodaX resolves the client secret only for refresh, keeps an
|
|
3338
|
+
expiring token in process memory, coalesces refreshes, and retries one RPC once
|
|
3339
|
+
with a fresh token after `401`. Card, Agent RPC, and token endpoints remain
|
|
3340
|
+
separate safe-fetch trust boundaries, so a remote Agent cannot redirect a task
|
|
3341
|
+
payload to the token origin. API key, Basic, interactive OAuth, OIDC, mTLS, and
|
|
3342
|
+
multi-scheme AND requirements fail explicitly in the built-in client.
|
|
3343
|
+
|
|
3344
|
+
```ts
|
|
3345
|
+
import {
|
|
3346
|
+
createA2AAgentExecutorFactory,
|
|
3347
|
+
discoverA2ARegistration,
|
|
3348
|
+
} from '@kodax-ai/kodax/a2a';
|
|
3349
|
+
import { createKodaXRuntime } from '@kodax-ai/kodax/runtime';
|
|
3350
|
+
|
|
3351
|
+
const client = {
|
|
3352
|
+
networkPolicy: {
|
|
3353
|
+
// Card/RPC and OAuth token endpoints are separate trust boundaries.
|
|
3354
|
+
allowedOrigins: ['https://reviewer.example', 'https://identity.example'],
|
|
3355
|
+
allowPrivateAddresses: false,
|
|
3356
|
+
requestTimeoutMs: 10_000,
|
|
3357
|
+
maxResponseBytes: 1_048_576,
|
|
3358
|
+
maxRedirects: 2,
|
|
3359
|
+
},
|
|
3360
|
+
pollIntervalMs: 500,
|
|
3361
|
+
} as const;
|
|
3362
|
+
|
|
3363
|
+
const discovered = await discoverA2ARegistration({
|
|
3364
|
+
agentId: 'external:a2a-reviewer',
|
|
3365
|
+
agentCardUrl: 'https://reviewer.example/.well-known/agent-card.json',
|
|
3366
|
+
credentialRef: 'a2a/reviewer',
|
|
3367
|
+
effects: { remote: 'read' },
|
|
3368
|
+
}, client);
|
|
3369
|
+
|
|
3370
|
+
const runtime = await createKodaXRuntime({
|
|
3371
|
+
mode: 'embedded',
|
|
3372
|
+
isolation: 'inline',
|
|
3373
|
+
externalAgents: {
|
|
3374
|
+
factories: [createA2AAgentExecutorFactory(client)],
|
|
3375
|
+
credentialBroker: {
|
|
3376
|
+
async withCredential(ref, use) {
|
|
3377
|
+
const value = ref === 'a2a/reviewer'
|
|
3378
|
+
? process.env.A2A_REVIEWER_TOKEN
|
|
3379
|
+
: ref === 'a2a/reviewer-client-secret'
|
|
3380
|
+
? process.env.A2A_REVIEWER_CLIENT_SECRET
|
|
3381
|
+
: undefined;
|
|
3382
|
+
if (!value) throw new Error(`Missing credential for reference: ${ref}.`);
|
|
3383
|
+
return use(value);
|
|
3384
|
+
},
|
|
3385
|
+
},
|
|
3386
|
+
policy: ({ registration }) => ({ allowed: registration.effects.remote === 'read' }),
|
|
3387
|
+
defaultContext: { actorId: 'a2a-host' },
|
|
3388
|
+
},
|
|
3389
|
+
});
|
|
3390
|
+
|
|
3391
|
+
await runtime.admin.agentRegistrations.upsert(discovered.registration);
|
|
3392
|
+
const started = await runtime.agentTasks.start({
|
|
3393
|
+
agentId: discovered.registration.agentId,
|
|
3394
|
+
objective: 'Review this change and return cited findings.',
|
|
3395
|
+
context: { actorId: 'a2a-host' },
|
|
3396
|
+
readOnly: true,
|
|
3397
|
+
expectedConfigurationRevision: discovered.registration.configurationRevision,
|
|
3398
|
+
});
|
|
3399
|
+
const terminal = await runtime.agentTasks.wait(started.taskId, 60_000);
|
|
3400
|
+
```
|
|
3401
|
+
|
|
3402
|
+
For OAuth, replace the legacy `credentialRef` input with the structured form;
|
|
3403
|
+
the same F258 `credentialBroker` must resolve `clientSecretRef`. The shared
|
|
3404
|
+
network policy must admit both origins, while each Card, RPC, and token request
|
|
3405
|
+
is still narrowed to its own exact origin:
|
|
3406
|
+
|
|
3407
|
+
```ts
|
|
3408
|
+
const discovered = await discoverA2ARegistration({
|
|
3409
|
+
agentId: 'external:a2a-reviewer',
|
|
3410
|
+
agentCardUrl: 'https://reviewer.example/.well-known/agent-card.json',
|
|
3411
|
+
authentication: {
|
|
3412
|
+
type: 'oauth2-client-credentials',
|
|
3413
|
+
scheme: 'enterprise-oauth',
|
|
3414
|
+
issuer: 'https://identity.example/',
|
|
3415
|
+
tokenUrl: 'https://identity.example/oauth/token',
|
|
3416
|
+
clientId: 'kodax-reviewer',
|
|
3417
|
+
clientSecretRef: 'a2a/reviewer-client-secret',
|
|
3418
|
+
scopes: ['a2a.invoke'],
|
|
3419
|
+
resource: 'https://reviewer.example/',
|
|
3420
|
+
clientAuthentication: 'client-secret-basic',
|
|
3421
|
+
},
|
|
3422
|
+
effects: { remote: 'read' },
|
|
3423
|
+
}, client);
|
|
3424
|
+
```
|
|
3425
|
+
|
|
3426
|
+
The executor supports durable task start/get, input continuation, cancel,
|
|
3427
|
+
reconcile, SSE events, and polling fallback. An ambiguous start is not retried
|
|
3428
|
+
automatically. A `credentialRef` is resolved just in time by the F258 broker;
|
|
3429
|
+
the registration, task store, and diagnostics never contain the credential.
|
|
3430
|
+
Authenticated SSE uses that same broker. JSON-RPC ID/version and task/context
|
|
3431
|
+
correlation are validated before an event is accepted; if a stream ends
|
|
3432
|
+
normally before a terminal snapshot, the executor resumes bounded polling.
|
|
3433
|
+
Streamed `artifactUpdate` chunks are accumulated by artifact ID according to
|
|
3434
|
+
`append`, and direct Message file Parts are preserved as authorized artifact
|
|
3435
|
+
references.
|
|
3436
|
+
|
|
3437
|
+
### Built-in configured path (no host code)
|
|
3438
|
+
|
|
3439
|
+
The CLI product path stores one user document at
|
|
3440
|
+
`~/.kodax/integrations/a2a.json` and uses the same F258 plane:
|
|
3441
|
+
|
|
3442
|
+
```bash
|
|
3443
|
+
kodax a2a add reviewer https://reviewer.example/.well-known/agent-card.json \
|
|
3444
|
+
--credential-env A2A_REVIEWER_TOKEN --effect read
|
|
3445
|
+
kodax a2a test reviewer
|
|
3446
|
+
kodax a2a call reviewer "Review this document"
|
|
3447
|
+
```
|
|
3448
|
+
|
|
3449
|
+
The no-code OAuth path stores only the environment-variable name for the client
|
|
3450
|
+
secret. It can be staged disabled and hot-activated later:
|
|
3451
|
+
|
|
3452
|
+
```bash
|
|
3453
|
+
export A2A_REVIEWER_CLIENT_SECRET='provisioned-out-of-band'
|
|
3454
|
+
# PowerShell: $env:A2A_REVIEWER_CLIENT_SECRET='provisioned-out-of-band'
|
|
3455
|
+
# PowerShell: use one line or replace each trailing \ with a backtick.
|
|
3456
|
+
kodax a2a add reviewer https://reviewer.example/.well-known/agent-card.json \
|
|
3457
|
+
--disabled --effect read --oauth-scheme enterprise-oauth \
|
|
3458
|
+
--oauth-issuer https://identity.example/ \
|
|
3459
|
+
--oauth-token-url https://identity.example/oauth/token \
|
|
3460
|
+
--oauth-client-id kodax-reviewer \
|
|
3461
|
+
--oauth-client-secret-env A2A_REVIEWER_CLIENT_SECRET \
|
|
3462
|
+
--oauth-scope a2a.invoke --oauth-resource https://reviewer.example/
|
|
3463
|
+
kodax a2a enable reviewer
|
|
3464
|
+
kodax a2a disable reviewer
|
|
3465
|
+
```
|
|
3466
|
+
|
|
3467
|
+
Embedded CLI Runtimes and the user-owned daemon automatically reconcile these
|
|
3468
|
+
entries as `external:<name>`. Discovery/update failure retains that entry's
|
|
3469
|
+
last-known-good registration; another entry can still update. The environment
|
|
3470
|
+
broker resolves `credentialEnv` only at call time. Automatic Runtime
|
|
3471
|
+
registration accepts public HTTPS and exact loopback targets; explicit private
|
|
3472
|
+
network access remains an operator action on the direct CLI/SDK path.
|
|
3473
|
+
|
|
3474
|
+
`enabled` is desired state in `a2a.json`, not a fabricated cross-process live
|
|
3475
|
+
flag. `a2a list` reports configured entries and that desired state. The owning
|
|
3476
|
+
Runtime's `admin.agentRegistrations.list()` is authoritative for applied
|
|
3477
|
+
registrations. Automatic reconciliation handles disables/removals first,
|
|
3478
|
+
skips unchanged peers, performs no Card or token request for disabled entries,
|
|
3479
|
+
and rediscovers before re-enable. Once the owning Runtime observes and applies
|
|
3480
|
+
the revision, disable blocks all new starts, including an explicit
|
|
3481
|
+
`external:<name>` target, but does not cancel or break an already admitted task.
|
|
3482
|
+
The CLI mutation returning is not cross-process acknowledgement. A failed
|
|
3483
|
+
activation remains retryable through the owning
|
|
3484
|
+
`ConfiguredA2ARuntimeHandle.reload()` even when the disk revision is unchanged;
|
|
3485
|
+
the passive `kodax integrations reload` command validates only its own process.
|
|
3486
|
+
|
|
3487
|
+
`kodax a2a test` performs Card discovery and security planning only. It never
|
|
3488
|
+
requests an OAuth access token; token acquisition starts at `a2a call` or the
|
|
3489
|
+
first Runtime dispatch.
|
|
3490
|
+
|
|
3491
|
+
Inbound publication is also no-code:
|
|
3492
|
+
|
|
3493
|
+
```bash
|
|
3494
|
+
export KODAX_A2A_TOKEN='replace-with-a-long-random-token'
|
|
3495
|
+
# PowerShell: $env:KODAX_A2A_TOKEN='replace-with-a-long-random-token'
|
|
3496
|
+
kodax a2a expose # Runtime default Agent
|
|
3497
|
+
kodax a2a expose document-agent # ~/.kodax/agents/document-agent.md
|
|
3498
|
+
kodax a2a serve --port 8765
|
|
3499
|
+
```
|
|
3500
|
+
|
|
3501
|
+
The fixed token above is the compatibility profile. For dynamic production
|
|
3502
|
+
tokens, configure KodaX as an OAuth Resource Server and point it at an external
|
|
3503
|
+
issuer:
|
|
3504
|
+
|
|
3505
|
+
```bash
|
|
3506
|
+
kodax a2a expose document-agent --auth oauth2-jwt \
|
|
3507
|
+
--oauth-scheme enterprise-oauth \
|
|
3508
|
+
--oauth-issuer https://identity.example/ \
|
|
3509
|
+
--oauth-audience https://kodax.example/a2a \
|
|
3510
|
+
--oauth-jwks-url https://identity.example/.well-known/jwks.json \
|
|
3511
|
+
--oauth-token-url https://identity.example/oauth/token \
|
|
3512
|
+
--oauth-metadata-url https://identity.example/.well-known/oauth-authorization-server \
|
|
3513
|
+
--required-scope a2a.invoke
|
|
3514
|
+
kodax a2a serve --port 8765
|
|
3515
|
+
```
|
|
3516
|
+
|
|
3517
|
+
The Authorization Server authenticates clients, provisions client IDs/secrets,
|
|
3518
|
+
issues/rotates/revokes tokens and, for JWT access tokens, signs them and
|
|
3519
|
+
publishes metadata/JWKS. The calling A2A
|
|
3520
|
+
client obtains a token out of band or with Client Credentials and sends it in
|
|
3521
|
+
the Bearer header. KodaX validates JWT type, asymmetric signature, issuer,
|
|
3522
|
+
audience, lifetime, subject, and required scopes before task lookup, then maps
|
|
3523
|
+
`sub` to the A2A principal. Missing/invalid credentials return `401`; a valid
|
|
3524
|
+
token without the required scope returns `403 insufficient_scope`. KodaX does
|
|
3525
|
+
not hold the issuer signing key or expose token, refresh, client-registration,
|
|
3526
|
+
login, or consent endpoints. Opaque-token introspection and mTLS deployments
|
|
3527
|
+
must use a host authentication adapter or reverse proxy. Offline JWT/JWKS
|
|
3528
|
+
validation also cannot observe immediate per-token revocation: use short access
|
|
3529
|
+
token lifetimes, signing-key rotation, or an introspecting proxy/adapter when
|
|
3530
|
+
that property is required.
|
|
3531
|
+
|
|
3532
|
+
#### Upgrade retained pre-realm tasks
|
|
3533
|
+
|
|
3534
|
+
Realm-aware task ownership intentionally has no normal-request legacy fallback:
|
|
3535
|
+
an authority switch must never adopt tasks merely because it reuses a subject.
|
|
3536
|
+
If a v0.7.70 task store must remain addressable after upgrading, stop the A2A
|
|
3537
|
+
server and first inspect an exact-owner migration plan:
|
|
3538
|
+
|
|
3539
|
+
```bash
|
|
3540
|
+
kodax a2a migrate-tasks
|
|
3541
|
+
kodax a2a migrate-tasks --apply --confirm-server-stopped
|
|
3542
|
+
|
|
3543
|
+
# OAuth identity is token-specific, so provide the known historical subject.
|
|
3544
|
+
kodax a2a migrate-tasks --subject trusted-orchestrator
|
|
3545
|
+
```
|
|
3546
|
+
|
|
3547
|
+
The configured Bearer profile supplies its fixed `principalId`; OAuth requires
|
|
3548
|
+
`--subject`. Dry-run does not rewrite `tasks.json`. Apply rekeys only exact
|
|
3549
|
+
matches, preserves unmatched records, and refuses a live task-store owner.
|
|
3550
|
+
Custom SDK hosts can plan multiple known owners without exposing raw tokens:
|
|
3551
|
+
|
|
3552
|
+
```ts
|
|
3553
|
+
import { migrateA2ALegacyTaskOwners } from '@kodax-ai/kodax/a2a';
|
|
3554
|
+
|
|
3555
|
+
const mappings = [{
|
|
3556
|
+
securityRealm: 'oauth2-jwt:https://identity.example/',
|
|
3557
|
+
subject: 'trusted-orchestrator',
|
|
3558
|
+
}] as const;
|
|
3559
|
+
const plan = migrateA2ALegacyTaskOwners({
|
|
3560
|
+
dataDir: '/var/lib/kodax/a2a', mappings, apply: false,
|
|
3561
|
+
});
|
|
3562
|
+
|
|
3563
|
+
// After the host/operator verifies the plan:
|
|
3564
|
+
if (plan.matchedLegacyTaskCount > 0) {
|
|
3565
|
+
migrateA2ALegacyTaskOwners({
|
|
3566
|
+
dataDir: '/var/lib/kodax/a2a', mappings, apply: true,
|
|
3567
|
+
});
|
|
3568
|
+
}
|
|
3569
|
+
```
|
|
3570
|
+
|
|
3571
|
+
The SDK also accepts `tenant` when a custom authentication adapter historically
|
|
3572
|
+
returned one. Two mappings that claim the same legacy owner for different
|
|
3573
|
+
realms are ambiguous and rejected; split or guessed ownership is never applied.
|
|
3574
|
+
|
|
3575
|
+
`a2a serve` resolves its Runtime provider in this order: explicit CLI option,
|
|
3576
|
+
environment, core configuration, then the built-in default. Provider-compatible
|
|
3577
|
+
model selection follows the normal hosted Runtime rule. A selected Markdown
|
|
3578
|
+
Agent may declare its own validated `provider`; remote A2A input cannot choose
|
|
3579
|
+
or override provider, model, reasoning, profile, workspace, or tools.
|
|
3580
|
+
|
|
3581
|
+
`expose` validates a named user Markdown Agent before writing its reference.
|
|
3582
|
+
`serve` loads configured MCP and Extensions before it resolves the execution
|
|
3583
|
+
binding or opens a socket. Native workspace read tools are admitted by
|
|
3584
|
+
workspace access; writes, narrow Extension Tools, MCP capabilities, subagents,
|
|
3585
|
+
and isolated Skill scripts require their corresponding exact `toolPolicy`
|
|
3586
|
+
authority. Internal Skills come from `~/.kodax/skills`, `~/.agents/skills`,
|
|
3587
|
+
plugins, and built-ins; public Agent Card skills are a separate explicit
|
|
3588
|
+
projection and never reveal the private Skill inventory.
|
|
3589
|
+
|
|
3590
|
+
The running server pins Agent, Skill, workspace, tool registration, process and
|
|
3591
|
+
store revisions. Card/auth/limits can hot reload; execution-authority changes
|
|
3592
|
+
require an explicit restart. Managed contexts live below
|
|
3593
|
+
`~/kodax_a2a_server_workspace/<runtime-profile>/contexts/<context-key>/`. Exact Skill scripts require
|
|
3594
|
+
`process: isolated`, an admitted `scripts/...` path, and a passing
|
|
3595
|
+
`kodax sandbox doctor`; KodaX never falls back to an unsandboxed shell.
|
|
3596
|
+
|
|
3597
|
+
Every concrete file reached by `read`, `grep`, or `glob` is checked against the
|
|
3598
|
+
bound workspace. Child runs inherit ceilings for native reads, tools, Skills,
|
|
3599
|
+
and Skill scripts; they cannot expand the parent's admitted authority.
|
|
3600
|
+
|
|
3601
|
+
### Publish one KodaX Agent
|
|
3602
|
+
|
|
3603
|
+
Publication is host-owned and opt-in. The public card describes only the
|
|
3604
|
+
configured Agent, media types, and skills. Authentication runs before task
|
|
3605
|
+
lookup; authorization runs per operation; task visibility is principal-scoped.
|
|
3606
|
+
|
|
3607
|
+
```ts
|
|
3608
|
+
import {
|
|
3609
|
+
createBearerEnvA2AAuthentication,
|
|
3610
|
+
createKodaXA2AServer,
|
|
3611
|
+
} from '@kodax-ai/kodax/a2a';
|
|
3612
|
+
import { createKodaXRuntime } from '@kodax-ai/kodax/runtime';
|
|
3613
|
+
|
|
3614
|
+
const runtime = await createKodaXRuntime({ mode: 'embedded', isolation: 'inline' });
|
|
3615
|
+
const server = createKodaXA2AServer({
|
|
3616
|
+
runtime,
|
|
3617
|
+
dataDir: '/var/lib/kodax/a2a',
|
|
3618
|
+
agent: {
|
|
3619
|
+
name: 'KodaX Reviewer',
|
|
3620
|
+
description: 'Reviews bounded code changes.',
|
|
3621
|
+
version: '1.0.0',
|
|
3622
|
+
publicBaseUrl: 'https://kodax.example',
|
|
3623
|
+
skills: [{ id: 'review', name: 'Review', description: 'Review code.', tags: ['code'] }],
|
|
3624
|
+
inputModes: ['text/plain'],
|
|
3625
|
+
outputModes: ['text/plain'],
|
|
3626
|
+
},
|
|
3627
|
+
authentication: createBearerEnvA2AAuthentication({
|
|
3628
|
+
type: 'bearer-env',
|
|
3629
|
+
tokenEnv: 'KODAX_A2A_TOKEN',
|
|
3630
|
+
principalId: 'trusted-orchestrator',
|
|
3631
|
+
}),
|
|
3632
|
+
async authorize({ principal }) { return principal.scopes.includes('a2a:invoke'); },
|
|
3633
|
+
limits: {
|
|
3634
|
+
maxRequestBytes: 1_048_576,
|
|
3635
|
+
maxPartBytes: 524_288,
|
|
3636
|
+
maxConcurrentTasks: 8,
|
|
3637
|
+
maxTaskWaitMs: 30_000,
|
|
3638
|
+
maxActiveTasksPerPrincipal: 8,
|
|
3639
|
+
maxRetainedTasksPerPrincipal: 64,
|
|
3640
|
+
maxEventsPerTask: 1_000,
|
|
3641
|
+
maxEventBytesPerTask: 16_777_216,
|
|
3642
|
+
maxWorkspaceBytesPerContext: 1_073_741_824,
|
|
3643
|
+
},
|
|
3644
|
+
});
|
|
3645
|
+
|
|
3646
|
+
// Development only: the built-in listener refuses non-loopback hosts.
|
|
3647
|
+
const localBaseUrl = await server.listen({ hostname: '127.0.0.1', port: 0 });
|
|
3648
|
+
```
|
|
3649
|
+
|
|
3650
|
+
Production hosts route `GET /.well-known/agent-card.json` and canonical
|
|
3651
|
+
JSON-RPC `POST /a2a` to `server.handle(request)` behind their own TLS
|
|
3652
|
+
terminator. `POST /` remains an accepted compatibility alias. `listen()` waits for durable recovery before
|
|
3653
|
+
it resolves. A host that wires `handle()` directly may explicitly await
|
|
3654
|
+
`server.whenReady()` before it starts accepting traffic; `handle()` also waits
|
|
3655
|
+
for the same recovery promise. The durable edge store supports get/list,
|
|
3656
|
+
continuation, cancellation, ordered SSE subscription, and surviving Runtime-run
|
|
3657
|
+
reattachment after an edge restart. Push notifications, A2A 0.3, gRPC, and
|
|
3658
|
+
HTTP+JSON are not advertised; unsupported push methods return the standard
|
|
3659
|
+
`PushNotificationNotSupportedError`.
|
|
3660
|
+
|
|
3661
|
+
Non-streaming `SendMessage` waits at most `maxTaskWaitMs` (30 seconds by
|
|
3662
|
+
default). When that bound is reached the response contains the current working
|
|
3663
|
+
task; it does not cancel the Runtime run, and clients can continue with
|
|
3664
|
+
`GetTask` or `SubscribeToTask`.
|
|
3665
|
+
|
|
3666
|
+
When a task enters `INPUT_REQUIRED`, the next accepted input answers the pending
|
|
3667
|
+
interaction on the original Runtime run; it does not start a replacement run.
|
|
3668
|
+
History length and list filters are validated and bounded. Task listing uses a
|
|
3669
|
+
stable opaque cursor, while per-principal retention prunes only the oldest
|
|
3670
|
+
terminal records. Terminal subscriptions and failed-start resources are closed
|
|
3671
|
+
by their owning lifecycle.
|
|
3672
|
+
|
|
3673
|
+
Remote messages are ordinary user inputs. They cannot select provider, model,
|
|
3674
|
+
profile, tools, working directory, permission mode, or Runtime configuration.
|
|
3675
|
+
URL parts are rejected; inline raw/data parts are bounded and materialized under
|
|
3676
|
+
the server-owned data directory. Responses expose final approved output only,
|
|
3677
|
+
not system prompts, reasoning deltas, tool payloads, credentials, or local paths.
|
|
3678
|
+
|
|
3679
|
+
Generated files are published only through the trusted output broker: a normal
|
|
3680
|
+
tool or Extension stages a file in the context's `.kodax-a2a-staging` area, or
|
|
3681
|
+
a successfully admitted `run_skill_script` promotes one of its declared
|
|
3682
|
+
outputs. The server rechecks that the result is a regular non-symlink file in
|
|
3683
|
+
the real bound workspace and applies part-size/output-mode limits before
|
|
3684
|
+
inlining it. A declaration from a failed Skill run, an ordinary `write`/`edit`
|
|
3685
|
+
elsewhere in the workspace, and a local path in model text never become A2A
|
|
3686
|
+
artifacts implicitly.
|
|
3687
|
+
|
|
3688
|
+
The normative baseline is A2A repository commit
|
|
3689
|
+
`2183794bfb9b67af4aee1be0a0ef726050642873`, protocol `1.0`, with
|
|
3690
|
+
`specification/a2a.proto` SHA-256
|
|
3691
|
+
`e195bf96ab630c69797851970203e1b2b6b19528f2e9803b7d904b91a5104016`.
|
|
3692
|
+
|
|
3693
|
+
---
|
|
3694
|
+
|
|
3695
|
+
## 23. Shared Coder daemon for Space and IDE hosts (FEATURE_269, v0.7.69)
|
|
3696
|
+
|
|
3697
|
+
FEATURE_269 makes one local daemon the source of truth for a Coder profile.
|
|
3698
|
+
CLI, Space, IDE, and SDK clients can observe and control the same sessions and
|
|
3699
|
+
runs. The transport remains local to the current OS user; it is not a remote
|
|
3700
|
+
collaboration protocol. Closing a client detaches that client and does not stop
|
|
3701
|
+
the daemon or another client's run.
|
|
3702
|
+
|
|
3703
|
+
Partner is deliberately outside this migration. Keep Partner on its existing
|
|
3704
|
+
inline callbacks and give it a distinct product data root and sessions root.
|
|
3705
|
+
Do not point a Partner inline Runtime at the Coder daemon profile or the Coder
|
|
3706
|
+
data root.
|
|
3707
|
+
|
|
3708
|
+
### Connect and fail closed on required capabilities
|
|
3709
|
+
|
|
3710
|
+
Space should own the daemon SDK client in Electron Main. Persist a random,
|
|
3711
|
+
stable `instanceId` and a separate 32+ character `instanceSecret` per Space
|
|
3712
|
+
installation. Store the secret in the OS keychain; never accept either value
|
|
3713
|
+
from renderer or model output. `connectKodaXRuntime()` is attach-only unless `autoStart: true`.
|
|
3714
|
+
An explicit inline rollback policy blocks auto-start until the owner policy is
|
|
3715
|
+
explicitly changed back to daemon.
|
|
3716
|
+
|
|
3717
|
+
For Electron, `homeDir` is still the CLI-style base directory, not
|
|
3718
|
+
`process.env.KODAX_HOME`. Packaged/asar applications may use `autoStart: true`
|
|
3719
|
+
directly; the SDK launches only the daemon child in Electron's Node execution
|
|
3720
|
+
mode and does not mutate the application's environment or start a second GUI
|
|
3721
|
+
instance. `ELECTRON_RUN_AS_NODE` exists only at the child exec boundary and is
|
|
3722
|
+
removed before daemon application code loads, so Bash, MCP, LSP, sandboxed
|
|
3723
|
+
commands, and ordinary external processes do not inherit Electron Node mode.
|
|
3724
|
+
|
|
3725
|
+
Packaged auto-start requires Electron's `RunAsNode` fuse, which Electron enables
|
|
3726
|
+
by default. If an embedder deliberately disables that fuse, the packaged
|
|
3727
|
+
executable cannot serve as a detached Node host: start the daemon with an
|
|
3728
|
+
ordinary Node/CLI process and use attach-only mode instead. A packaged
|
|
3729
|
+
`autoStart: true` timeout includes this fuse requirement in its diagnostic; the
|
|
3730
|
+
SDK does not relaunch the GUI or silently fall back to an inline Runtime.
|
|
3731
|
+
|
|
3732
|
+
```ts
|
|
3733
|
+
import { connectKodaXRuntime } from '@kodax-ai/kodax/runtime';
|
|
3734
|
+
|
|
3735
|
+
const runtime = await connectKodaXRuntime({
|
|
3736
|
+
profile: 'coder',
|
|
3737
|
+
autoStart: true,
|
|
3738
|
+
homeDir: coderRuntimeBaseDir, // owns <coderRuntimeBaseDir>/.kodax
|
|
3739
|
+
clientInfo: {
|
|
3740
|
+
name: 'kodax-space',
|
|
3741
|
+
version: '0.1.32',
|
|
3742
|
+
instanceId: spaceInstallationId,
|
|
3743
|
+
instanceSecret: await spaceKeychain.readRuntimeClientSecret(),
|
|
3744
|
+
},
|
|
3745
|
+
capabilities: {
|
|
3746
|
+
richEvents: true,
|
|
3747
|
+
permissionPrompts: true,
|
|
3748
|
+
operationDeduplication: true,
|
|
3749
|
+
},
|
|
3750
|
+
requirements: {
|
|
3751
|
+
operationDeduplication: 1,
|
|
3752
|
+
sessionObservation: 1,
|
|
3753
|
+
afterTurnInput: 1,
|
|
3754
|
+
interruptInput: 1,
|
|
3755
|
+
askUserTransport: 1,
|
|
3756
|
+
permissionCas: 1,
|
|
3757
|
+
providerCredentialBroker: 1,
|
|
3758
|
+
runBoundHostTools: 1,
|
|
3759
|
+
coderOwnerFencing: 1,
|
|
3760
|
+
crashOutcomeModel: 1,
|
|
3761
|
+
coderFeatureMatrix: 1,
|
|
3762
|
+
sessionAdmission: 1,
|
|
3763
|
+
completeObservationSnapshot: 1,
|
|
3764
|
+
connectionLifecycle: 1,
|
|
3765
|
+
typedRuntimeEvents: 1,
|
|
3766
|
+
daemonSafeRunInput: 1,
|
|
3767
|
+
sharedSessionSettings: 1,
|
|
3768
|
+
durableRecoveryQueries: 1,
|
|
3769
|
+
daemonManagement: 1,
|
|
3770
|
+
runtimeAutoModeGuardrail: 3,
|
|
3771
|
+
},
|
|
3772
|
+
});
|
|
3773
|
+
```
|
|
3774
|
+
|
|
3775
|
+
Requirements are server facts, not authorization requests. Check
|
|
3776
|
+
`runtime.grantedScopes` before enabling controls. Missing capabilities or
|
|
3777
|
+
scopes must disable the affected UI; Space must not silently start inline
|
|
3778
|
+
Coder. Products that depend on same-Run delivery should require
|
|
3779
|
+
`{ interruptInput: 1 }`. Individual active Runs without a safe Actor boundary
|
|
3780
|
+
(for example, SA execution) still return `unsupported_capability`; do not
|
|
3781
|
+
silently substitute `delivery:'after_turn'` unless that is the user's intent.
|
|
3782
|
+
|
|
3783
|
+
The v0.7.73 SDK requires `runtimeAutoModeGuardrail:3` automatically for
|
|
3784
|
+
`autoStart: true`, even when the caller omits it from `requirements`. If the healthy
|
|
3785
|
+
profile daemon advertises v1 or v2, the SDK first requires `daemonManagement:1`,
|
|
3786
|
+
takes a revision/owner-policy fenced preflight, and replaces it only when no
|
|
3787
|
+
active or queued run, Workflow, Agent turn, pending permission/user input, or
|
|
3788
|
+
other logical client exists. A busy or still-older daemon is never stopped: the
|
|
3789
|
+
connection rejects with `RuntimeDaemonCapabilityUpgradeError`, whose
|
|
3790
|
+
`recoverable` and `restartRequired` fields are `true` and whose optional
|
|
3791
|
+
`preflight` explains the blockers. Attach-only connections never mutate daemon
|
|
3792
|
+
ownership and must request `runtimeAutoModeGuardrail:1` explicitly when they
|
|
3793
|
+
depend only on the v1 owner contract, v2 for bounded input, effective-default
|
|
3794
|
+
metadata, structured diagnostics, and speculative-window parity, or v3 for
|
|
3795
|
+
opaque exact grant suggestions and concrete permission matchers. Capability
|
|
3796
|
+
requirements are minimum versions: v3 satisfies v1/v2, v2 satisfies v1, and an
|
|
3797
|
+
older daemon never satisfies a newer requirement.
|
|
3798
|
+
|
|
3799
|
+
The `coderFeatureMatrix` capability reports daemon availability for managed
|
|
3800
|
+
runs, transcript/session operations, Todo projection, managed tasks, Workflow,
|
|
3801
|
+
MCP, Reference External Agent, Memory, and Runtime artifacts. Reference
|
|
3802
|
+
External Agent is `false` when the daemon owner did not install its executor
|
|
3803
|
+
plane.
|
|
3804
|
+
|
|
3805
|
+
The packaged daemon authenticates one local OS-user/profile trust domain with
|
|
3806
|
+
a random token stored beside daemon state and a user-only local endpoint. It
|
|
3807
|
+
does not issue a different daemon token to each application in v0.7.69. The
|
|
3808
|
+
returned scope set is chosen by the host (the packaged host grants the public
|
|
3809
|
+
local-user set). `clientInfo.instanceId` is stable attribution for origin and
|
|
3810
|
+
operation deduplication. `instanceSecret` proves that a new authenticated
|
|
3811
|
+
connection is the same stable client when it resumes that client's credential
|
|
3812
|
+
or Host Tool leases; only its hash participates in daemon-owned bridge state.
|
|
3813
|
+
Keep all three values in Electron Main. Mutually distrusting processes running
|
|
3814
|
+
as the same OS account remain outside this release's threat model.
|
|
3815
|
+
|
|
3816
|
+
### Join atomically and resync after disconnect
|
|
3817
|
+
|
|
3818
|
+
`sessions.observe()` installs the live subscription before taking the
|
|
3819
|
+
snapshot. Its snapshot contains one authoritative `runtimeId`, cursor,
|
|
3820
|
+
`transcriptRevision`, complete transcript, versioned settings, run/queue state,
|
|
3821
|
+
queued continuation IDs/order/origin/safe previews, pending permission and
|
|
3822
|
+
AskUser requests, and live assistant/thinking/tool/Todo/managed-task
|
|
3823
|
+
projection. Run requirements include the current credential/Host Tool
|
|
3824
|
+
availability. Listener events are strictly after the returned cursor.
|
|
3825
|
+
|
|
3826
|
+
```ts
|
|
3827
|
+
let observedRuntimeId: string | undefined;
|
|
3828
|
+
let lastCursor = 0;
|
|
3829
|
+
|
|
3830
|
+
async function openCoderSession(sessionId: string) {
|
|
3831
|
+
const observation = await runtime.sessions.observe(sessionId, (event) => {
|
|
3832
|
+
if (event.seq <= lastCursor) return;
|
|
3833
|
+
applyRuntimeEvent(event);
|
|
3834
|
+
lastCursor = event.seq;
|
|
3835
|
+
});
|
|
3836
|
+
|
|
3837
|
+
const { snapshot } = observation;
|
|
3838
|
+
const runtimeChanged = observedRuntimeId !== undefined
|
|
3839
|
+
&& observedRuntimeId !== snapshot.runtimeId;
|
|
3840
|
+
observedRuntimeId = snapshot.runtimeId;
|
|
3841
|
+
lastCursor = snapshot.cursor;
|
|
3842
|
+
replaceSessionProjection(snapshot, { runtimeChanged });
|
|
3843
|
+
return observation;
|
|
3844
|
+
}
|
|
3845
|
+
```
|
|
3846
|
+
|
|
3847
|
+
Subscribe to `runtime.connection` to freeze mutation UI immediately rather
|
|
3848
|
+
than waiting for a status poll:
|
|
3849
|
+
|
|
3850
|
+
```ts
|
|
3851
|
+
runtime.connection?.subscribe((state) => {
|
|
3852
|
+
setCoderConnectionState(state.state, state.reason);
|
|
3853
|
+
if (state.state === 'disconnected' && state.reconnectable) {
|
|
3854
|
+
scheduleReconnect();
|
|
3855
|
+
}
|
|
3856
|
+
});
|
|
3857
|
+
```
|
|
3858
|
+
|
|
3859
|
+
The SDK reports the current `connectionId`, `runtimeEpoch`, optional
|
|
3860
|
+
`journalEpoch`, disconnect reason, and whether a new connection may be
|
|
3861
|
+
attempted. It does not transparently replay requests or subscriptions. Space
|
|
3862
|
+
creates a replacement Runtime client, checks its new epochs, resumes eligible
|
|
3863
|
+
leases, and observes the session again.
|
|
3864
|
+
|
|
3865
|
+
On transport failure, Runtime change, expired history, or `resync_required`,
|
|
3866
|
+
discard the local derived projection and call `sessions.observe()` again. Do
|
|
3867
|
+
not merge a new snapshot into the old projection. The handshake buffer is
|
|
3868
|
+
bounded; overflow fails explicitly instead of dropping events. A Runtime
|
|
3869
|
+
restart changes `runtimeId`, marks persisted non-terminal runs with a durable
|
|
3870
|
+
terminal fact, and closes old in-memory AskUser/permission requests through the
|
|
3871
|
+
reset boundary.
|
|
3872
|
+
|
|
3873
|
+
### Durable mutations, stable ordering, and settings CAS
|
|
3874
|
+
|
|
3875
|
+
Every durable public control mutation uses an operation envelope. Credential
|
|
3876
|
+
and Host Tool register/revoke/supply/complete requests are reverse-bridge
|
|
3877
|
+
control frames and are deliberately excluded from the control journal so
|
|
3878
|
+
secrets/results are not persisted. They still enter the daemon management
|
|
3879
|
+
draining fence: once an atomic stop begins, they fail with typed `conflict` and
|
|
3880
|
+
cannot change reverse-bridge state. The SDK creates an operation ID for
|
|
3881
|
+
ordinary one-shot calls. A
|
|
3882
|
+
product-level retry after a lost response must reuse its own stable operation
|
|
3883
|
+
ID; changing its method, payload, resource, or authenticated principal is
|
|
3884
|
+
rejected.
|
|
3885
|
+
|
|
3886
|
+
```ts
|
|
3887
|
+
const session = await runtime.sessions.create({
|
|
3888
|
+
sessionId: stableSpaceSessionId,
|
|
3889
|
+
title: 'Shared session',
|
|
3890
|
+
surface: 'space-desktop',
|
|
3891
|
+
operation: { operationId: loadOrCreatePendingOperationId('space-session-draft-7') },
|
|
3892
|
+
});
|
|
3893
|
+
|
|
3894
|
+
const operationId = loadOrCreatePendingOperationId('space-run-draft-42');
|
|
3895
|
+
const handle = await runtime.runs.start({
|
|
3896
|
+
sessionId: session.id,
|
|
3897
|
+
input: { type: 'text', text: prompt },
|
|
3898
|
+
options: { provider: 'anthropic' },
|
|
3899
|
+
operation: { operationId },
|
|
3900
|
+
});
|
|
3901
|
+
|
|
3902
|
+
const current = await runtime.sessions.getSettingsVersioned(session.id);
|
|
3903
|
+
const updated = await runtime.sessions.updateSettingsVersioned(
|
|
3904
|
+
session.id,
|
|
3905
|
+
{ model: 'claude-sonnet-4-5' },
|
|
3906
|
+
{
|
|
3907
|
+
operationId: loadOrCreatePendingOperationId('space-settings-draft-9'),
|
|
3908
|
+
expectedRevision: current.revision,
|
|
3909
|
+
},
|
|
3910
|
+
);
|
|
3911
|
+
```
|
|
3912
|
+
|
|
3913
|
+
Create retries with the same explicit session and operation IDs cannot overwrite
|
|
3914
|
+
an existing session. Same-session starts and after-turn inputs receive a durable `sessionOrder`.
|
|
3915
|
+
Retries with the same operation ID return the canonical result and do not
|
|
3916
|
+
create another run. Settings use compare-and-swap; a stale revision returns a
|
|
3917
|
+
structured conflict and must be reloaded, never silently overwritten. The
|
|
3918
|
+
shared settings keys are `provider`, `model`, `effort`, `thinking`,
|
|
3919
|
+
`reasoningMode`, `permissionMode`, `executionCwd`, `agentMode`, and
|
|
3920
|
+
`autoModeEngine`, `autoModeClassifierModel`, `autoModeTimeoutMs`, and
|
|
3921
|
+
`autoModeSpeculativeWindowMs`.
|
|
3922
|
+
|
|
3923
|
+
```ts
|
|
3924
|
+
const queued = await runtime.runs.submitInput({
|
|
3925
|
+
sessionId: session.id,
|
|
3926
|
+
afterRunId: handle.runId,
|
|
3927
|
+
delivery: 'after_turn',
|
|
3928
|
+
input: { type: 'text', text: 'Also update the tests.' },
|
|
3929
|
+
operation: { operationId: loadOrCreatePendingOperationId('space-input-17') },
|
|
3930
|
+
});
|
|
3931
|
+
|
|
3910
3932
|
if (!queued.accepted) {
|
|
3911
|
-
// stale_run or
|
|
3912
|
-
|
|
3913
|
-
```
|
|
3914
|
-
|
|
3915
|
-
Run status exposes acceptance/start/queue times, authenticated origin,
|
|
3916
|
-
`sessionOrder`, and a single terminal fact. Important terminal codes include
|
|
3917
|
-
`runtime_restarted`, `daemon_crashed`, `credential_unavailable`,
|
|
3918
|
-
`host_not_dispatched`, `host_outcome_unknown`, and
|
|
3919
|
-
`control_history_untrusted`. Respect `effectOutcome`; `unknown` must never be
|
|
3920
|
-
presented as success or automatically retried. After a lost response, query
|
|
3921
|
-
`runtime.operations.get({ operationId, journalEpoch })`; applied receipts
|
|
3922
|
-
include the canonical result. Permission grants remain daemon-owned and
|
|
3923
|
-
revisioned.
|
|
3924
|
-
|
|
3925
|
-
### AskUser and permission from any client
|
|
3926
|
-
|
|
3927
|
-
AskUser is no longer an in-process callback for daemon Coder runs. Any client
|
|
3928
|
-
with the responder scope can list the pending request and answer or dismiss it.
|
|
3929
|
-
The request revision and run binding prevent a stale UI from answering a new
|
|
3930
|
-
request. Exactly one concurrent answer is accepted.
|
|
3931
|
-
|
|
3932
|
-
```ts
|
|
3933
|
-
for (const request of await runtime.userInputs.listPending({ sessionId })) {
|
|
3934
|
-
const resolution = await runtime.userInputs.respond(request.id, answer, {
|
|
3935
|
-
expectedRevision: request.revision,
|
|
3936
|
-
runId: request.runId,
|
|
3937
|
-
});
|
|
3938
|
-
if (!resolution.accepted) refreshPendingInteractions();
|
|
3939
|
-
}
|
|
3940
|
-
|
|
3941
|
-
for (const request of await runtime.permissions.listPending({ sessionId })) {
|
|
3942
|
-
const sessionScope = request.grantSuggestions
|
|
3943
|
-
?.find((candidate) => candidate.kind === 'session');
|
|
3944
|
-
const accepted = await runtime.permissions.respond(
|
|
3945
|
-
request.id,
|
|
3946
|
-
sessionScope
|
|
3947
|
-
? { type: 'allow_session', suggestionId: sessionScope.id }
|
|
3948
|
-
: { type: 'allow_once' },
|
|
3949
|
-
{ runId: request.runId },
|
|
3950
|
-
);
|
|
3951
|
-
if (!accepted) refreshPendingInteractions();
|
|
3933
|
+
// stale_run, unsupported_capability, or interrupt_window_closed:
|
|
3934
|
+
// show the factual result and preserve the user's unsent input.
|
|
3952
3935
|
}
|
|
3953
|
-
```
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
{
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
`
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
daemon
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
3936
|
+
```
|
|
3937
|
+
|
|
3938
|
+
For input that must join the current active Run, submit `delivery:'interrupt'`.
|
|
3939
|
+
This does not create a Run. Each accepted input appears as `queued` in the
|
|
3940
|
+
owning Run's `interruptInputs`. At the next safe boundary, all accumulated
|
|
3941
|
+
interrupts are drained FIFO, remain separate user messages in one next LLM
|
|
3942
|
+
request, and produce one `run.input.delivered` event whose `inputs` array is the
|
|
3943
|
+
complete ordered batch. Exact operation retries return the same `inputId`.
|
|
3944
|
+
The accepted result's `runId` is the existing owning Run (equal to
|
|
3945
|
+
`afterRunId`), not a newly created continuation.
|
|
3946
|
+
|
|
3947
|
+
Interrupt admission closes when the Runner publishes its final completion or
|
|
3948
|
+
terminal error signal, or when the Run's supplied `abortSignal` aborts, even if
|
|
3949
|
+
the outer Run is still settling. Non-terminal observer diagnostics do not close
|
|
3950
|
+
the window. A submission after closure returns `accepted:false` with
|
|
3951
|
+
`reason:'interrupt_window_closed'` and is not queued. Keep the original input
|
|
3952
|
+
available for retry after the Run ends; do not silently change its delivery to
|
|
3953
|
+
`after_turn`. As a final race/recovery
|
|
3954
|
+
guard, inspect terminal Run status: any `interruptInputs` entry whose state is
|
|
3955
|
+
`terminal` was not delivered. Reconcile it by `inputId` and present a visible
|
|
3956
|
+
non-delivery outcome rather than leaving a pending queue indicator.
|
|
3957
|
+
|
|
3958
|
+
```ts
|
|
3959
|
+
const interrupted = await runtime.runs.submitInput({
|
|
3960
|
+
sessionId: session.id,
|
|
3961
|
+
afterRunId: handle.runId,
|
|
3962
|
+
delivery: 'interrupt',
|
|
3963
|
+
input: { type: 'text', text: 'Also preserve the public API.' },
|
|
3964
|
+
operation: { operationId: loadOrCreatePendingOperationId('space-input-18') },
|
|
3965
|
+
});
|
|
3966
|
+
```
|
|
3967
|
+
|
|
3968
|
+
Run status exposes acceptance/start/queue times, authenticated origin,
|
|
3969
|
+
`sessionOrder`, and a single terminal fact. Important terminal codes include
|
|
3970
|
+
`runtime_restarted`, `daemon_crashed`, `credential_unavailable`,
|
|
3971
|
+
`host_not_dispatched`, `host_outcome_unknown`, and
|
|
3972
|
+
`control_history_untrusted`. Respect `effectOutcome`; `unknown` must never be
|
|
3973
|
+
presented as success or automatically retried. After a lost response, query
|
|
3974
|
+
`runtime.operations.get({ operationId, journalEpoch })`; applied receipts
|
|
3975
|
+
include the canonical result. Permission grants remain daemon-owned and
|
|
3976
|
+
revisioned.
|
|
3977
|
+
|
|
3978
|
+
### AskUser and permission from any client
|
|
3979
|
+
|
|
3980
|
+
AskUser is no longer an in-process callback for daemon Coder runs. Any client
|
|
3981
|
+
with the responder scope can list the pending request and answer or dismiss it.
|
|
3982
|
+
The request revision and run binding prevent a stale UI from answering a new
|
|
3983
|
+
request. Exactly one concurrent answer is accepted.
|
|
3984
|
+
|
|
3985
|
+
```ts
|
|
3986
|
+
for (const request of await runtime.userInputs.listPending({ sessionId })) {
|
|
3987
|
+
const resolution = await runtime.userInputs.respond(request.id, answer, {
|
|
3988
|
+
expectedRevision: request.revision,
|
|
3989
|
+
runId: request.runId,
|
|
3990
|
+
});
|
|
3991
|
+
if (!resolution.accepted) refreshPendingInteractions();
|
|
3992
|
+
}
|
|
3993
|
+
|
|
3994
|
+
for (const request of await runtime.permissions.listPending({ sessionId })) {
|
|
3995
|
+
const sessionScope = request.grantSuggestions
|
|
3996
|
+
?.find((candidate) => candidate.kind === 'session');
|
|
3997
|
+
const accepted = await runtime.permissions.respond(
|
|
3998
|
+
request.id,
|
|
3999
|
+
sessionScope
|
|
4000
|
+
? { type: 'allow_session', suggestionId: sessionScope.id }
|
|
4001
|
+
: { type: 'allow_once' },
|
|
4002
|
+
{ runId: request.runId },
|
|
4003
|
+
);
|
|
4004
|
+
if (!accepted) refreshPendingInteractions();
|
|
4005
|
+
}
|
|
4006
|
+
```
|
|
4007
|
+
|
|
4008
|
+
Persistent `allow_always` grants are owned by the daemon and require
|
|
4009
|
+
`permission:grant-admin`. Use `permissions.listGrants()` and
|
|
4010
|
+
`revokeGrant(grantId, expectedRevision)` for revision-safe administration.
|
|
4011
|
+
Clients must return one opaque `grantSuggestions[].id` from the pending request;
|
|
4012
|
+
they must not infer, construct, or widen a scope from the display label or input
|
|
4013
|
+
preview. A safe request can offer `allow_session` and `allow_always`; a risky or
|
|
4014
|
+
dynamic shell request deliberately omits the persistent candidate. Command
|
|
4015
|
+
grants match one exact normalized command/cwd/shell/background combination,
|
|
4016
|
+
while path grants match one tool and normalized absolute path (the future
|
|
4017
|
+
Write/Edit content may differ). Generic extension calls can receive only an
|
|
4018
|
+
exact in-memory Session grant. Raw command/argv data is not stored in the
|
|
4019
|
+
matcher; grants and audit contain only its fingerprint plus a bounded,
|
|
4020
|
+
secret-redacted operator label. Clients must not keep separate persistent
|
|
4021
|
+
permission rule stores. Runtime capability `runtimeAutoModeGuardrail` v3 advertises this
|
|
4022
|
+
opaque concrete-grant contract; restart or upgrade an older daemon instead of
|
|
4023
|
+
falling back to a client-side alias.
|
|
4024
|
+
|
|
4025
|
+
### Broker a Space keychain credential
|
|
4026
|
+
|
|
4027
|
+
Credentials remain owned by Space's OS keychain. Register a broker in Electron
|
|
4028
|
+
Main, bind the returned lease only to runs that Space starts, and return the
|
|
4029
|
+
key only after checking the daemon-provided provider/session/run context.
|
|
4030
|
+
|
|
4031
|
+
```ts
|
|
4032
|
+
const credentialLease = await runtime.credentials.register(
|
|
4033
|
+
{ providers: ['anthropic'] },
|
|
4034
|
+
async ({ provider, sessionId: requestedSession, runId }) => {
|
|
4035
|
+
authorizeSpaceRunCredential({ provider, sessionId: requestedSession, runId });
|
|
4036
|
+
return spaceKeychain.readProviderCredential(provider);
|
|
4037
|
+
},
|
|
4038
|
+
);
|
|
4039
|
+
|
|
4040
|
+
const run = await runtime.runs.start({
|
|
4041
|
+
sessionId,
|
|
4042
|
+
input: { type: 'text', text: prompt },
|
|
4043
|
+
options: { provider: 'anthropic' },
|
|
4044
|
+
credential: { leaseId: credentialLease.id, provider: 'anthropic' },
|
|
4045
|
+
operation: { operationId: loadOrCreatePendingOperationId('space-run-88') },
|
|
4046
|
+
});
|
|
4047
|
+
```
|
|
4048
|
+
|
|
4049
|
+
The secret crosses only the authenticated reverse frame and an in-memory
|
|
4050
|
+
run/provider scope. It is excluded from events, status, logs, diagnostics,
|
|
4051
|
+
operation records, and Runtime persistence. While such a scope is active,
|
|
4052
|
+
provider mismatch fails closed and never falls back to daemon environment.
|
|
4053
|
+
Without a stable `instanceSecret`, registration ends when the Space connection
|
|
4054
|
+
closes. With it, the daemon keeps the registration owned by that stable client;
|
|
4055
|
+
a replacement Space process reattaches the callback with
|
|
4056
|
+
`runtime.credentials.resume(leaseId, broker)`. An accepted run has already
|
|
4057
|
+
acquired its scoped credential, so it reports `requirements.credential.state`
|
|
4058
|
+
as `ready` and may continue after disconnect. If the broker cannot answer, the
|
|
4059
|
+
start is rejected instead of accepting an indefinitely waiting run. Expiry or
|
|
4060
|
+
Runtime restart is explicit (`expired` or terminal); no provider request is
|
|
4061
|
+
automatically replayed.
|
|
4062
|
+
|
|
4063
|
+
### Bind Space-owned Host Tools to one run
|
|
4064
|
+
|
|
4065
|
+
Register only narrow product capabilities. Descriptors are data; handlers stay
|
|
4066
|
+
in Electron Main. A lease grants nothing until its ID is explicitly bound to a
|
|
4067
|
+
run.
|
|
4068
|
+
|
|
4069
|
+
```ts
|
|
4070
|
+
const hostLease = await runtime.hostTools.register([{
|
|
4071
|
+
name: 'space_artifact_create',
|
|
4072
|
+
description: 'Create a Space-owned artifact for this run.',
|
|
4073
|
+
inputSchema: {
|
|
4074
|
+
type: 'object',
|
|
4075
|
+
properties: { title: { type: 'string' } },
|
|
4076
|
+
required: ['title'],
|
|
4077
|
+
additionalProperties: false,
|
|
4078
|
+
},
|
|
4079
|
+
sideEffect: 'non_idempotent',
|
|
4080
|
+
}], {
|
|
4081
|
+
async space_artifact_create(invocation) {
|
|
4082
|
+
authorizeBoundSpaceInvocation(invocation);
|
|
4083
|
+
const artifact = await createSpaceArtifact(invocation.input);
|
|
4084
|
+
return { content: `Created artifact ${artifact.displayId}` };
|
|
4085
|
+
},
|
|
4086
|
+
});
|
|
4087
|
+
|
|
4088
|
+
await runtime.runs.start({
|
|
4089
|
+
sessionId,
|
|
4090
|
+
input: { type: 'text', text: 'Create the report artifact.' },
|
|
4091
|
+
hostTools: { leaseId: hostLease.id },
|
|
4092
|
+
operation: { operationId: loadOrCreatePendingOperationId('space-artifact-run') },
|
|
4093
|
+
});
|
|
4094
|
+
```
|
|
4095
|
+
|
|
4096
|
+
The daemon injects session/run/lease/invocation identity; renderer, model, and
|
|
4097
|
+
ordinary tool input cannot choose it. CLI runs never inherit a Space lease just
|
|
4098
|
+
because Space later observes their session. The client memoizes one handler
|
|
4099
|
+
promise per invocation ID, and the daemon never replays a dispatched Host Tool.
|
|
4100
|
+
After a stable-client reconnect, call
|
|
4101
|
+
`runtime.hostTools.resume(leaseId, handlers)`. Bound run status reports
|
|
4102
|
+
`ready`, `waiting_host`, `expired`, or `terminal`. Disconnect or timeout after dispatch produces `host_outcome_unknown`; Space
|
|
4103
|
+
must reconcile the product side effect itself before offering a new user
|
|
4104
|
+
action. `runtime.hostTools.getInvocation(invocationId)` returns the durable
|
|
4105
|
+
metadata state `prepared`, `dispatched`, `completed`, `unknown`, or
|
|
4106
|
+
`not_dispatched`; it never returns handler input, result, or credential data.
|
|
4107
|
+
The daemon writes the `dispatched` marker before attempting the reverse frame
|
|
4108
|
+
and never auto-replays an invocation.
|
|
4109
|
+
|
|
4110
|
+
### Coder admission, typed events, and transport-safe inputs
|
|
4111
|
+
|
|
4112
|
+
The daemon enforces Coder session admission on the server for list/create/load,
|
|
4113
|
+
run, settings, delete, rewind, fork, compact, transcript, event, interaction,
|
|
4114
|
+
and diagnostic paths. Coder
|
|
4115
|
+
surfaces are `code`, `cli`, `repl`, `acp`, `a2a`, `sdk`, `ide`, and
|
|
4116
|
+
`space-desktop`. A session marked with
|
|
4117
|
+
Partner surface/profile metadata, or any unknown product surface, fails with
|
|
4118
|
+
typed `session_not_admitted` before mutation. Space must continue marking
|
|
4119
|
+
Partner sessions as `surface: 'partner'` and keep their inline storage root
|
|
4120
|
+
separate. Legacy sessions without a surface remain admitted for existing Coder
|
|
4121
|
+
compatibility, so absence of metadata is not a Partner namespace mechanism.
|
|
4122
|
+
|
|
4123
|
+
`RuntimeEventPayloadMap` and `RuntimeTypedEvent` provide the public
|
|
4124
|
+
discriminated contract for known events. Existing raw listeners remain
|
|
4125
|
+
compatible; consumers can use `parseRuntimeEvent(value)` before exhaustive
|
|
4126
|
+
handling. One unknown or malformed event is diagnosed and dropped without
|
|
4127
|
+
closing the observation stream.
|
|
4128
|
+
|
|
4129
|
+
Daemon clients use `RuntimeDaemonStartRunInput`. Function callbacks,
|
|
4130
|
+
`AbortSignal`, Extension Runtime objects, and guardrail instances are excluded
|
|
4131
|
+
from that type and rejected at runtime with `RuntimeTransportBoundaryError`
|
|
4132
|
+
and an exact value path if an untyped caller supplies them. Host-only values
|
|
4133
|
+
remain valid only for embedded Runtime calls.
|
|
4134
|
+
|
|
4135
|
+
### Recovery queries and stop preflight
|
|
4136
|
+
|
|
4137
|
+
`runtime.status.preflight()` returns the initialized logical-client count,
|
|
4138
|
+
active and queued runs, running/paused Workflows, every non-terminal External
|
|
4139
|
+
Agent task (including `unknown`), pending AskUser/permission records, blockers,
|
|
4140
|
+
and `canStop`. The background-work blockers are `active_workflows` and
|
|
4141
|
+
`active_agent_tasks`. The current facade counts as one; daemon self-connections
|
|
4142
|
+
and bounded health probes do not count. A second process changes the count to
|
|
4143
|
+
two, and its awaited `close()` makes the count converge back to one.
|
|
4144
|
+
|
|
4145
|
+
Preflight is useful for UI, but it is not a stop authorization token. Use
|
|
4146
|
+
`runtime.daemon.inspect()` to obtain one consistent management revision,
|
|
4147
|
+
verified owner fence, owner-policy revision, and preflight projection. Only
|
|
4148
|
+
`runtime.daemon.stopForInline()` atomically rechecks and commits a rollback.
|
|
4149
|
+
The management revision also advances when the preflight projection changes,
|
|
4150
|
+
so a Workflow or AgentTask lifecycle transition between inspect and commit
|
|
4151
|
+
invalidates the stale stop. Capability details
|
|
4152
|
+
`daemonManagement.backgroundWorkPreflight` and
|
|
4153
|
+
`daemonManagement.reverseBridgeDrainingFence` identify this complete contract.
|
|
4154
|
+
`runtime.operations.get()` reconciles durable mutations,
|
|
4155
|
+
`hostTools.getInvocation()` reconciles Host Tool metadata, and
|
|
4156
|
+
`permissions.listGrants()` returns the daemon-owned persistent grant set.
|
|
4157
|
+
|
|
4158
|
+
Terminal notification read/unread state is intentionally client-owned in
|
|
4159
|
+
v0.7.69 (`durableRecoveryQueries.terminalAcknowledgement === false`): Space
|
|
4160
|
+
persists its own UI acknowledgement cursor against Runtime/run terminal facts.
|
|
4161
|
+
This avoids a false claim that one client's acknowledgement is global daemon
|
|
4162
|
+
truth.
|
|
4163
|
+
|
|
4164
|
+
### Owner policy, rollback, and Electron boundary
|
|
4165
|
+
|
|
4166
|
+
Daemon and inline Coder use one profile fence. Do not compose
|
|
4167
|
+
`status.preflight()` with a low-level unconditional stop: another client or run
|
|
4168
|
+
can appear between those calls. The public rollback transaction gates new
|
|
4169
|
+
clients and mutations, rechecks the same Runtime and management/policy
|
|
4170
|
+
revisions, verifies there is no other client or active/queued/pending work,
|
|
4171
|
+
commits sticky inline policy while that daemon still owns the fence, and then
|
|
4172
|
+
requests shutdown.
|
|
4173
|
+
|
|
4174
|
+
```ts
|
|
4175
|
+
import {
|
|
4176
|
+
acquireKodaXInlineOwner,
|
|
4177
|
+
enableKodaXDaemonOwner,
|
|
4178
|
+
getKodaXRuntimeOwnerState,
|
|
4179
|
+
} from '@kodax-ai/kodax/runtime';
|
|
4180
|
+
|
|
4181
|
+
const management = await runtime.daemon.inspect();
|
|
4182
|
+
if (!management.preflight.canStop) {
|
|
4183
|
+
showRollbackBlockers(management.preflight.blockers);
|
|
4184
|
+
return;
|
|
4185
|
+
}
|
|
4186
|
+
|
|
4187
|
+
const rollback = await runtime.daemon.stopForInline({
|
|
4188
|
+
expectedRuntimeId: management.runtimeId,
|
|
4189
|
+
expectedRevision: management.revision,
|
|
4190
|
+
expectedOwnerPolicyRevision: management.ownerPolicy.revision,
|
|
4191
|
+
operation: { operationId: loadOrCreatePendingOperationId('coder-inline-rollback') },
|
|
4192
|
+
});
|
|
4193
|
+
|
|
4194
|
+
// `accepted` means inline policy is committed and shutdown is in progress.
|
|
4195
|
+
// Wait through the public owner-state query; never infer release from a PID.
|
|
4196
|
+
const shutdownDeadline = Date.now() + 30_000;
|
|
4197
|
+
while (getKodaXRuntimeOwnerState({ homeDir: kodaxHome, profile: 'coder' }).owner?.runtimeId
|
|
4198
|
+
=== rollback.runtimeId) {
|
|
4199
|
+
if (Date.now() >= shutdownDeadline) throw new Error('Timed out waiting for daemon owner release.');
|
|
4200
|
+
await delay(25);
|
|
4201
|
+
}
|
|
4202
|
+
const releasedOwner = getKodaXRuntimeOwnerState({ homeDir: kodaxHome, profile: 'coder' });
|
|
4203
|
+
if (releasedOwner.ownerStatus !== 'unowned') {
|
|
4204
|
+
throw new Error('Coder profile acquired a different owner during rollback.');
|
|
4205
|
+
}
|
|
4206
|
+
await runtime.close(); // Detach only; it does not perform a second stop.
|
|
4207
|
+
const inlineOwner = acquireKodaXInlineOwner({ homeDir: kodaxHome, profile: 'coder' });
|
|
4208
|
+
|
|
4209
|
+
// Later, after the inline owner has released its fence:
|
|
4210
|
+
inlineOwner.close();
|
|
4211
|
+
const daemonPolicy = enableKodaXDaemonOwner({ homeDir: kodaxHome, profile: 'coder' });
|
|
4212
|
+
// daemonPolicy.revision is authoritative; no expectedRevision guess is needed.
|
|
4213
|
+
```
|
|
4214
|
+
|
|
4215
|
+
Any management revision change, another logical client, active or queued run,
|
|
4216
|
+
running/paused Workflow, non-terminal/unknown AgentTask, pending
|
|
4217
|
+
AskUser/permission, or in-flight mutation returns structured `conflict`; the
|
|
4218
|
+
daemon remains running and policy remains unchanged. Draining also rejects
|
|
4219
|
+
credential and Host Tool state changes without journaling their secrets or
|
|
4220
|
+
results. The inline policy is sticky: later CLI auto-start is rejected until
|
|
4221
|
+
`enableKodaXDaemonOwner()` changes it back to `daemon`. `runtime.close()` still
|
|
4222
|
+
only detaches. Stale-owner handling validates the owned lock/state and never
|
|
4223
|
+
kills a process merely because a PID was reused.
|
|
4224
|
+
|
|
4225
|
+
Keep all trusted objects in Electron Main: daemon token/endpoint, stable client
|
|
4226
|
+
identity, operation IDs, owner policy, keychain broker, Host Tool handlers, and
|
|
4227
|
+
permission-grant administration. Renderer IPC should expose product-specific
|
|
4228
|
+
commands and sanitized projections only. Never pass daemon credentials,
|
|
4229
|
+
leases, operation epochs, or trusted session/run context to renderer or model
|
|
4230
|
+
tool arguments.
|
|
4231
|
+
|
|
4232
|
+
---
|
|
4233
|
+
|
|
4234
|
+
## 24. Runtime-owned Auto Mode and plan-approval bridges (v0.7.72–v0.7.73)
|
|
4235
|
+
|
|
4236
|
+
Auto Mode is a Runtime session contract, including in shared-daemon mode. Do
|
|
4237
|
+
not implement a second classifier or decide permissions from a client-side
|
|
4238
|
+
`beforeToolExecute` hook before the Runtime has classified the call.
|
|
4239
|
+
|
|
4240
|
+
### Configure the session, not an individual UI callback
|
|
4241
|
+
|
|
4242
|
+
```ts
|
|
4243
|
+
await runtime.sessions.updateSettings(session.id, {
|
|
4244
|
+
permissionMode: 'auto',
|
|
4245
|
+
autoModeEngine: 'llm',
|
|
4246
|
+
autoModeClassifierModel: 'zhipu:glm-5.2', // optional; otherwise follow the run model
|
|
4247
|
+
autoModeTimeoutMs: 20_000, // positive safe integer, optional
|
|
4248
|
+
autoModeSpeculativeWindowMs: 0, // non-negative safe integer, optional
|
|
4249
|
+
executionCwd: projectDirectory,
|
|
4250
|
+
});
|
|
4251
|
+
```
|
|
4252
|
+
|
|
4253
|
+
All three Auto fields are durable session settings in inline, Worker, and
|
|
4254
|
+
daemon forms. A `null` patch removes an optional override. Timeout must be a
|
|
4255
|
+
positive safe integer; speculative window must be non-negative, so `0` is a
|
|
4256
|
+
valid request to wait for the actual verdict. Daemon capability discovery
|
|
4257
|
+
advertises all fields in `sharedSessionSettings.keys`.
|
|
4258
|
+
|
|
4259
|
+
SDK hosts that need config precedence without creating a REPL can reuse the
|
|
4260
|
+
same typed resolver as KodaX:
|
|
4261
|
+
|
|
4262
|
+
```ts
|
|
4263
|
+
import {
|
|
4264
|
+
loadAutoModeSettings,
|
|
4265
|
+
resolveAutoModeSettings,
|
|
4266
|
+
type ResolveAutoModeSettingsInput,
|
|
4267
|
+
} from '@kodax-ai/kodax/repl';
|
|
4268
|
+
|
|
4269
|
+
const persisted = loadAutoModeSettings(process.env);
|
|
4270
|
+
const preview = resolveAutoModeSettings({
|
|
4271
|
+
settings: { engine: 'llm', speculativeWindowMs: 0 },
|
|
4272
|
+
env: process.env,
|
|
4273
|
+
});
|
|
4274
|
+
```
|
|
4275
|
+
|
|
4276
|
+
`resolveAutoModeSettings()` is pure. `loadAutoModeSettings()` reads the KodaX
|
|
4277
|
+
config once and delegates to that resolver; `loadConfig()` declares and
|
|
4278
|
+
returns the same optional `autoMode` object.
|
|
4279
|
+
|
|
4280
|
+
Starting with the v0.7.73 patch, `permissionMode: 'auto'` with an omitted
|
|
4281
|
+
`autoModeEngine` still means the documented `llm` default and is still owned by
|
|
4282
|
+
Runtime. If neither `autoModeClassifierModel` nor the effective run/session/
|
|
4283
|
+
Runtime model exists, `runs.start()` rejects with
|
|
4284
|
+
`RuntimeAutoModeConfigurationError` (`code:
|
|
4285
|
+
'auto_mode_classifier_model_required'`, `recoverable: true`) before provider
|
|
4286
|
+
construction, a classifier call, or a pending permission. Blank and malformed
|
|
4287
|
+
classifier model specs are rejected by the same typed configuration boundary;
|
|
4288
|
+
a live rules-to-LLM switch is blocked rather than converted into approval work.
|
|
4289
|
+
|
|
4290
|
+
Direct consumers of `createAutoModeToolGuardrail()` receive the same terminal
|
|
4291
|
+
model boundary. After resolving CLI/env/session/settings/live-default
|
|
4292
|
+
precedence, an empty effective model returns a local configuration `block`
|
|
4293
|
+
before provider lookup. It does not call `askUser`, mutate the denial or
|
|
4294
|
+
circuit-breaker trackers, or change the engine to rules. An explicit non-empty
|
|
4295
|
+
classifier override remains valid even when the main-session model is empty.
|
|
4296
|
+
|
|
4297
|
+
The Runtime owns one serialized permission-settings stream and one shared
|
|
4298
|
+
engine/denial/breaker state per Session. It reuses bounded context-specific
|
|
4299
|
+
guardrails across turns while provider/model, repository boundary, execution
|
|
4300
|
+
directory, classifier model, and timeout remain the same. Updating one of
|
|
4301
|
+
those inputs selects a new context guardrail by design without copying stale
|
|
4302
|
+
state from a queued turn. Active runs, queued runs, explicit settings updates,
|
|
4303
|
+
and automatic LLM-to-rules fallback merge through the same Session mutation
|
|
4304
|
+
queue; fallback is persisted before a later classification reads the engine.
|
|
4305
|
+
|
|
4306
|
+
### What an embedder should expect
|
|
4307
|
+
|
|
4308
|
+
The Runtime's execution order is fixed:
|
|
4309
|
+
|
|
4310
|
+
```text
|
|
4311
|
+
Runtime Auto Mode guardrail -> host permission bridge only for escalate -> tool execution
|
|
4312
|
+
```
|
|
4313
|
+
|
|
4314
|
+
Consequently, an LLM/rules `allow` does not create a pending permission request
|
|
4315
|
+
just because a host installed a static approval hook. `block` does not become a
|
|
4316
|
+
spurious approval prompt. A real `escalate` uses the existing shared
|
|
4317
|
+
`runtime.permissions` flow, so another authorized client may render and answer
|
|
4318
|
+
it. Hosts should subscribe to permission events to display such a request, but
|
|
4319
|
+
must not treat a missing request as an error for a safe tool call.
|
|
4320
|
+
|
|
4321
|
+
The classifier deadline remains 20 seconds by default and includes connection
|
|
4322
|
+
setup, provider Retry-After/backoff, inference, and stream completion. KodaX
|
|
4323
|
+
does not solve timeouts by extending that deadline indefinitely. Before the
|
|
4324
|
+
provider call it removes assistant prose/thinking and image paths, limits each
|
|
4325
|
+
tool result to 2 KiB and the serialized permission-relevant transcript to
|
|
4326
|
+
8 KiB, then enforces 16 KiB action and 32 KiB total-prompt ceilings plus a
|
|
4327
|
+
256-token output cap. An oversized action or prompt escalates without a
|
|
4328
|
+
provider call; it is never truncated into an automatic allow. These limits are
|
|
4329
|
+
owned by `classify()` itself, so custom callers cannot accidentally bypass the
|
|
4330
|
+
session-history boundary.
|
|
4331
|
+
|
|
4332
|
+
`ClassifyDecision.diagnostics` and the lower-level
|
|
4333
|
+
`SideQueryResult.diagnostics` expose provider, model, effective timeout,
|
|
4334
|
+
elapsed time, retry count/wait, and a coarse terminal phase without including
|
|
4335
|
+
the prompt, action, messages, or response text. `pre_output` means no non-empty
|
|
4336
|
+
text delta was observed; `streaming` means output began before termination.
|
|
4337
|
+
`firstOutputMs` and `streamMs` are present only when the provider adapter emits
|
|
4338
|
+
a text delta. The current provider API cannot honestly separate DNS/connect,
|
|
4339
|
+
TLS, provider queueing, and inference, so embedders must not infer those stages
|
|
4340
|
+
from `pre_output`.
|
|
4341
|
+
|
|
4342
|
+
The permission event's `inputPreview` is a display-safe diagnostic projection:
|
|
4343
|
+
it is bounded, credential-redacted, valid JSON, and includes the effective
|
|
4344
|
+
execution directory. Use the Runtime owner’s typed tool input for execution;
|
|
4345
|
+
do not reconstruct or authorize a tool from the preview. `gitRoot` remains the
|
|
4346
|
+
session repository safety boundary, whereas relative operands resolve from the
|
|
4347
|
+
validated `executionCwd`. In particular, quoted Python/JavaScript/regexp source
|
|
4348
|
+
inside a shell command is not a path operand.
|
|
4349
|
+
|
|
4350
|
+
The user-level `.kodax` directory is a credential/configuration boundary, not
|
|
4351
|
+
an ordinary project path. Direct shell mutations, output redirects, and
|
|
4352
|
+
recognized nested-shell payloads whose target is provably beneath that
|
|
4353
|
+
directory are rejected before LLM classification. The check is segment-safe
|
|
4354
|
+
and Windows case-insensitive. KodaX deliberately does not scan arbitrary
|
|
4355
|
+
quoted language source for path-looking substrings: doing so would turn Python,
|
|
4356
|
+
JavaScript, YAML, and regular expressions into false Tier-0 matches. Trusted
|
|
4357
|
+
configuration changes should use the KodaX config CLI or SDK configuration API.
|
|
4358
|
+
|
|
4359
|
+
### 0.7.x source compatibility
|
|
4360
|
+
|
|
4361
|
+
The v0.7.72 public declarations retain the following migration aliases:
|
|
4362
|
+
|
|
4363
|
+
| Legacy source | Current source | Contract |
|
|
4364
|
+
|---|---|---|
|
|
4365
|
+
| `agentMode: 'amaw'` | `'ama'` | accepted as deprecated input and normalized to AMA; no separate AMAW runtime is restored |
|
|
4366
|
+
| `SkillSource` | `ResolvedSkillSource` | formal source union remains `project \| user \| plugin \| builtin`; only resolved discovery output adds `learned` |
|
|
4367
|
+
| `RuntimeDaemonPreflight.activeAgentTasks` | `activeAgentTurns` | both required fields are returned and reference the same array throughout the 0.7.x line |
|
|
4368
|
+
|
|
4369
|
+
### Plan capability is opt-in
|
|
4370
|
+
|
|
4371
|
+
`exit_plan_mode` is exposed to a Runtime run only when that run supplies an
|
|
4372
|
+
approval bridge. A daemon or headless host that cannot approve a plan should
|
|
4373
|
+
leave the bridge absent; KodaX removes the tool from that run's scope.
|
|
4374
|
+
|
|
4375
|
+
```ts
|
|
4376
|
+
const planned = await runtime.runs.start({
|
|
4377
|
+
sessionId: session.id,
|
|
4378
|
+
prompt: 'Draft a migration plan, then ask for approval.',
|
|
4379
|
+
options: {
|
|
4380
|
+
events: {
|
|
4381
|
+
exitPlanMode: async () => showPlanAndAskUser(),
|
|
4382
|
+
},
|
|
4383
|
+
},
|
|
4384
|
+
});
|
|
4385
|
+
await planned.result;
|
|
4386
|
+
```
|
|
4387
|
+
|
|
4388
|
+
The callback belongs in a trusted host process (for Electron, Main rather than
|
|
4389
|
+
renderer). It is intentionally not inferred from the presence of a permission
|
|
4390
|
+
UI: tool permission and plan approval are different user decisions.
|
|
4391
|
+
|
|
4392
|
+
See [ADR-056](ADR.md#adr-056-runtime-owns-auto-mode-permission-decisions-and-host-capability-exposure)
|
|
4393
|
+
for the ownership decision, [the v0.7.72 design](features/v0.7.72.md#2026-07-18-runtime-permission-queue-and-resume-closure)
|
|
4394
|
+
for the release boundary, and [Known Issue 187](KNOWN_ISSUES.md#187-shared-daemon-auto-permission-ownership-upgrade-fencing-preview-bounds-and-sdk-compatibility-were-incomplete)
|
|
4395
|
+
for the final capability-upgrade and compatibility closure.
|
|
4396
|
+
|
|
4397
|
+
---
|
|
4398
|
+
|
|
4399
|
+
## 25. Always-on context compaction and bounded transcript recovery (v0.7.74)
|
|
4400
|
+
|
|
4401
|
+
Automatic large compaction is always enabled. `enabled` remains accepted for
|
|
4402
|
+
v0.7.x source compatibility, but `false` is normalized to `true`.
|
|
4403
|
+
`triggerPercent` defaults to `75` and is clamped to `15..90`. The optional
|
|
4404
|
+
absolute threshold is inactive when omitted or zero; otherwise the smaller
|
|
4405
|
+
percentage, absolute, and physical-capacity threshold wins.
|
|
4406
|
+
|
|
4407
|
+
```ts
|
|
4408
|
+
const run = await startKodaX({
|
|
4409
|
+
provider: 'zhipu-coding',
|
|
4410
|
+
model: 'glm-5.2',
|
|
4411
|
+
compaction: {
|
|
4412
|
+
triggerPercent: 60,
|
|
4413
|
+
triggerTokens: 300_000,
|
|
4414
|
+
},
|
|
4415
|
+
});
|
|
4416
|
+
```
|
|
4417
|
+
|
|
4418
|
+
The recent raw tail is 20% of that effective trigger, not 20% of the model's
|
|
4419
|
+
maximum context window. A manual Runtime compact bypasses only the trigger
|
|
4420
|
+
comparison and uses the Session's same effective policy:
|
|
4421
|
+
|
|
4422
|
+
```ts
|
|
4423
|
+
await runtime.sessions.updateSettings(session.id, {
|
|
4424
|
+
compactionTriggerPercent: 60,
|
|
4425
|
+
compactionTriggerTokens: 300_000,
|
|
4426
|
+
});
|
|
4427
|
+
|
|
4428
|
+
await runtime.sessions.compact({ sessionId: session.id });
|
|
4429
|
+
```
|
|
4430
|
+
|
|
4431
|
+
Setting `compactionTriggerTokens: 0` removes the absolute Session override.
|
|
4432
|
+
Percentage updates are normalized to `15..90`; negative/fractional absolute
|
|
4433
|
+
values are rejected. Explicit per-run `options.compaction` values override
|
|
4434
|
+
Session settings.
|
|
4435
|
+
|
|
4436
|
+
Large compaction covers the complete eligible prefix once, preserves an atomic
|
|
4437
|
+
recent tail, and installs a synthetic user checkpoint. Every genuine user query
|
|
4438
|
+
is rendered mechanically in its checkpoint ledger; tool-result wire messages
|
|
4439
|
+
and synthetic prompts are excluded. The normal summary request preserves the
|
|
4440
|
+
main request's system, message, tool, model, and reasoning prefix and appends a
|
|
4441
|
+
text-only ephemeral instruction so providers can reuse prompt/KV cache.
|
|
4442
|
+
|
|
4443
|
+
The exact pre-compaction transcript has a separate durability guarantee. The
|
|
4444
|
+
root host persists all pre-compaction messages (including messages created in
|
|
4445
|
+
the active Run) before old payload is evicted from memory. Island records are
|
|
4446
|
+
flushed before a slim main JSONL is published. If the main replacement fails,
|
|
4447
|
+
main and sidecar may temporarily overlap, but stable entry IDs project the
|
|
4448
|
+
logical entry once. A persistence failure keeps the exact live copy; child
|
|
4449
|
+
compaction never writes root Session lineage.
|
|
4450
|
+
|
|
4451
|
+
The in-process `onCompactedMessages` callback may return a `Promise`. KodaX
|
|
4452
|
+
awaits it before the next provider request and before
|
|
4453
|
+
`context.compaction.finished`. Embedded and daemon Runtime execution always
|
|
4454
|
+
uses Runtime-owned Session storage, regardless of a client-side
|
|
4455
|
+
`persistedByHost` value; a daemon client cannot be the durability owner because
|
|
4456
|
+
its callback is not present in the daemon process. Runtime-backed Ink and
|
|
4457
|
+
classic REPL hosts therefore update only their live projection after the
|
|
4458
|
+
Runtime acknowledgement; they do not perform a second Session write. If a
|
|
4459
|
+
headless Runtime Run compacts before its first routine snapshot, Runtime seeds
|
|
4460
|
+
the new Session from explicit Run metadata before applying the exact compact
|
|
4461
|
+
transaction. A rejected durability callback also rolls back the tentative
|
|
4462
|
+
`contextRevision`, so a later successful compact does not expose a phantom gap.
|
|
4463
|
+
|
|
4464
|
+
### Context-owned events
|
|
4465
|
+
|
|
4466
|
+
`context.compaction.finished` is the canonical post-commit Runtime fact. It
|
|
4467
|
+
includes stable root/child identity, revision, before/after tokens, strategy,
|
|
4468
|
+
effective trigger, protected budget, and component accounting. Consumers must
|
|
4469
|
+
not use `scope: 'worker'` as a parent/child identity substitute.
|
|
4470
|
+
|
|
4471
|
+
```ts
|
|
4472
|
+
const subscription = runtime.events.subscribe(
|
|
4473
|
+
{ sessionId: session.id, types: ['context.compaction.finished'] },
|
|
4474
|
+
(event) => {
|
|
4475
|
+
if (event.type !== 'context.compaction.finished') return;
|
|
4476
|
+
const fact = event.payload;
|
|
4477
|
+
if (fact.contextKind === 'root' && fact.committed) {
|
|
4478
|
+
renderRootContext(fact.tokensAfter, fact.tokensBefore);
|
|
4479
|
+
}
|
|
4480
|
+
},
|
|
4481
|
+
);
|
|
4482
|
+
```
|
|
4483
|
+
|
|
4484
|
+
Legacy `onCompactStats`/`onCompact` callbacks remain compatibility projections.
|
|
4485
|
+
The old `onCompact` callback now receives the post-compact count; it no longer
|
|
4486
|
+
echoes the pre-compact `currentTokens` value. Hosts that need ownership or
|
|
4487
|
+
component metrics should use `onContextCompactionFinished` or the Runtime
|
|
4488
|
+
event. Compatibility success callbacks fire only after a strict token
|
|
4489
|
+
reduction has restored physical request validity and committed. An unchanged,
|
|
4490
|
+
failed, stale, or still-oversized candidate is not a successful compaction.
|
|
4491
|
+
|
|
4492
|
+
### Transcript observation below the daemon frame limit
|
|
4493
|
+
|
|
4494
|
+
`sessions.observe()` no longer embeds `FullTranscriptSessionData`. Its snapshot
|
|
4495
|
+
contains a bounded `RuntimeTranscriptSlice`. Inline entries carry the complete
|
|
4496
|
+
transcript entry; an oversized entry carries an explicit descriptor.
|
|
4497
|
+
|
|
4498
|
+
```ts
|
|
4499
|
+
const observation = await runtime.sessions.observe(session.id, onLiveEvent);
|
|
4500
|
+
let page = observation.snapshot.transcript;
|
|
4501
|
+
|
|
4502
|
+
while (page) {
|
|
4503
|
+
for (const descriptor of page.entries) {
|
|
4504
|
+
if (descriptor.entry) consume(descriptor.entry);
|
|
4505
|
+
else await consumeEntryChunks(runtime, session.id, page.revision, descriptor.index);
|
|
4506
|
+
}
|
|
4507
|
+
if (!page.hasMore) break;
|
|
4508
|
+
page = await runtime.sessions.transcriptPage({
|
|
4509
|
+
sessionId: session.id,
|
|
4510
|
+
cursor: page.nextCursor,
|
|
4511
|
+
});
|
|
4512
|
+
}
|
|
4513
|
+
```
|
|
4514
|
+
|
|
4515
|
+
`transcriptEntryChunk()` returns lossless `base64-json` chunks. Concatenate the
|
|
4516
|
+
decoded bytes and parse JSON only after `hasMore` becomes false. Page and entry
|
|
4517
|
+
cursors are opaque and revision-bound; a changed transcript produces an
|
|
4518
|
+
explicit resync error, so restart from a fresh observation. The shared daemon's
|
|
4519
|
+
legacy `session.transcript` method rejects payloads above 512 KiB and names the
|
|
4520
|
+
page/chunk methods rather than attempting a frame near the 8 MiB ceiling.
|
|
4521
|
+
|
|
4522
|
+
### Search compacted history before fetching exact content
|
|
4523
|
+
|
|
4524
|
+
Use `transcriptSearch()` when the host or user knows a historical detail but
|
|
4525
|
+
not its page/index. It searches the authoritative main-plus-sidecar lineage and
|
|
4526
|
+
returns bounded revision-bound hits with stable `entryId`/`logicalId`, entry
|
|
4527
|
+
index, role/source, timestamp, active/compacted status, and a citation:
|
|
4528
|
+
|
|
4529
|
+
```ts
|
|
4530
|
+
const found = await runtime.sessions.transcriptSearch({
|
|
4531
|
+
sessionId: session.id,
|
|
4532
|
+
query: 'permission test output capture',
|
|
4533
|
+
role: 'assistant',
|
|
4534
|
+
limit: 5,
|
|
4535
|
+
});
|
|
4536
|
+
|
|
4537
|
+
for (const hit of found.hits) {
|
|
4538
|
+
renderSearchHit(hit.citation, hit.snippet);
|
|
4539
|
+
// For an oversized exact entry, pass found.revision + hit.entryIndex to
|
|
4540
|
+
// transcriptEntryChunk(); ordinary entries can be obtained from the page.
|
|
4541
|
+
}
|
|
4542
|
+
```
|
|
4543
|
+
|
|
4544
|
+
Search is deterministic Unicode lexical/metadata ranking, not an embedding or
|
|
4545
|
+
background-model index. The Action LLM gets the corresponding
|
|
4546
|
+
`session_history_search` and `session_history_read` pair only when its current
|
|
4547
|
+
Run owns full-lineage-capable Session storage. A root Run reads its root
|
|
4548
|
+
lineage. A persistent child Run gets a separately minted hidden
|
|
4549
|
+
`managed-task-worker` Session and can recover only that child's compacted
|
|
4550
|
+
history; it is never given root-history access. Storage-less Runs and a tool
|
|
4551
|
+
visibility policy that hides either member expose neither member. Results are
|
|
4552
|
+
low-authority historical evidence; current instructions and freshly verified
|
|
4553
|
+
workspace state take precedence. System/control entries, hidden-only content,
|
|
4554
|
+
synthetic current or legacy `[对话历史摘要]` checkpoints, and `[compacted]`
|
|
4555
|
+
placeholders are neither searchable nor directly readable. Short ordinary
|
|
4556
|
+
terms do not gain a metadata match merely because they occur inside a random
|
|
4557
|
+
entry ID; direct identifier lookup is reserved for a sufficiently specific ID
|
|
4558
|
+
query. Sessions compacted by older builds without an exact main/sidecar copy
|
|
4559
|
+
cannot reconstruct bytes that were already discarded.
|
|
4560
|
+
|
|
4561
|
+
Clients that depend on these guarantees should require
|
|
4562
|
+
`contextCompaction: 3`, `transcriptPaging: 1`, and `transcriptSearch: 1` during
|
|
4563
|
+
connection.
|
|
4178
4564
|
|
|
4179
4565
|
---
|
|
4180
4566
|
|
|
4181
|
-
##
|
|
4182
|
-
|
|
4183
|
-
Auto Mode is a Runtime session contract, including in shared-daemon mode. Do
|
|
4184
|
-
not implement a second classifier or decide permissions from a client-side
|
|
4185
|
-
`beforeToolExecute` hook before the Runtime has classified the call.
|
|
4186
|
-
|
|
4187
|
-
### Configure the session, not an individual UI callback
|
|
4188
|
-
|
|
4189
|
-
```ts
|
|
4190
|
-
await runtime.sessions.updateSettings(session.id, {
|
|
4191
|
-
permissionMode: 'auto',
|
|
4192
|
-
autoModeEngine: 'llm',
|
|
4193
|
-
autoModeClassifierModel: 'zhipu:glm-5.2', // optional; otherwise follow the run model
|
|
4194
|
-
autoModeTimeoutMs: 20_000, // positive safe integer, optional
|
|
4195
|
-
autoModeSpeculativeWindowMs: 0, // non-negative safe integer, optional
|
|
4196
|
-
executionCwd: projectDirectory,
|
|
4197
|
-
});
|
|
4198
|
-
```
|
|
4199
|
-
|
|
4200
|
-
All three Auto fields are durable session settings in inline, Worker, and
|
|
4201
|
-
daemon forms. A `null` patch removes an optional override. Timeout must be a
|
|
4202
|
-
positive safe integer; speculative window must be non-negative, so `0` is a
|
|
4203
|
-
valid request to wait for the actual verdict. Daemon capability discovery
|
|
4204
|
-
advertises all fields in `sharedSessionSettings.keys`.
|
|
4205
|
-
|
|
4206
|
-
SDK hosts that need config precedence without creating a REPL can reuse the
|
|
4207
|
-
same typed resolver as KodaX:
|
|
4208
|
-
|
|
4209
|
-
```ts
|
|
4210
|
-
import {
|
|
4211
|
-
loadAutoModeSettings,
|
|
4212
|
-
resolveAutoModeSettings,
|
|
4213
|
-
type ResolveAutoModeSettingsInput,
|
|
4214
|
-
} from '@kodax-ai/kodax/repl';
|
|
4215
|
-
|
|
4216
|
-
const persisted = loadAutoModeSettings(process.env);
|
|
4217
|
-
const preview = resolveAutoModeSettings({
|
|
4218
|
-
settings: { engine: 'llm', speculativeWindowMs: 0 },
|
|
4219
|
-
env: process.env,
|
|
4220
|
-
});
|
|
4221
|
-
```
|
|
4222
|
-
|
|
4223
|
-
`resolveAutoModeSettings()` is pure. `loadAutoModeSettings()` reads the KodaX
|
|
4224
|
-
config once and delegates to that resolver; `loadConfig()` declares and
|
|
4225
|
-
returns the same optional `autoMode` object.
|
|
4226
|
-
|
|
4227
|
-
Starting with the v0.7.73 patch, `permissionMode: 'auto'` with an omitted
|
|
4228
|
-
`autoModeEngine` still means the documented `llm` default and is still owned by
|
|
4229
|
-
Runtime. If neither `autoModeClassifierModel` nor the effective run/session/
|
|
4230
|
-
Runtime model exists, `runs.start()` rejects with
|
|
4231
|
-
`RuntimeAutoModeConfigurationError` (`code:
|
|
4232
|
-
'auto_mode_classifier_model_required'`, `recoverable: true`) before provider
|
|
4233
|
-
construction, a classifier call, or a pending permission. Blank and malformed
|
|
4234
|
-
classifier model specs are rejected by the same typed configuration boundary;
|
|
4235
|
-
a live rules-to-LLM switch is blocked rather than converted into approval work.
|
|
4236
|
-
|
|
4237
|
-
Direct consumers of `createAutoModeToolGuardrail()` receive the same terminal
|
|
4238
|
-
model boundary. After resolving CLI/env/session/settings/live-default
|
|
4239
|
-
precedence, an empty effective model returns a local configuration `block`
|
|
4240
|
-
before provider lookup. It does not call `askUser`, mutate the denial or
|
|
4241
|
-
circuit-breaker trackers, or change the engine to rules. An explicit non-empty
|
|
4242
|
-
classifier override remains valid even when the main-session model is empty.
|
|
4243
|
-
|
|
4244
|
-
The Runtime owns one serialized permission-settings stream and one shared
|
|
4245
|
-
engine/denial/breaker state per Session. It reuses bounded context-specific
|
|
4246
|
-
guardrails across turns while provider/model, repository boundary, execution
|
|
4247
|
-
directory, classifier model, and timeout remain the same. Updating one of
|
|
4248
|
-
those inputs selects a new context guardrail by design without copying stale
|
|
4249
|
-
state from a queued turn. Active runs, queued runs, explicit settings updates,
|
|
4250
|
-
and automatic LLM-to-rules fallback merge through the same Session mutation
|
|
4251
|
-
queue; fallback is persisted before a later classification reads the engine.
|
|
4252
|
-
|
|
4253
|
-
### What an embedder should expect
|
|
4567
|
+
## 26. Agent mailbox control versus SDK event telemetry (v0.7.74)
|
|
4254
4568
|
|
|
4255
|
-
The Runtime
|
|
4569
|
+
The model-visible `wait_agent` tool and the public Runtime Actor event APIs have
|
|
4570
|
+
different jobs. Do not expose `runtime.agents.wait()` to the model as though it
|
|
4571
|
+
were the same operation.
|
|
4256
4572
|
|
|
4257
|
-
|
|
4258
|
-
Runtime Auto Mode guardrail -> host permission bridge only for escalate -> tool execution
|
|
4259
|
-
```
|
|
4260
|
-
|
|
4261
|
-
Consequently, an LLM/rules `allow` does not create a pending permission request
|
|
4262
|
-
just because a host installed a static approval hook. `block` does not become a
|
|
4263
|
-
spurious approval prompt. A real `escalate` uses the existing shared
|
|
4264
|
-
`runtime.permissions` flow, so another authorized client may render and answer
|
|
4265
|
-
it. Hosts should subscribe to permission events to display such a request, but
|
|
4266
|
-
must not treat a missing request as an error for a safe tool call.
|
|
4267
|
-
|
|
4268
|
-
The classifier deadline remains 20 seconds by default and includes connection
|
|
4269
|
-
setup, provider Retry-After/backoff, inference, and stream completion. KodaX
|
|
4270
|
-
does not solve timeouts by extending that deadline indefinitely. Before the
|
|
4271
|
-
provider call it removes assistant prose/thinking and image paths, limits each
|
|
4272
|
-
tool result to 2 KiB and the serialized permission-relevant transcript to
|
|
4273
|
-
8 KiB, then enforces 16 KiB action and 32 KiB total-prompt ceilings plus a
|
|
4274
|
-
256-token output cap. An oversized action or prompt escalates without a
|
|
4275
|
-
provider call; it is never truncated into an automatic allow. These limits are
|
|
4276
|
-
owned by `classify()` itself, so custom callers cannot accidentally bypass the
|
|
4277
|
-
session-history boundary.
|
|
4278
|
-
|
|
4279
|
-
`ClassifyDecision.diagnostics` and the lower-level
|
|
4280
|
-
`SideQueryResult.diagnostics` expose provider, model, effective timeout,
|
|
4281
|
-
elapsed time, retry count/wait, and a coarse terminal phase without including
|
|
4282
|
-
the prompt, action, messages, or response text. `pre_output` means no non-empty
|
|
4283
|
-
text delta was observed; `streaming` means output began before termination.
|
|
4284
|
-
`firstOutputMs` and `streamMs` are present only when the provider adapter emits
|
|
4285
|
-
a text delta. The current provider API cannot honestly separate DNS/connect,
|
|
4286
|
-
TLS, provider queueing, and inference, so embedders must not infer those stages
|
|
4287
|
-
from `pre_output`.
|
|
4288
|
-
|
|
4289
|
-
The permission event's `inputPreview` is a display-safe diagnostic projection:
|
|
4290
|
-
it is bounded, credential-redacted, valid JSON, and includes the effective
|
|
4291
|
-
execution directory. Use the Runtime owner’s typed tool input for execution;
|
|
4292
|
-
do not reconstruct or authorize a tool from the preview. `gitRoot` remains the
|
|
4293
|
-
session repository safety boundary, whereas relative operands resolve from the
|
|
4294
|
-
validated `executionCwd`. In particular, quoted Python/JavaScript/regexp source
|
|
4295
|
-
inside a shell command is not a path operand.
|
|
4296
|
-
|
|
4297
|
-
The user-level `.kodax` directory is a credential/configuration boundary, not
|
|
4298
|
-
an ordinary project path. Direct shell mutations, output redirects, and
|
|
4299
|
-
recognized nested-shell payloads whose target is provably beneath that
|
|
4300
|
-
directory are rejected before LLM classification. The check is segment-safe
|
|
4301
|
-
and Windows case-insensitive. KodaX deliberately does not scan arbitrary
|
|
4302
|
-
quoted language source for path-looking substrings: doing so would turn Python,
|
|
4303
|
-
JavaScript, YAML, and regular expressions into false Tier-0 matches. Trusted
|
|
4304
|
-
configuration changes should use the KodaX config CLI or SDK configuration API.
|
|
4305
|
-
|
|
4306
|
-
### 0.7.x source compatibility
|
|
4307
|
-
|
|
4308
|
-
The v0.7.72 public declarations retain the following migration aliases:
|
|
4309
|
-
|
|
4310
|
-
| Legacy source | Current source | Contract |
|
|
4573
|
+
| Need | API | Wake/data contract |
|
|
4311
4574
|
|---|---|---|
|
|
4312
|
-
|
|
|
4313
|
-
|
|
|
4314
|
-
|
|
|
4315
|
-
|
|
4316
|
-
|
|
4575
|
+
| Let the action model yield until useful coordination evidence exists | `wait_agent({ timeout_ms })` | Caller-scoped Agent mailbox, root user input, interruption, or timeout; returns only a small acknowledgement. |
|
|
4576
|
+
| Render or diagnose Actor activity | `runtime.agents.events(sessionId, afterSequence?)` | Bounded event snapshot/replay, including progress and terminal events. |
|
|
4577
|
+
| Long-poll Actor telemetry from a host | `runtime.agents.wait(sessionId, afterSequence?, timeoutMs?)` | Returns the next sequenced Actor event, including progress. |
|
|
4578
|
+
| Read one known result | `runtime.agents.output(sessionId, actorPath, turnId?)` | Bounded current/terminal output and structured artifact metadata. |
|
|
4579
|
+
| Deliver a real user follow-up to the active root Run | `runtime.runs.submitInput(...)` | Ordered active-run input delivered at the next safe Runner boundary; requires `interruptInput:1`. |
|
|
4317
4580
|
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
leave the bridge absent; KodaX removes the tool from that run's scope.
|
|
4581
|
+
For example, a host activity view can replay the current tail and then wait
|
|
4582
|
+
from its last sequence without causing another action-model request:
|
|
4321
4583
|
|
|
4322
4584
|
```ts
|
|
4323
|
-
const
|
|
4324
|
-
|
|
4325
|
-
prompt: 'Draft a migration plan, then ask for approval.',
|
|
4326
|
-
options: {
|
|
4327
|
-
events: {
|
|
4328
|
-
exitPlanMode: async () => showPlanAndAskUser(),
|
|
4329
|
-
},
|
|
4330
|
-
},
|
|
4331
|
-
});
|
|
4332
|
-
await planned.result;
|
|
4333
|
-
```
|
|
4585
|
+
const snapshot = await runtime.agents.events(session.id);
|
|
4586
|
+
for (const event of snapshot) renderActorEvent(event);
|
|
4334
4587
|
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4588
|
+
const afterSequence = snapshot.at(-1)?.sequence;
|
|
4589
|
+
const next = await runtime.agents.wait(session.id, afterSequence, 30_000);
|
|
4590
|
+
if (next) renderActorEvent(next);
|
|
4591
|
+
```
|
|
4338
4592
|
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4593
|
+
Model `wait_agent` has only `timeout_ms` in its schema (10,000 to 3,600,000 ms,
|
|
4594
|
+
default 120,000). Actor progress and Runtime `system-reminder` messages do not
|
|
4595
|
+
end that wait. A scoped Agent message/completion produces `mailbox`; queued root
|
|
4596
|
+
input produces `user_input_pending`; cancellation and expiry produce
|
|
4597
|
+
`interrupted` and `wait_expired`. Authenticated Agent evidence is drained at the
|
|
4598
|
+
next safe Runner boundary as synthetic context, while root input remains a real
|
|
4599
|
+
user turn. The acknowledgement itself never carries raw event batches.
|
|
4600
|
+
|
|
4601
|
+
Completion delivery is post-transcript and crash-recoverable. The Actor snapshot
|
|
4602
|
+
persists the explicit root completion turn IDs that still await transcript
|
|
4603
|
+
acknowledgement. A hard restart republishes only those IDs; a same-process
|
|
4604
|
+
Runtime rebuild deduplicates the projected queue by child turn ID. Once the
|
|
4605
|
+
parent transcript commits and acknowledges the completion, later restores do
|
|
4606
|
+
not replay it. Legacy snapshots without the explicit pending set do not infer
|
|
4607
|
+
replay work from historical mailbox content.
|
|
4608
|
+
|
|
4609
|
+
This separation changes no Actor event capability or daemon version: existing
|
|
4610
|
+
SDK snapshot, replay, and long-poll clients keep their telemetry surface. It
|
|
4611
|
+
only prevents high-frequency progress from becoming a model control signal.
|
|
4343
4612
|
|
|
4344
4613
|
---
|
|
4345
4614
|
|
|
@@ -4347,5 +4616,8 @@ for the final capability-upgrade and compatibility closure.
|
|
|
4347
4616
|
|
|
4348
4617
|
- [README.md](../README.md) — end-user CLI quick start
|
|
4349
4618
|
- [docs/ADR.md ADR-024](ADR.md#adr-024-npm-发布物正名-kodax-aikodax--sdk-subpath-exports-形式化-v0739) — SDK subpath architecture rationale
|
|
4350
|
-
- [docs/ADR.md ADR-032](ADR.md#adr-032-sdk-embedder-surface-closure-feature_186-v0742) — FEATURE_186 design record (all 8 phases)
|
|
4351
|
-
- [docs/
|
|
4619
|
+
- [docs/ADR.md ADR-032](ADR.md#adr-032-sdk-embedder-surface-closure-feature_186-v0742) — FEATURE_186 design record (all 8 phases)
|
|
4620
|
+
- [docs/ADR.md ADR-057](ADR.md#adr-057-large-compaction-is-an-always-on-context-scoped-full-coverage-transaction) — v0.7.74 compaction and exact-history ownership
|
|
4621
|
+
- [docs/ADR.md ADR-058](ADR.md#adr-058-model-agent-wait-is-mailbox-control-not-event-telemetry) — mailbox control versus Actor telemetry
|
|
4622
|
+
- [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
|
|
4623
|
+
- [docs/features/v0.7.74.md](features/v0.7.74.md) — v0.7.74 release-candidate design and verification record
|