@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,112 @@
1
+ ---
2
+ kind: knowledge
3
+ when-and-why-to-read: When you need Errors, this knowledge should be read
4
+ because **An agent's own outcome is returned, never thrown.** A run that
5
+ declined the schema, hit its deadline, or crashed comes back as a settled
6
+ `NodeOutcome` from `waitForOutcome`, `createAndWait`, or `parse`. Narrow on it
7
+ — see [[crouter-sdk/nodes]].
8
+ ---
9
+
10
+ # Errors
11
+
12
+ ## What throws and what does not
13
+
14
+ **An agent's own outcome is returned, never thrown.** A run that declined the schema, hit its deadline, or crashed comes back as a settled `NodeOutcome` from `waitForOutcome`, `createAndWait`, or `parse`. Narrow on it — see [[crouter-sdk/nodes]].
15
+
16
+ Exceptions are for the layer underneath: the SDK rejected an invalid local identifier or configuration, the daemon refused the request, the connection failed, the request timed out, or you aborted.
17
+
18
+ ## Classes and mappings
19
+
20
+ Every class below extends `APIError`, which carries `status`, `code`, `message`, `details`, and `headers`.
21
+
22
+ | Condition | Class |
23
+ |---|---|
24
+ | SDK configuration error | `CrouterError` with `status: 0`, `code: 'crouter_error'` |
25
+ | HTTP 400 | `BadRequestError` |
26
+ | HTTP 401 | `AuthenticationError` |
27
+ | HTTP 403 | `PermissionDeniedError` |
28
+ | HTTP 404 | `NotFoundError` |
29
+ | HTTP 409 | `ConflictError` |
30
+ | HTTP 413, 422 | `UnprocessableEntityError` |
31
+ | HTTP 429 | `RateLimitError` |
32
+ | HTTP 504 or code `request_timeout` | `APIConnectionTimeoutError` (extends `APIConnectionError`) |
33
+ | Other HTTP ≥ 500 | `InternalServerError` |
34
+ | Code `daemon_unavailable`, `transport_error`, `daemon_request_interrupted`, or `daemon_health_unavailable`; or a non-API transport failure | `APIConnectionError` |
35
+ | Code `request_aborted` | `APIUserAbortError` |
36
+ | Any other daemon response | `APIError` |
37
+
38
+ A terminal SSE `error` event other than `stream_gap` rejects `NodeStream.finalOutcome()` and the iterator with `APIError` carrying that event's code, message, and details. `stream_gap` is delivered as an event and the stream continues. See [[crouter-sdk/streaming]].
39
+
40
+ ```ts
41
+ import { ConflictError, NotFoundError, APIConnectionError } from '@north-light/crouter-sdk';
42
+
43
+ try {
44
+ await client.nodes.create({ prompt, node_id: 'my-run-42' });
45
+ } catch (error) {
46
+ if (error instanceof ConflictError && error.code === 'node_id_exists') {
47
+ // the run already exists — attach to it instead of spawning a second one
48
+ return client.nodes.waitForOutcome('my-run-42');
49
+ }
50
+ if (error instanceof NotFoundError) throw new Error('no such node');
51
+ if (error instanceof APIConnectionError) throw new Error('the daemon is not reachable');
52
+ throw error;
53
+ }
54
+ ```
55
+
56
+ The subclasses add **no fields**. `status` and `code` on the base class already decide everything there is to branch on. They exist so that `catch (error) { if (error instanceof NotFoundError) … }` — what an OpenAI SDK user writes without thinking about it — works here too.
57
+
58
+ ## Identifier validation
59
+
60
+ The SDK validates path-segment identifiers before it makes a request. Invalid node ids, cron ids, bash-job ids, human-request ids, inbox ticket ids, provider names, and profile names throw `TypeError` locally. Node ids apply to core node calls, lifecycle calls, and nested node resources. `nodes.events()` returns a `NodeStream` synchronously, so its invalid-id `TypeError` rejects `stream.node`, `stream.finalOutcome()`, and iteration instead. File paths are not identifiers; invalid or relative file paths reach the daemon and return its mapped API error. `nodes.outcome(id, { wait })` separately throws `RangeError` when `wait` is not an integer from 0 through 25.
61
+
62
+ ## Memory requests
63
+
64
+ | Condition | Class, status, and code |
65
+ |---|---|
66
+ | `retrieve` or `resolve` names a document that does not exist, including a document deleted earlier | `NotFoundError`, 404 `memory_document_not_found` |
67
+ | `history` names a document with no document or revision history | `NotFoundError`, 404 `not_found` |
68
+ | History is requested for a builtin or plugin document | `BadRequestError`, 400 `usage` |
69
+ | A mutation directly sets protected frontmatter (`kind`, `when-and-why-to-read`, `origin`, or `last-updated`) | `BadRequestError`, 400 `invalid_request` |
70
+ | An update would make no change | `BadRequestError`, 400 `usage` |
71
+ | A node-targeted caller lacks `memory:read` or `memory:write` | `PermissionDeniedError`, 403 `scope_denied` |
72
+
73
+ Invalid names, invalid scope combinations, invalid limits, builtin mutation selections, and invalid search combinations are also `BadRequestError` responses. Builtin and plugin documents cannot be mutated; no code should treat a 400 refusal as a successful write.
74
+
75
+ ## The wire body
76
+
77
+ The daemon answers an error with:
78
+
79
+ ```json
80
+ { "error": { "code": "node_id_exists", "message": "…", "details": { } } }
81
+ ```
82
+
83
+ `code` is the stable, machine-readable identity — `node_id_exists`, `daemon_unavailable`, `request_timeout`, `node_dormant`. Branch on it. There is no separate `type` field and no `param` field.
84
+
85
+ ## Retries
86
+
87
+ `maxRetries` defaults to `2`, with exponential backoff. It applies to:
88
+
89
+ - connection errors, and
90
+ - HTTP 429 and 5xx responses, **on `GET`, `HEAD`, and `DELETE` only**.
91
+
92
+ **`POST` and `PATCH` are never retried automatically.** Creating a node, delivering a message, and pushing a report are not idempotent, and a mutation whose response was interrupted may already have been applied — replaying it would spawn a second agent, deliver a second message, or push a second report.
93
+
94
+ If you want a create that survives a retry, pass `node_id`. A duplicate then fails loudly with `409 node_id_exists` instead of quietly running twice:
95
+
96
+ ```ts
97
+ const runId = `invoice-${invoice.id}`;
98
+
99
+ try {
100
+ await client.nodes.create({ prompt, node_id: runId, root: true });
101
+ } catch (error) {
102
+ if (!(error instanceof ConflictError && error.code === 'node_id_exists')) throw error;
103
+ }
104
+
105
+ const outcome = await client.nodes.waitForOutcome(runId);
106
+ ```
107
+
108
+ Override the policy per request with `{ maxRetries: 0 }` or `{ maxRetries: 5 }`. Raising it on a `POST` still does not make the client retry that `POST`.
109
+
110
+ ## In-repo callers
111
+
112
+ `ApiError` from `@north-light/crouter-api` is an alias of `APIError`, so existing crouter code and its transport checks keep working unchanged. Daemon handlers keep throwing `{ status, code, message }` and never name an SDK class — the hierarchy is one `status → constructor` table on the client side and nothing else depends on it.
@@ -0,0 +1,51 @@
1
+ ---
2
+ kind: knowledge
3
+ when-and-why-to-read: When you need `client.files`, this knowledge should be
4
+ read because `client.files` reads, writes, and lists host files through the
5
+ daemon.
6
+ ---
7
+
8
+ # `client.files`
9
+
10
+ `client.files` reads, writes, and lists host files through the daemon.
11
+
12
+ ```ts
13
+ const source = await client.files.read('/absolute/path/to/input.txt');
14
+ const written = await client.files.write('/absolute/path/to/output.txt', source.content);
15
+ const directory = await client.files.list('/absolute/path/to');
16
+ ```
17
+
18
+ Every `path` is absolute. Relative paths and `~` are not expanded. File paths are sent to the daemon unchanged; the daemon checks that they are absolute and returns a mapped API error when they are not.
19
+
20
+ Every method accepts ordinary request options in its final object: `headers`, `signal`, `timeout`, and `maxRetries`. `read` and `write` add `encoding`; `list` adds `limit`.
21
+
22
+ ## Read
23
+
24
+ ```ts
25
+ await client.files.read(path, { encoding: 'utf8', timeout: 5_000 });
26
+ await client.files.read(path, { encoding: 'base64' });
27
+ ```
28
+
29
+ `read(path, options?)` returns `Promise<FilePeekDTO>`, `{ path, content, truncated }`, through `GET /v1/files/peek`. It captures at most 512 KiB of file bytes. `truncated: true` means `content` is only the prefix. Request `base64` for binary content.
30
+
31
+ ## Write
32
+
33
+ ```ts
34
+ const result = await client.files.write(path, content, { encoding: 'utf8' });
35
+ // result is { path, bytes_written }
36
+ await client.files.write(path, bytes.toString('base64'), { encoding: 'base64' });
37
+ ```
38
+
39
+ `write(path, content, options?)` returns `Promise<FileWriteDTO>`, `{ path, bytes_written }`. Writes create parent directories and atomically replace the target. If `path` is an existing symlink, the daemon resolves it and atomically replaces its destination; the symlink remains. Decoded content above 1 MiB is refused with `UnprocessableEntityError` carrying `status: 413` and `code: 'file_too_large'`; it is never truncated or partially written.
40
+
41
+ ## List
42
+
43
+ ```ts
44
+ const result = await client.files.list(path, { limit: 100 });
45
+ ```
46
+
47
+ `list(path, options?)` returns `Promise<FileListDTO>`, `{ path, entries, truncated }`, for one directory level. Each entry has `name`, `type: 'file' | 'dir' | 'other'`, `size`, and ISO-8601 `modified`. `limit` must be a positive integer; the default is 1000. `truncated: true` means the directory had more entries than the requested limit.
48
+
49
+ ## Authority
50
+
51
+ The token is the owner credential. Anyone holding it can already create an agent with a bash tool, so these methods run with the daemon user's authority and do not provide a restricted file area.
@@ -0,0 +1,169 @@
1
+ ---
2
+ kind: knowledge
3
+ when-and-why-to-read: "When you need Getting started, this knowledge should be
4
+ read because An agent run needs a crouter daemon (`crtrd`) to run it. The SDK
5
+ is a client for that daemon — it never runs an agent itself. There are two
6
+ ways to reach one: the unix socket on the machine you are running on, or a TCP
7
+ listener with a bearer token."
8
+ ---
9
+
10
+ # Getting started
11
+
12
+ Phase 1.
13
+
14
+ An agent run needs a crouter daemon (`crtrd`) to run it. The SDK is a client for that daemon — it never runs an agent itself. There are two ways to reach one: the unix socket on the machine you are running on, or a TCP listener with a bearer token.
15
+
16
+ ## 1. Install the runtime
17
+
18
+ ```bash
19
+ npm i -g @north-light/crouter
20
+ ```
21
+
22
+ That installs the `crtr` CLI and the `crtrd` daemon. You do not have to start the daemon: `new Crouter()` starts it for you on a cold socket (see `autostart` in [[crouter-sdk/client]]).
23
+
24
+ ## 2. Install the SDK in your application
25
+
26
+ ```bash
27
+ npm i @north-light/crouter-sdk
28
+ ```
29
+
30
+ One dependency. It re-exports every data type you need, so you do not also install `@north-light/crouter-api`.
31
+
32
+ ## 3. Node, on the owner's own daemon
33
+
34
+ Pass nothing. The client resolves the same socket path the `crtr` CLI does, and starts the daemon if it is not already up.
35
+
36
+ ```ts
37
+ import Crouter from '@north-light/crouter-sdk';
38
+
39
+ const client = new Crouter();
40
+
41
+ const node = await client.nodes.create({
42
+ prompt: 'List the top-level directories here and say what each one is for.',
43
+ cwd: process.cwd(),
44
+ root: true,
45
+ root_lifecycle: 'terminal',
46
+ });
47
+
48
+ const outcome = await client.nodes.waitForOutcome(node.node_id);
49
+ if (outcome.kind === 'result') console.log(outcome.final_report_path);
50
+ ```
51
+
52
+ `root: true` says this run has no parent node and reports to nobody — which is what an application's run is. `root_lifecycle: 'terminal'` says it finishes and reaps; use `'resident'` for a run a person will open and keep talking to.
53
+
54
+ ## 4. A browser or a remote application
55
+
56
+ A process that is not on the daemon's machine — or a page in a browser, which has no unix sockets at all — reaches the daemon over TCP with a bearer token. The token is the owner credential: whoever holds it can drive the whole surface.
57
+
58
+ ### Turn the listener on, once
59
+
60
+ On the machine running the daemon:
61
+
62
+ ```bash
63
+ crtr sys connect
64
+ ```
65
+
66
+ This stores a listener address and generates a token if either is missing, then prints the base URL and token before handing the daemon over to a successor that boots with the listener on. It then logs in to the selected model provider when that provider has no usable credential. Run it again and, when the listener is up and that provider is ready, it prints the credentials and changes nothing.
67
+
68
+ The command returns `base_url` and `token`. Use JSON output when a program or a setup UI needs those exact fields:
69
+
70
+ ```text
71
+ $ crtr --json sys connect
72
+ {"base_url":"http://127.0.0.1:8787","token":"<64-character bearer token>"}
73
+ ```
74
+
75
+ ### Check setup before generating
76
+
77
+ Construct the client from the application's saved connection. On the first visit that value is absent, and `client.auth.status()` returns `'connect'` without a request. Once the user pastes the base URL and token printed by `crtr sys connect`, construct it again and call `status()` to check the selected provider.
78
+
79
+ ```ts
80
+ import Crouter from '@north-light/crouter-sdk';
81
+
82
+ const saved = loadConnection(); // { baseURL: string; token: string } | null
83
+ const client = new Crouter(saved ?? {});
84
+ const status = await client.auth.status({ profile: 'my-app', cwd: '/path/to/repo' });
85
+
86
+ if (status.next_step !== null) {
87
+ showSetupPanel({ step: status.next_step, instructions: status.instructions! });
88
+ return;
89
+ }
90
+
91
+ const run = await client.nodes.createAndWait({
92
+ prompt: 'What changed in this repo today?',
93
+ cwd: '/path/to/repo',
94
+ profile: 'my-app',
95
+ root: true,
96
+ });
97
+ ```
98
+
99
+ `status()` returns `next_step: 'connect'` when the application has no daemon transport, when the saved bearer token is rejected, or when the saved base URL cannot be reached. It returns `next_step: 'login'` when the daemon resolves the selected run to a provider without a ready credential. In both cases, display `instructions`: it names the exact `crtr sys connect` action and, for a login, the provider. When `status()` receives `profile`, `cwd`, `kind`, or `model`, the login instruction passes those selectors to `crtr sys connect`; use the text unchanged. A daemon that is still starting is connected with `next_step: null`; inspect `status.daemon.startup_phase` and wait for it to become `ready` before starting a run.
100
+
101
+ An isolated auth-status probe returned:
102
+
103
+ ```json
104
+ {
105
+ "no_request": {
106
+ "next_step": "connect",
107
+ "requests": 0
108
+ },
109
+ "missing": {
110
+ "provider": "anthropic",
111
+ "credential": "missing",
112
+ "next_step": "login",
113
+ "instructions": "Run `crtr sys connect --cwd /private/tmp/crouter-sdk-auth-proof.P3fSWg/empty --model anthropic/claude-opus-5` to log in to anthropic."
114
+ },
115
+ "ready": {
116
+ "provider": "anthropic",
117
+ "credential": "ready",
118
+ "next_step": null
119
+ }
120
+ }
121
+ ```
122
+
123
+ The root entry of the SDK is browser-safe: it imports nothing from `node:*`. The unix-socket code is reached only through a dynamic import taken when `socketPath` is set, so a browser bundle never resolves it.
124
+
125
+ ### Cross-origin calls
126
+
127
+ The daemon answers the browser's `OPTIONS` preflight with `Access-Control-Allow-Origin: *` and allows the `authorization` and `content-type` headers, and every authorized response carries the same origin header. This happens **only when a token is set** — an unauthenticated listener stays same-origin, because otherwise any page the user visits could drive their daemon.
128
+
129
+ The credential is a header your application already holds, never a cookie, so the daemon sends no `Access-Control-Allow-Credentials`.
130
+
131
+ ### Chrome's local-network permission
132
+
133
+ A page served over HTTPS that calls `http://localhost:<port>` is governed by Chrome's Local Network Access permission. The browser prompts the user once per site; the daemon needs **no special response header** for it — ordinary CORS plus a secure context is the whole requirement. See [developer.chrome.com/blog/local-network-access](https://developer.chrome.com/blog/local-network-access).
134
+
135
+ Tell your users what the prompt is for. A prompt that appears with no explanation gets dismissed, and the dismissal is sticky.
136
+
137
+ ## 5. A typed result
138
+
139
+ `parse()` creates the run, waits for it to settle, and types the structured result against the schema you gave it.
140
+
141
+ ```ts
142
+ import { z } from 'zod';
143
+
144
+ const run = await client.nodes.parse({
145
+ prompt: 'Read package.json here and report its name and version.',
146
+ cwd: '/path/to/repo',
147
+ output_schema: z.object({ name: z.string(), version: z.string() }),
148
+ });
149
+
150
+ if (run.kind === 'result') {
151
+ console.log(run.output_parsed.name, run.output_parsed.version);
152
+ } else if (run.reason === 'declined') {
153
+ console.warn('the agent refused the schema:', run.declined?.reason);
154
+ } else {
155
+ console.error('the run failed:', run.reason, run.detail);
156
+ }
157
+ ```
158
+
159
+ `output_parsed` is non-null on a result. A Zod schema supplies its inferred output type; a Standard Schema supplies `~standard.types.output`. A JSON-Schema literal or any other `{ toJSONSchema() }` object is accepted but makes `output_parsed` `unknown`.
160
+
161
+ An outcome is returned, never thrown — including a decline and a failure. Only transport faults, daemon errors, and your own abort throw. See [[crouter-sdk/errors]].
162
+
163
+ ## Where to go next
164
+
165
+ - Every constructor option and environment-variable fallback: [[crouter-sdk/client]]
166
+ - Checking the daemon connection and selected provider before a run: `client.auth.status()` above
167
+ - The full create-parameter table and the outcome union: [[crouter-sdk/nodes]]
168
+ - Watching a run as it works: [[crouter-sdk/streaming]]
169
+ - Running the daemon in a container instead: [[crouter-sdk/docker]]
@@ -0,0 +1,88 @@
1
+ ---
2
+ kind: knowledge
3
+ when-and-why-to-read: When you need `client.memory`, this knowledge should be
4
+ read because Phase 3. `client.memory` reads and writes the memory documents
5
+ that shape an agent run. Every call names its target; the daemon never uses
6
+ its own working directory or environment to select memory.
7
+ ---
8
+
9
+ # `client.memory`
10
+
11
+ Phase 3. `client.memory` reads and writes the memory documents that shape an agent run. Every call names its target; the daemon never uses its own working directory or environment to select memory.
12
+
13
+ ```ts
14
+ const page = await client.memory.list({ node: nodeId, kind: 'knowledge', limit: 50 });
15
+ const document = await client.memory.retrieve('insights/capture', { node: nodeId, frontmatter: true });
16
+ ```
17
+
18
+ ## Target
19
+
20
+ ```ts
21
+ type MemoryScope = {
22
+ node?: string;
23
+ cwd?: string;
24
+ profile?: string;
25
+ store?: 'node' | 'project' | 'profile' | 'user' | 'builtin';
26
+ };
27
+ ```
28
+
29
+ Pass `node` to use that node's working directory and profile. Do not combine it with `cwd` or `profile`. With no target, only user and builtin memory are searched. `store` restricts the request to one memory tier; `store: 'node'` requires `node`, and `store: 'profile'` requires a profile target. `create` defaults to the nearest project store when one is available, otherwise the user store. `update`, `delete`, and `move` resolve the existing document through normal scope precedence when `store` is omitted. `builtin` is read-only.
30
+
31
+ When a request supplies `node`, its node row must hold `memory:read` for `list`, `retrieve`, `search`, `history`, and `resolve`, or `memory:write` for `create`, `update`, `delete`, and `move`. Calls without `node` are external application calls and are not narrowed by a node scope list.
32
+
33
+ ## Methods
34
+
35
+ Every method accepts `RequestOptions` as its final optional argument (`signal`, `timeout`, `maxRetries`, and `headers`). A document name can contain `/`; the SDK encodes it as one path segment.
36
+
37
+ | Method | Parameters | Result |
38
+ |---|---|---|
39
+ | `memory.list(query?, options?)` | `MemoryScope & { kind?, limit?, after? }`; `limit` is 1–100 (default 100) | `MemoryPage<MemoryDocSummaryDTO>` |
40
+ | `memory.retrieve(name, query?, options?)` | `name`; `MemoryScope & { frontmatter? }` | `MemoryDocDTO` |
41
+ | `memory.create(params, options?)` | `MemoryDocCreateRequest` | `MemoryDocDTO` |
42
+ | `memory.update(name, params, options?)` | `name`; `MemoryDocUpdateRequest` | `MemoryDocDTO` |
43
+ | `memory.delete(name, scope?, options?)` | `name`; `MemoryScope` | `MemoryDocDeletedDTO` |
44
+ | `memory.move(name, params, options?)` | `name`; `MemoryDocMoveRequest` | `MemoryDocMovedDTO` |
45
+ | `memory.search(params, options?)` | `MemorySearchRequest` | `MemoryPage<MemorySearchHitDTO>` |
46
+ | `memory.history(name, query?, options?)` | `name`; `MemoryHistoryQuery` | `MemoryHistoryDTO` |
47
+ | `memory.resolve(name, scope?, options?)` | `name`; `MemoryScope` | `MemoryDocRefDTO` |
48
+
49
+ `MemoryDocCreateRequest` requires `name`, `kind`, `when_and_why_to_read`, and `body`; it also accepts `frontmatter`, `extensions`, and the scope fields. `MemoryDocUpdateRequest` requires `rationale`, which is stored in revision history rather than document text, and optionally accepts `body`, `frontmatter`, `extensions`, `unset`, and the scope fields. `MemoryDocMoveRequest` requires `to` and accepts the scope fields.
50
+
51
+ `retrieve` returns the body by default. With `frontmatter: true`, its `body` is the complete document source. `MemoryDocDTO` also contains `frontmatter`, `representation` (`'leaf-file'` or `'directory-index'`), `links`, and any lint `findings`; document DTOs include their canonical name, kind, scope, extensions, path, store root, physical relative path, and any competing candidates. `resolve` returns the winning document's location and optional plugin name without reading its body.
52
+
53
+ ```ts
54
+ await client.memory.create({
55
+ name: 'billing/refund-policy',
56
+ kind: 'knowledge',
57
+ when_and_why_to_read: 'When handling a refund request, read this because the eligibility window is not in the order record.',
58
+ body: 'Refunds are available for 60 days.',
59
+ profile: 'my-app',
60
+ });
61
+
62
+ await client.memory.update('billing/refund-policy', {
63
+ rationale: 'The eligibility window changed on 2026-01-04.',
64
+ body: 'Refunds are available for 90 days.',
65
+ profile: 'my-app',
66
+ });
67
+ ```
68
+
69
+ The daemon returns document text without expanding shell blocks. Builtin and plugin documents are read-only. `kind`, `when-and-why-to-read`, `origin`, and `last-updated` cannot be set or removed directly. Each mutation writes a complete before/after history record; delete leaves a tombstone. `retrieve` and `resolve` return `NotFoundError` with status 404 and code `memory_document_not_found` for a deleted document or an unknown name. Concurrent writes are last-write-wins.
70
+
71
+ `search` uses ranked search by default. Set `body: true` to include document bodies in ranking, or `grep: true` for regular-expression line matches; `grep` and `body` cannot be combined, and `min_score` is only available in ranked search. Ranked hits contain `kind`, `score`, and `short_form`; grep hits contain `line` and `text`.
72
+
73
+ `history` returns newest-first revision summaries, optionally with `diff`, or the full before/after record for one `revision`. A delete leaves history available through its tombstone.
74
+
75
+ ## Pages
76
+
77
+ `list` and `search` return `{ object: 'list', data, has_more }` as a `MemoryPage`. A page has `hasNextPage()`, `getNextPage()`, and is async iterable across every page. The list cursor is the final document's canonical name. Ranked search uses the final hit's name; grep uses an opaque cursor that includes the match location, which the SDK handles.
78
+
79
+ ```ts
80
+ const page = await client.memory.search({ query: 'refund window', node: nodeId });
81
+ for (const hit of page.data) console.log(hit.name);
82
+
83
+ if (page.hasNextPage()) {
84
+ const next = await page.getNextPage();
85
+ }
86
+
87
+ for await (const hit of page) console.log(hit.name);
88
+ ```
@@ -0,0 +1,118 @@
1
+ ---
2
+ kind: knowledge
3
+ when-and-why-to-read: When you need Migration from `generate()` and `local()`,
4
+ this knowledge should be read because **This is a hard cut.** `generate()`,
5
+ `local()`, and the `Environment` interface are deleted in the same release
6
+ that adds `Crouter`. There is no coexistence period, no deprecated re-export,
7
+ and no compatibility shim. Both packages are pre-1.0 and the callers are
8
+ countable.
9
+ ---
10
+
11
+ # Migration from `generate()` and `local()`
12
+
13
+ Phase 1.
14
+
15
+ **This is a hard cut.** `generate()`, `local()`, and the `Environment` interface are deleted in the same release that adds `Crouter`. There is no coexistence period, no deprecated re-export, and no compatibility shim. Both packages are pre-1.0 and the callers are countable.
16
+
17
+ Pin your old version or migrate; there is no third option.
18
+
19
+ ## Before and after
20
+
21
+ ```text
22
+ // before
23
+ import { generate, local } from '@north-light/crouter-sdk';
24
+ import { z } from 'zod';
25
+
26
+ const schema = z.object({ name: z.string(), version: z.string() });
27
+
28
+ const result = await generate({
29
+ prompt: 'Read package.json and report its name and version.',
30
+ schema,
31
+ env: local(),
32
+ cwd: '/path/to/repo',
33
+ profile: 'my-app',
34
+ kind: 'general',
35
+ model: 'anthropic/strong',
36
+ deadline: '10m',
37
+ signal,
38
+ });
39
+
40
+ if (result.kind === 'result') console.log(result.value.name);
41
+ else if (result.kind === 'declined') console.warn(result.reason);
42
+ else console.error(result.reason, result.detail);
43
+ ```
44
+
45
+ ```ts
46
+ // after
47
+ import Crouter from '@north-light/crouter-sdk';
48
+ import { z } from 'zod';
49
+
50
+ const client = new Crouter();
51
+
52
+ const run = await client.nodes.parse(
53
+ {
54
+ prompt: 'Read package.json and report its name and version.',
55
+ output_schema: z.object({ name: z.string(), version: z.string() }),
56
+ cwd: '/path/to/repo',
57
+ profile: 'my-app',
58
+ kind: 'general',
59
+ model: 'anthropic/strong',
60
+ deadline: '10m',
61
+ },
62
+ { signal },
63
+ );
64
+
65
+ if (run.kind === 'result') console.log(run.output_parsed.name);
66
+ else if (run.reason === 'declined') console.warn(run.declined?.reason);
67
+ else console.error(run.reason, run.detail);
68
+ ```
69
+
70
+ ## What moved where
71
+
72
+ | Before | After |
73
+ |---|---|
74
+ | `generate({ … })` | `client.nodes.parse({ … })` |
75
+ | `local()` / `env: local()` | `new Crouter()` — the client is the connection |
76
+ | `local({ autostart: false })` | `new Crouter({ autostart: false })` |
77
+ | `env.daemon()` + `new CrtrClient(…)` | `new Crouter(env.connection())` — see [[crouter-sdk/docker]] |
78
+ | `client.ensureProfile('x')` | `client.profiles.ensure('x')` |
79
+ | `schema` | `output_schema` |
80
+ | `signal` in the params object | `signal` in the second argument, the per-request options |
81
+ | `onEvent` | `client.nodes.stream()` — [[crouter-sdk/streaming]] |
82
+ | the `Environment` interface | deleted; nothing implements it |
83
+
84
+ ## The result union changed
85
+
86
+ `generate()` returned a three-way union of its own invention. `parse()` returns the settled `NodeOutcome` the daemon already defines — the same union the wire has, not a translation of it.
87
+
88
+ | `generate()` | `parse()` |
89
+ |---|---|
90
+ | `{ kind: 'result', value, nodeId, reportPath }` | `{ kind: 'result', output_parsed, structured_result, final_report_path, … }` |
91
+ | `{ kind: 'declined', reason, nodeId }` | `{ kind: 'failure', reason: 'declined', declined: { reason, code, retryable } \| null, … }` |
92
+ | `{ kind: 'failure', reason, detail, nodeId }` | `{ kind: 'failure', reason, detail, … }` |
93
+
94
+ A decline is now a `failure` with `reason: 'declined'`, so narrow on `run.reason === 'declined'` rather than `run.kind === 'declined'`. It carries more than the old shape did: the agent's own code and a `retryable` flag.
95
+
96
+ `value` became `output_parsed`, and `reportPath` became `final_report_path`.
97
+
98
+ Every `NodeOutcomeDTO` carries the wire field `node_id`.
99
+
100
+ ## `signal` actually cancels now
101
+
102
+ `generate()` raced an abort against a promise it could not cancel. `parse()` passes the signal to `fetch`, so aborting stops the in-flight request and raises `APIUserAbortError`.
103
+
104
+ Aborting still stops **your client waiting**, not the run. Call `client.nodes.cancel(id)` to stop the agent, or give it a `deadline` so the daemon stops it for you.
105
+
106
+ ## Progress reporting
107
+
108
+ `onEvent` emitted three notifications: node created, report pushed, settled. Two of its three are available in phase 1 without it:
109
+
110
+ - The node is created when `client.nodes.create()` returns — you hold the node.
111
+ - Reports are available from `client.nodes.reports.list(id)`.
112
+ - Settlement is the return of `waitForOutcome`.
113
+
114
+ Streaming is strictly more capable than `onEvent` was: the three events it emitted are three of the events the stream carries, alongside assistant text deltas and tool calls.
115
+
116
+ ## `CrtrClient` is no longer yours to construct
117
+
118
+ The SDK does not re-export `CrtrClient` — the raw client is an implementation detail of `Crouter`. If you were constructing one to reach a route the SDK does not wrap, use `client.request()` instead; see [[crouter-sdk/resources]].