@1kbirds/chidori 3.6.0 → 3.7.0

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/agent.js CHANGED
@@ -44,3 +44,31 @@ export function run(handler) {
44
44
  throw new Error("run() is only available inside the chidori runtime; this import is " +
45
45
  "replaced when an agent runs under chidori.");
46
46
  }
47
+ /**
48
+ * Wrap a function into a {@link ToolHandle} by attaching its documented
49
+ * signature — a `name`, a `description`, and a JSON-schema for its
50
+ * `parameters`. That signature is all the model needs to call the function;
51
+ * the function itself is just code. No `tools/` directory, no registry —
52
+ * import (or define inline) the handle like any other value. `parameters`
53
+ * defaults to an empty object schema; `description` to `""`. Inside the
54
+ * runtime this import is replaced by the live implementation, exactly like
55
+ * `run` and `chidori`.
56
+ *
57
+ * ```ts
58
+ * import { chidori, run, defineTool } from "chidori:agent";
59
+ *
60
+ * const search = defineTool({
61
+ * name: "search",
62
+ * description: "Search the corpus.",
63
+ * parameters: { type: "object", properties: { q: { type: "string" } }, required: ["q"] },
64
+ * run: async ({ q }) => (await fetch(BASE + q)).json(),
65
+ * });
66
+ *
67
+ * run(async () => chidori.prompt("find chidori", { tools: [search], maxTurns: 6 }));
68
+ * ```
69
+ */
70
+ export function defineTool(def) {
71
+ void def;
72
+ throw new Error("defineTool() is only available inside the chidori runtime; this import " +
73
+ "is replaced when an agent runs under chidori.");
74
+ }
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * dependencies; uses the global `fetch` available in Node 18+ and browsers.
5
5
  */
6
6
  import type { SignalSender } from "./agent.js";
7
- export type { AgentFunction, AgentJson, CacheTtl, Chidori, CompactOptions, Context, Conversation, ConversationLoopOptions, ConversationOptions, ConversationTurn, DatePolicy, InputOptions, JsonObject, JsonSchema, LlmResponseJson, MapSetSnapshotPolicy, MemoryAction, ParallelOptions, PromptOptions, PromptStreamType, RandomPolicy, RetryOptions, RuntimePolicyConfig, Signal, SignalOptions, SignalSender, SignalTimeout, ToolDefinition, ToolFunction, TryCallResult, TypeScriptImportPolicy, WorkspaceEntry, WorkspaceFileStatus, WorkspaceHost, WorkspaceListOptions, WorkspaceWriteOptions, } from "./agent.js";
7
+ export type { ActorHandle, ActorMessage, ActorOutcome, ActorOutcomeStatus, ActorRestartStrategy, Actors, ActorStatus, ActorStillRunning, AgentFunction, AgentJson, AgentOutput, AppData, BranchOptions, BranchOutcome, BranchStatus, BranchVariant, CacheTtl, Chidori, ChidoriUtil, CompactOptions, Context, Conversation, ConversationLoopOptions, ConversationOptions, ConversationTurn, DatePolicy, DetachedAgentHandle, DetachedAgentOutcome, DetachedAgents, DetachedAgentStatus, InputOptions, JoinActorOptions, JsonObject, JsonSchema, LlmResponseJson, LogFields, MapSetSnapshotPolicy, MemoryStore, ParallelOptions, PromptOptions, PromptStreamType, RandomPolicy, ReceiveOptions, RetryOptions, RuntimePolicyConfig, Signal, SignalOptions, SignalSender, SignalTimeout, SpawnActorOptions, SpawnAgentOptions, ToolDefinition, ToolFunction, TryCallResult, TypeScriptImportPolicy, WorkspaceEntry, WorkspaceFileStatus, WorkspaceHost, WorkspaceListOptions, WorkspaceWriteOptions, } from "./agent.js";
8
8
  export { chidori, run } from "./agent.js";
9
9
  /** JSON-serialisable value — what agents produce as output and accept as input. */
