framework-agents 0.0.0-stage → 0.1.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.
- package/README.md +66 -2
- package/dist/agent-25ZNQBEB.js +6 -0
- package/dist/agent-ZWGIDKF6.js +7 -0
- package/dist/agent-ZWGIDKF6.js.map +1 -0
- package/dist/chunk-77OZE2QD.js +1811 -0
- package/dist/chunk-77OZE2QD.js.map +1 -0
- package/dist/chunk-LUGUQQLR.js +1033 -0
- package/dist/cli.js +1209 -0
- package/dist/index.d.ts +2842 -0
- package/dist/index.js +3951 -0
- package/dist/index.js.map +1 -0
- package/package.json +71 -4
|
@@ -0,0 +1,1033 @@
|
|
|
1
|
+
// src/types/index.ts
|
|
2
|
+
import { z as z2 } from "zod";
|
|
3
|
+
|
|
4
|
+
// src/types/content.ts
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
var TextBlockSchema = z.object({ type: z.literal("text"), text: z.string() });
|
|
7
|
+
var ReasoningBlockSchema = z.object({ type: z.literal("reasoning"), reasoning: z.string() });
|
|
8
|
+
var ImageBlockSchema = z.object({
|
|
9
|
+
type: z.literal("image"),
|
|
10
|
+
url: z.string().optional(),
|
|
11
|
+
data: z.string().optional(),
|
|
12
|
+
mimeType: z.string().optional(),
|
|
13
|
+
fileId: z.string().optional()
|
|
14
|
+
});
|
|
15
|
+
var AudioBlockSchema = z.object({
|
|
16
|
+
type: z.literal("audio"),
|
|
17
|
+
url: z.string().optional(),
|
|
18
|
+
data: z.string().optional(),
|
|
19
|
+
mimeType: z.string().optional(),
|
|
20
|
+
fileId: z.string().optional()
|
|
21
|
+
});
|
|
22
|
+
var FileBlockSchema = z.object({
|
|
23
|
+
type: z.literal("file"),
|
|
24
|
+
url: z.string().optional(),
|
|
25
|
+
data: z.string().optional(),
|
|
26
|
+
mimeType: z.string().optional(),
|
|
27
|
+
fileId: z.string().optional()
|
|
28
|
+
});
|
|
29
|
+
var ToolCallBlockSchema = z.object({
|
|
30
|
+
type: z.literal("tool_call"),
|
|
31
|
+
id: z.string(),
|
|
32
|
+
name: z.string(),
|
|
33
|
+
args: z.unknown()
|
|
34
|
+
});
|
|
35
|
+
var NonStandardBlockSchema = z.object({ type: z.literal("non_standard"), value: z.unknown() });
|
|
36
|
+
var ContentBlockSchema = z.discriminatedUnion("type", [
|
|
37
|
+
TextBlockSchema,
|
|
38
|
+
ReasoningBlockSchema,
|
|
39
|
+
ImageBlockSchema,
|
|
40
|
+
AudioBlockSchema,
|
|
41
|
+
FileBlockSchema,
|
|
42
|
+
ToolCallBlockSchema,
|
|
43
|
+
NonStandardBlockSchema
|
|
44
|
+
]);
|
|
45
|
+
function wireTextOf(content) {
|
|
46
|
+
if (typeof content === "string") return content;
|
|
47
|
+
return content.filter((b) => b.type !== "reasoning").map((b) => b.type === "text" ? b.text : "").join("");
|
|
48
|
+
}
|
|
49
|
+
function hasImages(content) {
|
|
50
|
+
return typeof content !== "string" && content.some((b) => b.type === "image");
|
|
51
|
+
}
|
|
52
|
+
function imagesOf(content) {
|
|
53
|
+
if (typeof content === "string") return [];
|
|
54
|
+
return content.filter((b) => b.type === "image").map((b) => ({ url: b.url, data: b.data, mimeType: b.mimeType }));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/types/index.ts
|
|
58
|
+
var ChatMessageSchema = z2.object({
|
|
59
|
+
id: z2.string().optional(),
|
|
60
|
+
role: z2.enum(["system", "user", "assistant", "tool"]),
|
|
61
|
+
content: z2.union([z2.string(), z2.array(ContentBlockSchema)]),
|
|
62
|
+
/** Provider user-identification; behavior varies, optional. */
|
|
63
|
+
name: z2.string().optional(),
|
|
64
|
+
toolCallId: z2.string().optional(),
|
|
65
|
+
toolName: z2.string().optional(),
|
|
66
|
+
/** Pinned messages survive conversation-manager trimming. */
|
|
67
|
+
pinned: z2.boolean().optional()
|
|
68
|
+
});
|
|
69
|
+
var AgentError = class extends Error {
|
|
70
|
+
code;
|
|
71
|
+
retryable;
|
|
72
|
+
constructor(code, message, retryable = false) {
|
|
73
|
+
super(message);
|
|
74
|
+
this.name = "AgentError";
|
|
75
|
+
this.code = code;
|
|
76
|
+
this.retryable = retryable;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// src/middleware/index.ts
|
|
81
|
+
import { z as z3 } from "zod";
|
|
82
|
+
|
|
83
|
+
// src/tools/index.ts
|
|
84
|
+
function defaultToResult(output) {
|
|
85
|
+
return { content: typeof output === "string" ? output : JSON.stringify(output) };
|
|
86
|
+
}
|
|
87
|
+
function defineTool(tool) {
|
|
88
|
+
if (!tool.name) throw new Error("Tool must have a name");
|
|
89
|
+
return tool;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// src/middleware/index.ts
|
|
93
|
+
import { mkdir, readdir, readFile, writeFile } from "fs/promises";
|
|
94
|
+
import { dirname, resolve } from "path";
|
|
95
|
+
var ToolRejection = class extends Error {
|
|
96
|
+
feedback;
|
|
97
|
+
constructor(feedback) {
|
|
98
|
+
super(feedback);
|
|
99
|
+
this.name = "ToolRejection";
|
|
100
|
+
this.feedback = feedback;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
var HitlInterrupt = class extends Error {
|
|
104
|
+
interruptId;
|
|
105
|
+
tool;
|
|
106
|
+
args;
|
|
107
|
+
constructor(interruptId, tool, args) {
|
|
108
|
+
super(`Approval needed for tool "${tool}" (interrupt ${interruptId})`);
|
|
109
|
+
this.name = "HitlInterrupt";
|
|
110
|
+
this.interruptId = interruptId;
|
|
111
|
+
this.tool = tool;
|
|
112
|
+
this.args = args;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
function todoListMiddleware() {
|
|
116
|
+
let todos = [];
|
|
117
|
+
return {
|
|
118
|
+
name: "todo-list",
|
|
119
|
+
tools: [
|
|
120
|
+
defineTool({
|
|
121
|
+
name: "write_todos",
|
|
122
|
+
description: "Track multi-step progress. Set exactly one item active at a time; mark completed as you finish.",
|
|
123
|
+
schema: z3.object({ todos: z3.array(z3.object({ content: z3.string(), status: z3.enum(["pending", "active", "completed"]) })) }),
|
|
124
|
+
execute: async ({ todos: next }) => {
|
|
125
|
+
todos = next;
|
|
126
|
+
const open = todos.filter((t) => t.status !== "completed").length;
|
|
127
|
+
return `Todos updated (${open} open):
|
|
128
|
+
${todos.map((t) => `- [${t.status === "completed" ? "x" : " "}] ${t.content}`).join("\n")}`;
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
],
|
|
132
|
+
beforeModel: (req, ctx) => {
|
|
133
|
+
if (ctx.step === 0) todos = [];
|
|
134
|
+
const hinted = req.messages[0]?.content;
|
|
135
|
+
const hint = "For multi-step tasks, plan with write_todos first; keep exactly one item active.";
|
|
136
|
+
if (typeof hinted === "string" && !hinted.includes("write_todos")) {
|
|
137
|
+
return { ...req, messages: [{ ...req.messages[0], content: `${hinted}
|
|
138
|
+
${hint}` }, ...req.messages.slice(1)] };
|
|
139
|
+
}
|
|
140
|
+
return void 0;
|
|
141
|
+
},
|
|
142
|
+
listTodos: () => [...todos]
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function filesystemTools(root) {
|
|
146
|
+
const base = resolve(root);
|
|
147
|
+
const resolveIn = (p) => {
|
|
148
|
+
const full = resolve(base, p);
|
|
149
|
+
if (full !== base && !full.startsWith(`${base}/`)) throw new AgentError("FS_ESCAPE", `Path escapes root: ${p}`, false);
|
|
150
|
+
return full;
|
|
151
|
+
};
|
|
152
|
+
return [
|
|
153
|
+
defineTool({
|
|
154
|
+
name: "ls",
|
|
155
|
+
description: "List files in a directory (relative to workspace root).",
|
|
156
|
+
schema: z3.object({ path: z3.string().optional() }),
|
|
157
|
+
execute: async ({ path }) => (await readdir(resolveIn(path ?? "."))).join("\n")
|
|
158
|
+
}),
|
|
159
|
+
defineTool({
|
|
160
|
+
name: "read_file",
|
|
161
|
+
description: "Read a file's full text.",
|
|
162
|
+
schema: z3.object({ path: z3.string() }),
|
|
163
|
+
execute: async ({ path }) => readFile(resolveIn(path), "utf8")
|
|
164
|
+
}),
|
|
165
|
+
defineTool({
|
|
166
|
+
name: "write_file",
|
|
167
|
+
description: "Write (create or overwrite) a file.",
|
|
168
|
+
schema: z3.object({ path: z3.string(), content: z3.string() }),
|
|
169
|
+
execute: async ({ path, content }) => {
|
|
170
|
+
const full = resolveIn(path);
|
|
171
|
+
await mkdir(dirname(full), { recursive: true });
|
|
172
|
+
await writeFile(full, content, "utf8");
|
|
173
|
+
return `wrote ${path} (${content.length} chars)`;
|
|
174
|
+
}
|
|
175
|
+
}),
|
|
176
|
+
defineTool({
|
|
177
|
+
name: "edit_file",
|
|
178
|
+
description: "Replace the first occurrence of oldString with newString in a file.",
|
|
179
|
+
schema: z3.object({ path: z3.string(), oldString: z3.string(), newString: z3.string() }),
|
|
180
|
+
execute: async ({ path, oldString, newString: ns }) => {
|
|
181
|
+
const full = resolveIn(path);
|
|
182
|
+
const content = await readFile(full, "utf8");
|
|
183
|
+
if (!content.includes(oldString)) throw new AgentError("FS_NO_MATCH", `oldString not found in ${path}`, false);
|
|
184
|
+
await writeFile(full, content.replace(oldString, ns), "utf8");
|
|
185
|
+
return `edited ${path}`;
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
];
|
|
189
|
+
}
|
|
190
|
+
function filesystemMiddleware(opts) {
|
|
191
|
+
return { name: "filesystem", tools: filesystemTools(opts.root) };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// src/conversation/index.ts
|
|
195
|
+
var NullConversationManager = class {
|
|
196
|
+
name = "null";
|
|
197
|
+
removedCount = 0;
|
|
198
|
+
apply(messages) {
|
|
199
|
+
return messages;
|
|
200
|
+
}
|
|
201
|
+
reduce(messages) {
|
|
202
|
+
return messages;
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// src/state/index.ts
|
|
207
|
+
function assertJsonSerializable(key, value) {
|
|
208
|
+
try {
|
|
209
|
+
JSON.stringify(value);
|
|
210
|
+
} catch {
|
|
211
|
+
throw new Error(`AgentState: value for "${key}" is not JSON-serializable`);
|
|
212
|
+
}
|
|
213
|
+
if (typeof value === "function" || typeof value === "symbol" || typeof value === "bigint") {
|
|
214
|
+
throw new Error(`AgentState: value for "${key}" is not JSON-serializable`);
|
|
215
|
+
}
|
|
216
|
+
if (value instanceof Map || value instanceof Set || value instanceof WeakMap || value instanceof WeakSet) {
|
|
217
|
+
throw new Error(`AgentState: value for "${key}" is a ${value.constructor.name}; use a plain object/array`);
|
|
218
|
+
}
|
|
219
|
+
if (typeof value === "object" && value !== null && !(Array.isArray(value) || Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null)) {
|
|
220
|
+
throw new Error(`AgentState: value for "${key}" has a custom prototype; plain objects round-trip, class instances don't`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
var AgentState = class _AgentState {
|
|
224
|
+
data;
|
|
225
|
+
constructor(initial = {}) {
|
|
226
|
+
for (const [k, v] of Object.entries(initial)) assertJsonSerializable(k, v);
|
|
227
|
+
this.data = { ...initial };
|
|
228
|
+
}
|
|
229
|
+
get(key) {
|
|
230
|
+
if (key === void 0) return { ...this.data };
|
|
231
|
+
return this.data[key];
|
|
232
|
+
}
|
|
233
|
+
set(key, value) {
|
|
234
|
+
assertJsonSerializable(key, value);
|
|
235
|
+
this.data[key] = value;
|
|
236
|
+
}
|
|
237
|
+
delete(key) {
|
|
238
|
+
delete this.data[key];
|
|
239
|
+
}
|
|
240
|
+
/** Plain-object copy for snapshots/persistence. */
|
|
241
|
+
toJSON() {
|
|
242
|
+
return JSON.parse(JSON.stringify(this.data));
|
|
243
|
+
}
|
|
244
|
+
static fromJSON(raw) {
|
|
245
|
+
return new _AgentState(raw);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// src/sessions/index.ts
|
|
250
|
+
import { mkdir as mkdir2, readdir as readdir2, readFile as readFile2, rm, writeFile as writeFile2 } from "fs/promises";
|
|
251
|
+
import { join } from "path";
|
|
252
|
+
var LocalFileStorage = class {
|
|
253
|
+
constructor(root) {
|
|
254
|
+
this.root = root;
|
|
255
|
+
}
|
|
256
|
+
root;
|
|
257
|
+
path(key) {
|
|
258
|
+
const segments = key.split("/").filter((seg) => seg !== "");
|
|
259
|
+
if (segments.includes("..") || key.includes("\0")) {
|
|
260
|
+
throw new AgentError("STORAGE_KEY_INVALID", `Invalid storage key: ${JSON.stringify(key)}`, false);
|
|
261
|
+
}
|
|
262
|
+
return join(this.root, ...segments);
|
|
263
|
+
}
|
|
264
|
+
async read(key) {
|
|
265
|
+
try {
|
|
266
|
+
return await readFile2(this.path(key), "utf8");
|
|
267
|
+
} catch {
|
|
268
|
+
return void 0;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
async write(key, value) {
|
|
272
|
+
const full = this.path(key);
|
|
273
|
+
await mkdir2(join(full, ".."), { recursive: true });
|
|
274
|
+
await writeFile2(full, value, "utf8");
|
|
275
|
+
}
|
|
276
|
+
async deletePrefix(prefix) {
|
|
277
|
+
await rm(this.path(prefix), { recursive: true, force: true });
|
|
278
|
+
}
|
|
279
|
+
async list(prefix) {
|
|
280
|
+
const out = [];
|
|
281
|
+
const walk = async (dir, rel) => {
|
|
282
|
+
let entries;
|
|
283
|
+
try {
|
|
284
|
+
entries = await readdir2(dir, { withFileTypes: true });
|
|
285
|
+
} catch {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
for (const e of entries) {
|
|
289
|
+
if (e.isDirectory()) await walk(join(dir, e.name), `${rel}${e.name}/`);
|
|
290
|
+
else out.push(`${prefix}${rel}${e.name}`);
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
await walk(this.path(prefix), "");
|
|
294
|
+
return out.sort();
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
var SESSION_PRESET = ["messages", "state"];
|
|
298
|
+
function selectSnapshotFields(full, opts = {}) {
|
|
299
|
+
const base = new Set(opts.preset === "session" ? SESSION_PRESET : []);
|
|
300
|
+
for (const f of opts.include ?? []) base.add(f);
|
|
301
|
+
for (const f of opts.exclude ?? []) base.delete(f);
|
|
302
|
+
const data = {};
|
|
303
|
+
if (base.has("messages")) data.messages = full.messages;
|
|
304
|
+
if (base.has("state")) data.state = full.state;
|
|
305
|
+
if (base.has("systemPrompt")) data.systemPrompt = full.systemPrompt;
|
|
306
|
+
return data;
|
|
307
|
+
}
|
|
308
|
+
function uuid() {
|
|
309
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
310
|
+
const r = Math.random() * 16 | 0;
|
|
311
|
+
return (c === "x" ? r : r & 3 | 8).toString(16);
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
var SessionManager = class {
|
|
315
|
+
sessionId;
|
|
316
|
+
agentId;
|
|
317
|
+
storage;
|
|
318
|
+
constructor(opts) {
|
|
319
|
+
this.sessionId = opts.sessionId ?? uuid();
|
|
320
|
+
this.agentId = opts.agentId ?? "default";
|
|
321
|
+
this.storage = opts.storage;
|
|
322
|
+
}
|
|
323
|
+
scope() {
|
|
324
|
+
return `session/${this.sessionId}/scopes/agent/${this.agentId}/snapshots`;
|
|
325
|
+
}
|
|
326
|
+
async saveLatest(data) {
|
|
327
|
+
await this.storage.write(`${this.scope()}/snapshot_latest.json`, JSON.stringify({ data }));
|
|
328
|
+
}
|
|
329
|
+
async loadLatest() {
|
|
330
|
+
const raw = await this.storage.read(`${this.scope()}/snapshot_latest.json`);
|
|
331
|
+
if (!raw) return void 0;
|
|
332
|
+
try {
|
|
333
|
+
return JSON.parse(raw).data;
|
|
334
|
+
} catch {
|
|
335
|
+
return void 0;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/** Immutable checkpoint; returns its id. */
|
|
339
|
+
async saveSnapshot(data, appData) {
|
|
340
|
+
const snapshot = { id: uuid(), createdAt: (/* @__PURE__ */ new Date()).toISOString(), data, appData };
|
|
341
|
+
await this.storage.write(`${this.scope()}/immutable_history/snapshot_${snapshot.id}.json`, JSON.stringify(snapshot));
|
|
342
|
+
return snapshot.id;
|
|
343
|
+
}
|
|
344
|
+
async listSnapshotIds() {
|
|
345
|
+
const keys = await this.storage.list(`${this.scope()}/immutable_history/`);
|
|
346
|
+
return keys.map((k) => k.match(/snapshot_(.+)\.json$/)?.[1]).filter((id) => !!id).sort();
|
|
347
|
+
}
|
|
348
|
+
async restoreSnapshot(id) {
|
|
349
|
+
const raw = await this.storage.read(`${this.scope()}/immutable_history/snapshot_${id}.json`);
|
|
350
|
+
if (!raw) return void 0;
|
|
351
|
+
try {
|
|
352
|
+
return JSON.parse(raw);
|
|
353
|
+
} catch {
|
|
354
|
+
return void 0;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
async deleteSession() {
|
|
358
|
+
await this.storage.deletePrefix(`session/${this.sessionId}/`);
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
// src/structured/index.ts
|
|
363
|
+
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
364
|
+
function responseFormatName(format) {
|
|
365
|
+
return format.name ?? "structured_response";
|
|
366
|
+
}
|
|
367
|
+
function responseFormatJsonSchema(format) {
|
|
368
|
+
return zodToJsonSchema(format.schema, { $refStrategy: "none" });
|
|
369
|
+
}
|
|
370
|
+
function parseStructured(format, raw) {
|
|
371
|
+
const parsed = format.schema.safeParse(raw);
|
|
372
|
+
if (parsed.success) return parsed.data;
|
|
373
|
+
throw new AgentError(
|
|
374
|
+
"STRUCTURED_INVALID",
|
|
375
|
+
`Structured output failed validation for "${responseFormatName(format)}": ${parsed.error.message}`,
|
|
376
|
+
true
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// src/agent/index.ts
|
|
381
|
+
import { zodToJsonSchema as zodToJsonSchema2 } from "zod-to-json-schema";
|
|
382
|
+
|
|
383
|
+
// src/memory/index.ts
|
|
384
|
+
import { mkdir as mkdir3, readFile as readFile3, rename, writeFile as writeFile3 } from "fs/promises";
|
|
385
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
386
|
+
var InMemoryMemory = class {
|
|
387
|
+
messages = [];
|
|
388
|
+
maxMessages;
|
|
389
|
+
constructor(maxMessages = 50) {
|
|
390
|
+
this.maxMessages = maxMessages;
|
|
391
|
+
}
|
|
392
|
+
async load() {
|
|
393
|
+
return [...this.messages];
|
|
394
|
+
}
|
|
395
|
+
async save(messages) {
|
|
396
|
+
this.messages = [...this.messages, ...messages].slice(-this.maxMessages);
|
|
397
|
+
}
|
|
398
|
+
async clear() {
|
|
399
|
+
this.messages = [];
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
// src/models/pricing.ts
|
|
404
|
+
var PRICES = {
|
|
405
|
+
"Qwen/Qwen3.5-9B": { inputPerMillion: 0.2, outputPerMillion: 0.8 },
|
|
406
|
+
"meta-llama/Llama-3.3-70B-Instruct-Turbo": { inputPerMillion: 0.88, outputPerMillion: 0.88 },
|
|
407
|
+
"deepseek-ai/DeepSeek-V4.1-Flash": { inputPerMillion: 0.2, outputPerMillion: 0.6 },
|
|
408
|
+
"zai-org/GLM-5.3-Flash": { inputPerMillion: 0.2, outputPerMillion: 0.6 },
|
|
409
|
+
"gpt-4o-mini": { inputPerMillion: 0.15, outputPerMillion: 0.6 },
|
|
410
|
+
"claude-sonnet-4-6": { inputPerMillion: 3, outputPerMillion: 15 },
|
|
411
|
+
"qwen2.5": { inputPerMillion: 0, outputPerMillion: 0 }
|
|
412
|
+
};
|
|
413
|
+
function priceFor(modelId) {
|
|
414
|
+
if (/^ollama:/.test(modelId)) return { inputPerMillion: 0, outputPerMillion: 0 };
|
|
415
|
+
const bare = modelId.includes(":") ? modelId.split(":").slice(1).join(":") : modelId;
|
|
416
|
+
if (bare in PRICES) return PRICES[bare];
|
|
417
|
+
return void 0;
|
|
418
|
+
}
|
|
419
|
+
function usageCost(usage, modelId, override) {
|
|
420
|
+
const price = override ?? priceFor(modelId);
|
|
421
|
+
if (!price) return void 0;
|
|
422
|
+
return (usage.inputTokens * price.inputPerMillion + usage.outputTokens * price.outputPerMillion) / 1e6;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// src/runtime/index.ts
|
|
426
|
+
function toAbortError() {
|
|
427
|
+
return new AgentError("ABORTED", "Run aborted", false);
|
|
428
|
+
}
|
|
429
|
+
async function runWithBudget(fn, budget = {}) {
|
|
430
|
+
const { signal, timeoutMs } = budget;
|
|
431
|
+
try {
|
|
432
|
+
signal?.throwIfAborted();
|
|
433
|
+
} catch {
|
|
434
|
+
throw toAbortError();
|
|
435
|
+
}
|
|
436
|
+
if (!timeoutMs && !signal) return fn();
|
|
437
|
+
let timer;
|
|
438
|
+
try {
|
|
439
|
+
return await Promise.race([
|
|
440
|
+
fn(),
|
|
441
|
+
new Promise((_, reject) => {
|
|
442
|
+
if (timeoutMs) {
|
|
443
|
+
timer = setTimeout(() => reject(new AgentError("TIMEOUT", `Exceeded ${timeoutMs}ms`, true)), timeoutMs);
|
|
444
|
+
}
|
|
445
|
+
signal?.addEventListener(
|
|
446
|
+
"abort",
|
|
447
|
+
() => {
|
|
448
|
+
if (timer) clearTimeout(timer);
|
|
449
|
+
reject(toAbortError());
|
|
450
|
+
},
|
|
451
|
+
{ once: true }
|
|
452
|
+
);
|
|
453
|
+
})
|
|
454
|
+
]);
|
|
455
|
+
} catch (err) {
|
|
456
|
+
if (err instanceof DOMException && err.name === "AbortError") throw toAbortError();
|
|
457
|
+
throw err;
|
|
458
|
+
} finally {
|
|
459
|
+
if (timer) clearTimeout(timer);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// src/agent/index.ts
|
|
464
|
+
var DEFAULT_SYSTEM = "You are a helpful agent. Use tools when needed. Be concise.";
|
|
465
|
+
var DEFAULT_MAX_STEPS = 6;
|
|
466
|
+
var toolJsonSchemas = /* @__PURE__ */ new WeakMap();
|
|
467
|
+
function toolParameters(t) {
|
|
468
|
+
if (!toolJsonSchemas.has(t)) {
|
|
469
|
+
try {
|
|
470
|
+
toolJsonSchemas.set(t, zodToJsonSchema2(t.schema, { $refStrategy: "none" }));
|
|
471
|
+
} catch {
|
|
472
|
+
toolJsonSchemas.set(t, void 0);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return toolJsonSchemas.get(t);
|
|
476
|
+
}
|
|
477
|
+
var Agent = class _Agent {
|
|
478
|
+
options;
|
|
479
|
+
seeded = false;
|
|
480
|
+
restored = false;
|
|
481
|
+
lastMessages = [];
|
|
482
|
+
revisions = [];
|
|
483
|
+
agentState;
|
|
484
|
+
instanceSessionId;
|
|
485
|
+
constructor(options) {
|
|
486
|
+
this.options = {
|
|
487
|
+
name: options.name ?? "agent",
|
|
488
|
+
description: options.description ?? "",
|
|
489
|
+
systemPrompt: options.systemPrompt ?? DEFAULT_SYSTEM,
|
|
490
|
+
model: options.model,
|
|
491
|
+
// Middleware-contributed tools (filesystem, todos, subagents) come first.
|
|
492
|
+
// withTools()/branch() pass an already-merged list back in, so dedupe
|
|
493
|
+
// by name (first occurrence wins) to keep clones from advertising
|
|
494
|
+
// every middleware tool twice.
|
|
495
|
+
tools: [...(options.middleware ?? []).flatMap((m) => m.tools ?? []), ...options.tools ?? []].filter((t, i, all) => all.findIndex((o) => o.name === t.name) === i),
|
|
496
|
+
memory: options.memory,
|
|
497
|
+
maxSteps: options.maxSteps ?? DEFAULT_MAX_STEPS,
|
|
498
|
+
maxToolCalls: options.maxToolCalls,
|
|
499
|
+
budgets: options.budgets,
|
|
500
|
+
outputVersion: options.outputVersion ?? "v0",
|
|
501
|
+
responseFormat: options.responseFormat,
|
|
502
|
+
middleware: options.middleware ?? [],
|
|
503
|
+
initialMessages: options.initialMessages ?? [],
|
|
504
|
+
conversationManager: options.conversationManager ?? new NullConversationManager(),
|
|
505
|
+
sessionManager: options.sessionManager,
|
|
506
|
+
events: options.events
|
|
507
|
+
};
|
|
508
|
+
this.agentState = options.state instanceof AgentState ? options.state : new AgentState(options.state ?? {});
|
|
509
|
+
}
|
|
510
|
+
/** Public identity for orchestration menus and logs. */
|
|
511
|
+
get name() {
|
|
512
|
+
return this.options.name;
|
|
513
|
+
}
|
|
514
|
+
get description() {
|
|
515
|
+
return this.options.description;
|
|
516
|
+
}
|
|
517
|
+
get model() {
|
|
518
|
+
return this.options.model;
|
|
519
|
+
}
|
|
520
|
+
/** Member tools (copy) — introspection for orchestration/menus. */
|
|
521
|
+
get tools() {
|
|
522
|
+
return [...this.options.tools];
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Clone with extra tools. Memory, session, and state are NOT carried over
|
|
526
|
+
* (fresh per clone); model, tools, middleware, and formats are shared.
|
|
527
|
+
* Used by coordinators (swarm handoffs) that inject control tools.
|
|
528
|
+
*/
|
|
529
|
+
withTools(...extra) {
|
|
530
|
+
return new _Agent({
|
|
531
|
+
name: this.options.name,
|
|
532
|
+
description: this.options.description,
|
|
533
|
+
systemPrompt: this.options.systemPrompt,
|
|
534
|
+
model: this.options.model,
|
|
535
|
+
tools: [...this.options.tools, ...extra],
|
|
536
|
+
maxSteps: this.options.maxSteps,
|
|
537
|
+
maxToolCalls: this.options.maxToolCalls,
|
|
538
|
+
budgets: this.options.budgets,
|
|
539
|
+
outputVersion: this.options.outputVersion,
|
|
540
|
+
responseFormat: this.options.responseFormat,
|
|
541
|
+
middleware: [...this.options.middleware],
|
|
542
|
+
conversationManager: this.options.conversationManager,
|
|
543
|
+
events: this.options.events
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
/** Key-value state (never sent to the model). */
|
|
547
|
+
get state() {
|
|
548
|
+
return this.agentState;
|
|
549
|
+
}
|
|
550
|
+
/** Session handle: the manager's id, or a generated per-instance id. */
|
|
551
|
+
get sessionId() {
|
|
552
|
+
if (this.options.sessionManager) return this.options.sessionManager.sessionId;
|
|
553
|
+
this.instanceSessionId ??= Math.random().toString(16).slice(2, 10);
|
|
554
|
+
return this.instanceSessionId;
|
|
555
|
+
}
|
|
556
|
+
/** Full conversation history from the last run (system + memory + new turns). */
|
|
557
|
+
get messages() {
|
|
558
|
+
return [...this.lastMessages];
|
|
559
|
+
}
|
|
560
|
+
/** Version timeline: runs, edits, branches, resumes (append-only). */
|
|
561
|
+
get timeline() {
|
|
562
|
+
return [...this.revisions];
|
|
563
|
+
}
|
|
564
|
+
record(kind, label) {
|
|
565
|
+
this.revisions.push({ rev: this.revisions.length + 1, kind, ...label ? { label } : {}, at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Edit a message in the current history (by id or index) and keep the
|
|
569
|
+
* version in the timeline. History + memory stay in sync.
|
|
570
|
+
*/
|
|
571
|
+
async editMessage(idOrIndex, content) {
|
|
572
|
+
const idx = typeof idOrIndex === "number" ? idOrIndex : this.lastMessages.findIndex((m) => m.id === idOrIndex);
|
|
573
|
+
if (idx < 0 || idx >= this.lastMessages.length) {
|
|
574
|
+
throw new AgentError("MESSAGE_NOT_FOUND", `No message ${String(idOrIndex)} in history`, false);
|
|
575
|
+
}
|
|
576
|
+
if (typeof this.lastMessages[idx].content !== "string" && this.lastMessages[idx].content.some((b) => b.type === "tool_call")) {
|
|
577
|
+
throw new AgentError("MESSAGE_NOT_EDITABLE", `Message ${String(idOrIndex)} contains tool calls; branch() from a point before it instead`, false);
|
|
578
|
+
}
|
|
579
|
+
const updated = { ...this.lastMessages[idx], content };
|
|
580
|
+
this.lastMessages = [...this.lastMessages.slice(0, idx), updated, ...this.lastMessages.slice(idx + 1)];
|
|
581
|
+
if (this.options.memory) {
|
|
582
|
+
await this.options.memory.clear();
|
|
583
|
+
await this.options.memory.save(this.lastMessages.filter((m) => m.role !== "system"));
|
|
584
|
+
}
|
|
585
|
+
this.record("edit", `message ${updated.id ?? idx}`);
|
|
586
|
+
return updated;
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Branch the current history into an independent agent. Model, tools,
|
|
590
|
+
* middleware, and formats are shared; state is copied; memory is fresh
|
|
591
|
+
* (seeded with this history); sessions are NOT carried over (to avoid
|
|
592
|
+
* double-persist collisions). Both timelines record the branch.
|
|
593
|
+
*/
|
|
594
|
+
async branch(label, overrides = {}) {
|
|
595
|
+
const memory = new InMemoryMemory();
|
|
596
|
+
await memory.save(this.lastMessages.filter((m) => m.role !== "system"));
|
|
597
|
+
const child = new _Agent({
|
|
598
|
+
name: this.options.name,
|
|
599
|
+
description: this.options.description,
|
|
600
|
+
systemPrompt: this.options.systemPrompt,
|
|
601
|
+
model: this.options.model,
|
|
602
|
+
tools: [...this.options.tools],
|
|
603
|
+
memory,
|
|
604
|
+
maxSteps: this.options.maxSteps,
|
|
605
|
+
maxToolCalls: this.options.maxToolCalls,
|
|
606
|
+
budgets: this.options.budgets,
|
|
607
|
+
outputVersion: this.options.outputVersion,
|
|
608
|
+
responseFormat: this.options.responseFormat,
|
|
609
|
+
middleware: [...this.options.middleware],
|
|
610
|
+
state: this.agentState.toJSON(),
|
|
611
|
+
conversationManager: this.options.conversationManager,
|
|
612
|
+
events: this.options.events,
|
|
613
|
+
...overrides
|
|
614
|
+
});
|
|
615
|
+
child.lastMessages = [...this.lastMessages];
|
|
616
|
+
child.revisions = [...this.revisions];
|
|
617
|
+
child.record("branch", label);
|
|
618
|
+
this.record("branch", label);
|
|
619
|
+
return child;
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Pause mid-conversation: persist history + state under threadId without
|
|
623
|
+
* needing an interrupt. Pair with resume() (empty decisions continue).
|
|
624
|
+
*/
|
|
625
|
+
async pause(threadId, opts) {
|
|
626
|
+
await opts.checkpointer.save(threadId, {
|
|
627
|
+
messages: [...this.lastMessages],
|
|
628
|
+
state: this.agentState.toJSON(),
|
|
629
|
+
pending: []
|
|
630
|
+
});
|
|
631
|
+
return { threadId, messages: this.lastMessages.length };
|
|
632
|
+
}
|
|
633
|
+
/** Manual checkpoint: messages + state (+ system prompt on request) + appData. */
|
|
634
|
+
takeSnapshot(opts = { preset: "session" }) {
|
|
635
|
+
return {
|
|
636
|
+
id: `snapshot-${Date.now().toString(36)}`,
|
|
637
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
638
|
+
data: selectSnapshotFields(
|
|
639
|
+
{ messages: [...this.lastMessages], state: this.agentState.toJSON(), systemPrompt: this.options.systemPrompt },
|
|
640
|
+
opts
|
|
641
|
+
),
|
|
642
|
+
...opts.appData ? { appData: opts.appData } : {}
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
/** Restore fields present in the snapshot; absent fields keep current values. */
|
|
646
|
+
async loadSnapshot(snapshot) {
|
|
647
|
+
if (snapshot.data.state) {
|
|
648
|
+
for (const [k, v] of Object.entries(snapshot.data.state)) this.agentState.set(k, v);
|
|
649
|
+
}
|
|
650
|
+
if (snapshot.data.messages) {
|
|
651
|
+
this.lastMessages = [...snapshot.data.messages];
|
|
652
|
+
if (this.options.memory) {
|
|
653
|
+
await this.options.memory.clear();
|
|
654
|
+
await this.options.memory.save(snapshot.data.messages.filter((m) => m.role !== "system"));
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Resume a suspended run: restores the checkpoint, seeds human decisions,
|
|
660
|
+
* and continues the loop without new input. Decisions are keyed by
|
|
661
|
+
* `hitlKey(tool, args)` for an exact match, or by bare tool name to cover
|
|
662
|
+
* every re-issued call of that tool (models re-issue with regenerated arg
|
|
663
|
+
* text, so exact keys alone would never converge).
|
|
664
|
+
*/
|
|
665
|
+
async resume(threadId, decisions = {}, opts) {
|
|
666
|
+
const cp = await opts.checkpointer.load(threadId);
|
|
667
|
+
if (!cp) throw new AgentError("CHECKPOINT_MISSING", `No checkpoint for thread "${threadId}"`, false);
|
|
668
|
+
if (cp.state) {
|
|
669
|
+
for (const [k, v] of Object.entries(cp.state)) this.agentState.set(k, v);
|
|
670
|
+
}
|
|
671
|
+
this.lastMessages = [...cp.messages];
|
|
672
|
+
if (this.options.memory) {
|
|
673
|
+
await this.options.memory.clear();
|
|
674
|
+
await this.options.memory.save(cp.messages.filter((m) => m.role !== "system"));
|
|
675
|
+
}
|
|
676
|
+
const invocationState = { ...opts.invocationState ?? {}, __hitlDecisions: decisions };
|
|
677
|
+
const gen = this.iterateContinuation(opts.signal, invocationState, { checkpointer: opts.checkpointer, threadId });
|
|
678
|
+
for (; ; ) {
|
|
679
|
+
const next = await runWithBudget(() => gen.next(), { signal: opts.signal });
|
|
680
|
+
if (next.done) {
|
|
681
|
+
this.record("resume", threadId);
|
|
682
|
+
return next.value;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
buildRuntime(invocationState) {
|
|
687
|
+
const { model, memory, systemPrompt, events, middleware, conversationManager, sessionManager } = this.options;
|
|
688
|
+
const format = this.options.responseFormat;
|
|
689
|
+
const strategy = format ? format.strategy && format.strategy !== "auto" ? format.strategy : model.capabilities?.structuredOutput ? "provider" : "tool" : void 0;
|
|
690
|
+
const tools = strategy === "tool" && format ? [...this.options.tools, structuredTool(format)] : this.options.tools;
|
|
691
|
+
const toolCtx = { agentName: this.options.name, state: this.agentState, invocationState };
|
|
692
|
+
const ctxFor = (step) => ({ step, invocationState });
|
|
693
|
+
return {
|
|
694
|
+
model,
|
|
695
|
+
memory,
|
|
696
|
+
systemPrompt,
|
|
697
|
+
events,
|
|
698
|
+
middleware,
|
|
699
|
+
conversationManager,
|
|
700
|
+
sessionManager,
|
|
701
|
+
tools,
|
|
702
|
+
format,
|
|
703
|
+
strategy,
|
|
704
|
+
maxSteps: this.options.maxSteps,
|
|
705
|
+
maxToolCalls: this.options.maxToolCalls,
|
|
706
|
+
budgets: this.options.budgets,
|
|
707
|
+
outputVersion: this.options.outputVersion,
|
|
708
|
+
state: this.agentState,
|
|
709
|
+
toolCtx,
|
|
710
|
+
ctxFor
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
/** Continue the loop from restored history (no new user message). */
|
|
714
|
+
async *iterateContinuation(signal, invocationState, checkpoint) {
|
|
715
|
+
const rt = this.buildRuntime(invocationState);
|
|
716
|
+
const messages = [
|
|
717
|
+
{ role: "system", content: rt.systemPrompt },
|
|
718
|
+
...rt.memory ? await rt.memory.load() : this.lastMessages.filter((m) => m.role !== "system")
|
|
719
|
+
];
|
|
720
|
+
return yield* this.runLoop(messages, rt, signal, invocationState, checkpoint);
|
|
721
|
+
}
|
|
722
|
+
async run(input, signalOrOpts) {
|
|
723
|
+
const opts = signalOrOpts instanceof AbortSignal ? { signal: signalOrOpts } : signalOrOpts ?? {};
|
|
724
|
+
const stream = this.stream(input, opts);
|
|
725
|
+
for (; ; ) {
|
|
726
|
+
const next = await stream.next();
|
|
727
|
+
if (next.done) {
|
|
728
|
+
this.record("run", typeof input === "string" ? input.slice(0, 60) : "blocks");
|
|
729
|
+
return next.value;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
async *stream(input, signalOrOpts, invocationState) {
|
|
734
|
+
const opts = signalOrOpts instanceof AbortSignal ? { signal: signalOrOpts, invocationState } : signalOrOpts ?? {};
|
|
735
|
+
const checkpoint = opts.checkpointer && opts.threadId ? { checkpointer: opts.checkpointer, threadId: opts.threadId } : void 0;
|
|
736
|
+
const gen = this.iterate(input, opts.signal, opts.invocationState ?? {}, checkpoint);
|
|
737
|
+
for (; ; ) {
|
|
738
|
+
const next = await runWithBudget(() => gen.next(), { signal: opts.signal });
|
|
739
|
+
if (next.done) return next.value;
|
|
740
|
+
yield next.value;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
async *iterate(input, signal, invocationState = {}, checkpoint) {
|
|
744
|
+
const rt = this.buildRuntime(invocationState);
|
|
745
|
+
const { memory, systemPrompt } = this.options;
|
|
746
|
+
const sessionManager = this.options.sessionManager;
|
|
747
|
+
if (sessionManager && !this.restored) {
|
|
748
|
+
this.restored = true;
|
|
749
|
+
const latest = await sessionManager.loadLatest();
|
|
750
|
+
if (latest?.state) {
|
|
751
|
+
for (const [k, v] of Object.entries(latest.state)) this.agentState.set(k, v);
|
|
752
|
+
}
|
|
753
|
+
if (latest?.messages?.length) {
|
|
754
|
+
this.lastMessages = [...latest.messages];
|
|
755
|
+
if (memory && (await memory.load()).length === 0) {
|
|
756
|
+
await memory.save(latest.messages.filter((m) => m.role !== "system"));
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
if (!this.seeded && this.options.initialMessages.length > 0) {
|
|
761
|
+
this.seeded = true;
|
|
762
|
+
if (memory && (await memory.load()).length === 0) {
|
|
763
|
+
await memory.save(this.options.initialMessages.filter((m) => m.role !== "system"));
|
|
764
|
+
} else if (!memory && this.lastMessages.length === 0) {
|
|
765
|
+
this.lastMessages = [...this.options.initialMessages];
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
const messages = [
|
|
769
|
+
{ role: "system", content: systemPrompt },
|
|
770
|
+
...memory ? await memory.load() : this.lastMessages.filter((m) => m.role !== "system"),
|
|
771
|
+
{ role: "user", content: input }
|
|
772
|
+
];
|
|
773
|
+
return yield* this.runLoop(messages, rt, signal, invocationState, checkpoint);
|
|
774
|
+
}
|
|
775
|
+
async *runLoop(messages, rt, signal, invocationState, checkpoint) {
|
|
776
|
+
const { model, memory, systemPrompt, events, middleware, conversationManager, sessionManager, tools, format, strategy, toolCtx, ctxFor } = rt;
|
|
777
|
+
const toolCalls = [];
|
|
778
|
+
const usage = { inputTokens: 0, outputTokens: 0 };
|
|
779
|
+
const startedAt = Date.now();
|
|
780
|
+
let text = "";
|
|
781
|
+
let structuredResponse;
|
|
782
|
+
let hasStructured = false;
|
|
783
|
+
try {
|
|
784
|
+
for (let step = 0; step < rt.maxSteps; step++) {
|
|
785
|
+
signal?.throwIfAborted();
|
|
786
|
+
if (conversationManager.beforeModel) {
|
|
787
|
+
const managed2 = await conversationManager.beforeModel(messages, step);
|
|
788
|
+
if (managed2 !== messages) {
|
|
789
|
+
messages.length = 0;
|
|
790
|
+
messages.push(...managed2);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
events?.onStep?.({ step, messages });
|
|
794
|
+
yield { type: "step", step };
|
|
795
|
+
let req = {
|
|
796
|
+
messages,
|
|
797
|
+
tools: tools.map((t) => ({ name: t.name, description: t.description, ...toolParameters(t) ? { parametersSchema: toolParameters(t) } : {} })),
|
|
798
|
+
...strategy === "provider" && format ? { responseFormat: { name: responseFormatName(format), schema: responseFormatJsonSchema(format) } } : {}
|
|
799
|
+
};
|
|
800
|
+
for (const mw of middleware) req = await mw.beforeModel?.(req, ctxFor(step)) ?? req;
|
|
801
|
+
const pending = [];
|
|
802
|
+
const callModel = (r) => model.complete({
|
|
803
|
+
...r,
|
|
804
|
+
// Propagate the run's abort into the in-flight provider request.
|
|
805
|
+
...signal ? { signal } : {},
|
|
806
|
+
onToken: (d) => {
|
|
807
|
+
events?.onToken?.(d);
|
|
808
|
+
pending.push({ type: "token", delta: d });
|
|
809
|
+
},
|
|
810
|
+
onThinking: (d) => {
|
|
811
|
+
events?.onThinking?.(d);
|
|
812
|
+
pending.push({ type: "thinking", delta: d });
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
const modelChain = middleware.filter((mw) => mw.wrapModel).reduceRight(
|
|
816
|
+
(next, mw) => (r) => mw.wrapModel(r, next, ctxFor(step)),
|
|
817
|
+
callModel
|
|
818
|
+
);
|
|
819
|
+
let completion = await modelChain(req);
|
|
820
|
+
for (const e of pending.splice(0)) {
|
|
821
|
+
if (e.delta !== "") yield e;
|
|
822
|
+
}
|
|
823
|
+
for (const mw of [...middleware].reverse()) completion = await mw.afterModel?.(completion, req, ctxFor(step)) ?? completion;
|
|
824
|
+
usage.inputTokens += completion.usage.inputTokens;
|
|
825
|
+
usage.outputTokens += completion.usage.outputTokens;
|
|
826
|
+
usage.reasoningTokens = (usage.reasoningTokens ?? 0) + (completion.usage.reasoningTokens ?? 0);
|
|
827
|
+
checkBudgets(rt, usage, startedAt, model.id);
|
|
828
|
+
if (completion.toolCalls.length === 0) {
|
|
829
|
+
text = completion.text;
|
|
830
|
+
if (strategy === "provider" && format) {
|
|
831
|
+
try {
|
|
832
|
+
structuredResponse = parseStructured(format, JSON.parse(text));
|
|
833
|
+
hasStructured = true;
|
|
834
|
+
} catch (err) {
|
|
835
|
+
const retryable = err instanceof AgentError ? err.retryable : true;
|
|
836
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
837
|
+
if (step < rt.maxSteps - 1 && retryable) {
|
|
838
|
+
messages.push({ role: "assistant", content: text });
|
|
839
|
+
messages.push({
|
|
840
|
+
role: "user",
|
|
841
|
+
content: `Your last response was not valid for "${responseFormatName(format)}": ${message}. Reply with corrected JSON only.`
|
|
842
|
+
});
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
throw err instanceof AgentError ? err : new AgentError("STRUCTURED_INVALID", `Invalid structured output: ${message}`, true);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
messages.push(
|
|
849
|
+
rt.outputVersion === "v1" && completion.thinking ? {
|
|
850
|
+
role: "assistant",
|
|
851
|
+
content: [
|
|
852
|
+
{ type: "reasoning", reasoning: completion.thinking },
|
|
853
|
+
{ type: "text", text }
|
|
854
|
+
]
|
|
855
|
+
} : { role: "assistant", content: text }
|
|
856
|
+
);
|
|
857
|
+
break;
|
|
858
|
+
}
|
|
859
|
+
messages.push({
|
|
860
|
+
role: "assistant",
|
|
861
|
+
content: completion.toolCalls.map((c) => ({ type: "tool_call", id: c.id, name: c.name, args: c.args }))
|
|
862
|
+
});
|
|
863
|
+
for (const call of completion.toolCalls) {
|
|
864
|
+
if (rt.maxToolCalls != null && toolCalls.length >= rt.maxToolCalls) {
|
|
865
|
+
throw new AgentError(
|
|
866
|
+
"BUDGET_EXCEEDED",
|
|
867
|
+
`Exceeded maxToolCalls (${rt.maxToolCalls})`,
|
|
868
|
+
false
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
const tool = tools.find((t) => t.name === call.name);
|
|
872
|
+
if (!tool) {
|
|
873
|
+
messages.push({
|
|
874
|
+
role: "tool",
|
|
875
|
+
content: `Unknown tool: ${call.name}`,
|
|
876
|
+
toolCallId: call.id,
|
|
877
|
+
toolName: call.name
|
|
878
|
+
});
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
881
|
+
let toolCall = { name: call.name, args: call.args };
|
|
882
|
+
let output;
|
|
883
|
+
try {
|
|
884
|
+
for (const mw of middleware) toolCall = await mw.beforeTool?.(toolCall, ctxFor(step)) ?? toolCall;
|
|
885
|
+
events?.onToolCall?.({ tool: toolCall.name, args: toolCall.args });
|
|
886
|
+
yield { type: "tool_call", name: toolCall.name, args: toolCall.args };
|
|
887
|
+
const invoke = () => tool.execute(toolCall.args, toolCtx);
|
|
888
|
+
const chain = middleware.filter((mw) => mw.aroundTool).reduceRight(
|
|
889
|
+
(next, mw) => () => mw.aroundTool(toolCall, next, ctxFor(step)),
|
|
890
|
+
invoke
|
|
891
|
+
);
|
|
892
|
+
output = await chain();
|
|
893
|
+
} catch (err) {
|
|
894
|
+
if (err instanceof HitlInterrupt) throw err;
|
|
895
|
+
if (err instanceof ToolRejection) {
|
|
896
|
+
output = err.feedback;
|
|
897
|
+
} else {
|
|
898
|
+
if (err instanceof AgentError) throw err;
|
|
899
|
+
throw new AgentError(
|
|
900
|
+
"TOOL_EXECUTION",
|
|
901
|
+
`Tool "${toolCall.name}" failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
902
|
+
false
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
for (const mw of [...middleware].reverse()) output = await mw.afterTool?.(toolCall, output, ctxFor(step)) ?? output;
|
|
907
|
+
const result = (tool.toResult ?? defaultToResult)(output);
|
|
908
|
+
toolCalls.push({ id: call.id, name: toolCall.name, args: toolCall.args, output, ...result.artifact !== void 0 ? { artifact: result.artifact } : {} });
|
|
909
|
+
if (strategy === "tool" && format && toolCall.name === responseFormatName(format) && !hasStructured) {
|
|
910
|
+
try {
|
|
911
|
+
structuredResponse = parseStructured(format, output);
|
|
912
|
+
hasStructured = true;
|
|
913
|
+
} catch {
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
events?.onToolResult?.({ tool: toolCall.name, output });
|
|
917
|
+
yield { type: "tool_result", name: toolCall.name, output };
|
|
918
|
+
messages.push({
|
|
919
|
+
role: "tool",
|
|
920
|
+
content: result.content,
|
|
921
|
+
toolCallId: call.id,
|
|
922
|
+
toolName: toolCall.name
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
if (step === rt.maxSteps - 1) {
|
|
926
|
+
text = toolCalls.length ? `Completed ${toolCalls.length} tool call(s). Last output: ${JSON.stringify(toolCalls.at(-1)?.output)}` : "";
|
|
927
|
+
messages.push({ role: "assistant", content: text });
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
} catch (err) {
|
|
931
|
+
if (err instanceof HitlInterrupt && checkpoint) {
|
|
932
|
+
const pendingArgs = JSON.stringify(err.args ?? {});
|
|
933
|
+
let cut = messages.length;
|
|
934
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
935
|
+
const m = messages[i];
|
|
936
|
+
if (m.role === "assistant" && typeof m.content !== "string" && m.content.some((b) => b.type === "tool_call" && b.name === err.tool && JSON.stringify(b.args ?? {}) === pendingArgs)) {
|
|
937
|
+
cut = i;
|
|
938
|
+
break;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
await checkpoint.checkpointer.save(checkpoint.threadId, {
|
|
942
|
+
messages: messages.slice(0, cut),
|
|
943
|
+
state: rt.state.toJSON(),
|
|
944
|
+
pending: [{ interruptId: err.interruptId, tool: err.tool, args: err.args }]
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
throw err;
|
|
948
|
+
}
|
|
949
|
+
if (format && !hasStructured) {
|
|
950
|
+
throw new AgentError(
|
|
951
|
+
"STRUCTURED_NO_MATCH",
|
|
952
|
+
`No valid "${responseFormatName(format)}" produced within ${rt.maxSteps} steps`,
|
|
953
|
+
false
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
const managed = await conversationManager.apply(messages);
|
|
957
|
+
if (memory) {
|
|
958
|
+
await memory.clear();
|
|
959
|
+
await memory.save(managed.filter((m) => m.role !== "system"));
|
|
960
|
+
}
|
|
961
|
+
this.lastMessages = managed;
|
|
962
|
+
if (sessionManager) {
|
|
963
|
+
await sessionManager.saveLatest({
|
|
964
|
+
messages: managed,
|
|
965
|
+
state: rt.state.toJSON(),
|
|
966
|
+
systemPrompt
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
events?.onDone?.({ text, toolCalls });
|
|
970
|
+
yield { type: "done", text };
|
|
971
|
+
const costUsd = usageCost(usage, model.id, rt.budgets?.prices);
|
|
972
|
+
return {
|
|
973
|
+
text,
|
|
974
|
+
messages: [...managed],
|
|
975
|
+
toolCalls,
|
|
976
|
+
usage: { ...usage, ...costUsd !== void 0 ? { costUsd } : {} },
|
|
977
|
+
invocationState,
|
|
978
|
+
...hasStructured ? { structuredResponse } : {}
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
};
|
|
982
|
+
function checkBudgets(rt, usage, startedAt, modelId) {
|
|
983
|
+
const budgets = rt.budgets;
|
|
984
|
+
if (!budgets) return;
|
|
985
|
+
const total = usage.inputTokens + usage.outputTokens;
|
|
986
|
+
if (budgets.maxTokens != null && total > budgets.maxTokens) {
|
|
987
|
+
throw new AgentError("BUDGET_EXCEEDED", `Exceeded maxTokens (${budgets.maxTokens}, used ${total})`, false);
|
|
988
|
+
}
|
|
989
|
+
if (budgets.maxCostUsd != null) {
|
|
990
|
+
const prices = budgets.prices ?? priceFor(modelId);
|
|
991
|
+
if (!prices) {
|
|
992
|
+
throw new AgentError("BUDGET_NO_PRICE", `Cannot enforce maxCostUsd: no price for model "${modelId}" (set budgets.prices)`, false);
|
|
993
|
+
}
|
|
994
|
+
const cost = usageCost(usage, modelId, prices);
|
|
995
|
+
if (cost != null && cost > budgets.maxCostUsd) {
|
|
996
|
+
throw new AgentError("BUDGET_EXCEEDED", `Exceeded maxCostUsd ($${budgets.maxCostUsd}, used $${cost.toFixed(4)})`, false);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
if (budgets.deadlineMs != null && Date.now() - startedAt > budgets.deadlineMs) {
|
|
1000
|
+
throw new AgentError("BUDGET_EXCEEDED", `Exceeded deadline (${budgets.deadlineMs}ms)`, false);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
function structuredTool(format) {
|
|
1004
|
+
const name = responseFormatName(format);
|
|
1005
|
+
const maxAttempts = format.maxAttempts ?? 2;
|
|
1006
|
+
let attempts = 0;
|
|
1007
|
+
return defineTool({
|
|
1008
|
+
name,
|
|
1009
|
+
description: `Return the final answer as structured "${name}". Call exactly once with valid arguments.`,
|
|
1010
|
+
schema: format.schema,
|
|
1011
|
+
execute: async (args) => {
|
|
1012
|
+
attempts += 1;
|
|
1013
|
+
const parsed = format.schema.safeParse(args);
|
|
1014
|
+
if (parsed.success) return parsed.data;
|
|
1015
|
+
if (attempts >= maxAttempts) {
|
|
1016
|
+
throw new AgentError("STRUCTURED_NO_MATCH", `Invalid "${name}" after ${attempts} attempts: ${parsed.error.message}`, false);
|
|
1017
|
+
}
|
|
1018
|
+
return `Error: invalid "${name}": ${parsed.error.message}. Fix the arguments and call again.`;
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
export {
|
|
1024
|
+
wireTextOf,
|
|
1025
|
+
hasImages,
|
|
1026
|
+
imagesOf,
|
|
1027
|
+
AgentError,
|
|
1028
|
+
todoListMiddleware,
|
|
1029
|
+
filesystemMiddleware,
|
|
1030
|
+
LocalFileStorage,
|
|
1031
|
+
SessionManager,
|
|
1032
|
+
Agent
|
|
1033
|
+
};
|