@north-light/crouter 0.3.321 → 0.3.323

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 (111) hide show
  1. package/bin/progress-painter.mjs +89 -0
  2. package/bin/runtime-selector.mjs +123 -4
  3. package/dist/api/client.d.ts +35 -18
  4. package/dist/api/client.js +2 -2
  5. package/dist/api/dto/broker-ops.d.ts +9 -0
  6. package/dist/api/dto/canvas.d.ts +23 -1
  7. package/dist/api/dto/modelauth.d.ts +15 -0
  8. package/dist/api/dto/nodes.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 +3 -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/chrome/canvas-panels.js +1 -1
  22. package/dist/clients/attach/overlays/graph.d.ts +6 -5
  23. package/dist/clients/attach/overlays/graph.js +1 -1
  24. package/dist/clients/attach/render/chat-view.d.ts +5 -1
  25. package/dist/clients/attach/render/chat-view.js +1 -1
  26. package/dist/clients/attach/session/frames.d.ts +1 -0
  27. package/dist/clients/attach/session/frames.js +1 -1
  28. package/dist/clients/attach/viewer.js +576 -576
  29. package/dist/commands/api-client.js +3 -3
  30. package/dist/commands/memory/read.js +2 -2
  31. package/dist/commands/node/create.js +3 -3
  32. package/dist/commands/sys/branch.js +1 -1
  33. package/dist/commands/sys/connect.d.ts +1 -0
  34. package/dist/commands/sys/connect.js +3 -0
  35. package/dist/commands/sys/daemon.js +1 -1
  36. package/dist/commands/sys/panels/plugins-panel.js +1 -1
  37. package/dist/commands/sys/panels/provider-panel.d.ts +4 -0
  38. package/dist/commands/sys/panels/provider-panel.js +1 -1
  39. package/dist/commands/sys/provider-login.d.ts +8 -0
  40. package/dist/commands/sys/provider-login.js +9 -0
  41. package/dist/commands/sys/setup-core.d.ts +4 -0
  42. package/dist/commands/sys/setup-core.js +7 -4
  43. package/dist/commands/sys/setup-wizard.d.ts +1 -1
  44. package/dist/commands/sys/setup-wizard.js +1 -1
  45. package/dist/commands/sys.js +1 -1
  46. package/dist/core/canvas/canvas.d.ts +24 -0
  47. package/dist/core/canvas/canvas.js +29 -15
  48. package/dist/core/canvas/migrations.js +35 -32
  49. package/dist/core/canvas/nav-model.d.ts +1 -1
  50. package/dist/core/canvas/nav-model.js +1 -1
  51. package/dist/core/canvas/nav-render.d.ts +31 -25
  52. package/dist/core/canvas/nav-render.js +1 -1
  53. package/dist/core/canvas/paths.d.ts +4 -4
  54. package/dist/core/canvas/render-source.d.ts +6 -16
  55. package/dist/core/canvas/render-source.js +9 -9
  56. package/dist/core/canvas/types.d.ts +16 -0
  57. package/dist/core/config.js +1 -1
  58. package/dist/core/provider-credential.d.ts +6 -0
  59. package/dist/core/provider-credential.js +1 -0
  60. package/dist/core/runtime/boot-root.d.ts +2 -0
  61. package/dist/core/runtime/boot-root.js +1 -1
  62. package/dist/core/runtime/broker/daemon-ops.d.ts +2 -1
  63. package/dist/core/runtime/broker/daemon-ops.js +1 -1
  64. package/dist/core/runtime/front-door-login.d.ts +2 -0
  65. package/dist/core/runtime/front-door-login.js +1 -0
  66. package/dist/core/runtime/front-door.js +7 -5
  67. package/dist/core/runtime/nerd-font.d.ts +19 -1
  68. package/dist/core/runtime/nerd-font.js +2 -2
  69. package/dist/core/runtime/progress-bar.d.ts +13 -0
  70. package/dist/core/runtime/progress-bar.js +2 -0
  71. package/dist/core/runtime/provider-credential-gate.d.ts +4 -0
  72. package/dist/core/runtime/provider-credential-gate.js +2 -0
  73. package/dist/core/runtime/spawn-env.d.ts +7 -0
  74. package/dist/core/runtime/spawn-env.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/canvas.js +4 -4
  85. package/dist/daemon/api/handlers/health.d.ts +1 -1
  86. package/dist/daemon/api/handlers/health.js +3 -3
  87. package/dist/daemon/api/handlers/modelauth.js +1 -1
  88. package/dist/daemon/api/map.d.ts +15 -1
  89. package/dist/daemon/api/map.js +2 -2
  90. package/dist/daemon/api/server.d.ts +5 -1
  91. package/dist/daemon/api/server.js +4 -4
  92. package/dist/daemon/crtrd-cli.js +1 -1
  93. package/dist/daemon/fleet.js +1 -1
  94. package/dist/daemon/manage.d.ts +1 -0
  95. package/dist/daemon/manage.js +2 -2
  96. package/dist/pi-extensions/canvas-stophook.js +1 -1
  97. package/dist/types.d.ts +7 -0
  98. package/dist/types.js +1 -1
  99. package/docs/sdk/README.md +54 -0
  100. package/docs/sdk/client.md +92 -0
  101. package/docs/sdk/docker.md +63 -0
  102. package/docs/sdk/errors.md +85 -0
  103. package/docs/sdk/getting-started.md +160 -0
  104. package/docs/sdk/memory.md +107 -0
  105. package/docs/sdk/migration.md +108 -0
  106. package/docs/sdk/nodes.md +193 -0
  107. package/docs/sdk/resources.md +71 -0
  108. package/docs/sdk/streaming.md +124 -0
  109. package/package.json +1 -1
  110. package/runtime.lock.json +6 -6
  111. package/scripts/install-runtime.mjs +7 -3
