@dudousxd/nestjs-agent 1.20.0 → 1.22.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
@@ -10,6 +10,13 @@ via signals).
10
10
  This is the NestJS module — it re-exports the entire `@dudousxd/nestjs-agent-core` surface, so you
11
11
  import tools, types, and the module from one place.
12
12
 
13
+ ## Component registries and server rendering
14
+
15
+ Tools support a `present` hook in both functional and decorated class forms. Register custom components per app, share React renderers between web screens and static exports, and generate paginated PNG/PDF tables and charts for your own delivery adapters.
16
+
17
+ See the [component rendering guide](../../docs/component-rendering.md) for complete examples, schemas, text fallbacks, and capture configuration.
18
+
19
+
13
20
  ## Install
14
21
 
15
22
  ```bash
@@ -405,6 +412,57 @@ In anonymous mode the limits apply per browser (each is its own actor).
405
412
 
406
413
  ### Who approves an action, and for how long
407
414
 
415
+ Action handlers can implement an optional read-only `preflight(input, ctx, { phase })`. The
416
+ registry authorizes and validates the input first. `prepare` runs before an approval is requested;
417
+ `execute` rechecks current state inside the tool checkpoint immediately before its side effect.
418
+ Automatic and remembered approvals still run both checks; direct registry and MCP invocations
419
+ run the execution check too.
420
+
421
+ ```ts
422
+ class RemoveSessionsTool implements ToolHandler<{ accountId: string }, { removed: number }> {
423
+ constructor(private readonly sessions: SessionsService) {}
424
+
425
+ async preflight(input: { accountId: string }, ctx: AiToolCtx, { phase }: ToolPreflightOptions): Promise<ToolPreflightResult<{ removed: number }>> {
426
+ const count = await this.sessions.countForActor(input.accountId, ctx.actor);
427
+ if (count === 0) return { status: 'completed', output: { removed: 0 } };
428
+ if (await this.sessions.isLocked(input.accountId)) {
429
+ return { status: 'denied', reason: 'This account is locked.' };
430
+ }
431
+ return {
432
+ status: 'ready',
433
+ confirmation: { title: `Remove ${count} sessions?`, verb: 'Remove' },
434
+ };
435
+ }
436
+
437
+ async execute(input: { accountId: string }, ctx: AiToolCtx) {
438
+ return this.sessions.removeForActor(input.accountId, ctx.actor, ctx.idempotencyKey);
439
+ }
440
+ }
441
+ ```
442
+
443
+ `ready` may supply fully resolved `ToolConfirmation` strings (`title`, `verb`, optional `detail`)
444
+ that override the tool's presentation templates for this call, including after a history reload.
445
+ `denied` returns a domain refusal to the model, and `completed` returns an existing result. Both
446
+ skip approval and execution during preparation. A completed terminal action ends the turn.
447
+ A refusal after a person approves preserves that approval while reporting that the mutation did
448
+ not run. An exception becomes a failed tool result. Read tools ignore this hook.
449
+
450
+ Functional tools use the same handler contract:
451
+
452
+ ```ts
453
+ provideAgentTool({
454
+ spec: removeSessionsSpec,
455
+ handler: { preflight: checkSessions, execute: removeSessions },
456
+ });
457
+ ```
458
+
459
+ Preflight must not perform writes or emit UI. Its preparation result is journaled with the existing
460
+ call checkpoint (and, for remote execution, first with the model worker's turn); replay uses that
461
+ result without rerunning the check or changing its approval branch. The execution check is
462
+ journaled inside the existing tool checkpoint. Keep domain checks inside `execute` or an atomic
463
+ transaction as needed: a preflight check cannot eliminate concurrent changes between reads and
464
+ writes, and it does not replace idempotency.
465
+
408
466
  Every `action` tool call waits on the person chatting by default. `approvalPolicy` changes that per
409
467
  call:
410
468
 
@@ -1,9 +1,13 @@
1
- import { ModelProvider, AgentStore, ToolRegistry, RolesPolicy, ApprovalPolicy, QuotaStore, TokenStreamSink, PromptBuilder, PromptContributor, Persona, ToolTransientRetrySetting, Retriever, AgentPricingStore, HistoryPolicy, InputProcessor, OutputProcessor, AgentIntake, SkillsConfig, MemoryConfig, ToolSpec, ToolHandler, QuotaProvider, QuotaQuery, QuotaReport, ModelCatalog, EmptyRoles, ActorResolver, AgentHistoryWindow, SkillProvider, ScopeResolver, MemoryProvider, AgentRegistry, PersonaCatalogEntry, AttachmentStagingStore, ChatQueueStore, ChatQueueState, AgentRunner, AgentRunInput, AgentStreamEvent, QueuePauseReason, Actor } from '@dudousxd/nestjs-agent-core';
1
+ import { ResolveToolUiCatalog, ModelProvider, AgentStore, ToolRegistry, RolesPolicy, ApprovalPolicy, QuotaStore, TokenStreamSink, PromptBuilder, PromptContributor, Persona, ToolTransientRetrySetting, Retriever, AgentPricingStore, HistoryPolicy, InputProcessor, OutputProcessor, AgentIntake, SkillsConfig, MemoryConfig, ToolSpec, ToolHandler, createFunctionalTool, QuotaProvider, QuotaQuery, QuotaReport, ActionApprovalMode, BackgroundActorResolver, TextActionProposalConfig, ModelCatalog, EmptyRoles, ActorResolver, AgentHistoryWindow, SkillProvider, ScopeResolver, MemoryProvider, AgentRegistry, PersonaCatalogEntry, ActionProposal, Actor, AttachmentStagingStore, ChatQueueStore, ChatQueueState, AgentRunner, AgentRunInput, AgentStreamEvent, QueuePauseReason } from '@dudousxd/nestjs-agent-core';
2
+ import { Catalog } from '@dudousxd/nestjs-agent-core/genui';
2
3
  import { StandardSchemaV1 } from '@standard-schema/spec';
3
4
  import { Provider, FactoryProvider, Type, CanActivate, ModuleMetadata, InjectionToken, OptionalFactoryDependency } from '@nestjs/common';
5
+ import { GenuiCatalogResolver } from './genui/index.js';
4
6
 
5
7
  /** Everything `runAgentLoop` needs, minus the per-run `day` the runner stamps. */
6
8
  interface AgentDeps {
9
+ resolveUiCatalog?: ResolveToolUiCatalog;
10
+ actionApprovalMode?: 'blocking' | 'independent';
7
11
  model: ModelProvider;
8
12
  store: AgentStore;
9
13
  registry: ToolRegistry;
@@ -94,6 +98,8 @@ declare function provideAgentTool(factory: (...deps: never[]) => FunctionalTool,
94
98
  * factory's result is branded element by element; `AiToolDiscoveryService` registers each.
95
99
  */
96
100
  declare function provideAgentTools(factory: (...deps: never[]) => readonly FunctionalTool[] | Promise<readonly FunctionalTool[]>, inject?: FactoryProvider['inject']): Provider;
101
+ /** Author a functional tool with inferred Standard Schema input and domain output. */
102
+ declare const defineTool: typeof createFunctionalTool;
97
103
 
98
104
  /** Ceilings for a {@link LedgerQuotaProvider} window. Either, both, or neither. */
99
105
  interface QuotaWindowLimits {
@@ -240,6 +246,42 @@ interface AgentMemoryOptions {
240
246
  maxFactChars?: number;
241
247
  }
242
248
  interface AgentModuleOptions {
249
+ /**
250
+ * How an `action` tool waits for its approval. `'blocking'` (default): the turn parks on the
251
+ * decision and resumes when it lands. `'independent'`: the call is recorded as an action proposal
252
+ * and answered at once with a receipt (`{ proposalId, status: 'pending', executed: false }`), the
253
+ * turn finishes, and a background worker runs the action once it is approved — decided through
254
+ * `POST <base>/threads/:threadId/action-proposals/:proposalId/approve|reject`, the approval port or
255
+ * a text command. Needs `backgroundActorResolver` and a store with the proposal capabilities
256
+ * (both checked at boot). See `docs/independent-approvals.md`.
257
+ */
258
+ actionApprovalMode?: ActionApprovalMode;
259
+ /**
260
+ * Resolves the CURRENT actor (id, tenant, roles) for a proposal's recorded `actorRef`/`tenantRef`
261
+ * when the worker runs it — there is no request to read an identity from. Return `null` for an
262
+ * actor that no longer exists or no longer belongs to the tenant; the action then fails rather
263
+ * than running. Required under `actionApprovalMode: 'independent'`.
264
+ */
265
+ backgroundActorResolver?: BackgroundActorResolver;
266
+ /**
267
+ * Tuning for the worker that runs approved proposals under `actionApprovalMode: 'independent'`
268
+ * (started with the module unless `surface: 'http'`). `pollIntervalMs` — how often it looks for
269
+ * queued work, default 1000; `leaseMs` — how long a claim holds before another worker may recover
270
+ * it, default 30000; `maxConcurrency` — proposals claimed per poll, default 1. All positive
271
+ * integers, and `pollIntervalMs` must stay below a third of `leaseMs`.
272
+ */
273
+ actionProposalWorker?: {
274
+ pollIntervalMs?: number;
275
+ leaseMs?: number;
276
+ maxConcurrency?: number;
277
+ };
278
+ /**
279
+ * The words a chat message must consist of to approve or reject a proposal by text, and what the
280
+ * agent answers. English by default (`yes`/`confirm`/`approve`, `no`/`cancel`/`reject`, …); pass
281
+ * `ptBrActionProposalText` from `@dudousxd/nestjs-agent-core` for Portuguese, or your own — either
282
+ * part replaces the default it names, field by field.
283
+ */
284
+ actionProposalText?: TextActionProposalConfig;
243
285
  /**
244
286
  * The LLM provider — `aiSdkModel(openai('gpt-5-mini'))`, or `aiSdkModels({ … })` for a model
245
287
  * picker (its catalog is used when `models` is omitted). The only required option.
@@ -525,7 +567,9 @@ declare class AgentDepsFactory {
525
567
  private readonly pricingStore;
526
568
  private readonly skills;
527
569
  private readonly memory;
528
- 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);
570
+ private readonly uiCatalog?;
571
+ private readonly uiCatalogResolver?;
572
+ 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, uiCatalog?: Catalog | undefined, uiCatalogResolver?: GenuiCatalogResolver | undefined);
529
573
  /** The turn's memory seam, shared with the read-back endpoint. Undefined → memory is not configured. */
530
574
  memoryConfig(): MemoryConfig | undefined;
531
575
  /** The turn's skills seam, shared with the listing endpoint. Undefined → skills are not configured. */
@@ -558,6 +602,7 @@ declare class AgentDepsFactory {
558
602
  requested?: string;
559
603
  threadPersona?: string | null;
560
604
  }): string | undefined;
605
+ forProposal(proposal: ActionProposal, _actor: Actor): AgentDeps;
561
606
  forAgent(agentName?: string): AgentDeps;
562
607
  /**
563
608
  * A persona baked into an agent's deps — how a run journaled under an agent name that a persona
@@ -715,4 +760,4 @@ declare class ChatQueueService {
715
760
  private freshAttachments;
716
761
  }
717
762
 
718
- export { AgentDepsFactory as A, ChatQueueService as C, type FunctionalTool as F, LedgerQuotaProvider as L, type QueuePlan as Q, type AgentSurface as a, type AgentModuleOptions as b, type AgentModuleAsyncOptions as c, type AgentProtocolAdapter as d, type AgentSkillsOptions as e, AGENT_TOOL_BRAND as f, type AgentAttachmentsOptions as g, type AgentDeps as h, type AgentMemoryOptions as i, type QueueSettleOutcome as j, type QueuedTurn as k, type QueuedTurnStarter as l, type QuotaLimits as m, type QuotaWindowLimits as n, delegateToolName as o, provideAgentTool as p, provideAgentTools as q, utcDay as u };
763
+ export { AgentDepsFactory as A, ChatQueueService as C, type FunctionalTool as F, LedgerQuotaProvider as L, type QueuePlan as Q, type AgentSurface as a, type AgentModuleOptions as b, type AgentModuleAsyncOptions as c, type AgentProtocolAdapter as d, type AgentSkillsOptions as e, AGENT_TOOL_BRAND as f, type AgentAttachmentsOptions as g, type AgentDeps as h, type AgentMemoryOptions as i, type QueueSettleOutcome as j, type QueuedTurn as k, type QueuedTurnStarter as l, type QuotaLimits as m, type QuotaWindowLimits as n, defineTool as o, delegateToolName as p, provideAgentTool as q, provideAgentTools as r, utcDay as u };
@@ -1,9 +1,13 @@
1
- import { ModelProvider, AgentStore, ToolRegistry, RolesPolicy, ApprovalPolicy, QuotaStore, TokenStreamSink, PromptBuilder, PromptContributor, Persona, ToolTransientRetrySetting, Retriever, AgentPricingStore, HistoryPolicy, InputProcessor, OutputProcessor, AgentIntake, SkillsConfig, MemoryConfig, ToolSpec, ToolHandler, QuotaProvider, QuotaQuery, QuotaReport, ModelCatalog, EmptyRoles, ActorResolver, AgentHistoryWindow, SkillProvider, ScopeResolver, MemoryProvider, AgentRegistry, PersonaCatalogEntry, AttachmentStagingStore, ChatQueueStore, ChatQueueState, AgentRunner, AgentRunInput, AgentStreamEvent, QueuePauseReason, Actor } from '@dudousxd/nestjs-agent-core';
1
+ import { ResolveToolUiCatalog, ModelProvider, AgentStore, ToolRegistry, RolesPolicy, ApprovalPolicy, QuotaStore, TokenStreamSink, PromptBuilder, PromptContributor, Persona, ToolTransientRetrySetting, Retriever, AgentPricingStore, HistoryPolicy, InputProcessor, OutputProcessor, AgentIntake, SkillsConfig, MemoryConfig, ToolSpec, ToolHandler, createFunctionalTool, QuotaProvider, QuotaQuery, QuotaReport, ActionApprovalMode, BackgroundActorResolver, TextActionProposalConfig, ModelCatalog, EmptyRoles, ActorResolver, AgentHistoryWindow, SkillProvider, ScopeResolver, MemoryProvider, AgentRegistry, PersonaCatalogEntry, ActionProposal, Actor, AttachmentStagingStore, ChatQueueStore, ChatQueueState, AgentRunner, AgentRunInput, AgentStreamEvent, QueuePauseReason } from '@dudousxd/nestjs-agent-core';
2
+ import { Catalog } from '@dudousxd/nestjs-agent-core/genui';
2
3
  import { StandardSchemaV1 } from '@standard-schema/spec';
3
4
  import { Provider, FactoryProvider, Type, CanActivate, ModuleMetadata, InjectionToken, OptionalFactoryDependency } from '@nestjs/common';
5
+ import { GenuiCatalogResolver } from './genui/index.cjs';
4
6
 
5
7
  /** Everything `runAgentLoop` needs, minus the per-run `day` the runner stamps. */
6
8
  interface AgentDeps {
9
+ resolveUiCatalog?: ResolveToolUiCatalog;
10
+ actionApprovalMode?: 'blocking' | 'independent';
7
11
  model: ModelProvider;
8
12
  store: AgentStore;
9
13
  registry: ToolRegistry;
@@ -94,6 +98,8 @@ declare function provideAgentTool(factory: (...deps: never[]) => FunctionalTool,
94
98
  * factory's result is branded element by element; `AiToolDiscoveryService` registers each.
95
99
  */
96
100
  declare function provideAgentTools(factory: (...deps: never[]) => readonly FunctionalTool[] | Promise<readonly FunctionalTool[]>, inject?: FactoryProvider['inject']): Provider;
101
+ /** Author a functional tool with inferred Standard Schema input and domain output. */
102
+ declare const defineTool: typeof createFunctionalTool;
97
103
 
98
104
  /** Ceilings for a {@link LedgerQuotaProvider} window. Either, both, or neither. */
99
105
  interface QuotaWindowLimits {
@@ -240,6 +246,42 @@ interface AgentMemoryOptions {
240
246
  maxFactChars?: number;
241
247
  }
242
248
  interface AgentModuleOptions {
249
+ /**
250
+ * How an `action` tool waits for its approval. `'blocking'` (default): the turn parks on the
251
+ * decision and resumes when it lands. `'independent'`: the call is recorded as an action proposal
252
+ * and answered at once with a receipt (`{ proposalId, status: 'pending', executed: false }`), the
253
+ * turn finishes, and a background worker runs the action once it is approved — decided through
254
+ * `POST <base>/threads/:threadId/action-proposals/:proposalId/approve|reject`, the approval port or
255
+ * a text command. Needs `backgroundActorResolver` and a store with the proposal capabilities
256
+ * (both checked at boot). See `docs/independent-approvals.md`.
257
+ */
258
+ actionApprovalMode?: ActionApprovalMode;
259
+ /**
260
+ * Resolves the CURRENT actor (id, tenant, roles) for a proposal's recorded `actorRef`/`tenantRef`
261
+ * when the worker runs it — there is no request to read an identity from. Return `null` for an
262
+ * actor that no longer exists or no longer belongs to the tenant; the action then fails rather
263
+ * than running. Required under `actionApprovalMode: 'independent'`.
264
+ */
265
+ backgroundActorResolver?: BackgroundActorResolver;
266
+ /**
267
+ * Tuning for the worker that runs approved proposals under `actionApprovalMode: 'independent'`
268
+ * (started with the module unless `surface: 'http'`). `pollIntervalMs` — how often it looks for
269
+ * queued work, default 1000; `leaseMs` — how long a claim holds before another worker may recover
270
+ * it, default 30000; `maxConcurrency` — proposals claimed per poll, default 1. All positive
271
+ * integers, and `pollIntervalMs` must stay below a third of `leaseMs`.
272
+ */
273
+ actionProposalWorker?: {
274
+ pollIntervalMs?: number;
275
+ leaseMs?: number;
276
+ maxConcurrency?: number;
277
+ };
278
+ /**
279
+ * The words a chat message must consist of to approve or reject a proposal by text, and what the
280
+ * agent answers. English by default (`yes`/`confirm`/`approve`, `no`/`cancel`/`reject`, …); pass
281
+ * `ptBrActionProposalText` from `@dudousxd/nestjs-agent-core` for Portuguese, or your own — either
282
+ * part replaces the default it names, field by field.
283
+ */
284
+ actionProposalText?: TextActionProposalConfig;
243
285
  /**
244
286
  * The LLM provider — `aiSdkModel(openai('gpt-5-mini'))`, or `aiSdkModels({ … })` for a model
245
287
  * picker (its catalog is used when `models` is omitted). The only required option.
@@ -525,7 +567,9 @@ declare class AgentDepsFactory {
525
567
  private readonly pricingStore;
526
568
  private readonly skills;
527
569
  private readonly memory;
528
- 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);
570
+ private readonly uiCatalog?;
571
+ private readonly uiCatalogResolver?;
572
+ 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, uiCatalog?: Catalog | undefined, uiCatalogResolver?: GenuiCatalogResolver | undefined);
529
573
  /** The turn's memory seam, shared with the read-back endpoint. Undefined → memory is not configured. */
530
574
  memoryConfig(): MemoryConfig | undefined;
531
575
  /** The turn's skills seam, shared with the listing endpoint. Undefined → skills are not configured. */
@@ -558,6 +602,7 @@ declare class AgentDepsFactory {
558
602
  requested?: string;
559
603
  threadPersona?: string | null;
560
604
  }): string | undefined;
605
+ forProposal(proposal: ActionProposal, _actor: Actor): AgentDeps;
561
606
  forAgent(agentName?: string): AgentDeps;
562
607
  /**
563
608
  * A persona baked into an agent's deps — how a run journaled under an agent name that a persona
@@ -715,4 +760,4 @@ declare class ChatQueueService {
715
760
  private freshAttachments;
716
761
  }
717
762
 
718
- export { AgentDepsFactory as A, ChatQueueService as C, type FunctionalTool as F, LedgerQuotaProvider as L, type QueuePlan as Q, type AgentSurface as a, type AgentModuleOptions as b, type AgentModuleAsyncOptions as c, type AgentProtocolAdapter as d, type AgentSkillsOptions as e, AGENT_TOOL_BRAND as f, type AgentAttachmentsOptions as g, type AgentDeps as h, type AgentMemoryOptions as i, type QueueSettleOutcome as j, type QueuedTurn as k, type QueuedTurnStarter as l, type QuotaLimits as m, type QuotaWindowLimits as n, delegateToolName as o, provideAgentTool as p, provideAgentTools as q, utcDay as u };
763
+ export { AgentDepsFactory as A, ChatQueueService as C, type FunctionalTool as F, LedgerQuotaProvider as L, type QueuePlan as Q, type AgentSurface as a, type AgentModuleOptions as b, type AgentModuleAsyncOptions as c, type AgentProtocolAdapter as d, type AgentSkillsOptions as e, AGENT_TOOL_BRAND as f, type AgentAttachmentsOptions as g, type AgentDeps as h, type AgentMemoryOptions as i, type QueueSettleOutcome as j, type QueuedTurn as k, type QueuedTurnStarter as l, type QuotaLimits as m, type QuotaWindowLimits as n, defineTool as o, delegateToolName as p, provideAgentTool as q, provideAgentTools as r, utcDay as u };