@1kbirds/chidori 0.1.24 → 3.3.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 +115 -5
- package/dist/agent.d.ts +487 -0
- package/dist/agent.js +46 -0
- package/dist/index.d.ts +332 -0
- package/dist/index.js +318 -0
- package/package.json +27 -32
- package/src/agent.ts +548 -0
- package/src/index.ts +580 -0
- package/.pytest_cache/README.md +0 -8
- package/CHANGELOG.md +0 -33
- package/Cargo.toml +0 -83
- package/build/stage/v0.1.24/chidori-v0.1.24-node-v108-darwin-arm64-unknown.tar.gz +0 -0
- package/build/stage/v0.1.24/chidori-v0.1.24-node-v93-darwin-arm64-unknown.tar.gz +0 -0
- package/build.rs +0 -7
- package/package_node/index.d.ts +0 -0
- package/package_node/index.js +0 -130
- package/package_node/native/chidori.node +0 -0
- package/package_python/.idea/inspectionProfiles/profiles_settings.xml +0 -6
- package/package_python/.idea/misc.xml +0 -4
- package/package_python/.idea/modules.xml +0 -8
- package/package_python/.idea/package_python.iml +0 -8
- package/package_python/.idea/vcs.xml +0 -6
- package/package_python/chidori/__init__.py +0 -0
- package/package_python/chidori/__pycache__/__init__.cpython-310.pyc +0 -0
- package/package_python/chidori/__pycache__/test_chidori.cpython-310-pytest-7.4.0.pyc +0 -0
- package/package_python/chidori/__pycache__/test_promptgraph.cpython-310-pytest-7.4.0.pyc +0 -0
- package/package_python/chidori/chidori.pyi +0 -32
- package/package_python/chidori/py.typed +0 -0
- package/package_python/chidori/test_chidori.py +0 -36
- package/pyproject.toml +0 -33
- package/src/lib.rs +0 -23
- package/src/translations/mod.rs +0 -9
- package/src/translations/nodejs.rs +0 -739
- package/src/translations/python.rs +0 -920
- package/src/translations/rust.rs +0 -737
- package/src/translations/shared.rs +0 -49
- package/src/translations/wasm.rs +0 -1
- package/tests/nodejs/chidori.test.js +0 -41
- package/tests/nodejs/nodeHandle.test.js +0 -15
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chidori TypeScript SDK — HTTP client for a running `chidori serve`
|
|
3
|
+
* instance. Mirrors the Python SDK (`sdk/python/chidori`). Zero runtime
|
|
4
|
+
* dependencies; uses the global `fetch` available in Node 18+ and browsers.
|
|
5
|
+
*/
|
|
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";
|
|
8
|
+
export { chidori, run } from "./agent.js";
|
|
9
|
+
/** JSON-serialisable value — what agents produce as output and accept as input. */
|
|
10
|
+
export type Json = null | boolean | number | string | Json[] | {
|
|
11
|
+
[key: string]: Json;
|
|
12
|
+
};
|
|
13
|
+
/** Server-side session status. */
|
|
14
|
+
export type SessionStatus = "running" | "completed" | "failed" | "paused" | "cancelled" | "awaitingapproval";
|
|
15
|
+
/**
|
|
16
|
+
* Built-in policy profiles selectable per session. Layered on the server
|
|
17
|
+
* policy with stricter-wins semantics: a profile can tighten what the
|
|
18
|
+
* operator's policy allows, never relax it.
|
|
19
|
+
*/
|
|
20
|
+
export type PolicyProfile = "untrusted" | "supervised";
|
|
21
|
+
/** A single host function call recorded during an agent run. */
|
|
22
|
+
export interface CallRecord {
|
|
23
|
+
seq: number;
|
|
24
|
+
function: string;
|
|
25
|
+
args: Json;
|
|
26
|
+
result: Json;
|
|
27
|
+
duration_ms: number;
|
|
28
|
+
timestamp: string;
|
|
29
|
+
token_usage?: {
|
|
30
|
+
input_tokens: number;
|
|
31
|
+
output_tokens: number;
|
|
32
|
+
};
|
|
33
|
+
error?: string;
|
|
34
|
+
}
|
|
35
|
+
/** Source hash recorded in a runtime snapshot manifest. */
|
|
36
|
+
export interface SnapshotSourceFingerprint {
|
|
37
|
+
path: string;
|
|
38
|
+
hash: string;
|
|
39
|
+
}
|
|
40
|
+
/** Snapshot ABI recorded before loading VM snapshot bytes. */
|
|
41
|
+
export interface SnapshotAbi {
|
|
42
|
+
typescript_runtime: number;
|
|
43
|
+
quickjs_snapshot: number;
|
|
44
|
+
engine_fork: string;
|
|
45
|
+
}
|
|
46
|
+
/** Determinism policy captured with a runtime snapshot. */
|
|
47
|
+
export interface SnapshotRuntimePolicy {
|
|
48
|
+
typescript_imports: "none" | "relative" | "project";
|
|
49
|
+
date: "disabled" | "fixed" | "host";
|
|
50
|
+
random: "disabled" | "seeded" | "host";
|
|
51
|
+
maps_sets: "reject" | "serialize";
|
|
52
|
+
deterministic_seed: string;
|
|
53
|
+
}
|
|
54
|
+
/** Pending host operation captured at a durable snapshot safepoint. */
|
|
55
|
+
export interface PendingHostOperation {
|
|
56
|
+
id: number;
|
|
57
|
+
seq: number;
|
|
58
|
+
kind: "prompt" | "input" | "policy_approval" | "tool" | "call_agent" | "http" | "template" | "memory" | "checkpoint";
|
|
59
|
+
args: Json;
|
|
60
|
+
created_at: string;
|
|
61
|
+
}
|
|
62
|
+
export type HostPromiseState = "pending" | {
|
|
63
|
+
resolved: {
|
|
64
|
+
value: Json;
|
|
65
|
+
completed_at: string;
|
|
66
|
+
};
|
|
67
|
+
} | {
|
|
68
|
+
rejected: {
|
|
69
|
+
error: string;
|
|
70
|
+
completed_at: string;
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
export interface HostPromiseRecord {
|
|
74
|
+
operation: PendingHostOperation;
|
|
75
|
+
state: HostPromiseState;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Public snapshot metadata. It is safe to expose in SDK checkpoints; the raw
|
|
79
|
+
* `runtime.snapshot` VM bytes stay server-side unless an admin endpoint opts in.
|
|
80
|
+
*/
|
|
81
|
+
export interface SnapshotManifest {
|
|
82
|
+
run_id: string;
|
|
83
|
+
abi: SnapshotAbi;
|
|
84
|
+
policy: SnapshotRuntimePolicy;
|
|
85
|
+
entry: SnapshotSourceFingerprint;
|
|
86
|
+
modules: SnapshotSourceFingerprint[];
|
|
87
|
+
pending?: PendingHostOperation | null;
|
|
88
|
+
host_promises?: HostPromiseRecord[];
|
|
89
|
+
call_log_len: number;
|
|
90
|
+
snapshot_file: string;
|
|
91
|
+
created_at: string;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* A saved checkpoint — session id, input, the full call log, and optional
|
|
95
|
+
* runtime snapshot metadata. The call log is enough for deterministic replay;
|
|
96
|
+
* the snapshot manifest lets clients inspect durable-resume state without
|
|
97
|
+
* downloading raw VM snapshot bytes.
|
|
98
|
+
*/
|
|
99
|
+
export declare class Checkpoint {
|
|
100
|
+
readonly sessionId: string;
|
|
101
|
+
readonly input: Json;
|
|
102
|
+
readonly callLog: CallRecord[];
|
|
103
|
+
readonly snapshotManifest: SnapshotManifest | null;
|
|
104
|
+
constructor(sessionId: string, input: Json, callLog: CallRecord[], snapshotManifest?: SnapshotManifest | null);
|
|
105
|
+
/** Serialise to JSON. Pairs with `Checkpoint.fromJSON`. */
|
|
106
|
+
toJSON(): {
|
|
107
|
+
session_id: string;
|
|
108
|
+
input: Json;
|
|
109
|
+
call_log: CallRecord[];
|
|
110
|
+
snapshot_manifest?: SnapshotManifest | null;
|
|
111
|
+
};
|
|
112
|
+
static fromJSON(data: {
|
|
113
|
+
session_id: string;
|
|
114
|
+
input: Json;
|
|
115
|
+
call_log: CallRecord[];
|
|
116
|
+
snapshot_manifest?: SnapshotManifest | null;
|
|
117
|
+
}): Checkpoint;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Returned by `client.signal` when the signal was accepted but did not resolve
|
|
121
|
+
* a pause synchronously. Mirrors the server's 202 Accepted body:
|
|
122
|
+
* * `"queued"` — the run was not waiting on this name; the signal sits in
|
|
123
|
+
* the durable mailbox until a matching listen point drains it.
|
|
124
|
+
* * `"delivered_live"` — a live streaming worker supervises the run; the
|
|
125
|
+
* signal was enqueued into the running agent's in-memory mailbox and the
|
|
126
|
+
* worker was woken to resume a matching pause in-process.
|
|
127
|
+
*/
|
|
128
|
+
export interface SignalQueued {
|
|
129
|
+
/** Session id the signal was delivered to. */
|
|
130
|
+
id: string;
|
|
131
|
+
status: "queued" | "delivered_live";
|
|
132
|
+
/** The signal name, echoed back. */
|
|
133
|
+
name: string;
|
|
134
|
+
/** Monotonic per-run sequence freezing global arrival order across senders. */
|
|
135
|
+
delivery_seq: number;
|
|
136
|
+
}
|
|
137
|
+
/** A signal delivery request body for `client.signal`. */
|
|
138
|
+
export interface SignalDelivery {
|
|
139
|
+
/** Required, non-empty: the listen-point name the agent awaits. */
|
|
140
|
+
name: string;
|
|
141
|
+
/** Any JSON payload (default null). */
|
|
142
|
+
payload?: Json;
|
|
143
|
+
/** Sender provenance recorded in the trace (default null). */
|
|
144
|
+
from?: SignalSender | Json;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Type guard distinguishing the two `client.signal` outcomes: a resumed
|
|
148
|
+
* `Session` (the run was paused-waiting on this name) vs a `SignalQueued`
|
|
149
|
+
* descriptor (the signal was enqueued for a later listen point).
|
|
150
|
+
*/
|
|
151
|
+
export declare function isSignalQueued(result: Session | SignalQueued): result is SignalQueued;
|
|
152
|
+
/** One execution of an agent — result + call log + status. */
|
|
153
|
+
export declare class Session {
|
|
154
|
+
readonly id: string;
|
|
155
|
+
status: SessionStatus;
|
|
156
|
+
readonly input: Json;
|
|
157
|
+
output: Json | null;
|
|
158
|
+
error: string | null;
|
|
159
|
+
callLog: CallRecord[];
|
|
160
|
+
pendingPrompt: string | null;
|
|
161
|
+
private readonly client;
|
|
162
|
+
snapshotManifest: SnapshotManifest | null;
|
|
163
|
+
/**
|
|
164
|
+
* When the run is `paused` at a `chidori.signal(name)` listen point, the
|
|
165
|
+
* name it is waiting on (so a caller can deliver via `client.signal`).
|
|
166
|
+
* `null` for plain `input()` pauses and non-signal states.
|
|
167
|
+
*/
|
|
168
|
+
pendingSignalName: string | null;
|
|
169
|
+
/**
|
|
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)`.
|
|
172
|
+
* Empty for non-signal states.
|
|
173
|
+
*/
|
|
174
|
+
pendingSignalNames: string[];
|
|
175
|
+
/**
|
|
176
|
+
* Absolute deadline (ISO timestamp) for a signal pause created with
|
|
177
|
+
* `timeoutMs`; the server resolves the pause with the timeout sentinel
|
|
178
|
+
* when it passes. `null` when the pause has no timeout.
|
|
179
|
+
*/
|
|
180
|
+
pendingSignalDeadline: string | null;
|
|
181
|
+
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
|
+
/**
|
|
183
|
+
* When the run is `paused` at a `chidori.signal(name)` listen point, the
|
|
184
|
+
* name it is waiting on (so a caller can deliver via `client.signal`).
|
|
185
|
+
* `null` for plain `input()` pauses and non-signal states.
|
|
186
|
+
*/
|
|
187
|
+
pendingSignalName?: string | null,
|
|
188
|
+
/**
|
|
189
|
+
* 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)`.
|
|
191
|
+
* Empty for non-signal states.
|
|
192
|
+
*/
|
|
193
|
+
pendingSignalNames?: string[],
|
|
194
|
+
/**
|
|
195
|
+
* Absolute deadline (ISO timestamp) for a signal pause created with
|
|
196
|
+
* `timeoutMs`; the server resolves the pause with the timeout sentinel
|
|
197
|
+
* when it passes. `null` when the pause has no timeout.
|
|
198
|
+
*/
|
|
199
|
+
pendingSignalDeadline?: string | null);
|
|
200
|
+
get ok(): boolean;
|
|
201
|
+
/**
|
|
202
|
+
* Fetch the full call log from the server (if not already loaded) and
|
|
203
|
+
* wrap it in a Checkpoint suitable for saving / later replay.
|
|
204
|
+
*/
|
|
205
|
+
checkpoint(): Promise<Checkpoint>;
|
|
206
|
+
/** Replay this session through the server; same inputs, cached results. */
|
|
207
|
+
replay(): Promise<Session>;
|
|
208
|
+
}
|
|
209
|
+
/** Stream event yielded by `AgentClient.stream`. */
|
|
210
|
+
export type StreamEvent = {
|
|
211
|
+
type: "call";
|
|
212
|
+
record: CallRecord;
|
|
213
|
+
} | {
|
|
214
|
+
type: "prompt_start";
|
|
215
|
+
stream_id: string;
|
|
216
|
+
seq: number;
|
|
217
|
+
prompt_type?: string | null;
|
|
218
|
+
model: string;
|
|
219
|
+
} | {
|
|
220
|
+
type: "prompt_delta";
|
|
221
|
+
stream_id: string;
|
|
222
|
+
seq: number;
|
|
223
|
+
prompt_type?: string | null;
|
|
224
|
+
delta: string;
|
|
225
|
+
} | {
|
|
226
|
+
type: "prompt_end";
|
|
227
|
+
stream_id: string;
|
|
228
|
+
seq: number;
|
|
229
|
+
prompt_type?: string | null;
|
|
230
|
+
error?: string | null;
|
|
231
|
+
} | {
|
|
232
|
+
/**
|
|
233
|
+
* The streamed run paused at a `signal()` / `signalAny()` listen point
|
|
234
|
+
* and stays live: the worker keeps supervising, and a delivered signal
|
|
235
|
+
* (or the `timeoutMs` deadline) resumes it in-process — further events
|
|
236
|
+
* follow on the same stream. Deliver with `client.signal`.
|
|
237
|
+
*/
|
|
238
|
+
type: "paused";
|
|
239
|
+
id: string;
|
|
240
|
+
status: "paused";
|
|
241
|
+
pending_seq: number;
|
|
242
|
+
pending_signal_name?: string | null;
|
|
243
|
+
pending_signal_names?: string[];
|
|
244
|
+
pending_signal_deadline?: string | null;
|
|
245
|
+
} | {
|
|
246
|
+
type: "done";
|
|
247
|
+
id: string;
|
|
248
|
+
status: SessionStatus;
|
|
249
|
+
output?: Json;
|
|
250
|
+
error?: string;
|
|
251
|
+
};
|
|
252
|
+
/**
|
|
253
|
+
* HTTP client for an `chidori serve` instance.
|
|
254
|
+
*
|
|
255
|
+
* ```ts
|
|
256
|
+
* const client = new AgentClient("http://localhost:8080");
|
|
257
|
+
* const session = await client.run({ document: "Rust is a systems language." });
|
|
258
|
+
* console.log(session.output);
|
|
259
|
+
*
|
|
260
|
+
* const cp = await session.checkpoint();
|
|
261
|
+
* const replayed = await client.replay(cp); // zero LLM calls
|
|
262
|
+
* ```
|
|
263
|
+
*/
|
|
264
|
+
export declare class AgentClient {
|
|
265
|
+
readonly baseUrl: string;
|
|
266
|
+
constructor(baseUrl?: string);
|
|
267
|
+
health(): Promise<Json>;
|
|
268
|
+
/**
|
|
269
|
+
* Create a new session and run the agent with the given input.
|
|
270
|
+
*
|
|
271
|
+
* `options.policyProfile` optionally names a built-in policy profile
|
|
272
|
+
* ("untrusted" or "supervised") applied to every run of this session.
|
|
273
|
+
* It is layered on the server policy with stricter-wins semantics — it
|
|
274
|
+
* can tighten what the operator allows, never relax it. Under
|
|
275
|
+
* "supervised", gated calls pause the session as "awaitingapproval";
|
|
276
|
+
* approve or deny them via the server's /approve endpoint.
|
|
277
|
+
*/
|
|
278
|
+
run(input: Json, options?: {
|
|
279
|
+
policyProfile?: PolicyProfile;
|
|
280
|
+
}): Promise<Session>;
|
|
281
|
+
/** Replay an agent from a saved checkpoint. */
|
|
282
|
+
replay(checkpoint: Checkpoint): Promise<Session>;
|
|
283
|
+
/**
|
|
284
|
+
* Supply a response to a paused `input()` call and continue the run.
|
|
285
|
+
* The same session id advances to completed (or re-pauses on a later
|
|
286
|
+
* `input()`).
|
|
287
|
+
*/
|
|
288
|
+
resume(sessionId: string, response: string): Promise<Session>;
|
|
289
|
+
/**
|
|
290
|
+
* Deliver a signal `{ name, payload?, from? }` to a run
|
|
291
|
+
* (`POST /sessions/{id}/signal`).
|
|
292
|
+
*
|
|
293
|
+
* Two outcomes, distinguished by `isSignalQueued`:
|
|
294
|
+
* * the run was paused-waiting on this exact name → it resolves the pause
|
|
295
|
+
* and resumes; this returns the advanced `Session` (200), now `completed`
|
|
296
|
+
* or re-`paused`.
|
|
297
|
+
* * otherwise → the signal is accepted asynchronously; this returns a
|
|
298
|
+
* `SignalQueued` descriptor (202) carrying the assigned `delivery_seq`,
|
|
299
|
+
* with `status` `"queued"` (durable mailbox) or `"delivered_live"` (a
|
|
300
|
+
* live streaming worker received it in-memory and resumes a matching
|
|
301
|
+
* pause in-process).
|
|
302
|
+
*
|
|
303
|
+
* Throws on 400 (empty name), 404 (unknown session), or 409 (terminal run).
|
|
304
|
+
*/
|
|
305
|
+
signal(sessionId: string, delivery: SignalDelivery): Promise<Session | SignalQueued>;
|
|
306
|
+
/** Fetch an existing session by id. */
|
|
307
|
+
getSession(id: string): Promise<Session>;
|
|
308
|
+
/** List all sessions. Returns the raw summaries. */
|
|
309
|
+
listSessions(): Promise<Array<{
|
|
310
|
+
id: string;
|
|
311
|
+
status: SessionStatus;
|
|
312
|
+
error?: string;
|
|
313
|
+
}>>;
|
|
314
|
+
/** Fetch the full call log and optional snapshot manifest for a session. */
|
|
315
|
+
getCheckpoint(id: string): Promise<{
|
|
316
|
+
call_log: CallRecord[];
|
|
317
|
+
snapshot_manifest?: SnapshotManifest | null;
|
|
318
|
+
}>;
|
|
319
|
+
/** Fetch only the snapshot manifest metadata for a session, never VM bytes. */
|
|
320
|
+
getSnapshotManifest(id: string): Promise<SnapshotManifest>;
|
|
321
|
+
/**
|
|
322
|
+
* Stream an agent run: yields host function calls, prompt stream lifecycle
|
|
323
|
+
* events (`prompt_start`, `prompt_delta`, `prompt_end`), then a final
|
|
324
|
+
* `done` event. Prompt events include `prompt_type` so UIs can filter
|
|
325
|
+
* progress streams separately from final-answer streams.
|
|
326
|
+
*/
|
|
327
|
+
stream(input: Json): AsyncGenerator<StreamEvent, void, void>;
|
|
328
|
+
private sessionFrom;
|
|
329
|
+
private getJSON;
|
|
330
|
+
private postJSON;
|
|
331
|
+
private postJSONWithStatus;
|
|
332
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chidori TypeScript SDK — HTTP client for a running `chidori serve`
|
|
3
|
+
* instance. Mirrors the Python SDK (`sdk/python/chidori`). Zero runtime
|
|
4
|
+
* dependencies; uses the global `fetch` available in Node 18+ and browsers.
|
|
5
|
+
*/
|
|
6
|
+
// Authoring entrypoints: the host object and the `run(handler)` definer. These
|
|
7
|
+
// are value exports (the runtime strips the import and supplies them).
|
|
8
|
+
export { chidori, run } from "./agent.js";
|
|
9
|
+
/**
|
|
10
|
+
* A saved checkpoint — session id, input, the full call log, and optional
|
|
11
|
+
* runtime snapshot metadata. The call log is enough for deterministic replay;
|
|
12
|
+
* the snapshot manifest lets clients inspect durable-resume state without
|
|
13
|
+
* downloading raw VM snapshot bytes.
|
|
14
|
+
*/
|
|
15
|
+
export class Checkpoint {
|
|
16
|
+
sessionId;
|
|
17
|
+
input;
|
|
18
|
+
callLog;
|
|
19
|
+
snapshotManifest;
|
|
20
|
+
constructor(sessionId, input, callLog, snapshotManifest = null) {
|
|
21
|
+
this.sessionId = sessionId;
|
|
22
|
+
this.input = input;
|
|
23
|
+
this.callLog = callLog;
|
|
24
|
+
this.snapshotManifest = snapshotManifest;
|
|
25
|
+
}
|
|
26
|
+
/** Serialise to JSON. Pairs with `Checkpoint.fromJSON`. */
|
|
27
|
+
toJSON() {
|
|
28
|
+
return {
|
|
29
|
+
session_id: this.sessionId,
|
|
30
|
+
input: this.input,
|
|
31
|
+
call_log: this.callLog,
|
|
32
|
+
...(this.snapshotManifest ? { snapshot_manifest: this.snapshotManifest } : {}),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
static fromJSON(data) {
|
|
36
|
+
return new Checkpoint(data.session_id, data.input, data.call_log, data.snapshot_manifest ?? null);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Type guard distinguishing the two `client.signal` outcomes: a resumed
|
|
41
|
+
* `Session` (the run was paused-waiting on this name) vs a `SignalQueued`
|
|
42
|
+
* descriptor (the signal was enqueued for a later listen point).
|
|
43
|
+
*/
|
|
44
|
+
export function isSignalQueued(result) {
|
|
45
|
+
const status = result.status;
|
|
46
|
+
return status === "queued" || status === "delivered_live";
|
|
47
|
+
}
|
|
48
|
+
/** One execution of an agent — result + call log + status. */
|
|
49
|
+
export class Session {
|
|
50
|
+
id;
|
|
51
|
+
status;
|
|
52
|
+
input;
|
|
53
|
+
output;
|
|
54
|
+
error;
|
|
55
|
+
callLog;
|
|
56
|
+
pendingPrompt;
|
|
57
|
+
client;
|
|
58
|
+
snapshotManifest;
|
|
59
|
+
pendingSignalName;
|
|
60
|
+
pendingSignalNames;
|
|
61
|
+
pendingSignalDeadline;
|
|
62
|
+
constructor(id, status, input, output = null, error = null, callLog = [], pendingPrompt = null, client = null, snapshotManifest = null,
|
|
63
|
+
/**
|
|
64
|
+
* When the run is `paused` at a `chidori.signal(name)` listen point, the
|
|
65
|
+
* name it is waiting on (so a caller can deliver via `client.signal`).
|
|
66
|
+
* `null` for plain `input()` pauses and non-signal states.
|
|
67
|
+
*/
|
|
68
|
+
pendingSignalName = null,
|
|
69
|
+
/**
|
|
70
|
+
* 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)`.
|
|
72
|
+
* Empty for non-signal states.
|
|
73
|
+
*/
|
|
74
|
+
pendingSignalNames = [],
|
|
75
|
+
/**
|
|
76
|
+
* Absolute deadline (ISO timestamp) for a signal pause created with
|
|
77
|
+
* `timeoutMs`; the server resolves the pause with the timeout sentinel
|
|
78
|
+
* when it passes. `null` when the pause has no timeout.
|
|
79
|
+
*/
|
|
80
|
+
pendingSignalDeadline = null) {
|
|
81
|
+
this.id = id;
|
|
82
|
+
this.status = status;
|
|
83
|
+
this.input = input;
|
|
84
|
+
this.output = output;
|
|
85
|
+
this.error = error;
|
|
86
|
+
this.callLog = callLog;
|
|
87
|
+
this.pendingPrompt = pendingPrompt;
|
|
88
|
+
this.client = client;
|
|
89
|
+
this.snapshotManifest = snapshotManifest;
|
|
90
|
+
this.pendingSignalName = pendingSignalName;
|
|
91
|
+
this.pendingSignalNames = pendingSignalNames;
|
|
92
|
+
this.pendingSignalDeadline = pendingSignalDeadline;
|
|
93
|
+
}
|
|
94
|
+
get ok() {
|
|
95
|
+
return this.status === "completed";
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Fetch the full call log from the server (if not already loaded) and
|
|
99
|
+
* wrap it in a Checkpoint suitable for saving / later replay.
|
|
100
|
+
*/
|
|
101
|
+
async checkpoint() {
|
|
102
|
+
if ((this.callLog.length === 0 || this.snapshotManifest === null) && this.client) {
|
|
103
|
+
const data = await this.client.getCheckpoint(this.id);
|
|
104
|
+
this.callLog = data.call_log;
|
|
105
|
+
this.snapshotManifest = data.snapshot_manifest ?? null;
|
|
106
|
+
}
|
|
107
|
+
return new Checkpoint(this.id, this.input, this.callLog, this.snapshotManifest);
|
|
108
|
+
}
|
|
109
|
+
/** Replay this session through the server; same inputs, cached results. */
|
|
110
|
+
async replay() {
|
|
111
|
+
if (!this.client) {
|
|
112
|
+
throw new Error("Session has no client bound; use client.replay()");
|
|
113
|
+
}
|
|
114
|
+
const cp = await this.checkpoint();
|
|
115
|
+
return this.client.replay(cp);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* HTTP client for an `chidori serve` instance.
|
|
120
|
+
*
|
|
121
|
+
* ```ts
|
|
122
|
+
* const client = new AgentClient("http://localhost:8080");
|
|
123
|
+
* const session = await client.run({ document: "Rust is a systems language." });
|
|
124
|
+
* console.log(session.output);
|
|
125
|
+
*
|
|
126
|
+
* const cp = await session.checkpoint();
|
|
127
|
+
* const replayed = await client.replay(cp); // zero LLM calls
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
export class AgentClient {
|
|
131
|
+
baseUrl;
|
|
132
|
+
constructor(baseUrl = "http://localhost:8080") {
|
|
133
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
134
|
+
}
|
|
135
|
+
async health() {
|
|
136
|
+
return (await this.getJSON("/health"));
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Create a new session and run the agent with the given input.
|
|
140
|
+
*
|
|
141
|
+
* `options.policyProfile` optionally names a built-in policy profile
|
|
142
|
+
* ("untrusted" or "supervised") applied to every run of this session.
|
|
143
|
+
* It is layered on the server policy with stricter-wins semantics — it
|
|
144
|
+
* can tighten what the operator allows, never relax it. Under
|
|
145
|
+
* "supervised", gated calls pause the session as "awaitingapproval";
|
|
146
|
+
* approve or deny them via the server's /approve endpoint.
|
|
147
|
+
*/
|
|
148
|
+
async run(input, options) {
|
|
149
|
+
const body = { input };
|
|
150
|
+
if (options?.policyProfile) {
|
|
151
|
+
body.policy_profile = options.policyProfile;
|
|
152
|
+
}
|
|
153
|
+
const data = await this.postJSON("/sessions", body);
|
|
154
|
+
return this.sessionFrom(data, input);
|
|
155
|
+
}
|
|
156
|
+
/** Replay an agent from a saved checkpoint. */
|
|
157
|
+
async replay(checkpoint) {
|
|
158
|
+
const data = await this.postJSON("/sessions", {
|
|
159
|
+
input: checkpoint.input,
|
|
160
|
+
replay_from: checkpoint.callLog,
|
|
161
|
+
});
|
|
162
|
+
return this.sessionFrom(data, checkpoint.input);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Supply a response to a paused `input()` call and continue the run.
|
|
166
|
+
* The same session id advances to completed (or re-pauses on a later
|
|
167
|
+
* `input()`).
|
|
168
|
+
*/
|
|
169
|
+
async resume(sessionId, response) {
|
|
170
|
+
const data = await this.postJSON(`/sessions/${sessionId}/resume`, {
|
|
171
|
+
response,
|
|
172
|
+
});
|
|
173
|
+
return this.sessionFrom(data, data.input ?? null);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Deliver a signal `{ name, payload?, from? }` to a run
|
|
177
|
+
* (`POST /sessions/{id}/signal`).
|
|
178
|
+
*
|
|
179
|
+
* Two outcomes, distinguished by `isSignalQueued`:
|
|
180
|
+
* * the run was paused-waiting on this exact name → it resolves the pause
|
|
181
|
+
* and resumes; this returns the advanced `Session` (200), now `completed`
|
|
182
|
+
* or re-`paused`.
|
|
183
|
+
* * otherwise → the signal is accepted asynchronously; this returns a
|
|
184
|
+
* `SignalQueued` descriptor (202) carrying the assigned `delivery_seq`,
|
|
185
|
+
* with `status` `"queued"` (durable mailbox) or `"delivered_live"` (a
|
|
186
|
+
* live streaming worker received it in-memory and resumes a matching
|
|
187
|
+
* pause in-process).
|
|
188
|
+
*
|
|
189
|
+
* Throws on 400 (empty name), 404 (unknown session), or 409 (terminal run).
|
|
190
|
+
*/
|
|
191
|
+
async signal(sessionId, delivery) {
|
|
192
|
+
const { status, data } = await this.postJSONWithStatus(`/sessions/${sessionId}/signal`, {
|
|
193
|
+
name: delivery.name,
|
|
194
|
+
payload: delivery.payload ?? null,
|
|
195
|
+
from: delivery.from ?? null,
|
|
196
|
+
});
|
|
197
|
+
const accepted = data.status;
|
|
198
|
+
if (status === 202 || accepted === "queued" || accepted === "delivered_live") {
|
|
199
|
+
return data;
|
|
200
|
+
}
|
|
201
|
+
return this.sessionFrom(data, data.input ?? null);
|
|
202
|
+
}
|
|
203
|
+
/** Fetch an existing session by id. */
|
|
204
|
+
async getSession(id) {
|
|
205
|
+
const data = (await this.getJSON(`/sessions/${id}`));
|
|
206
|
+
return this.sessionFrom(data, data.input ?? null);
|
|
207
|
+
}
|
|
208
|
+
/** List all sessions. Returns the raw summaries. */
|
|
209
|
+
async listSessions() {
|
|
210
|
+
const data = (await this.getJSON("/sessions"));
|
|
211
|
+
return data.sessions;
|
|
212
|
+
}
|
|
213
|
+
/** Fetch the full call log and optional snapshot manifest for a session. */
|
|
214
|
+
async getCheckpoint(id) {
|
|
215
|
+
return (await this.getJSON(`/sessions/${id}/checkpoint`));
|
|
216
|
+
}
|
|
217
|
+
/** Fetch only the snapshot manifest metadata for a session, never VM bytes. */
|
|
218
|
+
async getSnapshotManifest(id) {
|
|
219
|
+
const data = (await this.getJSON(`/sessions/${id}/snapshot`));
|
|
220
|
+
return data.snapshot_manifest;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Stream an agent run: yields host function calls, prompt stream lifecycle
|
|
224
|
+
* events (`prompt_start`, `prompt_delta`, `prompt_end`), then a final
|
|
225
|
+
* `done` event. Prompt events include `prompt_type` so UIs can filter
|
|
226
|
+
* progress streams separately from final-answer streams.
|
|
227
|
+
*/
|
|
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
|
+
});
|
|
234
|
+
if (!resp.ok || !resp.body) {
|
|
235
|
+
throw new Error(`stream request failed: ${resp.status}`);
|
|
236
|
+
}
|
|
237
|
+
// Minimal SSE parser — just enough for the events our server emits.
|
|
238
|
+
const reader = resp.body.getReader();
|
|
239
|
+
const decoder = new TextDecoder();
|
|
240
|
+
let buffer = "";
|
|
241
|
+
while (true) {
|
|
242
|
+
const { value, done } = await reader.read();
|
|
243
|
+
if (done)
|
|
244
|
+
break;
|
|
245
|
+
buffer += decoder.decode(value, { stream: true });
|
|
246
|
+
let idx;
|
|
247
|
+
while ((idx = buffer.indexOf("\n\n")) !== -1) {
|
|
248
|
+
const frame = buffer.slice(0, idx);
|
|
249
|
+
buffer = buffer.slice(idx + 2);
|
|
250
|
+
const parsed = parseSseFrame(frame);
|
|
251
|
+
if (parsed)
|
|
252
|
+
yield parsed;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// -- internals ----------------------------------------------------------
|
|
257
|
+
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);
|
|
259
|
+
}
|
|
260
|
+
async getJSON(path) {
|
|
261
|
+
const resp = await fetch(this.baseUrl + path);
|
|
262
|
+
if (!resp.ok)
|
|
263
|
+
throw await httpError(resp);
|
|
264
|
+
return await resp.json();
|
|
265
|
+
}
|
|
266
|
+
async postJSON(path, body) {
|
|
267
|
+
return (await this.postJSONWithStatus(path, body)).data;
|
|
268
|
+
}
|
|
269
|
+
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);
|
|
277
|
+
return { status: resp.status, data: (await resp.json()) };
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
async function httpError(resp) {
|
|
281
|
+
const text = await resp.text().catch(() => "");
|
|
282
|
+
return new Error(`HTTP ${resp.status}: ${text}`);
|
|
283
|
+
}
|
|
284
|
+
function parseSseFrame(frame) {
|
|
285
|
+
let event = "message";
|
|
286
|
+
const dataLines = [];
|
|
287
|
+
for (const line of frame.split("\n")) {
|
|
288
|
+
if (line.startsWith("event:"))
|
|
289
|
+
event = line.slice(6).trim();
|
|
290
|
+
else if (line.startsWith("data:"))
|
|
291
|
+
dataLines.push(line.slice(5).trim());
|
|
292
|
+
}
|
|
293
|
+
if (dataLines.length === 0)
|
|
294
|
+
return null;
|
|
295
|
+
try {
|
|
296
|
+
const data = JSON.parse(dataLines.join("\n"));
|
|
297
|
+
if (event === "call")
|
|
298
|
+
return { type: "call", record: data };
|
|
299
|
+
if (event === "prompt_start") {
|
|
300
|
+
return { type: "prompt_start", ...data };
|
|
301
|
+
}
|
|
302
|
+
if (event === "prompt_delta") {
|
|
303
|
+
return { type: "prompt_delta", ...data };
|
|
304
|
+
}
|
|
305
|
+
if (event === "prompt_end") {
|
|
306
|
+
return { type: "prompt_end", ...data };
|
|
307
|
+
}
|
|
308
|
+
if (event === "paused") {
|
|
309
|
+
return { type: "paused", ...data };
|
|
310
|
+
}
|
|
311
|
+
if (event === "done")
|
|
312
|
+
return { type: "done", ...data };
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
}
|