@@ -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.
@@ -0,0 +1,124 @@
1
+ > **Phase 2 — not shipped.** Nothing on this page exists yet. There is no HTTP streaming route on the daemon today, and `client.nodes.stream` and `client.nodes.events` are not on the client. Until phase 2 lands, an application shows progress with `client.nodes.reports.list` and gets the result with `client.nodes.waitForOutcome`.
2
+
3
+ # Streaming
4
+
5
+ Watch a run as it works: assistant text as it is produced, tool calls as they start and finish, reports as they are pushed, and the settled outcome.
6
+
7
+ ## The SDK surface
8
+
9
+ <!-- TODO(verify): run against a live daemon once phase 2 lands. -->
10
+
11
+ ```ts
12
+ const stream = client.nodes.stream({ prompt: 'Fix the failing test', cwd });
13
+
14
+ stream.on('node.output_text.delta', (e) => process.stdout.write(e.delta));
15
+
16
+ for await (const event of stream) {
17
+ // the same events, typed by `type`
18
+ }
19
+
20
+ const outcome = await stream.finalOutcome(); // resolves on node.settled
21
+ stream.abort(); // ends the HTTP response; the node keeps running
22
+ ```
23
+
24
+ | Member | Behaviour |
25
+ |---|---|
26
+ | `client.nodes.stream(params)` | Creates the node, then opens its event stream. Returns **synchronously**; `stream.node` is a promise for the created node. |
27
+ | `client.nodes.events(id, { after?, signal? })` | Streams a node that already exists. |
28
+ | `.on(type, cb)` / `.off(type, cb)` | Typed event emitter. |
29
+ | `[Symbol.asyncIterator]` | Yields the same typed events. |
30
+ | `.finalOutcome()` | Resolves with the `NodeOutcome` from `node.settled`; rejects on a terminal `error` event. |
31
+ | `.abort()` / `signal` | Aborts the underlying `fetch`. Raises `APIUserAbortError` in an in-flight iteration. |
32
+
33
+ The method is called `events`, not `subscribe`: a subscription is already a push-delivery edge between two nodes on the canvas, and one word must not mean two things.
34
+
35
+ **A stream is an observer, never a lifecycle hold.** Disconnecting drops your subscriber and leaves the node running. Opening a stream never revives a dormant node — call `client.nodes.revive(id)` first if that is what you want.
36
+
37
+ ## Events
38
+
39
+ Each event carries `node_id` and a `sequence_number` in addition to the fields listed.
40
+
41
+ | Event | `data` |
42
+ |---|---|
43
+ | `node.output_text.delta` | `{ node_id, sequence_number, delta }` |
44
+ | `node.output_text.done` | `{ node_id, sequence_number, text }` |
45
+ | `node.tool_call.started` | `{ node_id, sequence_number, tool_call_id, tool, summary }` |
46
+ | `node.tool_call.completed` | `{ node_id, sequence_number, tool_call_id, tool, status: 'ok' \| 'error', summary }` |
47
+ | `node.turn.started` | `{ node_id, sequence_number }` |
48
+ | `node.turn.completed` | `{ node_id, sequence_number }` |
49
+ | `node.report.pushed` | `{ node_id, sequence_number, report }` |
50
+ | `node.status.changed` | `{ node_id, sequence_number, status }` |
51
+ | `node.settled` | `{ node_id, sequence_number, outcome }` — terminal; the daemon ends the response after it |
52
+ | `error` | `{ error: { code, message, details? } }` — terminal |
53
+
54
+ `summary` on a tool-call event is a capped rendering of the arguments, never the raw arguments.
55
+
56
+ **Deliberately not carried:** thinking deltas, the model's tool-call construction deltas, raw tool output, and the system prompt. Those belong to the owner's viewer, not to an application's run stream.
57
+
58
+ ## The route
59
+
60
+ ```
61
+ GET /v1/nodes/{id}/events Accept: text/event-stream
62
+ ?after=<sequence_number> resume from a cursor (optional)
63
+ ```
64
+
65
+ Server-sent events: one record per event as `event: <type>`, `data: <JSON>`, blank line, plus `: keepalive` comment lines every 15 s. Authentication is the daemon's existing rule — filesystem permission on the unix socket, bearer token over TCP.
66
+
67
+ ### What you get depending on the node's state
68
+
69
+ | Node state when you call | What the stream does |
70
+ |---|---|
71
+ | Already settled | Writes `node.settled` with the outcome and ends. Nothing is revived. |
72
+ | Running | Streams live, seeded as described below. |
73
+ | Dormant and not settled | Writes `node.status.changed { status: 'dormant' }` and holds the response open with keepalives. It starts streaming if and when the daemon brings a broker up for that node. |
74
+
75
+ A node can settle between your `create` and your `events` call, so both branches are ordinary. Either way the terminal event is `node.settled`, read from the same outcome row `nodes.outcome` reads — a client that joins after settlement and a client that was live at settlement see the same event.
76
+
77
+ ## Sequence, resume, and failure
78
+
79
+ `sequence_number` is monotonic per node. The daemon keeps one in-memory ring per streamed node holding the last 512 events or 256 KiB, whichever binds first.
80
+
81
+ | Situation | Behaviour |
82
+ |---|---|
83
+ | You join a run already in progress | Seeded from the broker's snapshot: one `node.output_text.done` per completed assistant message, then one `node.output_text.delta` carrying the accumulated partial, then live. No content is lost — only delta granularity. |
84
+ | A second subscriber joins | Seeded from the ring, so both subscribers see the same sequence numbers. |
85
+ | `after=N` within the ring | Replays from `N+1`. |
86
+ | `after=N` older than the ring floor | An `error` event with code **`stream_gap`** carrying the earliest available sequence, **then the stream continues from the floor**. It never silently skips; you learn exactly what you lost. |
87
+ | Your reader is too slow | The broker drops a client past 1000 queued frames or 32 MiB. You get an `error` with code **`stream_dropped`** and the response ends. One slow reader cannot stall the engine. |
88
+ | The node's broker is replaced (revive, respawn) | The daemon reconnects and emits `node.status.changed`. Sequence continues. |
89
+ | The daemon restarts | The ring is gone. A resuming client gets `stream_gap` from sequence 1. There is no durable event log. |
90
+ | You disconnect | Your subscriber is dropped. The node keeps running. |
91
+
92
+ ### Resuming
93
+
94
+ <!-- TODO(verify): run against a live daemon once phase 2 lands; confirm the field carrying the earliest available sequence on a `stream_gap` error. -->
95
+
96
+ ```ts
97
+ let cursor: number | undefined;
98
+
99
+ for (;;) {
100
+ const stream = client.nodes.events(id, { after: cursor });
101
+ try {
102
+ for await (const event of stream) {
103
+ if (event.type === 'error' && event.error.code === 'stream_gap') {
104
+ console.warn('missed events before', event.error.details?.earliest_sequence);
105
+ continue; // the stream continues from the floor
106
+ }
107
+ cursor = event.sequence_number;
108
+ handle(event);
109
+ }
110
+ return; // ended on node.settled
111
+ } catch (e) {
112
+ if (isDropped(e)) continue; // stream_dropped — reconnect from the cursor
113
+ throw e;
114
+ }
115
+ }
116
+ ```
117
+
118
+ `stream_gap` is recoverable and the stream continues after it. `stream_dropped` is terminal for that HTTP response — reconnect with the cursor you last saw.
119
+
120
+ ## Event shapes depend on the pinned pi version
121
+
122
+ The delta and tool-call shapes come from the installed `@earendil-works/pi-agent-core` and `pi-ai` packages, not from crouter. The daemon's translator is the one place that depends on them. A pi version bump must be checked against it.
123
+
124
+ <!-- TODO(verify): record the pi version the phase-2 event table was verified against once streaming lands. -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.321",
3
+ "version": "0.3.323",
4
4
  "description": "crtr — agent runtime with memory, plugins, and marketplaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/runtime.lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.321",
