@anteros/core 0.0.44 → 0.0.47

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 (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -1
  3. package/database/field.crypto.ts +38 -13
  4. package/database/mongodbadapter.ts +85 -25
  5. package/database/vars.ts +22 -2
  6. package/index.ts +22 -0
  7. package/lib/agent.ts +707 -79
  8. package/lib/agentMemory.ts +199 -0
  9. package/lib/agents.ts +133 -52
  10. package/lib/attachment.ts +24 -3
  11. package/lib/define.ts +5 -1
  12. package/lib/eval.ts +123 -0
  13. package/lib/files.ts +21 -15
  14. package/lib/jsonSchema.ts +8 -0
  15. package/lib/mcp.ts +52 -30
  16. package/lib/messages.ts +202 -19
  17. package/lib/partialJson.ts +79 -0
  18. package/lib/providers.ts +26 -2
  19. package/lib/redact.ts +11 -0
  20. package/lib/tracing.ts +20 -0
  21. package/lib/zip.ts +23 -3
  22. package/package.json +4 -5
  23. package/server/agents.ts +66 -1
  24. package/server/api.ts +302 -20
  25. package/server/boot.ts +13 -4
  26. package/server/hono.ts +146 -6
  27. package/tests/access-log-body.test.ts +253 -0
  28. package/tests/agent-checkpoint.test.ts +195 -0
  29. package/tests/agent-hardening.test.ts +238 -0
  30. package/tests/agent-memory.test.ts +175 -1
  31. package/tests/agent-stream-advanced.test.ts +246 -0
  32. package/tests/agent-tracing-eval.test.ts +219 -0
  33. package/tests/agent-working-memory.test.ts +7 -4
  34. package/tests/agent.test.ts +54 -3
  35. package/tests/agents-api.test.ts +21 -0
  36. package/tests/api-external-guards.test.ts +304 -0
  37. package/tests/attachment.test.ts +63 -0
  38. package/tests/audit-redact.test.ts +26 -1
  39. package/tests/field-encryption.test.ts +47 -3
  40. package/tests/filters-hardening.test.ts +81 -0
  41. package/tests/fixtures/ai-tenant/ai/agents/support/billing.agent.ts +9 -0
  42. package/tests/fixtures/ai-tenant/ai/agents/support/billing.tool.ts +9 -0
  43. package/tests/fixtures/ai-tenant/ai/agents/support.agent.ts +9 -0
  44. package/tests/fixtures/ai-tenant/ai/agents/support.tool.ts +9 -0
  45. package/tests/fixtures/ai-tenant/ai/mcp/resources/readme.resource.ts +9 -0
  46. package/tests/fixtures/ai-tenant/ai/mcp/tools/echo.tool.ts +9 -0
  47. package/tests/fixtures/fake-provider.ts +33 -0
  48. package/tests/fixtures/tool-tenant/ai/agents/report.agent.ts +12 -0
  49. package/tests/fixtures/tool-tenant/ai/agents/report.tool.ts +12 -0
  50. package/tests/fixtures/vars-tenant/vars/open.var.ts +18 -0
  51. package/tests/hono.test.ts +81 -1
  52. package/tests/jwt-tenant.test.ts +165 -0
  53. package/tests/logger.test.ts +62 -10
  54. package/tests/messages.test.ts +88 -32
  55. package/tests/tool-runtime.test.ts +291 -0
  56. package/tests/tool-types.ts +33 -0
  57. package/tests/upload.test.ts +175 -3
  58. package/tests/vars-api.test.ts +73 -1
  59. package/tests/vars.test.ts +1 -1
  60. package/types/agent.d.ts +386 -4
  61. package/types/config.d.ts +56 -2
  62. package/types/file.d.ts +1 -1
  63. package/types/mcp.d.ts +2 -2
  64. package/utils/func.ts +20 -0
  65. package/utils/logger.ts +125 -33
package/types/agent.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type Joi from "joi";
2
+ import type { z } from "zod";
2
3
  import type { useRest } from "../database/rest";
3
4
  import type { fn } from "../lib/error";
4
5
  import type { jwt } from "../utils/func";
@@ -55,6 +56,75 @@ export type AgentProvider = {
55
56
  headers?: Record<string, string>;
56
57
  /** Sampling and transport — `{ temperature, maxTokens, topP, timeout, retries }`. */
57
58
  options?: AgentOptions;
59
+ /**
60
+ * Tried **in order** when the primary fails for a reason another endpoint may not
61
+ * have: `429`, `5xx`, a network error or a timeout. `options` are merged over the
62
+ * primary's, so a fallback only states what differs.
63
+ */
64
+ fallbacks?: AgentFallback[];
65
+ /**
66
+ * USD per **million** tokens — turns `usage` into `usage.cost`. A fallback may
67
+ * price differently: the model that answered is the one billed.
68
+ */
69
+ pricing?: AgentPricing;
70
+ };
71
+
72
+ /** One step of `provider.fallbacks` — what may differ from the primary. */
73
+ export type AgentFallback = {
74
+ model: string;
75
+ apiKey?: string;
76
+ protocol?: AgentProtocol;
77
+ baseUrl?: string;
78
+ headers?: Record<string, string>;
79
+ options?: AgentOptions;
80
+ pricing?: AgentPricing;
81
+ };
82
+
83
+ /** Cost of a model, per million tokens (USD). */
84
+ export type AgentPricing = {
85
+ /** USD per million **input** tokens. */
86
+ input: number;
87
+ /** USD per million **output** tokens. */
88
+ output: number;
89
+ };
90
+
91
+ /**
92
+ * A guardrail — a check that can **stop** a run. `check` throws to block; the
93
+ * thrown message is what the caller reads.
94
+ */
95
+ export type AgentGuardrail = {
96
+ /** Named in the error, so a failing check is identifiable. */
97
+ name: string;
98
+ check: (value: string, ctx: AgentGuardrailContext) => void | Promise<void>;
99
+ };
100
+
101
+ /** Where a guardrail runs. The lists are additive: declaration first, then the call. */
102
+ export type AgentGuardrails = {
103
+ /** Before the first model call — the caller's prompt (the last user turn). */
104
+ input?: AgentGuardrail[];
105
+ /** Before the answer is returned or stored — what the model produced. */
106
+ output?: AgentGuardrail[];
107
+ /**
108
+ * After a tool **succeeded**, on its serialized result, before the model sees
109
+ * it (a tool result can carry what the prompt may not).
110
+ *
111
+ * A check that throws blocks **that result**, not the run: the model reads
112
+ * `Error [AGENT_GUARDRAIL_BLOCKED]: …` and goes on. A failed tool result is
113
+ * never checked.
114
+ */
115
+ tool?: AgentGuardrail[];
116
+ };
117
+
118
+ export type AgentGuardrailContext = {
119
+ agent: Agent;
120
+ tenant?: string;
121
+ thread?: string;
122
+ resource?: string;
123
+ signal: AbortSignal;
124
+ /** Set when the check runs on a **tool result** — the tool's name. */
125
+ tool?: string;
126
+ /** Set when the check runs on a tool result — the call it answers. */
127
+ toolCallId?: string;
58
128
  };
59
129
 
60
130
  /**
@@ -238,7 +308,13 @@ export type AgentToolMessage = {
238
308
  /** Id of the `tool_use` / `tool_call` this message answers. */
239
309
  toolCallId: string;
240
310
  name: string;
241
- content: string;
311
+ /**
312
+ * What the model reads. Text is the default; a tool may also return **media**
313
+ * (`{ content: [{ type: 'image', … }] }`) — Anthropic receives the image as a
314
+ * `tool_result` block, OpenAI only text plus a marker (its tool message is
315
+ * text-only), and the stored thread keeps a marker, never the base64.
316
+ */
317
+ content: string | AgentMediaPart[];
242
318
  isError?: boolean;
243
319
  };
