@crouter/sdk 0.3.389 → 0.3.390

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
@@ -97,6 +97,8 @@ const text = await client.runs.reply(runId, sent).text();
97
97
 
98
98
  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
99
 
100
+ `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.
101
+
100
102
  ```ts
101
103
  const run = await client.runs.start({prompt: instructions, message: 'Hello'});
102
104
  const {text, ended, error} = await client.runs.reply(run.run_id, run.message!).collect({onDelta: (delta) => send(delta)});
@@ -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';
@@ -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`, { message, ...(start_turn === undefined ? {} : { start_turn }) }, { ...rest, maxRetries: 0 });
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.retryable !== true || error.status === 0)
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crouter/sdk",
3
- "version": "0.3.389",
3
+ "version": "0.3.390",
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.389",
35
- "@crouter/identity": "^0.3.389",
34
+ "@crouter/api": "^0.3.390",
35
+ "@crouter/identity": "^0.3.390",
36
36
  "jose": "^6.2.1"
37
37
  },
38
38
  "peerDependencies": {