3
+ "version": "0.3.323",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@north-light/crouter",
9
- "version": "0.3.321",
9
+ "version": "0.3.323",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "workspaces": [
@@ -5206,20 +5206,20 @@
5206
5206
  },
5207
5207
  "packages/crouter-api": {
5208
5208
  "name": "@north-light/crouter-api",
5209
- "version": "0.3.321",
5209
+ "version": "0.3.323",
5210
5210
  "license": "UNLICENSED"
5211
5211
  },
5212
5212
  "packages/crouter-env-docker": {
5213
5213
  "name": "@north-light/crouter-env-docker",
5214
- "version": "0.3.321",
5214
+ "version": "0.3.323",
5215
5215
  "license": "UNLICENSED"
5216
5216
  },
5217
5217
  "packages/crouter-sdk": {
5218
5218
  "name": "@north-light/crouter-sdk",
5219
- "version": "0.3.321",
5219
+ "version": "0.3.323",
5220
5220
  "license": "UNLICENSED",
5221
5221
  "dependencies": {
5222
- "@north-light/crouter-api": "^0.3.295"
5222
+ "@north-light/crouter-api": "^0.3.321"
5223
5223
  }
5224
5224
  }
5225
5225
  }
@@ -166,7 +166,9 @@ async function validatePublished(root, id) {
166
166
  }
