@anteros/core 0.0.41 → 0.0.44

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.
Files changed (133) hide show
  1. package/database/audit.ts +9 -59
  2. package/database/collection.ts +3 -1
  3. package/database/deleteLog.ts +67 -3
  4. package/database/file.ts +4 -3
  5. package/database/mongodbadapter.ts +103 -42
  6. package/database/replication.ts +724 -58
  7. package/database/ttl.ts +68 -0
  8. package/database/workflow.ts +466 -240
  9. package/index.ts +6 -0
  10. package/lib/agent.ts +1567 -0
  11. package/lib/agentMemory.ts +689 -0
  12. package/lib/agents.ts +180 -0
  13. package/lib/ai.ts +0 -0
  14. package/lib/api.ts +246 -0
  15. package/lib/attachment.ts +351 -0
  16. package/lib/auditResult.ts +13 -0
  17. package/lib/define.ts +43 -1
  18. package/lib/endpoints.ts +80 -0
  19. package/lib/files.ts +2 -1
  20. package/lib/jsonSchema.ts +183 -0
  21. package/lib/lifecycle.ts +7 -1
  22. package/lib/load.ts +17 -0
  23. package/lib/mcp.ts +3 -2
  24. package/lib/mcpClient.ts +128 -0
  25. package/lib/messages.ts +299 -0
  26. package/lib/providers.ts +970 -0
  27. package/lib/routes.ts +14 -1
  28. package/lib/scriptWorker.ts +4 -1
  29. package/lib/scripts.ts +6 -1
  30. package/lib/skills.ts +481 -0
  31. package/lib/sockets.ts +3 -1
  32. package/lib/workflow.ts +209 -37
  33. package/lib/zip.ts +109 -0
  34. package/package.json +1 -1
  35. package/server/access.ts +96 -0
  36. package/server/agents.ts +472 -0
  37. package/server/api.ts +80 -65
  38. package/server/boot.ts +11 -0
  39. package/server/hono.ts +8 -3
  40. package/server/mcp.ts +17 -63
  41. package/server/routes.ts +19 -24
  42. package/tests/agent-mcp.test.ts +116 -0
  43. package/tests/agent-memory.test.ts +617 -0
  44. package/tests/agent-provider.test.ts +288 -0
  45. package/tests/agent-runtime.test.ts +369 -0
  46. package/tests/agent-skills.test.ts +177 -0
  47. package/tests/agent-summarize.test.ts +124 -0
  48. package/tests/agent-working-memory.test.ts +404 -0
  49. package/tests/agent.test.ts +1241 -0
  50. package/tests/agents-api.test.ts +593 -0
  51. package/tests/api-private-fields.test.ts +196 -0
  52. package/tests/api.test.ts +51 -0
  53. package/tests/attachment.test.ts +241 -0
  54. package/tests/audit-operations.test.ts +1 -1
  55. package/tests/audit-redact.test.ts +29 -3
  56. package/tests/audit-run-action.test.ts +1 -1
  57. package/tests/endpoints.test.ts +80 -0
  58. package/tests/file-collection-replication.test.ts +2 -0
  59. package/tests/fixtures/agent-tenant/agents/support.agent.ts +9 -0
  60. package/tests/fixtures/agent-tenant/agents/weather.agent.ts +21 -0
  61. package/tests/fixtures/agents-api/agents/assistant.agent.ts +46 -0
  62. package/tests/fixtures/agents-api/agents/plain.agent.ts +23 -0
  63. package/tests/fixtures/agents-api/agents/private.agent.ts +15 -0
  64. package/tests/fixtures/agents-api/collections/notes.model.ts +26 -0
  65. package/tests/fixtures/agents-api/routes/probe.route.ts +21 -0
  66. package/tests/fixtures/agents-api/services/probe.service.ts +19 -0
  67. package/tests/fixtures/attachment/data.csv +2 -0
  68. package/tests/fixtures/fake-provider.ts +128 -0
  69. package/tests/fixtures/repl-only/app.ts +3 -0
  70. package/tests/fixtures/replication-meta/agents/chat.agent.ts +16 -0
  71. package/tests/fixtures/replication-meta/collections/counters.model.ts +13 -0
  72. package/tests/fixtures/replication-meta/collections/orders.model.ts +9 -0
  73. package/tests/fixtures/replication-meta/workflows/run.workflow.ts +8 -0
  74. package/tests/fixtures/skills-tenant/agents/docs.agent.ts +23 -0
  75. package/tests/fixtures/skills-tenant/agents/incomplete.agent.ts +14 -0
  76. package/tests/fixtures/skills-tenant/broken/bad-name.md +6 -0
  77. package/tests/fixtures/skills-tenant/broken/dup-a.md +6 -0
  78. package/tests/fixtures/skills-tenant/broken/dup-b.md +6 -0
  79. package/tests/fixtures/skills-tenant/broken/empty-body.md +4 -0
  80. package/tests/fixtures/skills-tenant/broken/long-compat.md +7 -0
  81. package/tests/fixtures/skills-tenant/broken/long-description.md +6 -0
  82. package/tests/fixtures/skills-tenant/broken/mismatch/SKILL.md +6 -0
  83. package/tests/fixtures/skills-tenant/broken/no-description.md +5 -0
  84. package/tests/fixtures/skills-tenant/broken/no-name.md +5 -0
  85. package/tests/fixtures/skills-tenant/broken/plain.md +4 -0
  86. package/tests/fixtures/skills-tenant/broken/unknown-key.md +9 -0
  87. package/tests/fixtures/skills-tenant/docs/pdf-forms.md +18 -0
  88. package/tests/fixtures/skills-tenant/guide/ml.md +9 -0
  89. package/tests/fixtures/skills-tenant/guides/pdf/SKILL.md +9 -0
  90. package/tests/fixtures/skills-tenant/runbooks/incident.md +9 -0
  91. package/tests/fixtures/skills-tenant/runbooks/notes.txt +2 -0
  92. package/tests/fixtures/vars-tenant/vars/private.var.ts +4 -1
  93. package/tests/fixtures/workflow-engine/workflows/failing.workflow.ts +34 -0
  94. package/tests/fixtures/workflow-engine/workflows/flaky.workflow.ts +23 -0
  95. package/tests/fixtures/workflow-engine/workflows/hang.workflow.ts +17 -0
  96. package/tests/fixtures/workflow-engine/workflows/simple.workflow.ts +19 -0
  97. package/tests/fixtures/workflow-engine/workflows/slow.workflow.ts +23 -0
  98. package/tests/fixtures/workflow-engine/workflows/versioned.workflow.ts +19 -0
  99. package/tests/fixtures/workflow-tenant/workflows/broken.workflow.ts +3 -0
  100. package/tests/fixtures/workflow-tenant/workflows/disabled.workflow.ts +8 -0
  101. package/tests/fixtures/workflow-tenant/workflows/invoice.workflow.ts +21 -0
  102. package/tests/fixtures/workflow-tenant/workflows/no-id.workflow.ts +7 -0
  103. package/tests/fixtures/workflow-tenant/workflows/order-duplicate.workflow.ts +13 -0
  104. package/tests/fixtures/workflow-tenant/workflows/order.workflow.ts +17 -0
  105. package/tests/hono.test.ts +6 -3
  106. package/tests/locks.test.ts +120 -0
  107. package/tests/mcp.test.ts +11 -11
  108. package/tests/messages.test.ts +258 -0
  109. package/tests/replication-change-stream.test.ts +240 -0
  110. package/tests/replication-control.test.ts +4 -2
  111. package/tests/replication-initial-sync.test.ts +2 -0
  112. package/tests/replication-meta.test.ts +246 -0
  113. package/tests/replication.test.ts +2 -0
  114. package/tests/upload.test.ts +44 -12
  115. package/tests/vars-api.test.ts +1 -1
  116. package/tests/vars-replication.test.ts +5 -2
  117. package/tests/workflow-engine.test.ts +340 -0
  118. package/tests/workflow-retention.test.ts +103 -0
  119. package/tests/workflow.test.ts +186 -0
  120. package/types/agent.d.ts +1067 -0
  121. package/types/config.d.ts +16 -0
  122. package/types/file.d.ts +7 -7
  123. package/types/hook.d.ts +6 -0
  124. package/types/lifecycle.d.ts +6 -0
  125. package/types/mcp.d.ts +11 -1
  126. package/types/replication.d.ts +70 -4
  127. package/types/route.d.ts +7 -1
  128. package/types/scripts.d.ts +6 -0
  129. package/types/service.d.ts +7 -1
  130. package/types/tenant.d.ts +13 -0
  131. package/types/vars.d.ts +2 -2
  132. package/types/websocket.d.ts +6 -0
  133. package/types/workflow.d.ts +50 -3
