@anteros/core 0.0.40 → 0.0.42

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