@camelai/run 0.0.0 → 0.11.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 +21 -0
- package/README.md +62 -1
- package/dist/clients/agents.d.ts +265 -0
- package/dist/clients/agents.js +283 -0
- package/dist/clients/ai-sdk.d.ts +137 -0
- package/dist/clients/ai-sdk.js +461 -0
- package/dist/clients/chat.d.ts +237 -0
- package/dist/clients/chat.js +633 -0
- package/dist/clients/handler.d.ts +116 -0
- package/dist/clients/handler.js +513 -0
- package/dist/clients/markdown.d.ts +72 -0
- package/dist/clients/markdown.js +385 -0
- package/dist/clients/mcp.d.ts +13 -0
- package/dist/clients/mcp.js +38 -0
- package/dist/clients/node.d.ts +21 -0
- package/dist/clients/node.js +76 -0
- package/dist/clients/server.d.ts +83 -0
- package/dist/clients/server.js +187 -0
- package/dist/clients/testing.d.ts +60 -0
- package/dist/clients/testing.js +53 -0
- package/dist/clients/types.d.ts +264 -0
- package/dist/clients/types.js +6 -0
- package/dist/clients/typescript.d.ts +1161 -0
- package/dist/clients/typescript.js +1035 -0
- package/dist/clients/watch.d.ts +90 -0
- package/dist/clients/watch.js +482 -0
- package/dist/shared/client-protocol.d.ts +106 -0
- package/dist/shared/client-protocol.js +9 -0
- package/package.json +78 -1
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
CHANGED
|
@@ -1,3 +1,64 @@
|
|
|
1
1
|
# @camelai/run
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The TypeScript SDK for camelRun: durable agents you upsert by
|
|
4
|
+
key and run, with tools that are ordinary functions in your code. The runtime
|
|
5
|
+
runs the model loop, keeps each agent's history and files, and runs
|
|
6
|
+
model-written code in a sandbox that can only call your tools.
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm install @camelai/run
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Node 22 or later, Bun, Deno or Cloudflare Workers. Get an API key from the
|
|
13
|
+
console at <https://agents.camelai.dev/console> and export it as
|
|
14
|
+
`CAMELAI_API_KEY`.
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { Agents, schema, tool } from "@camelai/run";
|
|
18
|
+
|
|
19
|
+
const agents = new Agents();
|
|
20
|
+
|
|
21
|
+
const weather = tool({
|
|
22
|
+
description: "Today's weather in a city",
|
|
23
|
+
input: schema.Object({ city: schema.String() }),
|
|
24
|
+
execute: ({ city }) => ({ city, forecast: "sunny", highC: 24 }), // runs here, in your process
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const agent = await agents.upsert("quickstart", {
|
|
28
|
+
model: "anthropic/claude-sonnet-5-5",
|
|
29
|
+
instructions: "You are a concise assistant.",
|
|
30
|
+
tools: { weather },
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const run = await agent.run("Should I bring an umbrella in Lisbon today?");
|
|
34
|
+
console.log(run.text);
|
|
35
|
+
|
|
36
|
+
await agents.close();
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
- **Keyed agents.** `upsert(key, config)` makes the agent for your key, or brings
|
|
40
|
+
the existing one to `config`; its history and files last until you delete it.
|
|
41
|
+
- **Runs.** `run()` resolves with `{ status, text, inputs, error, toolErrors, … }`
|
|
42
|
+
and throws `RunError` on failure (unless `throwOnError: false`). No timeout: pass
|
|
43
|
+
an `AbortSignal` to stop waiting. `stream()` yields text, tool calls and results
|
|
44
|
+
as they happen, then the run.
|
|
45
|
+
- **People in the loop.** A tool with `needsApproval: true` waits for approval;
|
|
46
|
+
`run.inputs[0].answer(true, { from })` resumes the run.
|
|
47
|
+
- **Tools.** Each call's `context.idempotencyKey` is stable across retries;
|
|
48
|
+
`timeoutMs` and `context.progress()` handle long calls. One process at a time
|
|
49
|
+
serves an agent's tools; serverless and multi-user backends serve them over
|
|
50
|
+
HTTP with `serveTools` (`@camelai/run/server`).
|
|
51
|
+
- **Browsers.** A chat in your app: one route with `createAgentHandler`
|
|
52
|
+
(`@camelai/run/server`) and `<AgentChat>` or the hooks from
|
|
53
|
+
`@camelai/run-react` (or `/chat` without React, `/ai-sdk` with the AI
|
|
54
|
+
SDK's `useChat`); see the Frontend guide (`docs/frontend.md`), or start with
|
|
55
|
+
`npm create @camelai/run-app`. `watchAgent` (`@camelai/run/watch`)
|
|
56
|
+
shows an agent live with a browser token your server mints.
|
|
57
|
+
|
|
58
|
+
Documentation: [Quickstart](https://agents.camelai.dev/docs/quickstart.md),
|
|
59
|
+
[Concepts](https://agents.camelai.dev/docs/concepts.md),
|
|
60
|
+
[SDK reference](https://agents.camelai.dev/docs/reference/sdk.md),
|
|
61
|
+
and all of it as Markdown at <https://agents.camelai.dev/llms.txt>.
|
|
62
|
+
|
|
63
|
+
`AgentRuntime` and `AgentClient`, the lower-level interface the SDK is built on,
|
|
64
|
+
remain available. See the SDK reference's "Changes in 0.9" when upgrading.
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SDK's simple interface: keyed agents you upsert and run.
|
|
3
|
+
*
|
|
4
|
+
* const agents = new Agents({ apiKey: process.env.CAMELAI_API_KEY });
|
|
5
|
+
* const agent = await agents.upsert("support-triage", { model: "anthropic/claude-sonnet-5-5", instructions: "…" });
|
|
6
|
+
* const run = await agent.run("Summarize ticket 123");
|
|
7
|
+
* console.log(run.text);
|
|
8
|
+
* await agents.close();
|
|
9
|
+
*
|
|
10
|
+
* It is built on the lower-level `AgentRuntime` and `AgentClient`, which stay available (`agents.runtime`, `agent.client`).
|
|
11
|
+
*/
|
|
12
|
+
import { AgentClient, AgentRuntime, type AgentFiles, type Builtin, type RecordedMessage, type AgentInput, type AgentOptions, type Attachment, type HistoryPage, type Mount, type RunResult, type RunUsage, type RuntimeOptions, type Sender, type SessionCredentials, type ToolError, type ToolServer, type Tools, type AgentFile } from "./typescript.ts";
|
|
13
|
+
import type { AgentEvent, ThinkingLevel } from "./types.ts";
|
|
14
|
+
declare const INSPECT: unique symbol;
|
|
15
|
+
export interface AgentsOptions {
|
|
16
|
+
/** Your API key (the console's). Default: the CAMELAI_API_KEY environment variable. */
|
|
17
|
+
apiKey?: string;
|
|
18
|
+
/** The runtime's origin. Default: CAMELAI_BASE_URL, else https://agents.camelai.dev. */
|
|
19
|
+
url?: string;
|
|
20
|
+
fetch?: typeof globalThis.fetch;
|
|
21
|
+
/** Opens a local file to attach by its path; the Node entry sets it. */
|
|
22
|
+
openFile?: RuntimeOptions["openFile"];
|
|
23
|
+
pollMs?: number;
|
|
24
|
+
}
|
|
25
|
+
/** An agent's configuration: what `upsert` makes it, or changes it to. */
|
|
26
|
+
export interface AgentConfig {
|
|
27
|
+
/** A model from the catalog (GET /v1/models) as "provider/model-id", e.g. "anthropic/claude-sonnet-5-5". */
|
|
28
|
+
model?: string;
|
|
29
|
+
/** The system prompt. */
|
|
30
|
+
instructions?: string;
|
|
31
|
+
/** Text the model reads after the instructions (a definition's, say), e.g. per-conversation context. */
|
|
32
|
+
instructionsAppend?: string;
|
|
33
|
+
/** false: no file tools (read, write, edit, ls, glob, grep), for an application with file tools of its own. */
|
|
34
|
+
fileTools?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Tools that run in this process (`tool({...})`). An agent with them is attached to this process: it
|
|
37
|
+
* answers the agent's tool calls, one process at a time. Serverless or several processes: serve
|
|
38
|
+
* tools over HTTP (`serveTools`) and name them in a definition instead.
|
|
39
|
+
*/
|
|
40
|
+
tools?: Tools;
|
|
41
|
+
/** Or an MCP server of your own, attached the same way (`fromMcpServer`). */
|
|
42
|
+
mcp?: ToolServer;
|
|
43
|
+
/** A definition (reusable configuration with tool sources: MCP servers, OpenAPI specs, built-ins) to make it from. */
|
|
44
|
+
definition?: string;
|
|
45
|
+
/** Tools the runtime answers itself (web_fetch, web_search, schedule, ask_user), without a definition. */
|
|
46
|
+
builtins?: Builtin[];
|
|
47
|
+
thinkingLevel?: ThinkingLevel;
|
|
48
|
+
/** Who the agent acts for (a user id in your app): `identity.subject` in its tool calls. Set when it is made. */
|
|
49
|
+
subject?: string;
|
|
50
|
+
/** Claims your tools need (org, workspace…): `identity.context` in its tool calls. Set when it is made. */
|
|
51
|
+
context?: Record<string, unknown>;
|
|
52
|
+
keyScope?: string;
|
|
53
|
+
/** The most it may spend on model calls from now on (USD). */
|
|
54
|
+
spendLimit?: {
|
|
55
|
+
usd: number;
|
|
56
|
+
};
|
|
57
|
+
modelHeaders?: Record<string, string>;
|
|
58
|
+
mounts?: Mount[];
|
|
59
|
+
name?: string;
|
|
60
|
+
/** Every event, for display; see `AgentOptions.onEvent`. Runs apart from the connection, in order. */
|
|
61
|
+
onEvent?: (event: AgentEvent, runId?: string) => unknown | Promise<unknown>;
|
|
62
|
+
/** Answer human input as it is asked (return an answer), or later with `run.inputs[i].answer()`. */
|
|
63
|
+
onInput?: AgentOptions["onInput"];
|
|
64
|
+
onError?: (error: Error) => void;
|
|
65
|
+
onConnection?: (connected: boolean) => void;
|
|
66
|
+
/** Replace the process that serves this agent's tools now, instead of failing with APPLICATION_CONNECTED. */
|
|
67
|
+
takeover?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Serve `tools` from this process (default: true when there are any). false declares them, as the agent's
|
|
70
|
+
* configuration, but leaves serving them to another process: to run the agent from a second process.
|
|
71
|
+
*/
|
|
72
|
+
attach?: boolean;
|
|
73
|
+
}
|
|
74
|
+
/** A failure, as a run reports it. `code` is stable; `message` is for people. */
|
|
75
|
+
export interface RunFailure {
|
|
76
|
+
/** e.g. model_error, spend_limit, runtime_error, or the runtime's own (APPLICATION_NOT_CONNECTED…). */
|
|
77
|
+
code: string;
|
|
78
|
+
message: string;
|
|
79
|
+
/** The runtime could not tell whether the run's work took effect (a restart cut it short). */
|
|
80
|
+
uncertain?: boolean;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* A run of the agent: one message and everything the agent did about it.
|
|
84
|
+
* `completed`: it answered (`text`). `input_required`: it waits on people (`inputs`); answer them to resume it.
|
|
85
|
+
* `failed`: see `error` (and, unless `throwOnError: false`, the RunError thrown).
|
|
86
|
+
*/
|
|
87
|
+
export interface Run {
|
|
88
|
+
id: string;
|
|
89
|
+
status: "completed" | "input_required" | "failed";
|
|
90
|
+
/** The final reply's text; "" when it said nothing (or failed first). */
|
|
91
|
+
text: string;
|
|
92
|
+
/** What it waits on, when `input_required`. */
|
|
93
|
+
inputs: RunInput[];
|
|
94
|
+
error: RunFailure | null;
|
|
95
|
+
/** What its model calls used, where the runtime reports it; else null. */
|
|
96
|
+
usage: RunUsage | null;
|
|
97
|
+
/** Files it wrote (download them with `agent.files`). */
|
|
98
|
+
files: AgentFile[];
|
|
99
|
+
/**
|
|
100
|
+
* Tool calls that did not complete (the model was told, and carried on): check these where a tool's side
|
|
101
|
+
* effect matters. `not_connected`: no process served the agent's tools, so the call did not run.
|
|
102
|
+
*/
|
|
103
|
+
toolErrors: ToolError[];
|
|
104
|
+
/** Tool sources (MCP servers, OpenAPI specs) that could not be reached, so the model went without their tools. */
|
|
105
|
+
sourceErrors: {
|
|
106
|
+
kind: string;
|
|
107
|
+
source: string;
|
|
108
|
+
message: string;
|
|
109
|
+
}[];
|
|
110
|
+
/** The runtime's result as sent. */
|
|
111
|
+
raw: RunResult | null;
|
|
112
|
+
}
|
|
113
|
+
/** An answer's value: approval or url: true (yes/done) or false; question: the label chosen (or labels, or your own words), or a map of question to answer; form: its fields. */
|
|
114
|
+
export type InputValue = boolean | string | string[] | Record<string, unknown>;
|
|
115
|
+
export interface AnswerOptions {
|
|
116
|
+
/** Who answers (your user id): checked against who may, when the input names its audience. */
|
|
117
|
+
from?: string | Sender;
|
|
118
|
+
/** Throw a RunError if the resumed run fails (default true). */
|
|
119
|
+
throwOnError?: boolean;
|
|
120
|
+
signal?: AbortSignal;
|
|
121
|
+
}
|
|
122
|
+
/** Human input a run waits on, with the means to answer it. Answering resolves with the resumed run. */
|
|
123
|
+
export interface RunInput extends Omit<AgentInput, "answer"> {
|
|
124
|
+
answer(value: InputValue, options?: AnswerOptions): Promise<Run>;
|
|
125
|
+
decline(options?: AnswerOptions): Promise<Run>;
|
|
126
|
+
}
|
|
127
|
+
export interface RunOptions {
|
|
128
|
+
/** Who sent it (your user id, or a Sender): the model sees who, and tools get it as `identity.user`. */
|
|
129
|
+
user?: string | Sender;
|
|
130
|
+
files?: Attachment[];
|
|
131
|
+
/** Your own key-value data about the message; the model never sees it. */
|
|
132
|
+
metadata?: Record<string, string>;
|
|
133
|
+
/** The run's id: sending the same key again returns the same run, never a second one. */
|
|
134
|
+
idempotencyKey?: string;
|
|
135
|
+
/** Stop waiting (the run goes on; `agent.abort()` stops it). There is no timeout otherwise. */
|
|
136
|
+
signal?: AbortSignal;
|
|
137
|
+
/** Resolve with a failed run instead of throwing RunError. Default true: failures throw. */
|
|
138
|
+
throwOnError?: boolean;
|
|
139
|
+
/** While a turn runs, "queue" (default) runs after it; "steer" hands the message to it. */
|
|
140
|
+
whileRunning?: "queue" | "steer";
|
|
141
|
+
/**
|
|
142
|
+
* Run even when no process serves the agent's tools (calls to them then fail as not_connected). Without
|
|
143
|
+
* it, such a run is refused with an AgentError, code APPLICATION_NOT_CONNECTED.
|
|
144
|
+
*/
|
|
145
|
+
allowDisconnected?: boolean;
|
|
146
|
+
/** This run's own budget (USD): it ends before its next model request once it has spent this. The agent's spendLimit is unchanged. */
|
|
147
|
+
spendLimit?: {
|
|
148
|
+
usd: number;
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/** What `agent.stream()` yields. `raw` is the event it came from. */
|
|
152
|
+
export type StreamPart =
|
|
153
|
+
/** Reply text as the model writes it; successive messages are separated by a blank line. */
|
|
154
|
+
{
|
|
155
|
+
type: "text";
|
|
156
|
+
text: string;
|
|
157
|
+
raw: AgentEvent;
|
|
158
|
+
} | {
|
|
159
|
+
type: "tool_call";
|
|
160
|
+
id: string;
|
|
161
|
+
name: string;
|
|
162
|
+
arguments: unknown;
|
|
163
|
+
raw: AgentEvent;
|
|
164
|
+
}
|
|
165
|
+
/** `output`: the result's text. */
|
|
166
|
+
| {
|
|
167
|
+
type: "tool_result";
|
|
168
|
+
id: string;
|
|
169
|
+
name: string;
|
|
170
|
+
output: string;
|
|
171
|
+
isError: boolean;
|
|
172
|
+
raw: AgentEvent;
|
|
173
|
+
} | {
|
|
174
|
+
type: "input_required";
|
|
175
|
+
input: RunInput;
|
|
176
|
+
raw: AgentEvent;
|
|
177
|
+
}
|
|
178
|
+
/** Always last: the run as it ended. */
|
|
179
|
+
| {
|
|
180
|
+
type: "done";
|
|
181
|
+
run: Run;
|
|
182
|
+
};
|
|
183
|
+
/** `for await (const part of agent.stream(text))`; `result()` is the run, as `done` has it. */
|
|
184
|
+
export interface RunStream extends AsyncIterable<StreamPart> {
|
|
185
|
+
readonly id: string;
|
|
186
|
+
result(): Promise<Run>;
|
|
187
|
+
}
|
|
188
|
+
export declare class Agents {
|
|
189
|
+
/** The lower-level client: definitions, volumes, mounts, inbox. */
|
|
190
|
+
readonly runtime: AgentRuntime;
|
|
191
|
+
private readonly open;
|
|
192
|
+
constructor(options?: AgentsOptions);
|
|
193
|
+
/**
|
|
194
|
+
* The agent for `key` (your name for it: "support-triage", or "user-123"), made now if there is none,
|
|
195
|
+
* and set to `config` if it differs. The same key is the same agent, with its history and files, for
|
|
196
|
+
* as long as you keep it (until `agent.delete()`); any number of processes may upsert it.
|
|
197
|
+
*/
|
|
198
|
+
upsert(key: string, config?: AgentConfig): Promise<Agent>;
|
|
199
|
+
/** An agent you hold the credentials of (`agent.session` from another process, say). */
|
|
200
|
+
agent(session: SessionCredentials, config?: Pick<AgentConfig, "tools" | "mcp" | "onEvent" | "onInput" | "onError" | "onConnection" | "takeover" | "attach">): Promise<Agent>;
|
|
201
|
+
private connect;
|
|
202
|
+
/** Close every agent's connection (their runs go on in the runtime). */
|
|
203
|
+
close(): Promise<void>;
|
|
204
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
205
|
+
}
|
|
206
|
+
export declare class Agent {
|
|
207
|
+
/** The agent's id (client_…): safe to log and to store. */
|
|
208
|
+
readonly id: string;
|
|
209
|
+
/** The lower-level client: requests, schedules, execute, and everything else. */
|
|
210
|
+
readonly client: AgentClient;
|
|
211
|
+
private readonly closed;
|
|
212
|
+
constructor(client: AgentClient, closed?: () => void);
|
|
213
|
+
/** The agent's id and token (the token is secret, and left out of logs and JSON). */
|
|
214
|
+
get session(): SessionCredentials;
|
|
215
|
+
/** The agent's files: list, download, upload and link. */
|
|
216
|
+
get files(): AgentFiles;
|
|
217
|
+
/**
|
|
218
|
+
* Send a message and wait for the run it starts: its reply, or the input it waits on. There is no
|
|
219
|
+
* timeout (runs can take minutes, or wait on people for days); `signal` stops the wait. A failed run
|
|
220
|
+
* throws a RunError (with the run) unless `throwOnError: false`.
|
|
221
|
+
*/
|
|
222
|
+
run(text: string, options?: RunOptions): Promise<Run>;
|
|
223
|
+
/**
|
|
224
|
+
* Send a message and read the run as it happens: its text as it is written, tool calls and results,
|
|
225
|
+
* the input it waits on, and last, `done` with the run. Breaking off stops the reading, not the run.
|
|
226
|
+
*/
|
|
227
|
+
stream(text: string, options?: RunOptions): RunStream;
|
|
228
|
+
/** A run's outcome, however it ended, as a Run: thrown if it failed, unless `throwOnError` is false. */
|
|
229
|
+
private settle;
|
|
230
|
+
private toRun;
|
|
231
|
+
/** An input, with the means to answer it. */
|
|
232
|
+
private input;
|
|
233
|
+
/** Inputs waiting on people, across the agent's runs. */
|
|
234
|
+
pendingInputs(): Promise<RunInput[]>;
|
|
235
|
+
/** The agent's whole history (`historyPage` reads a page at a time). */
|
|
236
|
+
/** Its whole history: every message, oldest first (`historyPage` reads a page at a time). */
|
|
237
|
+
history(): Promise<RecordedMessage[]>;
|
|
238
|
+
historyPage(options?: {
|
|
239
|
+
before?: number;
|
|
240
|
+
limit?: number;
|
|
241
|
+
}): Promise<HistoryPage>;
|
|
242
|
+
/**
|
|
243
|
+
* A message for the running turn, which reads it after its current step and answers with it in mind; with
|
|
244
|
+
* no turn running, it starts one. Resolves with the run that took it: `run(text, { whileRunning: "steer" })`.
|
|
245
|
+
*/
|
|
246
|
+
steer(text: string, options?: Omit<RunOptions, "whileRunning">): Promise<Run>;
|
|
247
|
+
/** Change its model, instructions, thinking level or tools between runs. */
|
|
248
|
+
configure(config: Pick<AgentConfig, "model" | "instructions" | "thinkingLevel" | "tools" | "mcp">): Promise<any>;
|
|
249
|
+
/** Stop the running turn. */
|
|
250
|
+
abort(): Promise<any>;
|
|
251
|
+
/** Wake the agent later with a message (`text`), or run `code`; `everySeconds` (at least 60) repeats it. */
|
|
252
|
+
schedule(input: Parameters<AgentClient["schedule"]>[0]): Promise<import("./typescript.ts").Schedule>;
|
|
253
|
+
schedules(): Promise<import("./typescript.ts").Schedule[]>;
|
|
254
|
+
unschedule(id: string): Promise<any>;
|
|
255
|
+
/** Delete the agent, its history and its files, for good. */
|
|
256
|
+
delete(): Promise<void>;
|
|
257
|
+
/** Close this process's connection to it (its runs go on in the runtime). */
|
|
258
|
+
close(): Promise<void>;
|
|
259
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
260
|
+
toJSON(): {
|
|
261
|
+
id: string;
|
|
262
|
+
};
|
|
263
|
+
[INSPECT](): string;
|
|
264
|
+
}
|
|
265
|
+
export {};
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SDK's simple interface: keyed agents you upsert and run.
|
|
3
|
+
*
|
|
4
|
+
* const agents = new Agents({ apiKey: process.env.CAMELAI_API_KEY });
|
|
5
|
+
* const agent = await agents.upsert("support-triage", { model: "anthropic/claude-sonnet-5-5", instructions: "…" });
|
|
6
|
+
* const run = await agent.run("Summarize ticket 123");
|
|
7
|
+
* console.log(run.text);
|
|
8
|
+
* await agents.close();
|
|
9
|
+
*
|
|
10
|
+
* It is built on the lower-level `AgentRuntime` and `AgentClient`, which stay available (`agents.runtime`, `agent.client`).
|
|
11
|
+
*/
|
|
12
|
+
import { AgentError, AgentRuntime, RunError, } from "./typescript.js";
|
|
13
|
+
const INSPECT = Symbol.for("nodejs.util.inspect.custom");
|
|
14
|
+
const env = (name) => globalThis.process?.env?.[name] || undefined;
|
|
15
|
+
const senderOf = (user) => typeof user === "string" ? { id: user } : user;
|
|
16
|
+
const textOf = (value) => {
|
|
17
|
+
const content = value?.content;
|
|
18
|
+
if (!Array.isArray(content))
|
|
19
|
+
return typeof value === "string" ? value : JSON.stringify(value ?? null);
|
|
20
|
+
return content.flatMap(part => part && typeof part === "object" && typeof part.text === "string" ? [part.text] : []).join("\n");
|
|
21
|
+
};
|
|
22
|
+
export class Agents {
|
|
23
|
+
/** The lower-level client: definitions, volumes, mounts, inbox. */
|
|
24
|
+
runtime;
|
|
25
|
+
open = new Set();
|
|
26
|
+
constructor(options = {}) {
|
|
27
|
+
const apiKey = options.apiKey ?? env("CAMELAI_API_KEY") ?? env("AGENT_RUNTIME_TOKEN");
|
|
28
|
+
this.runtime = new AgentRuntime({
|
|
29
|
+
...options, url: options.url ?? env("CAMELAI_BASE_URL") ?? env("AGENT_URL"), ...(apiKey ? { apiKey } : {}),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The agent for `key` (your name for it: "support-triage", or "user-123"), made now if there is none,
|
|
34
|
+
* and set to `config` if it differs. The same key is the same agent, with its history and files, for
|
|
35
|
+
* as long as you keep it (until `agent.delete()`); any number of processes may upsert it.
|
|
36
|
+
*/
|
|
37
|
+
async upsert(key, config = {}) {
|
|
38
|
+
if (!this.runtime.options.apiKey)
|
|
39
|
+
throw new AgentError("Set apiKey (or the CAMELAI_API_KEY environment variable): create a key in the console at https://agents.camelai.dev");
|
|
40
|
+
const options = createOptions(config);
|
|
41
|
+
const { session } = await this.runtime.upsertAgent(key, options);
|
|
42
|
+
// The upsert declared these tools already (between the agent's turns, if it runs).
|
|
43
|
+
return this.connect(session, config, { ...options, syncTools: false });
|
|
44
|
+
}
|
|
45
|
+
/** An agent you hold the credentials of (`agent.session` from another process, say). */
|
|
46
|
+
async agent(session, config = {}) {
|
|
47
|
+
return this.connect(session, config, createOptions(config));
|
|
48
|
+
}
|
|
49
|
+
async connect(session, config, options) {
|
|
50
|
+
const attach = config.attach ?? (!!config.mcp || Object.keys(config.tools ?? {}).length > 0);
|
|
51
|
+
const client = await this.runtime.connectAgent(session, { ...options, attach });
|
|
52
|
+
const agent = new Agent(client, () => this.open.delete(agent));
|
|
53
|
+
this.open.add(agent);
|
|
54
|
+
return agent;
|
|
55
|
+
}
|
|
56
|
+
/** Close every agent's connection (their runs go on in the runtime). */
|
|
57
|
+
async close() { await Promise.all([...this.open].map(agent => agent.close())); }
|
|
58
|
+
async [Symbol.asyncDispose]() { await this.close(); }
|
|
59
|
+
}
|
|
60
|
+
function createOptions(config) {
|
|
61
|
+
const { instructions, instructionsAppend, onEvent, ...rest } = config;
|
|
62
|
+
return {
|
|
63
|
+
...rest, ...(instructions !== undefined ? { systemPrompt: instructions } : {}), ...(instructionsAppend !== undefined ? { systemPromptAppend: instructionsAppend } : {}),
|
|
64
|
+
...(onEvent ? { onEvent: (event, requestId) => onEvent(event, requestId) } : {}),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export class Agent {
|
|
68
|
+
/** The agent's id (client_…): safe to log and to store. */
|
|
69
|
+
id;
|
|
70
|
+
/** The lower-level client: requests, schedules, execute, and everything else. */
|
|
71
|
+
client;
|
|
72
|
+
closed;
|
|
73
|
+
constructor(client, closed = () => { }) { this.client = client; this.id = client.id; this.closed = closed; }
|
|
74
|
+
/** The agent's id and token (the token is secret, and left out of logs and JSON). */
|
|
75
|
+
get session() { return this.client.session; }
|
|
76
|
+
/** The agent's files: list, download, upload and link. */
|
|
77
|
+
get files() { return this.client.files; }
|
|
78
|
+
/**
|
|
79
|
+
* Send a message and wait for the run it starts: its reply, or the input it waits on. There is no
|
|
80
|
+
* timeout (runs can take minutes, or wait on people for days); `signal` stops the wait. A failed run
|
|
81
|
+
* throws a RunError (with the run) unless `throwOnError: false`.
|
|
82
|
+
*/
|
|
83
|
+
async run(text, options = {}) {
|
|
84
|
+
const id = options.idempotencyKey ?? globalThis.crypto.randomUUID();
|
|
85
|
+
return this.settle(id, this.client.prompt(text, promptOptions(id, options)), options.throwOnError);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Send a message and read the run as it happens: its text as it is written, tool calls and results,
|
|
89
|
+
* the input it waits on, and last, `done` with the run. Breaking off stops the reading, not the run.
|
|
90
|
+
*/
|
|
91
|
+
stream(text, options = {}) {
|
|
92
|
+
const id = options.idempotencyKey ?? globalThis.crypto.randomUUID();
|
|
93
|
+
const parts = [];
|
|
94
|
+
let wake;
|
|
95
|
+
let ended = false;
|
|
96
|
+
let failure;
|
|
97
|
+
const push = (part) => { parts.push(part); wake?.(); };
|
|
98
|
+
// Text of successive assistant messages, separated so it reads as prose.
|
|
99
|
+
let spoke = false, fresh = false;
|
|
100
|
+
const unlisten = this.client.listen((event, requestId) => {
|
|
101
|
+
if (requestId !== id)
|
|
102
|
+
return;
|
|
103
|
+
switch (event.type) {
|
|
104
|
+
case "message_start":
|
|
105
|
+
if (event.message.role === "assistant")
|
|
106
|
+
fresh = true;
|
|
107
|
+
break;
|
|
108
|
+
case "message_update": {
|
|
109
|
+
const delta = event.assistantMessageEvent;
|
|
110
|
+
if (delta.type !== "text_delta" || !delta.delta)
|
|
111
|
+
break;
|
|
112
|
+
push({ type: "text", text: (fresh && spoke ? "\n\n" : "") + delta.delta, raw: event });
|
|
113
|
+
spoke = true;
|
|
114
|
+
fresh = false;
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
case "tool_execution_start":
|
|
118
|
+
push({ type: "tool_call", id: event.toolCallId, name: event.toolName, arguments: event.args, raw: event });
|
|
119
|
+
break;
|
|
120
|
+
case "tool_execution_end":
|
|
121
|
+
push({ type: "tool_result", id: event.toolCallId, name: event.toolName, output: textOf(event.result), isError: event.isError, raw: event });
|
|
122
|
+
break;
|
|
123
|
+
case "input_required":
|
|
124
|
+
push({ type: "input_required", input: this.input(event.input), raw: event });
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
const result = this.settle(id, this.client.prompt(text, promptOptions(id, options)), false).then(run => {
|
|
129
|
+
push({ type: "done", run });
|
|
130
|
+
if (run.error && options.throwOnError !== false)
|
|
131
|
+
failure = new RunError(run);
|
|
132
|
+
return run;
|
|
133
|
+
}, error => { failure = error; throw error; }).finally(() => { ended = true; unlisten(); wake?.(); });
|
|
134
|
+
result.catch(() => { });
|
|
135
|
+
return {
|
|
136
|
+
id,
|
|
137
|
+
result: async () => {
|
|
138
|
+
const run = await result;
|
|
139
|
+
if (run.error && options.throwOnError !== false)
|
|
140
|
+
throw new RunError(run);
|
|
141
|
+
return run;
|
|
142
|
+
},
|
|
143
|
+
async *[Symbol.asyncIterator]() {
|
|
144
|
+
try {
|
|
145
|
+
for (;;) {
|
|
146
|
+
if (parts.length) {
|
|
147
|
+
yield parts.shift();
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (ended)
|
|
151
|
+
break;
|
|
152
|
+
await new Promise(resolve => { wake = resolve; });
|
|
153
|
+
wake = undefined;
|
|
154
|
+
}
|
|
155
|
+
if (failure)
|
|
156
|
+
throw failure;
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
if (!ended)
|
|
160
|
+
unlisten();
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/** A run's outcome, however it ended, as a Run: thrown if it failed, unless `throwOnError` is false. */
|
|
166
|
+
async settle(id, pending, throwOnError = true) {
|
|
167
|
+
let run;
|
|
168
|
+
try {
|
|
169
|
+
run = this.toRun(id, await pending);
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
// A run that ended in an error settles with it; anything else (a refused request, a closed client) is not a run.
|
|
173
|
+
if (!(error instanceof AgentError) || error.status !== 0 || error.requestId !== id || /^Client closed/.test(error.message))
|
|
174
|
+
throw error;
|
|
175
|
+
run = { id, status: "failed", text: "", inputs: [], usage: null, files: [], toolErrors: [], sourceErrors: [], raw: null, error: { code: error.code ?? "runtime_error", message: error.message, ...(error.uncertain ? { uncertain: true } : {}) } };
|
|
176
|
+
}
|
|
177
|
+
if (run.error && throwOnError)
|
|
178
|
+
throw new RunError(run);
|
|
179
|
+
return run;
|
|
180
|
+
}
|
|
181
|
+
toRun(id, result) {
|
|
182
|
+
const raw = result ?? null;
|
|
183
|
+
const error = raw?.error ? { code: raw.code ?? "model_error", message: raw.error }
|
|
184
|
+
: raw?.stopped === "spend_limit" ? { code: "spend_limit", message: "The agent reached its spend limit; raise it (configure spendLimit) to go on" } : null;
|
|
185
|
+
return {
|
|
186
|
+
id, status: error ? "failed" : raw?.stopped === "input_required" ? "input_required" : "completed",
|
|
187
|
+
text: raw?.reply ?? "", inputs: (raw?.inputs ?? []).map(input => this.input(input)), error,
|
|
188
|
+
usage: raw?.usage ?? null, files: raw?.files ?? [], toolErrors: raw?.toolErrors ?? [], sourceErrors: raw?.sourceErrors ?? [], raw,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/** An input, with the means to answer it. */
|
|
192
|
+
input(input) {
|
|
193
|
+
const respond = async (answer, options) => {
|
|
194
|
+
if (input.responders.audience?.length && !answer.from)
|
|
195
|
+
throw new AgentError(`Say who answers (from): only ${input.responders.audience.join(", ")} may answer this`);
|
|
196
|
+
const { request } = await this.client.answer(input.id, answer);
|
|
197
|
+
if (request)
|
|
198
|
+
return this.settle(request.id, this.client.waitForRequest(request.id, { ...(options.signal ? { signal: options.signal } : {}) }), options.throwOnError);
|
|
199
|
+
// Other inputs of the run still wait: it resumes once they are answered too.
|
|
200
|
+
const pending = (await this.client.inputs("pending")).filter(other => other.requestId === input.requestId);
|
|
201
|
+
return { id: input.requestId, status: "input_required", text: "", inputs: pending.map(other => this.input(other)), error: null, usage: null, files: [], toolErrors: [], sourceErrors: [], raw: null };
|
|
202
|
+
};
|
|
203
|
+
return {
|
|
204
|
+
...input,
|
|
205
|
+
answer: (value, options = {}) => respond({ ...answerFor(input, value), ...(options.from ? { from: senderOf(options.from) } : {}) }, options),
|
|
206
|
+
decline: (options = {}) => respond({ action: "decline", ...(options.from ? { from: senderOf(options.from) } : {}) }, options),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/** Inputs waiting on people, across the agent's runs. */
|
|
210
|
+
async pendingInputs() { return (await this.client.inputs("pending")).map(input => this.input(input)); }
|
|
211
|
+
/** The agent's whole history (`historyPage` reads a page at a time). */
|
|
212
|
+
/** Its whole history: every message, oldest first (`historyPage` reads a page at a time). */
|
|
213
|
+
async history() { return (await this.client.history()).messages; }
|
|
214
|
+
historyPage(options = {}) { return this.client.historyPage(options); }
|
|
215
|
+
/**
|
|
216
|
+
* A message for the running turn, which reads it after its current step and answers with it in mind; with
|
|
217
|
+
* no turn running, it starts one. Resolves with the run that took it: `run(text, { whileRunning: "steer" })`.
|
|
218
|
+
*/
|
|
219
|
+
steer(text, options = {}) { return this.run(text, { ...options, whileRunning: "steer" }); }
|
|
220
|
+
/** Change its model, instructions, thinking level or tools between runs. */
|
|
221
|
+
configure(config) {
|
|
222
|
+
const { instructions, ...rest } = config;
|
|
223
|
+
return this.client.configure({ ...rest, ...(instructions !== undefined ? { systemPrompt: instructions } : {}) });
|
|
224
|
+
}
|
|
225
|
+
/** Stop the running turn. */
|
|
226
|
+
abort() { return this.client.abort(); }
|
|
227
|
+
/** Wake the agent later with a message (`text`), or run `code`; `everySeconds` (at least 60) repeats it. */
|
|
228
|
+
schedule(input) { return this.client.schedule(input); }
|
|
229
|
+
schedules() { return this.client.schedules(); }
|
|
230
|
+
unschedule(id) { return this.client.unschedule(id); }
|
|
231
|
+
/** Delete the agent, its history and its files, for good. */
|
|
232
|
+
async delete() { try {
|
|
233
|
+
await this.client.destroy();
|
|
234
|
+
}
|
|
235
|
+
finally {
|
|
236
|
+
this.closed();
|
|
237
|
+
} }
|
|
238
|
+
/** Close this process's connection to it (its runs go on in the runtime). */
|
|
239
|
+
async close() { try {
|
|
240
|
+
await this.client.close();
|
|
241
|
+
}
|
|
242
|
+
finally {
|
|
243
|
+
this.closed();
|
|
244
|
+
} }
|
|
245
|
+
async [Symbol.asyncDispose]() { await this.close(); }
|
|
246
|
+
toJSON() { return { id: this.id }; }
|
|
247
|
+
[INSPECT]() { return `Agent { id: '${this.id}' }`; }
|
|
248
|
+
}
|
|
249
|
+
function messageOptions(options) {
|
|
250
|
+
return { ...(options.user ? { from: senderOf(options.user) } : {}), ...(options.files ? { files: options.files } : {}), ...(options.metadata ? { metadata: options.metadata } : {}) };
|
|
251
|
+
}
|
|
252
|
+
function promptOptions(id, options) {
|
|
253
|
+
return {
|
|
254
|
+
...messageOptions(options), idempotencyKey: id,
|
|
255
|
+
...(options.signal ? { signal: options.signal } : {}), ...(options.whileRunning ? { whileRunning: options.whileRunning } : {}),
|
|
256
|
+
...(options.allowDisconnected ? { allowDisconnected: true } : {}), ...(options.spendLimit ? { spendLimit: options.spendLimit } : {}),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
/** An answer for `input` from a plain value (see InputValue). */
|
|
260
|
+
function answerFor(input, value) {
|
|
261
|
+
switch (input.kind) {
|
|
262
|
+
case "approval":
|
|
263
|
+
case "url":
|
|
264
|
+
if (typeof value !== "boolean")
|
|
265
|
+
throw new AgentError(`Answer ${input.kind === "approval" ? "an approval" : "a url step"} with true or false`);
|
|
266
|
+
return { action: value ? "accept" : "decline" };
|
|
267
|
+
case "question": {
|
|
268
|
+
const questions = (input.detail.questions ?? []);
|
|
269
|
+
if (typeof value === "string" || Array.isArray(value)) {
|
|
270
|
+
if (questions.length !== 1)
|
|
271
|
+
throw new AgentError(`This input asks ${questions.length} questions: answer with { "<question>": "<answer>" } for each`);
|
|
272
|
+
return { action: "accept", content: { answers: { [questions[0].question]: value } } };
|
|
273
|
+
}
|
|
274
|
+
if (typeof value !== "object" || value === null)
|
|
275
|
+
throw new AgentError("Answer a question with the label chosen, or a map of question to answer");
|
|
276
|
+
return { action: "accept", content: { answers: value } };
|
|
277
|
+
}
|
|
278
|
+
case "form":
|
|
279
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
280
|
+
throw new AgentError("Answer a form with its fields, as an object");
|
|
281
|
+
return { action: "accept", content: value };
|
|
282
|
+
}
|
|
283
|
+
}
|