@dudousxd/nestjs-agent 0.11.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -41,6 +41,66 @@ The module mounts SSE + REST endpoints under `/agent` (`POST /agent/chat`, tool-
41
41
  threads, quota). Add `durable: true` + `AgentDurableModule` for the durable runner;
42
42
  `AgentModule.forFeature([…])` for multi-agent orchestration.
43
43
 
44
+ ### Message attachments
45
+
46
+ A chat turn attaches a file by **`mediaId` only**:
47
+
48
+ ```jsonc
49
+ POST /agent/chat
50
+ { "message": "what is in this?", "attachments": [{ "mediaId": "med_8f21…" }] }
51
+ ```
52
+
53
+ Every other field a client sends with it is discarded. The url the model provider fetches is read
54
+ back from the `AttachmentStagingStore` bound to `AGENT_ATTACHMENT_STAGING`, whose `resolve({ mediaId,
55
+ actor })` returns the attachment or `null` when the id is unknown or is not that actor's — so a
56
+ caller can never point the server at a url it chose (link-local metadata, an internal service), and
57
+ never at another actor's file. Resolution happens per turn, so a short-lived presigned url is minted
58
+ fresh rather than replayed from the stored message.
59
+
60
+ With nothing bound to `AGENT_ATTACHMENT_STAGING` there is no way to tell whose media an id is, so a
61
+ turn carrying attachments is refused with `501`. Text-only turns are unaffected. At most 10
62
+ attachments per turn.
63
+
64
+ The optional `POST /agent/attachments` upload route (`attachments: { upload: true }`) aborts a body
65
+ larger than `HARD_MAX_ATTACHMENT_BYTES` (32 MiB) while it streams; `attachments.maxBytes` narrows
66
+ that ceiling at request time and cannot raise it.
67
+
68
+ ### Sweeping attachments nobody sent
69
+
70
+ `stage()` writes bytes before any message exists, so a user who attaches a file and then never sends
71
+ it leaves media with nothing pointing at it. Two optional methods make that findable — `list` on
72
+ your `AttachmentStagingStore` (you stored the bytes, so only you can enumerate them) and
73
+ `referencedMediaIds` on your `AgentStore` (only it can see which media a live message carries).
74
+
75
+ ```ts
76
+ // a job the host runs; nothing here is reachable over HTTP
77
+ const olderThan = new Date(Date.now() - 48 * 60 * 60 * 1000);
78
+ for (const entry of await agents.collectableAttachments(actor, { olderThan })) {
79
+ await myMediaStore.delete(entry.mediaId); // the library never deletes your bytes
80
+ }
81
+ ```
82
+
83
+ `olderThan` is required and has no default: freshly staged media is an upload in flight, not
84
+ garbage, and only you know how long one of your composers may sit open. It is pushed down to `list`
85
+ and re-applied to the result, so a store that ignores the hint still cannot delete a live upload.
86
+
87
+ References are re-derived on every call rather than latched, because `truncateFrom` — which is what
88
+ regenerating a turn does — deletes messages and frees their media again. If either method is
89
+ missing the call raises `501` rather than reporting everything as collectable.
90
+
91
+ `GET /agent/attachments` returns the caller's own staged files as metadata (no urls — those are
92
+ minted per turn by `resolve`), bounded to `ATTACHMENT_PAGE_SIZE`, and rides the same
93
+ `attachments: { upload: true }` flag. It takes no `threadId` filter: a thread's attachments already
94
+ ride on its messages in `GET /agent/threads/:id`.
95
+
96
+ ### Who may read a run
97
+
98
+ `GET /agent/chat/:runId/stream` and `POST /agent/chat/:runId/cancel` both resolve the acting actor
99
+ and require they own the run currently streaming — `403` for another actor's run, `404` for a run
100
+ nobody is streaming (including one that has already finished). In-process callers that are
101
+ authorized elsewhere use `AgentService.subscribe(runId)`; anything reachable from a request must go
102
+ through `AgentService.subscribeAs(actor, runId)`.
103
+
44
104
  ### Deploying split API/worker pods
45
105
 
46
106
  By default `AgentModule`/`AgentDurableModule` wire everything: every controller, the `agent.run`
@@ -1,4 +1,5 @@
1
- import { ModelProvider, AgentStore, ToolRegistry, RolesPolicy, QuotaStore, TokenStreamSink, PromptBuilder, PromptContributor, ToolTransientRetrySetting, Retriever, AgentPricingStore, ToolSpec, ToolHandler, ActorResolver, AgentRegistry } from '@dudousxd/nestjs-agent-core';
1
+ import { ModelProvider, AgentStore, ToolRegistry, RolesPolicy, QuotaStore, TokenStreamSink, PromptBuilder, PromptContributor, ToolTransientRetrySetting, Retriever, AgentPricingStore, HistoryPolicy, InputProcessor, OutputProcessor, AgentIntake, SkillsConfig, MemoryConfig, ToolSpec, ToolHandler, ActorResolver, AgentHistoryWindow, SkillProvider, ScopeResolver, MemoryProvider, AgentRegistry } from '@dudousxd/nestjs-agent-core';
2
+ import { StandardSchemaV1 } from '@standard-schema/spec';
2
3
  import { Provider, FactoryProvider, Type, CanActivate, ModuleMetadata, InjectionToken, OptionalFactoryDependency } from '@nestjs/common';
3
4
 