167
167
 
168
168
  async function pack(archiveDir) {
169
- const output = await run('npm', ['pack', '--json', '--pack-destination', archiveDir], PACKAGE_ROOT, true);
169
+ // Errors still print; the prepack/postpack banners and notices do not, because
170
+ // on a fresh install this runs inside the user's first `crtr` command.
171
+ const output = await run('npm', ['pack', '--json', '--loglevel=error', '--foreground-scripts=false', '--pack-destination', archiveDir], PACKAGE_ROOT, true);
170
172
  const packed = JSON.parse(output);
171
173
  if (!Array.isArray(packed) || typeof packed[0]?.filename !== 'string') throw new Error('npm pack did not report an archive');
172
174
  return join(archiveDir, packed[0].filename);
@@ -416,7 +418,8 @@ async function install() {
416
418
  // Humanloop's postinstall only prewarms its external termrender cache; runtime rendering retries lazily with a plaintext fallback.
417
419
  const installEnv = { ...process.env };
418
420
  delete installEnv.npm_config_allow_scripts;
419
- await run('npm', ['ci', '--omit=dev', '--ignore-scripts'], stage, false, installEnv);
421
+ // stdout is captured and dropped (npm's "added N packages" summary); errors keep stderr.
422
+ await run('npm', ['ci', '--omit=dev', '--ignore-scripts', '--no-audit', '--no-fund', '--loglevel=error'], stage, true, installEnv);
420
423
  await patchPiTreeRootOrdering(stage);
421
424
  await smokeValidate(stage);
422
425
  const pkg = JSON.parse(await readFile(join(stage, 'package.json'), 'utf8'));
@@ -473,7 +476,8 @@ async function install() {
473
476
  } catch (error) {
474
477
  process.stderr.write(`runtime generation GC skipped: ${error.message}\n`);
475
478
  }
476
- process.stdout.write(`Installed immutable crouter runtime generation ${id}\n`);
479
+ // stderr, not stdout: on a fresh install this runs inside the user's first `crtr` command, which may be `--json`.
480
+ process.stderr.write(`Installed immutable crouter runtime generation ${id}\n`);
477
481
  return { generation: id, version: pkg.version, selected };
478
482
  } finally {
479
483
  if (temporary) await rm(temporary, { force: true });