@north-light/crouter 0.3.330 → 0.3.331
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/dist/builtin-memory/crouter-plugin/INDEX.md +12 -0
- package/dist/builtin-memory/crouter-plugin/README.md +25 -0
- package/dist/builtin-memory/crouter-plugin/bundles-and-memory.md +32 -0
- package/dist/builtin-memory/crouter-plugin/commands.md +21 -0
- package/dist/builtin-memory/crouter-plugin/deploying.md +33 -0
- package/dist/builtin-memory/crouter-plugin/errors.md +42 -0
- package/dist/builtin-memory/crouter-plugin/getting-started.md +28 -0
- package/dist/builtin-memory/crouter-plugin/output.md +30 -0
- package/dist/builtin-memory/crouter-plugin/parameters.md +29 -0
- package/dist/builtin-memory/crouter-sdk/INDEX.md +13 -0
- package/dist/builtin-memory/crouter-sdk/README.md +65 -0
- package/dist/builtin-memory/crouter-sdk/bash.md +41 -0
- package/dist/builtin-memory/crouter-sdk/client.md +98 -0
- package/dist/builtin-memory/crouter-sdk/docker.md +72 -0
- package/dist/builtin-memory/crouter-sdk/errors.md +112 -0
- package/dist/builtin-memory/crouter-sdk/files.md +51 -0
- package/dist/builtin-memory/crouter-sdk/getting-started.md +169 -0
- package/dist/builtin-memory/crouter-sdk/memory.md +88 -0
- package/dist/builtin-memory/crouter-sdk/migration.md +118 -0
- package/dist/builtin-memory/crouter-sdk/nodes.md +197 -0
- package/dist/builtin-memory/crouter-sdk/resources.md +88 -0
- package/dist/builtin-memory/crouter-sdk/streaming.md +148 -0
- package/package.json +1 -1
- package/runtime.lock.json +8 -8
- package/dist/builtin-memory/crouter-plugin.md +0 -16
- package/dist/builtin-memory/crouter-sdk.md +0 -90
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: knowledge
|
|
3
|
+
when-and-why-to-read: When you need `client.nodes`, this knowledge should be
|
|
4
|
+
read because Shipped, except where a row says otherwise.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# `client.nodes`
|
|
8
|
+
|
|
9
|
+
Shipped, except where a row says otherwise.
|
|
10
|
+
|
|
11
|
+
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.
|
|
12
|
+
|
|
13
|
+
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.
|
|
14
|
+
|
|
15
|
+
## Methods
|
|
16
|
+
|
|
17
|
+
| Method | Route | Phase | Notes |
|
|
18
|
+
|---|---|---|---|
|
|
19
|
+
| `nodes.create(params)` | `POST /v1/nodes` | 1 | Returns immediately with the node; it is already running. |
|
|
20
|
+
| `nodes.retrieve(id)` | `GET /v1/nodes/{id}` | 1 | |
|
|
21
|
+
| `nodes.list(query?)` | `GET /v1/nodes` | 1 | Returns an array, as the daemon does. |
|
|
22
|
+
| `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'`. |
|
|
23
|
+
| `nodes.waitForOutcome(id, opts?)` | repeats the above | 1 | Loops until settled. Honours `signal`; no implicit time bound. |
|
|
24
|
+
| `nodes.createAndWait(params, opts?)` | create + wait | 1 | |
|
|
25
|
+
| `nodes.parse(params, opts?)` | create + wait + typed result | 1 | [See below](#parse-and-structured-output). |
|
|
26
|
+
| `nodes.message(id, body)` | `POST /v1/nodes/{id}/messages` | 1 | Send a follow-up to a running or dormant node. |
|
|
27
|
+
| `nodes.interrupt(id)` | `POST /v1/nodes/{id}/interrupt` | 1 | Stop the current turn, keep the node. |
|
|
28
|
+
| `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. |
|
|
29
|
+
| `nodes.update(id, patch)` | `PATCH /v1/nodes/{id}/config` | Shipped | |
|
|
30
|
+
| `nodes.fork(id)` | `POST /v1/nodes/{id}/fork` | Shipped | |
|
|
31
|
+
| `nodes.revive(id, body?)` | `POST /v1/nodes/{id}/revive` | Shipped | |
|
|
32
|
+
| `nodes.promote(id, body?)` / `nodes.demote(id)` | matching routes | Shipped | Move a node between base and orchestrator mode. |
|
|
33
|
+
| `nodes.recycle(id)` | `POST /v1/nodes/{id}/recycle` | Shipped | |
|
|
34
|
+
| `nodes.yield(id, body?)` | `POST /v1/nodes/{id}/yield` | Shipped | |
|
|
35
|
+
| `nodes.wait(id, body)` | `POST /v1/nodes/{id}/wait` | Shipped | |
|
|
36
|
+
| `nodes.relaunchRoot(id)` | matching route | Shipped | |
|
|
37
|
+
| `nodes.reviveAll()` | `POST /v1/nodes/revive-all` | Shipped | |
|
|
38
|
+
| `nodes.stream(params, options?)` | create + `GET /…/events` | Shipped | [[crouter-sdk/streaming]]. |
|
|
39
|
+
| `nodes.events(id, options?)` | `GET /v1/nodes/{id}/events` | Shipped | [[crouter-sdk/streaming]]. |
|
|
40
|
+
|
|
41
|
+
Action methods keep the product's literal name (`fork`, `revive`, `promote`, `yield`) rather than being renamed into a generic verb.
|
|
42
|
+
|
|
43
|
+
Every method in the table accepts `RequestOptions` as its final argument: `{ headers?, signal?, timeout?, maxRetries? }`. `nodes.stream(params, options?)` uses those options for create and stream opening, except that its stream has no wall-clock `timeout`; `nodes.events(id, options?)` takes `NodeEventsOptions`, which adds `after` and excludes `timeout`.
|
|
44
|
+
|
|
45
|
+
## Create parameters
|
|
46
|
+
|
|
47
|
+
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.
|
|
48
|
+
|
|
49
|
+
| Field | Type | Meaning |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| `prompt` | `string` | The run's entire brief. |
|
|
52
|
+
| `kind` | `string` | Persona. Omit for the profile's default. |
|
|
53
|
+
| `model` | `string` | Durable model override. |
|
|
54
|
+
| `profile` | `string` | The store, memory, and purview the run uses. An application passes **its own** profile here. |
|
|
55
|
+
| `cwd` | `string` | Where the request came from. |
|
|
56
|
+
| `pin_cwd` | `string` | Pin the node to this directory regardless of `cwd`. |
|
|
57
|
+
| `situational_context` | `string` | Ambient context kept out of the visible prompt. |
|
|
58
|
+
| `deadline` | `string` (`1h30m`) | Wall clock from spawn. Expiry cancels the node and records `deadline_exceeded`. |
|
|
59
|
+
| `output_schema` | `string \| JsonSchema \| { toJSONSchema(): JsonSchema }` | Widened from the wire's JSON string; the SDK serializes. A zod v4 object satisfies the third form. |
|
|
60
|
+
| `root` | `boolean` | No parent, no subscription. An application's run is a root. |
|
|
61
|
+
| `root_lifecycle` | `'terminal' \| 'resident'` | `terminal` for a bounded run; `resident` for one a person will open and keep. |
|
|
62
|
+
| `mode` | `'base' \| 'orchestrator'` | Whether the node works hands-on or fans out to children. |
|
|
63
|
+
| `name` | `string` | Display label. |
|
|
64
|
+
| `description` | `string` | Display description. |
|
|
65
|
+
| `parent` | node id | Graph placement. An external caller leaves this unset. |
|
|
66
|
+
| `creator` | node id | Graph placement. An external caller leaves this unset. |
|
|
67
|
+
| `scopes` | `string[]` | Per-run allow-list. Omit it to inherit every scope. In beta, `ask`, `act`, `schedule`, `memory:read`, and `memory:write` are enforced; `llm`, `files:<dir>`, `net`, provider groups, and peers are recorded because their performers are not available. |
|
|
68
|
+
| `worktree` | `string \| boolean` | Create a managed git worktree for the run. |
|
|
69
|
+
| `fork_from` | `string` | Start from an existing conversation. |
|
|
70
|
+
| `no_kickoff` | `boolean` | Create the node without sending the first message. |
|
|
71
|
+
| `node_id` | `string` | Spawn at an exact id. A collision answers `409 node_id_exists`. |
|
|
72
|
+
| `prefer_warm` | `boolean` | Serve from the warm pool when the launch tuple matches. |
|
|
73
|
+
| `outcome_delivery` | `{ action, payload? }` | Arm outcome delivery at birth. |
|
|
74
|
+
|
|
75
|
+
`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 [[crouter-sdk/errors]].
|
|
76
|
+
|
|
77
|
+
## Outcomes
|
|
78
|
+
|
|
79
|
+
`waitForOutcome`, `createAndWait`, and `parse` return the settled `NodeOutcome` — the same union the API defines, not a translation of it.
|
|
80
|
+
|
|
81
|
+
| Wire | Narrow on | Carries |
|
|
82
|
+
|---|---|---|
|
|
83
|
+
| `kind: 'result'` | `outcome.kind === 'result'` | `structured_result`, `final_report_path`, and on `parse` a typed `output_parsed` |
|
|
84
|
+
| `kind: 'failure'`, `reason: 'declined'` | `outcome.reason === 'declined'` | `declined: { reason, code, retryable } \| null` — the agent honestly refused the schema |
|
|
85
|
+
| `kind: 'failure'`, any other `reason` | anything else | `detail: NodeOutcomeDetailV1 \| null` — `deadline_exceeded`, a provider fault, a wedge |
|
|
86
|
+
|
|
87
|
+
**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.
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
const outcome = await client.nodes.waitForOutcome(node.node_id, { signal });
|
|
91
|
+
|
|
92
|
+
switch (true) {
|
|
93
|
+
case outcome.kind === 'result':
|
|
94
|
+
console.log(outcome.structured_result, outcome.final_report_path);
|
|
95
|
+
break;
|
|
96
|
+
case outcome.reason === 'declined':
|
|
97
|
+
console.warn(outcome.declined?.reason, outcome.declined?.retryable);
|
|
98
|
+
break;
|
|
99
|
+
default:
|
|
100
|
+
console.error(outcome.reason, outcome.detail);
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### `outcome()` versus `waitForOutcome()`
|
|
105
|
+
|
|
106
|
+
`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.
|
|
107
|
+
|
|
108
|
+
`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.
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
for (;;) {
|
|
112
|
+
const poll = await client.nodes.outcome(node.node_id, { wait: 25 });
|
|
113
|
+
if (poll.state === 'settled' && poll.outcome !== null) {
|
|
114
|
+
console.log(poll.outcome);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
// update your UI or job heartbeat here
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## `parse()` and structured output
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
import { z } from 'zod';
|
|
125
|
+
|
|
126
|
+
const run = await client.nodes.parse({
|
|
127
|
+
prompt: 'Summarize the failing tests in this repo.',
|
|
128
|
+
cwd: '/path/to/repo',
|
|
129
|
+
output_schema: z.object({ failures: z.array(z.string()), root_cause: z.string() }),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (run.kind === 'result') console.log(run.output_parsed.root_cause);
|
|
133
|
+
else if (run.reason === 'declined') console.warn(run.declined?.reason);
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`ParsedOutcome<T>` is `NodeOutcome & { output_parsed: T | null }`, discriminated so `output_parsed` is non-null exactly when `kind === 'result'`.
|
|
137
|
+
|
|
138
|
+
`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`.
|
|
139
|
+
|
|
140
|
+
A candidate from Basis's real structured SDK result:
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"tmp": "k1",
|
|
145
|
+
"type": "claim",
|
|
146
|
+
"slug": "config-memories-in-project",
|
|
147
|
+
"text": "All applet configuration memories belong in project memories.",
|
|
148
|
+
"quote": "All configuration memories should be in the project memories. We know that.",
|
|
149
|
+
"speaker": "Me",
|
|
150
|
+
"confidence": "green"
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Sending a follow-up
|
|
155
|
+
|
|
156
|
+
`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.
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
await client.nodes.message(node.node_id, { body: 'Also check the integration lane.' });
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Stopping a run
|
|
163
|
+
|
|
164
|
+
| Call | Effect |
|
|
165
|
+
|---|---|
|
|
166
|
+
| `nodes.interrupt(id)` | Stops the current turn. The node stays on the canvas and can be messaged or revived. |
|
|
167
|
+
| `nodes.cancel(id, body?)` | Tears the node down along with the subtree it exclusively owns. Terminal. |
|
|
168
|
+
|
|
169
|
+
Aborting a `signal` you passed to `waitForOutcome` stops **your client waiting**. It does not stop the node. Call `cancel` for that.
|
|
170
|
+
|
|
171
|
+
## Nested resources
|
|
172
|
+
|
|
173
|
+
Nested routes become nested properties.
|
|
174
|
+
|
|
175
|
+
| Property | Methods | Phase |
|
|
176
|
+
|---|---|---|
|
|
177
|
+
| `nodes.reports` | `list` | Shipped |
|
|
178
|
+
| `nodes.jobs` | `list`, `cancel` | Shipped |
|
|
179
|
+
| `nodes.worktree` | `close`, `abandon` | Shipped |
|
|
180
|
+
| `nodes.result` | `submit` | Shipped — only an agent inside a run calls this |
|
|
181
|
+
|
|
182
|
+
`nodes.reports.list(id)` returns the agent's pushed progress reports, newest first. Use it when reports are the progress view your application needs; use [[crouter-sdk/streaming]] for live output and tool-call events.
|
|
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 phase-3 memory routes. See [[crouter-sdk/memory]].
|
|
194
|
+
|
|
195
|
+
## Identifier validation
|
|
196
|
+
|
|
197
|
+
Methods that take a node id validate it before the request: core node methods, lifecycle methods, `nodes.reports`, `nodes.jobs`, `nodes.worktree`, and `nodes.result`. An invalid id throws `TypeError` locally and sends no request. `nodes.outcome(id, { wait })` also throws `RangeError` locally when `wait` is not an integer from 0 through 25.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: knowledge
|
|
3
|
+
when-and-why-to-read: When you need Resource map, this knowledge should be read
|
|
4
|
+
because Every namespace exported by the client. Namespaces are camelCase.
|
|
5
|
+
Verbs are `create`, `retrieve`, `list`, `update`, `delete`, and `cancel`,
|
|
6
|
+
except where the product already has a literal name for the action (`fork`,
|
|
7
|
+
`revive`, `promote`, `pause`, `poke`) — in which case the SDK uses that name.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Resource map
|
|
11
|
+
|
|
12
|
+
Every namespace exported by the client. 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.
|
|
13
|
+
|
|
14
|
+
| Namespace | Methods | Routes |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| `nodes` | `create`, `retrieve`, `list`, `update`, `cancel`, `interrupt`, `message`, `outcome`, `waitForOutcome`, `createAndWait`, `parse`, `stream`, `events`, `fork`, `revive`, `reviveAll`, `promote`, `demote`, `recycle`, `yield`, `wait`, `relaunchRoot` | `/v1/nodes…` |
|
|
17
|
+
| `nodes.reports` | `list` | `/v1/nodes/{id}/reports` |
|
|
18
|
+
| `nodes.jobs` | `list`, `cancel` | `/v1/nodes/{id}/jobs` |
|
|
19
|
+
| `nodes.worktree` | `close`, `abandon` | `/v1/nodes/{id}/worktree/…` |
|
|
20
|
+
| `nodes.result` | `submit` | `POST /v1/nodes/{id}/result` |
|
|
21
|
+
| `profiles` | `ensure`, `retrieve` | `/v1/profiles…` |
|
|
22
|
+
| `auth` | `status` | `GET /v1/status`, then `GET /v1/model-auth/readiness` once the daemon is ready |
|
|
23
|
+
| `system` | `status`, `health` | `/v1/status`, `/healthz` |
|
|
24
|
+
| `files` | `read`, `write`, `list` | `/v1/files/peek`, `/v1/files/write`, `/v1/files/list` |
|
|
25
|
+
| `bash` | `run` | `/v1/bash` |
|
|
26
|
+
| `canvas` | `attention`, `attentionCounts`, `snapshot`, `roster`, `dashboard`, `prune` | `/v1/canvas…` and composed node/status requests for `dashboard` |
|
|
27
|
+
| `canvas.history` | `search`, `grep`, `read`, `stats` | `/v1/canvas/history/…` |
|
|
28
|
+
| `crons` | `create`, `retrieve`, `list`, `pause`, `resume`, `run`, `delete`, `poke` | `/v1/crons…` |
|
|
29
|
+
| `human.requests` | `create`, `retrieve`, `replace`, `respond`, `dismiss`, `cancel` | `/v1/human/requests…` |
|
|
30
|
+
| `human.inbox` | `list`, `retrieve`, `respond`, `progress`, `cancel`, `history`, `response` | `/v1/human/inbox…` |
|
|
31
|
+
| `models.credentials` | `list`, `install`, `remove` | `/v1/model-auth…` |
|
|
32
|
+
| `models.config` | `update` | `PUT /v1/model-config` |
|
|
33
|
+
| `memory` | `list`, `retrieve`, `create`, `update`, `delete`, `move`, `search`, `history`, `resolve` | `/v1/memory…`; see [[crouter-sdk/memory]] |
|
|
34
|
+
|
|
35
|
+
Action methods keep the product's literal name (`fork`, `revive`, `promote`, `yield`) rather than being renamed into a generic verb. Every request-capable method takes `RequestOptions` as its final argument after its path, body, or query arguments. `RequestOptions` is `{ headers?, signal?, timeout?, maxRetries? }`; stream event options exclude `timeout` and add `after`.
|
|
36
|
+
|
|
37
|
+
## Files and bash
|
|
38
|
+
|
|
39
|
+
[[crouter-sdk/files]] reads through `GET /v1/files/peek`, atomically writes, and lists absolute host paths. Reads and lists report `truncated`; writes over 1 MiB are refused instead of truncated. [[crouter-sdk/bash]] runs one command in a required absolute working directory and returns non-zero exits as values.
|
|
40
|
+
|
|
41
|
+
## Canvas
|
|
42
|
+
|
|
43
|
+
`client.canvas.attention()`, `attentionCounts(body)`, `snapshot()`, `roster()`, and `prune(body)` each send their matching canvas route. `dashboard(query?)` composes `GET /v1/nodes` and `GET /v1/status`, because the daemon deliberately has no dashboard route. `client.canvas.history.search(body)`, `grep(body)`, `read(query)`, and `stats(body)` wrap the four history routes.
|
|
44
|
+
|
|
45
|
+
## Crons
|
|
46
|
+
|
|
47
|
+
`client.crons.create(body)`, `retrieve(id, query?)`, `list(query?)`, `pause(id, query?)`, `resume(id, query?)`, `run(id, query?)`, `delete(id, query?)`, and `poke()` retain the daemon's cron DTOs and snake_case query fields. `delete` is the SDK verb for the daemon's `DELETE /v1/crons/{id}` route.
|
|
48
|
+
|
|
49
|
+
## Human requests and inbox
|
|
50
|
+
|
|
51
|
+
`client.human.requests` exposes `create`, `retrieve`, `replace`, `respond`, `dismiss`, and `cancel`. `client.human.inbox` exposes `list`, `retrieve`, `respond`, `progress`, `cancel`, `history`, and `response`. The inbox methods preserve the page protocol's nested camelCase fields and the daemon envelope's snake_case fields.
|
|
52
|
+
|
|
53
|
+
## Models
|
|
54
|
+
|
|
55
|
+
`client.models.credentials.list()`, `install(provider, body)`, and `remove(provider)` wrap `/v1/model-auth`. `client.models.config.update(body)` sends `PUT /v1/model-config`. Credential material is accepted only by `install`; list responses remain sanitized daemon DTOs.
|
|
56
|
+
|
|
57
|
+
## Identifier validation
|
|
58
|
+
|
|
59
|
+
Methods with node, cron, bash-job, human-request, inbox-ticket, provider, or profile identifiers validate them before calling the daemon. Invalid values throw `TypeError` locally and send no request. This applies to node action methods and nested node resources, cron methods that take an id, `human.requests` methods that take an id, `human.inbox` methods that take a ticket id, `models.credentials.install` and `remove`, and profile `ensure` and `retrieve`. File paths are not subject to this identifier check; the daemon validates their absolute-path contract.
|
|
60
|
+
|
|
61
|
+
## Excluded from the typed surface
|
|
62
|
+
|
|
63
|
+
These `/v1` routes get no SDK method, with the reason. They are still reachable through `client.request()`.
|
|
64
|
+
|
|
65
|
+
| Routes | Why |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `/v1/focuses…` | A focus maps a node to a tmux pane. An SDK caller has no tmux. It stays on `/v1` for the viewer. |
|
|
68
|
+
| `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. |
|
|
69
|
+
| `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 [[crouter-sdk/streaming]] is the application-facing way to watch a node. |
|
|
70
|
+
| 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. |
|
|
71
|
+
|
|
72
|
+
## The escape hatch
|
|
73
|
+
|
|
74
|
+
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:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
client.request(method, path, body?, options?)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
It carries the same authentication, the same retry policy, and the same error mapping as every generated method — it is untyped, not unsupported.
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
const focuses = await client.request<FocusDTO[]>('GET', '/v1/focuses');
|
|
84
|
+
|
|
85
|
+
await client.request('POST', `/v1/nodes/${id}/some-new-route`, { field: 'value' }, { timeout: 5_000 });
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
If you find yourself reaching for `client.request()` for something an application genuinely needs, that route belongs in the table above. Say so rather than building on the escape hatch.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: knowledge
|
|
3
|
+
when-and-why-to-read: "When you need Streaming, this knowledge should be read
|
|
4
|
+
because Watch a run as it works: assistant text as it is produced, tool calls
|
|
5
|
+
as they start and finish, reports as they are pushed, and the settled
|
|
6
|
+
outcome."
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Streaming
|
|
10
|
+
|
|
11
|
+
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.
|
|
12
|
+
|
|
13
|
+
## The SDK surface
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
const stream = client.nodes.stream({ prompt: 'Fix the failing test', cwd }, { headers: { 'x-trace': 'run-9' } });
|
|
17
|
+
|
|
18
|
+
stream.on('node.output_text.delta', (event) => process.stdout.write(event.delta));
|
|
19
|
+
|
|
20
|
+
for await (const event of stream) {
|
|
21
|
+
// the same events, typed by `type`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const outcome = await stream.finalOutcome(); // resolves on node.settled
|
|
25
|
+
stream.abort(); // ends the HTTP response; the node keeps running
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
| Member | Behaviour |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `client.nodes.stream(params, options?)` | Creates the node, then opens its event stream. Returns **synchronously**; `stream.node` is a `Promise<NodeDetailDTO>` for the created node. The request options apply to creation and to opening the stream, except `timeout`, because a stream has no wall-clock timeout. |
|
|
31
|
+
| `client.nodes.events(id, options?)` | Validates and retrieves an existing node, then streams it. `options` is `NodeEventsOptions`: `after` plus request `headers`, `signal`, and `maxRetries`; it deliberately has no `timeout`. |
|
|
32
|
+
| `.on(type, listener)` / `.off(type, listener)` | Typed event emitter; each returns the `NodeStream`. |
|
|
33
|
+
| `[Symbol.asyncIterator]()` | Yields the same `NodeEventDTO` union. |
|
|
34
|
+
| `.finalOutcome()` | Returns `Promise<NodeOutcomeDTO>`, resolving on `node.settled` and rejecting on a terminal `error` event or a connection failure. |
|
|
35
|
+
| `.abort()` | Aborts the underlying `fetch`. An in-flight iterator and `finalOutcome()` reject with `APIUserAbortError`. Passing your own `signal` in the options aborts the stream the same way; `NodeStream` exposes no `signal` property. |
|
|
36
|
+
|
|
37
|
+
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.
|
|
38
|
+
|
|
39
|
+
**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.
|
|
40
|
+
|
|
41
|
+
## Activity helper
|
|
42
|
+
|
|
43
|
+
`followActivity(stream, { describe? })` turns streamed tool-call events into immutable `ActivityStep[]` snapshots for a plain activity feed. `describeToolDefault(tool, summary)` names the common tools; pass `describe` to use labels for your application or return `null` to hide a tool call. Tool summaries describe the argument shape, never argument values or raw arguments.
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { followActivity } from '@north-light/crouter-sdk';
|
|
47
|
+
|
|
48
|
+
for await (const steps of followActivity(stream)) {
|
|
49
|
+
render(steps);
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
A step starts as `running`; a `node.tool_call.completed` event changes it to `done` when `status` is `ok` or `failed` when `status` is `error`.
|
|
54
|
+
|
|
55
|
+
## Events
|
|
56
|
+
|
|
57
|
+
Every event except `error` carries `node_id` and `sequence_number` in addition to the fields listed. `error` carries only its error object.
|
|
58
|
+
|
|
59
|
+
| Event | `data` |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `node.output_text.delta` | `{ node_id, sequence_number, delta }` |
|
|
62
|
+
| `node.output_text.done` | `{ node_id, sequence_number, text }` |
|
|
63
|
+
| `node.tool_call.started` | `{ node_id, sequence_number, tool_call_id, tool, summary }` |
|
|
64
|
+
| `node.tool_call.completed` | `{ node_id, sequence_number, tool_call_id, tool, status: 'ok' \| 'error', summary }` |
|
|
65
|
+
| `node.turn.started` | `{ node_id, sequence_number }` |
|
|
66
|
+
| `node.turn.completed` | `{ node_id, sequence_number }` |
|
|
67
|
+
| `node.report.pushed` | `{ node_id, sequence_number, report }` |
|
|
68
|
+
| `node.status.changed` | `{ node_id, sequence_number, status }`, where `status` can also be stream-only `'dormant'` |
|
|
69
|
+
| `node.settled` | `{ node_id, sequence_number, outcome }` — terminal; the daemon ends the response after it |
|
|
70
|
+
| `error` | `{ error: { code: 'stream_gap' \| 'stream_dropped' \| 'stream_error', message, details?: { earliest_sequence?: number } } }` — `stream_gap` is recoverable; the other codes terminate the response |
|
|
71
|
+
|
|
72
|
+
`summary` on a tool-call event states whether arguments are null, an array and its item count, an object and its field count, or a primitive type. It never includes argument values or tool output.
|
|
73
|
+
|
|
74
|
+
**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.
|
|
75
|
+
|
|
76
|
+
## The route
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
GET /v1/nodes/{id}/events Accept: text/event-stream
|
|
80
|
+
?after=<sequence_number> resume from a cursor (optional)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
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.
|
|
84
|
+
|
|
85
|
+
### What you get depending on the node's state
|
|
86
|
+
|
|
87
|
+
| Node state when you call | What the stream does |
|
|
88
|
+
|---|---|
|
|
89
|
+
| Already settled | Writes retained events when available, ending in `node.settled`; otherwise writes `node.settled` with the outcome and ends. Nothing is revived. |
|
|
90
|
+
| Running | Streams live, seeded as described below. |
|
|
91
|
+
| 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. |
|
|
92
|
+
|
|
93
|
+
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 outcome.
|
|
94
|
+
|
|
95
|
+
## Sequence, resume, and failure
|
|
96
|
+
|
|
97
|
+
`sequence_number` is monotonic per node while that node's in-memory stream hub exists. The daemon keeps one in-memory ring per streamed node holding the last 512 events or 256 KiB, whichever binds first.
|
|
98
|
+
|
|
99
|
+
| Situation | Behaviour |
|
|
100
|
+
|---|---|
|
|
101
|
+
| You join a run already in progress | First replays already-pushed reports oldest first, then seeds 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. |
|
|
102
|
+
| A second subscriber joins | Seeded from the ring, so both subscribers see the same sequence numbers. |
|
|
103
|
+
| `after=N` within the retained range | Replays events after `N`. |
|
|
104
|
+
| `after=N` outside the retained range, including a future cursor | An `error` event with code **`stream_gap`** carries the earliest available sequence, then the stream continues from that point. It never silently skips. |
|
|
105
|
+
| One event exceeds the 256 KiB replay budget | Live subscribers receive it, but the daemon clears the resume ring. A cursor before that event gets `stream_gap`; a cursor on it resumes at the next retained event. |
|
|
106
|
+
| Your HTTP reader is too slow | The daemon ends the response with an `error` event whose code is **`stream_dropped`**. One slow reader cannot stall the engine. |
|
|
107
|
+
| The node's broker is replaced or its observer socket closes | The daemon reconnects and emits `node.status.changed`. Sequence continues while the stream hub remains in memory. |
|
|
108
|
+
| The daemon restarts | The ring is gone. A resuming client gets `stream_gap` with `earliest_sequence: 1`. There is no durable event log. |
|
|
109
|
+
| You disconnect | Your subscriber is dropped. The node keeps running. |
|
|
110
|
+
|
|
111
|
+
### Resuming
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
import { APIError } from '@north-light/crouter-sdk';
|
|
115
|
+
|
|
116
|
+
let cursor: number | undefined;
|
|
117
|
+
|
|
118
|
+
for (;;) {
|
|
119
|
+
const stream = client.nodes.events(id, { after: cursor });
|
|
120
|
+
try {
|
|
121
|
+
for await (const event of stream) {
|
|
122
|
+
if (event.type === 'error') {
|
|
123
|
+
if (event.error.code === 'stream_gap') {
|
|
124
|
+
console.warn('missed events before', event.error.details?.earliest_sequence);
|
|
125
|
+
continue; // the stream continues from the floor
|
|
126
|
+
}
|
|
127
|
+
throw new APIError(0, event.error.code, event.error.message, event.error.details);
|
|
128
|
+
}
|
|
129
|
+
cursor = event.sequence_number;
|
|
130
|
+
handle(event);
|
|
131
|
+
}
|
|
132
|
+
return; // ended on node.settled
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (error instanceof APIError && error.code === 'stream_dropped') continue;
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`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.
|
|
141
|
+
|
|
142
|
+
## Identifier validation
|
|
143
|
+
|
|
144
|
+
`nodes.events(id, options?)` validates `id` before it requests the node or opens SSE. Because it returns a `NodeStream` synchronously, an invalid node identifier rejects `stream.node`, `stream.finalOutcome()`, and an iteration with `TypeError`; no request is sent. See [[crouter-sdk/errors]].
|
|
145
|
+
|
|
146
|
+
## Event shapes depend on the pinned pi version
|
|
147
|
+
|
|
148
|
+
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.
|
package/package.json
CHANGED
package/runtime.lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/crouter",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.331",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@north-light/crouter",
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.331",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "GPL-3.0-only",
|
|
12
12
|
"workspaces": [
|
|
@@ -4795,28 +4795,28 @@
|
|
|
4795
4795
|
},
|
|
4796
4796
|
"packages/crouter-api": {
|
|
4797
4797
|
"name": "@north-light/crouter-api",
|
|
4798
|
-
"version": "0.3.
|
|
4798
|
+
"version": "0.3.331",
|
|
4799
4799
|
"license": "GPL-3.0-only"
|
|
4800
4800
|
},
|
|
4801
4801
|
"packages/crouter-env-docker": {
|
|
4802
4802
|
"name": "@north-light/crouter-env-docker",
|
|
4803
|
-
"version": "0.3.
|
|
4803
|
+
"version": "0.3.331",
|
|
4804
4804
|
"license": "GPL-3.0-only"
|
|
4805
4805
|
},
|
|
4806
4806
|
"packages/crouter-plugin": {
|
|
4807
4807
|
"name": "@north-light/crouter-plugin",
|
|
4808
|
-
"version": "0.3.
|
|
4808
|
+
"version": "0.3.331",
|
|
4809
4809
|
"license": "GPL-3.0-only",
|
|
4810
4810
|
"dependencies": {
|
|
4811
|
-
"@north-light/crouter-api": "^0.3.
|
|
4811
|
+
"@north-light/crouter-api": "^0.3.331"
|
|
4812
4812
|
}
|
|
4813
4813
|
},
|
|
4814
4814
|
"packages/crouter-sdk": {
|
|
4815
4815
|
"name": "@north-light/crouter-sdk",
|
|
4816
|
-
"version": "0.3.
|
|
4816
|
+
"version": "0.3.331",
|
|
4817
4817
|
"license": "GPL-3.0-only",
|
|
4818
4818
|
"dependencies": {
|
|
4819
|
-
"@north-light/crouter-api": "^0.3.
|
|
4819
|
+
"@north-light/crouter-api": "^0.3.331"
|
|
4820
4820
|
}
|
|
4821
4821
|
}
|
|
4822
4822
|
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
kind: knowledge
|
|
3
|
-
when-and-why-to-read: When an application needs to expose operations as crtr commands over HTTP, this knowledge should be read because one typed command definition can provide the handler and install archive without a hand-written manifest drifting from the app.
|
|
4
|
-
short-form: Use @north-light/crouter-plugin to expose an application's typed HTTP operations as crtr commands from one TypeScript command tree.
|
|
5
|
-
surfaces:
|
|
6
|
-
- on: boot
|
|
7
|
-
at: preview
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
# `@north-light/crouter-plugin`
|
|
11
|
-
|
|
12
|
-
Use `@north-light/crouter-plugin` when an application should provide crtr commands through an HTTP endpoint. It generates `commands.json` and the endpoint install archive from the same typed command tree that its Fetch handler runs, so an application does not write a manifest, route table, or `plugin.json`.
|
|
13
|
-
|
|
14
|
-
Read `node_modules/@north-light/crouter-plugin/README.md` after installing the package for the shortest working plugin, and https://github.com/vallum-security/crouter/tree/main/docs/plugin for parameters, outputs, errors, streaming, deployment, bundles, and memory docs. Confirm the installed package exports before writing against it; the authoring surface is `definePlugin`, `defineBranch`, `defineLeaf`, `defineStreamingLeaf`, `param`, `field`, `createFetchHandler`, `buildCommandManifest`, `buildBundle`, `LeafError`, `ManifestInvalidError`, `PluginDefinitionError`, and `kebab`.
|
|
15
|
-
|
|
16
|
-
Install a deployed handler with `crtr pkg plugin install --endpoint <public-handler-url> --name <plugin-name>`. The generated REST paths include the handler's public mount path, so install the URL the handler actually serves. Crouter synthesizes `.crouter-plugin/plugin.json` for this install; the application does not author it.
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
kind: knowledge
|
|
3
|
-
description: Driving a crouter daemon from an application with @north-light/crouter-sdk
|
|
4
|
-
when-and-why-to-read: When you are writing or reviewing application code that runs a crtr agent — installing @north-light/crouter-sdk, constructing a client, creating a node, watching its event stream, reading its result, reading or writing memory, or connecting a browser to a daemon — this reference should be read because `generate()` and `local()` were deleted, while the typed client keeps daemon transport and agent outcomes distinct.
|
|
5
|
-
short-form: The ESM-only application-facing client for a crtrd daemon. Construction and transports, `client.nodes` create/wait/parse/stream, activity snapshots, phase-2 resources including files and bash, `client.memory`, the returned-not-thrown outcome union, and error classes.
|
|
6
|
-
surfaces:
|
|
7
|
-
- on: boot
|
|
8
|
-
at: name
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
# `@north-light/crouter-sdk`
|
|
12
|
-
|
|
13
|
-
One ESM-only client an application installs to drive a crouter daemon over its `/v1` API: create agent runs, watch their event streams, wait for typed results, and reach the rest of the daemon. Resource methods issue `/v1` requests; `createAndWait`, `parse`, `stream`, and `auth.status` compose requests. crtrd stays the sole owner of canvas state. The runtime model behind the objects — nodes, the canvas, lifecycle, reports — is `internal/nodes-and-canvas`.
|
|
14
|
-
|
|
15
|
-
## Check what has shipped before you write against it
|
|
16
|
-
|
|
17
|
-
Streaming is shipped: `client.nodes.stream`, `client.nodes.events`, and `GET /v1/nodes/{id}/events`. Memory is shipped: `client.memory` exposes `list`, `retrieve`, `create`, `update`, `delete`, `move`, `search`, `history`, and `resolve`.
|
|
18
|
-
|
|
19
|
-
The client also ships construction and transports, `client.nodes` create / retrieve / list / outcome / waitForOutcome / createAndWait / parse / message / cancel / interrupt, `nodes.reports.list`, `profiles`, `system.status`, `files.read` / `write` / `list`, the error hierarchy, and per-request options. Phase 2 also ships node lifecycle, job, worktree, and result resources; canvas and canvas history; crons; human requests and inbox; models; and `client.bash.run`.
|
|
20
|
-
|
|
21
|
-
Confirm against the installed package's types rather than against any document, including this one.
|
|
22
|
-
|
|
23
|
-
## The published README is stale
|
|
24
|
-
|
|
25
|
-
`generate()`, `local()`, and the `Environment` interface were **deleted**, not deprecated — a hard cut with no shim. Code written from the npm README or from training data will call `generate({ prompt, schema, env: local() })` and will not compile. The replacement is `new Crouter()` plus `client.nodes.parse({ prompt, output_schema })`.
|
|
26
|
-
|
|
27
|
-
## A run, end to end
|
|
28
|
-
|
|
29
|
-
An agent run needs a daemon. `npm i -g @north-light/crouter` installs `crtr` and `crtrd`; `new Crouter()` starts the daemon itself on a cold socket, so nothing has to be running first.
|
|
30
|
-
|
|
31
|
-
```ts
|
|
32
|
-
import Crouter from '@north-light/crouter-sdk';
|
|
33
|
-
import { z } from 'zod';
|
|
34
|
-
|
|
35
|
-
const client = new Crouter();
|
|
36
|
-
|
|
37
|
-
const run = await client.nodes.parse({
|
|
38
|
-
prompt: 'Read package.json here and report its name and version.',
|
|
39
|
-
cwd: '/path/to/repo',
|
|
40
|
-
output_schema: z.object({ name: z.string(), version: z.string() }),
|
|
41
|
-
root: true,
|
|
42
|
-
root_lifecycle: 'terminal',
|
|
43
|
-
});
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
`root: true` means the run has no parent node and reports to nobody, which is what an application's run is. `root_lifecycle: 'terminal'` finishes and reaps; `'resident'` keeps the node for a person to open. Wire fields are `snake_case` and the SDK does not camelize them.
|
|
47
|
-
|
|
48
|
-
## The agent's own outcome is returned, never thrown
|
|
49
|
-
|
|
50
|
-
A run that refused the schema, hit its deadline, or crashed comes back as a value. Wrapping the call in `try`/`catch` and treating the catch as the failure path silently drops every agent-side outcome, because none of them throw.
|
|
51
|
-
|
|
52
|
-
```ts
|
|
53
|
-
if (run.kind === 'result') use(run.output_parsed);
|
|
54
|
-
else if (run.reason === 'declined') handleRefusal(run.declined?.reason, run.declined?.retryable);
|
|
55
|
-
else handleFailure(run.reason, run.detail);
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
A decline is `kind: 'failure'` with `reason: 'declined'` — narrow on `reason`, not on `kind`. `output_parsed` is non-null exactly when `kind === '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 makes it `unknown`. The SDK does not re-validate the result because the daemon already enforced the schema when the agent submitted.
|
|
59
|
-
|
|
60
|
-
Exceptions are for the layer underneath: `BadRequestError`, `AuthenticationError`, `PermissionDeniedError`, `NotFoundError`, `ConflictError`, `UnprocessableEntityError`, `RateLimitError`, `InternalServerError`, `APIConnectionError`, `APIConnectionTimeoutError`, `APIUserAbortError`, all extending `APIError` with `status`, `code`, `message`, `details`, `headers`. Branch on `code` — it is the stable machine-readable identity (`node_id_exists`, `daemon_unavailable`, `node_dormant`).
|
|
61
|
-
|
|
62
|
-
## A retried create spawns a second agent unless you pass `node_id`
|
|
63
|
-
|
|
64
|
-
`maxRetries` (default 2) covers connection errors and 429/5xx on `GET`, `HEAD`, and `DELETE`. **`POST` and `PATCH` are never retried**, because a mutation whose response was interrupted may already have been applied. A caller that wants a retry-safe create passes `node_id`: the duplicate then fails with `409 node_id_exists`, which the caller catches and turns into a `waitForOutcome` on the run that already exists.
|
|
65
|
-
|
|
66
|
-
## `waitForOutcome` has no time bound
|
|
67
|
-
|
|
68
|
-
It polls until the node settles, however long that takes. Bound it from one side or the other: a `deadline` on `create` makes the daemon cancel the run, a `signal` makes your client stop waiting. Aborting the signal stops the client, not the agent — `client.nodes.cancel(id)` stops the agent, `client.nodes.interrupt(id)` stops only the current turn.
|
|
69
|
-
|
|
70
|
-
## Watch a run
|
|
71
|
-
|
|
72
|
-
`const stream = client.nodes.stream(params, options?)` returns synchronously. `stream.node` resolves to the created node; `for await (const event of stream)` and `stream.on(type, listener)` receive the same typed events; `await stream.finalOutcome()` resolves from `node.settled`; and `stream.abort()` disconnects the observer without stopping the node. `client.nodes.events(id, options?)` watches an existing node; its options add `after` and omit request `timeout`. A `stream_gap` error event is followed by continuation; `stream_dropped` and `stream_error` reject `finalOutcome()`.
|
|
73
|
-
|
|
74
|
-
`followActivity(stream, { describe? })` converts tool-call events into immutable `ActivityStep[]` snapshots. `describeToolDefault` names common tools. A custom `describe(tool, summary)` returns a label or `null` to hide the call; `summary` states only the argument shape, never values, raw arguments, or tool output.
|
|
75
|
-
|
|
76
|
-
## Reaching a daemon that is not on this machine
|
|
77
|
-
|
|
78
|
-
`new Crouter()` uses the owner's unix socket, which authenticates by filesystem permission. A browser has no unix sockets and a remote process has no access to that one, so both use the TCP listener with a bearer token: `new Crouter({ baseURL, token })`.
|
|
79
|
-
|
|
80
|
-
The listener is off until someone runs `crtr sys connect` on the daemon's machine, which stores the address and token and hands the daemon over to a successor that boots with it on, then prints both to paste into the application. The token is the owner credential — whoever holds it reaches the whole surface. Cross-origin calls work only when a token is set; an unauthenticated listener stays same-origin so that no page the user happens to visit can drive their daemon. A page served over HTTPS calling `http://localhost:<port>` also needs Chrome's Local Network Access permission, which the browser prompts for once per site and which requires no response header.
|
|
81
|
-
|
|
82
|
-
`baseURL` and `socketPath` are mutually exclusive — passing both throws `TypeError` at construction. Options fall back to `CRTR_BASE_URL`, `CRTR_SOCKET`, `CRTR_HOME`, and `CRTRD_TOKEN`, read once at construction.
|
|
83
|
-
|
|
84
|
-
## Reaching a route the SDK does not wrap
|
|
85
|
-
|
|
86
|
-
`client.request(method, path, body?, options?)` carries the same authentication, retry policy, and error mapping as every generated method. Some routes are deliberately outside the typed surface — tmux focuses, a node's own inbox mail claim, local attach, and the broker's control plane — because an external caller invoking them either cannot use the result or becomes a second writer to state the broker and daemon coordinate between themselves.
|
|
87
|
-
|
|
88
|
-
## The full reference
|
|
89
|
-
|
|
90
|
-
Page-per-topic detail — the complete create-parameter table, streaming event table, memory scope object, resource map, and migration table — lives in the crouter repository under `docs/sdk/`, published at <https://github.com/vallum-security/crouter/tree/main/docs/sdk>.
|