244
320
 
@@ -291,6 +367,12 @@ export type AgentToolResultEntry = {
291
367
  result?: any;
292
368
  /** Set when the tool threw, was unknown, or its arguments were invalid. */
293
369
  error?: string;
370
+ /**
371
+ * Stable code of that failure — `AGENT_TOOL_FAILED`, `AGENT_TOOL_TIMEOUT`,
372
+ * `AGENT_TOOL_ARGS_INVALID`, `AGENT_TOOL_UNKNOWN`, or the `AppError` code the
373
+ * tool threw. The model reads it as `Error [CODE]: message`, so it can branch.
374
+ */
375
+ errorCode?: string;
294
376
  durationMs: number;
295
377
  };
296
378
 
@@ -303,6 +385,8 @@ export type AgentUsage = {
303
385
  totalTokens: number;
304
386
  /** How many model calls the run needed (1 + one per tool round). */
305
387
  requests: number;
388
+ /** USD, when the provider declares `pricing` (delegated runs included). */
389
+ cost?: number;
306
390
  };
307
391
 
308
392
  /** One model call of a run, with the tools it asked for. */
@@ -364,6 +448,12 @@ export type AgentTool = {
364
448
  /** Shown to the model — say what it does and when to use it. */
365
449
  description?: string;
366
450
  enabled?: boolean;
451
+ /**
452
+ * The tool only **reads** — it may run concurrently with the other read-only
453
+ * tools of the same step, even when `parallelTools` is off. A tool that writes
454
+ * stays sequential: two writes must not interleave.
455
+ */
456
+ readOnly?: boolean;
367
457
  /** Arguments schema: Joi, zod or a plain JSON Schema (defaults to an open object). */
368
458
  inputSchema?: Joi.Schema | any;
369
459
  /** Handler. The returned value is serialized back to the model. */
@@ -387,8 +477,26 @@ export type AgentTool = {
387
477
  approval?: boolean;
388
478
  };
389
479
 
390
- /** What a tool must carry to be **declared** — `define.Tool` enforces the id. */
391
- export type AgentToolDefinition = AgentTool & { id: string };
480
+ /**
481
+ * What a tool must carry to be **declared** — `define.Tool` enforces the `id`
482
+ * **and** the `description`: a tool the model cannot understand is a tool it cannot
483
+ * choose, so both are required at the type level.
484
+ *
485
+ * `TInput` is the handler's input. It is **inferred** from a zod schema
486
+ * (`z.output`); Joi has no type inference, so give it explicitly:
487
+ *
488
+ * ```ts
489
+ * define.Tool({ inputSchema: z.object({ city: z.string() }), execute: ({ city }) => … });
490
+ * define.Tool<{ city: string }>({ inputSchema: v.object({ city: … }), execute: ({ city }) => … });
491
+ * ```
492
+ */
493
+ export type AgentToolDefinition<TInput = any> = Omit<AgentTool, "inputSchema" | "execute"> & {
494
+ id: string;
495
+ description: string;
496
+ /** Joi (`v`), zod, or a plain JSON Schema. */
497
+ inputSchema?: Joi.Schema | z.ZodType | any;
498
+ execute?: (input: TInput, ctx: AgentToolContext) => any | Promise<any>;
499
+ };
392
500
 
393
501
  export type AgentTools = Record<string, AgentTool> | AgentTool[];
394
502
 
@@ -457,6 +565,83 @@ export type AgentMemoryProcessor = {
457
565
  ) => AgentMessage[] | void | Promise<AgentMessage[] | void>;
458
566
  };
