@north-light/crouter 0.3.329 → 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.
Files changed (28) hide show
  1. package/dist/builtin-memory/crouter-plugin/INDEX.md +12 -0
  2. package/dist/builtin-memory/crouter-plugin/README.md +25 -0
  3. package/dist/builtin-memory/crouter-plugin/bundles-and-memory.md +32 -0
  4. package/dist/builtin-memory/crouter-plugin/commands.md +21 -0
  5. package/dist/builtin-memory/crouter-plugin/deploying.md +33 -0
  6. package/dist/builtin-memory/crouter-plugin/errors.md +42 -0
  7. package/dist/builtin-memory/crouter-plugin/getting-started.md +28 -0
  8. package/dist/builtin-memory/crouter-plugin/output.md +30 -0
  9. package/dist/builtin-memory/crouter-plugin/parameters.md +29 -0
  10. package/dist/builtin-memory/crouter-sdk/INDEX.md +13 -0
  11. package/dist/builtin-memory/crouter-sdk/README.md +65 -0
  12. package/dist/builtin-memory/crouter-sdk/bash.md +41 -0
  13. package/dist/builtin-memory/crouter-sdk/client.md +98 -0
  14. package/dist/builtin-memory/crouter-sdk/docker.md +72 -0
  15. package/dist/builtin-memory/crouter-sdk/errors.md +112 -0
  16. package/dist/builtin-memory/crouter-sdk/files.md +51 -0
  17. package/dist/builtin-memory/crouter-sdk/getting-started.md +169 -0
  18. package/dist/builtin-memory/crouter-sdk/memory.md +88 -0
  19. package/dist/builtin-memory/crouter-sdk/migration.md +118 -0
  20. package/dist/builtin-memory/crouter-sdk/nodes.md +197 -0
  21. package/dist/builtin-memory/crouter-sdk/resources.md +88 -0
  22. package/dist/builtin-memory/crouter-sdk/streaming.md +148 -0
  23. package/dist/clients/attach/overlays/graph.js +1 -1
  24. package/dist/clients/attach/viewer.js +508 -508
  25. package/package.json +1 -1
  26. package/runtime.lock.json +8 -8
  27. package/dist/builtin-memory/crouter-plugin.md +0 -16
  28. 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.
