@crouter/sdk 0.3.390 → 0.3.392

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/README.md CHANGED
@@ -45,6 +45,23 @@ const client = new Crouter({
45
45
  });
46
46
  ```
47
47
 
48
+ ## Runtime version
49
+
50
+ The SDK talks only to a runtime at or above its own version (`SDK_VERSION`), and has no compatibility mode for older ones: its types, such as `RunObject.activity`, describe what a runtime at its release sends. Every runtime answer carries a `Crouter-Runtime-Version` header. When that version is older than the SDK, or the header is missing (every runtime released before this rule shipped), the call throws `RuntimeVersionError` before the SDK reads the response: `code` is `runtime_version_unsupported`, `status` is 426, `retryable` is false, and `sdkVersion` and `runtimeVersion` name both versions. Upgrade the runtime first, then the SDK.
51
+
52
+ Pin `@crouter/sdk` to an exact version. One release is one version set: `@crouter/sdk@X` depends on exactly `@crouter/api@X` and `@crouter/identity@X`, so the pin fixes all three.
53
+
54
+ ```ts
55
+ import { RuntimeVersionError } from '@crouter/sdk';
56
+
57
+ try {
58
+ await client.runs.get(runId);
59
+ } catch (error) {
60
+ if (error instanceof RuntimeVersionError) console.error(`runtime ${error.runtimeVersion ?? '(unnamed)'} is older than SDK ${error.sdkVersion}`);
61
+ else throw error;
62
+ }
63
+ ```
64
+
48
65
  ## Run and parse a result
49
66
 
50
67
  `parse()` creates a root run, waits for its outcome, and types `output_parsed` from a Zod or Standard Schema. Pass `scopes` to give a run a per-run allow-list; under a scoped token the list must stay inside the token's ceiling, and omitting it gives the run the ceiling.
package/dist/client.js CHANGED
@@ -1,4 +1,4 @@
1
- import { APIError, CrtrClient, routes, } from '@crouter/api';
1
+ import { APIError, CrtrClient, RUNTIME_VERSION_HEADER, compareReleaseVersions, routes, } from '@crouter/api';
2
2
  import { APIConnectionError, APIUserAbortError, AuthenticationError, CrouterError, mapError } from './errors.js';
3
3
  import { Bash } from './resources/bash.js';
4
4
  import { Canvas } from './resources/canvas.js';
@@ -19,6 +19,7 @@ import { StructuredRequests } from './resources/structured-requests.js';
19
19
  import { Memory } from './resources/memory.js';
20
20
  import { Uploads } from './resources/uploads.js';
21
21
  import { resolveJsonSchema } from './schema.js';
22
+ import { SDK_VERSION } from './version.js';
22
23
  const LOCAL_SOCKET_FETCH = Symbol.for('@crouter/api/local-socket-fetch');
23
24
  const DEFAULT_TIMEOUT_MS = 30_000;
24
25
  const POLL_WAIT_SECONDS = 25;
@@ -100,7 +101,7 @@ export class Crouter {
100
101
  ...options.headers,
101
102
  ...(token === undefined ? {} : { Authorization: `Bearer ${token}` }),
102
103
  };
103
- const baseFetch = options.fetch ?? (baseURL === undefined && isNodeRuntime() ? localSocketFetch(options.socketPath ?? env['CRTR_SOCKET']) : fetch);
104
+ const baseFetch = requireCurrentRuntime(options.fetch ?? (baseURL === undefined && isNodeRuntime() ? localSocketFetch(options.socketPath ?? env['CRTR_SOCKET']) : undefined));
104
105
  this.eventFetch = options.tokenSource ? (input, init) => {
105
106
  const url = new URL(input instanceof Request ? input.url : String(input));
106
107
  if (url.pathname === '/v1/health')
@@ -125,7 +126,7 @@ export class Crouter {
125
126
  this.socketPath = undefined;
126
127
  this.client = new CrtrClient({
127
128
  baseUrl: 'http://localhost',
128
- fetch: options.fetch,
129
+ fetch: baseFetch,
129
130
  headers,
130
131
  timeoutMs: this.timeout,
131
132
  maxRetries: options.maxRetries,
@@ -134,11 +135,10 @@ export class Crouter {
134
135
  else {
135
136
  const socketPath = options.socketPath ?? env['CRTR_SOCKET'];
136
137
  this.socketPath = socketPath;
137
- const fetch = options.fetch ?? localSocketFetch(socketPath);
138
138
  const autostart = options.autostart ?? true;
139
139
  this.client = new CrtrClient({
140
140
  baseUrl: 'http://localhost',
141
- fetch,
141
+ fetch: baseFetch,
142
142
  headers,
143
143
  timeoutMs: this.timeout,
144
144
  maxRetries: options.maxRetries,
@@ -467,6 +467,38 @@ function nodeEnvironment() {
467
467
  function isNodeRuntime() {
468
468
  return typeof process !== 'undefined' && process.versions?.node !== undefined;
469
469
  }
470
+ /**
471
+ * Latest only (P2): the SDK accepts a runtime at or above its own release (`SDK_VERSION`) and nothing older, so its
472
+ * types never describe a field an older runtime does not send. Every runtime response names its release in
473
+ * `Crouter-Runtime-Version`; a successful answer from an older runtime, or one that names no release (every runtime
474
+ * before the header existed), is replaced by a 426 `runtime_version_unsupported` envelope before anything reads it,
475
+ * which the client raises as `RuntimeVersionError`. A refusal without the header passes through unchanged: it can come
476
+ * from a router or proxy in front of the runtime, and it carries no runtime data. `/healthz`, the unversioned
477
+ * substrate probe the cold-start wait polls, is exempt.
478
+ */
479
+ function requireCurrentRuntime(inner) {
480
+ const checked = async (input, init) => {
481
+ const response = await (inner ?? globalThis.fetch)(input, init);
482
+ const url = new URL(input instanceof Request ? input.url : String(input), 'http://localhost');
483
+ if (url.pathname === '/healthz')
484
+ return response;
485
+ const runtimeVersion = response.headers.get(RUNTIME_VERSION_HEADER);
486
+ if (runtimeVersion === null ? !response.ok : compareReleaseVersions(runtimeVersion, SDK_VERSION) >= 0)
487
+ return response;
488
+ await response.body?.cancel();
489
+ const message = runtimeVersion === null
490
+ ? `The runtime did not name its version, so it predates the runtime version header; @crouter/sdk ${SDK_VERSION} requires a runtime at ${SDK_VERSION} or later. Upgrade the runtime, or pin @crouter/sdk to the runtime's release.`
491
+ : `The runtime is ${runtimeVersion}; @crouter/sdk ${SDK_VERSION} requires a runtime at ${SDK_VERSION} or later. Upgrade the runtime, or pin @crouter/sdk to ${runtimeVersion}.`;
492
+ return new Response(JSON.stringify({ error: {
493
+ code: 'runtime_version_unsupported', message, type: 'invalid_request_error', retryable: false,
494
+ details: { sdk_version: SDK_VERSION, runtime_version: runtimeVersion },
495
+ } }), { status: 426, headers: { 'content-type': 'application/json' } });
496
+ };
497
+ if (inner !== undefined && inner[LOCAL_SOCKET_FETCH] === true) {
498
+ checked[LOCAL_SOCKET_FETCH] = true;
499
+ }
500
+ return checked;
501
+ }
470
502
  function localSocketFetch(socketPath) {
471
503
  const load = new Function('specifier', 'return import(specifier)');
472
504
  let fetchPromise;
@@ -2,6 +2,6 @@ export { errorCodes } from '@crouter/api';
2
2
  export type { ErrorCode, ErrorEnvelope, ErrorOrigin, ErrorType } from '@crouter/api';
3
3
  export { mapError } from './errors.js';
4
4
  /** Codes the SDK raises itself rather than reading from a daemon or directory response. */
5
- export declare const sdkErrorCodes: readonly ["crouter_error", "id_token_invalid", "oauth_nonce_missing", "oauth_state_mismatch", "oauth_exchange_mismatch", "run_idle", "waiting_on_user", "request_aborted", "request_timeout", "connection_error", "transport_error", "daemon_unavailable", "daemon_request_interrupted", "daemon_health_unavailable", "invalid_response", "stream_error", "stream_ended"];
5
+ export declare const sdkErrorCodes: readonly ["crouter_error", "id_token_invalid", "oauth_nonce_missing", "oauth_state_mismatch", "oauth_exchange_mismatch", "run_idle", "waiting_on_user", "request_aborted", "request_timeout", "connection_error", "transport_error", "daemon_unavailable", "daemon_request_interrupted", "daemon_health_unavailable", "runtime_version_unsupported", "invalid_response", "stream_error", "stream_ended"];
6
6
  /** A code the SDK raises itself. An `APIError`'s `code` is one of these or an `ErrorCode` from the contract. */
7
7
  export type SdkErrorCode = typeof sdkErrorCodes[number];
@@ -27,6 +27,8 @@ export const sdkErrorCodes = [
27
27
  'daemon_unavailable',
28
28
  'daemon_request_interrupted',
29
29
  'daemon_health_unavailable',
30
+ /** The runtime is older than this SDK, or sent no `Crouter-Runtime-Version` (`RuntimeVersionError`). */
31
+ 'runtime_version_unsupported',
30
32
  /** A response the SDK could not read as the API's JSON or error envelope. */
31
33
  'invalid_response',
32
34
  /** An event stream failed, or ended before the reply or node it carried did. */
package/dist/errors.d.ts CHANGED
@@ -6,6 +6,20 @@ export declare class CrouterError extends APIError {
6
6
  readonly code: SdkErrorCode;
7
7
  constructor(message: string, code?: SdkErrorCode);
8
8
  }
9
+ /** The runtime is older than this SDK (or names no version at all), so the SDK's types do not describe what it
10
+ * sends. Raised before the SDK reads the response; never retryable. Upgrade the runtime (or pin the SDK to the
11
+ * runtime's release). `code` is `runtime_version_unsupported`, `status` 426. */
12
+ export declare class RuntimeVersionError extends APIError {
13
+ /** The oldest runtime this SDK accepts: its own release version. */
14
+ get sdkVersion(): string;
15
+ /** The version the runtime named in `Crouter-Runtime-Version`, or `null` when it sent none (a runtime from before the header existed). */
16
+ get runtimeVersion(): string | null;
17
+ }
18
+ /** `RuntimeVersionError.details`. */
19
+ export interface RuntimeVersionDetails {
20
+ sdk_version: string;
21
+ runtime_version: string | null;
22
+ }
9
23
  /** The daemon rejected the request: HTTP 400. */
10
24
  export declare class BadRequestError extends APIError {
11
25
  }
package/dist/errors.js CHANGED
@@ -7,6 +7,15 @@ export class CrouterError extends APIError {
7
7
  this.name = 'CrouterError';
8
8
  }
9
9
  }
10
+ /** The runtime is older than this SDK (or names no version at all), so the SDK's types do not describe what it
11
+ * sends. Raised before the SDK reads the response; never retryable. Upgrade the runtime (or pin the SDK to the
12
+ * runtime's release). `code` is `runtime_version_unsupported`, `status` 426. */
13
+ export class RuntimeVersionError extends APIError {
14
+ /** The oldest runtime this SDK accepts: its own release version. */
15
+ get sdkVersion() { return this.details.sdk_version; }
16
+ /** The version the runtime named in `Crouter-Runtime-Version`, or `null` when it sent none (a runtime from before the header existed). */
17
+ get runtimeVersion() { return this.details.runtime_version; }
18
+ }
10
19
  /** The daemon rejected the request: HTTP 400. */
11
20
  export class BadRequestError extends APIError {
12
21
  }
@@ -45,29 +54,31 @@ export function mapError(error) {
45
54
  if (!(error instanceof APIError) || error.constructor !== APIError) {
46
55
  return error instanceof APIError ? error : new APIConnectionError(0, 'connection_error', error instanceof Error ? error.message : String(error));
47
56
  }
48
- const Constructor = error.code === 'request_aborted'
49
- ? APIUserAbortError
50
- : error.status === 504 || error.code === 'request_timeout'
51
- ? APIConnectionTimeoutError
52
- : error.code === 'daemon_unavailable' || error.code === 'transport_error' || error.code === 'daemon_request_interrupted' || error.code === 'daemon_health_unavailable'
53
- ? APIConnectionError
54
- : error.status === 400
55
- ? BadRequestError
56
- : error.status === 401
57
- ? AuthenticationError
58
- : error.status === 403
59
- ? PermissionDeniedError
60
- : error.status === 404
61
- ? NotFoundError
62
- : error.status === 409
63
- ? ConflictError
64
- : error.status === 413 || error.status === 422
65
- ? UnprocessableEntityError
66
- : error.status === 402 || error.status === 429
67
- ? RateLimitError
68
- : error.status >= 500
69
- ? InternalServerError
70
- : APIError;
57
+ const Constructor = error.code === 'runtime_version_unsupported'
58
+ ? RuntimeVersionError
59
+ : error.code === 'request_aborted'
60
+ ? APIUserAbortError
61
+ : error.status === 504 || error.code === 'request_timeout'
62
+ ? APIConnectionTimeoutError
63
+ : error.code === 'daemon_unavailable' || error.code === 'transport_error' || error.code === 'daemon_request_interrupted' || error.code === 'daemon_health_unavailable'
64
+ ? APIConnectionError
65
+ : error.status === 400
66
+ ? BadRequestError
67
+ : error.status === 401
68
+ ? AuthenticationError
69
+ : error.status === 403
70
+ ? PermissionDeniedError
71
+ : error.status === 404
72
+ ? NotFoundError
73
+ : error.status === 409
74
+ ? ConflictError
75
+ : error.status === 413 || error.status === 422
76
+ ? UnprocessableEntityError
77
+ : error.status === 402 || error.status === 429
78
+ ? RateLimitError
79
+ : error.status >= 500
80
+ ? InternalServerError
81
+ : APIError;
71
82
  return new Constructor(error.status, error.code, error.message, error.details, error.headers, {
72
83
  type: error.type, param: error.param, origin: error.origin, retryable: error.retryable,
73
84
  retry_after_s: error.retryAfterS, reset_at: error.resetAt, request_id: error.requestId, run_id: error.runId,
package/dist/index.d.ts CHANGED
@@ -26,7 +26,9 @@ export { cursorFromRequest, forwardRunEvents } from './resources/forward.js';
26
26
  export type { NodeStreamEvent, NodeStreamEventListener, NodeStreamEventType } from './resources/node-stream.js';
27
27
  export { describeToolDefault, followActivity } from './resources/activity.js';
28
28
  export type { ActivityStep, DescribeTool } from './resources/activity.js';
29
- export { APIError, APIConnectionError, APIConnectionTimeoutError, APIUserAbortError, AuthenticationError, BadRequestError, ConflictError, CrouterError, InternalServerError, NotFoundError, PermissionDeniedError, RateLimitError, UnprocessableEntityError, mapError, } from './errors.js';
29
+ export { APIError, APIConnectionError, APIConnectionTimeoutError, APIUserAbortError, AuthenticationError, BadRequestError, ConflictError, CrouterError, InternalServerError, NotFoundError, PermissionDeniedError, RateLimitError, RuntimeVersionError, UnprocessableEntityError, mapError, } from './errors.js';
30
+ export type { RuntimeVersionDetails } from './errors.js';
31
+ export { SDK_VERSION } from './version.js';
30
32
  export { sdkErrorCodes } from './error-codes.js';
31
33
  export type { SdkErrorCode } from './error-codes.js';
32
34
  export type { AuthStatus, AuthStatusParams, CancelParams, EnsureProfileParams, JsonSchema, MessageParams, NodeCreateParams, NodeEventsOptions, NodeOutcomeOptions, OutputSchema, ParsedOutcome, RequestOptions, SchemaOutput, } from './types.js';
package/dist/index.js CHANGED
@@ -9,5 +9,6 @@ export { generateKeyPair } from './oauth/keygen.js';
9
9
  export { parsePrivateJwk, privateKeyFromFile } from './oauth/keygen.js';
10
10
  export { cursorFromRequest, forwardRunEvents } from './resources/forward.js';
11
11
  export { describeToolDefault, followActivity } from './resources/activity.js';
12
- export { APIError, APIConnectionError, APIConnectionTimeoutError, APIUserAbortError, AuthenticationError, BadRequestError, ConflictError, CrouterError, InternalServerError, NotFoundError, PermissionDeniedError, RateLimitError, UnprocessableEntityError, mapError, } from './errors.js';
12
+ export { APIError, APIConnectionError, APIConnectionTimeoutError, APIUserAbortError, AuthenticationError, BadRequestError, ConflictError, CrouterError, InternalServerError, NotFoundError, PermissionDeniedError, RateLimitError, RuntimeVersionError, UnprocessableEntityError, mapError, } from './errors.js';
13
+ export { SDK_VERSION } from './version.js';
13
14
  export { sdkErrorCodes } from './error-codes.js';
@@ -1,4 +1,4 @@
1
- import { APIError, APIUserAbortError } from '../errors.js';
1
+ import { APIError, APIUserAbortError, mapError } from '../errors.js';
2
2
  /** A resumable run event stream. Closing it does not stop the run. */
3
3
  export class RunStream {
4
4
  open;
@@ -202,5 +202,5 @@ export class RunStream {
202
202
  async function streamResponseError(response) {
203
203
  const payload = await response.json();
204
204
  const error = payload.error;
205
- return new APIError(response.status, error?.code ?? 'invalid_response', error?.message ?? `HTTP ${response.status}`, error?.details, response.headers, error);
205
+ return mapError(new APIError(response.status, error?.code ?? 'invalid_response', error?.message ?? `HTTP ${response.status}`, error?.details, response.headers, error));
206
206
  }
@@ -0,0 +1,2 @@
1
+ /** This SDK's release version. It is also the oldest runtime the SDK accepts: one release is one version set. */
2
+ export declare const SDK_VERSION = "0.3.392";
@@ -0,0 +1,3 @@
1
+ // Written by scripts/release-version.mjs on every release; do not edit by hand.
2
+ /** This SDK's release version. It is also the oldest runtime the SDK accepts: one release is one version set. */
3
+ export const SDK_VERSION = '0.3.392';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crouter/sdk",
3
- "version": "0.3.390",
3
+ "version": "0.3.392",
4
4
  "description": "Typed Node and browser client for running crouter agents through the crtrd /v1 API.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -31,8 +31,8 @@
31
31
  "build": "tsc -p tsconfig.json && chmod 755 dist/keygen-cli.js"
32
32
  },
33
33
  "dependencies": {
34
- "@crouter/api": "^0.3.390",
35
- "@crouter/identity": "^0.3.390",
34
+ "@crouter/api": "0.3.392",
35
+ "@crouter/identity": "0.3.392",
36
36
  "jose": "^6.2.1"
37
37
  },
38
38
  "peerDependencies": {