459
567
 
568
+ /** One hit returned by the vector search of a semantic-recall processor. */
569
+ export type AgentRecallHit = {
570
+ /** Stable id of the indexed message — the one `upsert` stored. */
571
+ id?: string;
572
+ /** The text to put back in the prompt (the store may expand `messageRange`). */
573
+ text: string;
574
+ role?: string;
575
+ thread?: string;
576
+ resourceId?: string;
577
+ score?: number;
578
+ };
579
+
580
+ /** One message handed to the vector store by the recall processor. */
581
+ export type AgentRecallRecord = {
582
+ id: string;
583
+ text: string;
584
+ vector: number[];
585
+ thread?: string;
586
+ resourceId?: string;
587
+ role?: string;
588
+ };
589
+
590
+ /** What a recall processor asks the vector store — Mastra's `semanticRecall` options. */
591
+ export type AgentRecallQuery = {
592
+ /** Number of similar messages to retrieve. */
593
+ topK: number;
594
+ /** Surrounding messages the store should include with each hit. */
595
+ messageRange: number;
596
+ /** Search the current thread only, or every thread of the caller. */
597
+ scope: "thread" | "resource";
598
+ /** Metadata criteria, applied by the store when it supports them. */
599
+ filter?: Record<string, unknown>;
600
+ /** The run's thread (scoping the search). */
601
+ thread?: string;
602
+ /** The run's caller (namespacing the search). */
603
+ resourceId?: string;
604
+ };
605
+
606
+ /**
607
+ * Options of `agents.memory.semanticRecall()` — the framework's processor-shaped
608
+ * projection of Mastra's `semanticRecall`.
609
+ *
610
+ * Mapping: Mastra's `embedder` → `embed`, `vector.search` → `search`,
611
+ * `vector.upsert` → `upsert`, and `options.semanticRecall` → `topK`,
612
+ * `messageRange`, `scope`, `filter`.
613
+ */
614
+ export type AgentSemanticRecallOptions = {
615
+ /**
616
+ * Embed texts. `kind` separates the two sides of an asymmetric model:
617
+ * `'query'` for the current question, `'passage'` for indexed messages
618
+ * (E5 wants `query:` / `passage:` prefixes).
619
+ */
620
+ embed: (texts: string[], kind: "query" | "passage") => Promise<number[][]> | number[][];
621
+ /**
622
+ * Vector search — return `[]` when nothing is close enough. `ctx` is the run
623
+ * context: a Mongo-backed store reaches the tenant database through `ctx.rest`.
624
+ */
625
+ search: (vector: number[], query: AgentRecallQuery, ctx: AgentMemoryContext) => Promise<AgentRecallHit[]> | AgentRecallHit[];
626
+ /**
627
+ * Index messages. Optional: without it the processor only **recalls** from an
628
+ * index you fill elsewhere (a job, `remember()`, another writer). Should be
629
+ * idempotent — the same `id` may be written again after a restart. `ctx` is
630
+ * the run context, with the same `rest` as `search`.
631
+ */
632
+ upsert?: (records: AgentRecallRecord[], ctx: AgentMemoryContext) => Promise<void> | void;
633
+ /** Similar messages retrieved (default `3`). */
634
+ topK?: number;
635
+ /** Messages around each hit, left to the store (default `2`). */
636
+ messageRange?: number;
637
+ /** `'thread'` (default) or `'resource'` — the caller's other conversations. */
638
+ scope?: "thread" | "resource";
639
+ /** Metadata filter forwarded to the store. */
640
+ filter?: Record<string, unknown>;
641
+ /** How the hits are rendered into the injected system message. */
642
+ format?: (hits: AgentRecallHit[]) => string;
643
+ };
644
+
460
645
  /** What a memory store knows about the run asking for a thread. */
