@dudousxd/nestjs-agent 1.10.0 → 1.12.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
@@ -18,28 +18,47 @@ pnpm add @dudousxd/nestjs-agent @dudousxd/nestjs-agent-core
18
18
 
19
19
  ## Use
20
20
 
21
+ The whole server — a public chat with one tool:
22
+
21
23
  ```ts
22
- import { AgentModule, AiTool, HeaderActorResolver, type ToolHandler } from '@dudousxd/nestjs-agent';
24
+ import { AgentModule, AiTool } from '@dudousxd/nestjs-agent';
25
+ import { aiSdkModel } from '@dudousxd/nestjs-agent-ai-sdk';
26
+ import { openai } from '@ai-sdk/openai';
23
27
  import { z } from 'zod';
24
28
 
25
- @AiTool({ name: 'getWeather', kind: 'read', description: 'Current weather.', input: z.object({ city: z.string() }) })
26
- export class GetWeatherTool implements ToolHandler<{ city: string }> {
27
- async execute(input: { city: string }) { return { tempC: 21 }; }
29
+ @AiTool({ description: 'Current weather for a city', input: z.object({ city: z.string() }) })
30
+ export class GetWeatherTool { // name: getWeather, kind: read
31
+ async execute({ city }: { city: string }) { return { city, tempC: 21 }; }
28
32
  }
29
33
 
30
34
  @Module({
31
- imports: [AgentModule.forRoot({
32
- model, store, actorResolver: new HeaderActorResolver(),
33
- defaultAgent: { systemPrompt: '…', modelId: 'claude-sonnet-4-6' },
34
- })],
35
+ imports: [AgentModule.forRoot({ model: aiSdkModel(openai('gpt-5-mini')) })],
35
36
  providers: [GetWeatherTool],
36
37
  })
37
38
  export class AppModule {}
38
39
  ```
39
40
 
40
- The module mounts SSE + REST endpoints under `/agent` (`POST /agent/chat`, tool-call approve/reject,
41
- threads, quota). Add `durable: true` + `AgentDurableModule` for the durable runner;
42
- `AgentModule.forFeature([…])` for multi-agent orchestration.
41
+ The module mounts SSE + REST endpoints under `/agent` (`POST /agent/chat`, threads, tool-call
42
+ approve/reject, quota, models, tools). Everything but `model` defaults, and each default is one
43
+ option away:
44
+
45
+ | Default | Change it with |
46
+ |---|---|
47
+ | In-memory store (boot warning: not for production) | `store`, or import a store module (`MikroOrmAgentStoreModule.forFeature()`) — found automatically |
48
+ | Public endpoints; each browser its own anonymous actor (HttpOnly cookie) | `actorResolver: requestUserActorResolver()` (reads `req.user`), or your own |
49
+ | Every tool callable by any resolved actor | `@AiTool({ roles })`, `defaultRoles`, or a `rolesPolicy` |
50
+ | `'You are a helpful assistant.'` | `systemPrompt: 'You are …'` or `({ actor }) => …`; `@Agent` classes for personas |
51
+ | `@AiTool` name = class name minus `Tool`, camelCased; kind `read` | `@AiTool({ name, kind: 'action' })` |
52
+
53
+ **Anonymous mode, and what it means.** With no `actorResolver`, anyone who can reach the API can
54
+ chat, and each browser gets its own identity: a random token in an `HttpOnly`, `SameSite=Lax`
55
+ cookie (`Secure` over HTTPS), the actor id a SHA-256 digest of it. Threads, quota and attachments
56
+ are scoped by that id, so visitors never see each other's conversations; clearing cookies starts
57
+ over. `action` tools still park on approval — but the approver is the requester, so for an anonymous
58
+ visitor approval is a confirmation step, not an authorization: gate a consequential tool with
59
+ `roles` (which anonymous actors do not hold). Without a quota, a public deployment's model spend is
60
+ unbounded — set `quota` limits (they apply per anonymous id) before going public. Requiring login
61
+ is one line: `actorResolver: requestUserActorResolver()`.
43
62
 
