@dudousxd/nestjs-agent 1.8.0 → 1.10.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 +138 -0
- package/dist/{agent-deps.factory-Cus_RqwJ.d.cts → agent-deps.factory-DuTy1UyU.d.cts} +7 -1
- package/dist/{agent-deps.factory-Cus_RqwJ.d.ts → agent-deps.factory-DuTy1UyU.d.ts} +7 -1
- package/dist/durable/index.cjs +19 -1
- 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 +19 -1
- package/dist/durable/index.js.map +1 -1
- package/dist/genui/index.cjs +186 -0
- package/dist/genui/index.cjs.map +1 -0
- package/dist/genui/index.d.cts +81 -0
- package/dist/genui/index.d.ts +81 -0
- package/dist/genui/index.js +155 -0
- package/dist/genui/index.js.map +1 -0
- package/dist/index.cjs +21 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +20 -0
- 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 +29 -2
package/README.md
CHANGED
|
@@ -70,6 +70,39 @@ tools THIS actor can reach through that agent (the default agent when omitted; `
|
|
|
70
70
|
one), built by `ToolRegistry.visibleSpecs`, the same gates the model is offered tools through. The
|
|
71
71
|
React package's `useToolCatalog` reads it. The model never sees `presentation`.
|
|
72
72
|
|
|
73
|
+
### Generative UI
|
|
74
|
+
|
|
75
|
+
A tool pushes a component into the answer with `ctx.emitUi(component, props)` — always present; a
|
|
76
|
+
no-op where there is no conversation (MCP). To let the MODEL compose UI from a catalog, import
|
|
77
|
+
`AgentGenuiModule` from `@dudousxd/nestjs-agent/genui` next to `AgentModule`:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
// catalog.ts — isomorphic: the React app imports this same file
|
|
81
|
+
import { defineCatalog, defineComponent } from '@dudousxd/nestjs-agent-core/genui';
|
|
82
|
+
import { BUILTIN_COMPONENTS } from '@dudousxd/nestjs-agent-core/genui/builtins';
|
|
83
|
+
export const catalog = defineCatalog([...BUILTIN_COMPONENTS, DealCard]);
|
|
84
|
+
|
|
85
|
+
// app.module.ts
|
|
86
|
+
AgentGenuiModule.forRoot({ catalog, mode: 'tree', terminal: true });
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
It registers the tools (`ui__show_<component>` per component by default, one `ui__render` in `tree`
|
|
90
|
+
mode, plus a generic `ui__show` taking `{ component, props }` with `showTool: true`), validating
|
|
91
|
+
every call against the catalog. Options: `mode`, `terminal`, `treeToolName`, `treeInstructions`,
|
|
92
|
+
`treeLimits`, `namePrefix`, `showTool`, `showInstructions`, `roles`, `presentation`.
|
|
93
|
+
`forRootAsync({ imports, inject, useFactory })` builds them from config. The catalog is injectable
|
|
94
|
+
(`@InjectGenuiCatalog()`, token `GENUI_CATALOG`) and replaceable in a test with
|
|
95
|
+
`overrideProvider(GENUI_CATALOG)` — the tools are built from the injected one.
|
|
96
|
+
|
|
97
|
+
**Per-request catalogs.** Pass `resolver` (a class, resolved with DI, or an instance) extending
|
|
98
|
+
`GenuiCatalogResolver` — `resolve({ actor, threadId, tenant, agentName }) → Catalog | Promise<Catalog>`
|
|
99
|
+
(`tenant` is `actor.tenantRef`). It is consulted when a call is validated and when a turn's tool list
|
|
100
|
+
is described to the model (the core's `ToolHandler.describe`), so tenant-defined, versioned
|
|
101
|
+
components work without rebuilding tools at boot: the tree tool's and the show tool's descriptions
|
|
102
|
+
and schemas list the tenant's components for that turn, and a pushed component carries the resolved
|
|
103
|
+
definition's `version`. A tenant's own components are reached through the tree tool or
|
|
104
|
+
`showTool: true` — no boot-time tool can name them. Cache inside `resolve` if it costs a round trip.
|
|
105
|
+
|
|
73
106
|
### Message attachments
|
|
74
107
|
|
|
75
108
|
A chat turn attaches a file by **`mediaId` only**:
|
|
@@ -122,6 +155,111 @@ minted per turn by `resolve`), bounded to `ATTACHMENT_PAGE_SIZE`, and rides the
|
|
|
122
155
|
`attachments: { upload: true }` flag. It takes no `threadId` filter: a thread's attachments already
|
|
123
156
|
ride on its messages in `GET /agent/threads/:id`.
|
|
124
157
|
|
|
158
|
+
### Attachments on nestjs-media (`@dudousxd/nestjs-agent/media`)
|
|
159
|
+
|
|
160
|
+
A ready `AGENT_ATTACHMENT_STAGING` backed by [`@dudousxd/nestjs-media`](https://davidecarvalho.github.io/aviary/docs/media),
|
|
161
|
+
with resumable (tus) uploads. `@dudousxd/nestjs-media` is an **optional** peer: only this subpath
|
|
162
|
+
imports it, and nothing on the root entry needs it.
|
|
163
|
+
|
|
164
|
+
Minimal — next to a `MediaModule` that has `store`, `uploadSessions` and `tus`:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
@Module({
|
|
168
|
+
imports: [
|
|
169
|
+
MediaModule.forRoot({ /* disks, store, uploadSessions, tus */ }),
|
|
170
|
+
AgentModule.forRoot({ /* … */ }),
|
|
171
|
+
AgentMediaAttachmentsModule.forRoot(), // ← the whole integration
|
|
172
|
+
],
|
|
173
|
+
})
|
|
174
|
+
// main.ts — nestjs-media's own requirement for tus PATCH bodies:
|
|
175
|
+
app.useBodyParser('raw', { type: 'application/offset+octet-stream' });
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Everything is overridable when you need it:
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
AgentMediaAttachmentsModule.forRoot({
|
|
182
|
+
maxBytes: 50 * 1024 * 1024,
|
|
183
|
+
allowedContentTypes: ['image/png', 'image/jpeg', 'application/pdf'],
|
|
184
|
+
collection: 'chat-files',
|
|
185
|
+
canAccess: ({ allowed, actor }) => allowed || actor.roles?.includes('support') === true,
|
|
186
|
+
resolveUrl: (record) => signMyProxyUrl(record.id), // instead of presign / public / inline
|
|
187
|
+
indexForRag: true, // see below
|
|
188
|
+
guards: [SessionGuard],
|
|
189
|
+
});
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Each attachment is a media record (`ownerType: 'agent-actor'`, `ownerId: actor.id`, collection
|
|
193
|
+
`agent-attachments`). The upload never goes through the agent:
|
|
194
|
+
|
|
195
|
+
| Step | Route | Who serves it |
|
|
196
|
+
| --- | --- | --- |
|
|
197
|
+
| 1. open | `POST /agent/attachments/uploads` `{ filename, contentType, size }` → `{ mediaId, uploadId, location }` | agent — validates type/size and opens a tus session the actor owns |
|
|
198
|
+
| 2. bytes | `PATCH <location>` (tus, chunked, resumable) | **nestjs-media** |
|
|
199
|
+
| 3. complete | `POST /agent/attachments/uploads/:mediaId/complete` → `MessageAttachment` | agent — checks the bytes landed at the declared size |
|
|
200
|
+
| drop | `DELETE /agent/attachments/uploads/:mediaId` | agent — aborts the session, deletes bytes + record |
|
|
201
|
+
|
|
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.
|
|
206
|
+
|
|
207
|
+
`resolve` hands the model provider a presigned url when the disk can mint one (`urlExpiresInSeconds`,
|
|
208
|
+
default 7 days), the disk's public `url()` with `visibility: 'public'`, or — on a disk that can do
|
|
209
|
+
neither, such as local disk in development — the bytes inline as a `data:` url (logged once; that
|
|
210
|
+
url is persisted with the message). `resolveUrl(record, { disk })` takes over entirely. The actor may
|
|
211
|
+
use media they own, and media a message in one of their own threads already carries; anything else
|
|
212
|
+
resolves `null` (`403` on the chat route).
|
|
213
|
+
|
|
214
|
+
Cleanup: an upload abandoned half-way is already a record, so it shows up in `list` and ages into
|
|
215
|
+
`collectableAttachments`. Delete through the store so the tus session is aborted too:
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
const media = app.get(MediaAttachmentStaging);
|
|
219
|
+
for (const entry of await agents.collectableAttachments(actor, { olderThan })) {
|
|
220
|
+
await media.remove(entry.mediaId);
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`indexForRag: true` announces every ready attachment on `aviary:media:attach` (and its removal on
|
|
225
|
+
`aviary:media:delete`) so `@dudousxd/nestjs-agent-rag-media` indexes it. Off by default — only turn
|
|
226
|
+
it on where retrieval is filtered by owner (`FilteredRetriever` on `ownerType`/`ownerId`), and
|
|
227
|
+
restrict rag-media's `collections` accordingly.
|
|
228
|
+
|
|
229
|
+
`forRootAsync({ imports, inject, useFactory, path, guards })` takes the same options; `path` must
|
|
230
|
+
match `AgentModule`'s `path` (default `agent`), and `guards` gates the upload routes.
|
|
231
|
+
|
|
232
|
+
### Bring your own storage
|
|
233
|
+
|
|
234
|
+
Nothing requires nestjs-media. Bind your own `AttachmentStagingStore` and upload however you like:
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
@Injectable()
|
|
238
|
+
export class S3Staging implements AttachmentStagingStore {
|
|
239
|
+
async stage({ data, filename, contentType, actor }: StageAttachmentInput) {
|
|
240
|
+
const mediaId = randomUUID();
|
|
241
|
+
await this.s3.put(`chat/${actor.id}/${mediaId}`, data, { contentType });
|
|
242
|
+
await this.db.insert(files).values({ mediaId, owner: actor.id, filename, contentType });
|
|
243
|
+
return { mediaId, url: '', contentType, name: filename };
|
|
244
|
+
}
|
|
245
|
+
async resolve({ mediaId, actor }: ResolveAttachmentInput) {
|
|
246
|
+
const row = await this.db.query.files.findFirst({ where: eq(files.mediaId, mediaId) });
|
|
247
|
+
if (!row || row.owner !== actor.id) return null; // unknown and foreign look the same
|
|
248
|
+
const url = await this.s3.presign(`chat/${actor.id}/${mediaId}`, 3600);
|
|
249
|
+
return { mediaId, url, contentType: row.contentType, name: row.filename };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// alongside AgentModule, in a @Global() module (or any module AgentModule can see):
|
|
254
|
+
{ provide: AGENT_ATTACHMENT_STAGING, useClass: S3Staging }
|
|
255
|
+
```
|
|
256
|
+
|
|
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`
|
|
259
|
+
(`useAttachments({ upload })` / a backend's `uploadAttachment`) that resolves to a `{ mediaId, … }`
|
|
260
|
+
your `resolve` recognises. `list` is optional and only needed for `GET /agent/attachments` and
|
|
261
|
+
sweeping.
|
|
262
|
+
|
|
125
263
|
### Who may read a run
|
|
126
264
|
|
|
127
265
|
`GET /agent/chat/:runId/stream` and `POST /agent/chat/:runId/cancel` both resolve the acting actor
|
|
@@ -86,6 +86,12 @@ interface FunctionalTool {
|
|
|
86
86
|
*/
|
|
87
87
|
declare function provideAgentTool(tool: FunctionalTool): Provider;
|
|
88
88
|
declare function provideAgentTool(factory: (...deps: never[]) => FunctionalTool, inject?: FactoryProvider['inject']): Provider;
|
|
89
|
+
/**
|
|
90
|
+
* Registers a LIST of functional tools from one factory — for a producer whose tool count is only
|
|
91
|
+
* known once its dependencies resolve (a catalog loaded from config, one tool per entry). The
|
|
92
|
+
* factory's result is branded element by element; `AiToolDiscoveryService` registers each.
|
|
93
|
+
*/
|
|
94
|
+
declare function provideAgentTools(factory: (...deps: never[]) => readonly FunctionalTool[] | Promise<readonly FunctionalTool[]>, inject?: FactoryProvider['inject']): Provider;
|
|
89
95
|
|
|
90
96
|
/** Ceilings for a {@link LedgerQuotaProvider} window. Either, both, or neither. */
|
|
91
97
|
interface QuotaWindowLimits {
|
|
@@ -520,4 +526,4 @@ declare class AgentDepsFactory {
|
|
|
520
526
|
private followUpsCount;
|
|
521
527
|
}
|
|
522
528
|
|
|
523
|
-
export { AgentDepsFactory as A, type FunctionalTool as F, LedgerQuotaProvider as L, type QuotaLimits as Q, type AgentSurface as a, type AgentModuleOptions as b, type AgentModuleAsyncOptions as c, type AgentSkillsOptions as d, AGENT_TOOL_BRAND as e, type AgentAttachmentsOptions as f, type AgentDeps as g, type AgentMemoryOptions as h, type QuotaWindowLimits as i, delegateToolName as j, provideAgentTool as p, utcDay as u };
|
|
529
|
+
export { AgentDepsFactory as A, type FunctionalTool as F, LedgerQuotaProvider as L, type QuotaLimits as Q, type AgentSurface as a, type AgentModuleOptions as b, type AgentModuleAsyncOptions as c, type AgentSkillsOptions as d, AGENT_TOOL_BRAND as e, type AgentAttachmentsOptions as f, type AgentDeps as g, type AgentMemoryOptions as h, type QuotaWindowLimits as i, delegateToolName as j, provideAgentTools as k, provideAgentTool as p, utcDay as u };
|
|
@@ -86,6 +86,12 @@ interface FunctionalTool {
|
|
|
86
86
|
*/
|
|
87
87
|
declare function provideAgentTool(tool: FunctionalTool): Provider;
|
|
88
88
|
declare function provideAgentTool(factory: (...deps: never[]) => FunctionalTool, inject?: FactoryProvider['inject']): Provider;
|
|
89
|
+
/**
|
|
90
|
+
* Registers a LIST of functional tools from one factory — for a producer whose tool count is only
|
|
91
|
+
* known once its dependencies resolve (a catalog loaded from config, one tool per entry). The
|
|
92
|
+
* factory's result is branded element by element; `AiToolDiscoveryService` registers each.
|
|
93
|
+
*/
|
|
94
|
+
declare function provideAgentTools(factory: (...deps: never[]) => readonly FunctionalTool[] | Promise<readonly FunctionalTool[]>, inject?: FactoryProvider['inject']): Provider;
|
|
89
95
|
|
|
90
96
|
/** Ceilings for a {@link LedgerQuotaProvider} window. Either, both, or neither. */
|
|
91
97
|
interface QuotaWindowLimits {
|
|
@@ -520,4 +526,4 @@ declare class AgentDepsFactory {
|
|
|
520
526
|
private followUpsCount;
|
|
521
527
|
}
|
|
522
528
|
|
|
523
|
-
export { AgentDepsFactory as A, type FunctionalTool as F, LedgerQuotaProvider as L, type QuotaLimits as Q, type AgentSurface as a, type AgentModuleOptions as b, type AgentModuleAsyncOptions as c, type AgentSkillsOptions as d, AGENT_TOOL_BRAND as e, type AgentAttachmentsOptions as f, type AgentDeps as g, type AgentMemoryOptions as h, type QuotaWindowLimits as i, delegateToolName as j, provideAgentTool as p, utcDay as u };
|
|
529
|
+
export { AgentDepsFactory as A, type FunctionalTool as F, LedgerQuotaProvider as L, type QuotaLimits as Q, type AgentSurface as a, type AgentModuleOptions as b, type AgentModuleAsyncOptions as c, type AgentSkillsOptions as d, AGENT_TOOL_BRAND as e, type AgentAttachmentsOptions as f, type AgentDeps as g, type AgentMemoryOptions as h, type QuotaWindowLimits as i, delegateToolName as j, provideAgentTools as k, provideAgentTool as p, utcDay as u };
|
package/dist/durable/index.cjs
CHANGED
|
@@ -100,7 +100,14 @@ var AgentRunSteps = class {
|
|
|
100
100
|
const tools = (0, import_nestjs_agent_core.withMemoryTool)({
|
|
101
101
|
tools: (0, import_nestjs_agent_core.withSkillTool)({
|
|
102
102
|
tools: (0, import_nestjs_agent_core.withAskTool)({
|
|
103
|
-
tools: await deps.registry.definitionsFor(input.actor, deps.rolesPolicy, deps.toolAllowList
|
|
103
|
+
tools: await deps.registry.definitionsFor(input.actor, deps.rolesPolicy, deps.toolAllowList, {
|
|
104
|
+
...input.threadId !== void 0 ? {
|
|
105
|
+
threadId: input.threadId
|
|
106
|
+
} : {},
|
|
107
|
+
...input.agentName !== void 0 ? {
|
|
108
|
+
agentName: input.agentName
|
|
109
|
+
} : {}
|
|
110
|
+
}),
|
|
104
111
|
ask: deps.ask
|
|
105
112
|
}),
|
|
106
113
|
enabled: deps.skills !== void 0
|
|
@@ -3497,6 +3504,14 @@ var AiToolDiscoveryService = class _AiToolDiscoveryService {
|
|
|
3497
3504
|
if (instance === null || instance === void 0 || typeof instance !== "object") {
|
|
3498
3505
|
continue;
|
|
3499
3506
|
}
|
|
3507
|
+
if (Array.isArray(instance) && instance.length > 0 && instance.every((each) => typeof each === "object" && each !== null && isBrandedFunctionalTool(each))) {
|
|
3508
|
+
for (const tool of instance) {
|
|
3509
|
+
if (this.registerFunctionalTool(tool)) {
|
|
3510
|
+
functional += 1;
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
continue;
|
|
3514
|
+
}
|
|
3500
3515
|
if (isBrandedFunctionalTool(instance)) {
|
|
3501
3516
|
if (this.registerFunctionalTool(instance)) {
|
|
3502
3517
|
functional += 1;
|
|
@@ -3544,6 +3559,9 @@ var AiToolDiscoveryService = class _AiToolDiscoveryService {
|
|
|
3544
3559
|
} : {},
|
|
3545
3560
|
...typeof handler.canUse === "function" ? {
|
|
3546
3561
|
canUse: /* @__PURE__ */ __name((actor) => instance.canUse(actor), "canUse")
|
|
3562
|
+
} : {},
|
|
3563
|
+
...typeof handler.describe === "function" ? {
|
|
3564
|
+
describe: /* @__PURE__ */ __name((scope) => instance.describe(scope), "describe")
|
|
3547
3565
|
} : {}
|
|
3548
3566
|
});
|
|
3549
3567
|
count += 1;
|