package/lib/agent.ts ADDED
@@ -0,0 +1,1567 @@
1
+ import { AppError, fn } from "./error";
2
+ import { toJsonSchema, validateWithSchema } from "./jsonSchema";
3
+ import { InMemoryAgentMemory, trimMessages } from "./agentMemory";
4
+ import { forMemory, resolveAttachments } from "./attachment";
5
+ import { normalizeMessages, serializeToolResult } from "./messages";
6
+ import { mcpToolsFor } from "./mcpClient";
7
+ import { skillsBlock } from "./skills";
8
+ import {
9
+ EMPTY_USAGE,
10
+ resolveProvider,
11
+ } from "./providers";
12
+ import type {
13
+ AgentToolSpec,
14
+ ModelAdapter,
15
+ ModelChatRequest,
16
+ } from "./providers";
17
+ import type { useRest } from "../database/rest";
18
+ import type {
19
+ AgentCallOptions,
20
+ AgentConfig,
21
+ AgentGenerateResult,
22
+ AgentInput,
23
+ AgentInstructions,
24
+ AgentMediaPart,
25
+ AgentMemory,
26
+ AgentMemoryContext,
27
+ AgentMemoryProcessor,
28
+ AgentMemoryState,
29
+ AgentMessage,
30
+ AgentOptions,
31
+ AgentPendingApproval,
32
+ AgentSkill,
33
+ AgentTextPart,
34
+ AgentThinkingBlock,
35
+ AgentThread,
36
+ AgentProvider,
37
+ AgentStreamChunk,
38
+ AgentStreamResult,
39
+ AgentStep,
40
+ AgentStructuredOutput,
41
+ AgentTool,
42
+ AgentToolCall,
43
+ AgentToolContext,
44
+ AgentToolMessage,
45
+ AgentToolResultEntry,
46
+ AgentTools,
47
+ AgentUsage,
48
+ AgentWorkingMemory,
49
+ } from "../types/agent";
50
+
51
+ /**
52
+ * Agents — an LLM with instructions, tools and memory.
53
+ *
54
+ * Use an agent when the task is open-ended: the model decides which tools to
55
+ * call, how many times, and when to answer. Use a workflow when the steps are
56
+ * known in advance.
57
+ *
58
+ * ```ts
59
+ * // v1/agents/weather.agent.ts
60
+ * import { define, v } from '@anteros/core'
61
+ *
62
+ * export default define.Agent({
63
+ * id: 'weather',
64
+ * instructions: 'You are a concise weather assistant.',
65
+ * provider: { model: 'gpt-4o-mini', apiKey: Bun.env.OPENAI_API_KEY },
66
+ * tools: {
67
+ * forecast: {
68
+ * description: 'Current weather for a city',
69
+ * inputSchema: v.object({ city: v.string().required() }),
70
+ * execute: async ({ city }, { rest }) => ({ city, celsius: 21 }),
71
+ * },
72
+ * },
73
+ * })
74
+ *
75
+ * // anywhere you have a context — a route, a service, a hook, a script…
76
+ * const { text } = await agents.get('weather')!.generate('Weather in Paris?')
77
+ * ```
78
+ *
79
+ * The provider is declared inline — `{ model, apiKey, protocol }` — with no
80
+ * SDK: `'openai'` (the default, and every OpenAI-compatible gateway) or
81
+ * `'anthropic'`.
82
+ */
83
+
84
+ const DEFAULT_MAX_STEPS = 5;
85
+
86
+ // ─── Small async primitives ──────────────────────────────────────────────
87
+
88
+ /** Unbounded async queue — bridges a push producer with a pull consumer. */
89
+ class AsyncQueue<T> {
90
+ #items: T[] = [];
91
+ #waiters: Array<() => void> = [];
92
+ #closed = false;
93
+ #error: any = null;
94
+
95
+ push(item: T): void {
96
+ if (this.#closed) return;
97
+ this.#items.push(item);
98
+ for (const wake of this.#waiters.splice(0)) wake();
99
+ }
100
+
101
+ close(): void {
102
+ if (this.#closed) return;
103
+ this.#closed = true;
104
+ for (const wake of this.#waiters.splice(0)) wake();
105
+ }
106
+
107
+ fail(error: any): void {
108
+ if (this.#closed) return;
109
+ this.#error = error;
110
+ this.#closed = true;
111
+ for (const wake of this.#waiters.splice(0)) wake();
112
+ }
113
+
114
+ async next(): Promise<IteratorResult<T>> {
115
+ for (;;) {
116
+ if (this.#error) throw this.#error;
117
+ if (this.#items.length) return { value: this.#items.shift() as T, done: false };
118
+ if (this.#closed) return { value: undefined as any, done: true };
119
+ await new Promise<void>((resolve) => this.#waiters.push(resolve));
120
+ }
121
+ }
122
+
123
+ [Symbol.asyncIterator](): AsyncIterator<T> {
124
+ return { next: () => this.next() };
125
+ }
126
+ }
127
+
128
+ type Deferred<T> = {
129
+ promise: Promise<T>;
130
+ resolve: (value: T) => void;
131
+ reject: (reason?: any) => void;
132
+ };
133
+
134
+ function deferred<T>(): Deferred<T> {
135
+ let resolve!: (value: T) => void;
136
+ let reject!: (reason?: any) => void;
137
+ const promise = new Promise<T>((res, rej) => {
138
+ resolve = res;
139
+ reject = rej;
140
+ });
141
+ return { promise, resolve, reject };
142
+ }
143
+
144
+ // ─── Tool helpers ────────────────────────────────────────────────────────
145
+
146
+ /** Normalize `AgentTools` (record or array) into entries, keyed by tool id. */
147
+ function toolEntries(tools: AgentTools): Array<[string, AgentTool]> {
148
+ const entries: Array<[string, AgentTool]> = [];
149
+
150
+ const resolve = (tool: AgentTool, label?: string): [string, AgentTool] => {
151
+ // An MCP tool carries the protocol's `name`; our own tools carry an `id`
152
+ const id = tool?.id ?? tool?.name;
153
+ if (!id) {
154
+ throw new AppError("A tool requires an `id` (`define.Tool({ id, description, execute })`)", {
155
+ status: 500, code: "AGENT_TOOL_INVALID",
156
+ });
157
+ }
158
+ // The record key is a label for the reader — it must agree with the id, since
159
+ // the id is what the model calls
160
+ if (label !== undefined && label !== id) {
161
+ throw new AppError(
162
+ `Tool '${label}' has id '${id}' — the key must match the id`,
163
+ { status: 500, code: "AGENT_TOOL_ID_MISMATCH" },
164
+ );
165
+ }
166
+ return [id, tool];
167
+ };
168
+
169
+ if (Array.isArray(tools)) {
170
+ for (const tool of tools) entries.push(resolve(tool));
171
+ return entries;
172
+ }
173
+
174
+ for (const [label, tool] of Object.entries(tools)) entries.push(resolve(tool, label));
175
+ return entries;
176
+ }
177
+
178
+ function requireHandler(name: string, tool: AgentTool): void {
179
+ if (typeof tool.execute !== "function" && typeof tool.exec !== "function") {
180
+ throw new AppError(`Tool '${name}' has neither \`execute\` nor \`exec\``, {
181
+ status: 500,
182
+ code: "AGENT_TOOL_INVALID",
183
+ });
184
+ }
185
+ }
186
+
187
+ /** Parse the arguments the model produced (`{}` when it emitted invalid JSON). */
188
+ function toToolCall(raw: { id: string; name: string; argsText: string }): AgentToolCall {
189
+ const argsText = raw.argsText ?? "";
190
+ let args: Record<string, any> = {};
191
+ if (argsText.trim()) {
192
+ try {
193
+ const parsed = JSON.parse(argsText);
194
+ args = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : { value: parsed };
195
+ } catch {
196
+ args = {};
197
+ }
198
+ }
199
+ return { id: raw.id, name: raw.name, args, argsText };
200
+ }
201
+
202
+ /** The tool the framework adds when `workingMemory` is declared. */
203
+ const WORKING_MEMORY_TOOL = "updateWorkingMemory";
204
+
205
+ /**
206
+ * The system-prompt block of the working memory — what the model reads, and the
207
+ * rule that keeps it up to date.
208
+ */
209
+ function workingMemoryBlock(config: AgentWorkingMemory, value: any): string {
210
+ const head = "## Working memory\n\n";
211
+ const rule = config.tool === false
212
+ ? ""
213
+ : `\n\nKeep it up to date with the \`${WORKING_MEMORY_TOOL}\` tool: record what is durable,\nremove what is no longer true, and never store secrets.`;
214
+
215
+ if (config.schema) {
216
+ return `${head}\`\`\`json\n${JSON.stringify(value ?? {}, null, 2)}\n\`\`\`${rule}`;
217
+ }
218
+
219
+ const body = typeof value === "string" && value.trim() ? value : (config.template ?? "");
220
+ return `${head}${body}${rule}`;
221
+ }
222
+
223
+ /**
224
+ * A **partial** update of a structured working memory: objects are deep merged,
225
+ * a `null` removes a key, an array replaces the array.
226
+ */
227
+ function mergeWorkingMemory(current: any, patch: any): any {
228
+ if (!patch || typeof patch !== "object" || Array.isArray(patch)) return patch;
229
+
230
+ const base: Record<string, any> = current && typeof current === "object" && !Array.isArray(current)
231
+ ? { ...current }
232
+ : {};
233
+
234
+ for (const [key, value] of Object.entries(patch)) {
235
+ if (value === null) delete base[key];
236
+ else if (value && typeof value === "object" && !Array.isArray(value)) base[key] = mergeWorkingMemory(base[key], value);
237
+ else base[key] = value;
238
+ }
239
+
240
+ return base;
241
+ }
242
+
243
+ /**
244
+ * Run the `load` or `save` processors in order. A processor that returns nothing
245
+ * leaves the conversation as it is; a thrown error stops the pipeline (for
246
+ * `save`, the caller catches it and stores nothing).
247
+ */
248
+ async function applyProcessors(
249
+ processors: AgentMemoryProcessor[] | undefined,
250
+ hook: "load" | "save",
251
+ messages: AgentMessage[],
252
+ ctx: AgentMemoryContext,
253
+ ): Promise<AgentMessage[]> {
254
+ let current = messages;
255
+ for (const processor of processors ?? []) {
256
+ const result = await processor[hook]?.(current, ctx);
257
+ if (Array.isArray(result)) current = result;
258
+ }
259
+ return current;
260
+ }
261
+
262
+ // ─── Usage / structured output ───────────────────────────────────────────
263
+
264
+ function addUsage(base: AgentUsage, next: Partial<AgentUsage>): AgentUsage {
265
+ const inputTokens = base.inputTokens + (next.inputTokens ?? 0);
266
+ const outputTokens = base.outputTokens + (next.outputTokens ?? 0);
267
+ return {
268
+ inputTokens,
269
+ outputTokens,
270
+ totalTokens: inputTokens + outputTokens,
271
+ requests: base.requests + (next.requests ?? 0),
272
+ };
273
+ }
274
+
275
+ function structuredSuffix(schema: any): string {
276
+ return "\n\nRespond with a single JSON object matching this JSON Schema, and nothing else"
277
+ + " — no markdown fence, no commentary:\n"
278
+ + JSON.stringify(schema, null, 2);
279
+ }
280
+
281
+ /**
282
+ * Attach resolved parts to the turn being asked — the **last user message**, like
283
+ * an email with attachments. A missing user turn gets a new one.
284
+ */
285
+ function attachToTurn(messages: AgentMessage[], parts: AgentMediaPart[]): void {
286
+ const last = messages[messages.length - 1];
287
+
288
+ if (last?.role === "user") {
289
+ const existing = Array.isArray(last.content) ? last.content : [];
290
+ const text = typeof last.content === "string" && last.content ? [{ type: "text" as const, text: last.content }] : [];
291
+ last.content = [...text, ...existing, ...parts];
292
+ return;
293
+ }
294
+
295
+ messages.push({ role: "user", content: parts });
296
+ }
297
+
298
+ /**
299
+ * What a run keeps in its memory: the text verbatim, the heavy parts replaced by
300
+ * a note (`[file report.pdf]`) — a conversation is text, not a file store.
301
+ */
302
+ function lightenForMemory(messages: AgentMessage[]): AgentMessage[] {
303
+ return messages.map((message) => {
304
+ if (!Array.isArray(message.content)) return message;
305
+
306
+ const text = message.content
307
+ .filter((part): part is AgentTextPart => part.type === "text")
308
+ .map((part) => part.text);
309
+ const notes = message.content
310
+ .filter((part) => part.type !== "text")
311
+ .map((part) => forMemory(part).text)
312
+ .filter(Boolean);
313
+
314
+ return { ...message, content: [...text, ...notes].join("\n\n") };
315
+ });
316
+ }
317
+
318
+ /** Read the JSON out of a model answer, tolerating a markdown fence. */
319
+ function extractJson(text: string): { value?: any; error?: string } {
320
+ const trimmed = (text ?? "").trim();
321
+ if (!trimmed) return { error: "the answer was empty" };
322
+ const fenced = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
323
+ const candidate = (fenced ? fenced[1] : trimmed) ?? trimmed;
324
+ try {
325
+ return { value: JSON.parse(candidate) };
326
+ } catch (err: any) {
327
+ return { error: err?.message ?? "invalid JSON" };
328
+ }
329
+ }
330
+
331
+ // ─── Default memory ──────────────────────────────────────────────────────
332
+ // The stores live in `lib/agentMemory.ts` (`agents.memory.InMemory` / `.Mongo`).
333
+
334
+ export { InMemoryAgentMemory };
335
+
336
+ // ─── The agent ───────────────────────────────────────────────────────────
337
+
338
+ /**
339
+ * Give up on a tool after `ms` — the promise is abandoned, not cancelled: a tool
340
+ * that must really stop has to watch `ctx.signal` (which carries the same deadline).
341
+ */
342
+ async function withTimeout<T>(work: Promise<T>, ms: number, name: string): Promise<T> {
343
+ let timer: ReturnType<typeof setTimeout> | undefined;
344
+ try {
345
+ return await Promise.race([
346
+ work,
347
+ new Promise<never>((_resolve, reject) => {
348
+ timer = setTimeout(
349
+ () => reject(new AppError(
350
+ `Tool '${name}' timed out after ${ms}ms`,
351
+ { status: 504, code: "AGENT_TOOL_TIMEOUT" },
352
+ )),
353
+ ms,
354
+ );
355
+ }),
356
+ ]);
357
+ } finally {
358
+ if (timer) clearTimeout(timer);
359
+ }
360
+ }
361
+
362
+ /**
363
+ * The tool calls the conversation has not answered yet — the ones of its last
364
+ * assistant turn that carries calls, minus the `tool` turns that follow. That is
365
+ * exactly what a paused run left behind, and what a resume has to resolve.
366
+ */
367
+ function unansweredToolCalls(messages: AgentMessage[]): AgentToolCall[] {
368
+ for (let index = messages.length - 1; index >= 0; index--) {
369
+ const message = messages[index]!;
370
+ if (message.role !== "assistant" || !message.toolCalls?.length) continue;
371
+ const answered = new Set(
372
+ messages.slice(index + 1)
373
+ .filter((turn): turn is AgentToolMessage => turn.role === "tool" && !!turn.toolCallId)
374
+ .map((turn) => turn.toolCallId),
375
+ );
376
+ return message.toolCalls.filter((call) => !answered.has(call.id));
377
+ }
378
+ return [];
379
+ }
380
+
381
+ /** The `tool` turn a result becomes in the conversation — one shape, one place. */
382
+ function toolTurnFor(result: AgentToolResultEntry): AgentMessage {
383
+ return {
384
+ role: "tool",
385
+ toolCallId: result.id,
386
+ name: result.name,
387
+ content: serializeToolResult(result),
388
+ isError: !!result.error,
389
+ };
390
+ }
391
+
392
+ /**
393
+ * The agents another agent may call, **exposed as tools** — the model decides when
394
+ * to delegate, exactly like any other tool.
395
+ *
396
+ * The sub-agent is built per call, bound to the **calling** `rest`: its own tools then
397
+ * run with the right tenant, session and transaction.
398
+ */
399
+ function subAgentTools(
400
+ declared: AgentConfig | AgentConfig[] | undefined,
401
+ rest: InstanceType<typeof useRest> | undefined,
402
+ tenant: string | undefined,
403
+ ): Array<[string, AgentTool]> {
404
+ const definitions = !declared ? [] : Array.isArray(declared) ? declared : [declared];
405
+ const out: Array<[string, AgentTool]> = [];
406
+
407
+ for (const definition of definitions) {
408
+ const id = definition?.id;
409
+ if (!id || out.some(([name]) => name === id)) continue;
410
+
411
+ out.push([id, {
412
+ id,
413
+ description: definition.description ?? `Delegate to the '${id}' agent.`,
414
+ inputSchema: {
415
+ type: "object",
416
+ properties: {
417
+ input: { type: "string", description: "What to ask the agent." },
418
+ thread: { type: "string", description: "Its memory thread, when the agent has a memory." },
419
+ resource: { type: "string", description: "Its caller identity, when the agent has a memory." },
420
+ },
421
+ required: ["input"],
422
+ additionalProperties: false,
423
+ },
424
+ execute: async ({ input, thread, resource }: any) => {
425
+ const sub = new Agent(definition, { rest, tenant });
426
+ const answer = await sub.generate(input, { thread, resource });
427
+ return answer.text;
428
+ },
429
+ }]);
430
+ }
431
+
432
+ return out;
433
+ }
434
+
435
+ type RunOptions = {
436
+ input: AgentInput;
437
+ call: AgentCallOptions;
438
+ streaming: boolean;
439
+ controller: AbortController;
440
+ emit?: (chunk: AgentStreamChunk) => void;
441
+ };
442
+
443
+ class Agent {
444
+ _isAgent_ = true;
445
+ _tenant_?: string;
446
+
447
+ #config: AgentConfig;
448
+ #tools = new Map<string, AgentTool>();
449
+ #rest?: InstanceType<typeof useRest>;
450
+
451
+ constructor(config: AgentConfig, options: { rest?: InstanceType<typeof useRest>; tenant?: string } = {}) {
452
+ if (!config || typeof config !== "object") {
453
+ throw new AppError("An agent requires a configuration object", { status: 500, code: "AGENT_INVALID" });
454
+ }
455
+ if (!config.instructions) {
456
+ throw new AppError(`Agent '${config.id ?? "?"}' requires \`instructions\``, { status: 500, code: "AGENT_INVALID" });
457
+ }
458
+ if (!config.id) {
459
+ throw new AppError("An agent requires an `id`", { status: 500, code: "AGENT_INVALID" });
460
+ }
461
+ if (!config.description) {
462
+ throw new AppError(`Agent '${config.id}' requires a \`description\``, { status: 500, code: "AGENT_INVALID" });
463
+ }
464
+ if (!config.provider?.model) {
465
+ throw new AppError(`Agent '${config.id}' requires \`provider.model\``, { status: 500, code: "AGENT_INVALID" });
466
+ }
467
+ if (config.workingMemory && !!config.workingMemory.template === !!config.workingMemory.schema) {
468
+ throw new AppError(
469
+ `Agent '${config.id}': \`workingMemory\` needs a \`template\` **or** a \`schema\` — not both, not neither`,
470
+ { status: 500, code: "AGENT_WORKING_MEMORY_INVALID" },
471
+ );
472
+ }
473
+
474
+ this.#config = config;
475
+ this._tenant_ = config._tenant_ ?? options.tenant;
476
+ this.#rest = options.rest;
477
+ if (config.tools) this.setTools(config.tools);
478
+ }
479
+
480
+ static isAgent(value: any): boolean {
481
+ return !!value && value._isAgent_ === true;
482
+ }
483
+
484
+ // ── Identity ─────────────────────────────────────────────────────────
485
+
486
+ getId(): string | undefined {
487
+ return this.#config.id;
488
+ }
489
+
490
+ getName(): string {
491
+ return this.#config.name ?? this.#config.id ?? "agent";
492
+ }
493
+
494
+ getDescription(): string | undefined {
495
+ return this.#config.description;
496
+ }
497
+
498
+ getTenant(): string | undefined {
499
+ return this._tenant_;
500
+ }
501
+
502
+ getConfig(): AgentConfig {
503
+ return this.#config;
504
+ }
505
+
506
+ // ── Provider ─────────────────────────────────────────────────────────
507
+
508
+ /** The provider as declared — the API key is **not** included. */
509
+ getProvider(): AgentProvider {
510
+ const { apiKey, ...rest } = this.#config.provider;
511
+ return { ...rest, options: { ...this.#config.provider.options } };
512
+ }
513
+
514
+ /** Sampling and transport options as declared (`provider.options`). */
515
+ getOptions(): AgentOptions {
516
+ return { ...this.#config.provider.options };
517
+ }
518
+
519
+ /** Merge `provider.options` (`{ temperature, maxTokens, topP, timeout, retries }`). */
520
+ setOptions(options: AgentOptions): this {
521
+ this.#config.provider.options = { ...this.#config.provider.options, ...options };
522
+ return this;
523
+ }
524
+
525
+ getModel(): string {
526
+ return this.#config.provider.model;
527
+ }
528
+
529
+ /** Merge a provider override (a single field is enough — e.g. the model). */
530
+ setProvider(provider: Partial<AgentProvider>): this {
531
+ this.#config.provider = { ...this.#config.provider, ...provider };
532
+ return this;
533
+ }
534
+
535
+ setModel(model: string): this {
536
+ return this.setProvider({ model });
537
+ }
538
+
539
+ // ── Instructions ─────────────────────────────────────────────────────
540
+
541
+ /** The declaration as written (a string, or a function). */
542
+ getInstructions(): AgentInstructions {
543
+ return this.#config.instructions;
544
+ }
545
+
546
+ /** The resolved system prompt for this call. */
547
+ async resolveInstructions(ctx: { thread?: string; resource?: string; threadId?: string; resourceId?: string } = {}): Promise<string> {
548
+ const raw = this.#config.instructions;
549
+ if (typeof raw === "function") {
550
+ // Both spellings are handed over: a prompt written against `threadId`
551
+ // keeps working after the renaming.
552
+ const thread = ctx.thread ?? ctx.threadId;
553
+ const resource = ctx.resource ?? ctx.resourceId;
554
+ return (await raw({ agent: this, rest: this.#rest, thread, resource, threadId: thread, resourceId: resource })) ?? "";
555
+ }
556
+ return raw ?? "";
557
+ }
558
+
559
+ setInstructions(instructions: AgentInstructions): this {
560
+ this.#config.instructions = instructions;
561
+ return this;
562
+ }
563
+
564
+ // ── Tools ────────────────────────────────────────────────────────────
565
+
566
+ /** The declared tools, keyed by name. */
567
+ getTools(): Record<string, AgentTool> {
568
+ return Object.fromEntries(this.#tools);
569
+ }
570
+
571
+ hasTool(name: string): boolean {
572
+ return this.#tools.has(name);
573
+ }
574
+
575
+ /** Replace the tool set. */
576
+ setTools(tools: AgentTools): this {
577
+ const entries = toolEntries(tools);
578
+ for (const [name, tool] of entries) requireHandler(name, tool);
579
+ this.#tools = new Map(entries);
580
+ return this;
581
+ }
582
+
583
+ /** Add or replace one tool — its `id` is the name the model sees. */
584
+ addTool(tool: AgentTool): this {
585
+ const entry = toolEntries([tool])[0]!;
586
+ requireHandler(entry[0], entry[1]);
587
+ this.#tools.set(entry[0], entry[1]);
588
+ return this;
589
+ }
590
+
591
+ removeTool(name: string): boolean {
592
+ return this.#tools.delete(name);
593
+ }
594
+
595
+ // ── Skills ───────────────────────────────────────────────────────────
596
+
597
+ /**
598
+ * The resolved skills, in prompt order (by name). Read-only: they are resolved
599
+ * once at load, from the files `skills` points at — a run reads no file.
600
+ */
601
+ getSkills(): AgentSkill[] {
602
+ return this.#config._skills_ ?? [];
603
+ }
604
+
605
+ // ── Memory ───────────────────────────────────────────────────────────
606
+
607
+ getMemory(): AgentMemory | undefined {
608
+ return this.#config.memory;
609
+ }
610
+
611
+ // ── Working memory ───────────────────────────────────────────────────
612
+
613
+ /**
614
+ * The key the working memory is stored under: the caller (`'resource'`, the
615
+ * default) or the conversation (`'thread'`). `undefined` when the run has
616
+ * nothing to scope it with — the scratchpad is then simply not used.
617
+ */
618
+ #workingMemoryKey(ctx: { thread?: string; resource?: string } = {}): string | undefined {
619
+ const config = this.#config.workingMemory;
620
+ if (!config) return undefined;
621
+
622
+ if ((config.scope ?? "resource") === "thread") {
623
+ return ctx.thread ? (ctx.resource ? `thread:${ctx.resource}:${ctx.thread}` : `thread:${ctx.thread}`) : undefined;
624
+ }
625
+ return ctx.resource ? `resource:${ctx.resource}` : undefined;
626
+ }
627
+
628
+ /** The store of the scratchpad, when it has one. */
629
+ #stateStore(): AgentMemory & Required<Pick<AgentMemory, "getState" | "setState">> {
630
+ const memory = this.#config.memory;
631
+ if (!memory) {
632
+ throw new AppError(`Agent '${this.getId() ?? "?"}' declares no memory`, {
633
+ status: 400, code: "AGENT_NO_MEMORY",
634
+ });
635
+ }
636
+ if (typeof memory.getState !== "function" || typeof memory.setState !== "function") {
637
+ throw new AppError(
638
+ `The memory of agent '${this.getId() ?? "?"}' does not store a working memory`,
639
+ { status: 400, code: "AGENT_WORKING_MEMORY_UNSUPPORTED" },
640
+ );
641
+ }
642
+ return memory as AgentMemory & Required<Pick<AgentMemory, "getState" | "setState">>;
643
+ }
644
+
645
+ /** The scratchpad value must fit the declared schema, whoever wrote it. */
646
+ async #validateWorkingMemory(value: any): Promise<void> {
647
+ const schema = this.#config.workingMemory?.schema;
648
+ if (!schema) return;
649
+
650
+ const { error } = await validateWithSchema(schema, value);
651
+ if (error) {
652
+ throw new AppError(`Invalid working memory: ${error}`, {
653
+ status: 400, code: "AGENT_WORKING_MEMORY_VALUE_INVALID",
654
+ });
655
+ }
656
+ }
657
+
658
+ /**
659
+ * Read the agent's scratchpad — the profile it keeps about a caller, or the
660
+ * state of a conversation (`workingMemory.scope`).
661
+ */
662
+ async getWorkingMemory(ctx: { thread?: string; resource?: string } = {}): Promise<any> {
663
+ const key = this.#workingMemoryKey(ctx);
664
+ if (!key) return undefined;
665
+ const state = await this.#stateStore().getState(key, this.#memoryCtx(ctx.resource));
666
+ return state?.value;
667
+ }
668
+
669
+ /**
670
+ * Write the scratchpad **without calling the model** — seed a profile, or
671
+ * correct it from your own code.
672
+ *
673
+ * ```ts
674
+ * await support.setWorkingMemory('# Profile\n- Name: Sam\n- Timezone: CET', {
675
+ * resource: 'user-42',
676
+ * });
677
+ * ```
678
+ *
679
+ * With a `schema`, the value is validated and **merged** into what is stored
680
+ * (`null` removes a key); with a `template`, it **replaces** the block.
681
+ */
682
+ async setWorkingMemory(value: any, ctx: { thread?: string; resource?: string } = {}): Promise<any> {
683
+ const memory = this.#stateStore(); // `AGENT_NO_MEMORY` / unsupported first: nothing to write into
684
+ const config = this.#config.workingMemory;
685
+ if (!config) {
686
+ throw new AppError(`Agent '${this.getId() ?? "?"}' declares no \`workingMemory\``, {
687
+ status: 400, code: "AGENT_WORKING_MEMORY_DISABLED",
688
+ });
689
+ }
690
+
691
+ const key = this.#workingMemoryKey(ctx);
692
+ if (!key) {
693
+ throw new AppError(
694
+ `A \`${config.scope ?? "resource"}\`-scoped working memory needs \`${(config.scope ?? "resource") === "thread" ? "thread" : "resource"}\``,
695
+ { status: 400, code: "AGENT_WORKING_MEMORY_SCOPE_REQUIRED" },
696
+ );
697
+ }
698
+
699
+ const context = this.#memoryCtx(ctx.resource);
700
+ let next = value;
701
+
702
+ if (config.schema) {
703
+ const current = (await memory.getState(key, context))?.value;
704
+ next = mergeWorkingMemory(current, value);
705
+ await this.#validateWorkingMemory(next);
706
+ }
707
+
708
+ await memory.setState(key, { value: next, updatedAt: new Date() }, context);
709
+ return next;
710
+ }
711
+
712
+ /** Forget a scratchpad. */
713
+ async clearWorkingMemory(ctx: { thread?: string; resource?: string } = {}): Promise<void> {
714
+ const key = this.#workingMemoryKey(ctx);
715
+ if (!key) return;
716
+ const memory = this.#stateStore();
717
+ await memory.clearState?.(key, this.#memoryCtx(ctx.resource));
718
+ }
719
+
720
+ setMemory(memory?: AgentMemory): this {
721
+ this.#config.memory = memory;
722
+ return this;
723
+ }
724
+
725
+ /** What a store needs to know — the client, the tenant, the caller. */
726
+ #memoryCtx(resourceId?: string, threadId?: string): AgentMemoryContext {
727
+ return {
728
+ agentId: this.#config.id,
729
+ tenant: this._tenant_,
730
+ rest: this.#rest,
731
+ resourceId,
732
+ // The store and the thread are on the **run** context only: that is what a
733
+ // processor needs to cache something per thread
734
+ ...(threadId ? { thread: threadId } : {}),
735
+ ...(this.#config.memory ? { memory: this.#config.memory } : {}),
736
+ };
737
+ }
738
+
739
+ /** The stored conversation of a thread (`[]` when there is no memory). */
740
+ async getMessages(thread: string, ctx?: { resource?: string; resourceId?: string }): Promise<AgentMessage[]> {
741
+ if (!this.#config.memory) return [];
742
+ const resource = ctx?.resource ?? ctx?.resourceId;
743
+ return [...(await this.#config.memory.get(thread, this.#memoryCtx(resource)))];
744
+ }
745
+
746
+ /**
747
+ * The tool the model uses to keep its scratchpad up to date. Template mode
748
+ * replaces the block; schema mode takes a **partial** object (every field
749
+ * optional) and merges it.
750
+ */
751
+ async #workingMemoryTool(stateKey: string, ctx: AgentMemoryContext): Promise<AgentTool> {
752
+ const config = this.#config.workingMemory!;
753
+ const memory = this.#config.memory!;
754
+
755
+ // Schema mode → patch semantics: what the model omits is left as it is
756
+ let inputSchema: any = {
757
+ type: "object",
758
+ properties: {
759
+ content: {
760
+ type: "string",
761
+ description: "The complete working memory block, as markdown — what should be kept from now on.",
762
+ },
763
+ },
764
+ required: ["content"],
765
+ };
766
+
767
+ if (config.schema) {
768
+ inputSchema = { ...(await toJsonSchema(config.schema)) };
769
+ delete inputSchema.required;
770
+ }
771
+
772
+ return {
773
+ id: WORKING_MEMORY_TOOL,
774
+ description: config.schema
775
+ ? "Update the working memory with the fields you learned. Send only the fields to change; send null to remove one."
776
+ : "Save the working memory: the profile and the durable facts worth keeping. Send the whole block, updated.",
777
+ inputSchema,
778
+ execute: async (input: any) => {
779
+ const current = (await memory.getState!(stateKey, ctx))?.value;
780
+ const next = config.schema
781
+ ? mergeWorkingMemory(current, input)
782
+ : (input?.content ?? "");
783
+
784
+ // A patch that breaks the schema is reported to the model — the run goes on
785
+ if (config.schema) await this.#validateWorkingMemory(next);
786
+
787
+ await memory.setState!(stateKey, { value: next, updatedAt: new Date() }, ctx);
788
+ return { ok: true };
789
+ },
790
+ };
791
+ }
792
+
793
+ /**
794
+ * Add turns to a stored thread **without calling the model**.
795
+ *
796
+ * A store replaces a thread on `save()`, so this reads it, appends and writes
797
+ * it back — the `maxMessages` cap and the caller namespace still apply, and the
798
+ * heavy payloads of an attachment are stored as a note, exactly like a run
799
+ * does. Use it to seed a conversation, to import a history from elsewhere, or
800
+ * to persist what happened outside the agent.
801
+ *
802
+ * ```ts
803
+ * await agent.remember('chat-1', [
804
+ * { role: 'user', content: 'Bonjour' },
805
+ * { role: 'assistant', content: 'Bonjour ! Comment puis-je aider ?' },
806
+ * ]);
807
+ * ```
808
+ *
809
+ * @returns the whole stored thread, as it now is.
810
+ */
811
+ async remember(
812
+ thread: string,
813
+ messages: AgentInput,
814
+ ctx?: { resource?: string; resourceId?: string },
815
+ ): Promise<AgentMessage[]> {
816
+ const memory = this.#config.memory;
817
+ if (!memory) {
818
+ throw new AppError(`Agent '${this.getId() ?? "?"}' declares no memory`, {
819
+ status: 400, code: "AGENT_NO_MEMORY",
820
+ });
821
+ }
822
+
823
+ const context = this.#memoryCtx(ctx?.resource ?? ctx?.resourceId);
824
+ const existing = [...(await memory.get(thread, context))];
825
+ const next = [...existing, ...lightenForMemory(await normalizeMessages(messages))];
826
+ await memory.save(thread, next, context);
827
+ return next;
828
+ }
829
+
830
+ async clearMessages(thread: string, ctx?: { resource?: string; resourceId?: string }): Promise<void> {
831
+ const resource = ctx?.resource ?? ctx?.resourceId;
832
+ await this.#config.memory?.clear(thread, this.#memoryCtx(resource));
833
+ }
834
+
835
+ /**
836
+ * Every thread of a caller — only for a store that supports it
837
+ * (`agents.memory.Mongo().list()`); `[]` otherwise.
838
+ */
839
+ async listThreads(ctx?: { resource?: string; resourceId?: string; limit?: number }): Promise<AgentThread[]> {
840
+ const memory = this.#config.memory;
841
+ if (!memory?.list) return [];
842
+ const resource = ctx?.resource ?? ctx?.resourceId;
843
+ return [...(await memory.list({ ...this.#memoryCtx(resource), limit: ctx?.limit }))];
844
+ }
845
+
846
+ /** Forget a thread — or the whole in-process memory when no thread is given. */
847
+ async reset(thread?: string): Promise<void> {
848
+ if (thread) return this.clearMessages(thread);
849
+ this.#config.memory = new InMemoryAgentMemory();
850
+ }
851
+
852
+ // ── Runtime wiring (used by the loader) ──────────────────────────────
853
+
854
+ setRest(rest: InstanceType<typeof useRest>): this {
855
+ this.#rest = rest;
856
+ return this;
857
+ }
858
+
859
+ getRest(): InstanceType<typeof useRest> | undefined {
860
+ return this.#rest;
861
+ }
862
+
863
+ /** Serializable view — never exposes the API key. */
864
+ toJSON(): Record<string, any> {
865
+ // `protocol` is canonical: the deprecated `compatible` alias is normalized
866
+ // away here, so the API, the SDK and the audit only ever see one field.
867
+ const { compatible, ...provider } = this.getProvider();
868
+ return {
869
+ id: this.#config.id,
870
+ name: this.getName(),
871
+ /** What this prompt is — the version an audit entry carries. */
872
+ version: this.#config.version,
873
+ description: this.#config.description,
874
+ tenant: this._tenant_,
875
+ provider: { ...provider, protocol: this.#config.provider.protocol ?? compatible ?? "openai" },
876
+ instructions: typeof this.#config.instructions === "string" ? this.#config.instructions : "[dynamic]",
877
+ tools: [...this.#tools.keys()],
878
+ skills: this.getSkills().map((skill) => ({ name: skill.name, description: skill.description })),
879
+ maxSteps: this.#config.maxSteps ?? DEFAULT_MAX_STEPS,
880
+ toolChoice: this.#config.toolChoice ?? "auto",
881
+ thinking: this.#config.thinking ?? false,
882
+ memory: !!this.#config.memory,
883
+ };
884
+ }
885
+
886
+ // ── Run ──────────────────────────────────────────────────────────────
887
+
888
+ /**
889
+ * Run the agent to completion and return the final answer.
890
+ * Tool calls are executed in between, until the model answers without
891
+ * asking for a tool (or `maxSteps` is reached).
892
+ */
893
+ async generate<T = any>(input: AgentInput, options: AgentCallOptions = {}): Promise<AgentGenerateResult<T>> {
894
+ return this.#run<T>({ input, call: options, streaming: false, controller: new AbortController() });
895
+ }
896
+
897
+ /**
898
+ * Same as `generate()`, but the answer is forced into a typed object and
899
+ * validated with the schema (`AGENT_OUTPUT_INVALID` when it stays invalid).
900
+ */
901
+ async generateObject<T = any>(input: AgentInput, options: AgentCallOptions = {}): Promise<T> {
902
+ if (!options.schema && !options.structuredOutput) {
903
+ throw new AppError("`generateObject` requires a `schema` (or `structuredOutput`)", {
904
+ status: 500,
905
+ code: "AGENT_SCHEMA_REQUIRED",
906
+ });
907
+ }
908
+ const result = await this.generate<T>(input, options);
909
+ if (result.object === undefined) {
910
+ throw new AppError(`Agent '${this.getId() ?? "?"}' did not return a valid object`, {
911
+ status: 502,
912
+ code: "AGENT_OUTPUT_INVALID",
913
+ reason: result.text,
914
+ });
915
+ }
916
+ return result.object;
917
+ }
918
+
919
+ /**
920
+ * Run the agent and stream the answer token by token.
921
+ * The promises of the returned object resolve when the **run** is over
922
+ * (tool rounds included) — `await stream.text` is not the last token.
923
+ */
924
+ stream<T = any>(input: AgentInput, options: AgentCallOptions = {}): AgentStreamResult<T> {
925
+ const textQueue = new AsyncQueue<string>();
926
+ const fullQueue = new AsyncQueue<AgentStreamChunk>();
927
+ const controller = new AbortController();
928
+
929
+ const emit = (chunk: AgentStreamChunk) => {
930
+ fullQueue.push(chunk);
931
+ if (chunk.type === "text") textQueue.push(chunk.text);
932
+ };
933
+
934
+ const text = deferred<string>();
935
+ const reasoning = deferred<string | undefined>();
936
+ const object = deferred<T | undefined>();
937
+ const toolCalls = deferred<AgentToolCall[]>();
938
+ const toolResults = deferred<AgentToolResultEntry[]>();
939
+ const steps = deferred<AgentStep[]>();
940
+ const usage = deferred<AgentUsage>();
941
+ const finishReason = deferred<string>();
942
+ const messages = deferred<AgentMessage[]>();
943
+
944
+ const deferreds = [text, reasoning, object, toolCalls, toolResults, steps, usage, finishReason, messages];
945
+
946
+ // The caller may only consume `textStream` — a rejected promise nobody
947
+ // awaits must not become an unhandled rejection.
948
+ for (const pending of deferreds) {
949
+ pending.promise.catch(() => {});
950
+ }
951
+
952
+ void (async () => {
953
+ try {
954
+ const result = await this.#run<T>({ input, call: options, streaming: true, controller, emit });
955
+ text.resolve(result.text);
956
+ reasoning.resolve(result.reasoning);
957
+ object.resolve(result.object);
958
+ toolCalls.resolve(result.toolCalls);
959
+ toolResults.resolve(result.toolResults);
960
+ steps.resolve(result.steps);
961
+ usage.resolve(result.usage);
962
+ finishReason.resolve(result.finishReason);
963
+ messages.resolve(result.messages);
964
+ emit({ type: "finish", finishReason: result.finishReason, usage: result.usage });
965
+ textQueue.close();
966
+ fullQueue.close();
967
+ } catch (err) {
968
+ for (const pending of deferreds) pending.reject(err);
969
+ textQueue.fail(err);
970
+ fullQueue.fail(err);
971
+ }
972
+ })();
973
+
974
+ return {
975
+ textStream: textQueue,
976
+ fullStream: fullQueue,
977
+ text: text.promise,
978
+ /** Everything the model thought, when `thinking` was on — like `generate()`. */
979
+ reasoning: reasoning.promise,
980
+ object: object.promise,
981
+ toolCalls: toolCalls.promise,
982
+ toolResults: toolResults.promise,
983
+ steps: steps.promise,
984
+ usage: usage.promise,
985
+ finishReason: finishReason.promise,
986
+ messages: messages.promise,
987
+ abort: (reason?: string) => controller.abort(reason),
988
+ };
989
+ }
990
+
991
+ // ── Internals ────────────────────────────────────────────────────────
992
+
993
+ /** Provider merged with the call-level override — `options` merged too, not replaced. */
994
+ #provider(override?: Partial<AgentProvider>): AgentProvider {
995
+ const base = this.#config.provider;
996
+ if (!override) return base;
997
+ return {
998
+ ...base,
999
+ ...override,
1000
+ options: { ...base.options, ...override.options },
1001
+ };
1002
+ }
1003
+
1004
+ /**
1005
+ * The tools of one run — `true` (all), `false` (none), an array of **names**
1006
+ * (an allow-list) or extra tools to merge in.
1007
+ *
1008
+ * The declared agents and the **remote MCP servers** are resolved here, before the
1009
+ * allow-list is applied, so every rule below treats them like any other tool.
1010
+ */
1011
+ async #collectTools(requested?: boolean | string[] | AgentTools): Promise<Map<string, AgentTool>> {
1012
+ // `tools: false` — the run answers directly (and no remote is even contacted)
1013
+ if (requested === false) return new Map();
1014
+
1015
+ const tools = new Map(this.#tools);
1016
+
1017
+ // The other agents this one may delegate to
1018
+ for (const [name, tool] of subAgentTools(this.#config.agents, this.#rest, this._tenant_)) {
1019
+ if (!tools.has(name)) tools.set(name, tool);
1020
+ }
1021
+
1022
+ // The remote MCP servers — connected on the first run that needs them, and
1023
+ // memoized per server. A tool the agent declares itself wins over a remote one.
1024
+ for (const tool of await mcpToolsFor(this.#config.mcp)) {
1025
+ const name = tool.id ?? tool.name;
1026
+ if (name && !tools.has(name)) tools.set(name, tool);
1027
+ }
1028
+
1029
+ // `tools: ['forecast', 'echo']` — only these (an unknown name is ignored:
1030
+ // tool availability may legitimately vary between callers)
1031
+ if (Array.isArray(requested) && requested.every((entry) => typeof entry === "string")) {
1032
+ const wanted = new Set(requested as string[]);
1033
+ for (const name of [...tools.keys()]) {
1034
+ if (!wanted.has(name)) tools.delete(name);
1035
+ }
1036
+ return tools;
1037
+ }
1038
+
1039
+ // `tools: true` — everything the agent declares; a record/array of tools merges in
1040
+ if (requested && requested !== true) {
1041
+ for (const [name, tool] of toolEntries(requested as AgentTools)) {
1042
+ requireHandler(name, tool);
1043
+ tools.set(name, tool);
1044
+ }
1045
+ }
1046
+
1047
+ return tools;
1048
+ }
1049
+
1050
+ async #toolSpecs(tools: Map<string, AgentTool>): Promise<AgentToolSpec[]> {
1051
+ const specs: AgentToolSpec[] = [];
1052
+ for (const [name, tool] of tools) {
1053
+ if (tool.enabled === false) continue;
1054
+ specs.push({
1055
+ name,
1056
+ description: tool.description,
1057
+ parameters: await toJsonSchema(tool.inputSchema),
1058
+ });
1059
+ }
1060
+ return specs;
1061
+ }
1062
+
1063
+ /** One model call, without streaming. */
1064
+ async #chat(adapter: ModelAdapter, request: ModelChatRequest): Promise<{
1065
+ text: string;
1066
+ reasoning?: string;
1067
+ thinking?: AgentThinkingBlock[];
1068
+ toolCalls: AgentToolCall[];
1069
+ finishReason: string;
1070
+ usage: AgentUsage;
1071
+ }> {
1072
+ const response = await adapter.chat(request);
1073
+ return {
1074
+ text: response.text,
1075
+ ...(response.reasoning ? { reasoning: response.reasoning } : {}),
1076
+ ...(response.thinking?.length ? { thinking: response.thinking } : {}),
1077
+ toolCalls: response.toolCalls.map(toToolCall),
1078
+ finishReason: response.finishReason,
1079
+ usage: response.usage,
1080
+ };
1081
+ }
1082
+
1083
+ /** One model call, streaming — the deltas are reassembled into a full turn. */
1084
+ async #chatStream(adapter: ModelAdapter, request: ModelChatRequest, emit: (chunk: AgentStreamChunk) => void): Promise<{
1085
+ text: string;
1086
+ reasoning?: string;
1087
+ thinking?: AgentThinkingBlock[];
1088
+ toolCalls: AgentToolCall[];
1089
+ finishReason: string;
1090
+ usage: AgentUsage;
1091
+ }> {
1092
+ const partial = new Map<number, { id?: string; name?: string; argsText?: string }>();
1093
+ let text = "";
1094
+ let reasoning = "";
1095
+ let thinking: AgentThinkingBlock[] | undefined;
1096
+ let finishReason = "stop";
1097
+ let usage: Partial<AgentUsage> = {};
1098
+
1099
+ for await (const event of adapter.chatStream(request)) {
1100
+ switch (event.type) {
1101
+ case "text":
1102
+ text += event.text;
1103
+ emit({ type: "text", text: event.text });
1104
+ break;
1105
+ case "reasoning":
1106
+ reasoning += event.text;
1107
+ emit({ type: "reasoning", text: event.text });
1108
+ break;
1109
+ case "thinking_blocks":
1110
+ thinking = [...(thinking ?? []), ...event.blocks];
1111
+ break;
1112
+ case "tool_call": {
1113
+ const current = partial.get(event.index) ?? {};
1114
+ if (event.id) current.id = event.id;
1115
+ if (event.name) current.name = event.name;
1116
+ if (event.argsText) current.argsText = (current.argsText ?? "") + event.argsText;
1117
+ partial.set(event.index, current);
1118
+ break;
1119
+ }
1120
+ case "usage":
1121
+ usage = {
1122
+ inputTokens: event.usage.inputTokens ?? usage.inputTokens,
1123
+ outputTokens: event.usage.outputTokens ?? usage.outputTokens,
1124
+ };
1125
+ break;
1126
+ case "finish":
1127
+ finishReason = event.finishReason;
1128
+ break;
1129
+ }
1130
+ }
1131
+
1132
+ const toolCalls = [...partial.entries()]
1133
+ .sort((a, b) => a[0] - b[0])
1134
+ .map(([index, call]) => toToolCall({
1135
+ id: call.id ?? `call_${index}`,
1136
+ name: call.name ?? "unknown",
1137
+ argsText: call.argsText ?? "{}",
1138
+ }));
1139
+
1140
+ return {
1141
+ text,
1142
+ ...(reasoning ? { reasoning } : {}),
1143
+ ...(thinking?.length ? { thinking } : {}),
1144
+ toolCalls,
1145
+ finishReason,
1146
+ usage: {
1147
+ inputTokens: usage.inputTokens ?? 0,
1148
+ outputTokens: usage.outputTokens ?? 0,
1149
+ totalTokens: (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0),
1150
+ requests: 1,
1151
+ },
1152
+ };
1153
+ }
1154
+
1155
+ async #executeTool(
1156
+ call: AgentToolCall,
1157
+ tools: Map<string, AgentTool>,
1158
+ step: number,
1159
+ signal: AbortSignal,
1160
+ timeoutMs?: number,
1161
+ ): Promise<AgentToolResultEntry> {
1162
+ const started = performance.now();
1163
+ const entry: AgentToolResultEntry = { id: call.id, name: call.name, args: call.args, durationMs: 0 };
1164
+ const done = (): AgentToolResultEntry => {
1165
+ entry.durationMs = Math.round((performance.now() - started) * 100) / 100;
1166
+ return entry;
1167
+ };
1168
+
1169
+ const tool = tools.get(call.name);
1170
+ if (!tool) {
1171
+ entry.error = `Unknown tool '${call.name}'`;
1172
+ return done();
1173
+ }
1174
+
1175
+ const { value, error } = await validateWithSchema(tool.inputSchema, call.args);
1176
+ if (error) {
1177
+ entry.error = `Invalid arguments for tool '${call.name}': ${error}`;
1178
+ return done();
1179
+ }
1180
+
1181
+ // The deadline an outbound call must respect: the run's signal **and** the
1182
+ // tool's own, so a cooperative tool can abort itself instead of being abandoned
1183
+ const timeout = tool.timeoutMs ?? timeoutMs;
1184
+ const toolSignal = timeout && timeout > 0
1185
+ ? AbortSignal.any([signal, AbortSignal.timeout(timeout)])
1186
+ : signal;
1187
+
1188
+ const ctx: AgentToolContext = {
1189
+ rest: this.#rest,
1190
+ agent: this,
1191
+ tenant: this._tenant_,
1192
+ toolCallId: call.id,
1193
+ step,
1194
+ signal: toolSignal,
1195
+ error: fn.error,
1196
+ };
1197
+
1198
+ try {
1199
+ const work: Promise<any> = tool.execute
1200
+ ? tool.execute(value, ctx)
1201
+ // MCP tools take the framework's own context shape
1202
+ : tool.exec!({ ...ctx, args: value, c: (this.#rest as any)?.c });
1203
+
1204
+ entry.result = timeout && timeout > 0
1205
+ ? await withTimeout(work, timeout, call.name)
1206
+ : await work;
1207
+ return done();
1208
+ } catch (err: any) {
1209
+ entry.error = err?.message ?? String(err);
1210
+ return done();
1211
+ }
1212
+ }
1213
+
1214
+ async #run<T>(run: RunOptions): Promise<AgentGenerateResult<T>> {
1215
+ const { input, call, streaming, controller } = run;
1216
+ const emit = run.emit;
1217
+
1218
+ const tools = await this.#collectTools(call.tools);
1219
+ const provider = this.#provider(call.provider);
1220
+ const adapter = resolveProvider(provider);
1221
+
1222
+ const structured: AgentStructuredOutput | undefined = call.structuredOutput ?? (call.schema ? { schema: call.schema } : undefined);
1223
+ const schema = structured?.schema;
1224
+ const jsonSchema = schema ? await toJsonSchema(schema) : undefined;
1225
+ // The name a provider accepts is `[a-zA-Z0-9_-]{1,64}` — an agent id is not
1226
+ // always that (`support.billing` is not)
1227
+ const schemaName = (structured?.name ?? this.#config.id ?? "response")
1228
+ .replace(/[^a-zA-Z0-9_-]+/g, "_")
1229
+ .slice(0, 64) || "response";
1230
+
1231
+ // `memory: { thread, resource }` (the Mastra-style shorthand) wins over the
1232
+ // flat `thread` / `resource` options — both name the same thing, and the
1233
+ // `…Id` spellings are still read as aliases.
1234
+ const thread = call.memory?.thread ?? call.memory?.threadId ?? call.thread ?? call.threadId;
1235
+ const resource = call.memory?.resource ?? call.memory?.resourceId ?? call.resource ?? call.resourceId;
1236
+ const signal = call.signal ? AbortSignal.any([call.signal, controller.signal]) : controller.signal;
1237
+
1238
+ // Per-tool deadline and tool parallelism: the call wins over the declaration
1239
+ const toolTimeout = call.toolTimeoutMs ?? this.#config.toolTimeoutMs;
1240
+ const parallel = call.parallelTools ?? this.#config.parallelTools ?? false;
1241
+ const savePartial = call.savePartial ?? this.#config.savePartial ?? false;
1242
+
1243
+ // Resuming a paused run: the conversation is the caller's — the paused step is
1244
+ // resolved from it instead of being asked of the model again
1245
+ const resuming = call.resume ? await normalizeMessages(call.resume.messages) : undefined;
1246
+
1247
+ const history = resuming ?? (thread && this.#config.memory
1248
+ ? [...(await this.#config.memory.get(thread, this.#memoryCtx(resource)))]
1249
+ : []);
1250
+
1251
+ // What the caller wants the run to **know**: injected after the thread
1252
+ // history and before the question, and saved with the rest of the
1253
+ // conversation. A trailing assistant turn is legitimate ("continue from
1254
+ // here"), so the block is not forced to end on a user turn.
1255
+ const injected = !resuming && call.messages ? await normalizeMessages(call.messages) : [];
1256
+
1257
+ // What this run **adds to the thread** — the injected turns and the input,
1258
+ // with the attachments on the turn being asked. The history window and the
1259
+ // `load` processors only shape the prompt: neither reaches the store.
1260
+ // A resumed run adds nothing: its input **is** the conversation it was given.
1261
+ const additions: AgentMessage[] = resuming ? [] : [...injected, ...(await normalizeMessages(input))];
1262
+
1263
+ // Attachments of this run — read now, attached to the turn being asked
1264
+ if (!resuming && call.files) {
1265
+ const parts = await resolveAttachments(call.files, { baseDir: call.filesBaseDir });
1266
+ if (parts.length) attachToTurn(additions, parts);
1267
+ }
1268
+
1269
+ // The history window: the thread is what the store keeps, the window is what
1270
+ // the prompt replays (a cheap bound on the prompt)
1271
+ const window = call.lastMessages ?? this.#config.lastMessages;
1272
+ const replay = window && window > 0 ? trimMessages(history, window) : history;
1273
+
1274
+ const memoryCtx = this.#memoryCtx(resource, thread);
1275
+ let messages: AgentMessage[] = [...replay, ...additions];
1276
+
1277
+ // Memory processors, `load` side: they see exactly what the model will see
1278
+ // (history, injected turns, input and attachments included)
1279
+ messages = await applyProcessors(this.#config.processors, "load", messages, memoryCtx);
1280
+
1281
+ let system = call.instructions ?? await this.resolveInstructions({ thread, resource });
1282
+
1283
+ // The skills are part of the instructions, and they never change: injected
1284
+ // **before** the working memory (which a run rewrites) so the prefix of the
1285
+ // prompt stays stable — and cacheable — across calls
1286
+ const skills = this.#config._skills_;
1287
+ if (skills?.length) system += `\n\n${skillsBlock(skills)}`;
1288
+
1289
+ // The scratchpad is part of the prompt, right after the instructions
1290
+ const stateKey = this.#workingMemoryKey({ thread, resource });
1291
+ const working = this.#config.workingMemory;
1292
+ if (working && stateKey && typeof this.#config.memory?.getState === "function") {
1293
+ const state = await this.#config.memory.getState(stateKey, memoryCtx);
1294
+ system += `\n\n${workingMemoryBlock(working, state?.value)}`;
1295
+ }
1296
+
1297
+ if (jsonSchema) system += structuredSuffix(jsonSchema);
1298
+
1299
+ const maxSteps = call.maxSteps ?? this.#config.maxSteps ?? DEFAULT_MAX_STEPS;
1300
+ // Reasoning defaults to the declaration, like `toolChoice` — a per-call
1301
+ // `thinking` wins, and `false` turns it off for one run
1302
+ const thinking = call.thinking ?? this.#config.thinking;
1303
+ const steps: AgentStep[] = [];
1304
+ const allToolCalls: AgentToolCall[] = [];
1305
+ const allToolResults: AgentToolResultEntry[] = [];
1306
+ let usage: AgentUsage = { ...EMPTY_USAGE };
1307
+ let text = "";
1308
+ let reasoning = "";
1309
+ let finishReason = "stop";
1310
+
1311
+ // What the model may use, and how — the framework picks no tool itself
1312
+ const toolChoice = call.toolChoice ?? this.#config.toolChoice ?? "auto";
1313
+
1314
+ // The working-memory tool is part of the run's toolbox: it disappears with
1315
+ // `tools: false` and with an allow-list, and a tool the tenant declared under
1316
+ // the same id wins (nothing is overridden behind their back)
1317
+ const narrowed = Array.isArray(call.tools) || call.tools === false;
1318
+ if (working && stateKey && working.tool !== false && !call.memory?.readOnly && !narrowed && !tools.has(WORKING_MEMORY_TOOL)) {
1319
+ tools.set(WORKING_MEMORY_TOOL, await this.#workingMemoryTool(stateKey, memoryCtx));
1320
+ }
1321
+
1322
+ const specs = await this.#toolSpecs(tools);
1323
+ const offeredIds = [...tools.keys()];
1324
+
1325
+ // A paused run left tool calls unanswered: they are resolved here, from the
1326
+ // caller's decisions, and the loop then continues from the completed step —
1327
+ // the model is never asked again for the step it already produced
1328
+ const pending: AgentPendingApproval[] = [];
1329
+ if (resuming) {
1330
+ for (const toolCall of unansweredToolCalls(messages)) {
1331
+ const tool = tools.get(toolCall.name);
1332
+ const decision = call.resume!.decisions?.[toolCall.id];
1333
+ // Undecided: the run pauses again, and nothing runs
1334
+ if (tool?.approval && decision === undefined) {
1335
+ pending.push({ toolCallId: toolCall.id, name: toolCall.name, args: toolCall.args });
1336
+ continue;
1337
+ }
1338
+ const result: AgentToolResultEntry = decision === false
1339
+ ? { id: toolCall.id, name: toolCall.name, args: toolCall.args, durationMs: 0, error: "Refused by the caller" }
1340
+ : await this.#executeTool(toolCall, tools, 0, signal, toolTimeout);
1341
+ const turn = toolTurnFor(result);
1342
+ messages.push(turn);
1343
+ additions.push({ ...turn });
1344
+ allToolCalls.push(toolCall);
1345
+ allToolResults.push(result);
1346
+ emit?.({ type: "tool_result", toolResult: result });
1347
+ }
1348
+ }
1349
+
1350
+ try {
1351
+ // Nothing to ask the model while a decision is pending
1352
+ for (let step = 0; !pending.length && step < maxSteps; step++) {
1353
+ if (signal.aborted) {
1354
+ finishReason = "aborted";
1355
+ break;
1356
+ }
1357
+
1358
+ const request: ModelChatRequest = {
1359
+ system,
1360
+ messages,
1361
+ tools: specs.length ? specs : undefined,
1362
+ toolChoice,
1363
+ thinking,
1364
+ signal,
1365
+ json: !!schema,
1366
+ // What the provider can really enforce: `json_schema` on
1367
+ // OpenAI-compatible gateways, a forced tool on Anthropic
1368
+ ...(jsonSchema
1369
+ ? { jsonSchema: { name: schemaName, ...(structured?.description ? { description: structured.description } : {}), schema: jsonSchema } }
1370
+ : {}),
1371
+ };
1372
+
1373
+ const turn = streaming && emit
1374
+ ? await this.#chatStream(adapter, request, emit)
1375
+ : await this.#chat(adapter, request);
1376
+
1377
+ usage = addUsage(usage, turn.usage);
1378
+ text = turn.text;
1379
+ if (turn.reasoning) reasoning = [reasoning, turn.reasoning].filter(Boolean).join("\n\n");
1380
+
1381
+ const current: AgentStep = {
1382
+ step,
1383
+ text: turn.text,
1384
+ ...(turn.reasoning ? { reasoning: turn.reasoning } : {}),
1385
+ ...(offeredIds.length ? { tools: offeredIds } : {}),
1386
+ toolCalls: turn.toolCalls,
1387
+ toolResults: [],
1388
+ finishReason: turn.finishReason,
1389
+ };
1390
+
1391
+ // The assistant turn is always echoed into the conversation — it
1392
+ // carries the tool call ids the results are attached to, and it is
1393
+ // what a memory replays on the next call.
1394
+ const assistantTurn: AgentMessage = {
1395
+ role: "assistant",
1396
+ content: turn.text || null,
1397
+ ...(turn.toolCalls.length ? { toolCalls: turn.toolCalls } : {}),
1398
+ ...(turn.thinking?.length ? { thinking: turn.thinking } : {}),
1399
+ };
1400
+ messages.push(assistantTurn);
1401
+ additions.push({ ...assistantTurn });
1402
+ allToolCalls.push(...turn.toolCalls);
1403
+
1404
+ if (!turn.toolCalls.length) {
1405
+ finishReason = turn.finishReason;
1406
+ steps.push(current);
1407
+ await call.onStepFinish?.(current);
1408
+ break;
1409
+ }
1410
+
1411
+ // A tool that needs a decision stops the run **before anything runs**: a
1412
+ // step is all-or-nothing, because every call of an assistant turn must be
1413
+ // answered before the conversation can go back to the model
1414
+ const waiting = turn.toolCalls.filter(
1415
+ (toolCall) => tools.get(toolCall.name)?.approval && call.resume?.decisions?.[toolCall.id] === undefined,
1416
+ );
1417
+ if (waiting.length) {
1418
+ pending.push(...waiting.map((toolCall) => ({
1419
+ toolCallId: toolCall.id,
1420
+ name: toolCall.name,
1421
+ args: toolCall.args,
1422
+ })));
1423
+ break;
1424
+ }
1425
+
1426
+ // The tools of one step: sequential by default (two writes must not
1427
+ // interleave), together when the caller asks for it. The results are
1428
+ // attached in the model's order either way.
1429
+ const entries: AgentToolResultEntry[] = [];
1430
+ if (parallel) {
1431
+ entries.push(...await Promise.all(
1432
+ turn.toolCalls.map((toolCall) => this.#executeTool(toolCall, tools, step, signal, toolTimeout)),
1433
+ ));
1434
+ } else {
1435
+ for (const toolCall of turn.toolCalls) {
1436
+ entries.push(await this.#executeTool(toolCall, tools, step, signal, toolTimeout));
1437
+ }
1438
+ }
1439
+
1440
+ turn.toolCalls.forEach((toolCall, index) => {
1441
+ const result = entries[index]!;
1442
+ current.toolResults.push(result);
1443
+ allToolResults.push(result);
1444
+ const toolTurn = toolTurnFor(result);
1445
+ messages.push(toolTurn);
1446
+ additions.push({ ...toolTurn });
1447
+ emit?.({ type: "tool_result", toolResult: result });
1448
+ });
1449
+
1450
+ steps.push(current);
1451
+ await call.onStepFinish?.(current);
1452
+ emit?.({ type: "step", step: current });
1453
+ finishReason = turn.finishReason;
1454
+ if (step === maxSteps - 1) finishReason = "max_steps";
1455
+ }
1456
+ } catch (err: any) {
1457
+ if (signal.aborted) {
1458
+ finishReason = "aborted";
1459
+ } else {
1460
+ // Keep what the run produced before it died: the tokens are paid, and a
1461
+ // run that leaves no trace makes the caller pay them twice
1462
+ if (savePartial && thread && this.#config.memory && !call.memory?.readOnly) {
1463
+ await this.#persist(thread, [...history, ...additions], memoryCtx).catch(() => {});
1464
+ }
1465
+ throw err;
1466
+ }
1467
+ }
1468
+
1469
+ // A run that paused for a decision has no answer to validate
1470
+ if (pending.length) finishReason = "approval_required";
1471
+
1472
+ // Structured output — one repair turn, then give up with a stable code.
1473
+ let object: T | undefined;
1474
+ if (schema && !pending.length) {
1475
+ const attempt = extractJson(text);
1476
+ let candidate: any = attempt.value;
1477
+ let failure = attempt.error;
1478
+ if (!failure) {
1479
+ const validated = await validateWithSchema(schema, candidate);
1480
+ if (validated.error) failure = validated.error;
1481
+ else candidate = validated.value;
1482
+ }
1483
+
1484
+ if (failure && text.trim()) {
1485
+ // One repair turn — `messages` already ends with the rejected answer.
1486
+ const repair = [...messages, {
1487
+ role: "user" as const,
1488
+ content: `That answer was rejected: ${failure}. Reply with a single valid JSON object matching the schema — no markdown, no commentary.`,
1489
+ }];
1490
+ const turn = await this.#chat(adapter, {
1491
+ system,
1492
+ messages: repair,
1493
+ temperature: 0,
1494
+ signal,
1495
+ json: true,
1496
+ ...(jsonSchema ? { jsonSchema: { name: schemaName, schema: jsonSchema } } : {}),
1497
+ });
1498
+
1499
+ usage = addUsage(usage, turn.usage);
1500
+ text = turn.text;
1501
+ messages.push({ role: "assistant", content: text });
1502
+ additions.push({ role: "assistant", content: text });
1503
+
1504
+ const retry = extractJson(text);
1505
+ failure = retry.error;
1506
+ if (!failure) {
1507
+ const validated = await validateWithSchema(schema, retry.value);
1508
+ if (validated.error) failure = validated.error;
1509
+ else candidate = validated.value;
1510
+ }
1511
+ }
1512
+
1513
+ if (failure) {
1514
+ throw new AppError(`Agent '${this.#config.id ?? "?"}' did not return valid structured output: ${failure}`, {
1515
+ status: 502,
1516
+ code: "AGENT_OUTPUT_INVALID",
1517
+ reason: text,
1518
+ });
1519
+ }
1520
+ object = candidate as T;
1521
+ }
1522
+
1523
+ // A paused run stores **nothing**: the caller resumes with the conversation it
1524
+ // was handed back, and an assistant turn with unanswered calls would poison the
1525
+ // thread for the protocol
1526
+ if (thread && this.#config.memory && !call.memory?.readOnly && !pending.length) {
1527
+ try {
1528
+ await this.#persist(thread, [...history, ...additions], memoryCtx);
1529
+ } catch (err: any) {
1530
+ console.error(`The conversation of thread '${thread}' was not saved: ${err?.message}`);
1531
+ }
1532
+ }
1533
+
1534
+ return {
1535
+ text,
1536
+ ...(object !== undefined ? { object } : {}),
1537
+ ...(reasoning ? { reasoning } : {}),
1538
+ toolCalls: allToolCalls,
1539
+ toolResults: allToolResults,
1540
+ steps,
1541
+ usage,
1542
+ finishReason,
1543
+ messages: [...history, ...additions],
1544
+ ...(pending.length ? { pendingApprovals: pending } : {}),
1545
+ };
1546
+ }
1547
+
1548
+ /**
1549
+ * Store the thread. The `save` processors run last — they see what the run
1550
+ * produced, and a throw keeps the whole conversation out of the store (that is
1551
+ * the documented guardrail).
1552
+ */
1553
+ async #persist(thread: string, messages: AgentMessage[], ctx: AgentMemoryContext): Promise<void> {
1554
+ const stored = await applyProcessors(this.#config.processors, "save", messages, ctx);
1555
+ await this.#config.memory!.save(thread, lightenForMemory(stored), ctx);
1556
+ }
1557
+ }
1558
+
1559
+ /**
1560
+ * Declares an agent — the object is the config; the loader instantiates it.
1561
+ * (Also available as `define.Agent`.)
1562
+ */
1563
+ function agent(config: AgentConfig): AgentConfig & { _isAgent_: true } {
1564
+ return { ...config, enabled: config.enabled ?? true, _isAgent_: true };
1565
+ }
1566
+
1567
+ export { Agent, agent };