@dudousxd/nestjs-agent 1.9.0 → 1.11.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
 
@@ -155,6 +174,111 @@ minted per turn by `resolve`), bounded to `ATTACHMENT_PAGE_SIZE`, and rides the
155
174
  `attachments: { upload: true }` flag. It takes no `threadId` filter: a thread's attachments already
156
175
  ride on its messages in `GET /agent/threads/:id`.
157
176
 
177
+ ### Attachments on nestjs-media (`@dudousxd/nestjs-agent/media`)
178
+
179
+ A ready `AGENT_ATTACHMENT_STAGING` backed by [`@dudousxd/nestjs-media`](https://davidecarvalho.github.io/aviary/docs/media),
180
+ with resumable (tus) uploads. `@dudousxd/nestjs-media` is an **optional** peer: only this subpath
181
+ imports it, and nothing on the root entry needs it.
182
+
183
+ Minimal — next to a `MediaModule` that has `store`, `uploadSessions` and `tus`:
184
+
185
+ ```ts
186
+ @Module({
187
+ imports: [
188
+ MediaModule.forRoot({ /* disks, store, uploadSessions, tus */ }),
189
+ AgentModule.forRoot({ /* … */ }),
190
+ AgentMediaAttachmentsModule.forRoot(), // ← the whole integration
191
+ ],
192
+ })
193
+ // main.ts — nestjs-media's own requirement for tus PATCH bodies:
194
+ app.useBodyParser('raw', { type: 'application/offset+octet-stream' });
195
+ ```
196
+
197
+ Everything is overridable when you need it:
198
+
199
+ ```ts
200
+ AgentMediaAttachmentsModule.forRoot({
201
+ maxBytes: 50 * 1024 * 1024,
202
+ allowedContentTypes: ['image/png', 'image/jpeg', 'application/pdf'],
203
+ collection: 'chat-files',
204
+ canAccess: ({ allowed, actor }) => allowed || actor.roles?.includes('support') === true,
205
+ resolveUrl: (record) => signMyProxyUrl(record.id), // instead of presign / public / inline
206
+ indexForRag: true, // see below
207
+ guards: [SessionGuard],
208
+ });
209
+ ```
210
+
211
+ Each attachment is a media record (`ownerType: 'agent-actor'`, `ownerId: actor.id`, collection
212
+ `agent-attachments`). The upload never goes through the agent:
213
+
214
+ | Step | Route | Who serves it |
215
+ | --- | --- | --- |
216
+ | 1. open | `POST /agent/attachments/uploads` `{ filename, contentType, size }` → `{ mediaId, uploadId, location }` | agent — validates type/size and opens a tus session the actor owns |
217
+ | 2. bytes | `PATCH <location>` (tus, chunked, resumable) | **nestjs-media** |
218
+ | 3. complete | `POST /agent/attachments/uploads/:mediaId/complete` → `MessageAttachment` | agent — checks the bytes landed at the declared size |
219
+ | drop | `DELETE /agent/attachments/uploads/:mediaId` | agent — aborts the session, deletes bytes + record |
220
+
221
+ `@dudousxd/nestjs-agent-react/media` drives all three for `useAttachments`. `POST /agent/attachments`
222
+ (the buffered multipart route, `attachments: { upload: true }`) keeps working on the same store — its
223
+ `stage()` writes a media record too — but it is not needed with this module; `GET
224
+ /agent/attachments` lists either kind.
225
+
226
+ `resolve` hands the model provider a presigned url when the disk can mint one (`urlExpiresInSeconds`,
227
+ default 7 days), the disk's public `url()` with `visibility: 'public'`, or — on a disk that can do
228
+ neither, such as local disk in development — the bytes inline as a `data:` url (logged once; that
229
+ url is persisted with the message). `resolveUrl(record, { disk })` takes over entirely. The actor may
230
+ use media they own, and media a message in one of their own threads already carries; anything else
231
+ resolves `null` (`403` on the chat route).
232
+
233
+ Cleanup: an upload abandoned half-way is already a record, so it shows up in `list` and ages into
234
+ `collectableAttachments`. Delete through the store so the tus session is aborted too:
235
+
236
+ ```ts
237
+ const media = app.get(MediaAttachmentStaging);
238
+ for (const entry of await agents.collectableAttachments(actor, { olderThan })) {
239
+ await media.remove(entry.mediaId);
240
+ }
241
+ ```
242
+
243
+ `indexForRag: true` announces every ready attachment on `aviary:media:attach` (and its removal on
244
+ `aviary:media:delete`) so `@dudousxd/nestjs-agent-rag-media` indexes it. Off by default — only turn
245
+ it on where retrieval is filtered by owner (`FilteredRetriever` on `ownerType`/`ownerId`), and
246
+ restrict rag-media's `collections` accordingly.
247
+
248
+ `forRootAsync({ imports, inject, useFactory, path, guards })` takes the same options; `path` must
249
+ match `AgentModule`'s `path` (default `agent`), and `guards` gates the upload routes.
250
+
251
+ ### Bring your own storage
252
+
253
+ Nothing requires nestjs-media. Bind your own `AttachmentStagingStore` and upload however you like:
254
+
255
+ ```ts
256
+ @Injectable()
257
+ export class S3Staging implements AttachmentStagingStore {
258
+ async stage({ data, filename, contentType, actor }: StageAttachmentInput) {
259
+ const mediaId = randomUUID();
260
+ await this.s3.put(`chat/${actor.id}/${mediaId}`, data, { contentType });
261
+ await this.db.insert(files).values({ mediaId, owner: actor.id, filename, contentType });
262
+ return { mediaId, url: '', contentType, name: filename };
263
+ }
264
+ async resolve({ mediaId, actor }: ResolveAttachmentInput) {
265
+ const row = await this.db.query.files.findFirst({ where: eq(files.mediaId, mediaId) });
266
+ if (!row || row.owner !== actor.id) return null; // unknown and foreign look the same
267
+ const url = await this.s3.presign(`chat/${actor.id}/${mediaId}`, 3600);
268
+ return { mediaId, url, contentType: row.contentType, name: row.filename };
269
+ }
270
+ }
271
+
272
+ // alongside AgentModule, in a @Global() module (or any module AgentModule can see):
273
+ { provide: AGENT_ATTACHMENT_STAGING, useClass: S3Staging }
274
+ ```
275
+
276
+ Then either set `attachments: { upload: true }` for the built-in `POST /agent/attachments` (which
277
+ calls your `stage`), or upload through your own route and give the React side your own `upload`
278
+ (`useAttachments({ upload })` / a backend's `uploadAttachment`) that resolves to a `{ mediaId, … }`
279
+ your `resolve` recognises. `list` is optional and only needed for `GET /agent/attachments` and
280
+ sweeping.
281
+
158
282
  ### Who may read a run
159
283
 
160
284
  `GET /agent/chat/:runId/stream` and `POST /agent/chat/:runId/cancel` both resolve the acting actor
@@ -181,36 +305,37 @@ bundled stores and `InMemoryAgentStore` have them) — `501` otherwise.
181
305
 
182
306
  ```ts
183
307
  AgentModule.forRoot({
184
- model: aiSdkModel('openai/gpt-4o-mini', { resolveModel: (id) => gateway(id) }),
185
- models: {
186
- list: ({ actor }) => ({
187
- default: 'openai/gpt-4o-mini',
188
- providers: [
189
- {
190
- id: 'openai',
191
- label: 'OpenAI',
192
- models: [
193
- { id: 'openai/gpt-4o-mini', label: 'GPT-4o mini', badges: ['fast'], available: true },
194
- {
195
- id: 'openai/o3',
196
- label: 'o3',
197
- badges: ['reasoning'],
198
- available: actor.roles?.includes('PRO') === true,
199
- unavailableReason: 'Pro plan',
200
- },
201
- ],
202
- },
203
- ],
204
- }),
205
- },
308
+ model: aiSdkModels(
309
+ {
310
+ 'openai/gpt-4o-mini': { model: 'openai/gpt-4o-mini', label: 'GPT-4o mini', badges: ['fast'] },
311
+ 'openai/o3': { model: 'openai/o3', label: 'o3', badges: ['reasoning'] },
312
+ },
313
+ { default: 'openai/gpt-4o-mini' },
314
+ ),
206
315
  });
207
316
  ```
208
317
 
318
+ `aiSdkModels` (from `@dudousxd/nestjs-agent-ai-sdk`) carries its own catalog, used when `models` is
319
+ omitted. For availability that depends on the caller, pass a `models` catalog of your own next to it:
320
+
321
+ ```ts
322
+ models: {
323
+ list: ({ actor }) => ({
324
+ default: 'openai/gpt-4o-mini',
325
+ providers: [{ id: 'openai', label: 'OpenAI', models: [
326
+ { id: 'openai/gpt-4o-mini', label: 'GPT-4o mini', available: true },
327
+ { id: 'openai/o3', label: 'o3', available: actor.roles?.includes('PRO') === true, unavailableReason: 'Pro plan' },
328
+ ] }],
329
+ }),
330
+ },
331
+ ```
332
+
209
333
  `GET /agent/models?agent=` answers the catalog for the caller (empty without one). A turn runs on
210
334
  the send's `model`, else the thread's pinned one (`PATCH /agent/threads/:id { model }`), else the
211
335
  provider's default; the id reaches the provider as `ModelTurnArgs.model` for every call of the turn.
212
336
  A model the catalog does not list as available for that actor and agent is refused with `400` —
213
- when pinned and again on each turn. `staticModelCatalog(view)` wraps a fixed list.
337
+ when pinned and again on each turn — and a provider asked for a model it does not serve fails the
338
+ turn rather than answering on another. `staticModelCatalog(view)` wraps a fixed list.
214
339
 
215
340
  ### Budgets: `GET /agent/quota`
216
341
 
@@ -219,12 +219,22 @@ interface AgentMemoryOptions {
219
219
  maxFactChars?: number;
220
220
  }
221
221
  interface AgentModuleOptions {
222
- /** The LLM provider (e.g. a Vercel AI SDK wrapper). Required. */
222
+ /**
223
+ * The LLM provider — `aiSdkModel(openai('gpt-5-mini'))`, or `aiSdkModels({ … })` for a model
224
+ * picker (its catalog is used when `models` is omitted). The only required option.
225
+ */
223
226
  model: ModelProvider;
224
227
  /**
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.
228
+ * The base prompt of the default agent (and of any `@Agent` that declares none) — a string, or a
229
+ * function of the turn's `{ actor, agentName, pageContext }`. Omit → `'You are a helpful
230
+ * assistant.'`.
231
+ */
232
+ systemPrompt?: string | PromptBuilder;
233
+ /**
234
+ * Persistence adapter. Omit it and either import a store module (e.g.
235
+ * `MikroOrmAgentStoreModule.forFeature()`, which binds `AGENT_STORE` globally — it is found and
236
+ * used), or run on the built-in in-memory store — fine for a demo, logged at boot as not for
237
+ * production: threads vanish on restart and are not shared between processes.
228
238
  */
229
239
  store?: AgentStore;
230
240
  /** Live token transport. Defaults to a single-process in-memory sink. */
@@ -267,15 +277,20 @@ interface AgentModuleOptions {
267
277
  * deciding actor must hold that role. See `ApprovalPolicy`.
268
278
  */
269
279
  approvalPolicy?: ApprovalPolicy;
270
- /** Default roles a tool requires when its `roles` is omitted. Defaults to `['ADMIN']`. */
280
+ /**
281
+ * Roles a tool requires when its own `roles` is omitted. Default `[]` — no restriction: every
282
+ * tool is callable by whoever `actorResolver` resolved (an anonymous visitor included, when none is
283
+ * configured). `action` tools still park on approval regardless.
284
+ */
271
285
  defaultRoles?: string[];
272
286
  /**
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.
287
+ * Resolves the acting actor for each request (the identity seam). Omit → the endpoints are public
288
+ * and every browser is its own anonymous actor (`AnonymousActorResolver`: an `HttpOnly` cookie,
289
+ * the actor id a digest of it), so visitors never see each other's threads, quota or attachments.
290
+ * A boot notice says so. Require login with `requestUserActorResolver()` (reads `req.user`), or a
291
+ * resolver of your own over your session / JWT / `nestjs-context`.
277
292
  */
278
- actorResolver: ActorResolver;
293
+ actorResolver?: ActorResolver;
279
294
  /** Route prefix the controllers mount under. Defaults to `'agent'` (→ `/agent/chat`, …). */
280
295
  path?: string;
281
296
  /**
@@ -441,17 +456,6 @@ interface AgentModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
441
456
  * the runner. Requires importing `AgentDurableModule` and a configured `DurableModule`.
442
457
  */
443
458
  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
459
  /**
456
460
  * Guard(s) applied uniformly to every controller this module mounts. A STATIC field on the async
457
461
  * config object itself — NOT part of what `useFactory` resolves — because controllers (and the
@@ -219,12 +219,22 @@ interface AgentMemoryOptions {
219
219
  maxFactChars?: number;
220
220
  }
221
221
  interface AgentModuleOptions {
222
- /** The LLM provider (e.g. a Vercel AI SDK wrapper). Required. */
222
+ /**
223
+ * The LLM provider — `aiSdkModel(openai('gpt-5-mini'))`, or `aiSdkModels({ … })` for a model
224
+ * picker (its catalog is used when `models` is omitted). The only required option.
225
+ */
223
226
  model: ModelProvider;
224
227
  /**
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.
228
+ * The base prompt of the default agent (and of any `@Agent` that declares none) — a string, or a
229
+ * function of the turn's `{ actor, agentName, pageContext }`. Omit → `'You are a helpful
230
+ * assistant.'`.
231
+ */
232
+ systemPrompt?: string | PromptBuilder;
233
+ /**
234
+ * Persistence adapter. Omit it and either import a store module (e.g.
235
+ * `MikroOrmAgentStoreModule.forFeature()`, which binds `AGENT_STORE` globally — it is found and
236
+ * used), or run on the built-in in-memory store — fine for a demo, logged at boot as not for
237
+ * production: threads vanish on restart and are not shared between processes.
228
238
  */
229
239
  store?: AgentStore;
230
240
  /** Live token transport. Defaults to a single-process in-memory sink. */
@@ -267,15 +277,20 @@ interface AgentModuleOptions {
267
277
  * deciding actor must hold that role. See `ApprovalPolicy`.
268
278
  */
269
279
  approvalPolicy?: ApprovalPolicy;
270
- /** Default roles a tool requires when its `roles` is omitted. Defaults to `['ADMIN']`. */
280
+ /**
281
+ * Roles a tool requires when its own `roles` is omitted. Default `[]` — no restriction: every
282
+ * tool is callable by whoever `actorResolver` resolved (an anonymous visitor included, when none is
283
+ * configured). `action` tools still park on approval regardless.
284
+ */
271
285
  defaultRoles?: string[];
272
286
  /**
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.
287
+ * Resolves the acting actor for each request (the identity seam). Omit → the endpoints are public
288
+ * and every browser is its own anonymous actor (`AnonymousActorResolver`: an `HttpOnly` cookie,
289
+ * the actor id a digest of it), so visitors never see each other's threads, quota or attachments.
290
+ * A boot notice says so. Require login with `requestUserActorResolver()` (reads `req.user`), or a
291
+ * resolver of your own over your session / JWT / `nestjs-context`.
277
292
  */
278
- actorResolver: ActorResolver;
293
+ actorResolver?: ActorResolver;
279
294
  /** Route prefix the controllers mount under. Defaults to `'agent'` (→ `/agent/chat`, …). */
280
295
  path?: string;
281
296
  /**
@@ -441,17 +456,6 @@ interface AgentModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
441
456
  * the runner. Requires importing `AgentDurableModule` and a configured `DurableModule`.
442
457
  */
443
458
  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
459
  /**
456
460
  * Guard(s) applied uniformly to every controller this module mounts. A STATIC field on the async
457
461
  * config object itself — NOT part of what `useFactory` resolves — because controllers (and the
@@ -953,7 +953,7 @@ var AgentDepsFactory2 = class {
953
953
  } : {},
954
954
  registry: this.registry,
955
955
  promptContributors: this.promptContributors,
956
- systemPrompt: definition?.systemPrompt ?? "You are a helpful assistant.",
956
+ systemPrompt: definition?.systemPrompt ?? this.options.systemPrompt ?? "You are a helpful assistant.",
957
957
  maxSteps: definition?.maxSteps ?? 8,
958
958
  ...definition?.maxDelegationDepth !== void 0 ? {
959
959
  maxDelegationDepth: definition.maxDelegationDepth
@@ -3822,6 +3822,93 @@ var LedgerQuotaStore = class {
3822
3822
  }
3823
3823
  };
3824
3824
 
3825
+ // src/resolver/anonymous-actor-resolver.ts
3826
+ var import_node_crypto2 = require("crypto");
3827
+ var TOKEN_PATTERN = /^[A-Za-z0-9_-]{43}$/;
3828
+ var MINTED = /* @__PURE__ */ Symbol("nestjs-agent:anonymous-token");
3829
+ function readCookie(req, name) {
3830
+ const header = req.headers?.cookie;
3831
+ const raw = Array.isArray(header) ? header.join("; ") : header;
3832
+ if (raw === void 0) return void 0;
3833
+ for (const pair of raw.split(";")) {
3834
+ const index = pair.indexOf("=");
3835
+ if (index === -1) continue;
3836
+ if (pair.slice(0, index).trim() === name) {
3837
+ try {
3838
+ return decodeURIComponent(pair.slice(index + 1).trim());
3839
+ } catch {
3840
+ return void 0;
3841
+ }
3842
+ }
3843
+ }
3844
+ return void 0;
3845
+ }
3846
+ __name(readCookie, "readCookie");
3847
+ function isHttps(req) {
3848
+ if (req.secure === true) return true;
3849
+ const forwarded = req.headers?.["x-forwarded-proto"];
3850
+ const proto = Array.isArray(forwarded) ? forwarded[0] : forwarded;
3851
+ return proto?.split(",")[0]?.trim() === "https";
3852
+ }
3853
+ __name(isHttps, "isHttps");
3854
+ var AnonymousActorResolver = class {
3855
+ static {
3856
+ __name(this, "AnonymousActorResolver");
3857
+ }
3858
+ options;
3859
+ cookieName;
3860
+ roles;
3861
+ constructor(options = {}) {
3862
+ this.options = options;
3863
+ this.cookieName = options.cookieName ?? "agent_anon";
3864
+ this.roles = options.roles ?? [
3865
+ "anonymous"
3866
+ ];
3867
+ }
3868
+ resolve(request) {
3869
+ const req = typeof request === "object" && request !== null ? request : {};
3870
+ let token = req[MINTED] ?? readCookie(req, this.cookieName);
3871
+ if (token === void 0 || !TOKEN_PATTERN.test(token)) {
3872
+ token = (0, import_node_crypto2.randomBytes)(32).toString("base64url");
3873
+ req[MINTED] = token;
3874
+ this.setCookie(req, token);
3875
+ }
3876
+ const digest = (0, import_node_crypto2.createHash)("sha256").update(token).digest("base64url").slice(0, 32);
3877
+ return {
3878
+ id: `anon:${digest}`,
3879
+ roles: [
3880
+ ...this.roles
3881
+ ]
3882
+ };
3883
+ }
3884
+ setCookie(req, token) {
3885
+ const res = req.res;
3886
+ if (res?.setHeader === void 0 || res.headersSent === true) return;
3887
+ const sameSite = this.options.sameSite ?? "lax";
3888
+ const secureOption = this.options.secure ?? "auto";
3889
+ const secure = sameSite === "none" || (secureOption === "auto" ? isHttps(req) : secureOption);
3890
+ const maxAge = Math.round((this.options.maxAgeDays ?? 365) * 24 * 60 * 60);
3891
+ const cookie = [
3892
+ `${this.cookieName}=${token}`,
3893
+ `Path=${this.options.path ?? "/"}`,
3894
+ `Max-Age=${maxAge}`,
3895
+ "HttpOnly",
3896
+ `SameSite=${sameSite === "none" ? "None" : sameSite === "strict" ? "Strict" : "Lax"}`,
3897
+ ...secure ? [
3898
+ "Secure"
3899
+ ] : []
3900
+ ].join("; ");
3901
+ const existing = res.getHeader?.("Set-Cookie");
3902
+ const list = Array.isArray(existing) ? existing.map(String) : existing !== void 0 ? [
3903
+ String(existing)
3904
+ ] : [];
3905
+ res.setHeader("Set-Cookie", [
3906
+ ...list,
3907
+ cookie
3908
+ ]);
3909
+ }
3910
+ };
3911
+
3825
3912
  // src/runner/inline-agent-runner.ts
3826
3913
  var import_nestjs_agent_core25 = require("@dudousxd/nestjs-agent-core");
3827
3914
  var import_common22 = require("@nestjs/common");
@@ -4296,7 +4383,51 @@ function _ts_decorate23(decorators, target, key, desc) {
4296
4383
  }
4297
4384
  __name(_ts_decorate23, "_ts_decorate");
4298
4385
  var DEFAULT_PATH = "agent";
4299
- function sharedProviders(durable, includeStore) {
4386
+ var logger = new import_common23.Logger("AgentModule");
4387
+ function externalStoreWrapper(modules) {
4388
+ for (const module2 of modules.values()) {
4389
+ if (module2.metatype === AgentModule) continue;
4390
+ const wrapper = module2.providers.get(import_nestjs_agent_core27.AGENT_STORE);
4391
+ if (wrapper !== void 0) return wrapper;
4392
+ }
4393
+ return void 0;
4394
+ }
4395
+ __name(externalStoreWrapper, "externalStoreWrapper");
4396
+ function forwardingStore(wrapper) {
4397
+ const target = /* @__PURE__ */ __name(() => {
4398
+ const instance = wrapper.instance;
4399
+ if (instance === void 0 || instance === null) {
4400
+ throw new Error("AgentModule: the AGENT_STORE bound by another module is not ready yet");
4401
+ }
4402
+ return instance;
4403
+ }, "target");
4404
+ return new Proxy({}, {
4405
+ get(_, property) {
4406
+ const store = target();
4407
+ const value = store[property];
4408
+ return typeof value === "function" ? value.bind(store) : value;
4409
+ },
4410
+ has(_, property) {
4411
+ return property in target();
4412
+ }
4413
+ });
4414
+ }
4415
+ __name(forwardingStore, "forwardingStore");
4416
+ function resolveStore(options, modules) {
4417
+ if (options.store !== void 0) return options.store;
4418
+ const external = externalStoreWrapper(modules);
4419
+ if (external !== void 0) return forwardingStore(external);
4420
+ logger.warn("No `store` configured and no store module imported \u2014 using the in-memory store. Threads are lost on restart and not shared between processes: not for production. Pass `store`, or import a store module (e.g. MikroOrmAgentStoreModule.forFeature()).");
4421
+ return new import_nestjs_agent_core27.InMemoryAgentStore();
4422
+ }
4423
+ __name(resolveStore, "resolveStore");
4424
+ function resolveCatalog(options) {
4425
+ if (options.models !== void 0) return options.models;
4426
+ const carried = options.model.catalog;
4427
+ return carried !== void 0 && typeof carried.list === "function" ? carried : void 0;
4428
+ }
4429
+ __name(resolveCatalog, "resolveCatalog");
4430
+ function sharedProviders(durable) {
4300
4431
  const providers = [
4301
4432
  {
4302
4433
  provide: import_nestjs_agent_core27.AGENT_TOOL_REGISTRY,
@@ -4354,7 +4485,11 @@ function sharedProviders(durable, includeStore) {
4354
4485
  },
4355
4486
  {
4356
4487
  provide: import_nestjs_agent_core27.AGENT_ACTOR_RESOLVER,
4357
- useFactory: /* @__PURE__ */ __name((options) => options.actorResolver, "useFactory"),
4488
+ useFactory: /* @__PURE__ */ __name((options) => {
4489
+ if (options.actorResolver !== void 0) return options.actorResolver;
4490
+ logger.warn("No `actorResolver` configured \u2014 the agent endpoints are PUBLIC: every browser gets its own anonymous identity (an HttpOnly cookie). Set `actorResolver: requestUserActorResolver()` (or your own) to require login.");
4491
+ return new AnonymousActorResolver();
4492
+ }, "useFactory"),
4358
4493
  inject: [
4359
4494
  import_nestjs_agent_core27.AGENT_OPTIONS
4360
4495
  ]
@@ -4388,7 +4523,7 @@ function sharedProviders(durable, includeStore) {
4388
4523
  },
4389
4524
  {
4390
4525
  provide: import_nestjs_agent_core27.AGENT_MODEL_CATALOG,
4391
- useFactory: /* @__PURE__ */ __name((o) => o.models, "useFactory"),
4526
+ useFactory: /* @__PURE__ */ __name((o) => resolveCatalog(o), "useFactory"),
4392
4527
  inject: [
4393
4528
  import_nestjs_agent_core27.AGENT_OPTIONS
4394
4529
  ]
@@ -4413,15 +4548,14 @@ function sharedProviders(durable, includeStore) {
4413
4548
  useExisting: AgentApprovalPortAdapter
4414
4549
  }
4415
4550
  ];
4416
- if (includeStore) {
4417
- providers.push({
4418
- provide: import_nestjs_agent_core27.AGENT_STORE,
4419
- useFactory: /* @__PURE__ */ __name((o) => o.store, "useFactory"),
4420
- inject: [
4421
- import_nestjs_agent_core27.AGENT_OPTIONS
4422
- ]
4423
- });
4424
- }
4551
+ providers.push({
4552
+ provide: import_nestjs_agent_core27.AGENT_STORE,
4553
+ useFactory: /* @__PURE__ */ __name((o, modules) => resolveStore(o, modules), "useFactory"),
4554
+ inject: [
4555
+ import_nestjs_agent_core27.AGENT_OPTIONS,
4556
+ import_core4.ModulesContainer
4557
+ ]
4558
+ });
4425
4559
  if (durable) {
4426
4560
  providers.push({
4427
4561
  provide: import_nestjs_agent_core27.AGENT_RUNNER,
@@ -4447,7 +4581,7 @@ function sharedProviders(durable, includeStore) {
4447
4581
  return providers;
4448
4582
  }
4449
4583
  __name(sharedProviders, "sharedProviders");
4450
- function exportsFor(includeStore) {
4584
+ function exportsFor() {
4451
4585
  return [
4452
4586
  import_nestjs_agent_core27.AGENT_OPTIONS,
4453
4587
  import_nestjs_agent_core27.AGENT_TOOL_REGISTRY,
@@ -4456,9 +4590,7 @@ function exportsFor(includeStore) {
4456
4590
  import_nestjs_agent_core27.AGENT_ROLES_POLICY,
4457
4591
  import_nestjs_agent_core27.AGENT_ACTOR_RESOLVER,
4458
4592
  import_nestjs_agent_core27.AGENT_MODEL,
4459
- ...includeStore ? [
4460
- import_nestjs_agent_core27.AGENT_STORE
4461
- ] : [],
4593
+ import_nestjs_agent_core27.AGENT_STORE,
4462
4594
  import_nestjs_agent_core27.AGENT_QUOTA_STORE,
4463
4595
  import_nestjs_agent_core27.AGENT_MODEL_CATALOG,
4464
4596
  import_nestjs_agent_core27.AGENT_QUOTA_PROVIDER,
@@ -4529,7 +4661,6 @@ var AgentModule = class _AgentModule {
4529
4661
  }
4530
4662
  static forRoot(options) {
4531
4663
  const path = options.path ?? DEFAULT_PATH;
4532
- const includeStore = options.store !== void 0;
4533
4664
  applyGuards(options.guards);
4534
4665
  return {
4535
4666
  module: _AgentModule,
@@ -4544,15 +4675,14 @@ var AgentModule = class _AgentModule {
4544
4675
  provide: import_nestjs_agent_core27.AGENT_OPTIONS,
4545
4676
  useValue: options
4546
4677
  },
4547
- ...sharedProviders(options.durable ?? false, includeStore),
4678
+ ...sharedProviders(options.durable ?? false),
4548
4679
  ...guardProviders(options.guards)
4549
4680
  ],
4550
- exports: exportsFor(includeStore)
4681
+ exports: exportsFor()
4551
4682
  };
4552
4683
  }
4553
4684
  static forRootAsync(options) {
4554
4685
  const path = options.path ?? DEFAULT_PATH;
4555
- const includeStore = options.externalStore !== true;
4556
4686
  applyGuards(options.guards);
4557
4687
  return {
4558
4688
  module: _AgentModule,
@@ -4579,10 +4709,10 @@ var AgentModule = class _AgentModule {
4579
4709
  }), "useFactory"),
4580
4710
  inject: options.inject ?? []
4581
4711
  },
4582
- ...sharedProviders(options.durable ?? false, includeStore),
4712
+ ...sharedProviders(options.durable ?? false),
4583
4713
  ...guardProviders(options.guards)
4584
4714
  ],
4585
- exports: exportsFor(includeStore)
4715
+ exports: exportsFor()
4586
4716
  };
4587
4717
  }
4588
4718
  };