@crouter/sdk 0.3.389 → 0.3.391
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 +19 -0
- package/dist/client.js +37 -5
- package/dist/error-codes.d.ts +1 -1
- package/dist/error-codes.js +2 -0
- package/dist/errors.d.ts +14 -0
- package/dist/errors.js +34 -23
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -1
- package/dist/resources/run-stream.js +2 -2
- package/dist/resources/runs.d.ts +8 -1
- package/dist/resources/runs.js +18 -6
- package/dist/version.d.ts +2 -0
- package/dist/version.js +3 -0
- package/package.json +3 -3
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.
|
|
@@ -97,6 +114,8 @@ const text = await client.runs.reply(runId, sent).text();
|
|
|
97
114
|
|
|
98
115
|
To answer a first message in the run's first turn, pass it to `runs.start`: `message` is stored with the run and always joins the first turn as context ahead of `prompt`, and the result's `message` is what `runs.reply` takes. (A `runs.message(id, text, {start_turn: false})` sent after `runs.start` joins the first turn only if it is stored before the run claims that turn's messages.)
|
|
99
116
|
|
|
117
|
+
`runs.message(id, text, {idempotencyKey})` sends an `Idempotency-Key` (a fresh `crypto.randomUUID()` per call, reused across the SDK's own retries). The daemon delivers a key once for 24 hours and returns the original result on replay; the same key with a different body fails with `409 idempotency_conflict`. Pass your own key, derived from your message id, to dedupe across process restarts.
|
|
118
|
+
|
|
100
119
|
```ts
|
|
101
120
|
const run = await client.runs.start({prompt: instructions, message: 'Hello'});
|
|
102
121
|
const {text, ended, error} = await client.runs.reply(run.run_id, run.message!).collect({onDelta: (delta) => send(delta)});
|
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']) :
|
|
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:
|
|
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;
|
package/dist/error-codes.d.ts
CHANGED
|
@@ -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];
|
package/dist/error-codes.js
CHANGED
|
@@ -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 === '
|
|
49
|
-
?
|
|
50
|
-
: error.
|
|
51
|
-
?
|
|
52
|
-
: error.
|
|
53
|
-
?
|
|
54
|
-
: error.
|
|
55
|
-
?
|
|
56
|
-
: error.status ===
|
|
57
|
-
?
|
|
58
|
-
: error.status ===
|
|
59
|
-
?
|
|
60
|
-
: error.status ===
|
|
61
|
-
?
|
|
62
|
-
: error.status ===
|
|
63
|
-
?
|
|
64
|
-
: error.status ===
|
|
65
|
-
?
|
|
66
|
-
: error.status ===
|
|
67
|
-
?
|
|
68
|
-
: error.status
|
|
69
|
-
?
|
|
70
|
-
:
|
|
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
|
}
|
package/dist/resources/runs.d.ts
CHANGED
|
@@ -138,9 +138,16 @@ export declare class Runs {
|
|
|
138
138
|
* `start_turn: false` stores it without starting a turn; it is delivered as
|
|
139
139
|
* context at the start of the next turn, ahead of that turn's input. For a
|
|
140
140
|
* new run that is the first turn only if it is stored before the run claims
|
|
141
|
-
* its first turn's messages; to guarantee it, pass `message` to `runs.start`.
|
|
141
|
+
* its first turn's messages; to guarantee it, pass `message` to `runs.start`.
|
|
142
|
+
*
|
|
143
|
+
* The send carries an `Idempotency-Key`: `idempotencyKey` when given, otherwise a UUID generated for this
|
|
144
|
+
* call and reused on every retry of it. The daemon keeps the key for 24 hours; a repeat with the same key
|
|
145
|
+
* and the same message returns the first send's result without delivering again, and the same key with a
|
|
146
|
+
* different message or `start_turn` throws `ConflictError` `idempotency_conflict`. To make a send safe
|
|
147
|
+
* across your own restarts, derive the key from your own record of the send (a batch or row id). */
|
|
142
148
|
message(runId: string, message: string, options?: RequestOptions & {
|
|
143
149
|
start_turn?: boolean;
|
|
150
|
+
idempotencyKey?: string;
|
|
144
151
|
}): Promise<SentRunMessage>;
|
|
145
152
|
interrupt(runId: string, options?: RequestOptions & {
|
|
146
153
|
children?: 'stop' | 'continue';
|
package/dist/resources/runs.js
CHANGED
|
@@ -19,8 +19,7 @@ export class Runs {
|
|
|
19
19
|
return await this.request('POST', '/v1/runs', body, { ...rest, maxRetries: 0, headers: { ...rest.headers, 'Idempotency-Key': idempotencyKey } });
|
|
20
20
|
}
|
|
21
21
|
catch (error) {
|
|
22
|
-
if (attempt >= maxRetries || !(error instanceof APIError) ||
|
|
23
|
-
!(error.retryable === true || error.code === 'transport_error' || error.code === 'daemon_request_interrupted' || error.code === 'connection_error' || error.code === 'request_timeout'))
|
|
22
|
+
if (attempt >= maxRetries || !(error instanceof APIError) || !retryableSend(error))
|
|
24
23
|
throw error;
|
|
25
24
|
await pause(error.retryAfterS ? error.retryAfterS * 1_000 : 500 * 2 ** attempt, rest.signal);
|
|
26
25
|
}
|
|
@@ -53,15 +52,24 @@ export class Runs {
|
|
|
53
52
|
* `start_turn: false` stores it without starting a turn; it is delivered as
|
|
54
53
|
* context at the start of the next turn, ahead of that turn's input. For a
|
|
55
54
|
* new run that is the first turn only if it is stored before the run claims
|
|
56
|
-
* its first turn's messages; to guarantee it, pass `message` to `runs.start`.
|
|
55
|
+
* its first turn's messages; to guarantee it, pass `message` to `runs.start`.
|
|
56
|
+
*
|
|
57
|
+
* The send carries an `Idempotency-Key`: `idempotencyKey` when given, otherwise a UUID generated for this
|
|
58
|
+
* call and reused on every retry of it. The daemon keeps the key for 24 hours; a repeat with the same key
|
|
59
|
+
* and the same message returns the first send's result without delivering again, and the same key with a
|
|
60
|
+
* different message or `start_turn` throws `ConflictError` `idempotency_conflict`. To make a send safe
|
|
61
|
+
* across your own restarts, derive the key from your own record of the send (a batch or row id). */
|
|
57
62
|
async message(runId, message, options = {}) {
|
|
58
|
-
const { maxRetries = 2, start_turn, ...rest } = options;
|
|
63
|
+
const { idempotencyKey = crypto.randomUUID(), maxRetries = 2, start_turn, ...rest } = options;
|
|
64
|
+
const body = { message, ...(start_turn === undefined ? {} : { start_turn }) };
|
|
65
|
+
// One key per logical send, reused on every retry: the daemon answers a repeat with the first send's
|
|
66
|
+
// result, so a retry after a lost response cannot deliver the message twice.
|
|
59
67
|
for (let attempt = 0;; attempt++) {
|
|
60
68
|
try {
|
|
61
|
-
return await this.request('POST', `${runPath(runId)}/messages`, {
|
|
69
|
+
return await this.request('POST', `${runPath(runId)}/messages`, body, { ...rest, maxRetries: 0, headers: { ...rest.headers, 'Idempotency-Key': idempotencyKey } });
|
|
62
70
|
}
|
|
63
71
|
catch (error) {
|
|
64
|
-
if (attempt >= maxRetries || !(error instanceof APIError) || error
|
|
72
|
+
if (attempt >= maxRetries || !(error instanceof APIError) || !retryableSend(error))
|
|
65
73
|
throw error;
|
|
66
74
|
await pause(error.retryAfterS ? error.retryAfterS * 1_000 : 500 * 2 ** attempt, rest.signal);
|
|
67
75
|
}
|
|
@@ -162,6 +170,10 @@ export class Runs {
|
|
|
162
170
|
}
|
|
163
171
|
}
|
|
164
172
|
}
|
|
173
|
+
/** A keyed send is safe to repeat after a refusal the daemon marked retryable or a transport failure. */
|
|
174
|
+
function retryableSend(error) {
|
|
175
|
+
return error.retryable === true || error.code === 'transport_error' || error.code === 'daemon_request_interrupted' || error.code === 'connection_error' || error.code === 'request_timeout';
|
|
176
|
+
}
|
|
165
177
|
function runPath(id) {
|
|
166
178
|
if (!/^[A-Za-z0-9_-]+$/.test(id))
|
|
167
179
|
throw new TypeError('Invalid run id');
|
package/dist/version.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crouter/sdk",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.391",
|
|
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": "
|
|
35
|
-
"@crouter/identity": "
|
|
34
|
+
"@crouter/api": "0.3.391",
|
|
35
|
+
"@crouter/identity": "0.3.391",
|
|
36
36
|
"jose": "^6.2.1"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|