@north-light/crouter 0.3.320 → 0.3.322

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/bin/runtime-selector.mjs +52 -3
  2. package/dist/api/client.d.ts +31 -17
  3. package/dist/api/client.js +2 -2
  4. package/dist/api/dto/broker-ops.d.ts +0 -1
  5. package/dist/api/dto/modelauth.d.ts +15 -0
  6. package/dist/api/dto/node-outcomes.d.ts +6 -0
  7. package/dist/api/dto/nodes.d.ts +4 -3
  8. package/dist/api/dto/review-comments.d.ts +8 -0
  9. package/dist/api/errors.d.ts +6 -3
  10. package/dist/api/errors.js +1 -1
  11. package/dist/api/index.d.ts +2 -2
  12. package/dist/api/index.js +1 -1
  13. package/dist/api/node-transport.d.ts +18 -0
  14. package/dist/api/node-transport.js +1 -0
  15. package/dist/api/routes.d.ts +2 -0
  16. package/dist/api/routes.js +1 -1
  17. package/dist/builtin-memory/crouter-sdk.md +86 -0
  18. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +1 -1
  19. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +1 -1
  20. package/dist/cli.js +8 -2
  21. package/dist/clients/attach/render/chat-view.d.ts +5 -1
  22. package/dist/clients/attach/render/chat-view.js +1 -1
  23. package/dist/clients/attach/session/frames.d.ts +1 -0
  24. package/dist/clients/attach/session/frames.js +1 -1
  25. package/dist/clients/attach/viewer.js +606 -606
  26. package/dist/clients/inbox/review/comments-client.d.ts +8 -1
  27. package/dist/clients/inbox/review/comments-client.js +1 -1
  28. package/dist/clients/inbox/review/companion-pane.d.ts +17 -0
  29. package/dist/clients/inbox/review/companion-pane.js +1 -1
  30. package/dist/clients/inbox/review/document-surface.d.ts +10 -0
  31. package/dist/clients/inbox/review/document-surface.js +2 -2
  32. package/dist/clients/inbox/review/frame.js +1 -1
  33. package/dist/clients/inbox/review/keys.d.ts +2 -0
  34. package/dist/clients/inbox/review/keys.js +1 -1
  35. package/dist/commands/api-client.js +3 -3
  36. package/dist/commands/human/prompts.js +1 -1
  37. package/dist/commands/memory/read.js +2 -2
  38. package/dist/commands/sys/branch.js +1 -1
  39. package/dist/commands/sys/connect.d.ts +1 -0
  40. package/dist/commands/sys/connect.js +3 -0
  41. package/dist/commands/sys/daemon.js +1 -1
  42. package/dist/commands/sys/panels/provider-panel.js +1 -1
  43. package/dist/commands/sys/provider-login.d.ts +8 -0
  44. package/dist/commands/sys/provider-login.js +9 -0
  45. package/dist/commands/sys/setup-core.d.ts +4 -0
  46. package/dist/commands/sys/setup-core.js +4 -4
  47. package/dist/commands/sys.js +1 -1
  48. package/dist/core/canvas/browse/app.js +3 -3
  49. package/dist/core/canvas/canvas.d.ts +3 -3
  50. package/dist/core/canvas/canvas.js +17 -14
  51. package/dist/core/canvas/migrations.js +34 -27
  52. package/dist/core/canvas/node-sql-migration.d.ts +1 -0
  53. package/dist/core/canvas/node-sql-migration.js +28 -2
  54. package/dist/core/canvas/paths.d.ts +8 -4
  55. package/dist/core/canvas/paths.js +1 -1
  56. package/dist/core/canvas/recovery.d.ts +1 -0
  57. package/dist/core/canvas/recovery.js +1 -1
  58. package/dist/core/canvas/worktrees.d.ts +1 -0
  59. package/dist/core/canvas/worktrees.js +1 -1
  60. package/dist/core/config.js +1 -1
  61. package/dist/core/human/requests.d.ts +1 -0
  62. package/dist/core/human/requests.js +17 -13
  63. package/dist/core/review/fork.d.ts +8 -0
  64. package/dist/core/review/fork.js +5 -0
  65. package/dist/core/runtime/broker/daemon-ops.js +1 -1
  66. package/dist/core/runtime/broker/extension-abort.d.ts +3 -0
  67. package/dist/core/runtime/broker/extension-abort.js +1 -1
  68. package/dist/core/runtime/broker.js +1 -1
  69. package/dist/core/runtime/spawn.d.ts +5 -1
  70. package/dist/core/runtime/spawn.js +2 -2
  71. package/dist/core/runtime/stop-guard.d.ts +1 -3
  72. package/dist/core/runtime/stop-guard.js +2 -2
  73. package/dist/core/runtime/stop-signals.d.ts +0 -1
  74. package/dist/core/runtime/stop-signals.js +1 -1
  75. package/dist/core/secrets.d.ts +3 -0
  76. package/dist/core/secrets.js +2 -2
  77. package/dist/core/subscription-state.d.ts +1 -1
  78. package/dist/core/subscription-state.js +3 -3
  79. package/dist/core/termrender/termrender.d.ts +3 -1
  80. package/dist/core/termrender/termrender.js +13 -13
  81. package/dist/core/user-settings.d.ts +4 -0
  82. package/dist/core/user-settings.js +1 -1
  83. package/dist/daemon/api/handlers/broker-ops.js +1 -1
  84. package/dist/daemon/api/handlers/health.d.ts +1 -1
  85. package/dist/daemon/api/handlers/health.js +14 -1
  86. package/dist/daemon/api/handlers/inbox.js +1 -1
  87. package/dist/daemon/api/handlers/modelauth.js +1 -1
  88. package/dist/daemon/api/handlers/review-comments.js +1 -1
  89. package/dist/daemon/api/map.d.ts +4 -2
  90. package/dist/daemon/api/map.js +2 -2
  91. package/dist/daemon/api/server.d.ts +5 -1
  92. package/dist/daemon/api/server.js +4 -4
  93. package/dist/daemon/companion-retire.d.ts +3 -1
  94. package/dist/daemon/companion-retire.js +1 -1
  95. package/dist/daemon/crtrd-cli.js +1 -1
  96. package/dist/daemon/crtrd.js +7 -7
  97. package/dist/daemon/human/finish.d.ts +4 -1
  98. package/dist/daemon/human/finish.js +7 -7
  99. package/dist/daemon/human/settle.d.ts +8 -6
  100. package/dist/daemon/human/settle.js +1 -1
  101. package/dist/daemon/manage.d.ts +1 -0
  102. package/dist/daemon/manage.js +2 -2
  103. package/dist/daemon/reconcilers/node-lifecycle/tick.js +1 -1
  104. package/dist/pi-extensions/canvas-inbox-watcher.js +1 -1
  105. package/dist/pi-extensions/canvas-stophook.js +1 -1
  106. package/dist/types.d.ts +7 -0
  107. package/dist/types.js +1 -1
  108. package/docs/sdk/README.md +54 -0
  109. package/docs/sdk/client.md +92 -0
  110. package/docs/sdk/docker.md +63 -0
  111. package/docs/sdk/errors.md +85 -0
  112. package/docs/sdk/getting-started.md +160 -0
  113. package/docs/sdk/memory.md +107 -0
  114. package/docs/sdk/migration.md +108 -0
  115. package/docs/sdk/nodes.md +193 -0
  116. package/docs/sdk/resources.md +71 -0
  117. package/docs/sdk/streaming.md +124 -0
  118. package/package.json +1 -1
  119. package/runtime.lock.json +6 -6