10
10
  export type Json = null | boolean | number | string | Json[] | {
@@ -168,7 +168,7 @@ export declare class Session {
168
168
  pendingSignalName: string | null;
169
169
  /**
170
170
  * The full awaited name set when paused on a signal listen point: `[name]`
171
- * for `chidori.signal(name)`, the listen set for `chidori.signalAny(names)`.
171
+ * for `chidori.signal(name)`, the listen set for the fan-in `chidori.signal(names)`.
172
172
  * Empty for non-signal states.
173
173
  */
174
174
  pendingSignalNames: string[];
@@ -178,6 +178,18 @@ export declare class Session {
178
178
  * when it passes. `null` when the pause has no timeout.
179
179
  */
180
180
  pendingSignalDeadline: string | null;
181
+ /**
182
+ * The artifact under review for an `input()` pause created with
183
+ * `{ details }` (a draft, a diff) — surface it so a human never approves
184
+ * blind. `null` when the pause carries no details.
185
+ */
186
+ pendingDetails: string | null;
187
+ /**
188
+ * The durable run directory id (`.chidori/runs/<run_id>`) this session
189
+ * journals into. Deliberately distinct from the session id: `chidori
190
+ * resume <agent.ts> <run_id>` and `chidori trace <run_id>` take THIS id.
191
+ */
192
+ runId: string | null;
181
193
  constructor(id: string, status: SessionStatus, input: Json, output?: Json | null, error?: string | null, callLog?: CallRecord[], pendingPrompt?: string | null, client?: AgentClient | null, snapshotManifest?: SnapshotManifest | null,
182
194
  /**
183
195
  * When the run is `paused` at a `chidori.signal(name)` listen point, the
@@ -187,7 +199,7 @@ export declare class Session {
187
199
  pendingSignalName?: string | null,
188
200
  /**
189
201
  * The full awaited name set when paused on a signal listen point: `[name]`
190
- * for `chidori.signal(name)`, the listen set for `chidori.signalAny(names)`.
202
+ * for `chidori.signal(name)`, the listen set for the fan-in `chidori.signal(names)`.
191
203
  * Empty for non-signal states.
192
204
  */
193
205
  pendingSignalNames?: string[],
@@ -196,7 +208,19 @@ export declare class Session {
196
208
  * `timeoutMs`; the server resolves the pause with the timeout sentinel
197
209
  * when it passes. `null` when the pause has no timeout.
198
210
  */
199
- pendingSignalDeadline?: string | null);
211
+ pendingSignalDeadline?: string | null,
212
+ /**
213
+ * The artifact under review for an `input()` pause created with
214
+ * `{ details }` (a draft, a diff) — surface it so a human never approves
215
+ * blind. `null` when the pause carries no details.
216
+ */
217
+ pendingDetails?: string | null,
218
+ /**
219
+ * The durable run directory id (`.chidori/runs/<run_id>`) this session
220
+ * journals into. Deliberately distinct from the session id: `chidori
221
+ * resume <agent.ts> <run_id>` and `chidori trace <run_id>` take THIS id.
222
+ */
223
+ runId?: string | null);
200
224
  get ok(): boolean;
201
225
  /**
202
226
  * Fetch the full call log from the server (if not already loaded) and
@@ -230,7 +254,7 @@ export type StreamEvent = {
230
254
  error?: string | null;
231
255
  } | {
232
256
  /**
233
- * The streamed run paused at a `signal()` / `signalAny()` listen point
257
+ * The streamed run paused at a `signal()` listen point
234
258
  * and stays live: the worker keeps supervising, and a delivered signal
235
259
  * (or the `timeoutMs` deadline) resumes it in-process — further events
236
260
  * follow on the same stream. Deliver with `client.signal`.
@@ -249,6 +273,86 @@ export type StreamEvent = {
249
273
  output?: Json;
250
274
  error?: string;
251
275
  };
276
+ /** Base class for every error the SDK throws. `catch (e) { if (e instanceof
277
+ * AgentClientError) ... }` covers HTTP failures, timeouts, and connection
278
+ * errors alike. */
279
+ export declare class AgentClientError extends Error {
280
+ }
281
+ /**
282
+ * A non-2xx HTTP response. Carries the parsed `status` so callers can
283
+ * distinguish the server's documented semantics — e.g. for `client.signal`:
284
+ * 400 (empty name), 404 (unknown session), 409 (terminal run) — instead of
285
+ * string-matching the message.
286
+ */
287
+ export declare class HttpError extends AgentClientError {
288
+ /** HTTP method of the failed request. */
289
+ readonly method: string;
290
+ /** Request path relative to the base URL. */
291
+ readonly path: string;
292
+ /** HTTP status code (400, 404, 409, 500, ...). */
293
+ readonly status: number;
294
+ /** Raw response body text (may be empty). */
295
+ readonly body: string;
296
+ /** The server's `error` field, when the body was `{ "error": ... }`. */
297
+ readonly detail: string | null;
298
+ constructor(
299
+ /** HTTP method of the failed request. */
300
+ method: string,
301
+ /** Request path relative to the base URL. */
302
+ path: string,
303
+ /** HTTP status code (400, 404, 409, 500, ...). */
304
+ status: number,
305
+ /** Raw response body text (may be empty). */
306
+ body: string,
307
+ /** The server's `error` field, when the body was `{ "error": ... }`. */
308
+ detail?: string | null);
309
+ static fromResponse(method: string, path: string, resp: Response): Promise<HttpError>;
310
+ }
311
+ /** The request exceeded the client's `timeoutMs` without completing. */
312
+ export declare class TimeoutError extends AgentClientError {
313
+ readonly method: string;
314
+ readonly path: string;
315
+ readonly timeoutMs: number;
316
+ constructor(method: string, path: string, timeoutMs: number);
317
+ }
318
+ /** The request never produced an HTTP response (refused, reset, DNS, ...). */
319
+ export declare class ConnectionError extends AgentClientError {
320
+ readonly method: string;
321
+ readonly path: string;
322
+ constructor(method: string, path: string, cause: unknown);
323
+ }
324
+ export interface AgentClientOptions {
325
+ /**
326
+ * Bearer token for a server running with `CHIDORI_API_KEY` set (the
327
+ * production posture — see docs/deployment.md). Sent as
328
+ * `Authorization: Bearer <apiKey>` on every request, including `stream()`.
329
+ * Omit against an unauthenticated loopback server.
330
+ */
331
+ apiKey?: string;
332
+ /**
333
+ * Extra headers merged into every request (after `apiKey`, so an explicit
334
+ * `Authorization` entry here wins). Escape hatch for proxies and custom
335
+ * auth schemes the `apiKey` option doesn't cover.
336
+ */
337
+ headers?: Record<string, string>;
338
+ /**
339
+ * Per-request timeout in milliseconds; `0` disables it. Defaults to
340
+ * 300 000 (5 minutes) — generous because `run()` executes the whole agent
341
+ * before responding, but finite so a hung server surfaces as a
342
+ * `TimeoutError` instead of blocking forever. For `stream()` the timeout
343
+ * covers connection establishment only, never an open event stream.
344
+ */
345
+ timeoutMs?: number;
346
+ /**
347
+ * How many times to retry **idempotent GET requests** after a connection
348
+ * error, timeout, or retryable status (429/502/503/504). Defaults to 2.
349
+ * POST requests are never retried — `run`/`resume`/`signal` are not
350
+ * idempotent, and a blind retry could execute an agent twice.
351
+ */
352
+ retries?: number;
353
+ /** Base delay between retries in milliseconds, doubling per attempt (default 250). */
354
+ retryDelayMs?: number;
355
+ }
252
356
  /**
253
357
  * HTTP client for an `chidori serve` instance.
254
358
  *
@@ -260,10 +364,19 @@ export type StreamEvent = {
260
364
  * const cp = await session.checkpoint();
261
365
  * const replayed = await client.replay(cp); // zero LLM calls
262
366
  * ```
367
+ *
368
+ * Failures throw typed errors (all extending {@link AgentClientError}):
369
+ * {@link HttpError} with a `.status` for non-2xx responses,
370
+ * {@link TimeoutError} after `timeoutMs`, {@link ConnectionError} when no
371
+ * response arrived at all.
263
372
  */
264
373
  export declare class AgentClient {
265
374
  readonly baseUrl: string;
266
- constructor(baseUrl?: string);
375
+ readonly timeoutMs: number;
376
+ readonly retries: number;
377
+ readonly retryDelayMs: number;
378
+ private readonly baseHeaders;
379
+ constructor(baseUrl?: string, options?: AgentClientOptions);
267
380
  health(): Promise<Json>;
268
381
  /**
269
382
  * Create a new session and run the agent with the given input.
@@ -323,10 +436,21 @@ export declare class AgentClient {
323
436
  * events (`prompt_start`, `prompt_delta`, `prompt_end`), then a final
324
437
  * `done` event. Prompt events include `prompt_type` so UIs can filter
325
438
  * progress streams separately from final-answer streams.
439
+ *
440
+ * `options.policyProfile` mirrors `run()`: a built-in profile layered on
441
+ * the server policy with stricter-wins semantics for this session.
326
442
  */
327
- stream(input: Json): AsyncGenerator<StreamEvent, void, void>;
443
+ stream(input: Json, options?: {
444
+ policyProfile?: PolicyProfile;
445
+ }): AsyncGenerator<StreamEvent, void, void>;
328
446
  private sessionFrom;
329
447
  private getJSON;
330
448
  private postJSON;
331
449
  private postJSONWithStatus;
450
+ /**
451
+ * One HTTP exchange with timeout and (for idempotent requests) retries.
452
+ * Resolves with a 2xx `Response`; throws {@link HttpError},
453
+ * {@link TimeoutError}, or {@link ConnectionError} otherwise.
454
+ */
455
+ private request;
332
456
  }
package/dist/index.js CHANGED
@@ -59,6 +59,8 @@ export class Session {
59
59
  pendingSignalName;
60
60
  pendingSignalNames;
61
61
  pendingSignalDeadline;
62
+ pendingDetails;
63
+ runId;
62
64
  constructor(id, status, input, output = null, error = null, callLog = [], pendingPrompt = null, client = null, snapshotManifest = null,
63
65
  /**
64
66
  * When the run is `paused` at a `chidori.signal(name)` listen point, the
@@ -68,7 +70,7 @@ export class Session {
68
70
  pendingSignalName = null,
69
71
  /**
70
72
  * The full awaited name set when paused on a signal listen point: `[name]`
71
- * for `chidori.signal(name)`, the listen set for `chidori.signalAny(names)`.
73
+ * for `chidori.signal(name)`, the listen set for the fan-in `chidori.signal(names)`.
72
74
  * Empty for non-signal states.
73
75
  */
74
76
  pendingSignalNames = [],
@@ -77,7 +79,19 @@ export class Session {
77
79
  * `timeoutMs`; the server resolves the pause with the timeout sentinel
78
80
  * when it passes. `null` when the pause has no timeout.
79
81
  */
80
- pendingSignalDeadline = null) {
82
+ pendingSignalDeadline = null,
83
+ /**
84
+ * The artifact under review for an `input()` pause created with
85
+ * `{ details }` (a draft, a diff) — surface it so a human never approves
86
+ * blind. `null` when the pause carries no details.
87
+ */
88
+ pendingDetails = null,
89
+ /**
90
+ * The durable run directory id (`.chidori/runs/<run_id>`) this session
91
+ * journals into. Deliberately distinct from the session id: `chidori
92
+ * resume <agent.ts> <run_id>` and `chidori trace <run_id>` take THIS id.
93
+ */
94
+ runId = null) {
81
95
  this.id = id;
82
96
  this.status = status;
83
97
  this.input = input;
@@ -90,6 +104,8 @@ export class Session {
90
104
  this.pendingSignalName = pendingSignalName;
91
105
  this.pendingSignalNames = pendingSignalNames;
92
106
  this.pendingSignalDeadline = pendingSignalDeadline;
107
+ this.pendingDetails = pendingDetails;
108
+ this.runId = runId;
93
109
  }
94
110
  get ok() {
95
111
  return this.status === "completed";
@@ -115,6 +131,84 @@ export class Session {
115
131
  return this.client.replay(cp);
116
132
  }
117
133
  }
134
+ /** Base class for every error the SDK throws. `catch (e) { if (e instanceof
135
+ * AgentClientError) ... }` covers HTTP failures, timeouts, and connection
136
+ * errors alike. */
137
+ export class AgentClientError extends Error {
138
+ }
139
+ /**
140
+ * A non-2xx HTTP response. Carries the parsed `status` so callers can
141
+ * distinguish the server's documented semantics — e.g. for `client.signal`:
142
+ * 400 (empty name), 404 (unknown session), 409 (terminal run) — instead of
143
+ * string-matching the message.
144
+ */
145
+ export class HttpError extends AgentClientError {
146
+ method;
147
+ path;
148
+ status;
149
+ body;
150
+ detail;
151
+ constructor(
152
+ /** HTTP method of the failed request. */
153
+ method,
154
+ /** Request path relative to the base URL. */
155
+ path,
156
+ /** HTTP status code (400, 404, 409, 500, ...). */
157
+ status,
158
+ /** Raw response body text (may be empty). */
159
+ body,
160
+ /** The server's `error` field, when the body was `{ "error": ... }`. */
161
+ detail = null) {
162
+ super(`${method} ${path} failed: HTTP ${status}${detail || body ? `: ${detail ?? body}` : ""}`);
163
+ this.method = method;
164
+ this.path = path;
165
+ this.status = status;
166
+ this.body = body;
167
+ this.detail = detail;
168
+ this.name = "HttpError";
169
+ }
170
+ static async fromResponse(method, path, resp) {
171
+ const body = await resp.text().catch(() => "");
172
+ let detail = null;
173
+ try {
174
+ const parsed = JSON.parse(body);
175
+ if (typeof parsed.error === "string")
176
+ detail = parsed.error;
177
+ }
178
+ catch {
179
+ // not JSON — leave detail null
180
+ }
181
+ return new HttpError(method, path, resp.status, body, detail);
182
+ }
183
+ }
184
+ /** The request exceeded the client's `timeoutMs` without completing. */
185
+ export class TimeoutError extends AgentClientError {
186
+ method;
187
+ path;
188
+ timeoutMs;
189
+ constructor(method, path, timeoutMs) {
190
+ super(`${method} ${path} timed out after ${timeoutMs}ms`);
191
+ this.method = method;
192
+ this.path = path;
193
+ this.timeoutMs = timeoutMs;
194
+ this.name = "TimeoutError";
195
+ }
196
+ }
197
+ /** The request never produced an HTTP response (refused, reset, DNS, ...). */
198
+ export class ConnectionError extends AgentClientError {
199
+ method;
200
+ path;
201
+ constructor(method, path, cause) {
202
+ super(`${method} ${path} failed: ${cause instanceof Error ? cause.message : String(cause)}`, {
203
+ cause,
204
+ });
205
+ this.method = method;
206
+ this.path = path;
207
+ this.name = "ConnectionError";
208
+ }
209
+ }
210
+ /** Response statuses worth retrying on idempotent requests. */
211
+ const RETRYABLE_STATUS = new Set([429, 502, 503, 504]);
118
212
  /**
119
213
  * HTTP client for an `chidori serve` instance.
120
214
  *
@@ -126,11 +220,27 @@ export class Session {
126
220
  * const cp = await session.checkpoint();
127
221
  * const replayed = await client.replay(cp); // zero LLM calls
128
222
  * ```
223
+ *
224
+ * Failures throw typed errors (all extending {@link AgentClientError}):
225
+ * {@link HttpError} with a `.status` for non-2xx responses,
226
+ * {@link TimeoutError} after `timeoutMs`, {@link ConnectionError} when no
227
+ * response arrived at all.
129
228
  */
130
229
  export class AgentClient {
131
230
  baseUrl;
132
- constructor(baseUrl = "http://localhost:8080") {
231
+ timeoutMs;
232
+ retries;
233
+ retryDelayMs;
234
+ baseHeaders;
235
+ constructor(baseUrl = "http://localhost:8080", options = {}) {
133
236
  this.baseUrl = baseUrl.replace(/\/$/, "");
237
+ this.timeoutMs = options.timeoutMs ?? 300_000;
238
+ this.retries = options.retries ?? 2;
239
+ this.retryDelayMs = options.retryDelayMs ?? 250;
240
+ this.baseHeaders = {
241
+ ...(options.apiKey ? { Authorization: `Bearer ${options.apiKey}` } : {}),
242
+ ...(options.headers ?? {}),
243
+ };
134
244
  }
135
245
  async health() {
136
246
  return (await this.getJSON("/health"));
@@ -224,15 +334,44 @@ export class AgentClient {
224
334
  * events (`prompt_start`, `prompt_delta`, `prompt_end`), then a final
225
335
  * `done` event. Prompt events include `prompt_type` so UIs can filter
226
336
  * progress streams separately from final-answer streams.
337
+ *
338
+ * `options.policyProfile` mirrors `run()`: a built-in profile layered on
339
+ * the server policy with stricter-wins semantics for this session.
227
340
  */
228
- async *stream(input) {
229
- const resp = await fetch(`${this.baseUrl}/sessions/stream`, {
230
- method: "POST",
231
- headers: { "Content-Type": "application/json", Accept: "text/event-stream" },
232
- body: JSON.stringify({ input }),
233
- });
341
+ async *stream(input, options) {
342
+ // The timeout covers connection establishment (until response headers
343
+ // arrive), not the open event stream — a healthy run may stream for a
344
+ // long time between events.
345
+ const controller = new AbortController();
346
+ const timer = this.timeoutMs > 0 ? setTimeout(() => controller.abort(), this.timeoutMs) : null;
347
+ const body = { input };
348
+ if (options?.policyProfile) {
349
+ body.policy_profile = options.policyProfile;
350
+ }
351
+ let resp;
352
+ try {
353
+ resp = await fetch(`${this.baseUrl}/sessions/stream`, {
354
+ method: "POST",
355
+ headers: {
356
+ ...this.baseHeaders,
357
+ "Content-Type": "application/json",
358
+ Accept: "text/event-stream",
359
+ },
360
+ body: JSON.stringify(body),
361
+ signal: controller.signal,
362
+ });
363
+ }
364
+ catch (err) {
365
+ throw controller.signal.aborted
366
+ ? new TimeoutError("POST", "/sessions/stream", this.timeoutMs)
367
+ : new ConnectionError("POST", "/sessions/stream", err);
368
+ }
369
+ finally {
370
+ if (timer)
371
+ clearTimeout(timer);
372
+ }
234
373
  if (!resp.ok || !resp.body) {
235
- throw new Error(`stream request failed: ${resp.status}`);
374
+ throw await HttpError.fromResponse("POST", "/sessions/stream", resp);
236
375
  }
237
376
  // Minimal SSE parser — just enough for the events our server emits.
238
377
  const reader = resp.body.getReader();
@@ -255,31 +394,75 @@ export class AgentClient {
255
394
  }
256
395
  // -- internals ----------------------------------------------------------
257
396
  sessionFrom(data, input) {
258
- return new Session(data.id, data.status ?? "failed", input, data.output ?? null, data.error ?? null, data.call_log ?? [], data.pending_prompt ?? null, this, data.snapshot_manifest ?? null, data.pending_signal_name ?? null, data.pending_signal_names ?? [], data.pending_signal_deadline ?? null);
397
+ return new Session(data.id, data.status ?? "failed", input, data.output ?? null, data.error ?? null, data.call_log ?? [], data.pending_prompt ?? null, this, data.snapshot_manifest ?? null, data.pending_signal_name ?? null, data.pending_signal_names ?? [], data.pending_signal_deadline ?? null, data.pending_details ?? null, data.run_id ?? null);
259
398
  }
260
399
  async getJSON(path) {
261
- const resp = await fetch(this.baseUrl + path);
262
- if (!resp.ok)
263
- throw await httpError(resp);
400
+ // GETs are idempotent: retry connection failures, timeouts, and
401
+ // retryable statuses with exponential backoff.
402
+ const resp = await this.request("GET", path, undefined, this.retries);
264
403
  return await resp.json();
265
404
  }
266
405
  async postJSON(path, body) {
267
406
  return (await this.postJSONWithStatus(path, body)).data;
268
407
  }
269
408
  async postJSONWithStatus(path, body) {
270
- const resp = await fetch(this.baseUrl + path, {
271
- method: "POST",
272
- headers: { "Content-Type": "application/json" },
273
- body: JSON.stringify(body),
274
- });
275
- if (!resp.ok)
276
- throw await httpError(resp);
409
+ // POSTs are never retried: run/resume/signal are not idempotent.
410
+ const resp = await this.request("POST", path, body, 0);
277
411
  return { status: resp.status, data: (await resp.json()) };
278
412
  }
413
+ /**
414
+ * One HTTP exchange with timeout and (for idempotent requests) retries.
415
+ * Resolves with a 2xx `Response`; throws {@link HttpError},
416
+ * {@link TimeoutError}, or {@link ConnectionError} otherwise.
417
+ */
418
+ async request(method, path, body, retries) {
419
+ let lastError = null;
420
+ for (let attempt = 0; attempt <= retries; attempt++) {
421
+ if (attempt > 0) {
422
+ await sleep(this.retryDelayMs * 2 ** (attempt - 1));
423
+ }
424
+ const controller = new AbortController();
425
+ const timer = this.timeoutMs > 0 ? setTimeout(() => controller.abort(), this.timeoutMs) : null;
426
+ try {
427
+ let resp;
428
+ try {
429
+ resp = await fetch(this.baseUrl + path, {
430
+ method,
431
+ signal: controller.signal,
432
+ headers: body !== undefined
433
+ ? { ...this.baseHeaders, "Content-Type": "application/json" }
434
+ : this.baseHeaders,
435
+ ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
436
+ });
437
+ }
438
+ catch (err) {
439
+ throw controller.signal.aborted
440
+ ? new TimeoutError(method, path, this.timeoutMs)
441
+ : new ConnectionError(method, path, err);
442
+ }
443
+ if (!resp.ok)
444
+ throw await HttpError.fromResponse(method, path, resp);
445
+ return resp;
446
+ }
447
+ catch (err) {
448
+ const retryable = err instanceof TimeoutError ||
449
+ err instanceof ConnectionError ||
450
+ (err instanceof HttpError && RETRYABLE_STATUS.has(err.status));
451
+ if (!retryable || attempt === retries)
452
+ throw err;
453
+ lastError = err;
454
+ }
455
+ finally {
456
+ if (timer)
457
+ clearTimeout(timer);
458
+ }
459
+ }
460
+ // Unreachable: the loop either returns or throws on the last attempt.
461
+ throw lastError ?? new ConnectionError(method, path, "retries exhausted");
462
+ }
279
463
  }
280
- async function httpError(resp) {
281
- const text = await resp.text().catch(() => "");
282
- return new Error(`HTTP ${resp.status}: ${text}`);
464
+ function sleep(ms) {
465
+ return new Promise((resolve) => setTimeout(resolve, ms));
283
466
  }
284
467
  function parseSseFrame(frame) {
285
468
  let event = "message";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1kbirds/chidori",
3
- "version": "3.6.0",
3
+ "version": "3.7.0",
4
4
  "description": "TypeScript SDK for the Chidori agent framework — HTTP client plus agent authoring types.",
5
5
  "keywords": ["ai", "agents", "typescript", "llm", "replay"],
6
6
  "publishConfig": { "access": "public" },
@@ -27,6 +27,7 @@
27
27
  "files": ["dist", "src", "README.md"],
28
28
  "scripts": {
29
29
  "build": "tsc",
30
+ "prepare": "npm run build",
30
31
  "typecheck": "tsc --noEmit",
31
32
  "pretest": "tsc",
32
33
  "test": "node --test test/*.test.mjs"