@camelai/run 0.0.0 → 0.11.1

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.
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Watch one agent from a browser (or any runtime with `fetch`): its messages as they stream, the
3
+ * running turn, tool progress, pending inputs and older history, with a browser token your server
4
+ * mints (`POST /v1/agents/:id/browser-tokens`). No Node APIs and no dependencies.
5
+ *
6
+ * ```ts
7
+ * import { watchAgent } from "@camelai/run/watch";
8
+ * const watcher = watchAgent({ url, agentId, token, getToken: () => fetch("/api/token").then(r => r.json()), onChange: render });
9
+ * ```
10
+ */
11
+ import type { AgentEvent, AssistantMessage, Message } from "./types.ts";
12
+ import type { AgentInput } from "./typescript.ts";
13
+ export type * from "./types.ts";
14
+ /** A token and when it expires (ms); `getToken` may answer either. */
15
+ export type BrowserToken = {
16
+ token: string;
17
+ expiresAt?: number;
18
+ };
19
+ export interface WatchOptions {
20
+ /** The runtime's URL (a browser token's `url`). */
21
+ url: string;
22
+ agentId: string;
23
+ /** A browser token for the agent, and when it expires. */
24
+ token: string;
25
+ expiresAt?: number;
26
+ /**
27
+ * A new token from your server: called before the current one expires, and when the runtime refuses it
28
+ * (401). Without it the watcher stops once the token expires, with `state.expired` set.
29
+ */
30
+ getToken?: () => Promise<BrowserToken | string>;
31
+ /** Called after every change to `state`. */
32
+ onChange?: (state: AgentView) => void;
33
+ /** Every event as it arrives (Pi's, and the runtime's own), after `state` took it in. */
34
+ onEvent?: (event: AgentEvent) => void;
35
+ onError?: (error: Error) => void;
36
+ /** "auto" (default): an SSE stream, and long polls where streams fail; or only one of them. */
37
+ transport?: "auto" | "sse" | "poll";
38
+ /** Messages a history page asks for (default 50). */
39
+ pageSize?: number;
40
+ /** A stream that delivers nothing (not even a heartbeat) this long is cut (default 35 s); two such, and it polls. */
41
+ stallMs?: number;
42
+ /** Close the stream while the page is hidden, after this long (default 30 s; 0: never). */
43
+ hiddenGraceMs?: number;
44
+ fetch?: typeof fetch;
45
+ }
46
+ /** What a watcher knows of its agent. */
47
+ export interface AgentView {
48
+ /**
49
+ * The agent's messages, oldest first, each at its index in the agent's history (`indexes`). A user
50
+ * message carries its `requestId` and the application's `metadata`, to match a bubble shown before it arrived.
51
+ */
52
+ messages: (Message & {
53
+ requestId?: string;
54
+ metadata?: Record<string, string>;
55
+ })[];
56
+ indexes: number[];
57
+ /** The assistant message streaming now, folded from its deltas (tool arguments as partial JSON). */
58
+ partial: AssistantMessage | null;
59
+ /** The latest progress of each tool call still running, by tool call id. */
60
+ progress: Map<string, unknown>;
61
+ /** Whether a turn runs. */
62
+ running: boolean;
63
+ /** Human input the agent waits on. */
64
+ pendingInputs: AgentInput[];
65
+ /** How the latest run ended. */
66
+ lastOutcome: {
67
+ id: string;
68
+ stopped?: string;
69
+ error?: string;
70
+ } | null;
71
+ /** Whether older history is there to load (`loadOlder`). */
72
+ hasOlder: boolean;
73
+ transport: "sse" | "poll" | null;
74
+ connected: boolean;
75
+ /** The token expired (or was refused) and could not be renewed: the watcher has stopped. Watch again with a new one. */
76
+ expired: boolean;
77
+ }
78
+ export interface Watcher {
79
+ readonly state: AgentView;
80
+ /** Load the page of history before the oldest message held; false when there is none. */
81
+ loadOlder(): Promise<boolean>;
82
+ close(): void;
83
+ }
84
+ /**
85
+ * JSON that may be cut off (a tool call's arguments as they stream), as far as it goes: open strings,
86
+ * arrays and objects are closed, and a dangling key, comma or partial literal is left out.
87
+ */
88
+ export declare function parsePartialJson(text: string): any;
89
+ /** Watch an agent: see `WatchOptions` and `AgentView`. */
90
+ export declare function watchAgent(options: WatchOptions): Watcher;
@@ -0,0 +1,483 @@
1
+ const sleep = (ms, signal) => new Promise(resolve => {
2
+ const timer = setTimeout(resolve, ms);
3
+ signal?.addEventListener("abort", () => { clearTimeout(timer); resolve(); }, { once: true });
4
+ });
5
+ /**
6
+ * JSON that may be cut off (a tool call's arguments as they stream), as far as it goes: open strings,
7
+ * arrays and objects are closed, and a dangling key, comma or partial literal is left out.
8
+ */
9
+ export function parsePartialJson(text) {
10
+ try {
11
+ return JSON.parse(text);
12
+ }
13
+ catch { /* cut off */ }
14
+ // One pass to find what is open, one repair, one parse: linear in the text, however it is cut.
15
+ const repaired = repair(text);
16
+ if (repaired !== undefined) {
17
+ try {
18
+ return JSON.parse(repaired.text);
19
+ }
20
+ catch { /* something in it is not JSON */ }
21
+ // Leave out the member being written (an escape JSON does not allow, say), once.
22
+ if (repaired.retry !== undefined)
23
+ try {
24
+ return JSON.parse(repaired.retry);
25
+ }
26
+ catch { /* give up */ }
27
+ }
28
+ return {};
29
+ }
30
+ /** `text` closed where it was cut, and (for a retry) closed before the member it was writing. */
31
+ function repair(text) {
32
+ const stack = [];
33
+ let string = null, escaped = false;
34
+ let token = null;
35
+ const settle = () => { const frame = stack.at(-1); if (frame)
36
+ frame.state = frame.state === "key" ? "colon" : "after"; };
37
+ for (let at = 0; at < text.length; at++) {
38
+ const char = text[at];
39
+ if (string) {
40
+ if (escaped)
41
+ escaped = false;
42
+ else if (char === "\\")
43
+ escaped = true;
44
+ else if (char === "\"") {
45
+ string = null;
46
+ settle();
47
+ }
48
+ continue;
49
+ }
50
+ if (token !== null) {
51
+ if (/[\w.+-]/.test(char))
52
+ continue;
53
+ token = null;
54
+ settle();
55
+ }
56
+ if (char === " " || char === "\n" || char === "\r" || char === "\t")
57
+ continue;
58
+ const frame = stack.at(-1);
59
+ if (char === "{" || char === "[")
60
+ stack.push({ close: char === "{" ? "}" : "]", state: char === "{" ? "key" : "value", member: at + 1 });
61
+ else if (char === "}" || char === "]") {
62
+ stack.pop();
63
+ settle();
64
+ }
65
+ else if (char === ":") {
66
+ if (frame)
67
+ frame.state = "value";
68
+ }
69
+ else if (char === ",") {
70
+ if (frame) {
71
+ frame.state = frame.close === "}" ? "key" : "value";
72
+ frame.member = at;
73
+ }
74
+ }
75
+ else if (char === "\"")
76
+ string = { start: at, key: frame?.close === "}" && frame.state === "key" };
77
+ else
78
+ token = at;
79
+ }
80
+ const frame = stack.at(-1);
81
+ const closers = () => stack.map(open => open.close).reverse().join("");
82
+ const before = (at) => text.slice(0, at).replace(/\s+$/, "");
83
+ let out;
84
+ if (string) {
85
+ if (!frame)
86
+ return undefined;
87
+ // A key being written is left out; a value being written is closed (without a dangling escape).
88
+ if (string.key)
89
+ out = before(frame.member);
90
+ else
91
+ out = `${escaped ? text.slice(0, -1) : text}"`;
92
+ }
93
+ else if (token !== null) {
94
+ const word = text.slice(token);
95
+ // A number is kept as far as it is one (-2. is -2); a literal only whole.
96
+ const number = /^-?\d+(\.\d+)?([eE][+-]?\d+)?/.exec(word)?.[0];
97
+ if (/^(true|false|null)$/.test(word))
98
+ out = text;
99
+ else if (number)
100
+ out = text.slice(0, token) + number;
101
+ else if (frame?.close === "}")
102
+ out = `${before(token)}null`;
103
+ else if (frame)
104
+ out = before(frame.member);
105
+ else
106
+ return undefined;
107
+ }
108
+ else if (frame) {
109
+ const trimmed = before(text.length);
110
+ if (frame.state === "colon")
111
+ out = before(frame.member);
112
+ else if (frame.state === "value" && frame.close === "}")
113
+ out = `${trimmed}null`;
114
+ else if (trimmed.endsWith(","))
115
+ out = trimmed.slice(0, -1);
116
+ else
117
+ out = trimmed;
118
+ }
119
+ else
120
+ return undefined;
121
+ return { text: out + closers(), ...(frame ? { retry: before(frame.member) + closers() } : {}) };
122
+ }
123
+ /** Fold one delta into the message it updates (a copy); `json` keeps each tool call's argument text. */
124
+ function fold(message, delta, json) {
125
+ const content = [...message.content];
126
+ const at = delta.contentIndex;
127
+ const block = content[at];
128
+ switch (delta.type) {
129
+ case "text_start":
130
+ content[at] = { type: "text", text: "" };
131
+ break;
132
+ case "text_delta":
133
+ content[at] = { ...block, type: "text", text: (block?.text ?? "") + delta.delta };
134
+ break;
135
+ case "text_end":
136
+ content[at] = { ...block, type: "text", text: delta.content };
137
+ break;
138
+ case "thinking_start":
139
+ content[at] = { type: "thinking", thinking: "" };
140
+ break;
141
+ case "thinking_delta":
142
+ content[at] = { ...block, type: "thinking", thinking: (block?.thinking ?? "") + delta.delta };
143
+ break;
144
+ case "thinking_end":
145
+ content[at] = { ...block, type: "thinking", thinking: delta.content };
146
+ break;
147
+ case "toolcall_start":
148
+ json.set(at, "");
149
+ content[at] = { type: "toolCall", id: delta.id ?? "", name: delta.name ?? "", arguments: {} };
150
+ break;
151
+ case "toolcall_delta": {
152
+ const text = (json.get(at) ?? "") + delta.delta;
153
+ json.set(at, text);
154
+ content[at] = { ...block, type: "toolCall", arguments: parsePartialJson(text) };
155
+ break;
156
+ }
157
+ case "toolcall_end":
158
+ json.delete(at);
159
+ content[at] = delta.toolCall;
160
+ break;
161
+ default: return message;
162
+ }
163
+ return { ...message, content };
164
+ }
165
+ /** Watch an agent: see `WatchOptions` and `AgentView`. */
166
+ export function watchAgent(options) {
167
+ const doFetch = options.fetch ?? globalThis.fetch.bind(globalThis);
168
+ const base = `${options.url.replace(/\/+$/, "")}/v1/agents/${encodeURIComponent(options.agentId)}`;
169
+ const pageSize = options.pageSize ?? 50;
170
+ let token = options.token, expiresAt = options.expiresAt;
171
+ const closed = new AbortController();
172
+ const messages = new Map();
173
+ const state = { messages: [], indexes: [], partial: null, progress: new Map(), running: false, pendingInputs: [], lastOutcome: null, hasOlder: false, transport: null, connected: false, expired: false };
174
+ /** Where the page older than those held ends (its `before`); null: there is none; undefined: no page yet. */
175
+ let before;
176
+ /** The index the next finished message takes, once the stream has said (a run's start, or a snapshot). */
177
+ let next;
178
+ let cursor = 0;
179
+ let json = new Map();
180
+ let stream;
181
+ const changed = () => {
182
+ const indexes = [...messages.keys()].sort((a, b) => a - b);
183
+ state.indexes = indexes;
184
+ state.messages = indexes.map(index => messages.get(index));
185
+ state.hasOlder = !!before;
186
+ options.onChange?.(state);
187
+ };
188
+ const report = (error) => options.onError?.(error instanceof Error ? error : new Error(String(error)));
189
+ async function refresh() {
190
+ if (!options.getToken)
191
+ throw new Error("The browser token expired, and there is no getToken to renew it: pass watchAgent a getToken that fetches a new one from your server");
192
+ const renewed = await options.getToken();
193
+ token = typeof renewed === "string" ? renewed : renewed.token;
194
+ expiresAt = typeof renewed === "string" ? undefined : renewed.expiresAt;
195
+ }
196
+ /** A read with the current token, renewed once on a 401. */
197
+ async function get(path, init = {}, signal) {
198
+ for (let attempt = 0;; attempt++) {
199
+ let response;
200
+ try {
201
+ response = await doFetch(base + path, { ...init, headers: { ...init.headers, Authorization: `Bearer ${token}` }, signal: signal ?? closed.signal });
202
+ }
203
+ catch (error) {
204
+ if ((signal ?? closed.signal).aborted || !(error instanceof TypeError))
205
+ throw error;
206
+ // A browser says only "Failed to fetch" for a network failure and for a CORS refusal alike.
207
+ throw new Error(`Could not reach the runtime at ${options.url} (${error.message}). In a browser this is also how a CORS refusal looks: the runtime allows any origin only for browser tokens (POST /v1/agents/:id/browser-tokens), never for an API key or an agent's own token; and check the url`, { cause: error });
208
+ }
209
+ if (response.status !== 401 || attempt || !options.getToken)
210
+ return response;
211
+ await response.body?.cancel();
212
+ await refresh();
213
+ }
214
+ }
215
+ const json200 = async (path) => {
216
+ const response = await get(path);
217
+ if (!response.ok)
218
+ throw Object.assign(new Error(`${path}: HTTP ${response.status}`), { status: response.status });
219
+ return response.json();
220
+ };
221
+ /** Whether the token reads history: until a 403 says not, when the watcher goes on with the stream alone. */
222
+ let readsHistory = true;
223
+ async function historyRead(path) {
224
+ if (!readsHistory)
225
+ return undefined;
226
+ try {
227
+ return await json200(path);
228
+ }
229
+ catch (error) {
230
+ if (error.status !== 403)
231
+ throw error;
232
+ readsHistory = false;
233
+ before = null;
234
+ return undefined;
235
+ }
236
+ }
237
+ /** Take in a page of history: its messages by index, and (for the first, or an older one) where the next older page ends. */
238
+ function page(value, older = false) {
239
+ for (const { index, message } of value.entries)
240
+ messages.set(index, message);
241
+ if (older || before === undefined)
242
+ before = value.next;
243
+ }
244
+ async function newest() {
245
+ const value = await historyRead(`/history?limit=${pageSize}`);
246
+ if (value)
247
+ page(value);
248
+ }
249
+ /** Take in one frame of the stream. */
250
+ async function receive(data) {
251
+ if (data.type === "snapshot") {
252
+ // Where the stream could not replay: the running turn as of now, and the settled history before it.
253
+ const turn = data.turn;
254
+ state.running = !!turn;
255
+ state.partial = turn?.partial ?? null;
256
+ json = new Map();
257
+ state.progress = new Map();
258
+ if (turn?.start !== null && turn?.start !== undefined) {
259
+ // What the run finished: all of it, unless it was too large to send (then from history, read below).
260
+ next = turn.start + (turn.count ?? turn.messages.length);
261
+ for (const [offset, message] of turn.messages.entries())
262
+ messages.set(turn.start + offset, message);
263
+ }
264
+ else
265
+ next = undefined;
266
+ await newest();
267
+ // No turn in it: none runs, or the token does not show it. Its state says which, where the token reads it: read
268
+ // after history, so a run that began in between is running here too, never a message in history with no turn.
269
+ if (!turn) {
270
+ const known = await json200("/state").catch(() => undefined);
271
+ state.running = !!known?.requests?.some(request => request.state === "running" && request.began && ["prompt", "continue", "resume"].includes(request.method));
272
+ }
273
+ return;
274
+ }
275
+ if (data.type === "response") {
276
+ state.lastOutcome = { id: data.id, ...(data.outcome?.stopped ? { stopped: data.outcome.stopped } : data.outcome?.result?.stopped ? { stopped: data.outcome.result.stopped } : {}), ...(data.outcome?.error ? { error: data.outcome.error } : {}) };
277
+ state.running = false;
278
+ state.partial = null;
279
+ return;
280
+ }
281
+ if (data.type !== "event")
282
+ return;
283
+ const event = data.event;
284
+ switch (event.type) {
285
+ case "turn_opened":
286
+ next = event.index;
287
+ state.running = true;
288
+ break;
289
+ case "agent_start":
290
+ state.running = true;
291
+ break;
292
+ case "agent_end":
293
+ state.running = false;
294
+ break;
295
+ case "message_start":
296
+ if (event.message?.role === "assistant") {
297
+ state.partial = event.message;
298
+ json = new Map();
299
+ }
300
+ break;
301
+ case "message_update":
302
+ if (state.partial)
303
+ state.partial = fold(state.partial, event.assistantMessageEvent, json);
304
+ break;
305
+ case "message_end":
306
+ if (event.message?.role === "assistant")
307
+ state.partial = null;
308
+ if (next !== undefined)
309
+ messages.set(next++, event.message);
310
+ break;
311
+ // A response taken back (before a retry, or a compaction on overflow) gives up its place.
312
+ case "message_retracted":
313
+ messages.delete(event.index);
314
+ if (next !== undefined)
315
+ next = event.index;
316
+ break;
317
+ // A message too large for the stream still takes its place; the newest page of history has it.
318
+ case "event_omitted":
319
+ if (event.was === "message_end" && next !== undefined) {
320
+ next++;
321
+ void newest().then(changed, report);
322
+ }
323
+ break;
324
+ case "tool_execution_update":
325
+ state.progress.set(event.toolCallId, event.partialResult);
326
+ break;
327
+ case "tool_execution_end":
328
+ state.progress.delete(event.toolCallId);
329
+ break;
330
+ case "input_required":
331
+ state.pendingInputs = [...state.pendingInputs.filter(input => input.id !== event.input.id), event.input];
332
+ break;
333
+ case "input_resolved":
334
+ state.pendingInputs = state.pendingInputs.filter(input => input.id !== event.id);
335
+ break;
336
+ }
337
+ options.onEvent?.(event);
338
+ }
339
+ /** One SSE stream until it ends; true when it delivered anything (so streams work here). */
340
+ async function sse(signal) {
341
+ const response = await get("/events?snapshot=1", { headers: { Accept: "text/event-stream", ...(cursor ? { "Last-Event-ID": String(cursor) } : {}) } }, signal);
342
+ if (!response.ok || !response.body)
343
+ throw Object.assign(new Error(`events: HTTP ${response.status}`), { status: response.status });
344
+ const reader = response.body.getReader();
345
+ const decoder = new TextDecoder();
346
+ let buffer = "", delivered = false, watchdog;
347
+ const touch = () => { clearTimeout(watchdog); watchdog = setTimeout(() => void reader.cancel().catch(() => { }), options.stallMs ?? 35_000); };
348
+ touch();
349
+ try {
350
+ for (;;) {
351
+ const { value, done } = await reader.read();
352
+ if (done)
353
+ return delivered;
354
+ touch();
355
+ buffer += decoder.decode(value, { stream: true });
356
+ for (let end; (end = buffer.indexOf("\n\n")) !== -1;) {
357
+ const lines = buffer.slice(0, end).split("\n");
358
+ buffer = buffer.slice(end + 2);
359
+ const text = lines.filter(line => line.startsWith("data:")).map(line => line.slice(5).trimStart()).join("\n");
360
+ if (!text)
361
+ continue;
362
+ delivered = true;
363
+ if (lines.includes("event: ready")) {
364
+ state.connected = true;
365
+ state.transport = "sse";
366
+ changed();
367
+ continue;
368
+ }
369
+ const id = Number(lines.find(line => line.startsWith("id:"))?.slice(3));
370
+ await receive(JSON.parse(text));
371
+ if (Number.isSafeInteger(id) && id > 0)
372
+ cursor = id;
373
+ changed();
374
+ }
375
+ }
376
+ }
377
+ finally {
378
+ clearTimeout(watchdog);
379
+ reader.releaseLock();
380
+ }
381
+ }
382
+ /** One long poll. */
383
+ async function poll(signal) {
384
+ const response = await get("/events?poll=1&wait=25&snapshot=1", { headers: cursor ? { "Last-Event-ID": String(cursor) } : {} }, signal);
385
+ if (!response.ok)
386
+ throw Object.assign(new Error(`events: HTTP ${response.status}`), { status: response.status });
387
+ const answer = await response.json();
388
+ state.connected = true;
389
+ state.transport = "poll";
390
+ for (const event of answer.events)
391
+ await receive(event.data);
392
+ cursor = answer.cursor;
393
+ changed();
394
+ }
395
+ async function run() {
396
+ state.pendingInputs = await json200("/inputs?state=pending").catch(error => { report(error); return []; });
397
+ let mode = options.transport === "poll" ? "poll" : "sse";
398
+ let failures = 0, backoff = 500;
399
+ while (!closed.signal.aborted) {
400
+ await visible();
401
+ if (closed.signal.aborted)
402
+ break;
403
+ stream = new AbortController();
404
+ const signal = AbortSignal.any([closed.signal, stream.signal]);
405
+ // Renew a token about to expire; its stream would end then anyway.
406
+ if (expiresAt !== undefined && options.getToken && expiresAt - Date.now() < 60_000)
407
+ await refresh().catch(report);
408
+ try {
409
+ if (mode === "sse") {
410
+ const delivered = await sse(signal);
411
+ failures = delivered ? 0 : failures + 1;
412
+ state.connected = false;
413
+ }
414
+ else
415
+ await poll(signal);
416
+ backoff = 500;
417
+ }
418
+ catch (error) {
419
+ if (closed.signal.aborted)
420
+ break;
421
+ const status = error.status;
422
+ if (status === 401 || status === 403 || status === 404) {
423
+ if (status === 401) {
424
+ state.expired = true;
425
+ report(new Error(options.getToken ? "The runtime refused the renewed browser token; the watcher stopped" : "The browser token expired, and there is no getToken to renew it; the watcher stopped (state.expired)"));
426
+ }
427
+ else
428
+ report(error);
429
+ state.connected = false;
430
+ changed();
431
+ break;
432
+ }
433
+ if (!stream.signal.aborted) {
434
+ report(error);
435
+ failures++;
436
+ }
437
+ state.connected = false;
438
+ changed();
439
+ await sleep(backoff, closed.signal);
440
+ backoff = Math.min(backoff * 2, 30_000);
441
+ }
442
+ // Streams that end or fail without delivering anything (a proxy that buffers them): poll instead.
443
+ if (mode === "sse" && options.transport !== "sse" && failures >= 2)
444
+ mode = "poll";
445
+ }
446
+ }
447
+ /** Wait while the page is hidden (after a grace period, the stream is closed). */
448
+ let hidden;
449
+ const doc = globalThis.document;
450
+ const grace = options.hiddenGraceMs ?? 30_000;
451
+ let hiding;
452
+ const onVisibility = () => {
453
+ if (doc?.visibilityState === "hidden") {
454
+ if (grace > 0)
455
+ hiding = setTimeout(() => stream?.abort(), grace);
456
+ }
457
+ else {
458
+ clearTimeout(hiding);
459
+ hidden?.();
460
+ }
461
+ };
462
+ doc?.addEventListener("visibilitychange", onVisibility);
463
+ const visible = () => doc?.visibilityState === "hidden" && grace > 0 ? new Promise(resolve => { hidden = resolve; closed.signal.addEventListener("abort", () => resolve(), { once: true }); }) : Promise.resolve();
464
+ void run().catch(report);
465
+ return {
466
+ state,
467
+ async loadOlder() {
468
+ if (!before)
469
+ return false;
470
+ const value = await historyRead(`/history?limit=${pageSize}&before=${before}`);
471
+ if (!value)
472
+ return false;
473
+ page(value, true);
474
+ changed();
475
+ return true;
476
+ },
477
+ close() {
478
+ closed.abort();
479
+ clearTimeout(hiding);
480
+ doc?.removeEventListener("visibilitychange", onVisibility);
481
+ },
482
+ };
483
+ }
@@ -0,0 +1,106 @@
1
+ export declare const FRAME_BYTES = 1100000;
2
+ /** `uncertain` marks an outcome nobody can confirm (timeout after claim, restart). It is informational, never a gate. */
3
+ export type Outcome = {
4
+ result: unknown;
5
+ error?: never;
6
+ uncertain?: never;
7
+ } | {
8
+ error: string;
9
+ uncertain?: boolean;
10
+ result?: never;
11
+ };
12
+ /** How an outcome ended: its error, the runtime's or the model's (`result.error`), and why its turn stopped early. */
13
+ export declare function outcomeEnding(outcome: Outcome | undefined): {
14
+ error?: string;
15
+ stopped?: "input_required" | "spend_limit";
16
+ };
17
+ /** `expiresAt` is null for agents that live until deleted. */
18
+ export interface SessionCredentials {
19
+ id: string;
20
+ token: string;
21
+ expiresAt: number | null;
22
+ }
23
+ export type RequestMethod = "prompt" | "execute" | "status" | "abort" | "continue" | "steer" | "configure" | "resume";
24
+ export type RequestRecord = {
25
+ id: string;
26
+ startedAt?: number;
27
+ endedAt?: number;
28
+ prompt?: string;
29
+ code?: string;
30
+ fingerprint: string;
31
+ method: RequestMethod;
32
+ /** "running" covers queued runs too: a run has begun once `began` is set. */
33
+ state: "running" | "completed";
34
+ outcome?: Outcome;
35
+ /** An ended request's error, from `outcome`: the runtime's, or the model's (`outcome.result.error`). Absent when it succeeded. */
36
+ error?: string;
37
+ /** Why an ended run stopped early, from `outcome.result.stopped`. */
38
+ stopped?: "input_required" | "spend_limit";
39
+ /** When the agent actually started this run (runs queue behind each other). */
40
+ began?: number;
41
+ /** Kept until the run begins, so a queued run survives a restart and runs exactly once. */
42
+ params?: unknown;
43
+ /** Times a new owner resumed this run's turn after the node running it was lost. */
44
+ resumes?: number;
45
+ /** Who the application said is acting in this run: passed to its tool calls (`act` in identity tokens). */
46
+ actor?: string;
47
+ /** A `resume` run's suspension: the run whose turn waited on human input, which this one continues. */
48
+ suspension?: string;
49
+ /** The application's key-value data sent with a message (prompt, steer), also kept on the message. */
50
+ metadata?: Record<string, string>;
51
+ /** A prompt sent with `whileRunning: "steer"` that a running turn took: that turn's request, whose outcome it shares. */
52
+ steeredInto?: string;
53
+ /** The runtime's own: an ended run whose webhook event (`run.completed` or `run.failed`) is not written yet. */
54
+ announce?: true;
55
+ };
56
+ /**
57
+ * Events on an agent's stream. `mcp` carries the runtime's JSON-RPC messages to the application's attached MCP server: live only, with no id, never replayed.
58
+ * `snapshot` goes only to subscribers that ask for one (`?snapshot=1`), in place of what they cannot replay: the running turn as of its id.
59
+ */
60
+ export type ClientEvent = {
61
+ type: "event";
62
+ requestId: string;
63
+ event: any;
64
+ } | {
65
+ type: "response";
66
+ id: string;
67
+ outcome: Outcome;
68
+ } | {
69
+ type: "mcp";
70
+ message: Record<string, unknown>;
71
+ } | TurnSnapshot;
72
+ /**
73
+ * The agent's running turn as a subscriber that saw every event since the turn began would have it: the messages its run
74
+ * finished (every `message_end`, in order) and the assistant message still streaming. `turn` is null when no turn runs.
75
+ * `truncated`: the turn's messages were too large to send; read them from history. `start` is the index its first message
76
+ * has in the agent's history, and `count` how many it finished (sent or not): the next takes `start + count`.
77
+ */
78
+ export type TurnSnapshot = {
79
+ type: "snapshot";
80
+ cursor: number;
81
+ requestId: string | null;
82
+ turn: {
83
+ start: number | null;
84
+ count: number;
85
+ messages: unknown[];
86
+ partial: unknown | null;
87
+ truncated?: true;
88
+ } | null;
89
+ };
90
+ export type SessionState = {
91
+ cursor: number;
92
+ requests: RequestRecord[];
93
+ };
94
+ /** A tool an application offers its agent; shared by the SDKs and the runtime. */
95
+ export interface ToolDefinition {
96
+ name: string;
97
+ description: string;
98
+ parameters: Record<string, unknown>;
99
+ resultFormat?: "json" | "content";
100
+ exposure?: "direct" | "codemode" | "both";
101
+ executionMode?: "sequential" | "parallel";
102
+ /** The user approves each call before it runs (a source's approval policy, or the tool's own needsApproval); such a tool is declared directly. */
103
+ needsApproval?: boolean;
104
+ /** How long a call may go without an answer or progress (ms): the tool's `_meta["agent-runtime/timeoutMs"]`. */
105
+ timeoutMs?: number;
106
+ }