@1kbirds/chidori 3.5.1 → 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/README.md +81 -6
- package/dist/agent.d.ts +483 -51
- package/dist/agent.js +28 -0
- package/dist/index.d.ts +131 -7
- package/dist/index.js +207 -24
- package/package.json +2 -1
- package/src/agent.ts +543 -59
- package/src/index.ts +258 -22
package/src/index.ts
CHANGED
|
@@ -7,10 +7,25 @@
|
|
|
7
7
|
import type { SignalSender } from "./agent.js";
|
|
8
8
|
|
|
9
9
|
export type {
|
|
10
|
+
ActorHandle,
|
|
11
|
+
ActorMessage,
|
|
12
|
+
ActorOutcome,
|
|
13
|
+
ActorOutcomeStatus,
|
|
14
|
+
ActorRestartStrategy,
|
|
15
|
+
Actors,
|
|
16
|
+
ActorStatus,
|
|
17
|
+
ActorStillRunning,
|
|
10
18
|
AgentFunction,
|
|
11
19
|
AgentJson,
|
|
20
|
+
AgentOutput,
|
|
21
|
+
AppData,
|
|
22
|
+
BranchOptions,
|
|
23
|
+
BranchOutcome,
|
|
24
|
+
BranchStatus,
|
|
25
|
+
BranchVariant,
|
|
12
26
|
CacheTtl,
|
|
13
27
|
Chidori,
|
|
28
|
+
ChidoriUtil,
|
|
14
29
|
CompactOptions,
|
|
15
30
|
Context,
|
|
16
31
|
Conversation,
|
|
@@ -18,22 +33,31 @@ export type {
|
|
|
18
33
|
ConversationOptions,
|
|
19
34
|
ConversationTurn,
|
|
20
35
|
DatePolicy,
|
|
36
|
+
DetachedAgentHandle,
|
|
37
|
+
DetachedAgentOutcome,
|
|
38
|
+
DetachedAgents,
|
|
39
|
+
DetachedAgentStatus,
|
|
21
40
|
InputOptions,
|
|
41
|
+
JoinActorOptions,
|
|
22
42
|
JsonObject,
|
|
23
43
|
JsonSchema,
|
|
24
44
|
LlmResponseJson,
|
|
45
|
+
LogFields,
|
|
25
46
|
MapSetSnapshotPolicy,
|
|
26
|
-
|
|
47
|
+
MemoryStore,
|
|
27
48
|
ParallelOptions,
|
|
28
49
|
PromptOptions,
|
|
29
50
|
PromptStreamType,
|
|
30
51
|
RandomPolicy,
|
|
52
|
+
ReceiveOptions,
|
|
31
53
|
RetryOptions,
|
|
32
54
|
RuntimePolicyConfig,
|
|
33
55
|
Signal,
|
|
34
56
|
SignalOptions,
|
|
35
57
|
SignalSender,
|
|
36
58
|
SignalTimeout,
|
|
59
|
+
SpawnActorOptions,
|
|
60
|
+
SpawnAgentOptions,
|
|
37
61
|
ToolDefinition,
|
|
38
62
|
ToolFunction,
|
|
39
63
|
TryCallResult,
|
|
@@ -256,7 +280,7 @@ export class Session {
|
|
|
256
280
|
public pendingSignalName: string | null = null,
|
|
257
281
|
/**
|
|
258
282
|
* The full awaited name set when paused on a signal listen point: `[name]`
|
|
259
|
-
* for `chidori.signal(name)`, the listen set for `chidori.
|
|
283
|
+
* for `chidori.signal(name)`, the listen set for the fan-in `chidori.signal(names)`.
|
|
260
284
|
* Empty for non-signal states.
|
|
261
285
|
*/
|
|
262
286
|
public pendingSignalNames: string[] = [],
|
|
@@ -266,6 +290,18 @@ export class Session {
|
|
|
266
290
|
* when it passes. `null` when the pause has no timeout.
|
|
267
291
|
*/
|
|
268
292
|
public pendingSignalDeadline: string | null = null,
|
|
293
|
+
/**
|
|
294
|
+
* The artifact under review for an `input()` pause created with
|
|
295
|
+
* `{ details }` (a draft, a diff) — surface it so a human never approves
|
|
296
|
+
* blind. `null` when the pause carries no details.
|
|
297
|
+
*/
|
|
298
|
+
public pendingDetails: string | null = null,
|
|
299
|
+
/**
|
|
300
|
+
* The durable run directory id (`.chidori/runs/<run_id>`) this session
|
|
301
|
+
* journals into. Deliberately distinct from the session id: `chidori
|
|
302
|
+
* resume <agent.ts> <run_id>` and `chidori trace <run_id>` take THIS id.
|
|
303
|
+
*/
|
|
304
|
+
public runId: string | null = null,
|
|
269
305
|
) {}
|
|
270
306
|
|
|
271
307
|
get ok(): boolean {
|
|
@@ -321,7 +357,7 @@ export type StreamEvent =
|
|
|
321
357
|
}
|
|
322
358
|
| {
|
|
323
359
|
/**
|
|
324
|
-
* The streamed run paused at a `signal()`
|
|
360
|
+
* The streamed run paused at a `signal()` listen point
|
|
325
361
|
* and stays live: the worker keeps supervising, and a delivered signal
|
|
326
362
|
* (or the `timeoutMs` deadline) resumes it in-process — further events
|
|
327
363
|
* follow on the same stream. Deliver with `client.signal`.
|
|
@@ -336,6 +372,109 @@ export type StreamEvent =
|
|
|
336
372
|
}
|
|
337
373
|
| { type: "done"; id: string; status: SessionStatus; output?: Json; error?: string };
|
|
338
374
|
|
|
375
|
+
/** Base class for every error the SDK throws. `catch (e) { if (e instanceof
|
|
376
|
+
* AgentClientError) ... }` covers HTTP failures, timeouts, and connection
|
|
377
|
+
* errors alike. */
|
|
378
|
+
export class AgentClientError extends Error {}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* A non-2xx HTTP response. Carries the parsed `status` so callers can
|
|
382
|
+
* distinguish the server's documented semantics — e.g. for `client.signal`:
|
|
383
|
+
* 400 (empty name), 404 (unknown session), 409 (terminal run) — instead of
|
|
384
|
+
* string-matching the message.
|
|
385
|
+
*/
|
|
386
|
+
export class HttpError extends AgentClientError {
|
|
387
|
+
constructor(
|
|
388
|
+
/** HTTP method of the failed request. */
|
|
389
|
+
readonly method: string,
|
|
390
|
+
/** Request path relative to the base URL. */
|
|
391
|
+
readonly path: string,
|
|
392
|
+
/** HTTP status code (400, 404, 409, 500, ...). */
|
|
393
|
+
readonly status: number,
|
|
394
|
+
/** Raw response body text (may be empty). */
|
|
395
|
+
readonly body: string,
|
|
396
|
+
/** The server's `error` field, when the body was `{ "error": ... }`. */
|
|
397
|
+
readonly detail: string | null = null,
|
|
398
|
+
) {
|
|
399
|
+
super(`${method} ${path} failed: HTTP ${status}${detail || body ? `: ${detail ?? body}` : ""}`);
|
|
400
|
+
this.name = "HttpError";
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
static async fromResponse(method: string, path: string, resp: Response): Promise<HttpError> {
|
|
404
|
+
const body = await resp.text().catch(() => "");
|
|
405
|
+
let detail: string | null = null;
|
|
406
|
+
try {
|
|
407
|
+
const parsed = JSON.parse(body) as { error?: unknown };
|
|
408
|
+
if (typeof parsed.error === "string") detail = parsed.error;
|
|
409
|
+
} catch {
|
|
410
|
+
// not JSON — leave detail null
|
|
411
|
+
}
|
|
412
|
+
return new HttpError(method, path, resp.status, body, detail);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** The request exceeded the client's `timeoutMs` without completing. */
|
|
417
|
+
export class TimeoutError extends AgentClientError {
|
|
418
|
+
constructor(
|
|
419
|
+
readonly method: string,
|
|
420
|
+
readonly path: string,
|
|
421
|
+
readonly timeoutMs: number,
|
|
422
|
+
) {
|
|
423
|
+
super(`${method} ${path} timed out after ${timeoutMs}ms`);
|
|
424
|
+
this.name = "TimeoutError";
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** The request never produced an HTTP response (refused, reset, DNS, ...). */
|
|
429
|
+
export class ConnectionError extends AgentClientError {
|
|
430
|
+
constructor(
|
|
431
|
+
readonly method: string,
|
|
432
|
+
readonly path: string,
|
|
433
|
+
cause: unknown,
|
|
434
|
+
) {
|
|
435
|
+
super(`${method} ${path} failed: ${cause instanceof Error ? cause.message : String(cause)}`, {
|
|
436
|
+
cause,
|
|
437
|
+
});
|
|
438
|
+
this.name = "ConnectionError";
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** Response statuses worth retrying on idempotent requests. */
|
|
443
|
+
const RETRYABLE_STATUS = new Set([429, 502, 503, 504]);
|
|
444
|
+
|
|
445
|
+
export interface AgentClientOptions {
|
|
446
|
+
/**
|
|
447
|
+
* Bearer token for a server running with `CHIDORI_API_KEY` set (the
|
|
448
|
+
* production posture — see docs/deployment.md). Sent as
|
|
449
|
+
* `Authorization: Bearer <apiKey>` on every request, including `stream()`.
|
|
450
|
+
* Omit against an unauthenticated loopback server.
|
|
451
|
+
*/
|
|
452
|
+
apiKey?: string;
|
|
453
|
+
/**
|
|
454
|
+
* Extra headers merged into every request (after `apiKey`, so an explicit
|
|
455
|
+
* `Authorization` entry here wins). Escape hatch for proxies and custom
|
|
456
|
+
* auth schemes the `apiKey` option doesn't cover.
|
|
457
|
+
*/
|
|
458
|
+
headers?: Record<string, string>;
|
|
459
|
+
/**
|
|
460
|
+
* Per-request timeout in milliseconds; `0` disables it. Defaults to
|
|
461
|
+
* 300 000 (5 minutes) — generous because `run()` executes the whole agent
|
|
462
|
+
* before responding, but finite so a hung server surfaces as a
|
|
463
|
+
* `TimeoutError` instead of blocking forever. For `stream()` the timeout
|
|
464
|
+
* covers connection establishment only, never an open event stream.
|
|
465
|
+
*/
|
|
466
|
+
timeoutMs?: number;
|
|
467
|
+
/**
|
|
468
|
+
* How many times to retry **idempotent GET requests** after a connection
|
|
469
|
+
* error, timeout, or retryable status (429/502/503/504). Defaults to 2.
|
|
470
|
+
* POST requests are never retried — `run`/`resume`/`signal` are not
|
|
471
|
+
* idempotent, and a blind retry could execute an agent twice.
|
|
472
|
+
*/
|
|
473
|
+
retries?: number;
|
|
474
|
+
/** Base delay between retries in milliseconds, doubling per attempt (default 250). */
|
|
475
|
+
retryDelayMs?: number;
|
|
476
|
+
}
|
|
477
|
+
|
|
339
478
|
/**
|
|
340
479
|
* HTTP client for an `chidori serve` instance.
|
|
341
480
|
*
|
|
@@ -347,12 +486,28 @@ export type StreamEvent =
|
|
|
347
486
|
* const cp = await session.checkpoint();
|
|
348
487
|
* const replayed = await client.replay(cp); // zero LLM calls
|
|
349
488
|
* ```
|
|
489
|
+
*
|
|
490
|
+
* Failures throw typed errors (all extending {@link AgentClientError}):
|
|
491
|
+
* {@link HttpError} with a `.status` for non-2xx responses,
|
|
492
|
+
* {@link TimeoutError} after `timeoutMs`, {@link ConnectionError} when no
|
|
493
|
+
* response arrived at all.
|
|
350
494
|
*/
|
|
351
495
|
export class AgentClient {
|
|
352
496
|
readonly baseUrl: string;
|
|
497
|
+
readonly timeoutMs: number;
|
|
498
|
+
readonly retries: number;
|
|
499
|
+
readonly retryDelayMs: number;
|
|
500
|
+
private readonly baseHeaders: Record<string, string>;
|
|
353
501
|
|
|
354
|
-
constructor(baseUrl: string = "http://localhost:8080") {
|
|
502
|
+
constructor(baseUrl: string = "http://localhost:8080", options: AgentClientOptions = {}) {
|
|
355
503
|
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
504
|
+
this.timeoutMs = options.timeoutMs ?? 300_000;
|
|
505
|
+
this.retries = options.retries ?? 2;
|
|
506
|
+
this.retryDelayMs = options.retryDelayMs ?? 250;
|
|
507
|
+
this.baseHeaders = {
|
|
508
|
+
...(options.apiKey ? { Authorization: `Bearer ${options.apiKey}` } : {}),
|
|
509
|
+
...(options.headers ?? {}),
|
|
510
|
+
};
|
|
356
511
|
}
|
|
357
512
|
|
|
358
513
|
async health(): Promise<Json> {
|
|
@@ -472,15 +627,45 @@ export class AgentClient {
|
|
|
472
627
|
* events (`prompt_start`, `prompt_delta`, `prompt_end`), then a final
|
|
473
628
|
* `done` event. Prompt events include `prompt_type` so UIs can filter
|
|
474
629
|
* progress streams separately from final-answer streams.
|
|
630
|
+
*
|
|
631
|
+
* `options.policyProfile` mirrors `run()`: a built-in profile layered on
|
|
632
|
+
* the server policy with stricter-wins semantics for this session.
|
|
475
633
|
*/
|
|
476
|
-
async *stream(
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
634
|
+
async *stream(
|
|
635
|
+
input: Json,
|
|
636
|
+
options?: { policyProfile?: PolicyProfile },
|
|
637
|
+
): AsyncGenerator<StreamEvent, void, void> {
|
|
638
|
+
// The timeout covers connection establishment (until response headers
|
|
639
|
+
// arrive), not the open event stream — a healthy run may stream for a
|
|
640
|
+
// long time between events.
|
|
641
|
+
const controller = new AbortController();
|
|
642
|
+
const timer =
|
|
643
|
+
this.timeoutMs > 0 ? setTimeout(() => controller.abort(), this.timeoutMs) : null;
|
|
644
|
+
const body: Record<string, unknown> = { input };
|
|
645
|
+
if (options?.policyProfile) {
|
|
646
|
+
body.policy_profile = options.policyProfile;
|
|
647
|
+
}
|
|
648
|
+
let resp: Response;
|
|
649
|
+
try {
|
|
650
|
+
resp = await fetch(`${this.baseUrl}/sessions/stream`, {
|
|
651
|
+
method: "POST",
|
|
652
|
+
headers: {
|
|
653
|
+
...this.baseHeaders,
|
|
654
|
+
"Content-Type": "application/json",
|
|
655
|
+
Accept: "text/event-stream",
|
|
656
|
+
},
|
|
657
|
+
body: JSON.stringify(body),
|
|
658
|
+
signal: controller.signal,
|
|
659
|
+
});
|
|
660
|
+
} catch (err) {
|
|
661
|
+
throw controller.signal.aborted
|
|
662
|
+
? new TimeoutError("POST", "/sessions/stream", this.timeoutMs)
|
|
663
|
+
: new ConnectionError("POST", "/sessions/stream", err);
|
|
664
|
+
} finally {
|
|
665
|
+
if (timer) clearTimeout(timer);
|
|
666
|
+
}
|
|
482
667
|
if (!resp.ok || !resp.body) {
|
|
483
|
-
throw
|
|
668
|
+
throw await HttpError.fromResponse("POST", "/sessions/stream", resp);
|
|
484
669
|
}
|
|
485
670
|
|
|
486
671
|
// Minimal SSE parser — just enough for the events our server emits.
|
|
@@ -517,12 +702,15 @@ export class AgentClient {
|
|
|
517
702
|
(data.pending_signal_name as string | undefined) ?? null,
|
|
518
703
|
(data.pending_signal_names as string[] | undefined) ?? [],
|
|
519
704
|
(data.pending_signal_deadline as string | undefined) ?? null,
|
|
705
|
+
(data.pending_details as string | undefined) ?? null,
|
|
706
|
+
(data.run_id as string | undefined) ?? null,
|
|
520
707
|
);
|
|
521
708
|
}
|
|
522
709
|
|
|
523
710
|
private async getJSON(path: string): Promise<unknown> {
|
|
524
|
-
|
|
525
|
-
|
|
711
|
+
// GETs are idempotent: retry connection failures, timeouts, and
|
|
712
|
+
// retryable statuses with exponential backoff.
|
|
713
|
+
const resp = await this.request("GET", path, undefined, this.retries);
|
|
526
714
|
return await resp.json();
|
|
527
715
|
}
|
|
528
716
|
|
|
@@ -534,19 +722,67 @@ export class AgentClient {
|
|
|
534
722
|
path: string,
|
|
535
723
|
body: unknown,
|
|
536
724
|
): Promise<{ status: number; data: Record<string, unknown> }> {
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
headers: { "Content-Type": "application/json" },
|
|
540
|
-
body: JSON.stringify(body),
|
|
541
|
-
});
|
|
542
|
-
if (!resp.ok) throw await httpError(resp);
|
|
725
|
+
// POSTs are never retried: run/resume/signal are not idempotent.
|
|
726
|
+
const resp = await this.request("POST", path, body, 0);
|
|
543
727
|
return { status: resp.status, data: (await resp.json()) as Record<string, unknown> };
|
|
544
728
|
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* One HTTP exchange with timeout and (for idempotent requests) retries.
|
|
732
|
+
* Resolves with a 2xx `Response`; throws {@link HttpError},
|
|
733
|
+
* {@link TimeoutError}, or {@link ConnectionError} otherwise.
|
|
734
|
+
*/
|
|
735
|
+
private async request(
|
|
736
|
+
method: "GET" | "POST",
|
|
737
|
+
path: string,
|
|
738
|
+
body: unknown,
|
|
739
|
+
retries: number,
|
|
740
|
+
): Promise<Response> {
|
|
741
|
+
let lastError: AgentClientError | null = null;
|
|
742
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
743
|
+
if (attempt > 0) {
|
|
744
|
+
await sleep(this.retryDelayMs * 2 ** (attempt - 1));
|
|
745
|
+
}
|
|
746
|
+
const controller = new AbortController();
|
|
747
|
+
const timer =
|
|
748
|
+
this.timeoutMs > 0 ? setTimeout(() => controller.abort(), this.timeoutMs) : null;
|
|
749
|
+
try {
|
|
750
|
+
let resp: Response;
|
|
751
|
+
try {
|
|
752
|
+
resp = await fetch(this.baseUrl + path, {
|
|
753
|
+
method,
|
|
754
|
+
signal: controller.signal,
|
|
755
|
+
headers:
|
|
756
|
+
body !== undefined
|
|
757
|
+
? { ...this.baseHeaders, "Content-Type": "application/json" }
|
|
758
|
+
: this.baseHeaders,
|
|
759
|
+
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
760
|
+
});
|
|
761
|
+
} catch (err) {
|
|
762
|
+
throw controller.signal.aborted
|
|
763
|
+
? new TimeoutError(method, path, this.timeoutMs)
|
|
764
|
+
: new ConnectionError(method, path, err);
|
|
765
|
+
}
|
|
766
|
+
if (!resp.ok) throw await HttpError.fromResponse(method, path, resp);
|
|
767
|
+
return resp;
|
|
768
|
+
} catch (err) {
|
|
769
|
+
const retryable =
|
|
770
|
+
err instanceof TimeoutError ||
|
|
771
|
+
err instanceof ConnectionError ||
|
|
772
|
+
(err instanceof HttpError && RETRYABLE_STATUS.has(err.status));
|
|
773
|
+
if (!retryable || attempt === retries) throw err;
|
|
774
|
+
lastError = err as AgentClientError;
|
|
775
|
+
} finally {
|
|
776
|
+
if (timer) clearTimeout(timer);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
// Unreachable: the loop either returns or throws on the last attempt.
|
|
780
|
+
throw lastError ?? new ConnectionError(method, path, "retries exhausted");
|
|
781
|
+
}
|
|
545
782
|
}
|
|
546
783
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
return new Error(`HTTP ${resp.status}: ${text}`);
|
|
784
|
+
function sleep(ms: number): Promise<void> {
|
|
785
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
550
786
|
}
|
|
551
787
|
|
|
552
788
|
function parseSseFrame(frame: string): StreamEvent | null {
|