461
646
  export type AgentMemoryContext = {
462
647
  agentId?: string;
@@ -502,6 +687,36 @@ export type AgentMemoryOptions = {
502
687
  scoped?: boolean;
503
688
  };
504
689
 
690
+ /**
691
+ * Options of `agents.memory.custom()` — your own storage behind the `AgentMemory`
692
+ * contract, with the framework's scoping (`scoped`) and message cap
693
+ * (`maxMessages`) applied for you.
694
+ *
695
+ * A handler receives the **resolved key**: `resourceId:threadId` when `scoped`
696
+ * (default) and a caller identity is present, the bare thread id otherwise. The
697
+ * caller's context (`thread`, `resourceId`, `agentId`, `tenant`, `rest`) travels
698
+ * as the second argument, so a handler can key on whatever it needs.
699
+ */
700
+ export type CustomAgentMemoryOptions = AgentMemoryOptions & {
701
+ /** A label for logs and error messages (default `'custom'`). */
702
+ name?: string;
703
+ /** Read a thread — `[]` when it does not exist. */
704
+ get: (key: string, ctx: AgentMemoryContext) => Promise<AgentMessage[]> | AgentMessage[];
705
+ /** Persist a thread — `messages` is already capped to `maxMessages`. */
706
+ save: (key: string, messages: AgentMessage[], ctx: AgentMemoryContext) => Promise<void> | void;
707
+ /** Forget a thread. */
708
+ clear: (key: string, ctx: AgentMemoryContext) => Promise<void> | void;
709
+ /** The caller's threads, most recent first (needed by a chat UI). */
710
+ list?: (ctx: AgentMemoryContext & { limit?: number }) => Promise<AgentThread[]> | AgentThread[];
711
+ /** Working memory — one durable value per scope key. */
712
+ getState?: (key: string, ctx: AgentMemoryContext) => Promise<AgentMemoryState | undefined> | AgentMemoryState | undefined;
713
+ setState?: (key: string, value: AgentMemoryState, ctx: AgentMemoryContext) => Promise<void> | void;
714
+ /** Forget a scratchpad. */
715
+ clearState?: (key: string, ctx: AgentMemoryContext) => Promise<void> | void;
716
+ /** Storage collections to replicate (Mongo-backed stores only). */
717
+ collections?: () => string[];
718
+ };
719
+
505
720
  /**
506
721
  * The agent's **working memory** — a durable scratchpad it keeps up to date
507
722
  * (a user profile, the state of a task):
@@ -632,6 +847,49 @@ export type AgentResume = {
632
847
  decisions: Record<string, boolean>;
633
848
  };
634
849
 
850
+ /**
851
+ * What a run leaves behind at a step boundary — enough to continue it later
852
+ * (`agent.resumeRun()`), whatever killed the process in between.
853
+ *
854
+ * The conversation is **always valid**: an assistant turn with unanswered tool
855
+ * calls is never stored, so replaying it back to the model is safe.
856
+ */
857
+ export type AgentRunState = {
858
+ runId: string;
859
+ agentId?: string;
860
+ /** Completed steps. */
861
+ step: number;
862
+ /** The whole conversation as of the last completed step — always valid (no unanswered tool call). */
863
+ messages: AgentMessage[];
864
+ usage: AgentUsage;
865
+ toolStats: AgentToolStats;
866
+ model?: string;
867
+ finishReason?: string;
868
+ /** ISO timestamp. */
869
+ at: string;
870
+ };
871
+
872
+ /**
873
+ * Durable checkpoint of a run — one state per run id, overwritten at every step
874
+ * boundary and cleared when the run completes.
875
+ *
876
+ * ```ts
877
+ * const checkpoint: AgentRunStore = {
878
+ * load: (runId) => runs.get(runId),
879
+ * save: (runId, state) => { runs.set(runId, state); },
880
+ * clear: (runId) => { runs.delete(runId); },
881
+ * };
882
+ * ```
883
+ *
884
+ * The three methods may be synchronous; a failure of `save` never fails the run
885
+ * (it is logged), and the run id is the caller's identity for that run.
886
+ */
887
+ export type AgentRunStore = {
888
+ load(runId: string): Promise<AgentRunState | undefined> | AgentRunState | undefined;
889
+ save(runId: string, state: AgentRunState): Promise<void> | void;
890
+ clear(runId: string): Promise<void> | void;
891
+ };
892
+
635
893
  export type AgentCallOptions = {
636
894
  /** Thread to read from / write to the memory (same as `memory.thread`). */
637
895
  thread?: string;
@@ -730,6 +988,23 @@ export type AgentCallOptions = {
730
988
  };
731
989
  /** Called after each model call, before its tools run. */
732
990
  onStepFinish?: (step: AgentStep) => void | Promise<void>;
991
+ /**
992
+ * Observations of this run — see `AgentTraceEvent`. Overrides the agent's
993
+ * `onTrace`; a hook that throws is ignored (it never fails the run).
994
+ */
995
+ onTrace?: (event: AgentTraceEvent) => void;
996
+ /**
997
+ * Identifies this run in the trace (`run.start.runId`) — and the key of its
998
+ * durable checkpoint: with a `checkpoint` store, the run saves its state after
999
+ * every completed step under this id, and `agent.resumeRun(runId)` continues it.
1000
+ * No `runId` means no checkpoint (a run without identity cannot be resumed).
1001
+ */
1002
+ runId?: string;
1003
+ /**
1004
+ * Where this run's state is saved after each completed step — overrides the
1005
+ * agent's `checkpoint` for this call. Needs a `runId`.
1006
+ */
1007
+ checkpoint?: AgentRunStore;
733
1008
  /**
734
1009
  * Give up on a single tool after N ms (default: the agent's `toolTimeoutMs`).
735
1010
  * A tool that times out is reported to the model as an error, and the run goes on.
@@ -739,9 +1014,25 @@ export type AgentCallOptions = {
739
1014
  * Run the tools of one step **in parallel** (default `false` — sequential).
740
1015
  *
741
1016
  * Only safe when the tools do not write to the same place: two calls of the same
742
- * tool with the same target can interleave. Read-only tools are the point.
1017
+ * tool with the same target can interleave. Read-only tools (`readOnly: true`)
1018
+ * already run together without this flag.
743
1019
  */
744
1020
  parallelTools?: boolean;
1021
+ /**
1022
+ * How many tool calls of one step may be in flight at once (default `8`).
1023
+ * A model that emits 50 calls in a turn must not open 50 outbound requests.
1024
+ */
1025
+ maxParallelToolCalls?: number;
1026
+ /** Stop cleanly (`finishReason: 'budget'`) once the run used that many tokens. */
1027
+ maxTokens?: number;
1028
+ /** Stop cleanly once the run has lasted that long, in milliseconds. */
1029
+ maxDurationMs?: number;
1030
+ /** Stop cleanly once the run cost that much (USD) — needs `provider.pricing`. */
1031
+ maxCost?: number;
1032
+ /** Prompt budget in **tokens** — the oldest turns are dropped beyond it. */
1033
+ contextTokens?: number;
1034
+ /** Input/output guardrails for this run — added to the declared ones. */
1035
+ guardrails?: AgentGuardrails;
745
1036
  /**
746
1037
  * Store what the run produced even when it **fails** (default `false`).
747
1038
  *
@@ -757,6 +1048,41 @@ export type AgentCallOptions = {
757
1048
  resume?: AgentResume;
758
1049
  };
759
1050
 
1051
+ /** Aggregate of the tool calls of one run — what `result.toolStats` carries. */
1052
+ export type AgentToolStats = {
1053
+ /** Calls executed (unknown tools and invalid arguments included). */
1054
+ calls: number;
1055
+ /** Calls that failed, whatever the cause. */
1056
+ errors: number;
1057
+ /** Failures by code (`AGENT_TOOL_TIMEOUT`, `AGENT_TOOL_UNKNOWN`…). */
1058
+ byCode: Record<string, number>;
1059
+ /** Abandoned at their deadline (`AGENT_TOOL_TIMEOUT`). */
1060
+ timeouts: number;
1061
+ /** Sum of the durations of every call, in milliseconds. */
1062
+ durationMs: number;
1063
+ };
1064
+
1065
+ /**
1066
+ * One milestone of a run, handed to `onTrace` — a **plain** event, not a span.
1067
+ *
1068
+ * `at` is `Date.now()`; turning the events into spans (durations, parents,
1069
+ * attributes) is the caller's job, so no tracing dependency is involved.
1070
+ *
1071
+ * ```ts
1072
+ * const agent = new Agent({
1073
+ * …,
1074
+ * onTrace: (event) => console.log(event.type, event.at),
1075
+ * });
1076
+ * ```
1077
+ */
1078
+ export type AgentTraceEvent =
1079
+ | { type: "run.start"; at: number; agentId?: string; runId?: string; thread?: string; resource?: string }
1080
+ | { type: "step.start"; at: number; step: number }
1081
+ | { type: "model.end"; at: number; step: number; durationMs: number; model: string; usage: AgentUsage; finishReason: string }
1082
+ | { type: "tool.start"; at: number; step: number; tool: string; toolCallId: string }
1083
+ | { type: "tool.end"; at: number; step: number; tool: string; toolCallId: string; durationMs: number; errorCode?: string }
1084
+ | { type: "run.end"; at: number; finishReason: string; durationMs: number; usage: AgentUsage; toolStats: AgentToolStats; error?: string };
1085
+
760
1086
  export type AgentGenerateResult<T = any> = {
761
1087
  text: string;
762
1088
  /** Present when a `schema` / `structuredOutput` was requested. */
@@ -765,11 +1091,17 @@ export type AgentGenerateResult<T = any> = {
765
1091
  reasoning?: string;
766
1092
  toolCalls: AgentToolCall[];
767
1093
  toolResults: AgentToolResultEntry[];
1094
+ /** Aggregate of every tool call of the run — counts, failures, time. */
1095
+ toolStats: AgentToolStats;
768
1096
  steps: AgentStep[];
769
1097
  usage: AgentUsage;
770
1098
  finishReason: string;
771
1099
  /** The complete conversation, ready to be persisted. */
772
1100
  messages: AgentMessage[];
1101
+ /** The model that answered (a fallback may have been used). */
1102
+ model?: string;
1103
+ /** Present when the run stopped on a budget rather than on an answer. */
1104
+ budget?: { limit: "tokens" | "cost" | "duration"; value: number };
773
1105
  /**
774
1106
  * Calls waiting for a decision — present (and `finishReason`
775
1107
  * `'approval_required'`) when a tool declaring `approval: true` was called.
@@ -787,6 +1119,12 @@ export type AgentStreamChunk =
787
1119
  | { type: "tool_call"; toolCall: AgentToolCall }
788
1120
  | { type: "tool_result"; toolResult: AgentToolResultEntry }
789
1121
  | { type: "step"; step: AgentStep }
1122
+ /**
1123
+ * A chunk of a **delegated** run, re-emitted by the parent while the sub-agent
1124
+ * streams — `name` is the child's id. Only emitted when the parent streams;
1125
+ * the child's text never reaches the parent's `textStream`.
1126
+ */
1127
+ | { type: "subagent"; name: string; chunk: AgentStreamChunk }
790
1128
  | { type: "finish"; finishReason: string; usage: AgentUsage };
791
1129
 
792
1130
  /**
@@ -804,6 +1142,12 @@ export type AgentStreamResult<T = any> = {
804
1142
  object: Promise<T | undefined>;
805
1143
  toolCalls: Promise<AgentToolCall[]>;
806
1144
  toolResults: Promise<AgentToolResultEntry[]>;
1145
+ /** Aggregate of every tool call of the run — same as `generate()`. */
1146
+ toolStats: Promise<AgentToolStats>;
1147
+ /** The model that answered (a fallback may have been used). */
1148
+ model: Promise<string | undefined>;
1149
+ /** Present when the run stopped on a budget. */
1150
+ budget: Promise<{ limit: "tokens" | "cost" | "duration"; value: number } | undefined>;
807
1151
  steps: Promise<AgentStep[]>;
808
1152
  usage: Promise<AgentUsage>;
809
1153
  finishReason: Promise<string>;
@@ -812,6 +1156,21 @@ export type AgentStreamResult<T = any> = {
812
1156
  abort: (reason?: string) => void;
813
1157
  };
814
1158
 
1159
+ /**
1160
+ * `agent.streamObject()` result — everything `stream()` returns, plus the
1161
+ * objects parsed from the text as it arrives.
1162
+ *
1163
+ * `partialObjectStream` **consumes** the run's text, so a caller chooses either
1164
+ * `textStream` or `partialObjectStream` — not both.
1165
+ */
1166
+ export type AgentStreamObjectResult<T = any> = AgentStreamResult<T> & {
1167
+ /**
1168
+ * A best-effort parse of the answer so far — `undefined` until the partial
1169
+ * JSON parses. The final, **validated** object is `result.object`.
1170
+ */
1171
+ partialObjectStream: AsyncIterable<T | undefined>;
1172
+ };
1173
+
815
1174
  /** Instructions — static text, or computed per call (A/B tests, dynamic prompts). */
816
1175
  export type AgentInstructions =
817
1176
  | string
@@ -987,8 +1346,31 @@ export type AgentConfig = {
987
1346
  toolTimeoutMs?: number;
988
1347
  /** Run the tools of one step in parallel by default — see `AgentCallOptions`. */
989
1348
  parallelTools?: boolean;
1349
+ /** Cap the tool calls in flight per step — see `AgentCallOptions`. */
1350
+ maxParallelToolCalls?: number;
1351
+ /** Default run budget in tokens — see `AgentCallOptions.maxTokens`. */
1352
+ maxTokens?: number;
1353
+ /** Default wall-clock budget of a run, in milliseconds. */
1354
+ maxDurationMs?: number;
1355
+ /** Default cost budget (USD) — needs `provider.pricing`. */
1356
+ maxCost?: number;
1357
+ /** Default prompt budget in tokens — see `AgentCallOptions.contextTokens`. */
1358
+ contextTokens?: number;
1359
+ /** Guardrails of every run — input before the first call, output before the answer. */
1360
+ guardrails?: AgentGuardrails;
990
1361
  /** Store a failed run's conversation by default — see `AgentCallOptions`. */
991
1362
  savePartial?: boolean;
1363
+ /**
1364
+ * Default durable checkpoint — a run that declares a `runId` saves its state
1365
+ * after each completed step there, and `resumeRun()` continues it. A call's own
1366
+ * `checkpoint` wins. See `AgentRunStore`.
1367
+ */
1368
+ checkpoint?: AgentRunStore;
1369
+ /**
1370
+ * Observe every run of this agent — see `AgentTraceEvent`. A call's own
1371
+ * `onTrace` takes precedence; a hook that throws never fails the run.
1372
+ */
1373
+ onTrace?: (event: AgentTraceEvent) => void;
992
1374
  /**
993
1375
  * Other agents exposed to this one **as tools** — the model decides when to
994
1376
  * delegate: `agents: [researcher]` (an agent definition, usually the default
package/types/config.d.ts CHANGED
@@ -110,9 +110,42 @@ export type ServerConfig = {
110
110
  denyList?: IPRestrictionRule[];
111
111
  allowList?: IPRestrictionRule[];
112
112
  }
113
+ /**
114
+ * Security headers (Hono `secureHeaders`).
115
+ *
116
+ * `crossOriginResourcePolicy` sets `Cross-Origin-Resource-Policy` on the
117
+ * API responses. The default (`'same-origin'`) blocks another origin (a
118
+ * front served from a different host) from embedding them — an `<img
119
+ * src="https://api…">` then fails with `corp-not-same-origin`.
120
+ * Set `'cross-origin'` to allow embedding, or keep the default for an
121
+ * API only consumed by same-origin scripts.
122
+ */
123
+ security?: {
124
+ crossOriginResourcePolicy?: 'same-origin' | 'same-site' | 'cross-origin';
125
+ }
126
+ /**
127
+ * File serving (`GET /api/:tenant_id/files/:collection/:filename`).
128
+ */
129
+ files?: {
130
+ /**
131
+ * `Cross-Origin-Resource-Policy` on file responses (default:
132
+ * `'cross-origin'` — files are meant to be embedded by the app,
133
+ * possibly served from another origin). Set `'same-origin'` to
134
+ * forbid cross-origin embedding (tightest), or `'same-site'`.
135
+ */
136
+ crossOriginResourcePolicy?: 'same-origin' | 'same-site' | 'cross-origin';
137
+ }
113
138
  jwt?: {
114
139
  secret?: string;
115
140
  expiresIn?: string;
141
+ /**
142
+ * Reject a token that carries **no tenant claim** (default `false`).
143
+ *
144
+ * Tokens issued by the login action always carry `tenant` (the claim is
145
+ * injected by the framework). Legacy tokens issued before that change have
146
+ * no claim: set this to `true` once they have expired to require one.
147
+ */
148
+ requireTenantClaim?: boolean;
116
149
  }
117
150
  rateLimit?: {
118
151
  enabled?: boolean;
@@ -139,16 +172,37 @@ export type ServerConfig = {
139
172
  level?: 'debug' | 'info' | 'warn' | 'error';
140
173
  /** Also write to the console (default: true) */
141
174
  console?: boolean;
142
- /** File logging: true → <dir>/anteros.log, string → custom path, false → disabled (default: true) */
175
+ /** File logging: true → <dir>/app.log, string → custom path, false → disabled (default: true) */
143
176
  file?: boolean | string;
144
177
  /** Directory used when `file` is true (default: '.logs') */
145
178
  dir?: string;
146
- /** Rotate when the log file exceeds this size in bytes (default: 10MB) */
179
+ /** Rotation trigger (default: 'size'): size | hourly | daily | weekly | monthly | yearly */
180
+ rotate?: 'size' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly';
181
+ /** Rotate when the log file exceeds this size in bytes — `rotate: 'size'` only (default: 10MB) */
147
182
  maxSize?: number;
148
183
  /** Keep this many rotated files (default: 5) */
149
184
  maxFiles?: number;
150
185
  /** Log database operations slower than this many ms (default: 200) */
151
186
  slowQueryMs?: number;
187
+ /** HTTP access-log options (everything about requests) */
188
+ http?: {
189
+ /** Write the per-request access log (default: true) */
190
+ enabled?: boolean;
191
+ /** Capture raw request bodies in the access log (default: false) — may contain secrets, file only */
192
+ body?: boolean;
193
+ /** HTTP methods whose body is captured (default: GET, POST, PUT, PATCH, DELETE); [] disables it */
194
+ methods?: string[];
195
+ /** Max bytes captured per request body (default: 65536); beyond is truncated */
196
+ maxBodyBytes?: number;
197
+ /** @deprecated Use `logging.http.body`. */
198
+ captureBody?: boolean;
199
+ /** @deprecated Use `logging.http.methods`. */
200
+ captureMethods?: string[];
201
+ };
202
+ /** @deprecated Use `logging.http.captureBody`. */
203
+ captureBody?: boolean;
204
+ /** @deprecated Use `logging.http.maxBodyBytes`. */
205
+ maxBodyBytes?: number;
152
206
  };
153
207
  /** DB query caching (find + useCache) — driver + defaults for the query cache */
154
208
  cache?: {
package/types/file.d.ts CHANGED
@@ -50,7 +50,7 @@ export type FileCollection = {
50
50
  /** Allowed MIME types for upload.
51
51
  * @example ['image/jpeg', 'image/png', 'application/pdf'] */
52
52
  allowedMimeTypes?: string[];
53
- /** Maximum file size in bytes. Default: 10MB */
53
+ /** Maximum file size in bytes. Default: 100MB (a larger body than `server.body.maxSize` still hits the request body limit) */
54
54
  maxSize?: number;
55
55
  /**
56
56
  * Image transformations (resize, format, etc.).
package/types/mcp.d.ts CHANGED
@@ -16,7 +16,7 @@ export type McpToolResult = {
16
16
  };
17
17
 
18
18
  /**
19
- * MCP tool definition — auto-loaded from `{tenant.dir}/mcp/tools/**\/*.tool.ts`.
19
+ * MCP tool definition — auto-loaded from `{tenant.dir}/ai/mcp/tools/**\/*.tool.ts`.
20
20
  * Served over the MCP protocol at `GET/POST /api/:tenant_id/mcp` (Streamable HTTP).
21
21
  */
22
22
  export type McpTool = {
@@ -58,7 +58,7 @@ export type McpResourceResult = {
58
58
 
59
59
  /**
60
60
  * MCP resource — read-only data exposed by URI (like a file the LLM can open).
61
- * Auto-loaded from `{tenant.dir}/mcp/resources/**\/*.resource.ts`.
61
+ * Auto-loaded from `{tenant.dir}/ai/mcp/resources/**\/*.resource.ts`.
62
62
  *
63
63
  * The `uri` can be static (`orders://summary`) or a template
64
64
  * (`users://{id}`) — template params are extracted and passed to `read`.
package/utils/func.ts CHANGED
@@ -912,6 +912,24 @@ function clone<T>(data: T): T {
912
912
  const MAX_REGEX_LENGTH = 1000;
913
913
  const MAX_IN_ARRAY_SIZE = 10000;
914
914
 
915
+ /** Filter operators a client must never reach MongoDB with (JS execution). */
916
+ const FORBIDDEN_MATCH_KEYS = ['$where', '$expr', '$accumulator', '$function'];
917
+
918
+ /** Bound and sanitize a raw Mongo filter (#45) — same rules as `find`.
919
+ * Used by internal methods that accept a filter directly (`findOneAndUpdate`,
920
+ * `bulkWrite`, `bulkUpdate`, `countDocuments`). */
921
+ function assertSafeFilter(filter: unknown): void {
922
+ if (filter === null || filter === undefined || typeof filter !== 'object') return;
923
+ const forbidden = hasUnauthorizedKeys(filter, FORBIDDEN_MATCH_KEYS);
924
+ if (forbidden.length > 0) {
925
+ throw new AppError('Unauthorized match keys: ' + forbidden.join(', '), {
926
+ code: 'UNAUTHORIZED_MATCH_KEYS',
927
+ status: 400,
928
+ });
929
+ }
930
+ boundMatchOperators(filter);
931
+ }
932
+
915
933
  function boundMatchOperators(node: unknown): void {
916
934
  if (node === null || node === undefined) return;
917
935
 
@@ -1240,6 +1258,8 @@ function parseDuration(value: string | number): number | null {
1240
1258
  }
1241
1259
 
1242
1260
  export {
1261
+ assertSafeFilter,
1262
+ boundMatchOperators,
1243
1263
  buildPipeline,
1244
1264
  reorder,
1245
1265
  omit,