@camelai/agent-runtime 0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 qaml-ai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # @camelai/agent-runtime
2
+
3
+ SDK for the camelAI hosted agent runtime. Your application defines tools as
4
+ ordinary functions; the runtime runs the model loop, keeps each agent's history,
5
+ and executes model-written code in a sandbox that can only call your tools.
6
+ Tool calls come back to your process over SSE, so your data and credentials
7
+ never leave it.
8
+
9
+ Requires Node 22 or later (or Bun).
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ npm install @camelai/agent-runtime
15
+ ```
16
+
17
+ Upgrade with `npm update @camelai/agent-runtime`.
18
+
19
+ The SDK has one runtime dependency (`typebox`). Its message and model types come
20
+ from Pi; for full typing of history and events, also install
21
+ `@earendil-works/pi-agent-core@0.80.6` and `@earendil-works/pi-ai@0.80.6` as dev
22
+ dependencies. Without them those types resolve to `any` (with `skipLibCheck`).
23
+
24
+ ## Use
25
+
26
+ You need the runtime URL and your tenant's operator token. Keep the operator
27
+ token on your backend; it can create and control every agent in your tenant.
28
+
29
+ ```ts
30
+ import { AgentRuntime, schema, tool } from "@camelai/agent-runtime/node";
31
+
32
+ const runtime = new AgentRuntime({
33
+ url: "https://agents.camelai.dev",
34
+ apiKey: process.env.AGENT_RUNTIME_TOKEN,
35
+ stateDirectory: ".agent-runtime", // tool receipts and cursors survive restarts
36
+ });
37
+
38
+ const agent = await runtime.createAgent({
39
+ name: "Inventory planner",
40
+ type: "inventory",
41
+ model: "anthropic/claude-sonnet-5", // any "provider/model-id" from GET /v1/models
42
+ systemPrompt: "You plan restocks. Never place orders.",
43
+ tools: {
44
+ read_inventory: tool({
45
+ description: "Read stock and target quantities",
46
+ input: schema.Object({}),
47
+ execute: () => db.inventory(),
48
+ }),
49
+ plan_restock: tool({
50
+ description: "Save a restock plan",
51
+ input: schema.Object({ sku: schema.String(), quantity: schema.Integer() }),
52
+ // callId is stable across retries: use it as your idempotency key.
53
+ execute: ({ sku, quantity }, { callId }) => db.plan(sku, quantity, { idempotencyKey: callId }),
54
+ }),
55
+ },
56
+ onEvent: event => console.log(event.type),
57
+ });
58
+
59
+ await agent.prompt("Plan restocks for anything below target.");
60
+ ```
61
+
62
+ Switch models between turns with `await agent.configure({ model: "openai/gpt-5.2" })`;
63
+ the history carries over. Your tenant needs a key for that provider.
64
+
65
+ ## Console and REST API
66
+
67
+ Sign in at https://agents.camelai.dev/console with GitHub (qaml-ai members) to add
68
+ provider keys, browse models, create API tokens, watch agents and see usage.
69
+ Everything there is also available over REST with an API token:
70
+
71
+ ```sh
72
+ curl -X PUT https://agents.camelai.dev/v1/providers/anthropic/key \
73
+ -H "Authorization: Bearer $AGENT_RUNTIME_TOKEN" -H "Content-Type: application/json" \
74
+ -d '{"apiKey": "sk-ant-..."}'
75
+ curl "https://agents.camelai.dev/v1/models?available=true" -H "Authorization: Bearer $AGENT_RUNTIME_TOKEN"
76
+ ```
77
+
78
+ The routes are `/v1/me`, `/v1/providers` (+ `/:provider/key`), `/v1/models`,
79
+ `/v1/agents` (+ `/:id`, `/:id/history`, `/:id/prompt`, `/:id/abort`),
80
+ `/v1/tokens` and `/v1/usage`; see `src/api.ts`.
81
+
82
+ Save `agent.session` (it contains a scoped credential) to reconnect later with
83
+ `runtime.connectAgent(session, { tools })`. Pass the same `idempotencyKey` to
84
+ `createAgent` to get the same agent back instead of a new one.
85
+
86
+ `@camelai/agent-runtime` (without `/node`) is the portable build for Workers and
87
+ other runtimes without a filesystem; supply your own `journalStore`.
88
+
89
+ See `clients/README.md` in the repository for delivery
90
+ guarantees, tool-call semantics, and the full API.
@@ -0,0 +1,9 @@
1
+ import { AgentRuntime as PortableAgentRuntime, type RuntimeOptions as PortableRuntimeOptions, type JournalStore } from "./typescript.ts";
2
+ export * from "./typescript.ts";
3
+ export interface RuntimeOptions extends PortableRuntimeOptions {
4
+ stateDirectory?: string;
5
+ }
6
+ export declare function fileJournalStore(directory: string): JournalStore;
7
+ export declare class AgentRuntime extends PortableAgentRuntime {
8
+ constructor(options?: RuntimeOptions);
9
+ }
@@ -0,0 +1,35 @@
1
+ /** Node/Bun convenience entry. The portable SDK itself imports no Node modules. */
2
+ import { readFile } from "node:fs/promises";
3
+ import { join, resolve } from "node:path";
4
+ import { writeDurableJson } from "../shared/durable-json.js";
5
+ import { AgentRuntime as PortableAgentRuntime } from "./typescript.js";
6
+ export * from "./typescript.js";
7
+ export function fileJournalStore(directory) {
8
+ const root = resolve(directory);
9
+ const path = (id) => {
10
+ if (!/^client_[a-f0-9]{40}$/.test(id))
11
+ throw new Error("Invalid journal session id");
12
+ return join(root, `${id}.json`);
13
+ };
14
+ return {
15
+ async load(id) {
16
+ try {
17
+ return JSON.parse(await readFile(path(id), "utf8"));
18
+ }
19
+ catch (error) {
20
+ if (error.code === "ENOENT")
21
+ return undefined;
22
+ throw error;
23
+ }
24
+ },
25
+ async save(id, journal) { writeDurableJson(path(id), journal); },
26
+ };
27
+ }
28
+ export class AgentRuntime extends PortableAgentRuntime {
29
+ constructor(options = {}) {
30
+ super({ ...options, url: options.url ?? process.env.AGENT_URL,
31
+ apiKey: options.apiKey ?? process.env.AGENT_RUNTIME_TOKEN,
32
+ journalStore: options.journalStore ?? fileJournalStore(options.stateDirectory ?? process.env.AGENT_CLIENT_STATE_DIR ?? ".agent-runtime/client-sdk"),
33
+ });
34
+ }
35
+ }
@@ -0,0 +1,298 @@
1
+ import type { AgentMessage, ThinkingLevel } from "@earendil-works/pi-agent-core";
2
+ import type { Api, ImageContent, Model } from "@earendil-works/pi-ai";
3
+ import { Type, type TSchema, type Static } from "typebox";
4
+ import { type Outcome, type RequestMethod, type SessionCredentials, type SessionState } from "../shared/client-protocol.ts";
5
+ export { Type as schema };
6
+ export type { SessionCredentials, SessionState };
7
+ export interface ToolContext {
8
+ signal: AbortSignal;
9
+ callId: string;
10
+ toolCallId?: string;
11
+ /** Set by the runtime, e.g. `{ channel, conversationId, sender }` for a turn a channel message started. */
12
+ origin?: Record<string, unknown>;
13
+ }
14
+ export interface Tool<T = any> {
15
+ description: string;
16
+ resultFormat?: "json" | "content";
17
+ exposure?: "direct" | "codemode" | "both";
18
+ executionMode?: "sequential" | "parallel";
19
+ input: Record<string, unknown>;
20
+ execute: (args: T, context: ToolContext) => unknown | Promise<unknown>;
21
+ }
22
+ /** Infer callback arguments from the schema; no manually duplicated argument type. */
23
+ export declare function tool<S extends TSchema>(definition: Omit<Tool<Static<S>>, "input"> & {
24
+ input: S;
25
+ }): Tool<Static<S>>;
26
+ export type Tools = Record<string, Tool>;
27
+ export interface RuntimeOptions {
28
+ url?: string;
29
+ apiKey?: string;
30
+ /** Persist tool receipts and event cursors before acknowledging work. Default: memory only. */
31
+ journalStore?: JournalStore;
32
+ /** Injectable for tests, observability, or an application's HTTP stack. */
33
+ fetch?: typeof globalThis.fetch;
34
+ }
35
+ export interface AgentOptions {
36
+ tools: Tools;
37
+ onEvent?: (event: any, requestId?: string) => unknown | Promise<unknown>;
38
+ onConnection?: (connected: boolean) => void;
39
+ onError?: (error: Error) => void;
40
+ }
41
+ export type { ThinkingLevel };
42
+ export interface CreateAgentOptions extends AgentOptions {
43
+ idempotencyKey?: string;
44
+ /** Agent lifetime in seconds (60 to 366 days), or null to keep the agent until it is deleted. Default one day. */
45
+ ttlSeconds?: number | null;
46
+ systemPrompt?: string;
47
+ name?: string;
48
+ type?: string;
49
+ /**
50
+ * A model from the runtime's catalog as "provider/model-id" (see GET /v1/models),
51
+ * e.g. "anthropic/claude-sonnet-5" or "openrouter/openai/gpt-5.2". A full Pi
52
+ * model object is also accepted if its endpoint is trusted by the runtime.
53
+ */
54
+ model?: string | Model<Api>;
55
+ thinkingLevel?: ThinkingLevel;
56
+ initialMessages?: AgentMessage[];
57
+ /** Volumes for the agent's file tools (read, write, edit, ls, glob, grep). Default: its own workspace volume at /workspace. */
58
+ mounts?: Mount[];
59
+ }
60
+ /** A volume the agent's file tools see at `path`; `notify` prompts the agent when others change files there. */
61
+ export interface Mount {
62
+ volumeId: string;
63
+ path: string;
64
+ mode: "ro" | "rw";
65
+ subpath?: string;
66
+ notify?: boolean;
67
+ }
68
+ export interface Volume {
69
+ id: string;
70
+ name: string;
71
+ createdAt: number;
72
+ seq?: number;
73
+ files?: number;
74
+ bytes?: number;
75
+ origin?: {
76
+ volume: string;
77
+ snapshot?: string;
78
+ seq: number;
79
+ };
80
+ }
81
+ export interface VolumeFile {
82
+ path: string;
83
+ version: number;
84
+ size: number;
85
+ updatedAt: number;
86
+ by?: string;
87
+ }
88
+ export interface VolumeSnapshot {
89
+ id: string;
90
+ volume: string;
91
+ name: string;
92
+ seq: number;
93
+ createdAt: number;
94
+ files: number;
95
+ bytes: number;
96
+ }
97
+ export interface VolumeChanges {
98
+ seq: number;
99
+ changes: {
100
+ seq: number;
101
+ path: string;
102
+ kind: "write" | "delete";
103
+ version?: number;
104
+ size?: number;
105
+ by?: string;
106
+ at: number;
107
+ }[];
108
+ gap?: boolean;
109
+ }
110
+ export interface AgentHistory {
111
+ messages: AgentMessage[];
112
+ }
113
+ export interface Schedule {
114
+ id: string;
115
+ agent: string;
116
+ text?: string;
117
+ code?: string;
118
+ dueAt: number;
119
+ everySeconds?: number;
120
+ createdAt: number;
121
+ }
122
+ export interface RequestOptions {
123
+ idempotencyKey?: string;
124
+ timeoutMs?: number;
125
+ }
126
+ export declare class AgentError extends Error {
127
+ status: number;
128
+ requestId?: string;
129
+ constructor(message: string, status?: number, requestId?: string);
130
+ }
131
+ declare class Transport {
132
+ readonly base: string;
133
+ readonly fetcher: typeof globalThis.fetch;
134
+ constructor(options: RuntimeOptions);
135
+ json(path: string, token: string, method?: string, body?: unknown, retry?: boolean, headers?: Record<string, string>): Promise<any>;
136
+ /** A request with a raw body or response (volume file contents). */
137
+ raw(path: string, token: string, init?: {
138
+ method?: string;
139
+ body?: Uint8Array;
140
+ headers?: Record<string, string>;
141
+ }): Promise<Response>;
142
+ }
143
+ /** Trusted-backend SDK. Only createAgent needs the operator key. */
144
+ export declare class AgentRuntime {
145
+ readonly options: RuntimeOptions;
146
+ private readonly transport;
147
+ constructor(options?: RuntimeOptions);
148
+ createAgent(options: CreateAgentOptions): Promise<AgentClient>;
149
+ connectAgent(session: SessionCredentials, options: AgentOptions): Promise<AgentClient>;
150
+ private operator;
151
+ createVolume(options?: {
152
+ name?: string;
153
+ }): Promise<Volume>;
154
+ listVolumes(): Promise<Volume[]>;
155
+ /** A handle on one volume's files, snapshots and forks. */
156
+ volume(id: string): VolumeHandle;
157
+ mounts(agentId: string): Promise<Mount[]>;
158
+ /** Replace an agent's mounts; an idle agent restarts so its tools describe them. */
159
+ setMounts(agentId: string, mounts: Mount[]): Promise<Mount[]>;
160
+ }
161
+ /** Files are versioned: pass `version` to write or remove only if nobody changed the file since (0: must not exist). */
162
+ export declare class VolumeHandle {
163
+ readonly id: string;
164
+ private readonly transport;
165
+ private readonly token;
166
+ constructor(transport: Transport, token: string, id: string);
167
+ private path;
168
+ private file;
169
+ info(): Promise<Volume>;
170
+ delete(): Promise<any>;
171
+ snapshot(options?: {
172
+ name?: string;
173
+ }): Promise<VolumeSnapshot>;
174
+ snapshots(): Promise<VolumeSnapshot[]>;
175
+ deleteSnapshot(id: string): Promise<any>;
176
+ /** A new volume with this one's files (or a snapshot's); only metadata is copied. */
177
+ fork(options?: {
178
+ name?: string;
179
+ snapshot?: string;
180
+ }): Promise<Volume>;
181
+ changes(since?: number): Promise<VolumeChanges>;
182
+ list(options?: {
183
+ prefix?: string;
184
+ glob?: string;
185
+ after?: string;
186
+ limit?: number;
187
+ }): Promise<{
188
+ files: VolumeFile[];
189
+ next?: string;
190
+ }>;
191
+ write(path: string, data: string | Uint8Array, options?: {
192
+ version?: number;
193
+ }): Promise<VolumeFile>;
194
+ /** A file's bytes, or `range` of them ([start, end) in bytes). */
195
+ read(path: string, options?: {
196
+ range?: [number, number?];
197
+ }): Promise<{
198
+ data: Uint8Array;
199
+ version: number;
200
+ }>;
201
+ readText(path: string): Promise<string>;
202
+ remove(path: string, options?: {
203
+ version?: number;
204
+ }): Promise<any>;
205
+ }
206
+ export type Journal = {
207
+ version: 1;
208
+ cursor: number;
209
+ calls: Record<string, {
210
+ state: "started" | "done";
211
+ outcome?: Outcome;
212
+ }>;
213
+ };
214
+ /** Stores must resolve only once the complete snapshot is committed. Use one active client per agent. */
215
+ export interface JournalStore {
216
+ load(sessionId: string): Promise<Journal | undefined>;
217
+ save(sessionId: string, journal: Journal): Promise<void>;
218
+ }
219
+ export declare function memoryJournalStore(): JournalStore;
220
+ export declare class AgentClient {
221
+ readonly session: SessionCredentials;
222
+ readonly tools: Tools;
223
+ private readonly transport;
224
+ private readonly store;
225
+ private journal;
226
+ private loaded?;
227
+ private saving;
228
+ private readonly options;
229
+ private readonly pending;
230
+ private readonly active;
231
+ private readonly delivered;
232
+ private stream?;
233
+ private loop?;
234
+ private closed;
235
+ private fatal?;
236
+ private ready;
237
+ constructor(runtime: RuntimeOptions, session: SessionCredentials, options: AgentOptions);
238
+ private load;
239
+ private save;
240
+ private path;
241
+ private http;
242
+ private report;
243
+ connect(): Promise<void>;
244
+ private events;
245
+ /** Rename/regroup this agent without changing its conversation or tools. */
246
+ setMetadata(metadata: {
247
+ name: string;
248
+ type: string;
249
+ }): Promise<any>;
250
+ private receive;
251
+ private settle;
252
+ private sync;
253
+ private dispatch;
254
+ private runTool;
255
+ request(method: RequestMethod, params?: Record<string, unknown>, options?: RequestOptions): Promise<any>;
256
+ /** Observe an already accepted request. This never submits or re-executes work. */
257
+ waitForRequest(id: string, options?: {
258
+ timeoutMs?: number;
259
+ }): Promise<any>;
260
+ prompt(text: string, options?: RequestOptions & {
261
+ images?: ImageContent[];
262
+ }): Promise<any>;
263
+ history(): Promise<AgentHistory>;
264
+ continue(options?: RequestOptions): Promise<any>;
265
+ steer(text: string): Promise<any>;
266
+ followUp(text: string): Promise<any>;
267
+ /** Change the prompt, thinking level, tools, or model ("provider/model-id") between runs. */
268
+ configure(options: {
269
+ systemPrompt?: string;
270
+ thinkingLevel?: ThinkingLevel;
271
+ tools?: Tools;
272
+ model?: string;
273
+ }): Promise<any>;
274
+ execute(code: string, options?: RequestOptions & {
275
+ timeoutMs?: number;
276
+ executionTimeoutMs?: number;
277
+ }): Promise<any>;
278
+ /**
279
+ * Wake this agent later: with `text` it gets a prompt, with `code` it runs sandboxed
280
+ * code against your tools. `everySeconds` (at least 60) repeats it.
281
+ */
282
+ schedule(input: {
283
+ text?: string;
284
+ code?: string;
285
+ at?: string | Date;
286
+ inSeconds?: number;
287
+ everySeconds?: number;
288
+ }): Promise<Schedule>;
289
+ schedules(): Promise<Schedule[]>;
290
+ unschedule(id: string): Promise<any>;
291
+ status(): Promise<any>;
292
+ abort(): Promise<any>;
293
+ requestStatus(id: string): Promise<any>;
294
+ outcomes(): Promise<SessionState>;
295
+ close(): Promise<void>;
296
+ destroy(): Promise<void>;
297
+ [Symbol.asyncDispose](): Promise<void>;
298
+ }
@@ -0,0 +1,509 @@
1
+ import { Type } from "typebox";
2
+ import { Check } from "typebox/value";
3
+ import { FRAME_BYTES } from "../shared/client-protocol.js";
4
+ export { Type as schema };
5
+ /** Infer callback arguments from the schema; no manually duplicated argument type. */
6
+ export function tool(definition) {
7
+ return { ...definition, input: definition.input };
8
+ }
9
+ export class AgentError extends Error {
10
+ status;
11
+ requestId;
12
+ constructor(message, status = 0, requestId) { super(message); this.name = "AgentError"; this.status = status; this.requestId = requestId; }
13
+ }
14
+ const byteLength = (value) => new TextEncoder().encode(value).byteLength;
15
+ const pause = (ms) => new Promise(resolve => setTimeout(resolve, ms));
16
+ const definitions = (tools) => Object.entries(tools).map(([name, tool]) => ({ name, description: tool.description, parameters: tool.input, ...(tool.resultFormat ? { resultFormat: tool.resultFormat } : {}), ...(tool.exposure ? { exposure: tool.exposure } : {}), ...(tool.executionMode ? { executionMode: tool.executionMode } : {}) }));
17
+ async function rejectRedirect(response) {
18
+ if (response.status >= 300 && response.status < 400) {
19
+ await response.body?.cancel();
20
+ throw new AgentError("Runtime redirects are not allowed", response.status);
21
+ }
22
+ }
23
+ class Transport {
24
+ base;
25
+ fetcher;
26
+ constructor(options) {
27
+ const url = new URL(options.url ?? "http://127.0.0.1:8790");
28
+ if (url.username || url.password || url.search || url.hash || url.pathname !== "/")
29
+ throw new Error("Use a runtime origin without credentials, path, or query");
30
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)))
31
+ throw new Error("Remote runtimes require https://");
32
+ this.base = url.origin;
33
+ const fetcher = options.fetch;
34
+ this.fetcher = fetcher ? (input, init) => fetcher(input, init) : globalThis.fetch.bind(globalThis);
35
+ }
36
+ async json(path, token, method = "GET", body, retry = true, headers = {}) {
37
+ const data = body === undefined ? undefined : JSON.stringify(body);
38
+ if (data && byteLength(data) > FRAME_BYTES)
39
+ throw new AgentError("Request exceeds transport limit");
40
+ for (let attempt = 0;; attempt++) {
41
+ try {
42
+ const response = await this.fetcher(this.base + path, {
43
+ method, headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", ...headers }, body: data,
44
+ redirect: "manual", signal: AbortSignal.timeout(10_000),
45
+ });
46
+ await rejectRedirect(response);
47
+ const value = await response.json();
48
+ if (!response.ok)
49
+ throw new AgentError(value.error ?? `HTTP ${response.status}`, response.status);
50
+ return value;
51
+ }
52
+ catch (error) {
53
+ if (!retry || attempt >= 3 || (error instanceof AgentError && error.status < 500))
54
+ throw error;
55
+ await pause(100 * 2 ** attempt);
56
+ }
57
+ }
58
+ }
59
+ /** A request with a raw body or response (volume file contents). */
60
+ async raw(path, token, init = {}) {
61
+ const response = await this.fetcher(this.base + path, { method: init.method ?? "GET", body: init.body, headers: { Authorization: `Bearer ${token}`, ...init.headers }, redirect: "manual" });
62
+ await rejectRedirect(response);
63
+ if (!response.ok) {
64
+ const value = await response.json().catch(() => ({}));
65
+ throw new AgentError(value.error ?? `HTTP ${response.status}`, response.status);
66
+ }
67
+ return response;
68
+ }
69
+ }
70
+ /** Trusted-backend SDK. Only createAgent needs the operator key. */
71
+ export class AgentRuntime {
72
+ options;
73
+ transport;
74
+ constructor(options = {}) { this.options = options; this.transport = new Transport(options); }
75
+ async createAgent(options) {
76
+ const key = this.options.apiKey;
77
+ if (!key)
78
+ throw new AgentError("Set apiKey to provision an agent");
79
+ const session = await this.transport.json("/client-sessions", key, "POST", { tools: definitions(options.tools), ...(options.mounts !== undefined ? { mounts: options.mounts } : {}), ...(options.model !== undefined ? { model: options.model } : {}), ...(options.thinkingLevel !== undefined ? { thinkingLevel: options.thinkingLevel } : {}), ...(options.initialMessages !== undefined ? { initialMessages: options.initialMessages } : {}), ...(options.name !== undefined ? { name: options.name } : {}), ...(options.type !== undefined ? { type: options.type } : {}), ...(options.systemPrompt !== undefined ? { systemPrompt: options.systemPrompt } : {}), ...(options.ttlSeconds !== undefined ? { ttlSeconds: options.ttlSeconds } : {}) }, true, { "Idempotency-Key": options.idempotencyKey ?? globalThis.crypto.randomUUID() });
80
+ return this.connectAgent(session, options);
81
+ }
82
+ async connectAgent(session, options) {
83
+ const client = new AgentClient(this.options, session, options);
84
+ try {
85
+ await client.connect();
86
+ return client;
87
+ }
88
+ catch (error) {
89
+ await client.close();
90
+ throw error;
91
+ }
92
+ }
93
+ operator() {
94
+ if (!this.options.apiKey)
95
+ throw new AgentError("Set apiKey to manage volumes and mounts");
96
+ return this.options.apiKey;
97
+ }
98
+ createVolume(options = {}) { return this.transport.json("/v1/volumes", this.operator(), "POST", options, false); }
99
+ listVolumes() { return this.transport.json("/v1/volumes", this.operator()); }
100
+ /** A handle on one volume's files, snapshots and forks. */
101
+ volume(id) {
102
+ if (!/^vol_[a-f0-9]{24}$/.test(id))
103
+ throw new AgentError("Invalid volume id");
104
+ return new VolumeHandle(this.transport, this.operator(), id);
105
+ }
106
+ mounts(agentId) { return this.transport.json(`/v1/agents/${encodeURIComponent(agentId)}/mounts`, this.operator()); }
107
+ /** Replace an agent's mounts; an idle agent restarts so its tools describe them. */
108
+ setMounts(agentId, mounts) { return this.transport.json(`/v1/agents/${encodeURIComponent(agentId)}/mounts`, this.operator(), "PUT", { mounts }, false); }
109
+ }
110
+ /** Files are versioned: pass `version` to write or remove only if nobody changed the file since (0: must not exist). */
111
+ export class VolumeHandle {
112
+ id;
113
+ transport;
114
+ token;
115
+ constructor(transport, token, id) { this.transport = transport; this.token = token; this.id = id; }
116
+ path(suffix = "") { return `/v1/volumes/${this.id}${suffix}`; }
117
+ file(path) { return this.path(`/files/${path.split("/").filter(Boolean).map(encodeURIComponent).join("/")}`); }
118
+ info() { return this.transport.json(this.path(), this.token); }
119
+ delete() { return this.transport.json(this.path(), this.token, "DELETE", undefined, false); }
120
+ snapshot(options = {}) { return this.transport.json(this.path("/snapshots"), this.token, "POST", options, false); }
121
+ snapshots() { return this.transport.json(this.path("/snapshots"), this.token); }
122
+ deleteSnapshot(id) { return this.transport.json(this.path(`/snapshots/${encodeURIComponent(id)}`), this.token, "DELETE", undefined, false); }
123
+ /** A new volume with this one's files (or a snapshot's); only metadata is copied. */
124
+ fork(options = {}) { return this.transport.json(this.path("/fork"), this.token, "POST", options, false); }
125
+ changes(since = 0) { return this.transport.json(this.path(`/changes?since=${since}`), this.token); }
126
+ list(options = {}) {
127
+ const query = new URLSearchParams(Object.entries(options).filter(([, value]) => value !== undefined).map(([key, value]) => [key, String(value)]));
128
+ return this.transport.json(this.path(`/files${query.size ? `?${query}` : ""}`), this.token);
129
+ }
130
+ async write(path, data, options = {}) {
131
+ const body = typeof data === "string" ? new TextEncoder().encode(data) : data;
132
+ const headers = { "Content-Type": "application/octet-stream", ...(options.version === 0 ? { "If-None-Match": "*" } : options.version !== undefined ? { "If-Match": `"${options.version}"` } : {}) };
133
+ return (await this.transport.raw(this.file(path), this.token, { method: "PUT", body, headers })).json();
134
+ }
135
+ /** A file's bytes, or `range` of them ([start, end) in bytes). */
136
+ async read(path, options = {}) {
137
+ const [start, end] = options.range ?? [];
138
+ const response = await this.transport.raw(this.file(path), this.token, start !== undefined ? { headers: { Range: `bytes=${start}-${end !== undefined ? end - 1 : ""}` } } : {});
139
+ return { data: new Uint8Array(await response.arrayBuffer()), version: Number(response.headers.get("etag")?.replaceAll('"', "")) };
140
+ }
141
+ async readText(path) { return new TextDecoder().decode((await this.read(path)).data); }
142
+ async remove(path, options = {}) {
143
+ return (await this.transport.raw(this.file(path), this.token, { method: "DELETE", headers: options.version !== undefined ? { "If-Match": `"${options.version}"` } : {} })).json();
144
+ }
145
+ }
146
+ export function memoryJournalStore() {
147
+ const entries = new Map();
148
+ return {
149
+ async load(id) { const value = entries.get(id); return value && structuredClone(value); },
150
+ async save(id, journal) { entries.set(id, structuredClone(journal)); },
151
+ };
152
+ }
153
+ export class AgentClient {
154
+ session;
155
+ tools;
156
+ transport;
157
+ store;
158
+ journal = { version: 1, cursor: 0, calls: {} };
159
+ loaded;
160
+ saving = Promise.resolve();
161
+ options;
162
+ pending = new Map();
163
+ active = new Map();
164
+ delivered = new Set();
165
+ stream;
166
+ loop;
167
+ closed = false;
168
+ fatal;
169
+ ready = Promise.withResolvers();
170
+ constructor(runtime, session, options) {
171
+ if (!/^client_[a-f0-9]{40}$/.test(session.id))
172
+ throw new AgentError("Invalid session id");
173
+ this.session = { id: session.id, token: session.token, expiresAt: session.expiresAt };
174
+ this.tools = { ...options.tools };
175
+ this.options = options;
176
+ this.transport = new Transport(runtime);
177
+ this.store = runtime.journalStore ?? memoryJournalStore();
178
+ }
179
+ async load() {
180
+ const journal = await this.store.load(this.session.id);
181
+ if (!journal)
182
+ return;
183
+ if (journal.version !== 1 || !Number.isSafeInteger(journal.cursor) || journal.cursor < 0 || !journal.calls || typeof journal.calls !== "object")
184
+ throw new AgentError("Unsupported client journal");
185
+ this.journal = structuredClone(journal);
186
+ }
187
+ save() {
188
+ const snapshot = structuredClone(this.journal);
189
+ // Serialize commits so concurrent tool completions cannot overwrite newer receipts.
190
+ this.saving = this.saving.then(() => this.store.save(this.session.id, snapshot));
191
+ return this.saving;
192
+ }
193
+ path(suffix = "") { return `/clients/${this.session.id}${suffix}`; }
194
+ http(suffix, method = "GET", body, retry = true) { return this.transport.json(this.path(suffix), this.session.token, method, body, retry); }
195
+ report(error) { this.options.onError?.(error instanceof Error ? error : new Error(String(error))); }
196
+ async connect() {
197
+ if (this.closed)
198
+ throw new AgentError("Client closed");
199
+ await (this.loaded ??= this.load());
200
+ this.loop ??= this.events();
201
+ await Promise.race([this.ready.promise, new Promise((_, reject) => {
202
+ const timer = setTimeout(() => reject(new AgentError("Timed out connecting to agent")), 10_000);
203
+ this.ready.promise.finally(() => clearTimeout(timer)).catch(() => { });
204
+ })]);
205
+ }
206
+ async events() {
207
+ let backoff = 250;
208
+ while (!this.closed) {
209
+ this.stream = new AbortController();
210
+ let watchdog;
211
+ const touch = () => { clearTimeout(watchdog); watchdog = setTimeout(() => this.stream?.abort(), 20_000); };
212
+ touch();
213
+ try {
214
+ const response = await this.transport.fetcher(this.transport.base + this.path("/events"), {
215
+ headers: { Authorization: `Bearer ${this.session.token}`, Accept: "text/event-stream", "Last-Event-ID": String(this.journal.cursor) },
216
+ signal: this.stream.signal, redirect: "manual",
217
+ });
218
+ await rejectRedirect(response);
219
+ if (response.status === 409) {
220
+ await response.body?.cancel();
221
+ const snapshot = await this.sync();
222
+ this.journal.cursor = snapshot.cursor;
223
+ await this.save();
224
+ await this.options.onEvent?.({ type: "replay_gap", cursor: snapshot.cursor });
225
+ continue;
226
+ }
227
+ if (!response.ok) {
228
+ await response.body?.cancel();
229
+ throw new AgentError(`Event stream HTTP ${response.status}`, response.status);
230
+ }
231
+ if (!response.headers.get("content-type")?.startsWith("text/event-stream") || !response.body)
232
+ throw new AgentError("Expected an SSE response");
233
+ const reader = response.body.getReader();
234
+ const decoder = new TextDecoder();
235
+ let buffer = "";
236
+ try {
237
+ while (!this.closed) {
238
+ const { value, done } = await reader.read();
239
+ if (done)
240
+ break;
241
+ touch();
242
+ buffer += decoder.decode(value, { stream: true });
243
+ let end;
244
+ while ((end = buffer.indexOf("\n\n")) !== -1) {
245
+ const frame = buffer.slice(0, end);
246
+ buffer = buffer.slice(end + 2);
247
+ if (byteLength(frame) > FRAME_BYTES)
248
+ throw new AgentError("SSE frame too large");
249
+ const lines = frame.split("\n");
250
+ const data = lines.filter(line => line.startsWith("data:")).map(line => line.slice(5).trimStart()).join("\n");
251
+ if (!data)
252
+ continue;
253
+ if (lines.includes("event: ready")) {
254
+ await this.sync();
255
+ backoff = 250;
256
+ this.ready.resolve();
257
+ this.options.onConnection?.(true);
258
+ continue;
259
+ }
260
+ const id = Number(lines.find(line => line.startsWith("id:"))?.slice(3));
261
+ if (!Number.isSafeInteger(id) || id <= 0)
262
+ throw new AgentError("Invalid SSE cursor");
263
+ if (id <= this.journal.cursor)
264
+ continue;
265
+ const event = JSON.parse(data);
266
+ await this.receive(event);
267
+ this.journal.cursor = id;
268
+ // Display events are replayable only from the host's memory; persisting the
269
+ // cursor for each one would cost a storage write per streamed token.
270
+ if (event.type !== "event")
271
+ await this.save();
272
+ }
273
+ if (byteLength(buffer) > FRAME_BYTES)
274
+ throw new AgentError("SSE frame too large");
275
+ }
276
+ }
277
+ finally {
278
+ await reader.cancel().catch(() => { });
279
+ reader.releaseLock();
280
+ }
281
+ }
282
+ catch (error) {
283
+ if (this.closed)
284
+ break;
285
+ if (error instanceof AgentError && ([401, 403, 410].includes(error.status) || (error.status >= 300 && error.status < 400))) {
286
+ this.fatal = error;
287
+ this.ready.reject(error);
288
+ for (const waiter of this.pending.values())
289
+ waiter.reject(error);
290
+ this.pending.clear();
291
+ this.report(error);
292
+ break;
293
+ }
294
+ this.report(error);
295
+ }
296
+ finally {
297
+ clearTimeout(watchdog);
298
+ this.options.onConnection?.(false);
299
+ }
300
+ if (!this.closed) {
301
+ await pause(backoff);
302
+ backoff = Math.min(5000, backoff * 2);
303
+ }
304
+ }
305
+ }
306
+ /** Rename/regroup this agent without changing its conversation or tools. */
307
+ async setMetadata(metadata) {
308
+ return this.transport.json(this.path('/metadata'), this.session.token, 'POST', metadata);
309
+ }
310
+ async receive(event) {
311
+ if (event.type === "tool_call")
312
+ this.dispatch(event.call);
313
+ else if (event.type === "tool_cancel")
314
+ this.active.get(event.id)?.controller.abort();
315
+ else if (event.type === "response")
316
+ this.settle(event.id, event.outcome);
317
+ else if (event.type === "event")
318
+ await this.options.onEvent?.(event.event, event.requestId);
319
+ }
320
+ settle(id, value) {
321
+ const waiter = this.pending.get(id);
322
+ if (!waiter)
323
+ return;
324
+ this.pending.delete(id);
325
+ if ("error" in value)
326
+ waiter.reject(new AgentError(value.error ?? "Unknown failure", 0, id));
327
+ else
328
+ waiter.resolve(value.result);
329
+ }
330
+ async sync() {
331
+ const state = await this.outcomes();
332
+ for (const request of state.requests)
333
+ if (request.outcome)
334
+ this.settle(request.id, request.outcome);
335
+ for (const call of state.calls)
336
+ this.dispatch(call);
337
+ return state;
338
+ }
339
+ dispatch(call) {
340
+ if (this.closed || this.active.has(call.id) || this.delivered.has(call.id) || ["completed", "cancelled"].includes(call.state))
341
+ return;
342
+ const controller = new AbortController();
343
+ // Defer execution until the active entry exists; replay can arrive immediately.
344
+ const task = Promise.resolve().then(() => this.runTool(call, controller)).catch(error => this.report(error)).finally(() => this.active.delete(call.id));
345
+ this.active.set(call.id, { controller, task });
346
+ }
347
+ async runTool(call, controller) {
348
+ let receipt = this.journal.calls[call.id];
349
+ if (receipt?.state === "done") {
350
+ await this.http(`/calls/${call.id}/outcome`, "POST", receipt.outcome);
351
+ this.delivered.add(call.id);
352
+ return;
353
+ }
354
+ if (call.state === "uncertain")
355
+ return; // Already settled as unknown; never execute again.
356
+ let value;
357
+ if (call.state === "started")
358
+ value = { error: "The application lost this tool call's outcome; it may or may not have taken effect", uncertain: true };
359
+ else {
360
+ this.journal.calls[call.id] = { state: "started" };
361
+ await this.save();
362
+ try {
363
+ // Claim is deliberately NOT retried. A lost acknowledgement is ambiguous.
364
+ const claim = await this.http(`/calls/${call.id}/claim`, "POST", {}, false);
365
+ if (!claim.execute) {
366
+ if (claim.call.state !== "started")
367
+ return;
368
+ value = { error: "Tool execution was already claimed; outcome unknown", uncertain: true };
369
+ }
370
+ else {
371
+ const timer = setTimeout(() => controller.abort(), Math.max(1, call.deadline - Date.now()));
372
+ // A callback that ignores cancellation must not keep a closed client's process alive until the deadline.
373
+ timer.unref?.();
374
+ try {
375
+ const definition = this.tools[call.name];
376
+ if (!Object.hasOwn(this.tools, call.name) || !Check(definition.input, call.args))
377
+ throw new Error("Tool is missing or arguments failed validation");
378
+ controller.signal.throwIfAborted();
379
+ const result = await definition.execute(call.args, { callId: call.id, toolCallId: call.toolCallId, signal: controller.signal, ...(call.origin ? { origin: call.origin } : {}) });
380
+ if (result === undefined || byteLength(JSON.stringify(result)) > 1024 * 1024)
381
+ throw new Error("Tool must return a bounded JSON value");
382
+ value = { result };
383
+ }
384
+ catch (error) {
385
+ value = { error: String(error).slice(0, 2048), ...(controller.signal.aborted ? { uncertain: true } : {}) };
386
+ }
387
+ finally {
388
+ clearTimeout(timer);
389
+ }
390
+ }
391
+ }
392
+ catch (error) {
393
+ value = { error: `Execution claim failed: ${String(error).slice(0, 1800)}`, uncertain: true };
394
+ }
395
+ }
396
+ // Persist before POST; reconnect resends this receipt, never the side effect.
397
+ receipt = { state: "done", outcome: value };
398
+ this.journal.calls[call.id] = receipt;
399
+ await this.save();
400
+ await this.http(`/calls/${call.id}/outcome`, "POST", value);
401
+ this.delivered.add(call.id);
402
+ }
403
+ async request(method, params = {}, options = {}) {
404
+ if (this.closed || this.fatal)
405
+ throw this.fatal ?? new AgentError("Client closed");
406
+ if (this.pending.size >= 8)
407
+ throw new AgentError("Too many outstanding requests");
408
+ const id = options.idempotencyKey ?? globalThis.crypto.randomUUID();
409
+ if (this.pending.has(id))
410
+ throw new AgentError("Request already pending", 409, id);
411
+ const deferred = Promise.withResolvers();
412
+ // Attach immediately, even while the POST is pending, to avoid unhandled errors.
413
+ deferred.promise.catch(() => { });
414
+ this.pending.set(id, deferred);
415
+ const timer = setTimeout(() => {
416
+ this.pending.delete(id);
417
+ deferred.reject(new AgentError("Request timed out; inspect requestStatus() or reuse the same idempotencyKey", 0, id));
418
+ }, options.timeoutMs ?? 180_000);
419
+ try {
420
+ const record = await this.http("/requests", "POST", { id, method, params });
421
+ if (record.outcome)
422
+ this.settle(id, record.outcome);
423
+ return await deferred.promise;
424
+ }
425
+ catch (error) {
426
+ if (error instanceof AgentError) {
427
+ error.requestId ??= id;
428
+ throw error;
429
+ }
430
+ throw new AgentError(String(error), 0, id);
431
+ }
432
+ finally {
433
+ clearTimeout(timer);
434
+ this.pending.delete(id);
435
+ }
436
+ }
437
+ /** Observe an already accepted request. This never submits or re-executes work. */
438
+ async waitForRequest(id, options = {}) {
439
+ if (this.closed || this.fatal)
440
+ throw this.fatal ?? new AgentError("Client closed");
441
+ if (this.pending.has(id))
442
+ throw new AgentError("Request already pending", 409, id);
443
+ const deferred = Promise.withResolvers();
444
+ deferred.promise.catch(() => { });
445
+ this.pending.set(id, deferred);
446
+ const timer = setTimeout(() => {
447
+ deferred.reject(new AgentError("Request observation timed out; the request may still be running", 0, id));
448
+ }, options.timeoutMs ?? 180_000);
449
+ try {
450
+ const record = await this.requestStatus(id);
451
+ if (record.outcome)
452
+ this.settle(id, record.outcome);
453
+ return await deferred.promise;
454
+ }
455
+ finally {
456
+ clearTimeout(timer);
457
+ this.pending.delete(id);
458
+ }
459
+ }
460
+ prompt(text, options) { return this.request("prompt", { text, ...(options?.images ? { images: options.images } : {}) }, options); }
461
+ history() { return this.http("/history"); }
462
+ continue(options) { return this.request("continue", {}, options); }
463
+ steer(text) { return this.request("steer", { text }); }
464
+ followUp(text) { return this.request("followUp", { text }); }
465
+ /** Change the prompt, thinking level, tools, or model ("provider/model-id") between runs. */
466
+ async configure(options) {
467
+ const result = await this.request("configure", { ...options, ...(options.tools ? { tools: definitions(options.tools) } : {}) });
468
+ if (options.tools) {
469
+ for (const key of Object.keys(this.tools))
470
+ delete this.tools[key];
471
+ Object.assign(this.tools, options.tools);
472
+ }
473
+ return result;
474
+ }
475
+ execute(code, options) {
476
+ return this.request("execute", { code, ...(options?.executionTimeoutMs ? { timeoutMs: options.executionTimeoutMs } : {}) }, options);
477
+ }
478
+ /**
479
+ * Wake this agent later: with `text` it gets a prompt, with `code` it runs sandboxed
480
+ * code against your tools. `everySeconds` (at least 60) repeats it.
481
+ */
482
+ schedule(input) {
483
+ return this.http("/schedules", "POST", { ...input, ...(input.at instanceof Date ? { at: input.at.toISOString() } : {}) }, false);
484
+ }
485
+ schedules() { return this.http("/schedules"); }
486
+ unschedule(id) { return this.http(`/schedules/${encodeURIComponent(id)}`, "DELETE", undefined, false); }
487
+ status() { return this.request("status"); }
488
+ abort() { return this.request("abort"); }
489
+ requestStatus(id) { return this.http(`/requests/${encodeURIComponent(id)}`); }
490
+ outcomes() { return this.http("/state"); }
491
+ async close() {
492
+ this.closed = true;
493
+ this.stream?.abort();
494
+ for (const { controller } of this.active.values())
495
+ controller.abort();
496
+ for (const [id, waiter] of this.pending)
497
+ waiter.reject(new AgentError("Client closed; request may still be running", 0, id));
498
+ this.pending.clear();
499
+ await this.loop;
500
+ // User callbacks must cooperate with cancellation. Do not hang shutdown on one.
501
+ }
502
+ async destroy() { try {
503
+ await this.http("", "DELETE");
504
+ }
505
+ finally {
506
+ await this.close();
507
+ } }
508
+ async [Symbol.asyncDispose]() { await this.close(); }
509
+ }
@@ -0,0 +1,79 @@
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
+ /** `expiresAt` is null for agents that live until deleted. */
13
+ export interface SessionCredentials {
14
+ id: string;
15
+ token: string;
16
+ expiresAt: number | null;
17
+ }
18
+ export type RequestMethod = "prompt" | "execute" | "status" | "abort" | "history" | "continue" | "steer" | "followUp" | "configure";
19
+ export type CallRecord = {
20
+ id: string;
21
+ toolCallId?: string;
22
+ requestId?: string;
23
+ createdAt?: number;
24
+ name: string;
25
+ args: Record<string, unknown>;
26
+ deadline: number;
27
+ /** `uncertain`: claimed, but the outcome was lost; the model was told it is unknown. */
28
+ state: "offered" | "started" | "completed" | "cancelled" | "uncertain";
29
+ outcome?: Outcome;
30
+ /** A result that arrived after the call was already settled as uncertain. */
31
+ lateOutcome?: Outcome;
32
+ /** Where the turn came from, set by the runtime (e.g. a channel and its sender), so tools can authorize. */
33
+ origin?: Record<string, unknown>;
34
+ };
35
+ export type RequestRecord = {
36
+ id: string;
37
+ startedAt?: number;
38
+ endedAt?: number;
39
+ prompt?: string;
40
+ code?: string;
41
+ fingerprint: string;
42
+ method: RequestMethod;
43
+ /** "running" covers queued runs too: a run has begun once `began` is set. */
44
+ state: "running" | "completed";
45
+ outcome?: Outcome;
46
+ /** When the agent actually started this run (runs queue behind each other). */
47
+ began?: number;
48
+ /** Kept until the run begins, so a queued run survives a restart and runs exactly once. */
49
+ params?: unknown;
50
+ };
51
+ export type ClientEvent = {
52
+ type: "tool_call";
53
+ call: CallRecord;
54
+ } | {
55
+ type: "tool_cancel";
56
+ id: string;
57
+ } | {
58
+ type: "event";
59
+ requestId: string;
60
+ event: any;
61
+ } | {
62
+ type: "response";
63
+ id: string;
64
+ outcome: Outcome;
65
+ };
66
+ export type SessionState = {
67
+ cursor: number;
68
+ calls: CallRecord[];
69
+ requests: RequestRecord[];
70
+ };
71
+ /** A tool an application offers its agent; shared by the SDKs and the runtime. */
72
+ export interface ToolDefinition {
73
+ name: string;
74
+ description: string;
75
+ parameters: Record<string, unknown>;
76
+ resultFormat?: "json" | "content";
77
+ exposure?: "direct" | "codemode" | "both";
78
+ executionMode?: "sequential" | "parallel";
79
+ }
@@ -0,0 +1 @@
1
+ export const FRAME_BYTES = 1_100_000;
@@ -0,0 +1,3 @@
1
+ /** Local-disk commit before acknowledging a request or dispatching a side effect. */
2
+ export declare function writeDurableJson(path: string, value: unknown): void;
3
+ export declare function canonical(value: unknown): string;
@@ -0,0 +1,36 @@
1
+ import { closeSync, fsyncSync, mkdirSync, openSync, renameSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { randomUUID } from "node:crypto";
4
+ /** Local-disk commit before acknowledging a request or dispatching a side effect. */
5
+ export function writeDurableJson(path, value) {
6
+ const directory = dirname(path);
7
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
8
+ const temporary = `${path}.${randomUUID()}.tmp`;
9
+ const fd = openSync(temporary, "wx", 0o600);
10
+ try {
11
+ writeFileSync(fd, JSON.stringify(value));
12
+ fsyncSync(fd);
13
+ }
14
+ finally {
15
+ closeSync(fd);
16
+ }
17
+ renameSync(temporary, path);
18
+ const parent = openSync(directory, "r");
19
+ try {
20
+ fsyncSync(parent);
21
+ }
22
+ finally {
23
+ closeSync(parent);
24
+ }
25
+ }
26
+ export function canonical(value) {
27
+ if (Array.isArray(value))
28
+ return `[${value.map(canonical).join(",")}]`;
29
+ if (value !== null && typeof value === "object") {
30
+ return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`;
31
+ }
32
+ const json = JSON.stringify(value);
33
+ if (json === undefined)
34
+ throw new Error("Expected a JSON value");
35
+ return json;
36
+ }
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@camelai/agent-runtime",
3
+ "version": "0.3.0",
4
+ "description": "SDK for the camelAI hosted agent runtime: define tools in your app, and the runtime runs the model loop, history and sandbox.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/qaml-ai/agent-runtime.git",
10
+ "directory": "sdk"
11
+ },
12
+ "publishConfig": {
13
+ "registry": "https://registry.npmjs.org",
14
+ "access": "public"
15
+ },
16
+ "engines": {
17
+ "node": ">=22"
18
+ },
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/clients/typescript.d.ts",
22
+ "default": "./dist/clients/typescript.js"
23
+ },
24
+ "./node": {
25
+ "types": "./dist/clients/node.d.ts",
26
+ "default": "./dist/clients/node.js"
27
+ }
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "README.md",
32
+ "LICENSE"
33
+ ],
34
+ "scripts": {
35
+ "build": "rm -rf dist && tsc -p tsconfig.json",
36
+ "prepublishOnly": "npm run build"
37
+ },
38
+ "dependencies": {
39
+ "typebox": "1.1.38"
40
+ },
41
+ "homepage": "https://agents.camelai.dev"
42
+ }