44
63
  ### How a chat talks about a tool
45
64
 
@@ -123,9 +142,13 @@ With nothing bound to `AGENT_ATTACHMENT_STAGING` there is no way to tell whose m
123
142
  turn carrying attachments is refused with `501`. Text-only turns are unaffected. At most 10
124
143
  attachments per turn.
125
144
 
126
- The optional `POST /agent/attachments` upload route (`attachments: { upload: true }`) aborts a body
127
- larger than `HARD_MAX_ATTACHMENT_BYTES` (32 MiB) while it streams; `attachments.maxBytes` narrows
128
- that ceiling at request time and cannot raise it.
145
+ `POST /agent/attachments` (multipart, live whenever a staging store is bound — `501` otherwise)
146
+ aborts a body larger than `HARD_MAX_ATTACHMENT_BYTES` (32 MiB) while it streams; the configured
147
+ `maxBytes` narrows that ceiling at request time and cannot raise it. The limits come from ONE place:
148
+ the staging store's own `describe()` when it declares them (`AgentMediaAttachmentsModule` does),
149
+ else `AgentModule`'s `attachments: { maxBytes, allowedContentTypes }`, else 20 MiB and the default
150
+ types. `GET /agent/config` serves them — with the upload mode (`multipart` or `resumable`) and the
151
+ per-message cap — so a client never repeats them.
129
152
 
130
153
  ### Sweeping attachments nobody sent
131
154
 
@@ -151,8 +174,7 @@ regenerating a turn does — deletes messages and frees their media again. If ei
151
174
  missing the call raises `501` rather than reporting everything as collectable.
152
175
 
153
176
  `GET /agent/attachments` returns the caller's own staged files as metadata (no urls — those are
154
- minted per turn by `resolve`), bounded to `ATTACHMENT_PAGE_SIZE`, and rides the same
155
- `attachments: { upload: true }` flag. It takes no `threadId` filter: a thread's attachments already
177
+ minted per turn by `resolve`), bounded to `ATTACHMENT_PAGE_SIZE`. It takes no `threadId` filter: a thread's attachments already
156
178
  ride on its messages in `GET /agent/threads/:id`.
157
179
 
158
180
  ### Attachments on nestjs-media (`@dudousxd/nestjs-agent/media`)