4
5
  /** Everything `runAgentLoop` needs, minus the per-run `day` the runner stamps. */
@@ -15,6 +16,10 @@ interface AgentDeps {
15
16
  /** App-wide `@SystemPromptContributor()` sections appended after the agent's base prompt. */
16
17
  promptContributors: PromptContributor[];
17
18
  maxSteps: number;
19
+ /** Nesting ceiling for delegation below this agent. Undefined → the loop's default. */
20
+ maxDelegationDepth?: number;
21
+ /** How many times one agent may appear on a chain. Undefined → the loop's default. */
22
+ maxAgentAppearances?: number;
18
23
  /** Agent-level tool allow-list. Undefined → all tools (after role filtering). */
19
24
  toolAllowList?: string[];
20
25
  /** Per-tool execution timeout in ms (from module options). Undefined → no timeout. */
@@ -32,6 +37,30 @@ interface AgentDeps {
32
37
  retrievalTopK?: number;
33
38
  /** Bound `AGENT_PRICING_STORE`, when a module (e.g. a store's) provides one. Undefined → no pricing. */
34
39
  pricingStore?: AgentPricingStore;
40
+ /** The agent's history ceiling, resolved from `@Agent({ history })` / module options. Undefined → unbounded. */
41
+ historyPolicy?: HistoryPolicy;
42
+ /** Module-wide prompt rewriters, applied before every model call. Empty → none. */
43
+ inputProcessors: InputProcessor[];
44
+ /** Module-wide answer gates. NON-EMPTY MEANS THE TURN'S MODEL OUTPUT IS BUFFERED, not streamed. */
45
+ outputProcessors: OutputProcessor[];
46
+ /** The agent's `@Agent({ outputSchema })`, resolved from DI — never over the wire. Undefined → free text. */
47
+ outputSchema?: StandardSchemaV1;
48
+ /** Extra model calls allowed to repair an answer that failed `outputSchema`. Undefined → 1. */
49
+ outputRepairAttempts?: number;
50
+ /** The agent's `@Agent({ intake })` question set, asked before the turn starts. Undefined → none. */
51
+ intake?: AgentIntake;
52
+ /** Whether the model may call the built-in `ask` tool this turn. Undefined/false → it never sees it. */
53
+ ask?: boolean;
54
+ /**
55
+ * The resolved skills seam (`forRoot({ skills })` + discovered `@Skill` providers). Undefined →
56
+ * no catalog, no `skill` tool, and the turn spends no checkpoint on either.
57
+ */
58
+ skills?: SkillsConfig;
59
+ /**
60
+ * The resolved memory seam (`forRoot({ memory })`). Undefined → no block, no `remember` tool, and
61
+ * the turn spends no checkpoint on either.
62
+ */
63
+ memory?: MemoryConfig;
35
64
  }
36
65
  declare function utcDay(date?: Date): string;
37
66
 
@@ -96,6 +125,61 @@ interface AgentAttachmentsOptions {
96
125
  */
97
126
  upload?: boolean;
98
127
  }
128
+ /**
129
+ * `AgentModuleOptions.skills` — where a turn's skills come from, and which scopes an actor draws
130
+ * them from. Its PRESENCE turns skills on: `skills: {}` offers whatever the `@Skill`-decorated
131
+ * providers declare, resolved against the actor's own, their tenant's and the deployment's scopes.
132
+ */
133
+ interface AgentSkillsOptions {
134
+ /**
135
+ * A provider of your own — skills a host stores and administers itself (per user, per sector, per
136
+ * base). Merged with the discovered `@Skill` classes, and outranking them for a name published at
137
+ * the same scope. Omit → the decorated classes are the whole catalog.
138
+ */
139
+ provider?: SkillProvider;
140
+ /**
141
+ * Which scope tokens an actor draws from, most specific first — the precedence order. Omit → the
142
+ * actor's own (`actor:<id>`), their tenant's (`tenant:<ref>`, when they have one), and `global`.
143
+ * Supply one to add an axis this library has no key for: a sector, a squadron, a shift.
144
+ */
145
+ scopes?: ScopeResolver;
146
+ /**
147
+ * How many skills the catalog block offers before it starts leaving the widest ones out. Omit →
148
+ * 20. A ceiling on what the SYSTEM PROMPT carries, not on how many a deployment may hold.
149
+ */
150
+ maxSkills?: number;
151
+ }
152
+ /**
153
+ * `AgentModuleOptions.memory` — where what the assistant concluded about a person is stored, and how
154
+ * much of it a turn may carry. Its PRESENCE turns memory on.
155
+ *
156
+ * Unlike `skills`, there is no decorator and no zero-config form: a memory is by definition something
157
+ * the agent worked out about someone, so a memory a developer wrote in a class would be an
158
+ * instruction wearing a memory's clothes — which is what `@Skill` and `@SystemPromptContributor()`
159
+ * are for. The rows are the host's from the start.
160
+ */
161
+ interface AgentMemoryOptions {
162
+ /**
163
+ * Where the rows live. `forget` is required on it; `write` is what decides the `remember` tool;
164
+ * `search` is what decides whether a turn's block is selected by relevance or read whole. Supply
165
+ * `search` once the applicable set outgrows `maxMemories` — without it the ceiling selects by
166
+ * scope, and the widest scopes are the ones it starves.
167
+ */
168
+ provider: MemoryProvider;
169
+ /**
170
+ * Which scope tokens an actor draws from, most specific first — the precedence order, and the same
171
+ * seam `skills` uses. Omit → the actor's own, their tenant's, and `global`.
172
+ */
173
+ scopes?: ScopeResolver;
174
+ /**
175
+ * How many memories the block carries. Omit → 20. A budget on the PROMPT, never on the store: with
176
+ * a provider that can `search`, this is how many matter right now rather than how many a person
177
+ * may have. Always-on (`pinned`) memories are taken from it first.
178
+ */
179
+ maxMemories?: number;
180
+ /** How long one memory may be, checked when it is written. Omit → 240. */
181
+ maxFactChars?: number;
182
+ }
99
183
  interface AgentModuleOptions {
100
184
  /** The LLM provider (e.g. a Vercel AI SDK wrapper). Required. */
101
185
  model: ModelProvider;
@@ -139,11 +223,19 @@ interface AgentModuleOptions {
139
223
  durable?: boolean;
140
224
  /**
141
225
  * Dispatch the turn's model call and tool executions as routed durable steps
142
- * (`AgentRunSteps.llm` / `AgentRunSteps.tool`) instead of in-process `ctx.localStep`s.
143
- * DEFAULT `true` under `durable: true` — dispatching moves the run off its pod during the two
144
- * long steps (the correct production posture), and `AgentRunSteps` is always registered so the
145
- * routed groups are never unserved. Set `false` to keep the turn's steps in-process localSteps.
146
- * `true` without `durable: true` throws at module build.
226
+ * (`AgentRunSteps.llm` / `AgentRunSteps.tool`) instead of in-process `ctx.localStep`s. OMITTED
227
+ * keeps them in-process; `true` without `durable: true` throws at module build.
228
+ *
229
+ * WHAT IT BUYS: the run is not pinned to one pod for the two long parts of a turn, so a fleet can
230
+ * scale the model call and tool execution separately from the workflow itself.
231
+ *
232
+ * WHY IT IS NOT A DEFAULT. It relocates the HOST's own code. A tool handler no longer runs inside
233
+ * the turn's workflow body but inside whichever worker serves the routed group, and the `llm`
234
+ * step re-runs the host's tool-visibility callbacks there to rebuild its tool list. A handler
235
+ * that resolves anything per invocation from its caller's execution context — a request-scoped
236
+ * ORM EntityManager, an AsyncLocalStorage tenant, a CLS transaction — finds nothing in that
237
+ * worker, and the library has no way to know which handlers do that. Turn this on once the tools
238
+ * this deployment registers establish whatever context they need themselves.
147
239
  *
148
240
  * The cross-process-sink requirement is a property of `durable: true` itself, not of this flag:
149
241
  * the turn already runs on whichever worker takes `agent.run`, which may not be the pod holding
@@ -191,6 +283,75 @@ interface AgentModuleOptions {
191
283
  retriever: Retriever;
192
284
  topK?: number;
193
285
  };
286
+ /**
287
+ * Bounds how much of a thread rides into each turn. Omit and a turn carries EVERY message the
288
+ * thread holds, so a long-lived thread costs more each time and eventually exceeds the model's
289
+ * context limit outright. `{ maxMessages }` and/or `{ maxTokens }` keep the newest that fit;
290
+ * `{ summarize: true }` folds what they left out into a leading summary, at the price of one extra
291
+ * model call per run (recorded as `history_summary` usage). An agent can override this for itself
292
+ * with `@Agent({ history })`.
293
+ */
294
+ history?: AgentHistoryWindow;
295
+ /**
296
+ * A {@link HistoryPolicy} of your own — for a window the built-in can't express (pinning the
297
+ * thread's opening brief, keeping every message that carries a tool result, a budget that varies
298
+ * by actor). A convenience-vs-custom pair like `quotaLimitTokens`/`quota`: this outranks a
299
+ * module-wide `history`, and `@Agent({ history })` outranks both for the agent that declares it.
300
+ * `select` MUST be pure — see the SPI's determinism contract.
301
+ */
302
+ historyPolicy?: HistoryPolicy;
303
+ /**
304
+ * Rewrites the prompt before every model call — masking identifiers, stamping a policy preamble,
305
+ * trimming an oversized tool result. Applies to EVERY agent: a transformation one persona can opt
306
+ * out of is not a control. Transformation only; `history`/`historyPolicy` still own which messages
307
+ * are there to transform. A processor needing DI-resolved dependencies is built in
308
+ * `forRootAsync`'s `useFactory` (which has `inject`), the same way `retrieval.retriever` is.
309
+ */
310
+ inputProcessors?: InputProcessor[];
311
+ /**
312
+ * Gates every model answer before the stream, the store or the next step sees it — redact,
313
+ * replace, or refuse the turn outright (which fails it with an `output_rejected` stream error).
314
+ *
315
+ * REGISTERING ONE TURNS OFF LIVE TOKEN STREAMING. Reading the whole answer and streaming it as it
316
+ * is generated cannot both be true, so the turn's model output is buffered and released in one
317
+ * frame once the chain passes. That is the price of a gate that actually gates; see
318
+ * `AgentLoopDeps.outputProcessors` for exactly what a subscriber still receives live.
319
+ */
320
+ outputProcessors?: OutputProcessor[];
321
+ /**
322
+ * Offer every agent the built-in `ask` tool, so the model can put its own clarifying question set
323
+ * to the user when it judges the scope is missing. The turn parks on the answers exactly as it
324
+ * parks on a HITL approval, and they arrive through `POST /agent/tool-call/answer`.
325
+ *
326
+ * `ask` is not a registered tool and has no handler — the loop settles it against a person — so no
327
+ * `RolesPolicy` gates it: asking a question performs nothing. An `@Agent({ ask })` overrides this
328
+ * for one persona. Omit → the model never sees it, and nothing about a turn changes.
329
+ */
330
+ ask?: boolean;
331
+ /**
332
+ * Authored procedures any agent may pull in when a task calls for one — per-user, per-tenant or
333
+ * deployment-wide, with the most specific winning. Declared as `@Skill`-decorated providers and/or
334
+ * served by a `provider` of your own; offered to the model as a one-line-each catalog it loads
335
+ * from on demand, so an instruction costs a prompt only on the turns that need it.
336
+ *
337
+ * Omit → no catalog, no `skill` tool, and a turn's checkpoint sequence is byte-identical to one
338
+ * that never had the option. See {@link AgentSkillsOptions}.
339
+ */
340
+ skills?: AgentSkillsOptions;
341
+ /**
342
+ * What the assistant has concluded about the actor and their organisation, carried into every turn
343
+ * as a bounded block of one-line facts and written by the model through a built-in `remember` tool.
344
+ *
345
+ * NOT the same thing as retrieval, and not a second name for it: `retrieval` answers from documents
346
+ * a person curated and can fix at the source, memory answers from the agent's own inferences about
347
+ * someone who never saw them written. That is why the rows carry an origin, why the block tells the
348
+ * model they may be wrong, and why `GET /agent/memories` + `DELETE /agent/memories/:id` are part of
349
+ * the feature rather than an optional console.
350
+ *
351
+ * Omit → no block, no `remember` tool, and a turn's checkpoint sequence is byte-identical to one
352
+ * that never had the option. See {@link AgentMemoryOptions}.
353
+ */
354
+ memory?: AgentMemoryOptions;
194
355
  /**
195
356
  * The name of the agent a turn uses when the caller doesn't select one. Omit → the single
196
357
  * discovered `@Agent` (when there is exactly one), else `'default'` (a bare assistant if no
@@ -230,12 +391,11 @@ interface AgentModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
230
391
  */
231
392
  durable?: boolean;
232
393
  /**
233
- * Dispatch the turn's model call and tool executions as routed durable steps. DEFAULT `true`
234
- * under `durable: true`; set `false` to keep the turn's steps in-process localSteps. Same
235
- * static-wiring reasoning as `durable` above — it lives here (not in the async factory result)
236
- * because it decides how `AgentRunWorkflow` builds its hooks at module build time. `true` without
237
- * `durable: true` throws at module build. See `AgentModuleOptions.dispatchedSteps` for the full
238
- * contract.
394
+ * Dispatch the turn's model call and tool executions as routed durable steps. Omitted keeps the
395
+ * turn's steps in-process localSteps. Same static-wiring reasoning as `durable` above — it lives
396
+ * here (not in the async factory result) because it decides how `AgentRunWorkflow` builds its
397
+ * hooks at module build time. `true` without `durable: true` throws at module build. See
398
+ * `AgentModuleOptions.dispatchedSteps` for the full contract.
239
399
  */
240
400
  dispatchedSteps?: boolean;
241
401
  /**
@@ -273,8 +433,18 @@ interface AgentModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
273
433
  surface?: AgentSurface;
274
434
  }
275
435
 
276
- /** The synthesized `agent`-kind tool name an orchestrator uses to hand off to `target`. */
277
- declare function delegateToolName(target: string): string;
436
+ /**
437
+ * The synthesized `agent`-kind tool name an orchestrator uses to hand off to `target`.
438
+ *
439
+ * A detached edge gets its own verb — `start_research` next to `ask_research` — for two reasons.
440
+ * One agent can be both awaited AND backgrounded by the same orchestrator, and one name cannot
441
+ * carry both. And the name is the part of a tool the model reads first: `ask` promises an answer,
442
+ * `start` does not.
443
+ */
444
+ declare function delegateToolName(args: {
445
+ target: string;
446
+ detached?: boolean;
447
+ }): string;
278
448
  /** Builds the per-agent loop deps. The single-agent case is just the one registered `@Agent`. */
279
449
  declare class AgentDepsFactory {
280
450
  private readonly options;
@@ -287,7 +457,13 @@ declare class AgentDepsFactory {
287
457
  private readonly promptContributors;
288
458
  private readonly quota;
289
459
  private readonly pricingStore;
290
- constructor(options: AgentModuleOptions, model: ModelProvider, store: AgentStore, sink: TokenStreamSink, rolesPolicy: RolesPolicy, registry: ToolRegistry, agents: AgentRegistry, promptContributors: PromptContributor[], quota: QuotaStore | undefined, pricingStore: AgentPricingStore | undefined);
460
+ private readonly skills;
461
+ private readonly memory;
462
+ constructor(options: AgentModuleOptions, model: ModelProvider, store: AgentStore, sink: TokenStreamSink, rolesPolicy: RolesPolicy, registry: ToolRegistry, agents: AgentRegistry, promptContributors: PromptContributor[], quota: QuotaStore | undefined, pricingStore: AgentPricingStore | undefined, skills: SkillsConfig | undefined, memory: MemoryConfig | undefined);
463
+ /** The turn's memory seam, shared with the read-back endpoint. Undefined → memory is not configured. */
464
+ memoryConfig(): MemoryConfig | undefined;
465
+ /** The turn's skills seam, shared with the listing endpoint. Undefined → skills are not configured. */
466
+ skillsConfig(): SkillsConfig | undefined;
291
467
  /**
292
468
  * The agent a turn uses when the caller names none: the explicit `defaultAgent` option, else the
293
469
  * sole registered `@Agent` when there is exactly one, else `'default'` (a bare assistant).
@@ -295,8 +471,16 @@ declare class AgentDepsFactory {
295
471
  defaultAgentName(): string;
296
472
  private effectiveTools;
297
473
  forAgent(agentName?: string): AgentDeps;
474
+ /**
475
+ * The ceiling on how much of a thread this agent's turns carry, most specific wins: the agent's
476
+ * own `@Agent({ history })`, else a module-wide custom `historyPolicy`, else the module-wide
477
+ * `history` window. Undefined → unbounded, the behaviour of a module that configures none.
478
+ */
479
+ private historyPolicy;
480
+ /** Build the built-in window policy from the declarative `{ maxMessages, maxTokens, summarize }`. */
481
+ private window;
298
482
  /** Normalize the `followUps` option (`true` → 3, `{ count }` → count) to a number, or undefined. */
299
483
  private followUpsCount;
300
484
  }
301
485
 
302
- export { AgentDepsFactory as A, type FunctionalTool as F, type AgentSurface as a, type AgentModuleOptions as b, type AgentModuleAsyncOptions as c, AGENT_TOOL_BRAND as d, type AgentAttachmentsOptions as e, type AgentDeps as f, delegateToolName as g, provideAgentTool as p, utcDay as u };
486
+ export { AgentDepsFactory as A, type FunctionalTool as F, type AgentSurface as a, type AgentModuleOptions as b, type AgentModuleAsyncOptions as c, type AgentSkillsOptions as d, AGENT_TOOL_BRAND as e, type AgentAttachmentsOptions as f, type AgentDeps as g, type AgentMemoryOptions as h, delegateToolName as i, provideAgentTool as p, utcDay as u };
@@ -1,4 +1,5 @@
1
- import { ModelProvider, AgentStore, ToolRegistry, RolesPolicy, QuotaStore, TokenStreamSink, PromptBuilder, PromptContributor, ToolTransientRetrySetting, Retriever, AgentPricingStore, ToolSpec, ToolHandler, ActorResolver, AgentRegistry } from '@dudousxd/nestjs-agent-core';
1
+ import { ModelProvider, AgentStore, ToolRegistry, RolesPolicy, QuotaStore, TokenStreamSink, PromptBuilder, PromptContributor, ToolTransientRetrySetting, Retriever, AgentPricingStore, HistoryPolicy, InputProcessor, OutputProcessor, AgentIntake, SkillsConfig, MemoryConfig, ToolSpec, ToolHandler, ActorResolver, AgentHistoryWindow, SkillProvider, ScopeResolver, MemoryProvider, AgentRegistry } from '@dudousxd/nestjs-agent-core';
2
+ import { StandardSchemaV1 } from '@standard-schema/spec';
2
3
  import { Provider, FactoryProvider, Type, CanActivate, ModuleMetadata, InjectionToken, OptionalFactoryDependency } from '@nestjs/common';
3
4
 
4
5
  /** Everything `runAgentLoop` needs, minus the per-run `day` the runner stamps. */
@@ -15,6 +16,10 @@ interface AgentDeps {
15
16
  /** App-wide `@SystemPromptContributor()` sections appended after the agent's base prompt. */
16
17
  promptContributors: PromptContributor[];
17
18
  maxSteps: number;
19
+ /** Nesting ceiling for delegation below this agent. Undefined → the loop's default. */
20
+ maxDelegationDepth?: number;
21
+ /** How many times one agent may appear on a chain. Undefined → the loop's default. */
22
+ maxAgentAppearances?: number;
18
23
  /** Agent-level tool allow-list. Undefined → all tools (after role filtering). */
19
24
  toolAllowList?: string[];
20
25
  /** Per-tool execution timeout in ms (from module options). Undefined → no timeout. */
@@ -32,6 +37,30 @@ interface AgentDeps {
32
37
  retrievalTopK?: number;
33
38
  /** Bound `AGENT_PRICING_STORE`, when a module (e.g. a store's) provides one. Undefined → no pricing. */
34
39
  pricingStore?: AgentPricingStore;
40
+ /** The agent's history ceiling, resolved from `@Agent({ history })` / module options. Undefined → unbounded. */
41
+ historyPolicy?: HistoryPolicy;
42
+ /** Module-wide prompt rewriters, applied before every model call. Empty → none. */
43
+ inputProcessors: InputProcessor[];
44
+ /** Module-wide answer gates. NON-EMPTY MEANS THE TURN'S MODEL OUTPUT IS BUFFERED, not streamed. */
45
+ outputProcessors: OutputProcessor[];
46
+ /** The agent's `@Agent({ outputSchema })`, resolved from DI — never over the wire. Undefined → free text. */
47
+ outputSchema?: StandardSchemaV1;
48
+ /** Extra model calls allowed to repair an answer that failed `outputSchema`. Undefined → 1. */
49
+ outputRepairAttempts?: number;
50
+ /** The agent's `@Agent({ intake })` question set, asked before the turn starts. Undefined → none. */
51
+ intake?: AgentIntake;
52
+ /** Whether the model may call the built-in `ask` tool this turn. Undefined/false → it never sees it. */
53
+ ask?: boolean;
54
+ /**
55
+ * The resolved skills seam (`forRoot({ skills })` + discovered `@Skill` providers). Undefined →
56
+ * no catalog, no `skill` tool, and the turn spends no checkpoint on either.
57
+ */
58
+ skills?: SkillsConfig;
59
+ /**
60
+ * The resolved memory seam (`forRoot({ memory })`). Undefined → no block, no `remember` tool, and
61
+ * the turn spends no checkpoint on either.
62
+ */
63
+ memory?: MemoryConfig;
35
64
  }
36
65
  declare function utcDay(date?: Date): string;
37
66
 
@@ -96,6 +125,61 @@ interface AgentAttachmentsOptions {
96
125
  */
97
126
  upload?: boolean;
98
127
  }
128
+ /**
129
+ * `AgentModuleOptions.skills` — where a turn's skills come from, and which scopes an actor draws
130
+ * them from. Its PRESENCE turns skills on: `skills: {}` offers whatever the `@Skill`-decorated
131
+ * providers declare, resolved against the actor's own, their tenant's and the deployment's scopes.
132
+ */
133
+ interface AgentSkillsOptions {
134
+ /**
135
+ * A provider of your own — skills a host stores and administers itself (per user, per sector, per
136
+ * base). Merged with the discovered `@Skill` classes, and outranking them for a name published at
137
+ * the same scope. Omit → the decorated classes are the whole catalog.
138
+ */
139
+ provider?: SkillProvider;
140
+ /**
141
+ * Which scope tokens an actor draws from, most specific first — the precedence order. Omit → the
142
+ * actor's own (`actor:<id>`), their tenant's (`tenant:<ref>`, when they have one), and `global`.
143
+ * Supply one to add an axis this library has no key for: a sector, a squadron, a shift.
144
+ */
145
+ scopes?: ScopeResolver;
146
+ /**
147
+ * How many skills the catalog block offers before it starts leaving the widest ones out. Omit →
148
+ * 20. A ceiling on what the SYSTEM PROMPT carries, not on how many a deployment may hold.
149
+ */
150
+ maxSkills?: number;
151
+ }
152
+ /**
153
+ * `AgentModuleOptions.memory` — where what the assistant concluded about a person is stored, and how
154
+ * much of it a turn may carry. Its PRESENCE turns memory on.
155
+ *
156
+ * Unlike `skills`, there is no decorator and no zero-config form: a memory is by definition something
157
+ * the agent worked out about someone, so a memory a developer wrote in a class would be an
158
+ * instruction wearing a memory's clothes — which is what `@Skill` and `@SystemPromptContributor()`
159
+ * are for. The rows are the host's from the start.
160
+ */
161
+ interface AgentMemoryOptions {
162
+ /**
163
+ * Where the rows live. `forget` is required on it; `write` is what decides the `remember` tool;
164
+ * `search` is what decides whether a turn's block is selected by relevance or read whole. Supply
165
+ * `search` once the applicable set outgrows `maxMemories` — without it the ceiling selects by
166
+ * scope, and the widest scopes are the ones it starves.
167
+ */
168
+ provider: MemoryProvider;
169
+ /**
170
+ * Which scope tokens an actor draws from, most specific first — the precedence order, and the same
171
+ * seam `skills` uses. Omit → the actor's own, their tenant's, and `global`.
172
+ */
173
+ scopes?: ScopeResolver;
174
+ /**
175
+ * How many memories the block carries. Omit → 20. A budget on the PROMPT, never on the store: with
176
+ * a provider that can `search`, this is how many matter right now rather than how many a person
177
+ * may have. Always-on (`pinned`) memories are taken from it first.
178
+ */
179
+ maxMemories?: number;
180
+ /** How long one memory may be, checked when it is written. Omit → 240. */
181
+ maxFactChars?: number;
182
+ }
99
183
  interface AgentModuleOptions {
100
184
  /** The LLM provider (e.g. a Vercel AI SDK wrapper). Required. */
101
185
  model: ModelProvider;
@@ -139,11 +223,19 @@ interface AgentModuleOptions {
139
223
  durable?: boolean;
140
224
  /**
141
225
  * Dispatch the turn's model call and tool executions as routed durable steps
142
- * (`AgentRunSteps.llm` / `AgentRunSteps.tool`) instead of in-process `ctx.localStep`s.
143
- * DEFAULT `true` under `durable: true` — dispatching moves the run off its pod during the two
144
- * long steps (the correct production posture), and `AgentRunSteps` is always registered so the
145
- * routed groups are never unserved. Set `false` to keep the turn's steps in-process localSteps.
146
- * `true` without `durable: true` throws at module build.
226
+ * (`AgentRunSteps.llm` / `AgentRunSteps.tool`) instead of in-process `ctx.localStep`s. OMITTED
227
+ * keeps them in-process; `true` without `durable: true` throws at module build.
228
+ *
229
+ * WHAT IT BUYS: the run is not pinned to one pod for the two long parts of a turn, so a fleet can
230
+ * scale the model call and tool execution separately from the workflow itself.
231
+ *
232
+ * WHY IT IS NOT A DEFAULT. It relocates the HOST's own code. A tool handler no longer runs inside
233
+ * the turn's workflow body but inside whichever worker serves the routed group, and the `llm`
234
+ * step re-runs the host's tool-visibility callbacks there to rebuild its tool list. A handler
235
+ * that resolves anything per invocation from its caller's execution context — a request-scoped
236
+ * ORM EntityManager, an AsyncLocalStorage tenant, a CLS transaction — finds nothing in that
237
+ * worker, and the library has no way to know which handlers do that. Turn this on once the tools
238
+ * this deployment registers establish whatever context they need themselves.
147
239
  *
148
240
  * The cross-process-sink requirement is a property of `durable: true` itself, not of this flag:
149
241
  * the turn already runs on whichever worker takes `agent.run`, which may not be the pod holding
@@ -191,6 +283,75 @@ interface AgentModuleOptions {
191
283
  retriever: Retriever;
192
284
  topK?: number;
193
285
  };
286
+ /**
287
+ * Bounds how much of a thread rides into each turn. Omit and a turn carries EVERY message the
288
+ * thread holds, so a long-lived thread costs more each time and eventually exceeds the model's
289
+ * context limit outright. `{ maxMessages }` and/or `{ maxTokens }` keep the newest that fit;
290
+ * `{ summarize: true }` folds what they left out into a leading summary, at the price of one extra
291
+ * model call per run (recorded as `history_summary` usage). An agent can override this for itself
292
+ * with `@Agent({ history })`.
293
+ */
294
+ history?: AgentHistoryWindow;
295
+ /**
296
+ * A {@link HistoryPolicy} of your own — for a window the built-in can't express (pinning the
297
+ * thread's opening brief, keeping every message that carries a tool result, a budget that varies
298
+ * by actor). A convenience-vs-custom pair like `quotaLimitTokens`/`quota`: this outranks a
299
+ * module-wide `history`, and `@Agent({ history })` outranks both for the agent that declares it.
300
+ * `select` MUST be pure — see the SPI's determinism contract.
301
+ */
302
+ historyPolicy?: HistoryPolicy;
303
+ /**
304
+ * Rewrites the prompt before every model call — masking identifiers, stamping a policy preamble,
305
+ * trimming an oversized tool result. Applies to EVERY agent: a transformation one persona can opt
306
+ * out of is not a control. Transformation only; `history`/`historyPolicy` still own which messages
307
+ * are there to transform. A processor needing DI-resolved dependencies is built in
308
+ * `forRootAsync`'s `useFactory` (which has `inject`), the same way `retrieval.retriever` is.
309
+ */
310
+ inputProcessors?: InputProcessor[];
311
+ /**
312
+ * Gates every model answer before the stream, the store or the next step sees it — redact,
313
+ * replace, or refuse the turn outright (which fails it with an `output_rejected` stream error).
314
+ *
315
+ * REGISTERING ONE TURNS OFF LIVE TOKEN STREAMING. Reading the whole answer and streaming it as it
316
+ * is generated cannot both be true, so the turn's model output is buffered and released in one
317
+ * frame once the chain passes. That is the price of a gate that actually gates; see
318
+ * `AgentLoopDeps.outputProcessors` for exactly what a subscriber still receives live.
319
+ */
320
+ outputProcessors?: OutputProcessor[];
321
+ /**
322
+ * Offer every agent the built-in `ask` tool, so the model can put its own clarifying question set
323
+ * to the user when it judges the scope is missing. The turn parks on the answers exactly as it
324
+ * parks on a HITL approval, and they arrive through `POST /agent/tool-call/answer`.
325
+ *
326
+ * `ask` is not a registered tool and has no handler — the loop settles it against a person — so no
327
+ * `RolesPolicy` gates it: asking a question performs nothing. An `@Agent({ ask })` overrides this
328
+ * for one persona. Omit → the model never sees it, and nothing about a turn changes.
329
+ */
330
+ ask?: boolean;
331
+ /**
332
+ * Authored procedures any agent may pull in when a task calls for one — per-user, per-tenant or
333
+ * deployment-wide, with the most specific winning. Declared as `@Skill`-decorated providers and/or
334
+ * served by a `provider` of your own; offered to the model as a one-line-each catalog it loads
335
+ * from on demand, so an instruction costs a prompt only on the turns that need it.
336
+ *
337
+ * Omit → no catalog, no `skill` tool, and a turn's checkpoint sequence is byte-identical to one
338
+ * that never had the option. See {@link AgentSkillsOptions}.
339
+ */
340
+ skills?: AgentSkillsOptions;
341
+ /**
342
+ * What the assistant has concluded about the actor and their organisation, carried into every turn
343
+ * as a bounded block of one-line facts and written by the model through a built-in `remember` tool.
344
+ *
345
+ * NOT the same thing as retrieval, and not a second name for it: `retrieval` answers from documents
346
+ * a person curated and can fix at the source, memory answers from the agent's own inferences about
347
+ * someone who never saw them written. That is why the rows carry an origin, why the block tells the
348
+ * model they may be wrong, and why `GET /agent/memories` + `DELETE /agent/memories/:id` are part of
349
+ * the feature rather than an optional console.
350
+ *
351
+ * Omit → no block, no `remember` tool, and a turn's checkpoint sequence is byte-identical to one
352
+ * that never had the option. See {@link AgentMemoryOptions}.
353
+ */
354
+ memory?: AgentMemoryOptions;
194
355
  /**
195
356
  * The name of the agent a turn uses when the caller doesn't select one. Omit → the single
196
357
  * discovered `@Agent` (when there is exactly one), else `'default'` (a bare assistant if no
@@ -230,12 +391,11 @@ interface AgentModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
230
391
  */
231
392
  durable?: boolean;
232
393
  /**
233
- * Dispatch the turn's model call and tool executions as routed durable steps. DEFAULT `true`
234
- * under `durable: true`; set `false` to keep the turn's steps in-process localSteps. Same
235
- * static-wiring reasoning as `durable` above — it lives here (not in the async factory result)
236
- * because it decides how `AgentRunWorkflow` builds its hooks at module build time. `true` without
237
- * `durable: true` throws at module build. See `AgentModuleOptions.dispatchedSteps` for the full
238
- * contract.
394
+ * Dispatch the turn's model call and tool executions as routed durable steps. Omitted keeps the
395
+ * turn's steps in-process localSteps. Same static-wiring reasoning as `durable` above — it lives
396
+ * here (not in the async factory result) because it decides how `AgentRunWorkflow` builds its
397
+ * hooks at module build time. `true` without `durable: true` throws at module build. See
398
+ * `AgentModuleOptions.dispatchedSteps` for the full contract.
239
399
  */
240
400
  dispatchedSteps?: boolean;
241
401
  /**
@@ -273,8 +433,18 @@ interface AgentModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
273
433
  surface?: AgentSurface;
274
434
  }
275
435
 
276
- /** The synthesized `agent`-kind tool name an orchestrator uses to hand off to `target`. */
277
- declare function delegateToolName(target: string): string;
436
+ /**
437
+ * The synthesized `agent`-kind tool name an orchestrator uses to hand off to `target`.
438
+ *
439
+ * A detached edge gets its own verb — `start_research` next to `ask_research` — for two reasons.
440
+ * One agent can be both awaited AND backgrounded by the same orchestrator, and one name cannot
441
+ * carry both. And the name is the part of a tool the model reads first: `ask` promises an answer,
442
+ * `start` does not.
443
+ */
444
+ declare function delegateToolName(args: {
445
+ target: string;
446
+ detached?: boolean;
447
+ }): string;
278
448
  /** Builds the per-agent loop deps. The single-agent case is just the one registered `@Agent`. */
279
449
  declare class AgentDepsFactory {
280
450
  private readonly options;
@@ -287,7 +457,13 @@ declare class AgentDepsFactory {
287
457
  private readonly promptContributors;
288
458
  private readonly quota;
289
459
  private readonly pricingStore;
290
- constructor(options: AgentModuleOptions, model: ModelProvider, store: AgentStore, sink: TokenStreamSink, rolesPolicy: RolesPolicy, registry: ToolRegistry, agents: AgentRegistry, promptContributors: PromptContributor[], quota: QuotaStore | undefined, pricingStore: AgentPricingStore | undefined);
460
+ private readonly skills;
461
+ private readonly memory;
462
+ constructor(options: AgentModuleOptions, model: ModelProvider, store: AgentStore, sink: TokenStreamSink, rolesPolicy: RolesPolicy, registry: ToolRegistry, agents: AgentRegistry, promptContributors: PromptContributor[], quota: QuotaStore | undefined, pricingStore: AgentPricingStore | undefined, skills: SkillsConfig | undefined, memory: MemoryConfig | undefined);
463
+ /** The turn's memory seam, shared with the read-back endpoint. Undefined → memory is not configured. */
464
+ memoryConfig(): MemoryConfig | undefined;
465
+ /** The turn's skills seam, shared with the listing endpoint. Undefined → skills are not configured. */
466
+ skillsConfig(): SkillsConfig | undefined;
291
467
  /**
292
468
  * The agent a turn uses when the caller names none: the explicit `defaultAgent` option, else the
293
469
  * sole registered `@Agent` when there is exactly one, else `'default'` (a bare assistant).
@@ -295,8 +471,16 @@ declare class AgentDepsFactory {
295
471
  defaultAgentName(): string;
296
472
  private effectiveTools;
297
473
  forAgent(agentName?: string): AgentDeps;
474
+ /**
475
+ * The ceiling on how much of a thread this agent's turns carry, most specific wins: the agent's
476
+ * own `@Agent({ history })`, else a module-wide custom `historyPolicy`, else the module-wide
477
+ * `history` window. Undefined → unbounded, the behaviour of a module that configures none.
478
+ */
479
+ private historyPolicy;
480
+ /** Build the built-in window policy from the declarative `{ maxMessages, maxTokens, summarize }`. */
481
+ private window;
298
482
  /** Normalize the `followUps` option (`true` → 3, `{ count }` → count) to a number, or undefined. */
299
483
  private followUpsCount;
300
484
  }
301
485
 
302
- export { AgentDepsFactory as A, type FunctionalTool as F, type AgentSurface as a, type AgentModuleOptions as b, type AgentModuleAsyncOptions as c, AGENT_TOOL_BRAND as d, type AgentAttachmentsOptions as e, type AgentDeps as f, delegateToolName as g, provideAgentTool as p, utcDay as u };
486
+ export { AgentDepsFactory as A, type FunctionalTool as F, type AgentSurface as a, type AgentModuleOptions as b, type AgentModuleAsyncOptions as c, type AgentSkillsOptions as d, AGENT_TOOL_BRAND as e, type AgentAttachmentsOptions as f, type AgentDeps as g, type AgentMemoryOptions as h, delegateToolName as i, provideAgentTool as p, utcDay as u };