@north-light/cli 0.3.472 → 0.3.474
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/api/dto/nodes.d.ts +1 -1
- package/dist/builtin-memory/crouter-sdk/README.md +4 -47
- package/dist/builtin-memory/crouter-sdk/client.md +2 -231
- package/dist/builtin-memory/crouter-sdk/errors.md +9 -50
- package/dist/builtin-memory/crouter-sdk/getting-started.md +3 -65
- package/dist/builtin-memory/crouter-sdk/guides/README.md +1 -3
- package/dist/builtin-memory/crouter-sdk/guides/typed-extraction.md +1 -1
- package/dist/builtin-memory/crouter-sdk/nodes.md +1 -1
- package/dist/builtin-memory/crouter-sdk/release-notes.md +20 -20
- package/dist/builtin-memory/crouter-sdk/streaming.md +1 -70
- package/dist/builtin-memory/northlight-apps/INDEX.md +15 -0
- package/dist/builtin-memory/northlight-apps/cli.md +41 -0
- package/dist/builtin-memory/northlight-apps/coding-agents.md +75 -0
- package/dist/builtin-memory/northlight-apps/concepts/apps-and-sign-in.md +109 -0
- package/dist/builtin-memory/northlight-apps/concepts/connections.md +109 -0
- package/dist/builtin-memory/northlight-apps/concepts/providers.md +76 -0
- package/dist/builtin-memory/northlight-apps/concepts/runs.md +101 -0
- package/dist/builtin-memory/northlight-apps/concepts/scopes-and-grants.md +64 -0
- package/dist/builtin-memory/northlight-apps/concepts/testing-mode.md +54 -0
- package/dist/builtin-memory/northlight-apps/errors.md +119 -0
- package/dist/builtin-memory/northlight-apps/guides/background-runs.md +80 -0
- package/dist/builtin-memory/northlight-apps/guides/files-and-documents.md +61 -0
- package/dist/builtin-memory/northlight-apps/guides/go-live.md +39 -0
- package/dist/builtin-memory/northlight-apps/guides/grant-changes.md +100 -0
- package/dist/builtin-memory/northlight-apps/guides/manage-your-app.md +77 -0
- package/dist/builtin-memory/northlight-apps/guides/many-users.md +63 -0
- package/dist/builtin-memory/northlight-apps/guides/stream-a-run.md +84 -0
- package/dist/builtin-memory/{crouter-sdk/testing.md → northlight-apps/guides/test-your-app.md} +6 -6
- package/dist/builtin-memory/northlight-apps/guides/typed-results.md +99 -0
- package/dist/builtin-memory/northlight-apps/how-it-fits.md +49 -0
- package/dist/builtin-memory/northlight-apps/providers/README.md +39 -0
- package/dist/builtin-memory/northlight-apps/providers/context.md +34 -0
- package/dist/builtin-memory/northlight-apps/providers/device.md +28 -0
- package/dist/builtin-memory/northlight-apps/providers/integrations.md +28 -0
- package/dist/builtin-memory/northlight-apps/providers/search.md +31 -0
- package/dist/builtin-memory/northlight-apps/providers/sms.md +39 -0
- package/dist/builtin-memory/northlight-apps/quickstart.md +210 -0
- package/dist/commands/node/create.js +2 -2
- package/docs/apps/cli.md +40 -0
- package/docs/apps/coding-agents.md +72 -0
- package/docs/apps/concepts/apps-and-sign-in.md +82 -0
- package/docs/apps/concepts/connections.md +106 -0
- package/docs/apps/concepts/providers.md +59 -0
- package/docs/apps/concepts/runs.md +94 -0
- package/docs/apps/concepts/scopes-and-grants.md +62 -0
- package/docs/apps/concepts/testing-mode.md +36 -0
- package/docs/apps/errors.md +117 -0
- package/docs/apps/guides/background-runs.md +78 -0
- package/docs/apps/guides/files-and-documents.md +59 -0
- package/docs/apps/guides/go-live.md +37 -0
- package/docs/apps/guides/grant-changes.md +98 -0
- package/docs/apps/guides/manage-your-app.md +75 -0
- package/docs/apps/guides/many-users.md +61 -0
- package/docs/apps/guides/stream-a-run.md +82 -0
- package/docs/{sdk/testing.md → apps/guides/test-your-app.md} +5 -5
- package/docs/apps/guides/typed-results.md +72 -0
- package/docs/apps/how-it-fits.md +46 -0
- package/docs/apps/providers/README.md +26 -0
- package/docs/apps/providers/context.md +32 -0
- package/docs/apps/providers/device.md +26 -0
- package/docs/apps/providers/integrations.md +26 -0
- package/docs/apps/providers/search.md +29 -0
- package/docs/apps/providers/sms.md +37 -0
- package/docs/apps/quickstart.md +120 -2
- package/docs/cli/README.md +1 -0
- package/docs/cli/join-northlight.md +58 -0
- package/docs/sdk/README.md +4 -47
- package/docs/sdk/client.md +2 -231
- package/docs/sdk/errors.md +8 -49
- package/docs/sdk/getting-started.md +3 -65
- package/docs/sdk/guides/README.md +1 -3
- package/docs/sdk/guides/typed-extraction.md +1 -1
- package/docs/sdk/nodes.md +1 -1
- package/docs/sdk/release-notes.md +20 -20
- package/docs/sdk/streaming.md +1 -70
- package/package.json +2 -2
- package/packages/crouter-identity/package.json +1 -1
- package/runtime.lock.json +12 -12
- package/dist/builtin-memory/crouter-sdk/error-envelope.md +0 -38
- package/dist/builtin-memory/crouter-sdk/guides/apps-on-northlight.md +0 -423
- package/dist/builtin-memory/crouter-sdk/guides/apps-with-many-users.md +0 -61
- package/dist/builtin-memory/crouter-sdk/memory.md +0 -49
- package/docs/sdk/error-envelope.md +0 -36
- package/docs/sdk/guides/apps-on-northlight.md +0 -419
- package/docs/sdk/guides/apps-with-many-users.md +0 -59
- package/docs/sdk/memory.md +0 -47
|
@@ -71,28 +71,28 @@ The SDK no longer refuses a runtime because its release number is lower than the
|
|
|
71
71
|
|
|
72
72
|
Not breaking: no type or wire shape an app reads is removed or changed.
|
|
73
73
|
|
|
74
|
-
- An app Northlight has not verified is in testing: only its admin and named testers can sign in. A refused sign-in redirects with `error=access_denied`, `error_description=app_in_testing`; a refused refresh answers `invalid_grant` `app_in_testing`. The refresh refusal is a new `Refusal` with `code: 'app_in_testing'` and `kind: 'paused'`, like `grant_suspended`: keep the person's data. If you `switch` exhaustively on `refusal.code`, add the case. See [When a refresh is refused](
|
|
75
|
-
- New `authorizationRefusalOf(callback)` reads the sign-in refusal from the callback's query and returns `AuthorizationRefusal` (`{code: 'app_in_testing', error: 'access_denied', state}`), else `null`. See [When sign-in is refused](
|
|
76
|
-
- `@north-light/sdk/testing`: `cloud.testing` (`enable`, `disable`, `active`, `addTester`, `removeTester`, `testers`, `allowed`), `cloud.authorize(userId, {redirectUri, state})`, `user.setAccount({email, emailVerified, phone})`, and `'app_in_testing'` in `FakeRefusalCode`. The fake directory now serves `grant_type=authorization_code` for codes `cloud.authorize` issued. See [Testing mode](
|
|
74
|
+
- An app Northlight has not verified is in testing: only its admin and named testers can sign in. A refused sign-in redirects with `error=access_denied`, `error_description=app_in_testing`; a refused refresh answers `invalid_grant` `app_in_testing`. The refresh refusal is a new `Refusal` with `code: 'app_in_testing'` and `kind: 'paused'`, like `grant_suspended`: keep the person's data. If you `switch` exhaustively on `refusal.code`, add the case. See [When a refresh is refused](/docs/concepts/connections#when-a-refresh-is-refused).
|
|
75
|
+
- New `authorizationRefusalOf(callback)` reads the sign-in refusal from the callback's query and returns `AuthorizationRefusal` (`{code: 'app_in_testing', error: 'access_denied', state}`), else `null`. See [When sign-in is refused](/docs/concepts/apps-and-sign-in#when-the-directory-refuses-to-sign-someone-in).
|
|
76
|
+
- `@north-light/sdk/testing`: `cloud.testing` (`enable`, `disable`, `active`, `addTester`, `removeTester`, `testers`, `allowed`), `cloud.authorize(userId, {redirectUri, state})`, `user.setAccount({email, emailVerified, phone})`, and `'app_in_testing'` in `FakeRefusalCode`. The fake directory now serves `grant_type=authorization_code` for codes `cloud.authorize` issued. See [Testing mode](/docs/guides/test-your-app#testing-mode).
|
|
77
77
|
|
|
78
78
|
## 0.3.415
|
|
79
79
|
|
|
80
80
|
### Monthly spend cap enforced; `usage.get` (additive; one behavior change)
|
|
81
81
|
|
|
82
82
|
- **Behavior change:** user-paid model calls (`/v1/llm`) and provider calls now count against the grant's `spend_cap` (from `grant.get()`; Northlight sets $50) for the current UTC month. Once the month reaches it, the next call answers 402 `cap_exhausted` (a `RateLimitError`, `retryable: false`, `userAction: 'raise_cap'`, `resetAt` the first instant of next month) before any model or provider is called. A run whose model call is refused this way ends with outcome reason `cap_exhausted`. The check runs before each call, so the call that crosses the cap still completes. App-paid usage never counts. A grant with `spend_cap: null` is not capped.
|
|
83
|
-
- New `client.usage.get({month?})` (`month` is `YYYY-MM`, UTC; default the current month) answers `SpendUsageDTO`: `usd_equivalent` (user-paid spend, provider receipts included), `by_model`, `app_paid`, `spend_cap` and `reset_at`. See [Show what a person's month has cost](
|
|
83
|
+
- New `client.usage.get({month?})` (`month` is `YYYY-MM`, UTC; default the current month) answers `SpendUsageDTO`: `usd_equivalent` (user-paid spend, provider receipts included), `by_model`, `app_paid`, `spend_cap` and `reset_at`. See [Show what a person's month has cost](/docs/guides/grant-changes#show-what-a-persons-month-has-cost).
|
|
84
84
|
- A completion's `usage.usd_equivalent` is now the served model's list-price cost; before it was always `null`. It stays `null` for a model with no list price, and that usage adds nothing to the month.
|
|
85
85
|
|
|
86
86
|
### `npx @north-light/sdk env` (additive); keygen on an unjoined machine exits 2
|
|
87
87
|
|
|
88
|
-
- New `npx @north-light/sdk env --app <client id> [--out <path>]` writes `CROUTER_ISSUER`, `CROUTER_CLIENT_ID` and `APP_ORIGIN` from `crtr cloud apps get` into `local/app.env`, keeping every other line. See [Go live with an app key](
|
|
88
|
+
- New `npx @north-light/sdk env --app <client id> [--out <path>]` writes `CROUTER_ISSUER`, `CROUTER_CLIENT_ID` and `APP_ORIGIN` from `crtr cloud apps get` into `local/app.env`, keeping every other line. See [Go live with an app key](/docs/guides/go-live).
|
|
89
89
|
- `keygen --app` on a machine whose `crtr` isn't joined to a Northlight account now prints the join step and the manual steps and exits 2. A missing `crtr` keeps its message and exit 0.
|
|
90
90
|
|
|
91
91
|
### One deadline per SDK call, and a deadline on every OAuth/JWKS fetch (additive, with one behaviour change)
|
|
92
92
|
|
|
93
93
|
Not breaking: no type or wire shape an app reads is removed or changed. Apps pinned to the previous release keep working unchanged.
|
|
94
94
|
|
|
95
|
-
- `RequestOptions` gains `budgetMs` and `deadlineAt`: one absolute deadline for the call covering every attempt, every retry wait (including a `Retry-After`) and the token fetch/refresh. A wait that would pass the deadline rejects at once with the new `DeadlineExceededError` (`code: 'deadline_exceeded'`, `reason: 'elapsed' | 'retry_after'`, `deadlineAt`, `cause`), which extends `APIConnectionTimeoutError`, so code catching that still catches it. A transport that ignores its `AbortSignal` can no longer outlive the deadline. Without `budgetMs`/`deadlineAt` nothing about retries changes. See [One deadline per call](
|
|
95
|
+
- `RequestOptions` gains `budgetMs` and `deadlineAt`: one absolute deadline for the call covering every attempt, every retry wait (including a `Retry-After`) and the token fetch/refresh. A wait that would pass the deadline rejects at once with the new `DeadlineExceededError` (`code: 'deadline_exceeded'`, `reason: 'elapsed' | 'retry_after'`, `deadlineAt`, `cause`), which extends `APIConnectionTimeoutError`, so code catching that still catches it. A transport that ignores its `AbortSignal` can no longer outlive the deadline. Without `budgetMs`/`deadlineAt` nothing about retries changes. See [One deadline per call](/docs/errors#retries).
|
|
96
96
|
- An app that holds a lease or lock while it makes an SDK call should pass `budgetMs` and keep the lease longer than it, with margin, and state that in code.
|
|
97
97
|
- `OAuth2Options.timeoutMs` (default 10 s): one deadline per directory operation (discovery, token exchange, refresh, revoke, `runtime_provisioning` retries and their `retry_after_s`). A refresh under the connection-store lock now holds the lock at most `timeoutMs`. `JwksCache` (and so `verifyIdToken`) takes `{timeoutMs}` (default 10 s) over its discovery and key-set reads together.
|
|
98
98
|
- **Behaviour change:** these directory fetches used to have no deadline (only discovery had one). A directory that takes longer than 10 s now fails with `DeadlineExceededError` where it used to wait; set `timeoutMs` to change it.
|
|
@@ -112,11 +112,11 @@ Not breaking: no type or wire shape an app reads is removed or changed. Apps pin
|
|
|
112
112
|
|
|
113
113
|
### `@north-light/sdk/testing` (additive)
|
|
114
114
|
|
|
115
|
-
New `@north-light/sdk/testing`: `FakeCloud`, a fake directory and runtime your real `OAuth2Client` and `Crouter` talk to through `fetch`. It covers runs (events, replies, long-poll, idempotency, endings), grant watch, refusals, directory failures, memory, providers and run notices, and is checked against the real runtime on every release. Additive: `verifyRunNotice` accepts `fetch`. No behaviour change. See [Testing your app](
|
|
115
|
+
New `@north-light/sdk/testing`: `FakeCloud`, a fake directory and runtime your real `OAuth2Client` and `Crouter` talk to through `fetch`. It covers runs (events, replies, long-poll, idempotency, endings), grant watch, refusals, directory failures, memory, providers and run notices, and is checked against the real runtime on every release. Additive: `verifyRunNotice` accepts `fetch`. No behaviour change. See [Testing your app](/docs/guides/test-your-app).
|
|
116
116
|
|
|
117
117
|
### `oauth.accountAppUrl()` (additive), and a failed discovery read is retried (behaviour fix)
|
|
118
118
|
|
|
119
|
-
- New `OAuth2Client.accountAppUrl()` returns the app's page on the signed-in person's Northlight account, as the directory's discovery document advertises it (`account_app_uri_template`). Use it instead of building `<issuer>/account/apps/<client id>` yourself. It needs no user or connection and throws `CrouterError` naming the fault when the directory advertises no template or an invalid one. See [Link to the app's settings on Northlight](
|
|
119
|
+
- New `OAuth2Client.accountAppUrl()` returns the app's page on the signed-in person's Northlight account, as the directory's discovery document advertises it (`account_app_uri_template`). Use it instead of building `<issuer>/account/apps/<client id>` yourself. It needs no user or connection and throws `CrouterError` naming the fault when the directory advertises no template or an invalid one. See [Link to the app's settings on Northlight](/docs/guides/grant-changes#link-to-the-apps-settings-on-northlight).
|
|
120
120
|
- **Behaviour fix:** `OAuth2Client`'s discovery read now times out after 10 seconds, and a read that failed or timed out is retried by the next call that needs it. Before, one failed read was kept for the client's life, so every later `authorizeUrl`, `exchangeCode`, refresh and `verifyIdToken` failed until the process restarted, and a hung directory stalled them indefinitely. A successful read is still kept for the client's life.
|
|
121
121
|
|
|
122
122
|
## 0.3.405
|
|
@@ -167,25 +167,25 @@ The runtime gains what a sandbox that sleeps needs; no app-facing type or route
|
|
|
167
167
|
|
|
168
168
|
### Follow a person's grant with `oauth.watchGrants` (additive)
|
|
169
169
|
|
|
170
|
-
`grant.get()` answers a new field, `version`: opaque, it changes exactly when `rev`, `resolved_scopes`, `spend_cap`, `payer` or `dependencies` changes. `grant.get({wait, since})` (`wait` an integer 1–25; anything else throws `RangeError` before sending) holds the read until the grant's `version` differs from `since` or `wait` seconds pass, and extends the request timeout by `wait`. A grant revoked or removed during the hold answers 401 `grant_revoked`. `oauth.watchGrants({users, onChanged, onError?, sweepMs?, concurrency?, wait?})` returns a `GrantWatcher` that follows the grants of the people `users()` names and calls `onChanged({userId, grant, previous})` level-triggered, at least once per version, so an app learns when a required provider's setup turns `ready` without a browser tab or a poll loop. New exports `GrantWatcher`, `GrantWatchOptions`, `GrantChanged`. Not breaking: nothing existing changes shape, and `RunWatcher` behaves exactly as before. The hold needs a runtime at this release and, for a hosted runtime, a hosting router that lets `GET /v1/grant?wait=N` through (before then a held read answers 504 after 10 s and the watcher backs off). See [Learn when a person's grant changes](
|
|
170
|
+
`grant.get()` answers a new field, `version`: opaque, it changes exactly when `rev`, `resolved_scopes`, `spend_cap`, `payer` or `dependencies` changes. `grant.get({wait, since})` (`wait` an integer 1–25; anything else throws `RangeError` before sending) holds the read until the grant's `version` differs from `since` or `wait` seconds pass, and extends the request timeout by `wait`. A grant revoked or removed during the hold answers 401 `grant_revoked`. `oauth.watchGrants({users, onChanged, onError?, sweepMs?, concurrency?, wait?})` returns a `GrantWatcher` that follows the grants of the people `users()` names and calls `onChanged({userId, grant, previous})` level-triggered, at least once per version, so an app learns when a required provider's setup turns `ready` without a browser tab or a poll loop. New exports `GrantWatcher`, `GrantWatchOptions`, `GrantChanged`. Not breaking: nothing existing changes shape, and `RunWatcher` behaves exactly as before. The hold needs a runtime at this release and, for a hosted runtime, a hosting router that lets `GET /v1/grant?wait=N` through (before then a held read answers 504 after 10 s and the watcher backs off). See [Learn when a person's grant changes](/docs/guides/grant-changes#follow-grants-with-watchgrants).
|
|
171
171
|
|
|
172
172
|
## 0.3.399
|
|
173
173
|
|
|
174
174
|
### `onActivity` no longer blocks the watcher
|
|
175
175
|
|
|
176
|
-
In 0.3.398 the watcher awaited `onActivity` before its next read, so a slow or hung hook delayed that run's `onEnded`, held its `concurrency` slot, and kept `stop()` from resolving. The watcher now never awaits it: at most one call per run runs at a time, a newer reading taken meanwhile replaces any still waiting, and `stop()` does not wait for a call already running. A call can now finish after that run's `onEnded`: guard the write on the row still waiting on the run. Runs that leave `pending()` without an ending no longer keep their last phase in memory. See [Learn when a background run ends](
|
|
176
|
+
In 0.3.398 the watcher awaited `onActivity` before its next read, so a slow or hung hook delayed that run's `onEnded`, held its `concurrency` slot, and kept `stop()` from resolving. The watcher now never awaits it: at most one call per run runs at a time, a newer reading taken meanwhile replaces any still waiting, and `stop()` does not wait for a call already running. A call can now finish after that run's `onEnded`: guard the write on the row still waiting on the run. Runs that leave `pending()` without an ending no longer keep their last phase in memory. See [Learn when a background run ends](/docs/guides/background-runs#follow-runs-with-runwatcher).
|
|
177
177
|
|
|
178
178
|
## 0.3.398
|
|
179
179
|
|
|
180
180
|
### `RunWatcher` reports live progress with `onActivity` (additive)
|
|
181
181
|
|
|
182
|
-
`oauth.watchRuns({..., onActivity, activityWait?})` calls `onActivity({userId, runId, activity, run})` with each read of a still-`running` run whose `activity` differs from the last one handed over for that run (compared as JSON: `phase`, `tool.name`, `tool.summary`), so an app can show a progress badge from the watcher it already runs instead of a second `runs.get` per page read. It is never called for a run at rest. With `onActivity` set, running runs are read with `wait: activityWait` (integer 1–25, default 2) rather than 25, because the runtime's long-poll answers early only at rest. Best effort: a throwing `onActivity` goes to `onError` and is not retried, and `onEnded` delivery is unchanged. Not breaking: without `onActivity` the watcher behaves exactly as before. New exported type `RunActivityEvent`. See [Learn when a background run ends](
|
|
182
|
+
`oauth.watchRuns({..., onActivity, activityWait?})` calls `onActivity({userId, runId, activity, run})` with each read of a still-`running` run whose `activity` differs from the last one handed over for that run (compared as JSON: `phase`, `tool.name`, `tool.summary`), so an app can show a progress badge from the watcher it already runs instead of a second `runs.get` per page read. It is never called for a run at rest. With `onActivity` set, running runs are read with `wait: activityWait` (integer 1–25, default 2) rather than 25, because the runtime's long-poll answers early only at rest. Best effort: a throwing `onActivity` goes to `onError` and is not retried, and `onEnded` delivery is unchanged. Not breaking: without `onActivity` the watcher behaves exactly as before. New exported type `RunActivityEvent`. See [Learn when a background run ends](/docs/guides/background-runs#follow-runs-with-runwatcher).
|
|
183
183
|
|
|
184
184
|
## 0.3.397
|
|
185
185
|
|
|
186
186
|
### A runtime older than the SDK is refused before it handles the request
|
|
187
187
|
|
|
188
|
-
The SDK now sends `Crouter-Minimum-Runtime-Version: <SDK_VERSION>` on every request except `/healthz`. The hosting router in front of a hosted runtime, and a runtime at this release or later, answer a request that names a newer release than the runtime's with `RuntimeVersionError` (`status` 426, `code` `runtime_version_unsupported`) **before any handler runs**: nothing is accepted, no `Idempotency-Key` is recorded, and a retry after the runtime is rolled forward is a first send. Previously an older runtime handled the request and the SDK raised afterwards. Not breaking: no app code changes. An app that worked around the late check, such as a `runs.list` read before every send, can delete it once its runtimes are at or after this release behind a hosting router that has the check. Back off on `RuntimeVersionError` and retry under the same key. A browser app's preflight now lists `crouter-minimum-runtime-version` in `Access-Control-Allow-Headers`. See [Errors](
|
|
188
|
+
The SDK now sends `Crouter-Minimum-Runtime-Version: <SDK_VERSION>` on every request except `/healthz`. The hosting router in front of a hosted runtime, and a runtime at this release or later, answer a request that names a newer release than the runtime's with `RuntimeVersionError` (`status` 426, `code` `runtime_version_unsupported`) **before any handler runs**: nothing is accepted, no `Idempotency-Key` is recorded, and a retry after the runtime is rolled forward is a first send. Previously an older runtime handled the request and the SDK raised afterwards. Not breaking: no app code changes. An app that worked around the late check, such as a `runs.list` read before every send, can delete it once its runtimes are at or after this release behind a hosting router that has the check. Back off on `RuntimeVersionError` and retry under the same key. A browser app's preflight now lists `crouter-minimum-runtime-version` in `Access-Control-Allow-Headers`. See [Errors](/docs/errors).
|
|
189
189
|
|
|
190
190
|
## 0.3.396
|
|
191
191
|
|
|
@@ -199,7 +199,7 @@ The SDK now sends `Crouter-Minimum-Runtime-Version: <SDK_VERSION>` on every requ
|
|
|
199
199
|
|
|
200
200
|
### `OAuth2Client` owns the connection store; one `onRefusal` for a dead connection (breaking)
|
|
201
201
|
|
|
202
|
-
`OAuth2Client` now takes the app's `ConnectionStore` once, as `store` (required), plus an optional `onRefusal(refusal)` handler, and every refused refresh goes through that one handler: a request on a connected client, `refreshIdleConnections`, and the new `RunWatcher`. Apps no longer keep their own refusal table or delete dead connections themselves. See [When a refresh is refused](
|
|
202
|
+
`OAuth2Client` now takes the app's `ConnectionStore` once, as `store` (required), plus an optional `onRefusal(refusal)` handler, and every refused refresh goes through that one handler: a request on a connected client, `refreshIdleConnections`, and the new `RunWatcher`. Apps no longer keep their own refusal table or delete dead connections themselves. See [When a refresh is refused](/docs/concepts/connections#when-a-refresh-is-refused).
|
|
203
203
|
|
|
204
204
|
- **`oauth.client(connection, store)` is now `await oauth.client(userId)`.** It loads the stored connection and returns a `ConnectedCrouter` (a `Crouter` with `userId` and `runtimeUrl`). Nothing stored throws `CrouterError` code `not_connected`. Save `exchangeCode`'s connection with `store.save(connection)` first.
|
|
205
205
|
- **`oauth.refreshIdleConnections(store, {olderThanMs})` is now `oauth.refreshIdleConnections({olderThanMs})`** and returns `{refreshed, refused, failures}`. `RefreshFailure.reason` is removed: a refused connection is handled by `onRefusal`, removed, and counted in `refused`; `failures` holds only connections still stored (any other error, or a refusal whose handler threw).
|
|
@@ -213,25 +213,25 @@ The SDK now sends `Crouter-Minimum-Runtime-Version: <SDK_VERSION>` on every requ
|
|
|
213
213
|
|
|
214
214
|
### `RunWatcher`: learn when a background run ends (additive)
|
|
215
215
|
|
|
216
|
-
`oauth.watchRuns({pending, onEnded, onError?, sweepMs?, concurrency?})` returns a `RunWatcher` (`start()`, `nudge()`, `stop()`) that follows the runs `pending()` reads from the app's own rows with `runs.get(wait)` and calls `onEnded({userId, runId, ended, run})` when each comes to rest (`settled`, `idle`, `waiting_on_user`) or is deleted (`gone`, only for a 404 `not_found` from the runtime, never the router). Delivery is at least once per `(runId, status, latest_sequence)`: a delivered run is re-read once per sweep and delivered again only when that key changes (an `idle` run later canceled is delivered as `settled`). A throwing `onEnded` is retried after 5 s, doubling to 5 min. Runs are opened with `oauth.client(userId)`, so a refused connection reaches `onRefusal`. There is no runtime-to-app webhook: the runtime cannot mint an app-audience token, a directory relay would add a hop, and the app's rows already make a pull durable and work on localhost. See [Learn when a background run ends](
|
|
216
|
+
`oauth.watchRuns({pending, onEnded, onError?, sweepMs?, concurrency?})` returns a `RunWatcher` (`start()`, `nudge()`, `stop()`) that follows the runs `pending()` reads from the app's own rows with `runs.get(wait)` and calls `onEnded({userId, runId, ended, run})` when each comes to rest (`settled`, `idle`, `waiting_on_user`) or is deleted (`gone`, only for a 404 `not_found` from the runtime, never the router). Delivery is at least once per `(runId, status, latest_sequence)`: a delivered run is re-read once per sweep and delivered again only when that key changes (an `idle` run later canceled is delivered as `settled`). A throwing `onEnded` is retried after 5 s, doubling to 5 min. Runs are opened with `oauth.client(userId)`, so a refused connection reaches `onRefusal`. There is no runtime-to-app webhook: the runtime cannot mint an app-audience token, a directory relay would add a hop, and the app's rows already make a pull durable and work on localhost. See [Learn when a background run ends](/docs/guides/background-runs#follow-runs-with-runwatcher).
|
|
217
217
|
|
|
218
218
|
## 0.3.394
|
|
219
219
|
|
|
220
220
|
### `grant.get` dependencies are one `{ state, message?, next }` entry per provider (breaking)
|
|
221
221
|
|
|
222
|
-
`GrantGetDTO.dependencies[provider]` was a bare `SetupState` string; it is now a `ProviderDependency` (exported from `@north-light/sdk` and `@north-light/api`): `{ state, message?, next }`. `state` is the setup state the string used to be. `message` is the provider's own sentence for `pending` and `failed` (its `message_pending` / `message_failed`), so delete any copy of a provider's prose your app hard-codes and show `message` instead. `next` is the absolute URL of the directory's setup page for that provider and your app; link the person there (append `return_to` with your registered redirect origin) instead of building the URL yourself. **Change every read of `dependencies[p]` to `dependencies[p].state`** (or `dependencies[p]?.state` where the entry may be missing), and carry `message` and `next` into your `provider_setup_pending` details. The runtime reads only this shape from the directory's grant sync: a runtime on this release paired with a directory that still sends bare strings reports `dependencies` as `{}`, so roll the directory change with it. See [Waiting on a provider your app requires](
|
|
222
|
+
`GrantGetDTO.dependencies[provider]` was a bare `SetupState` string; it is now a `ProviderDependency` (exported from `@north-light/sdk` and `@north-light/api`): `{ state, message?, next }`. `state` is the setup state the string used to be. `message` is the provider's own sentence for `pending` and `failed` (its `message_pending` / `message_failed`), so delete any copy of a provider's prose your app hard-codes and show `message` instead. `next` is the absolute URL of the directory's setup page for that provider and your app; link the person there (append `return_to` with your registered redirect origin) instead of building the URL yourself. **Change every read of `dependencies[p]` to `dependencies[p].state`** (or `dependencies[p]?.state` where the entry may be missing), and carry `message` and `next` into your `provider_setup_pending` details. The runtime reads only this shape from the directory's grant sync: a runtime on this release paired with a directory that still sends bare strings reports `dependencies` as `{}`, so roll the directory change with it. See [Waiting on a provider your app requires](/docs/guides/grant-changes#wait-on-a-provider-your-app-requires).
|
|
223
223
|
|
|
224
224
|
## 0.3.393
|
|
225
225
|
|
|
226
226
|
### The runtime orders grant state by revision; a failed grant check is `service_unavailable` (breaking)
|
|
227
227
|
|
|
228
|
-
A runtime on this release requires the `rev` claim in every app access token, so it needs a directory that issues it; a token without `rev` is refused 401 `unauthorized`, "access token has no rev claim: the directory is older than this runtime". A freshly consented or re-consented grant is now authorized on the first call, with no wait: the runtime pulls grant state once whenever a token is newer than its mirror. **Delete any retry your app added for a 401 `grant_revoked` right after consent.** When that pull fails, the call answers 503 `service_unavailable` (`retryable: true`) instead of 401 `grant_revoked`, so a directory outage no longer looks like a refused connection. `service_unavailable` was already in `errorCodes`; app calls can now receive it. See [An app connection and refresh](
|
|
228
|
+
A runtime on this release requires the `rev` claim in every app access token, so it needs a directory that issues it; a token without `rev` is refused 401 `unauthorized`, "access token has no rev claim: the directory is older than this runtime". A freshly consented or re-consented grant is now authorized on the first call, with no wait: the runtime pulls grant state once whenever a token is newer than its mirror. **Delete any retry your app added for a 401 `grant_revoked` right after consent.** When that pull fails, the call answers 503 `service_unavailable` (`retryable: true`) instead of 401 `grant_revoked`, so a directory outage no longer looks like a refused connection. `service_unavailable` was already in `errorCodes`; app calls can now receive it. See [An app connection and refresh](/docs/concepts/connections).
|
|
229
229
|
|
|
230
230
|
## 0.3.391
|
|
231
231
|
|
|
232
232
|
### The SDK refuses a runtime older than itself (breaking)
|
|
233
233
|
|
|
234
|
-
`@north-light/sdk` now accepts only a runtime at or above its own version, and fails loudly otherwise instead of handing an app data its types do not describe. Every runtime answer carries `Crouter-Runtime-Version`; when that version is older than `SDK_VERSION`, or the header is absent (every runtime before this release), the call throws `RuntimeVersionError` (`code: 'runtime_version_unsupported'`, `status: 426`, `retryable: false`) whose message and `sdkVersion` / `runtimeVersion` name both versions. A refusal without the header (a router in front of the runtime answering 401 or 503) is still raised as that refusal. There is no capability negotiation: `RunObject.activity` and every other field are typed for a runtime at this release. **Upgrade every runtime the app talks to before upgrading the SDK**, and pin the SDK to an exact version. A test double that fakes the runtime with `fetch` must send `Crouter-Runtime-Version` at the SDK's version on its 2xx answers. See [Errors](
|
|
234
|
+
`@north-light/sdk` now accepts only a runtime at or above its own version, and fails loudly otherwise instead of handing an app data its types do not describe. Every runtime answer carries `Crouter-Runtime-Version`; when that version is older than `SDK_VERSION`, or the header is absent (every runtime before this release), the call throws `RuntimeVersionError` (`code: 'runtime_version_unsupported'`, `status: 426`, `retryable: false`) whose message and `sdkVersion` / `runtimeVersion` name both versions. A refusal without the header (a router in front of the runtime answering 401 or 503) is still raised as that refusal. There is no capability negotiation: `RunObject.activity` and every other field are typed for a runtime at this release. **Upgrade every runtime the app talks to before upgrading the SDK**, and pin the SDK to an exact version. A test double that fakes the runtime with `fetch` must send `Crouter-Runtime-Version` at the SDK's version on its 2xx answers. See [Errors](/docs/errors).
|
|
235
235
|
|
|
236
236
|
### One release is one version set
|
|
237
237
|
|
|
@@ -259,7 +259,7 @@ A runtime on this release requires the `rev` claim in every app access token, so
|
|
|
259
259
|
|
|
260
260
|
### `runs.get` reports what a running run is doing (additive)
|
|
261
261
|
|
|
262
|
-
`RunObject.activity` is `{phase: 'starting' | 'thinking' | 'writing' | 'between_turns'}` or `{phase: 'tool', tool: {name, summary}}` while the run is `running`, and `null` otherwise, so a polling app can show progress from `runs.get` without reading `runs.trace`. `runs.list` entries carry it too. See [Showing a background run's progress](
|
|
262
|
+
`RunObject.activity` is `{phase: 'starting' | 'thinking' | 'writing' | 'between_turns'}` or `{phase: 'tool', tool: {name, summary}}` while the run is `running`, and `null` otherwise, so a polling app can show progress from `runs.get` without reading `runs.trace`. `runs.list` entries carry it too. See [Showing a background run's progress](/docs/guides/typed-results#showing-progress). A test double that builds a `RunObject` literal must add `activity`.
|
|
263
263
|
|
|
264
264
|
## 0.3.374
|
|
265
265
|
|
|
@@ -271,7 +271,7 @@ A sign-in that grants the `profile` scope returns `name`, `givenName`, and `fami
|
|
|
271
271
|
|
|
272
272
|
### `CrouterError.code` is typed `SdkErrorCode`
|
|
273
273
|
|
|
274
|
-
Every code the SDK raises itself is now listed in `sdkErrorCodes` (exported with `SdkErrorCode` and `mapError` from `@north-light/sdk/errors`), and `CrouterError`'s `code` and constructor are typed with it. Runtime behavior is unchanged. **An app that constructs `new CrouterError(message, code)` with a code of its own, for example in a test double, no longer typechecks:** throw its own error class or an `APIError` instead. See [Errors](
|
|
274
|
+
Every code the SDK raises itself is now listed in `sdkErrorCodes` (exported with `SdkErrorCode` and `mapError` from `@north-light/sdk/errors`), and `CrouterError`'s `code` and constructor are typed with it. Runtime behavior is unchanged. **An app that constructs `new CrouterError(message, code)` with a code of its own, for example in a test double, no longer typechecks:** throw its own error class or an `APIError` instead. See [Errors](/docs/errors).
|
|
275
275
|
|
|
276
276
|
### Run ergonomics (additive)
|
|
277
277
|
|
|
@@ -287,7 +287,7 @@ When the directory refuses a connection's refresh token, the SDK now raises the
|
|
|
287
287
|
- `grant_suspended`: your app is paused for that person. Nothing was deleted; keep their data.
|
|
288
288
|
- `refresh_token_expired`: the refresh token went 90 days unused. The grant is still active; reconnecting skips the consent page.
|
|
289
289
|
|
|
290
|
-
Each is a 401 `AuthenticationError` with `retryable: false`, `userAction: 'reconnect'`, and `origin: 'directory'`, and the connected client from `oauth.client(...)` stops refreshing after it, as it already did for `grant_revoked` and `refresh_token_reused`. `errorCodes` and the `ErrorCode` type gain the three codes. The table of codes and app actions is in [When a refresh is refused](
|
|
290
|
+
Each is a 401 `AuthenticationError` with `retryable: false`, `userAction: 'reconnect'`, and `origin: 'directory'`, and the connected client from `oauth.client(...)` stops refreshing after it, as it already did for `grant_revoked` and `refresh_token_reused`. `errorCodes` and the `ErrorCode` type gain the three codes. The table of codes and app actions is in [When a refresh is refused](/docs/concepts/connections#when-a-refresh-is-refused).
|
|
291
291
|
|
|
292
292
|
**Behavior changes for existing apps:**
|
|
293
293
|
|
package/docs/sdk/streaming.md
CHANGED
|
@@ -7,76 +7,7 @@ description: Follow assistant text, tool activity, reports, and outcomes as an a
|
|
|
7
7
|
|
|
8
8
|
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.
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
The app server keeps the runtime token and connection; the browser connects only to the app. The app server calls `forwardRunEvents(client, runId, {after: cursorFromRequest(request), signal: request.signal})` and returns the Fetch `Response`. After a host cuts the browser connection, EventSource sends its last SSE id as `Last-Event-ID`; `cursorFromRequest` prefers it over the URL's `after` query parameter.
|
|
13
|
-
|
|
14
|
-
Every forwarded run event is an unnamed SSE record with its `sequence_number` as `id` and the complete event object as `data`. Receive all event types with one handler:
|
|
15
|
-
|
|
16
|
-
```ts
|
|
17
|
-
const source = new EventSource(`/api/runs/${runId}/events?after=${cursor}`);
|
|
18
|
-
source.onmessage = ({data}) => {
|
|
19
|
-
const event = JSON.parse(data);
|
|
20
|
-
if (event.type === 'stream.refused') { showRefusal(event.error); source.close(); return; }
|
|
21
|
-
if (event.type === 'stream.reconnecting') { showConnectionFailure(event.error); return; }
|
|
22
|
-
if (event.type === 'stream.reconnected') { clearConnectionFailure(); return; }
|
|
23
|
-
if (event.type === 'stream.idle') { source.close(); markIdle(event.cursor); return; } // reopen on your own trigger
|
|
24
|
-
showRunEvent(event);
|
|
25
|
-
};
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
The four `stream.*` records are forwarded-stream notices, not daemon run events; they have no `sequence_number` or SSE id.
|
|
29
|
-
|
|
30
|
-
`stream.idle` (`{type: 'stream.idle', cursor}`) means the runtime closed the stream after 60 s with no running turn and no new event. The helper writes it and then ends the response, so a browser `EventSource` would otherwise reconnect at once and keep a hosted runtime awake indefinitely. Close the `EventSource` on `stream.idle` and reopen it with `after=cursor` on your own trigger: the person sends a message, or your server handles a run notice (see `verifyRunNotice`). A `stream.refused` carries the full error envelope, including `stream_gap`'s `details.latest_sequence` and `details.earliest_sequence`. The helper ends that response after a refusal. The SDK does not recover a gap by skipping missing events.
|
|
31
|
-
|
|
32
|
-
When the page reloads mid-reply or gets `stream_gap`, the app reads `runs.trace(runId)` through its server and shows each node's `in_progress_message` (`{thinking, text}` or `null`) as that node's open message. The trace's `latest_sequence` is read consistently with its conversations and open messages. Resume `runs.events(runId, {after: trace.latest_sequence})` (or forward with that cursor); do not use a separately read `runs.get` cursor. Continue appending new deltas to the open message, but replace its thinking or text with the complete content repeated by `node.thinking.done` or `node.output_text.done`. This shows the message from its beginning without duplicating content. `runs.get` and every `runs.list` entry expose `latest_sequence` and `last_activity_at`; a cursor equal to `latest_sequence` is not a gap even when older events have expired.
|
|
33
|
-
|
|
34
|
-
## Following the reply to one message
|
|
35
|
-
|
|
36
|
-
A chat app sends a message and shows the answer to it. `runs.reply(runId, sent, {signal?})` takes what `runs.message` returned and follows only the turn that delivers that message:
|
|
37
|
-
|
|
38
|
-
```ts
|
|
39
|
-
const sent = await client.runs.message(runId, 'What did the report say?');
|
|
40
|
-
for await (const event of client.runs.reply(runId, sent)) {
|
|
41
|
-
if (event.type === 'text.delta') append(event.delta);
|
|
42
|
-
if (event.type === 'turn.completed' && event.error) showError(event.error); // {code: 'content_refused' | 'model_error', message}
|
|
43
|
-
}
|
|
44
|
-
// or collect it: text as it streams, then the whole text and how the turn ended
|
|
45
|
-
const {text, ended, error, outcome} = await client.runs.reply(runId, sent).collect({onDelta: (delta) => append(delta)});
|
|
46
|
-
// ended: 'completed' | 'error' (error set) | 'waiting_on_user' | 'settled' (outcome set)
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
It reads `runs.events(runId, {after: sent.sequence_number})`, skips child nodes' events, starts at the root's `node.turn.started` whose `input.message_ids` holds `sent.message_id`, and yields:
|
|
50
|
-
|
|
51
|
-
| Event | Fields | From |
|
|
52
|
-
|---|---|---|
|
|
53
|
-
| `text.delta` | `delta` | `node.output_text.delta` |
|
|
54
|
-
| `message.done` | `text` (the message's whole text, possibly empty), `stop_reason` | `node.output_text.done`, once per assistant message |
|
|
55
|
-
| `turn.completed` | `run_status`, `stop_reason`, `error?`, `message_ids` | `node.turn.completed`; always last |
|
|
56
|
-
| `run.settled` | `outcome` | `node.settled`, when the run settles before the turn completes; always last |
|
|
57
|
-
|
|
58
|
-
Every event also carries its `sequence_number`. The reply ends after `turn.completed` or `run.settled` and closes its event stream. A resident run never settles when it goes idle, so a plain `runs.events` stream stays open, but a reply does not. `.text()` consumes the reply and resolves to the text of its `message.done` events joined with a blank line, so a turn that speaks, calls a tool and speaks again returns both texts. `.collect({onDelta?})` consumes the reply too: it passes each streamed piece to `onDelta`, starting a later assistant message's first piece with a blank line, and resolves to `RunReplyResult` `{text, ended, error?, outcome?}`, where `text` is the trimmed messages joined with a blank line. `ended` is `completed` (text may be empty when the model wrote nothing), `error` (the turn's last message failed; `error` says why), `waiting_on_user` (the turn ended on a question to the person), or `settled` (the run settled first; `outcome` says how). `.text({onDelta?})` is `collect`'s `text`. A reply can be consumed once.
|
|
59
|
-
|
|
60
|
-
Several messages delivered in one turn, including a `start_turn: false` message that joined it, all resolve to that turn. To have a reply to the run's first message, pass it as `runs.start({prompt, message})` and follow `runs.reply(run.run_id, run.message!)`: a message stored with the run always joins the first turn. A refused or failed model call ends the turn with an empty `message.done` (`stop_reason: 'error'`) and `turn.completed.error`; the run stays usable, and `runs.get` shows the same error as `last_turn_error` until a later turn completes without one. A provider failure the runtime retries on its own (connection, rate limit, overloaded) does not end the reply: the recovery attempt continues the same turn, so the reply yields its answer and one `turn.completed` without an error, and only a turn whose retries run out ends with `model_error`. An interrupt, `runs.cancel`, or a closed node ends the turn with `stop_reason: 'aborted'` and no error. An expired cursor throws `APIError` with code `stream_gap`; aborting `signal` rejects with `APIUserAbortError`.
|
|
61
|
-
|
|
62
|
-
## A quiet run: `event: idle`
|
|
63
|
-
|
|
64
|
-
A runtime that sleeps when nothing holds a connection to it ends a run's SSE stream after 60 s in which the run has no running turn and no new event: it sends `event: idle` (`data: {}`) and closes. `RunStream` reports that and does not reconnect:
|
|
65
|
-
|
|
66
|
-
```ts
|
|
67
|
-
const stream = client.runs.events(runId, {after: cursor});
|
|
68
|
-
for await (const event of stream) handle(event);
|
|
69
|
-
const end = await stream.whenEnded(); // rejects on a transport failure or abort()
|
|
70
|
-
if (end.reason === 'idle') {
|
|
71
|
-
// the run is quiet and nothing was missed; reopen later with client.runs.events(runId, {after: end.cursor})
|
|
72
|
-
}
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
`stream.idle` and `stream.position` read the same facts synchronously. A settled run ends with `reason: 'settled'`; a transport error rejects as before. `runs.reply` reopens through an idle close on its own.
|
|
76
|
-
|
|
77
|
-
## Typed run events
|
|
78
|
-
|
|
79
|
-
`RunEvent`, exported by the SDK, is the union of every run event, discriminated on `type`. Its types are exactly the daemon's run event vocabulary, declared once. `RunEventOf<'node.turn.completed'>` picks one event, and `RunStream.on(type, listener)` passes the listener that event. An event type a newer daemon adds and this SDK does not declare is still delivered as-is.
|
|
10
|
+
Streaming a run of an app (forwarding it to a browser, following the reply to one message, `event: idle`, typed run events) is on the SDK docs site: [Stream a run](https://sdk.docs.northlight.ai/docs/guides/stream-a-run).
|
|
80
11
|
|
|
81
12
|
## The SDK surface
|
|
82
13
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.474",
|
|
4
4
|
"description": "crtr — agent runtime with memory, plugins, and marketplaces",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -137,7 +137,7 @@
|
|
|
137
137
|
},
|
|
138
138
|
"license": "UNLICENSED",
|
|
139
139
|
"dependencies": {
|
|
140
|
-
"@north-light/identity": "0.3.
|
|
140
|
+
"@north-light/identity": "0.3.474",
|
|
141
141
|
"@earendil-works/pi-agent-core": "1.1.0",
|
|
142
142
|
"@earendil-works/pi-ai": "1.1.0",
|
|
143
143
|
"@earendil-works/pi-coding-agent": "1.1.0",
|
package/runtime.lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.474",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@north-light/cli",
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.474",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "UNLICENSED",
|
|
12
12
|
"workspaces": [
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"@earendil-works/pi-ai": "1.1.0",
|
|
19
19
|
"@earendil-works/pi-coding-agent": "1.1.0",
|
|
20
20
|
"@earendil-works/pi-tui": "1.1.0",
|
|
21
|
-
"@north-light/identity": "0.3.
|
|
21
|
+
"@north-light/identity": "0.3.474",
|
|
22
22
|
"cron-parser": "^5.6.0",
|
|
23
23
|
"esbuild": "^0.27.7",
|
|
24
24
|
"jose": "^6.2.12",
|
|
@@ -12115,17 +12115,17 @@
|
|
|
12115
12115
|
},
|
|
12116
12116
|
"packages/crouter-api": {
|
|
12117
12117
|
"name": "@north-light/api",
|
|
12118
|
-
"version": "0.3.
|
|
12118
|
+
"version": "0.3.474",
|
|
12119
12119
|
"license": "UNLICENSED"
|
|
12120
12120
|
},
|
|
12121
12121
|
"packages/crouter-env-docker": {
|
|
12122
12122
|
"name": "@north-light/env-docker",
|
|
12123
|
-
"version": "0.3.
|
|
12123
|
+
"version": "0.3.474",
|
|
12124
12124
|
"license": "UNLICENSED"
|
|
12125
12125
|
},
|
|
12126
12126
|
"packages/crouter-identity": {
|
|
12127
12127
|
"name": "@north-light/identity",
|
|
12128
|
-
"version": "0.3.
|
|
12128
|
+
"version": "0.3.474",
|
|
12129
12129
|
"license": "UNLICENSED",
|
|
12130
12130
|
"dependencies": {
|
|
12131
12131
|
"jose": "^6.2.1"
|
|
@@ -12133,20 +12133,20 @@
|
|
|
12133
12133
|
},
|
|
12134
12134
|
"packages/crouter-plugin": {
|
|
12135
12135
|
"name": "@north-light/plugin",
|
|
12136
|
-
"version": "0.3.
|
|
12136
|
+
"version": "0.3.474",
|
|
12137
12137
|
"license": "UNLICENSED",
|
|
12138
12138
|
"dependencies": {
|
|
12139
|
-
"@north-light/api": "0.3.
|
|
12140
|
-
"@north-light/identity": "0.3.
|
|
12139
|
+
"@north-light/api": "0.3.474",
|
|
12140
|
+
"@north-light/identity": "0.3.474"
|
|
12141
12141
|
}
|
|
12142
12142
|
},
|
|
12143
12143
|
"packages/crouter-sdk": {
|
|
12144
12144
|
"name": "@north-light/sdk",
|
|
12145
|
-
"version": "0.3.
|
|
12145
|
+
"version": "0.3.474",
|
|
12146
12146
|
"license": "UNLICENSED",
|
|
12147
12147
|
"dependencies": {
|
|
12148
|
-
"@north-light/api": "0.3.
|
|
12149
|
-
"@north-light/identity": "0.3.
|
|
12148
|
+
"@north-light/api": "0.3.474",
|
|
12149
|
+
"@north-light/identity": "0.3.474",
|
|
12150
12150
|
"jose": "^6.2.1"
|
|
12151
12151
|
},
|
|
12152
12152
|
"bin": {
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
kind: knowledge
|
|
3
|
-
when-and-why-to-read: When you need App-facing error envelope, this knowledge
|
|
4
|
-
should be read because Contract 2 codes and the HTTP refusal body for
|
|
5
|
-
Northlight apps.
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
# App-facing error envelope
|
|
9
|
-
|
|
10
|
-
Northlight's app-facing HTTP endpoints use contract 2's error body. An app's **own** endpoints can use the same shape with `origin: 'app'`. The SDK publishes the fixed code tuples:
|
|
11
|
-
|
|
12
|
-
```ts
|
|
13
|
-
import { errorCodes, type ErrorEnvelope } from '@north-light/sdk/errors';
|
|
14
|
-
|
|
15
|
-
const row = errorCodes.invalid_request;
|
|
16
|
-
// { type: 'invalid_request_error', status: 400, retryable: false }
|
|
17
|
-
|
|
18
|
-
const body: ErrorEnvelope = {
|
|
19
|
-
error: {
|
|
20
|
-
code: 'invalid_request',
|
|
21
|
-
message: 'Missing prompt',
|
|
22
|
-
type: row.type,
|
|
23
|
-
origin: 'app',
|
|
24
|
-
retryable: row.retryable,
|
|
25
|
-
request_id: 'req-123',
|
|
26
|
-
param: 'prompt',
|
|
27
|
-
},
|
|
28
|
-
};
|
|
29
|
-
// Send body with HTTP status row.status.
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
The body is `{ error: { code, message, details?, type, param?, origin, retryable, retry_after_s?, reset_at?, request_id, run_id?, question_id?, user_action?, scope? } }`. `origin` is `directory`, `router`, `runtime`, `provider`, or `app`. `type` uses OpenAI's `invalid_request_error`, `authentication_error`, `permission_error`, `rate_limit_error`, or `server_error`. **Status belongs only in the HTTP status line**, not in the body. Send `Retry-After` when `retry_after_s` is set. Keep `request_id` in the body and echo it as a request-id response header. Do not expose stack traces in a 5xx message.
|
|
33
|
-
|
|
34
|
-
`errorCodes` is keyed by code. Each row gives that code's fixed contract-2 `{ type, status, retryable }` tuple; the daemon renderer uses this module for `type` via `@north-light/api`. An unsatisfiable byte range is `range_not_satisfiable` (416), an upstream model failure is `upstream_model_unavailable` (503, retryable), and an upstream vendor sign-in refusal is `upstream_refused` (502). The renderer preserves the thrown HTTP status and retry decision; throw sites must choose a code whose tuple matches those values. If no existing code fits an app-specific failure, declare an app code with a consistent type/status and document it in the app's OpenAPI spec.
|
|
35
|
-
|
|
36
|
-
Not every error has a fixed tuple. `provider_error` carries the provider's varying 4xx status and is not in `errorCodes`; `usage` is a legacy socket/loopback code that becomes `invalid_request` on the app-facing listener. `stream_gap`, `stream_dropped`, and `stream_error` are stream protocol codes with beta-defined behavior, not ordinary static HTTP refusal rows. SDK-generated connection failures (`daemon_unavailable`, `transport_error`, `daemon_request_interrupted`, `request_timeout`, `daemon_health_unavailable`) are not daemon refusals. `runtime_unreachable` applies to a hosted router, `runtime_provisioning`, `grant_removed`, `grant_suspended`, `refresh_token_expired`, and `refresh_token_reused` are raised by the SDK for directory token responses (see [[crouter-sdk/client]]), and `wake_timeout` is reserved for the later hosted sleep/wake stage; their presence in the table does not mean the local daemon emits them today.
|
|
37
|
-
|
|
38
|
-
`ErrorEnvelope` models the **full app-facing response**, not the SDK's permissive `ErrorBody` used to parse older and socket responses. A socket or v0 loopback response can have only `code`, `message`, and optional `details` (with run-layer fields in some cases); never assume `type` or `request_id` exists on those transports. `APIError.status` is separate from the parsed JSON body. See [[crouter-sdk/errors]] for error classes, transport failures, and retry rules.
|