@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 +159 -34
- package/dist/{agent-deps.factory-DuTy1UyU.d.cts → agent-deps.factory-BRTBu0lV.d.cts} +25 -21
- package/dist/{agent-deps.factory-DuTy1UyU.d.ts → agent-deps.factory-BRTBu0lV.d.ts} +25 -21
- package/dist/durable/index.cjs +153 -23
- package/dist/durable/index.cjs.map +1 -1
- package/dist/durable/index.d.cts +1 -1
- package/dist/durable/index.d.ts +1 -1
- package/dist/durable/index.js +156 -26
- package/dist/durable/index.js.map +1 -1
- package/dist/index.cjs +219 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +100 -11
- package/dist/index.d.ts +100 -11
- package/dist/index.js +218 -27
- package/dist/index.js.map +1 -1
- package/dist/media/index.cjs +641 -0
- package/dist/media/index.cjs.map +1 -0
- package/dist/media/index.d.cts +238 -0
- package/dist/media/index.d.ts +238 -0
- package/dist/media/index.js +608 -0
- package/dist/media/index.js.map +1 -0
- package/package.json +20 -3
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
|
|
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({
|
|
26
|
-
export class GetWeatherTool
|
|
27
|
-
async execute(
|
|
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
|
|
41
|
-
|
|
42
|
-
|
|
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:
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
226
|
-
* `
|
|
227
|
-
*
|
|
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
|
-
/**
|
|
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).
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
226
|
-
* `
|
|
227
|
-
*
|
|
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
|
-
/**
|
|
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).
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
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
|
|
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
|
package/dist/durable/index.cjs
CHANGED
|
@@ -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
|
-
|
|
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) =>
|
|
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
|
|
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
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
4678
|
+
...sharedProviders(options.durable ?? false),
|
|
4548
4679
|
...guardProviders(options.guards)
|
|
4549
4680
|
],
|
|
4550
|
-
exports: exportsFor(
|
|
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
|
|
4712
|
+
...sharedProviders(options.durable ?? false),
|
|
4583
4713
|
...guardProviders(options.guards)
|
|
4584
4714
|
],
|
|
4585
|
-
exports: exportsFor(
|
|
4715
|
+
exports: exportsFor()
|
|
4586
4716
|
};
|
|
4587
4717
|
}
|
|
4588
4718
|
};
|