@dudousxd/nestjs-agent 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Davide Carvalho
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # `@dudousxd/nestjs-agent`
2
+
3
+ > 🪺 Part of the [Aviary](https://davidecarvalho.github.io/aviary) — plug-n-play, fully-configurable NestJS libraries.
4
+
5
+ A **governed, durable-backed AI agent** for NestJS: chat + tool-calling + role/persona governance +
6
+ token quota + cost tracking + human-in-the-loop approval + resumable streaming + multi-agent
7
+ delegation. The agent turn runs in-process or as a **durable workflow** (replay-safe, resumable, HITL
8
+ via signals).
9
+
10
+ This is the NestJS module — it re-exports the entire `@dudousxd/nestjs-agent-core` surface, so you
11
+ import tools, types, and the module from one place.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ pnpm add @dudousxd/nestjs-agent @dudousxd/nestjs-agent-core
17
+ ```
18
+
19
+ ## Use
20
+
21
+ ```ts
22
+ import { AgentModule, AiTool, HeaderActorResolver, type ToolHandler } from '@dudousxd/nestjs-agent';
23
+ import { z } from 'zod';
24
+
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 }; }
28
+ }
29
+
30
+ @Module({
31
+ imports: [AgentModule.forRoot({
32
+ model, store, actorResolver: new HeaderActorResolver(),
33
+ defaultAgent: { systemPrompt: '…', modelId: 'claude-sonnet-4-6' },
34
+ })],
35
+ providers: [GetWeatherTool],
36
+ })
37
+ export class AppModule {}
38
+ ```
39
+
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.
43
+
44
+ See the [monorepo README](https://github.com/DavideCarvalho/aviary) for the full guide (durability,
45
+ multi-agent, authz, governed SQL, the React frontend, diagnostics).
46
+
47
+ ## License
48
+
49
+ MIT © Davide Carvalho
@@ -0,0 +1,173 @@
1
+ import { ModelProvider, AgentStore, ToolRegistry, RolesPolicy, QuotaStore, TokenStreamSink, PromptBuilder, Persona, Retriever, ToolSpec, ToolHandler, ActorResolver, AgentDefinition, AgentRegistry } from '@dudousxd/nestjs-agent-core';
2
+ import { Provider, FactoryProvider, ModuleMetadata, InjectionToken, OptionalFactoryDependency } from '@nestjs/common';
3
+
4
+ /** Everything `runAgentLoop` needs, minus the per-run `day` the runner stamps. */
5
+ interface AgentDeps {
6
+ model: ModelProvider;
7
+ store: AgentStore;
8
+ registry: ToolRegistry;
9
+ rolesPolicy: RolesPolicy;
10
+ quota?: QuotaStore;
11
+ sink: TokenStreamSink;
12
+ /** Fallback accounting label; the provider's turn result overrides it when set. */
13
+ modelId?: string;
14
+ systemPrompt: string | PromptBuilder;
15
+ maxSteps: number;
16
+ personas: Map<string, Persona>;
17
+ defaultPersona: string;
18
+ /** Agent-level tool allow-list (intersected with the persona's). Undefined → all tools. */
19
+ toolAllowList?: string[];
20
+ /** Per-tool execution timeout in ms (from module options). Undefined → no timeout. */
21
+ toolTimeoutMs?: number;
22
+ /** How many follow-up suggestions to generate after the final turn. Undefined/0 → off. */
23
+ followUpsCount?: number;
24
+ /** Inject-mode retriever (from `forRoot({ retrieval })`). Undefined → no prompt augmentation. */
25
+ retriever?: Retriever;
26
+ /** Passages inject-mode retrieval requests. Undefined → loop default (5). */
27
+ retrievalTopK?: number;
28
+ }
29
+ declare function utcDay(date?: Date): string;
30
+
31
+ /**
32
+ * Brand stamped onto a functional-tool provider instance so `AiToolDiscoveryService` picks it up
33
+ * when it walks every provider at boot — the declarative alternative to a manual `registry.register`.
34
+ */
35
+ declare const AGENT_TOOL_BRAND: unique symbol;
36
+ /** A tool expressed as data + handler (e.g. from `createExecuteSqlTool`), not an `@AiTool` class. */
37
+ interface FunctionalTool {
38
+ spec: ToolSpec;
39
+ handler: ToolHandler;
40
+ }
41
+ /**
42
+ * Registers a functional tool declaratively — drop the returned provider in a module's `providers`
43
+ * and `AiToolDiscoveryService` auto-registers it at boot. Each call uses a fresh, unique provide
44
+ * token so the same tool can be provided many times without collisions.
45
+ *
46
+ * Two forms: a static tool (`useValue`) or a factory with `inject` for DI-resolved dependencies
47
+ * (`useFactory`). Either way the resolved instance is branded with {@link AGENT_TOOL_BRAND}.
48
+ */
49
+ declare function provideAgentTool(tool: FunctionalTool): Provider;
50
+ declare function provideAgentTool(factory: (...deps: never[]) => FunctionalTool, inject?: FactoryProvider['inject']): Provider;
51
+
52
+ /**
53
+ * The implicit default agent, configured inline on `forRoot`. It is an {@link AgentDefinition}
54
+ * with `name` optional (defaults to `'default'`) — everything agent-shaped is an AgentDefinition,
55
+ * whether it's this default or one registered via `forFeature`. Omit it entirely for a bare
56
+ * assistant with no base prompt, personas, or tool allow-list.
57
+ */
58
+ type DefaultAgentOptions = Omit<AgentDefinition, 'name'> & {
59
+ name?: string;
60
+ };
61
+ interface AgentModuleOptions {
62
+ /** The LLM provider (e.g. a Vercel AI SDK wrapper). Required. */
63
+ model: ModelProvider;
64
+ /**
65
+ * Persistence adapter. Optional — omit it and import a store module (e.g.
66
+ * `MikroOrmAgentStoreModule.forFeature()`) that binds `AGENT_STORE` globally instead. When
67
+ * provided here it takes precedence within this module's scope.
68
+ */
69
+ store?: AgentStore;
70
+ /** Live token transport. Defaults to a single-process in-memory sink. */
71
+ sink?: TokenStreamSink;
72
+ /**
73
+ * A {@link QuotaStore} for the daily token budget. Optional — omit (and omit `quotaLimitTokens`)
74
+ * to disable quotas. Provide this to plug a custom budget; for the common case use the simpler
75
+ * `quotaLimitTokens` instead, which binds the built-in ledger-backed store.
76
+ */
77
+ quota?: QuotaStore;
78
+ /**
79
+ * Daily per-actor token budget, enforced against the persisted usage ledger by the built-in
80
+ * `LedgerQuotaStore`. A convenience over wiring a {@link QuotaStore} by hand — set this and quotas
81
+ * turn on with no extra store. Ignored when an explicit `quota` is provided. Omit to disable.
82
+ */
83
+ quotaLimitTokens?: number;
84
+ /** Tool authorization gate. Defaults to role-in-`defaultRoles`. */
85
+ rolesPolicy?: RolesPolicy;
86
+ /** Default roles a tool requires when its `roles` is omitted. Defaults to `['ADMIN']`. */
87
+ defaultRoles?: string[];
88
+ /**
89
+ * Resolves the acting actor for each request (the identity seam). Required — the agent NEVER
90
+ * fabricates a caller, so this is a compile-time obligation, not an optional with a throwing
91
+ * placeholder. Read your authenticated principal here (session / JWT / `nestjs-context`), or use
92
+ * the opt-in `HeaderActorResolver` for demos and header-trusting gateways.
93
+ */
94
+ actorResolver: ActorResolver;
95
+ /** Route prefix the controllers mount under. Defaults to `'agent'` (→ `/agent/chat`, …). */
96
+ path?: string;
97
+ /**
98
+ * Run each turn as a durable workflow instead of in-process. Requires importing
99
+ * `AgentDurableModule` from `@dudousxd/nestjs-agent/durable` and a configured `DurableModule`.
100
+ */
101
+ durable?: boolean;
102
+ /**
103
+ * Static functional tools (`{ spec, handler }`, e.g. from `createExecuteSqlTool`) to register at
104
+ * boot. For tools that need DI-resolved dependencies, use `provideAgentTool(factory, inject)` in a
105
+ * module's `providers` instead.
106
+ */
107
+ tools?: FunctionalTool[];
108
+ /**
109
+ * Per-tool execution timeout in ms. A tool that runs longer is aborted and recorded as failed (the
110
+ * model receives the timeout as its result and can adapt) instead of hanging the turn. Omit → none.
111
+ */
112
+ toolTimeoutMs?: number;
113
+ /**
114
+ * Suggest follow-up questions after the final turn. `true` → 3 suggestions; `{ count }` → that many.
115
+ * Costs one extra model call per turn (recorded as `follow_ups` usage), stored on the assistant
116
+ * message's `followUps`. Omit/`false` → disabled.
117
+ */
118
+ followUps?: boolean | {
119
+ count: number;
120
+ };
121
+ /**
122
+ * Always-on ("inject") RAG: before each turn, retrieve passages for the user message and augment
123
+ * the system prompt with them. For agentic retrieval (the model decides when to search) DON'T set
124
+ * this — instead expose the tool: `provideAgentTool(createRetrievalTool(retriever))`. Omit → off.
125
+ */
126
+ retrieval?: {
127
+ mode: 'inject';
128
+ retriever: Retriever;
129
+ topK?: number;
130
+ };
131
+ /**
132
+ * The implicit single agent's config (base prompt, personas, tool allow-list, model/step
133
+ * config). Omit for a bare assistant. Additional named agents are added via `forFeature`.
134
+ */
135
+ defaultAgent?: DefaultAgentOptions;
136
+ }
137
+ interface AgentModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
138
+ inject?: Array<InjectionToken | OptionalFactoryDependency>;
139
+ useFactory: (...args: never[]) => AgentModuleOptions | Promise<AgentModuleOptions>;
140
+ /**
141
+ * Route prefix the controllers mount under (default `'agent'`). Static routing metadata, so it
142
+ * lives here rather than in the async factory result (which resolves too late to mount routes).
143
+ */
144
+ path?: string;
145
+ /**
146
+ * Run each turn as a durable workflow. Static wiring metadata (which `AGENT_RUNNER` to bind), so
147
+ * it lives here rather than in the async factory result — the factory resolves too late to swap
148
+ * the runner. Requires importing `AgentDurableModule` and a configured `DurableModule`.
149
+ */
150
+ durable?: boolean;
151
+ }
152
+
153
+ /** The synthesized `agent`-kind tool name an orchestrator uses to delegate to `target`. */
154
+ declare function delegateToolName(target: string): string;
155
+ /** Builds the per-agent loop deps. The single-agent case is just the `default` definition. */
156
+ declare class AgentDepsFactory {
157
+ private readonly options;
158
+ private readonly model;
159
+ private readonly store;
160
+ private readonly sink;
161
+ private readonly rolesPolicy;
162
+ private readonly registry;
163
+ private readonly agents;
164
+ private readonly quota;
165
+ constructor(options: AgentModuleOptions, model: ModelProvider, store: AgentStore, sink: TokenStreamSink, rolesPolicy: RolesPolicy, registry: ToolRegistry, agents: AgentRegistry, quota: QuotaStore | undefined);
166
+ defaultAgentName(): string;
167
+ private effectiveTools;
168
+ forAgent(agentName?: string): AgentDeps;
169
+ /** Normalize the `followUps` option (`true` → 3, `{ count }` → count) to a number, or undefined. */
170
+ private followUpsCount;
171
+ }
172
+
173
+ export { AgentDepsFactory as A, type DefaultAgentOptions as D, type FunctionalTool as F, type AgentModuleOptions as a, type AgentModuleAsyncOptions as b, AGENT_TOOL_BRAND as c, type AgentDeps as d, delegateToolName as e, provideAgentTool as p, utcDay as u };
@@ -0,0 +1,173 @@
1
+ import { ModelProvider, AgentStore, ToolRegistry, RolesPolicy, QuotaStore, TokenStreamSink, PromptBuilder, Persona, Retriever, ToolSpec, ToolHandler, ActorResolver, AgentDefinition, AgentRegistry } from '@dudousxd/nestjs-agent-core';
2
+ import { Provider, FactoryProvider, ModuleMetadata, InjectionToken, OptionalFactoryDependency } from '@nestjs/common';
3
+
4
+ /** Everything `runAgentLoop` needs, minus the per-run `day` the runner stamps. */
5
+ interface AgentDeps {
6
+ model: ModelProvider;
7
+ store: AgentStore;
8
+ registry: ToolRegistry;
9
+ rolesPolicy: RolesPolicy;
10
+ quota?: QuotaStore;
11
+ sink: TokenStreamSink;
12
+ /** Fallback accounting label; the provider's turn result overrides it when set. */
13
+ modelId?: string;
14
+ systemPrompt: string | PromptBuilder;
15
+ maxSteps: number;
16
+ personas: Map<string, Persona>;
17
+ defaultPersona: string;
18
+ /** Agent-level tool allow-list (intersected with the persona's). Undefined → all tools. */
19
+ toolAllowList?: string[];
20
+ /** Per-tool execution timeout in ms (from module options). Undefined → no timeout. */
21
+ toolTimeoutMs?: number;
22
+ /** How many follow-up suggestions to generate after the final turn. Undefined/0 → off. */
23
+ followUpsCount?: number;
24
+ /** Inject-mode retriever (from `forRoot({ retrieval })`). Undefined → no prompt augmentation. */
25
+ retriever?: Retriever;
26
+ /** Passages inject-mode retrieval requests. Undefined → loop default (5). */
27
+ retrievalTopK?: number;
28
+ }
29
+ declare function utcDay(date?: Date): string;
30
+
31
+ /**
32
+ * Brand stamped onto a functional-tool provider instance so `AiToolDiscoveryService` picks it up
33
+ * when it walks every provider at boot — the declarative alternative to a manual `registry.register`.
34
+ */
35
+ declare const AGENT_TOOL_BRAND: unique symbol;
36
+ /** A tool expressed as data + handler (e.g. from `createExecuteSqlTool`), not an `@AiTool` class. */
37
+ interface FunctionalTool {
38
+ spec: ToolSpec;
39
+ handler: ToolHandler;
40
+ }
41
+ /**
42
+ * Registers a functional tool declaratively — drop the returned provider in a module's `providers`
43
+ * and `AiToolDiscoveryService` auto-registers it at boot. Each call uses a fresh, unique provide
44
+ * token so the same tool can be provided many times without collisions.
45
+ *
46
+ * Two forms: a static tool (`useValue`) or a factory with `inject` for DI-resolved dependencies
47
+ * (`useFactory`). Either way the resolved instance is branded with {@link AGENT_TOOL_BRAND}.
48
+ */
49
+ declare function provideAgentTool(tool: FunctionalTool): Provider;
50
+ declare function provideAgentTool(factory: (...deps: never[]) => FunctionalTool, inject?: FactoryProvider['inject']): Provider;
51
+
52
+ /**
53
+ * The implicit default agent, configured inline on `forRoot`. It is an {@link AgentDefinition}
54
+ * with `name` optional (defaults to `'default'`) — everything agent-shaped is an AgentDefinition,
55
+ * whether it's this default or one registered via `forFeature`. Omit it entirely for a bare
56
+ * assistant with no base prompt, personas, or tool allow-list.
57
+ */
58
+ type DefaultAgentOptions = Omit<AgentDefinition, 'name'> & {
59
+ name?: string;
60
+ };
61
+ interface AgentModuleOptions {
62
+ /** The LLM provider (e.g. a Vercel AI SDK wrapper). Required. */
63
+ model: ModelProvider;
64
+ /**
65
+ * Persistence adapter. Optional — omit it and import a store module (e.g.
66
+ * `MikroOrmAgentStoreModule.forFeature()`) that binds `AGENT_STORE` globally instead. When
67
+ * provided here it takes precedence within this module's scope.
68
+ */
69
+ store?: AgentStore;
70
+ /** Live token transport. Defaults to a single-process in-memory sink. */
71
+ sink?: TokenStreamSink;
72
+ /**
73
+ * A {@link QuotaStore} for the daily token budget. Optional — omit (and omit `quotaLimitTokens`)
74
+ * to disable quotas. Provide this to plug a custom budget; for the common case use the simpler
75
+ * `quotaLimitTokens` instead, which binds the built-in ledger-backed store.
76
+ */
77
+ quota?: QuotaStore;
78
+ /**
79
+ * Daily per-actor token budget, enforced against the persisted usage ledger by the built-in
80
+ * `LedgerQuotaStore`. A convenience over wiring a {@link QuotaStore} by hand — set this and quotas
81
+ * turn on with no extra store. Ignored when an explicit `quota` is provided. Omit to disable.
82
+ */
83
+ quotaLimitTokens?: number;
84
+ /** Tool authorization gate. Defaults to role-in-`defaultRoles`. */
85
+ rolesPolicy?: RolesPolicy;
86
+ /** Default roles a tool requires when its `roles` is omitted. Defaults to `['ADMIN']`. */
87
+ defaultRoles?: string[];
88
+ /**
89
+ * Resolves the acting actor for each request (the identity seam). Required — the agent NEVER
90
+ * fabricates a caller, so this is a compile-time obligation, not an optional with a throwing
91
+ * placeholder. Read your authenticated principal here (session / JWT / `nestjs-context`), or use
92
+ * the opt-in `HeaderActorResolver` for demos and header-trusting gateways.
93
+ */
94
+ actorResolver: ActorResolver;
95
+ /** Route prefix the controllers mount under. Defaults to `'agent'` (→ `/agent/chat`, …). */
96
+ path?: string;
97
+ /**
98
+ * Run each turn as a durable workflow instead of in-process. Requires importing
99
+ * `AgentDurableModule` from `@dudousxd/nestjs-agent/durable` and a configured `DurableModule`.
100
+ */
101
+ durable?: boolean;
102
+ /**
103
+ * Static functional tools (`{ spec, handler }`, e.g. from `createExecuteSqlTool`) to register at
104
+ * boot. For tools that need DI-resolved dependencies, use `provideAgentTool(factory, inject)` in a
105
+ * module's `providers` instead.
106
+ */
107
+ tools?: FunctionalTool[];
108
+ /**
109
+ * Per-tool execution timeout in ms. A tool that runs longer is aborted and recorded as failed (the
110
+ * model receives the timeout as its result and can adapt) instead of hanging the turn. Omit → none.
111
+ */
112
+ toolTimeoutMs?: number;
113
+ /**
114
+ * Suggest follow-up questions after the final turn. `true` → 3 suggestions; `{ count }` → that many.
115
+ * Costs one extra model call per turn (recorded as `follow_ups` usage), stored on the assistant
116
+ * message's `followUps`. Omit/`false` → disabled.
117
+ */
118
+ followUps?: boolean | {
119
+ count: number;
120
+ };
121
+ /**
122
+ * Always-on ("inject") RAG: before each turn, retrieve passages for the user message and augment
123
+ * the system prompt with them. For agentic retrieval (the model decides when to search) DON'T set
124
+ * this — instead expose the tool: `provideAgentTool(createRetrievalTool(retriever))`. Omit → off.
125
+ */
126
+ retrieval?: {
127
+ mode: 'inject';
128
+ retriever: Retriever;
129
+ topK?: number;
130
+ };
131
+ /**
132
+ * The implicit single agent's config (base prompt, personas, tool allow-list, model/step
133
+ * config). Omit for a bare assistant. Additional named agents are added via `forFeature`.
134
+ */
135
+ defaultAgent?: DefaultAgentOptions;
136
+ }
137
+ interface AgentModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
138
+ inject?: Array<InjectionToken | OptionalFactoryDependency>;
139
+ useFactory: (...args: never[]) => AgentModuleOptions | Promise<AgentModuleOptions>;
140
+ /**
141
+ * Route prefix the controllers mount under (default `'agent'`). Static routing metadata, so it
142
+ * lives here rather than in the async factory result (which resolves too late to mount routes).
143
+ */
144
+ path?: string;
145
+ /**
146
+ * Run each turn as a durable workflow. Static wiring metadata (which `AGENT_RUNNER` to bind), so
147
+ * it lives here rather than in the async factory result — the factory resolves too late to swap
148
+ * the runner. Requires importing `AgentDurableModule` and a configured `DurableModule`.
149
+ */
150
+ durable?: boolean;
151
+ }
152
+
153
+ /** The synthesized `agent`-kind tool name an orchestrator uses to delegate to `target`. */
154
+ declare function delegateToolName(target: string): string;
155
+ /** Builds the per-agent loop deps. The single-agent case is just the `default` definition. */
156
+ declare class AgentDepsFactory {
157
+ private readonly options;
158
+ private readonly model;
159
+ private readonly store;
160
+ private readonly sink;
161
+ private readonly rolesPolicy;
162
+ private readonly registry;
163
+ private readonly agents;
164
+ private readonly quota;
165
+ constructor(options: AgentModuleOptions, model: ModelProvider, store: AgentStore, sink: TokenStreamSink, rolesPolicy: RolesPolicy, registry: ToolRegistry, agents: AgentRegistry, quota: QuotaStore | undefined);
166
+ defaultAgentName(): string;
167
+ private effectiveTools;
168
+ forAgent(agentName?: string): AgentDeps;
169
+ /** Normalize the `followUps` option (`true` → 3, `{ count }` → count) to a number, or undefined. */
170
+ private followUpsCount;
171
+ }
172
+
173
+ export { AgentDepsFactory as A, type DefaultAgentOptions as D, type FunctionalTool as F, type AgentModuleOptions as a, type AgentModuleAsyncOptions as b, AGENT_TOOL_BRAND as c, type AgentDeps as d, delegateToolName as e, provideAgentTool as p, utcDay as u };