@@ -0,0 +1,160 @@
1
+ # Getting started
2
+
3
+ Phase 1.
4
+
5
+ An agent run needs a crouter daemon (`crtrd`) to run it. The SDK is a client for that daemon — it never runs an agent itself. There are two ways to reach one: the unix socket on the machine you are running on, or a TCP listener with a bearer token.
6
+
7
+ ## 1. Install the runtime
8
+
9
+ ```bash
10
+ npm i -g @north-light/crouter
11
+ ```
12
+
13
+ That installs the `crtr` CLI and the `crtrd` daemon. You do not have to start the daemon: `new Crouter()` starts it for you on a cold socket (see `autostart` in [Client construction](./client.md)).
14
+
15
+ ## 2. Install the SDK in your application
16
+
17
+ ```bash
18
+ npm i @north-light/crouter-sdk
19
+ ```
20
+
21
+ One dependency. It re-exports every data type you need, so you do not also install `@north-light/crouter-api`.
22
+
23
+ ## 3. Node, on the owner's own daemon
24
+
25
+ Pass nothing. The client resolves the same socket path the `crtr` CLI does, and starts the daemon if it is not already up.
26
+
27
+ ```ts
28
+ import Crouter from '@north-light/crouter-sdk';
29
+
30
+ const client = new Crouter();
31
+
32
+ const node = await client.nodes.create({
33
+ prompt: 'List the top-level directories here and say what each one is for.',
34
+ cwd: process.cwd(),
35
+ root: true,
36
+ root_lifecycle: 'terminal',
37
+ });
38
+
39
+ const outcome = await client.nodes.waitForOutcome(node.node_id);
40
+ if (outcome.kind === 'result') console.log(outcome.final_report_path);
41
+ ```
42
+
43
+ `root: true` says this run has no parent node and reports to nobody — which is what an application's run is. `root_lifecycle: 'terminal'` says it finishes and reaps; use `'resident'` for a run a person will open and keep talking to.
44
+
45
+ ## 4. A browser or a remote application
46
+
47
+ A process that is not on the daemon's machine — or a page in a browser, which has no unix sockets at all — reaches the daemon over TCP with a bearer token. The token is the owner credential: whoever holds it can drive the whole surface.
48
+
49
+ ### Turn the listener on, once
50
+
51
+ On the machine running the daemon:
52
+
53
+ ```bash
54
+ crtr sys connect
55
+ ```
56
+
57
+ This stores a listener address and generates a token if either is missing, then prints the base URL and token before handing the daemon over to a successor that boots with the listener on. It then logs in to the selected model provider when that provider has no usable credential. Run it again and, when the listener is up and that provider is ready, it prints the credentials and changes nothing.
58
+
59
+ The command returns `base_url` and `token`. Use JSON output when a program or a setup UI needs those exact fields:
60
+
61
+ ```text
62
+ $ crtr --json sys connect
63
+ {"base_url":"http://127.0.0.1:8787","token":"<64-character bearer token>"}
64
+ ```
65
+
66
+ ### Check setup before generating
67
+
68
+ Construct the client from the application's saved connection. On the first visit that value is absent, and `client.auth.status()` returns `'connect'` without a request. Once the user pastes the base URL and token printed by `crtr sys connect`, construct it again and call `status()` to check the selected provider.
69
+
70
+ ```ts
71
+ import Crouter from '@north-light/crouter-sdk';
72
+
73
+ const saved = loadConnection(); // { baseURL: string; token: string } | null
74
+ const client = new Crouter(saved ?? {});
75
+ const status = await client.auth.status({ profile: 'my-app', cwd: '/path/to/repo' });
76
+
77
+ if (status.next_step !== null) {
78
+ showSetupPanel({ step: status.next_step, instructions: status.instructions! });
79
+ return;
80
+ }
81
+
82
+ const run = await client.nodes.createAndWait({
83
+ prompt: 'What changed in this repo today?',
84
+ cwd: '/path/to/repo',
85
+ profile: 'my-app',
86
+ root: true,
87
+ });
88
+ ```
89
+
90
+ `status()` returns `next_step: 'connect'` when the application has no daemon transport, when the saved bearer token is rejected, or when the saved base URL cannot be reached. It returns `next_step: 'login'` when the daemon resolves the selected run to a provider without a ready credential. In both cases, display `instructions`: it names the exact `crtr sys connect` action and, for a login, the provider. When `status()` receives `profile`, `cwd`, `kind`, or `model`, the login instruction passes those selectors to `crtr sys connect`; use the text unchanged. A daemon that is still starting is connected with `next_step: null`; inspect `status.daemon.startup_phase` and wait for it to become `ready` before starting a run.
91
+
92
+ An isolated auth-status probe returned:
93
+
94
+ ```json
95
+ {
96
+ "no_request": {
97
+ "next_step": "connect",
98
+ "requests": 0
99
+ },
100
+ "missing": {
101
+ "provider": "anthropic",
102
+ "credential": "missing",
103
+ "next_step": "login",
104
+ "instructions": "Run `crtr sys connect --cwd /private/tmp/crouter-sdk-auth-proof.P3fSWg/empty --model anthropic/claude-opus-5` to log in to anthropic."
105
+ },
106
+ "ready": {
107
+ "provider": "anthropic",
108
+ "credential": "ready",
109
+ "next_step": null
110
+ }
111
+ }
112
+ ```
113
+
114
+ The root entry of the SDK is browser-safe: it imports nothing from `node:*`. The unix-socket code is reached only through a dynamic import taken when `socketPath` is set, so a browser bundle never resolves it.
115
+
116
+ ### Cross-origin calls
117
+
118
+ The daemon answers the browser's `OPTIONS` preflight with `Access-Control-Allow-Origin: *` and allows the `authorization` and `content-type` headers, and every authorized response carries the same origin header. This happens **only when a token is set** — an unauthenticated listener stays same-origin, because otherwise any page the user visits could drive their daemon.
119
+
120
+ The credential is a header your application already holds, never a cookie, so the daemon sends no `Access-Control-Allow-Credentials`.
121
+
122
+ ### Chrome's local-network permission
123
+
124
+ A page served over HTTPS that calls `http://localhost:<port>` is governed by Chrome's Local Network Access permission. The browser prompts the user once per site; the daemon needs **no special response header** for it — ordinary CORS plus a secure context is the whole requirement. See [developer.chrome.com/blog/local-network-access](https://developer.chrome.com/blog/local-network-access).
125
+
126
+ Tell your users what the prompt is for. A prompt that appears with no explanation gets dismissed, and the dismissal is sticky.
127
+
128
+ ## 5. A typed result
129
+
130
+ `parse()` creates the run, waits for it to settle, and types the structured result against the schema you gave it.
131
+
132
+ ```ts
133
+ import { z } from 'zod';
134
+
135
+ const run = await client.nodes.parse({
136
+ prompt: 'Read package.json here and report its name and version.',
137
+ cwd: '/path/to/repo',
138
+ output_schema: z.object({ name: z.string(), version: z.string() }),
139
+ });
140
+
141
+ if (run.kind === 'result') {
142
+ console.log(run.output_parsed.name, run.output_parsed.version);
143
+ } else if (run.reason === 'declined') {
144
+ console.warn('the agent refused the schema:', run.declined?.reason);
145
+ } else {
146
+ console.error('the run failed:', run.reason, run.detail);
147
+ }
148
+ ```
149
+
150
+ `output_parsed` is non-null on a result. A Zod schema supplies its inferred output type; a Standard Schema supplies `~standard.types.output`. A JSON-Schema literal or any other `{ toJSONSchema() }` object is accepted but makes `output_parsed` `unknown`.
151
+
152
+ An outcome is returned, never thrown — including a decline and a failure. Only transport faults, daemon errors, and your own abort throw. See [Errors](./errors.md).
153
+
154
+ ## Where to go next
155
+
156
+ - Every constructor option and environment-variable fallback: [Client construction](./client.md)
157
+ - Checking the daemon connection and selected provider before a run: `client.auth.status()` above
158
+ - The full create-parameter table and the outcome union: [Nodes](./nodes.md)
159
+ - Watching a run as it works: [Streaming](./streaming.md) (phase 2)
160
+ - Running the daemon in a container instead: [Docker environment](./docker.md)
@@ -0,0 +1,107 @@
1
+ > **Phase 3 — not shipped.** Only `GET /v1/memory/resolve` exists on the daemon today. Every other route on this page is new work, blocked on extracting a memory service the CLI leaves currently hold inside their own `run()` functions. `client.memory` is not on the client yet.
2
+
3
+ # `client.memory`
4
+
5
+ Read and write the memory documents an agent run consults — the knowledge and preference documents that shape how a node behaves.
6
+
7
+ ## Scope: every request names its target
8
+
9
+ A memory document's identity depends on where it is being read from. The CLI fills that in from its own process — its working directory, its profile, its node. **A daemon route must never do that**, because the daemon's own working directory is meaningless to a caller and would leak one caller's stores into another's read. So every memory call takes an explicit scope.
10
+
11
+ ```ts
12
+ type MemoryScope = {
13
+ node?: string; // node id — the daemon derives cwd and profile from the node row
14
+ cwd?: string; // project stores discovered by walking up from here
15
+ profile?: string; // profile store
16
+ store?: 'node' | 'project' | 'profile' | 'user' | 'builtin'; // restrict to one tier
17
+ };
18
+ ```
19
+
20
+ `node` is the convenient form: give a node id and the daemon reads that node's working directory and profile from canvas state.
21
+
22
+ **With no scope at all, only the user-global and builtin stores are in view.** That is deliberate, not a default to lean on — pass the scope you mean.
23
+
24
+ Stores resolve in precedence order: node → project (nearest first) → profile → user → builtin.
25
+
26
+ ## Methods
27
+
28
+ | Method | Route | What it does |
29
+ |---|---|---|
30
+ | `memory.list(scope & { kind?, limit?, after? })` | `GET /v1/memory/docs` | A page of document summaries. |
31
+ | `memory.retrieve(name, scope & { frontmatter? })` | `GET /v1/memory/docs/{name}` | One document with its body. |
32
+ | `memory.create(params)` | `POST /v1/memory/docs` | `name`, `kind`, `when_and_why_to_read`, `body`, optional `frontmatter`/`extensions`, plus scope. |
33
+ | `memory.update(name, params)` | `PATCH /v1/memory/docs/{name}` | `rationale` is **required**. |
34
+ | `memory.delete(name, scope)` | `DELETE /v1/memory/docs/{name}` | Leaves a tombstone. |
35
+ | `memory.move(name, { to, ...scope })` | `POST /v1/memory/docs/{name}/move` | Renames and rewrites inbound links. |
36
+ | `memory.search(params)` | `POST /v1/memory/search` | A page of hits; `grep` mode returns line matches instead of scored documents. |
37
+ | `memory.history(name, scope & { limit?, revision?, diff? })` | `GET /v1/memory/docs/{name}/history` | Revisions with their rationales. |
38
+ | `memory.resolve(name, scope)` | `GET /v1/memory/resolve` | Which document a name resolves to for that target. The one route that exists today. |
39
+
40
+ <!-- TODO(verify): run against a live daemon once phase 3 lands; confirm argument order and the exact parameter names on each method. -->
41
+
42
+ ```ts
43
+ const page = await client.memory.list({ node: nodeId, kind: 'knowledge', limit: 50 });
44
+ const doc = await client.memory.retrieve('insights/capture', { node: nodeId, frontmatter: true });
45
+
46
+ await client.memory.create({
47
+ name: 'billing/refund-policy',
48
+ kind: 'knowledge',
49
+ when_and_why_to_read: 'When handling a refund request, this should be read because the eligibility window is not derivable from the order record.',
50
+ body: '…',
51
+ profile: 'my-app',
52
+ });
53
+
54
+ await client.memory.update('billing/refund-policy', {
55
+ rationale: 'The window moved from 30 to 60 days on 2026-01-04.',
56
+ body: '…',
57
+ profile: 'my-app',
58
+ });
59
+ ```
60
+
61
+ A document name contains slashes (`insights/capture`) and is percent-encoded into a single path segment on the wire. The SDK does that for you.
62
+
63
+ ## What the routes enforce
64
+
65
+ These invariants come from the CLI's behaviour and are preserved exactly:
66
+
67
+ - `rationale` is required on every update and is stored **only in the history log**, never in the document text.
68
+ - A no-op edit is refused.
69
+ - `kind`, `when-and-why-to-read`, `origin`, and `last-updated` cannot be set directly through `frontmatter`.
70
+ - Builtin and plugin documents are read-only.
71
+ - The root project namespace is immutable.
72
+ - A canonical-name collision is checked against both `<local>.md` and `<local>/INDEX.md`.
73
+ - Every mutation appends a history record with the full before and after.
74
+ - A delete leaves a tombstone.
75
+
76
+ Lint runs per document on write and its findings ride back in the response. There is no corpus-lint route — `crtr memory lint` writes to stderr and sets an exit code, which is CLI behaviour with no SDK equivalent.
77
+
78
+ ## What the routes will not do
79
+
80
+ **The daemon does not execute shell blocks.** `crtr memory read` expands embedded shell in a document; the route returns the document source **unexpanded**. A daemon running shell from document content on behalf of an HTTP caller is a code-execution path with no caller-visible boundary. Expansion stays in the CLI.
81
+
82
+ **Gate predicates are not a security boundary.** When you pass `node`, the routes apply the document's gate — which decides what a given persona sees — and skip it when you do not. Access control is the credential you hold, not the gate.
83
+
84
+ **Concurrent writes are last-write-wins.** Two callers mutating the same document produce one file and two history records. Multi-file moves with link rewriting are not atomic. This is also what two CLI processes do today; there is no lock.
85
+
86
+ ## Pagination
87
+
88
+ `memory.list` and `memory.search` are the only routes in the SDK with a page envelope, because they are the only ones with a real cursor behind them. Documents are ordered by canonical name, which is stable, so the cursor is the last item's `name`.
89
+
90
+ <!-- TODO(verify): run against a live daemon once phase 3 lands; confirm the page object's method names. -->
91
+
92
+ ```ts
93
+ const page = await client.memory.search({ query: 'refund window', node: nodeId });
94
+
95
+ for (const hit of page.data) console.log(hit.name, hit.score);
96
+
97
+ if (page.hasNextPage()) {
98
+ const next = await page.getNextPage();
99
+ }
100
+
101
+ // or iterate across page boundaries
102
+ for await (const hit of client.memory.search({ query: 'refund window', node: nodeId })) {
103
+ console.log(hit.name);
104
+ }
105
+ ```
106
+
107
+ The envelope is `{ object: 'list', data, has_more }`. There is no `first_id` or `last_id` — the cursor is on the item.
@@ -0,0 +1,108 @@
1
+ # Migration from `generate()` and `local()`
2
+
3
+ Phase 1.
4
+
5
+ **This is a hard cut.** `generate()`, `local()`, and the `Environment` interface are deleted in the same release that adds `Crouter`. There is no coexistence period, no deprecated re-export, and no compatibility shim. Both packages are pre-1.0 and the callers are countable.
6
+
7
+ Pin your old version or migrate; there is no third option.
8
+
9
+ ## Before and after
10
+
11
+ ```text
12
+ // before
13
+ import { generate, local } from '@north-light/crouter-sdk';
14
+ import { z } from 'zod';
15
+
16
+ const schema = z.object({ name: z.string(), version: z.string() });
17
+
18
+ const result = await generate({
19
+ prompt: 'Read package.json and report its name and version.',
20
+ schema,
21
+ env: local(),
22
+ cwd: '/path/to/repo',
23
+ profile: 'my-app',
24
+ kind: 'general',
25
+ model: 'anthropic/strong',
26
+ deadline: '10m',
27
+ signal,
28
+ });
29
+
30
+ if (result.kind === 'result') console.log(result.value.name);
31
+ else if (result.kind === 'declined') console.warn(result.reason);
32
+ else console.error(result.reason, result.detail);
33
+ ```
34
+
35
+ ```ts
36
+ // after
37
+ import Crouter from '@north-light/crouter-sdk';
38
+ import { z } from 'zod';
39
+
40
+ const client = new Crouter();
41
+
42
+ const run = await client.nodes.parse(
43
+ {
44
+ prompt: 'Read package.json and report its name and version.',
45
+ output_schema: z.object({ name: z.string(), version: z.string() }),
46
+ cwd: '/path/to/repo',
47
+ profile: 'my-app',
48
+ kind: 'general',
49
+ model: 'anthropic/strong',
50
+ deadline: '10m',
51
+ },
52
+ { signal },
53
+ );
54
+
55
+ if (run.kind === 'result') console.log(run.output_parsed.name);
56
+ else if (run.reason === 'declined') console.warn(run.declined?.reason);
57
+ else console.error(run.reason, run.detail);
58
+ ```
59
+
60
+ ## What moved where
61
+
62
+ | Before | After |
63
+ |---|---|
64
+ | `generate({ … })` | `client.nodes.parse({ … })` |
65
+ | `local()` / `env: local()` | `new Crouter()` — the client is the connection |
66
+ | `local({ autostart: false })` | `new Crouter({ autostart: false })` |
67
+ | `env.daemon()` + `new CrtrClient(…)` | `new Crouter(env.connection())` — see [Docker environment](./docker.md) |
68
+ | `client.ensureProfile('x')` | `client.profiles.ensure('x')` |
69
+ | `schema` | `output_schema` |
70
+ | `signal` in the params object | `signal` in the second argument, the per-request options |
71
+ | `onEvent` | `client.nodes.stream()` — [Streaming](./streaming.md), phase 2 |
72
+ | the `Environment` interface | deleted; nothing implements it |
73
+
74
+ ## The result union changed
75
+
76
+ `generate()` returned a three-way union of its own invention. `parse()` returns the settled `NodeOutcome` the daemon already defines — the same union the wire has, not a translation of it.
77
+
78
+ | `generate()` | `parse()` |
79
+ |---|---|
80
+ | `{ kind: 'result', value, nodeId, reportPath }` | `{ kind: 'result', output_parsed, structured_result, final_report_path, … }` |
81
+ | `{ kind: 'declined', reason, nodeId }` | `{ kind: 'failure', reason: 'declined', declined: { reason, code, retryable } \| null, … }` |
82
+ | `{ kind: 'failure', reason, detail, nodeId }` | `{ kind: 'failure', reason, detail, … }` |
83
+
84
+ A decline is now a `failure` with `reason: 'declined'`, so narrow on `run.reason === 'declined'` rather than `run.kind === 'declined'`. It carries more than the old shape did: the agent's own code and a `retryable` flag.
85
+
86
+ `value` became `output_parsed`, and `reportPath` became `final_report_path`.
87
+
88
+ Every `NodeOutcomeDTO` carries the wire field `node_id`.
89
+
90
+ ## `signal` actually cancels now
91
+
92
+ `generate()` raced an abort against a promise it could not cancel. `parse()` passes the signal to `fetch`, so aborting stops the in-flight request and raises `APIUserAbortError`.
93
+
94
+ Aborting still stops **your client waiting**, not the run. Call `client.nodes.cancel(id)` to stop the agent, or give it a `deadline` so the daemon stops it for you.
95
+
96
+ ## Progress reporting
97
+
98
+ `onEvent` emitted three notifications: node created, report pushed, settled. Two of its three are available in phase 1 without it:
99
+
100
+ - The node is created when `client.nodes.create()` returns — you hold the node.
101
+ - Reports are available from `client.nodes.reports.list(id)`.
102
+ - Settlement is the return of `waitForOutcome`.
103
+
104
+ Streaming (phase 2) is strictly more capable than `onEvent` was: the three events it emitted are three of the events the stream carries, alongside assistant text deltas and tool calls.
105
+
106
+ ## `CrtrClient` is no longer yours to construct
107
+
108
+ The SDK does not re-export `CrtrClient` — the raw client is an implementation detail of `Crouter`. If you were constructing one to reach a route the SDK does not wrap, use `client.request()` instead; see [Resource map](./resources.md#the-escape-hatch).
@@ -0,0 +1,193 @@
1
+ # `client.nodes`
2
+
3
+ Phase 1, except where a row says otherwise.
4
+
5
+ A node is one agent run on the crouter canvas. It is asynchronous by construction: `create` returns as soon as the daemon has spawned it, and the run keeps working after your call returns.
6
+
7
+ There is one name for it — `client.nodes`. There is no `client.responses` alias. A node has a canvas identity, a kind, a working directory, a profile, a deadline, reports, subscriptions, children, and a lifecycle; naming it `responses` would promise `previous_response_id`, `output_text`, `tools`, and `store` semantics that do not exist here.
8
+
9
+ ## Methods
10
+
11
+ | Method | Route | Phase | Notes |
12
+ |---|---|---|---|
13
+ | `nodes.create(params)` | `POST /v1/nodes` | 1 | Returns immediately with the node; it is already running. |
14
+ | `nodes.retrieve(id)` | `GET /v1/nodes/{id}` | 1 | |
15
+ | `nodes.list(query?)` | `GET /v1/nodes` | 1 | Returns an array, as the daemon does. |
16
+ | `nodes.outcome(id, { wait })` | `GET /v1/nodes/{id}/outcome?wait=` | 1 | One long poll, `wait` 0–25 s. Returns the wire envelope with `state: 'pending' | 'settled'`. |
17
+ | `nodes.waitForOutcome(id, opts?)` | repeats the above | 1 | Loops until settled. Honours `signal`; no implicit time bound. |
18
+ | `nodes.createAndWait(params, opts?)` | create + wait | 1 | |
19
+ | `nodes.parse(params, opts?)` | create + wait + typed result | 1 | [See below](#parse-and-structured-output). |
20
+ | `nodes.message(id, body)` | `POST /v1/nodes/{id}/messages` | 1 | Send a follow-up to a running or dormant node. |
21
+ | `nodes.interrupt(id)` | `POST /v1/nodes/{id}/interrupt` | 1 | Stop the current turn, keep the node. |
22
+ | `nodes.cancel(id, body?)` | `POST /v1/nodes/{id}/close` | 1 | Tears the node and its exclusive subtree down. `cancel` is the method name; `close` is the route. |
23
+ | `nodes.update(id, patch)` | `PATCH /v1/nodes/{id}/config` | 2 | |
24
+ | `nodes.fork(id)` | `POST /v1/nodes/{id}/fork` | 2 | |
25
+ | `nodes.revive(id, body?)` | `POST /v1/nodes/{id}/revive` | 2 | |
26
+ | `nodes.promote(id, body?)` / `nodes.demote(id, body?)` | matching routes | 2 | Move a node between base and orchestrator mode. |
27
+ | `nodes.recycle(id, body?)` | `POST /v1/nodes/{id}/recycle` | 2 | |
28
+ | `nodes.yield(id, body?)` | `POST /v1/nodes/{id}/yield` | 2 | |
29
+ | `nodes.wait(id, body?)` | `POST /v1/nodes/{id}/wait` | 2 | |
30
+ | `nodes.relaunchRoot(id, body?)` | matching route | 2 | |
31
+ | `nodes.reviveAll(body?)` | `POST /v1/nodes/revive-all` | 2 | |
32
+ | `nodes.stream(params)` | create + `GET /…/events` | 2 | [Streaming](./streaming.md). |
33
+ | `nodes.events(id, opts?)` | `GET /v1/nodes/{id}/events` | 2 | [Streaming](./streaming.md). |
34
+
35
+ Action methods keep the product's literal name (`fork`, `revive`, `promote`, `yield`) rather than being renamed into a generic verb.
36
+
37
+ ## Create parameters
38
+
39
+ Wire fields are `snake_case`. Every `NodeCreateParams` property is optional; `parse()` additionally requires `output_schema`. `NodeCreateParams` is the daemon's `CreateNodeRequest`, except `output_schema` also accepts an object. When neither `parent` nor `root` is supplied, the SDK sends `root: true`; otherwise fields pass through without camelizing.
40
+
41
+ | Field | Type | Meaning |
42
+ |---|---|---|
43
+ | `prompt` | `string` | The run's entire brief. |
44
+ | `kind` | `string` | Persona. Omit for the profile's default. |
45
+ | `model` | `string` | Durable model override. |
46
+ | `profile` | `string` | The store, memory, and purview the run uses. An application passes **its own** profile here. |
47
+ | `cwd` | `string` | Where the request came from. |
48
+ | `pin_cwd` | `string` | Pin the node to this directory regardless of `cwd`. |
49
+ | `situational_context` | `string` | Ambient context kept out of the visible prompt. |
50
+ | `deadline` | `string` (`1h30m`) | Wall clock from spawn. Expiry cancels the node and records `deadline_exceeded`. |
51
+ | `output_schema` | `string \| JsonSchema \| { toJSONSchema(): JsonSchema }` | Widened from the wire's JSON string; the SDK serializes. A zod v4 object satisfies the third form. |
52
+ | `root` | `boolean` | No parent, no subscription. An application's run is a root. |
53
+ | `root_lifecycle` | `'terminal' \| 'resident'` | `terminal` for a bounded run; `resident` for one a person will open and keep. |
54
+ | `mode` | `'base' \| 'orchestrator'` | Whether the node works hands-on or fans out to children. |
55
+ | `name` | `string` | Display label. |
56
+ | `description` | `string` | Display description. |
57
+ | `parent` | node id | Graph placement. An external caller leaves this unset. |
58
+ | `creator` | node id | Graph placement. An external caller leaves this unset. |
59
+ | `worktree` | `string \| boolean` | Create a managed git worktree for the run. |
60
+ | `fork_from` | `string` | Start from an existing conversation. |
61
+ | `no_kickoff` | `boolean` | Create the node without sending the first message. |
62
+ | `node_id` | `string` | Spawn at an exact id. A collision answers `409 node_id_exists`. |
63
+ | `prefer_warm` | `boolean` | Serve from the warm pool when the launch tuple matches. |
64
+ | `outcome_delivery` | `{ action, payload? }` | Arm outcome delivery at birth. |
65
+
66
+ `node_id` is how you make a create safely retryable. Retry with the same id and a duplicate fails loudly with `409 node_id_exists` instead of quietly spawning a second agent — which is why the client never retries a `POST` for you. See [Errors](./errors.md).
67
+
68
+ ## Outcomes
69
+
70
+ `waitForOutcome`, `createAndWait`, and `parse` return the settled `NodeOutcome` — the same union the API defines, not a translation of it.
71
+
72
+ | Wire | Narrow on | Carries |
73
+ |---|---|---|
74
+ | `kind: 'result'` | `outcome.kind === 'result'` | `structured_result`, `final_report_path`, and on `parse` a typed `output_parsed` |
75
+ | `kind: 'failure'`, `reason: 'declined'` | `outcome.reason === 'declined'` | `declined: { reason, code, retryable } \| null` — the agent honestly refused the schema |
76
+ | `kind: 'failure'`, any other `reason` | anything else | `detail: NodeOutcomeDetailV1 \| null` — `deadline_exceeded`, a provider fault, a wedge |
77
+
78
+ **Agent-side outcomes are returned, never thrown.** A decline carries a typed reason, a code the agent chose, and a `retryable` flag; routing that through an exception would discard the payload and make the happy path lie about what happened. Only transport faults, daemon errors, and your own abort throw.
79
+
80
+ ```ts
81
+ const outcome = await client.nodes.waitForOutcome(node.node_id, { signal });
82
+
83
+ switch (true) {
84
+ case outcome.kind === 'result':
85
+ console.log(outcome.structured_result, outcome.final_report_path);
86
+ break;
87
+ case outcome.reason === 'declined':
88
+ console.warn(outcome.declined?.reason, outcome.declined?.retryable);
89
+ break;
90
+ default:
91
+ console.error(outcome.reason, outcome.detail);
92
+ }
93
+ ```
94
+
95
+ ### `outcome()` versus `waitForOutcome()`
96
+
97
+ `nodes.outcome(id, { wait })` is one long poll: the daemon holds the request open for up to `wait` seconds (0–25) and answers `{ node_id, state: 'pending' | 'settled', outcome, node_status, deadline_at }`. `outcome` is `null` while `state` is `'pending'`. Use it when your own loop owns the timing — a job runner that wants to do other work between polls, or a UI that shows a heartbeat.
98
+
99
+ `nodes.waitForOutcome(id, opts?)` repeats that poll until the node settles. It applies **no implicit time bound**: an agent that runs for an hour is polled for an hour. Bound it from either side — pass a `deadline` to `create` so the daemon cancels the run, or pass a `signal` so your client stops waiting.
100
+
101
+ ```ts
102
+ for (;;) {
103
+ const poll = await client.nodes.outcome(node.node_id, { wait: 25 });
104
+ if (poll.state === 'settled' && poll.outcome !== null) {
105
+ console.log(poll.outcome);
106
+ break;
107
+ }
108
+ // update your UI or job heartbeat here
109
+ }
110
+ ```
111
+
112
+ ## `parse()` and structured output
113
+
114
+ ```ts
115
+ import { z } from 'zod';
116
+
117
+ const run = await client.nodes.parse({
118
+ prompt: 'Summarize the failing tests in this repo.',
119
+ cwd: '/path/to/repo',
120
+ output_schema: z.object({ failures: z.array(z.string()), root_cause: z.string() }),
121
+ });
122
+
123
+ if (run.kind === 'result') console.log(run.output_parsed.root_cause);
124
+ else if (run.reason === 'declined') console.warn(run.declined?.reason);
125
+ ```
126
+
127
+ `ParsedOutcome<T>` is `NodeOutcome & { output_parsed: T | null }`, discriminated so `output_parsed` is non-null exactly when `kind === 'result'`.
128
+
129
+ `output_parsed` is `structured_result` typed to the schema you passed. A Zod schema uses its inferred output type; a Standard Schema uses `~standard.types.output`. A JSON-Schema literal and any other object with `toJSONSchema()` are accepted and serialized, but their `output_parsed` type is `unknown`. The SDK does not re-validate the result — the daemon already enforces the schema when the agent submits it, so a second validation pass would only produce a second set of error messages for the same rejection. There is no helper equivalent to OpenAI's `zodTextFormat`.
130
+
131
+ A candidate from Basis's real structured SDK result:
132
+
133
+ ```json
134
+ {
135
+ "tmp": "k1",
136
+ "type": "claim",
137
+ "slug": "config-memories-in-project",
138
+ "text": "All applet configuration memories belong in project memories.",
139
+ "quote": "All configuration memories should be in the project memories. We know that.",
140
+ "speaker": "Me",
141
+ "confidence": "green"
142
+ }
143
+ ```
144
+
145
+ ## Sending a follow-up
146
+
147
+ `nodes.message(id, body)` appends to a node's inbox. A dormant node wakes to read it; a running node picks it up at its next turn boundary.
148
+
149
+ ```ts
150
+ await client.nodes.message(node.node_id, { body: 'Also check the integration lane.' });
151
+ ```
152
+
153
+ ## Stopping a run
154
+
155
+ | Call | Effect |
156
+ |---|---|
157
+ | `nodes.interrupt(id)` | Stops the current turn. The node stays on the canvas and can be messaged or revived. |
158
+ | `nodes.cancel(id, body?)` | Tears the node down along with the subtree it exclusively owns. Terminal. |
159
+
160
+ Aborting a `signal` you passed to `waitForOutcome` stops **your client waiting**. It does not stop the node. Call `cancel` for that.
161
+
162
+ ## Nested resources
163
+
164
+ Nested routes become nested properties.
165
+
166
+ | Property | Methods | Phase |
167
+ |---|---|---|
168
+ | `nodes.reports` | `list` | 1 (`create` in 2) |
169
+ | `nodes.messages` | `list` | 2 |
170
+ | `nodes.artifacts` | `list` | 2 |
171
+ | `nodes.context` | `list` | 2 |
172
+ | `nodes.transcript` | `retrieve` | 2 |
173
+ | `nodes.session` | `retrieve` | 2 |
174
+ | `nodes.snapshot` | `retrieve` | 2 |
175
+ | `nodes.subject` | `retrieve` | 2 |
176
+ | `nodes.outcomeDelivery` | `create`, `retrieve`, `delete` | 2 |
177
+ | `nodes.subscriptions` | `create`, `delete` | 2 |
178
+ | `nodes.jobs` | `list`, `cancel` | 2 |
179
+ | `nodes.worktree` | `close`, `abandon` | 2 |
180
+ | `nodes.result` | `submit` | 2 — only an agent inside a run calls this |
181
+
182
+ `nodes.reports.list(id)` is how an application shows progress before streaming lands: an agent pushes a report whenever it has something to say, and the list is newest-first.
183
+
184
+ ```ts
185
+ const reports = await client.nodes.reports.list(node.node_id, { limit: 10 });
186
+ for (const report of reports) console.log(report.tier, report.body);
187
+ ```
188
+
189
+ ## Pagination
190
+
191
+ List routes return what the daemon returns. `nodes.list`, `nodes.reports.list`, and `nodes.jobs.list` return plain arrays — there is no page object, no `hasNextPage()`, and no `after` cursor on them, because the daemon has no paging substrate behind those routes and an envelope there would promise a continuation that can never happen.
192
+
193
+ The one place the page envelope exists is the memory routes. See [Memory](./memory.md).
@@ -0,0 +1,71 @@
1
+ # Resource map
2
+
3
+ Every namespace on the client, with the phase it lands in. A namespace marked phase 2 or phase 3 does not exist yet.
4
+
5
+ Namespaces are camelCase. Verbs are `create`, `retrieve`, `list`, `update`, `delete`, and `cancel`, except where the product already has a literal name for the action (`fork`, `revive`, `promote`, `pause`, `poke`) — in which case the SDK uses that name.
6
+
7
+ | Namespace | Methods | Routes | Phase |
8
+ |---|---|---|---|
9
+ | `nodes` | `create`, `retrieve`, `list`, `cancel`, `interrupt`, `message`, `outcome`, `waitForOutcome`, `createAndWait`, `parse` | `/v1/nodes…` | 1; remaining methods in 2 |
10
+ | `nodes.reports` | `list` | `/v1/nodes/{id}/reports` | 1 (`create` in 2) |
11
+ | `nodes.messages` | `list` | `/v1/nodes/{id}/messages` | 2 |
12
+ | `nodes.artifacts` | `list` | `/v1/nodes/{id}/artifacts` | 2 |
13
+ | `nodes.context` | `list` | `/v1/nodes/{id}/context` | 2 |
14
+ | `nodes.transcript` | `retrieve` | `/v1/nodes/{id}/transcript` | 2 |
15
+ | `nodes.session` | `retrieve` | `/v1/nodes/{id}/session` | 2 |
16
+ | `nodes.snapshot` | `retrieve` | `/v1/nodes/{id}/snapshot` | 2 |
17
+ | `nodes.subject` | `retrieve` | `/v1/nodes/{id}/subject` | 2 |
18
+ | `nodes.outcomeDelivery` | `create`, `retrieve`, `delete` | `/v1/nodes/{id}/outcome-delivery` | 2 |
19
+ | `nodes.subscriptions` | `create`, `delete` | `/v1/nodes/{id}/subscriptions` | 2 |
20
+ | `nodes.jobs` | `list`, `cancel` | `/v1/nodes/{id}/jobs` | 2 |
21
+ | `nodes.worktree` | `close`, `abandon` | `/v1/nodes/{id}/worktree/…` | 2 |
22
+ | `nodes.result` | `submit` | `POST /v1/nodes/{id}/result` | 2 — only an agent inside a run calls this |
23
+ | `profiles` | `ensure`, `retrieve` | `/v1/profiles…` | 1; remaining methods in 2 |
24
+ | `auth` | `status` | `GET /v1/status`, then `GET /v1/model-auth/readiness` once the daemon is ready | 1 |
25
+ | `system` | `status`, `health`, `restart`, `admit`, `migrate` | `/v1/status`, `/healthz`, `/v1/daemon/…` | 1 for `status` and `health`; 3 for the rest |
26
+ | `files` | `peek` | `/v1/files/peek` | 1 |
27
+ | `bash` | — | — | 2 — reserved; not on the client today |
28
+ | `llm` | — | — | Reserved; unbuilt |
29
+ | `providers` | — | — | Reserved; unbuilt |
30
+ | `canvas` | `attention`, `attentionCounts`, `snapshot`, `roster`, `dashboard`, `prune` | `/v1/canvas…` | 2 |
31
+ | `canvas.history` | `search`, `grep`, `read`, `stats` | `/v1/canvas/history/…` | 2 |
32
+ | `crons` | `create`, `retrieve`, `list`, `pause`, `resume`, `run`, `delete`, `poke` | `/v1/crons…` | 2 |
33
+ | `human.requests` | `create`, `retrieve`, `replace`, `respond`, `dismiss`, `cancel` | `/v1/human/requests…` | 2 |
34
+ | `human.inbox` | `list`, `retrieve`, `respond`, `progress`, `cancel`, `history`, `response` | `/v1/human/inbox…` | 2 |
35
+ | `models.credentials` | `list`, `install`, `remove` | `/v1/model-auth…` | 2 |
36
+ | `models.config` | `update` | `PUT /v1/model-config` | 2 |
37
+ | `memory` | `list`, `retrieve`, `create`, `update`, `delete`, `move`, `search`, `history`, `resolve` | `/v1/memory…` | 3 — [Memory](./memory.md) |
38
+ | `human.reviews` | `create`, `retrieve`, `list`, `submit`, `cancel`, `document` | `/v1/human/reviews…` | 3 |
39
+ | `human.reviews.comments` / `human.comments` | `create`, `list`, `retrieve`, `edit`, `resolve`, `reopen`, `delete`, `fork`, `events`, `ranges` | `/v1/human/…comments…` | 3 |
40
+ | `human.inbox.feedbackComments` | `create`, `resolve` | `/v1/human/inbox/{ticket}/feedback-comments…` | 3 |
41
+ | `models.chatInventory` | `retrieve(nodeId)`, `prospective(query)` | `/v1/nodes/{id}/chat-inventory`, `/v1/prospective-chat-inventory` | 3 |
42
+ | `worktrees` | `listQuarantined` | `/v1/worktrees/quarantined` | 3 |
43
+
44
+ ## Excluded from the typed surface
45
+
46
+ These `/v1` routes get no SDK method, with the reason. They are still reachable through `client.request()`.
47
+
48
+ | Routes | Why |
49
+ |---|---|
50
+ | `/v1/focuses…` | A focus maps a node to a tmux pane. An SDK caller has no tmux. It stays on `/v1` for the viewer. |
51
+ | `POST /v1/nodes/{id}/mail/claim`, `…/mail/acknowledge` | These are how a node's own broker takes delivery of its inbox. An external caller claiming another node's mail would consume deliveries that node then never sees. |
52
+ | `POST /v1/nodes/{id}/attach` | Returns the host-local path to a broker's viewer socket. A remote or browser caller cannot open that path, and [Streaming](./streaming.md) is the application-facing way to watch a node. |
53
+ | Broker operations, broker recovery, node faults | The broker's own control plane — session binding, settle directives, park completion, turn recording, provider-retry mutation, fault recording, model commit. They exist so the one process hosting a node can coordinate with crtrd about that node's runtime. An external caller invoking them introduces a second writer to state the broker and the daemon coordinate between themselves. |
54
+
55
+ ## The escape hatch
56
+
57
+ Nothing on `/v1` is unreachable. A route that is excluded above, or one added to the daemon after this SDK version was published, is reachable directly:
58
+
59
+ ```ts
60
+ client.request(method, path, body?, options?)
61
+ ```
62
+
63
+ It carries the same authentication, the same retry policy, and the same error mapping as every generated method — it is untyped, not unsupported.
64
+
65
+ ```ts
66
+ const focuses = await client.request<FocusDTO[]>('GET', '/v1/focuses');
67
+
68
+ await client.request('POST', `/v1/nodes/${id}/some-new-route`, { field: 'value' }, { timeout: 5_000 });
69
+ ```
70
+
71
+ If you find yourself reaching for `client.request()` for something an application genuinely needs, that route belongs in one of the tables above. Say so rather than building on the escape hatch.