@@ -199,10 +221,14 @@ Each attachment is a media record (`ownerType: 'agent-actor'`, `ownerId: actor.i
199
221
  | 3. complete | `POST /agent/attachments/uploads/:mediaId/complete` → `MessageAttachment` | agent — checks the bytes landed at the declared size |
200
222
  | drop | `DELETE /agent/attachments/uploads/:mediaId` | agent — aborts the session, deletes bytes + record |
201
223
 
202
- `@dudousxd/nestjs-agent-react/media` drives all three for `useAttachments`. `POST /agent/attachments`
203
- (the buffered multipart route, `attachments: { upload: true }`) keeps working on the same store — its
204
- `stage()` writes a media record too — but it is not needed with this module; `GET
205
- /agent/attachments` lists either kind.
224
+ `@dudousxd/nestjs-agent-react/media` drives all three for `useAttachments`; this is THE upload route
225
+ with this module (`GET /agent/config` answers `upload: 'resumable'`). The buffered multipart `POST
226
+ /agent/attachments` still stages into the same store, but is not the documented path here; `GET
227
+ /agent/attachments` lists either kind. The module's `maxBytes` / `allowedContentTypes` are the only
228
+ limits in force — `AgentModule`'s `attachments` option is ignored while it is imported.
229
+
230
+ A replayed thread (`GET /agent/threads/:id`) re-mints each attachment's url from the store by
231
+ `mediaId` on every read, so an old turn never shows an expired presigned link.
206
232
 
207
233
  `resolve` hands the model provider a presigned url when the disk can mint one (`urlExpiresInSeconds`,
208
234
  default 7 days), the disk's public `url()` with `visibility: 'public'`, or — on a disk that can do
@@ -254,8 +280,8 @@ export class S3Staging implements AttachmentStagingStore {
254
280
  { provide: AGENT_ATTACHMENT_STAGING, useClass: S3Staging }
255
281
  ```
256
282
 
257
- Then either set `attachments: { upload: true }` for the built-in `POST /agent/attachments` (which
258
- calls your `stage`), or upload through your own route and give the React side your own `upload`
283
+ Then the built-in `POST /agent/attachments` calls your `stage` (no flag — binding the store is what
284
+ turns it on), or upload through your own route and give the React side your own `upload`
259
285
  (`useAttachments({ upload })` / a backend's `uploadAttachment`) that resolves to a `{ mediaId, … }`
260
286
  your `resolve` recognises. `list` is optional and only needed for `GET /agent/attachments` and
261
287
  sweeping.
@@ -286,36 +312,37 @@ bundled stores and `InMemoryAgentStore` have them) — `501` otherwise.
286
312
 
287
313
  ```ts
288
314
  AgentModule.forRoot({
289
- model: aiSdkModel('openai/gpt-4o-mini', { resolveModel: (id) => gateway(id) }),
290
- models: {
291
- list: ({ actor }) => ({
292
- default: 'openai/gpt-4o-mini',
293
- providers: [
294
- {
295
- id: 'openai',
296
- label: 'OpenAI',
297
- models: [
298
- { id: 'openai/gpt-4o-mini', label: 'GPT-4o mini', badges: ['fast'], available: true },
299
- {
300
- id: 'openai/o3',
301
- label: 'o3',
302
- badges: ['reasoning'],
303
- available: actor.roles?.includes('PRO') === true,
304
- unavailableReason: 'Pro plan',
305
- },
306
- ],
307
- },
308
- ],
309
- }),
310
- },
315
+ model: aiSdkModels(
316
+ {
317
+ 'openai/gpt-4o-mini': { model: 'openai/gpt-4o-mini', label: 'GPT-4o mini', badges: ['fast'] },
318
+ 'openai/o3': { model: 'openai/o3', label: 'o3', badges: ['reasoning'] },
319
+ },
320
+ { default: 'openai/gpt-4o-mini' },
321
+ ),
311
322
  });
312
323
  ```
313
324
 
325
+ `aiSdkModels` (from `@dudousxd/nestjs-agent-ai-sdk`) carries its own catalog, used when `models` is
326
+ omitted. For availability that depends on the caller, pass a `models` catalog of your own next to it:
327
+
328
+ ```ts
329
+ models: {
330
+ list: ({ actor }) => ({
331
+ default: 'openai/gpt-4o-mini',
332
+ providers: [{ id: 'openai', label: 'OpenAI', models: [
333
+ { id: 'openai/gpt-4o-mini', label: 'GPT-4o mini', available: true },
334
+ { id: 'openai/o3', label: 'o3', available: actor.roles?.includes('PRO') === true, unavailableReason: 'Pro plan' },
335
+ ] }],
336
+ }),
337
+ },
338
+ ```
339
+
314
340
  `GET /agent/models?agent=` answers the catalog for the caller (empty without one). A turn runs on
315
341
  the send's `model`, else the thread's pinned one (`PATCH /agent/threads/:id { model }`), else the
316
342
  provider's default; the id reaches the provider as `ModelTurnArgs.model` for every call of the turn.
317
343
  A model the catalog does not list as available for that actor and agent is refused with `400` —
318
- when pinned and again on each turn. `staticModelCatalog(view)` wraps a fixed list.
344
+ when pinned and again on each turn — and a provider asked for a model it does not serve fails the
345
+ turn rather than answering on another. `staticModelCatalog(view)` wraps a fixed list.
319
346
 
320
347
  ### Budgets: `GET /agent/quota`
321
348
 
@@ -328,13 +355,13 @@ and only reports. Configure it and it also gates sends — a `blocked` report re
328
355
  ```ts
329
356
  AgentModule.forRoot({
330
357
  // ceilings on the default ledger provider
331
- quotaLimits: { day: { tokens: 200_000 }, month: { usd: 20 } },
358
+ quota: { limits: { day: { tokens: 200_000 }, month: { usd: 20 } } },
332
359
  // …or your own budget, e.g. an AI-gateway spend cap:
333
- // quotaProvider: { report: async ({ actor }) => myGatewayBudget(actor.id) },
360
+ // quota: { report: async ({ actor }) => myGatewayBudget(actor.id) },
334
361
  });
335
362
  ```
336
363
 
337
- `GET /agent/quota/today` stays as it was.
364
+ In anonymous mode the limits apply per browser (each is its own actor).
338
365
 
339
366
  ### Who approves an action, and for how long
340
367
 
@@ -98,7 +98,7 @@ interface QuotaWindowLimits {
98
98
  tokens?: number;
99
99
  usd?: number;
100
100
  }
101
- /** Per-window ceilings: `AgentModule.forRoot({ quotaLimits })`. */
101
+ /** Per-window ceilings: `AgentModule.forRoot({ quota: { limits } })`. */
102
102
  interface QuotaLimits {
103
103
  day?: QuotaWindowLimits;
104
104
  month?: QuotaWindowLimits;
@@ -106,8 +106,7 @@ interface QuotaLimits {
106
106
  /**
107
107
  * The default {@link QuotaProvider}: windows read off the usage ledger the loop already writes.
108
108
  *
109
- * - `day` — always; `usedTokens`/limit come from the bound {@link QuotaStore} when there is one (so
110
- * the report matches what the loop enforces), spend from the ledger.
109
+ * - `day` — always.
111
110
  * - `month` — when the store implements `usageBetween`.
112
111
  *
113
112
  * `limits` add ceilings per window (tokens and/or USD). A window that reaches one makes the report
@@ -115,9 +114,8 @@ interface QuotaLimits {
115
114
  */
116
115
  declare class LedgerQuotaProvider implements QuotaProvider {
117
116
  private readonly store;
118
- private readonly quota?;
119
117
  private readonly limits;
120
- constructor(store: AgentStore, quota?: QuotaStore | undefined, limits?: QuotaLimits);
118
+ constructor(store: AgentStore, limits?: QuotaLimits);
121
119
  report(query: QuotaQuery): Promise<QuotaReport>;
122
120
  private dayWindow;
123
121
  private window;
@@ -144,8 +142,9 @@ declare class LedgerQuotaProvider implements QuotaProvider {
144
142
  */
145
143
  type AgentSurface = 'http' | 'engine' | 'both';
146
144
  /**
147
- * `AgentModuleOptions.attachments` — bounds and content-type allowlist for the optional
148
- * `POST /agent/attachments` upload controller (see `attachments.upload`).
145
+ * `AgentModuleOptions.attachments` — limits for `POST <base>/attachments`, when the bound
146
+ * `AGENT_ATTACHMENT_STAGING` store does not declare its own (`AgentMediaAttachmentsModule` does, and
147
+ * then it is the only source). Served to clients by `GET <base>/config`.
149
148
  */
150
149
  interface AgentAttachmentsOptions {
151
150
  /** Per-file size cap. Defaults to 20 MiB. */
@@ -155,13 +154,6 @@ interface AgentAttachmentsOptions {
155
154
  * `image/png`, `image/jpeg`, `image/gif`, `image/webp`, `application/pdf`, `text/plain`, `text/csv`.
156
155
  */
157
156
  allowedContentTypes?: string[];
158
- /**
159
- * Mount `POST /agent/attachments`. Defaults to `false` — the controller is build-time (static)
160
- * wiring, so this can't be inferred from whether `AGENT_ATTACHMENT_STAGING` ends up bound (that's
161
- * a DI-time fact); set it explicitly. `true` with no `AGENT_ATTACHMENT_STAGING` provider bound
162
- * fails boot loudly instead of silently mounting a controller that 501s on every request.
163
- */
164
- upload?: boolean;
165
157
  }
166
158
  /**
167
159
  * `AgentModuleOptions.skills` — where a turn's skills come from, and which scopes an actor draws
@@ -219,45 +211,43 @@ interface AgentMemoryOptions {
219
211
  maxFactChars?: number;
220
212
  }
221
213
  interface AgentModuleOptions {
222
- /** The LLM provider (e.g. a Vercel AI SDK wrapper). Required. */
214
+ /**
215
+ * The LLM provider — `aiSdkModel(openai('gpt-5-mini'))`, or `aiSdkModels({ … })` for a model
216
+ * picker (its catalog is used when `models` is omitted). The only required option.
217
+ */
223
218
  model: ModelProvider;
224
219
  /**
225
- * Persistence adapter. Optional — omit it and import a store module (e.g.
226
- * `MikroOrmAgentStoreModule.forFeature()`) that binds `AGENT_STORE` globally instead. When
227
- * provided here it takes precedence within this module's scope.
220
+ * The base prompt of the default agent (and of any `@Agent` that declares none) — a string, or a
221
+ * function of the turn's `{ actor, agentName, pageContext }`. Omit → `'You are a helpful
222
+ * assistant.'`.
223
+ */
224
+ systemPrompt?: string | PromptBuilder;
225
+ /**
226
+ * Persistence adapter. Omit it and either import a store module (e.g.
227
+ * `MikroOrmAgentStoreModule.forFeature()`, which binds `AGENT_STORE` globally — it is found and
228
+ * used), or run on the built-in in-memory store — fine for a demo, logged at boot as not for
229
+ * production: threads vanish on restart and are not shared between processes.
228
230
  */
229
231
  store?: AgentStore;
230
232
  /** Live token transport. Defaults to a single-process in-memory sink. */
231
233
  sink?: TokenStreamSink;
232
234
  /**
233
- * A {@link QuotaStore} for the daily token budget. Optional — omit (and omit `quotaLimitTokens`)
234
- * to disable quotas. Provide this to plug a custom budget; for the common case use the simpler
235
- * `quotaLimitTokens` instead, which binds the built-in ledger-backed store.
235
+ * The caller's budget. `{ limits: { day?: { tokens?, usd? }, month?: { tokens?, usd? } } }` puts
236
+ * ceilings on the built-in ledger provider; a {@link QuotaProvider} of your own replaces it. Either
237
+ * way `GET <base>/quota` reports it and a send whose report comes back `blocked` is refused with
238
+ * `429` before the turn starts. Omit → reported (usage off the ledger), never enforced — for a
239
+ * public (anonymous) deployment that means unbounded model spend, so set limits there; they apply
240
+ * per actor, which is per browser in anonymous mode.
236
241
  */
237
- quota?: QuotaStore;
242
+ quota?: {
243
+ limits: QuotaLimits;
244
+ } | QuotaProvider;
238
245
  /**
239
246
  * Which models a caller may pick — `GET <base>/models`, a send's `model`, a thread's pinned model
240
- * (`PATCH <base>/threads/:id { model }`). The picked id reaches the {@link ModelProvider} as
241
- * `ModelTurnArgs.model`. Omit → an empty catalog, and a request naming a model is refused.
247
+ * (`PATCH <base>/threads/:id { model }`). Omit → the catalog the model provider carries
248
+ * (`aiSdkModels`), else none, and a request naming a model is refused.
242
249
  */
243
250
  models?: ModelCatalog;
244
- /**
245
- * The actor's budget across windows, for `GET <base>/quota` — and, once set, a gate: a send whose
246
- * report comes back `blocked` is refused with `429` before the turn starts. Default: a
247
- * {@link LedgerQuotaProvider} over the usage ledger (reporting only, unless `quotaLimits` is set).
248
- */
249
- quotaProvider?: QuotaProvider;
250
- /**
251
- * Ceilings for the default ledger provider's windows — `{ day?: { tokens?, usd? }, month?: {
252
- * tokens?, usd? } }`. Setting it turns the send gate on. Ignored when `quotaProvider` is given.
253
- */
254
- quotaLimits?: QuotaLimits;
255
- /**
256
- * Daily per-actor token budget, enforced against the persisted usage ledger by the built-in
257
- * `LedgerQuotaStore`. A convenience over wiring a {@link QuotaStore} by hand — set this and quotas
258
- * turn on with no extra store. Ignored when an explicit `quota` is provided. Omit to disable.
259
- */
260
- quotaLimitTokens?: number;
261
251
  /** Tool authorization gate. Defaults to role-in-`defaultRoles`. */
262
252
  rolesPolicy?: RolesPolicy;
263
253
  /**
@@ -267,15 +257,20 @@ interface AgentModuleOptions {
267
257
  * deciding actor must hold that role. See `ApprovalPolicy`.
268
258
  */
269
259
  approvalPolicy?: ApprovalPolicy;
270
- /** Default roles a tool requires when its `roles` is omitted. Defaults to `['ADMIN']`. */
260
+ /**
261
+ * Roles a tool requires when its own `roles` is omitted. Default `[]` — no restriction: every
262
+ * tool is callable by whoever `actorResolver` resolved (an anonymous visitor included, when none is
263
+ * configured). `action` tools still park on approval regardless.
264
+ */
271
265
  defaultRoles?: string[];
272
266
  /**
273
- * Resolves the acting actor for each request (the identity seam). Required — the agent NEVER
274
- * fabricates a caller, so this is a compile-time obligation, not an optional with a throwing
275
- * placeholder. Read your authenticated principal here (session / JWT / `nestjs-context`), or use
276
- * the opt-in `HeaderActorResolver` for demos and header-trusting gateways.
267
+ * Resolves the acting actor for each request (the identity seam). Omit → the endpoints are public
268
+ * and every browser is its own anonymous actor (`AnonymousActorResolver`: an `HttpOnly` cookie,
269
+ * the actor id a digest of it), so visitors never see each other's threads, quota or attachments.
270
+ * A boot notice says so. Require login with `requestUserActorResolver()` (reads `req.user`), or a
271
+ * resolver of your own over your session / JWT / `nestjs-context`.
277
272
  */
278
- actorResolver: ActorResolver;
273
+ actorResolver?: ActorResolver;
279
274
  /** Route prefix the controllers mount under. Defaults to `'agent'` (→ `/agent/chat`, …). */
280
275
  path?: string;
281
276
  /**
@@ -346,7 +341,7 @@ interface AgentModuleOptions {
346
341
  /**
347
342
  * A {@link HistoryPolicy} of your own — for a window the built-in can't express (pinning the
348
343
  * thread's opening brief, keeping every message that carries a tool result, a budget that varies
349
- * by actor). A convenience-vs-custom pair like `quotaLimitTokens`/`quota`: this outranks a
344
+ * by actor). A convenience-vs-custom pair: this outranks a
350
345
  * module-wide `history`, and `@Agent({ history })` outranks both for the agent that declares it.
351
346
  * `select` MUST be pure — see the SPI's determinism contract.
352
347
  */
@@ -411,14 +406,14 @@ interface AgentModuleOptions {
411
406
  defaultAgent?: string;
412
407
  /**
413
408
  * Guard(s) applied uniformly to EVERY controller this module mounts (chat, threads, tool-call,
414
- * quota, agents, and — when `attachments.upload` is set — attachments). Third-party controller
409
+ * quota, agents, attachments, config, …). Third-party controller
415
410
  * classes can't be annotated with `@UseGuards` by consumers, so without this option every route is
416
411
  * open beyond whatever `actorResolver` itself enforces. Guard classes are added to this module's
417
412
  * `providers` so Nest can DI-instantiate them; if a guard has its own dependencies, make sure they
418
413
  * resolve from this module's imports or a global module.
419
414
  */
420
415
  guards?: Type<CanActivate>[];
421
- /** Bounds/allowlist for the optional attachment-upload controller. Omit → 20 MiB, the documented default types, not mounted. */
416
+ /** Upload limits when the staging store declares none. Omit → 20 MiB and the default types. */
422
417
  attachments?: AgentAttachmentsOptions;
423
418
  /**
424
419
  * Which half of the module this process mounts (see {@link AgentSurface}). Omit → `'both'`,
@@ -441,17 +436,6 @@ interface AgentModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
441
436
  * the runner. Requires importing `AgentDurableModule` and a configured `DurableModule`.
442
437
  */
443
438
  durable?: boolean;
444
- /**
445
- * Set when `AGENT_STORE` is bound by a globally-imported store module (e.g.
446
- * `MikroOrmAgentStoreModule.forFeature()`) instead of returned as `store` from `useFactory`.
447
- *
448
- * `forRoot` decides this synchronously (`store` present or not), but the async factory resolves
449
- * too late to inspect — so by default `forRootAsync` binds `AGENT_STORE` from the factory result,
450
- * which would shadow a global store binding with `undefined` when the factory omits `store`. Set
451
- * this to `true` to skip the local binding and defer to the global one. Leave it unset (and return
452
- * `store` from the factory) when the store is constructed inside the factory.
453
- */
454
- externalStore?: boolean;
455
439
  /**
456
440
  * Guard(s) applied uniformly to every controller this module mounts. A STATIC field on the async
457
441
  * config object itself — NOT part of what `useFactory` resolves — because controllers (and the
@@ -461,16 +445,9 @@ interface AgentModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
461
445
  * `useFactory`. Same default-open caveat as `AgentModuleOptions.guards`.
462
446
  */
463
447
  guards?: Type<CanActivate>[];
464
- /**
465
- * Mount `POST /agent/attachments`. Static, build-time control (same reasoning as `durable`/`path`
466
- * above) — `useFactory` resolves too late to decide which controllers exist. The resolved
467
- * `AgentModuleOptions.attachments` (maxBytes/allowedContentTypes) still applies at request time;
468
- * only the yes/no mount decision has to live here.
469
- */
470
- attachmentsUpload?: boolean;
471
448
  /**
472
449
  * Which half of the module this process mounts (see {@link AgentSurface}). Same static-wiring
473
- * reasoning as `attachmentsUpload`/`durable` above — `useFactory` resolves too late to decide
450
+ * reasoning as `durable` above — `useFactory` resolves too late to decide
474
451
  * which controllers exist. Omit → `'both'`, today's behavior with zero change.
475
452
  */
476
453
  surface?: AgentSurface;
@@ -498,11 +475,10 @@ declare class AgentDepsFactory {
498
475
  private readonly registry;
499
476
  private readonly agents;
500
477
  private readonly promptContributors;
501
- private readonly quota;
502
478
  private readonly pricingStore;
503
479
  private readonly skills;
504
480
  private readonly memory;
505
- 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);
481
+ constructor(options: AgentModuleOptions, model: ModelProvider, store: AgentStore, sink: TokenStreamSink, rolesPolicy: RolesPolicy, registry: ToolRegistry, agents: AgentRegistry, promptContributors: PromptContributor[], pricingStore: AgentPricingStore | undefined, skills: SkillsConfig | undefined, memory: MemoryConfig | undefined);
506
482
  /** The turn's memory seam, shared with the read-back endpoint. Undefined → memory is not configured. */
507
483
  memoryConfig(): MemoryConfig | undefined;
508
484
  /** The turn's skills seam, shared with the listing endpoint. Undefined → skills are not configured. */