@@ -1 +1 @@
1
- var q=Object.defineProperty;var p=(m,e)=>q(m,"name",{value:e,configurable:!0});import{execFile as N}from"node:child_process";import{matchesKey as O,truncateToWidth as C}from"@earendil-works/pi-tui";import{formatBinding as P,matchesPiTuiInput as H}from"../../../core/keybindings/index.js";import{fullName as B}from"../../../core/canvas/labels.js";import{faultSummary as L}from"../../../core/canvas/status-glyph.js";import{RemoteCanvasSource as G}from"../../../core/canvas/remote-canvas-source.js";import{cliClient as F}from"../../../commands/api-client.js";import{statusRank as S}from"../../../core/canvas/node-order.js";import{navLabel as K,nodeGlyph as W,tokensCell as j,cycleBadge as Y,askBadge as D,activityCell as V,shortId as M,visibleWidth as E,fillBar as k,fillWidth as A,truncate as z,isAttached as U,VIEWPORT_FALLBACK_ROWS as X,DIM as v,RESET as g,BOLD as T,YELLOW as J,REVERSE as Q,BG_ATTACHED as Z,GREEN as tt}from"../../../core/canvas/nav-render.js";const et=.72,st={anchor:"center",width:"72%",minWidth:48,maxHeight:"72%",margin:1},it=[["crtr.graph.dismiss","dismiss"],["crtr.graph.focus","focus"],["crtr.graph.revive","revive"],["crtr.graph.close-subtree","close"],["crtr.graph.focus-manager","manager"]],nt=[["crtr.graph.dismiss","dismiss"]],rt=[["crtr.graph.down","down"],["crtr.graph.up","up"],["crtr.graph.first","first"],["crtr.graph.last","last"],["crtr.graph.collapse-or-manager","fold/manager"],["crtr.graph.expand-or-child","expand/child"]];function ot(m){return{node_id:m.node_id,name:m.name,kind:m.kind,description:m.description??void 0,cycles:m.cycles??void 0,status:m.status,frozen_at:m.frozen_at,pi_pid:m.pi_pid,telemetry_context_tokens:m.telemetry_context_tokens,telemetry_last_activity:m.telemetry_last_activity,fault:m.fault??null,streaming:m.streaming}}p(ot,"graphRow");class $t{static{p(this,"GraphOverlay")}tui;self;getAsks;palette;bindings;source;role;onRefreshFailure;onNotice;handle;userExpanded=new Set;userCollapsed=new Set;folds={userExpanded:this.userExpanded,userCollapsed:this.userCollapsed};cursorId;scrollTop=0;pendingConfirm;snapshot;snapshotSeq=0;remote;constructor(e,s,i,t,n,c,d="controller",l=()=>{},r=()=>{}){this.tui=e,this.self=s,this.getAsks=i,this.palette=t,this.bindings=n,this.source=c,this.role=d,this.onRefreshFailure=l,this.onNotice=r,this.remote=c instanceof G}setBindings(e){this.bindings=e,this.handle!==void 0&&this.tui.requestRender()}matches(e,s){return H(this.bindings,e,s,(i,t)=>O(i,t))}hint(e){return e.flatMap(([s,i])=>this.bindings.gestures(s).length===0?[]:[`${P(this.bindings,s)} ${i}`])}fitHints(e,s){const t=[];let n=0;for(const c of this.hint(e)){const d=(t.length===0?0:3)+E(c);if(n+d>s)break;t.push(c),n+=d}return t.join(" \xB7 ")}isOpen(){return this.handle!==void 0}open(){this.handle===void 0&&(this.scrollTop=0,this.pendingConfirm=void 0,this.cursorId=this.self,this.handle=this.tui.showOverlay(this,st),this.refresh())}close(){this.handle!==void 0&&(this.pendingConfirm=void 0,this.handle.hide(),this.handle=void 0,this.tui.requestRender())}toggle(){this.handle===void 0?this.open():this.close()}refresh(){if(this.handle===void 0)return;const e=++this.snapshotSeq;this.buildSnapshot().then(s=>{e===this.snapshotSeq&&(this.snapshot=s,this.tui.requestRender())}).catch(s=>{this.onRefreshFailure(s),this.tui.requestRender()})}invalidate(){this.refresh()}async fetchInput(){if(this.remote){const t=await this.source.listNodes().catch(()=>[]);return{nodes:new Map(t.map(n=>[n.node_id,n])),subscriptions:p(async n=>(await this.source.subscriptionsOf(n)).map(c=>c.node_id),"subscriptions"),subscribers:p(async n=>(await this.source.subscribersOf(n)).map(c=>c.node_id),"subscribers"),focused:new Set}}const e=await F().canvasGraph(),s=new Map,i=new Map;for(const t of e.edges){const n=s.get(t.from_id);n===void 0?s.set(t.from_id,[t.to_id]):n.push(t.to_id);const c=i.get(t.to_id);c===void 0?i.set(t.to_id,[t.from_id]):c.push(t.from_id)}return{nodes:new Map(e.nodes.map(t=>[t.node_id,ot(t)])),subscriptions:p(t=>Promise.resolve(s.get(t)??[]),"subscriptions"),subscribers:p(t=>Promise.resolve(i.get(t)??[]),"subscribers"),focused:new Set(e.focused_node_ids)}}async buildSnapshot(){const e=await this.fetchInput(),{nodes:s,focused:i}=e,t=new Map,n=new Map,c=new Map,d=new Map,l=p(async o=>{const a=t.get(o);if(a!==void 0)return a;let f;try{f=await e.subscriptions(o)}catch{f=[]}const b=f.filter(w=>s.get(w)?.kind!=="human");return t.set(o,b),b},"children"),r=p(async o=>{if(c.has(o))return c.get(o);let a;try{a=(await e.subscribers(o))[0]}catch{a=void 0}return c.set(o,a),a},"manager"),h=p(async()=>{let o=this.self;const a=new Set([o]);for(;;){const f=await r(o);if(f===void 0||a.has(f))break;a.add(f),o=f}return o},"climbRoot"),u=p(async o=>{const a=n.get(o);if(a!==void 0)return a;const f=[...await l(o)];return f.sort((b,w)=>S(s.get(b)?.status)-S(s.get(w)?.status)),n.set(o,f),f},"sortedChildren"),R=new Set,$=new Set,y=p(async o=>{const a=s.get(o)?.status==="active";if($.has(o))return{active:a?1:0,reveal:a||o===this.self};$.add(o);let f=0,b=!1;for(const w of await l(o)){const x=await y(w);f+=x.active,x.reveal&&(b=!0)}return d.set(o,f),b&&R.add(o),{active:f+(a?1:0),reveal:b||a||o===this.self}},"visitActivity"),I=await h();await y(I),await Promise.all([...t.keys()].map(o=>u(o)));const _={root:I,rows:[],autoExpanded:R,activeBelow:d,nodes:s,childIds:t,sortedChildIds:n,managerOf:c,focused:i};return _.rows=this.projectRows(_),_}projectRows(e){const s=[],i=new Set,t=[{id:e.root,prefix:"",isRoot:!0,isLast:!0}];for(;t.length>0;){const{id:n,prefix:c,isRoot:d,isLast:l}=t.pop(),r=d?"":l?"\u2514\u2500 ":"\u251C\u2500 ";if(i.has(n)){s.push({id:n,hasKids:!1,isSelf:n===this.self,branch:c+r,cycle:!0,collapsed:!1});continue}i.add(n);const h=e.sortedChildIds.get(n)??[],u=this.folds.userExpanded.has(n)?!1:this.folds.userCollapsed.has(n)?!0:!e.autoExpanded.has(n);if(s.push({id:n,hasKids:h.length>0,isSelf:n===this.self,branch:c+r,cycle:!1,collapsed:u}),u)continue;const R=d?"":c+(l?" ":"\u2502 ");for(let $=h.length-1;$>=0;$--)t.push({id:h[$],prefix:R,isRoot:!1,isLast:$===h.length-1})}return s}repaintFolds(){this.snapshot!==void 0&&(this.snapshot.rows=this.projectRows(this.snapshot)),this.tui.requestRender()}renderSnapshotRow(e,s,i,t){const n=p((w,x)=>i?k(w,A(),Q):x?k(w,A(),Z):z(w),"wrap");if(s.cycle){const w=`${s.branch} ${v}\u21BA ${M(s.id)}${g}`;return n(w,!1)}const c=e.nodes.get(s.id)??null,d=W(c),l=K(c,s.id),r=s.isSelf?`${T}${l}${g}`:l,h=`${v}${c?.kind??""}${g}`,u=`${v}${j(c,this.remote)}${g}`,R=s.hasKids&&s.collapsed,$=!i&&R?`${v}\u25B8${g} `:" ",y=Y(c),I=e.childIds.get(s.id)?.length??0,_=c!==null&&I>0?` ${v}\u2933${I}${g}`:"",o=c!==null&&c.status!=="active"&&(e.activeBelow.get(c.node_id)??0)>0?` ${tt}\u21E3${e.activeBelow.get(c.node_id)??0}${g}`:"",a=c?.fault??null,f=a!==null?` \xB7 ${L(a)}`:"",b=`${s.branch}${$}${d} ${r} ${h} ${u}${y}${_}${o}${D(s.id,t)}${V(c,this.remote)}${f}`;return n(b,U(s.id,c,e.focused,this.remote))}render(e){const s=this.getAsks(),i=this.snapshot?.rows??[];let t=i.findIndex(o=>o.id===this.cursorId);t<0&&(t=i.findIndex(o=>o.id===this.self),t<0&&(t=0)),this.cursorId=i[t]?.id??this.self;const n=process.stdout.rows??X,c=Math.max(3,Math.min(Math.floor(n*et),n-2)),d=Math.max(2,Math.min(i.length,c-2));let l=d;for(let o=0;o<4;o++){t<this.scrollTop&&(this.scrollTop=t),t>=this.scrollTop+l&&(this.scrollTop=t-l+1),this.scrollTop=Math.max(0,Math.min(this.scrollTop,Math.max(0,i.length-l)));const a=d-(this.scrollTop>0?1:0)-(this.scrollTop+l<i.length?1:0);if(a===l)break;l=Math.max(1,a)}const r=Math.min(i.length,this.scrollTop+l),h=[];this.scrollTop>0&&h.push(`${v} \u2191 ${this.scrollTop} more${g}`);for(let o=this.scrollTop;o<r;o++){const a=this.snapshot;h.push(a!==void 0?this.renderSnapshotRow(a,i[o],o===t,s):"")}for(r<i.length&&h.push(`${v} \u2193 ${i.length-r} more${g}`);h.length<d;)h.push("");const u=i.length>0?`${T}\u2317 canvas graph${g} ${v}(${t+1}/${i.length})${g}`:`${T}\u2317 canvas graph${g} ${v}(loading)${g}`,R=this.pendingConfirm!==void 0?`${J}${this.pendingConfirm.label} ${T}${this.hint([["crtr.graph.confirm.accept","accept"],["crtr.graph.confirm.cancel","cancel"]]).join(" \xB7 ")}${g}`:`${v}${this.fitHints([...this.remote||this.role!=="controller"?nt:it,...rt],Math.max(1,e-5))}${g}`,$=this.palette.border,y=Math.max(1,e-4),I=h.map(o=>`${$("\u2502")} ${C(o,y,"",!0)} ${$("\u2502")}`),_=this.palette.surface;return[this.borderRow("\u256D","\u256E",u,e),...I,this.borderRow("\u2570","\u256F",R,e)].map(_)}borderRow(e,s,i,t){const n=this.palette.border,c=Math.max(1,t-5),d=C(i,c,"\u2026"),l=Math.max(0,t-5-E(d));return`${n(`${e}\u2500`)} ${d} ${n("\u2500".repeat(l)+s)}`}handleInput(e){try{this.dispatch(e)}catch{}}dispatch(e){if(this.pendingConfirm!==void 0){if(this.matches("crtr.graph.confirm.accept",e)){const r=this.pendingConfirm.action;this.pendingConfirm=void 0,r()}else this.pendingConfirm=void 0;this.tui.requestRender();return}if(this.matches("crtr.graph.dismiss",e)){this.close();return}const s=this.snapshot,i=s?.rows??[];let t=i.findIndex(r=>r.id===this.cursorId);t<0&&(t=Math.max(0,i.findIndex(r=>r.id===this.self)));const n=i[t],c=p(r=>r!==void 0&&!r.cycle,"isSelectable"),d=p((r,h)=>{for(let u=r+h;u>=0&&u<i.length;u+=h)if(c(i[u]))return u;return r},"step"),l=p(r=>{const h=r===1?0:i.length-1;for(let u=h;u>=0&&u<i.length;u+=r)if(c(i[u]))return u;return t},"edge");if(this.matches("crtr.graph.down",e)){t=d(t,1),this.cursorId=i[t]?.id??this.cursorId,this.tui.requestRender();return}if(this.matches("crtr.graph.up",e)){t=d(t,-1),this.cursorId=i[t]?.id??this.cursorId,this.tui.requestRender();return}if(this.matches("crtr.graph.first",e)){this.cursorId=i[l(1)]?.id??this.cursorId,this.tui.requestRender();return}if(this.matches("crtr.graph.last",e)){this.cursorId=i[l(-1)]?.id??this.cursorId,this.tui.requestRender();return}if(this.matches("crtr.graph.collapse-or-manager",e)){if(n!==void 0&&n.hasKids&&!n.collapsed)this.userCollapsed.add(n.id),this.userExpanded.delete(n.id),this.repaintFolds();else{const r=s?.managerOf.get(this.cursorId??this.self);r!==void 0&&i.some(h=>h.id===r)&&(this.cursorId=r),this.tui.requestRender()}return}if(this.matches("crtr.graph.expand-or-child",e)){if(n!==void 0&&n.collapsed&&n.hasKids)this.userExpanded.add(n.id),this.userCollapsed.delete(n.id),this.repaintFolds();else if(n!==void 0&&n.hasKids){const r=s?.sortedChildIds.get(n.id)?.[0];r!==void 0&&(this.cursorId=r),this.tui.requestRender()}return}if(this.matches("crtr.graph.focus",e)){if(this.remote)return;if(this.role!=="controller"){this.onNotice("Read-only observer attach \u2014 cannot focus nodes");return}this.cursorId!==void 0&&this.focusTarget(this.cursorId),this.close();return}if(this.matches("crtr.graph.revive",e)){if(this.remote)return;if(this.role!=="controller"){this.onNotice("Read-only observer attach \u2014 cannot revive nodes");return}const r=this.cursorId,h=r===void 0?null:s?.nodes.get(r)??null;r!==void 0&&h!==null&&h.status!=="active"&&h.status!=="idle"&&(this.reviveTarget(r),this.close());return}if(this.matches("crtr.graph.focus-manager",e)){if(this.remote)return;if(this.role!=="controller"){this.onNotice("Read-only observer attach \u2014 cannot focus nodes");return}const r=s?.managerOf.get(this.self);r!==void 0&&(this.focusTarget(r),this.close());return}if(this.matches("crtr.graph.close-subtree",e)){if(this.remote)return;if(this.role!=="controller"){this.onNotice("Read-only observer attach \u2014 cannot close nodes");return}const r=this.cursorId??this.self,h=s?.nodes.get(r)??null,u=h!==null?B(h):M(r);this.pendingConfirm={label:`kill ${u}?`,action:p(()=>this.shellCrtr(["node","lifecycle","close","--node",r]),"action")},this.tui.requestRender();return}}focusTarget(e){const s=["surface","node","focus",e],i=process.env.TMUX_PANE;i!==void 0&&i!==""&&s.push("--pane",i),this.shellCrtr(s)}reviveTarget(e){this.shellCrtr(["node","lifecycle","revive",e],()=>this.focusTarget(e))}shellCrtr(e,s){try{N("crtr",e,t=>{t==null&&s?.()}).stdin?.end()}catch{}}}export{$t as GraphOverlay};
1
+ var O=Object.defineProperty;var m=(u,t)=>O(u,"name",{value:t,configurable:!0});import{execFile as q}from"node:child_process";import{matchesKey as P,truncateToWidth as T}from"@earendil-works/pi-tui";import{formatBinding as H,matchesPiTuiInput as L}from"../../../core/keybindings/index.js";import{fullName as B}from"../../../core/canvas/labels.js";import{faultSummary as G,resolveNodeVisual as C}from"../../../core/canvas/status-glyph.js";import{RemoteCanvasSource as W}from"../../../core/canvas/remote-canvas-source.js";import{cliClient as F}from"../../../commands/api-client.js";import{statusRank as M}from"../../../core/canvas/node-order.js";import{navLabel as K,nodeGlyph as j,tokensCell as D,cycleBadge as V,askBadge as Y,activityCell as z,shortId as E,visibleWidth as k,fillBar as A,fillWidth as N,truncate as U,isAttached as X,VIEWPORT_FALLBACK_ROWS as J,ESC as Q,DIM as R,RESET as g,BOLD as S,YELLOW as Z,REVERSE as ee,BG_ATTACHED as te,GREEN as se}from"../../../core/canvas/nav-render.js";const ie=.72,ne={anchor:"center",width:"72%",minWidth:48,maxHeight:"72%",margin:1},re=[["crtr.graph.dismiss","dismiss"],["crtr.graph.focus","focus"],["crtr.graph.revive","revive"],["crtr.graph.close-subtree","close"],["crtr.graph.focus-manager","manager"]],oe=[["crtr.graph.dismiss","dismiss"]],ce=[["crtr.graph.down","down"],["crtr.graph.up","up"],["crtr.graph.first","first"],["crtr.graph.last","last"],["crtr.graph.collapse-or-manager","fold/manager"],["crtr.graph.expand-or-child","expand/child"]],ae=[...["active","idle","done","dead","canceled"].map(u=>[C(u,{}),u]),[C("active",{streaming:!0}),"live"],[C("active",{frozen:!0}),"frozen"],[C("active",{hanging:{kind:"wedged"}}),"fault"]].map(([u,t])=>`${Q}${u.color}m${u.glyph}${g} ${R}${t}${g}`).join(" ");function he(u){return{node_id:u.node_id,name:u.name,kind:u.kind,description:u.description??void 0,cycles:u.cycles??void 0,status:u.status,frozen_at:u.frozen_at,pi_pid:u.pi_pid,telemetry_context_tokens:u.telemetry_context_tokens,telemetry_last_activity:u.telemetry_last_activity,fault:u.fault??null,streaming:u.streaming}}m(he,"graphRow");class be{static{m(this,"GraphOverlay")}tui;self;getAsks;palette;bindings;source;role;onRefreshFailure;onNotice;handle;userExpanded=new Set;userCollapsed=new Set;folds={userExpanded:this.userExpanded,userCollapsed:this.userCollapsed};cursorId;scrollTop=0;pendingConfirm;snapshot;snapshotSeq=0;remote;constructor(t,s,i,e,n,o,f="controller",p=()=>{},r=()=>{}){this.tui=t,this.self=s,this.getAsks=i,this.palette=e,this.bindings=n,this.source=o,this.role=f,this.onRefreshFailure=p,this.onNotice=r,this.remote=o instanceof W}setBindings(t){this.bindings=t,this.handle!==void 0&&this.tui.requestRender()}matches(t,s){return L(this.bindings,t,s,(i,e)=>P(i,e))}hint(t){return t.flatMap(([s,i])=>this.bindings.gestures(s).length===0?[]:[`${H(this.bindings,s)} ${i}`])}fitHints(t,s){const e=[];let n=0;for(const o of this.hint(t)){const f=(e.length===0?0:3)+k(o);if(n+f>s)break;e.push(o),n+=f}return e.join(" \xB7 ")}isOpen(){return this.handle!==void 0}open(){this.handle===void 0&&(this.scrollTop=0,this.pendingConfirm=void 0,this.cursorId=this.self,this.handle=this.tui.showOverlay(this,ne),this.refresh())}close(){this.handle!==void 0&&(this.pendingConfirm=void 0,this.handle.hide(),this.handle=void 0,this.tui.requestRender())}toggle(){this.handle===void 0?this.open():this.close()}refresh(){if(this.handle===void 0)return;const t=++this.snapshotSeq;this.buildSnapshot().then(s=>{t===this.snapshotSeq&&(this.snapshot=s,this.tui.requestRender())}).catch(s=>{this.onRefreshFailure(s),this.tui.requestRender()})}invalidate(){this.refresh()}async fetchInput(){if(this.remote){const e=await this.source.listNodes().catch(()=>[]);return{nodes:new Map(e.map(n=>[n.node_id,n])),subscriptions:m(async n=>(await this.source.subscriptionsOf(n)).map(o=>o.node_id),"subscriptions"),subscribers:m(async n=>(await this.source.subscribersOf(n)).map(o=>o.node_id),"subscribers"),focused:new Set}}const t=await F().canvasGraph(),s=new Map,i=new Map;for(const e of t.edges){const n=s.get(e.from_id);n===void 0?s.set(e.from_id,[e.to_id]):n.push(e.to_id);const o=i.get(e.to_id);o===void 0?i.set(e.to_id,[e.from_id]):o.push(e.from_id)}return{nodes:new Map(t.nodes.map(e=>[e.node_id,he(e)])),subscriptions:m(e=>Promise.resolve(s.get(e)??[]),"subscriptions"),subscribers:m(e=>Promise.resolve(i.get(e)??[]),"subscribers"),focused:new Set(t.focused_node_ids)}}async buildSnapshot(){const t=await this.fetchInput(),{nodes:s,focused:i}=t,e=new Map,n=new Map,o=new Map,f=new Map,p=m(async c=>{const l=e.get(c);if(l!==void 0)return l;let a;try{a=await t.subscriptions(c)}catch{a=[]}const $=a.filter(b=>s.get(b)?.kind!=="human");return e.set(c,$),$},"children"),r=m(async c=>{if(o.has(c))return o.get(c);let l;try{l=(await t.subscribers(c))[0]}catch{l=void 0}return o.set(c,l),l},"manager"),h=m(async()=>{let c=this.self;const l=new Set([c]);for(;;){const a=await r(c);if(a===void 0||l.has(a))break;l.add(a),c=a}return c},"climbRoot"),d=m(async c=>{const l=n.get(c);if(l!==void 0)return l;const a=[...await p(c)];return a.sort(($,b)=>M(s.get($)?.status)-M(s.get(b)?.status)),n.set(c,a),a},"sortedChildren"),w=new Set,v=new Set,I=m(async c=>{const l=s.get(c)?.status==="active";if(v.has(c))return{active:l?1:0,reveal:l||c===this.self};v.add(c);let a=0,$=!1;for(const b of await p(c)){const x=await I(b);a+=x.active,x.reveal&&($=!0)}return f.set(c,a),$&&w.add(c),{active:a+(l?1:0),reveal:$||l||c===this.self}},"visitActivity"),_=await h();await I(_),await Promise.all([...e.keys()].map(c=>d(c)));const y={root:_,rows:[],autoExpanded:w,activeBelow:f,nodes:s,childIds:e,sortedChildIds:n,managerOf:o,focused:i};return y.rows=this.projectRows(y),y}projectRows(t){const s=[],i=new Set,e=[{id:t.root,prefix:"",isRoot:!0,isLast:!0}];for(;e.length>0;){const{id:n,prefix:o,isRoot:f,isLast:p}=e.pop(),r=f?"":p?"\u2514\u2500 ":"\u251C\u2500 ";if(i.has(n)){s.push({id:n,hasKids:!1,isSelf:n===this.self,branch:o+r,cycle:!0,collapsed:!1});continue}i.add(n);const h=t.sortedChildIds.get(n)??[],d=this.folds.userExpanded.has(n)?!1:this.folds.userCollapsed.has(n)?!0:!t.autoExpanded.has(n);if(s.push({id:n,hasKids:h.length>0,isSelf:n===this.self,branch:o+r,cycle:!1,collapsed:d}),d)continue;const w=f?"":o+(p?" ":"\u2502 ");for(let v=h.length-1;v>=0;v--)e.push({id:h[v],prefix:w,isRoot:!1,isLast:v===h.length-1})}return s}repaintFolds(){this.snapshot!==void 0&&(this.snapshot.rows=this.projectRows(this.snapshot)),this.tui.requestRender()}renderSnapshotRow(t,s,i,e){const n=m((b,x)=>i?A(b,N(),ee):x?A(b,N(),te):U(b),"wrap");if(s.cycle){const b=`${s.branch} ${R}\u21BA ${E(s.id)}${g}`;return n(b,!1)}const o=t.nodes.get(s.id)??null,f=j(o),p=K(o,s.id),r=s.isSelf?`${S}${p}${g}`:p,h=`${R}${o?.kind??""}${g}`,d=`${R}${D(o,this.remote)}${g}`,w=s.hasKids&&s.collapsed,v=!i&&w?`${R}\u25B8${g} `:" ",I=V(o),_=t.childIds.get(s.id)?.length??0,y=o!==null&&_>0?` ${R}\u2933${_}${g}`:"",c=o!==null&&o.status!=="active"&&(t.activeBelow.get(o.node_id)??0)>0?` ${se}\u21E3${t.activeBelow.get(o.node_id)??0}${g}`:"",l=o?.fault??null,a=l!==null?` \xB7 ${G(l)}`:"",$=`${s.branch}${v}${f} ${r} ${h} ${d}${I}${y}${c}${Y(s.id,e)}${z(o,this.remote)}${a}`;return n($,X(s.id,o,t.focused,this.remote))}render(t){const s=this.getAsks(),i=this.snapshot?.rows??[];let e=i.findIndex(a=>a.id===this.cursorId);e<0&&(e=i.findIndex(a=>a.id===this.self),e<0&&(e=0)),this.cursorId=i[e]?.id??this.self;const n=process.stdout.rows??J,o=Math.max(3,Math.min(Math.floor(n*ie),n-2)),f=Math.min(o-2,Math.max(2,i.length)),p=f-1,r=p>=3;let h=p;for(let a=0;p>0&&a<4;a++){e<this.scrollTop&&(this.scrollTop=e),e>=this.scrollTop+h&&(this.scrollTop=e-h+1),this.scrollTop=Math.max(0,Math.min(this.scrollTop,Math.max(0,i.length-h)));const $=p-(r&&this.scrollTop>0?1:0)-(r&&this.scrollTop+h<i.length?1:0);if($===h)break;h=Math.max(1,$)}const d=Math.min(i.length,this.scrollTop+h),w=[];r&&this.scrollTop>0&&w.push(`${R} \u2191 ${this.scrollTop} more${g}`);for(let a=this.scrollTop;a<d;a++){const $=this.snapshot;w.push($!==void 0?this.renderSnapshotRow($,i[a],a===e,s):"")}for(r&&d<i.length&&w.push(`${R} \u2193 ${i.length-d} more${g}`),w.unshift(ae);w.length<f;)w.push("");const v=i.length>0?`${S}\u2317 canvas graph${g} ${R}(${e+1}/${i.length})${g}`:`${S}\u2317 canvas graph${g} ${R}(loading)${g}`,I=this.pendingConfirm!==void 0?`${Z}${this.pendingConfirm.label} ${S}${this.hint([["crtr.graph.confirm.accept","accept"],["crtr.graph.confirm.cancel","cancel"]]).join(" \xB7 ")}${g}`:`${R}${this.fitHints([...this.remote||this.role!=="controller"?oe:re,...ce],Math.max(1,t-5))}${g}`,_=this.palette.border,y=Math.max(1,t-4),c=w.map(a=>`${_("\u2502")} ${T(a,y,"",!0)} ${_("\u2502")}`),l=this.palette.surface;return[this.borderRow("\u256D","\u256E",v,t),...c,this.borderRow("\u2570","\u256F",I,t)].map(l)}borderRow(t,s,i,e){const n=this.palette.border,o=Math.max(1,e-5),f=T(i,o,"\u2026"),p=Math.max(0,e-5-k(f));return`${n(`${t}\u2500`)} ${f} ${n("\u2500".repeat(p)+s)}`}handleInput(t){try{this.dispatch(t)}catch{}}dispatch(t){if(this.pendingConfirm!==void 0){if(this.matches("crtr.graph.confirm.accept",t)){const r=this.pendingConfirm.action;this.pendingConfirm=void 0,r()}else this.pendingConfirm=void 0;this.tui.requestRender();return}if(this.matches("crtr.graph.dismiss",t)){this.close();return}const s=this.snapshot,i=s?.rows??[];let e=i.findIndex(r=>r.id===this.cursorId);e<0&&(e=Math.max(0,i.findIndex(r=>r.id===this.self)));const n=i[e],o=m(r=>r!==void 0&&!r.cycle,"isSelectable"),f=m((r,h)=>{for(let d=r+h;d>=0&&d<i.length;d+=h)if(o(i[d]))return d;return r},"step"),p=m(r=>{const h=r===1?0:i.length-1;for(let d=h;d>=0&&d<i.length;d+=r)if(o(i[d]))return d;return e},"edge");if(this.matches("crtr.graph.down",t)){e=f(e,1),this.cursorId=i[e]?.id??this.cursorId,this.tui.requestRender();return}if(this.matches("crtr.graph.up",t)){e=f(e,-1),this.cursorId=i[e]?.id??this.cursorId,this.tui.requestRender();return}if(this.matches("crtr.graph.first",t)){this.cursorId=i[p(1)]?.id??this.cursorId,this.tui.requestRender();return}if(this.matches("crtr.graph.last",t)){this.cursorId=i[p(-1)]?.id??this.cursorId,this.tui.requestRender();return}if(this.matches("crtr.graph.collapse-or-manager",t)){if(n!==void 0&&n.hasKids&&!n.collapsed)this.userCollapsed.add(n.id),this.userExpanded.delete(n.id),this.repaintFolds();else{const r=s?.managerOf.get(this.cursorId??this.self);r!==void 0&&i.some(h=>h.id===r)&&(this.cursorId=r),this.tui.requestRender()}return}if(this.matches("crtr.graph.expand-or-child",t)){if(n!==void 0&&n.collapsed&&n.hasKids)this.userExpanded.add(n.id),this.userCollapsed.delete(n.id),this.repaintFolds();else if(n!==void 0&&n.hasKids){const r=s?.sortedChildIds.get(n.id)?.[0];r!==void 0&&(this.cursorId=r),this.tui.requestRender()}return}if(this.matches("crtr.graph.focus",t)){if(this.remote)return;if(this.role!=="controller"){this.onNotice("Read-only observer attach \u2014 cannot focus nodes");return}this.cursorId!==void 0&&this.focusTarget(this.cursorId),this.close();return}if(this.matches("crtr.graph.revive",t)){if(this.remote)return;if(this.role!=="controller"){this.onNotice("Read-only observer attach \u2014 cannot revive nodes");return}const r=this.cursorId,h=r===void 0?null:s?.nodes.get(r)??null;r!==void 0&&h!==null&&h.status!=="active"&&h.status!=="idle"&&(this.reviveTarget(r),this.close());return}if(this.matches("crtr.graph.focus-manager",t)){if(this.remote)return;if(this.role!=="controller"){this.onNotice("Read-only observer attach \u2014 cannot focus nodes");return}const r=s?.managerOf.get(this.self);r!==void 0&&(this.focusTarget(r),this.close());return}if(this.matches("crtr.graph.close-subtree",t)){if(this.remote)return;if(this.role!=="controller"){this.onNotice("Read-only observer attach \u2014 cannot close nodes");return}const r=this.cursorId??this.self,h=s?.nodes.get(r)??null,d=h!==null?B(h):E(r);this.pendingConfirm={label:`kill ${d}?`,action:m(()=>this.shellCrtr(["node","lifecycle","close","--node",r]),"action")},this.tui.requestRender();return}}focusTarget(t){const s=["surface","node","focus",t],i=process.env.TMUX_PANE;i!==void 0&&i!==""&&s.push("--pane",i),this.shellCrtr(s)}reviveTarget(t){this.shellCrtr(["node","lifecycle","revive",t],()=>this.focusTarget(t))}shellCrtr(t,s){try{q("crtr",t,e=>{e==null&&s?.()}).stdin?.end()}catch{}}}export{be as GraphOverlay};