@dudousxd/nestjs-agent 0.9.0 → 0.10.1

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
@@ -41,6 +41,42 @@ The module mounts SSE + REST endpoints under `/agent` (`POST /agent/chat`, tool-
41
41
  threads, quota). Add `durable: true` + `AgentDurableModule` for the durable runner;
42
42
  `AgentModule.forFeature([…])` for multi-agent orchestration.
43
43
 
44
+ ### Deploying split API/worker pods
45
+
46
+ By default `AgentModule`/`AgentDurableModule` wire everything: every controller, the `agent.run`
47
+ durable workflow, and its dispatched steps (`AgentRunSteps.llm`/`.tool`). Fine for a single combined
48
+ pod — but an API/dashboard pod and a worker pod each importing the FULL module means the API pod also
49
+ registers the dispatched-step handlers, subscribing their queues and running LLM/tool work it has no
50
+ business doing (and a worker pod mounts HTTP routes nothing ever calls).
51
+
52
+ `surface: 'http' | 'engine' | 'both'` (default `'both'`) splits that in two:
53
+
54
+ ```ts
55
+ // worker container: no HTTP routes, but the workflow + dispatched steps ARE registered
56
+ imports: [
57
+ DurableModule.forRoot({ store, transport }),
58
+ AgentModule.forRoot({ model, store, actorResolver, durable: true, surface: 'engine' }),
59
+ AgentDurableModule.forRoot({ surface: 'engine' }),
60
+ ]
61
+
62
+ // api container: every controller works, but AgentRunSteps is never registered here —
63
+ // pair this with the durable `drive: false` enqueue-only config so this pod also never
64
+ // polls/executes work on its own.
65
+ imports: [
66
+ DurableModule.forRoot({ store, transport, drive: false }),
67
+ AgentModule.forRoot({ model, store, actorResolver, durable: true, surface: 'http' }),
68
+ AgentDurableModule.forRoot({ surface: 'http' }),
69
+ ]
70
+ ```
71
+
72
+ `AgentDurableModule.forRoot({ surface })` mirrors `AgentModule`'s own `surface` — set it in both
73
+ places (or use the `agentDurable(options)` helper, which threads one `surface` to both calls). The
74
+ `'http'` side still registers the `agent.run` WORKFLOW (so starting a run succeeds) but never the
75
+ step handlers, so it can never pick up LLM/tool work meant for the worker fleet. This is exactly the
76
+ fix for a durable skew-protection crash-loop where a bare-imported module on the wrong pod type
77
+ subscribed queues it should never have served. flip composes this the same way `APP_TYPE` composes
78
+ its other modules: the worker container loads the `engine` surface, the API container loads `http`.
79
+
44
80
  See the [monorepo README](https://github.com/DavideCarvalho/aviary) for the full guide (durability,
45
81
  multi-agent, authz, governed SQL, the React frontend, diagnostics).
46
82
 
@@ -56,6 +56,26 @@ interface FunctionalTool {
56
56
  declare function provideAgentTool(tool: FunctionalTool): Provider;
57
57
  declare function provideAgentTool(factory: (...deps: never[]) => FunctionalTool, inject?: FactoryProvider['inject']): Provider;
58
58
 
59
+ /**
60
+ * Which half of the module this process mounts — the split that lets an API pod and a WORKER pod
61
+ * each load `AgentModule` without either one doing the other's job:
62
+ *
63
+ * - `'both'` (default): today's behavior, unconditionally — every controller AND the full durable
64
+ * engine wiring (when `durable: true`). Zero-config single-pod deployments never touch this.
65
+ * - `'http'`: mounts every controller (chat/threads/tool-call/quota/agents/attachments) so the HTTP
66
+ * surface is fully functional, but under `durable: true` this pod must never EXECUTE agent work —
67
+ * see `AgentDurableModuleOptions.surface` (`@dudousxd/nestjs-agent/durable`), which is where the
68
+ * actual step-handler exclusion happens; this flag only controls the controllers here.
69
+ * - `'engine'`: mounts NO controllers (an API pod's routes have no business existing on a worker
70
+ * pod that never receives HTTP traffic) — everything else (registry/tools/model/store/sinks, the
71
+ * `agent.run` workflow, and its dispatched steps under `AgentDurableModule`) stays wired exactly
72
+ * as `'both'`.
73
+ *
74
+ * A durable deployment mirrors this on `AgentDurableModule.forRoot({ surface })` too — see that
75
+ * module's doc for why the split can't be inferred from this option alone (Nest's static module
76
+ * metadata can't be computed from a runtime-injected value).
77
+ */
78
+ type AgentSurface = 'http' | 'engine' | 'both';
59
79
  /**
60
80
  * `AgentModuleOptions.attachments` — bounds and content-type allowlist for the optional
61
81
  * `POST /agent/attachments` upload controller (see `attachments.upload`).
@@ -188,6 +208,12 @@ interface AgentModuleOptions {
188
208
  guards?: Type<CanActivate>[];
189
209
  /** Bounds/allowlist for the optional attachment-upload controller. Omit → 20 MiB, the documented default types, not mounted. */
190
210
  attachments?: AgentAttachmentsOptions;
211
+ /**
212
+ * Which half of the module this process mounts (see {@link AgentSurface}). Omit → `'both'`,
213
+ * today's behavior with zero change. STATIC top-level field (like `durable`/`path`) — it decides
214
+ * which controllers exist at module-build time, not something a request can flip.
215
+ */
216
+ surface?: AgentSurface;
191
217
  }
192
218
  interface AgentModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
193
219
  inject?: Array<InjectionToken | OptionalFactoryDependency>;
@@ -239,6 +265,12 @@ interface AgentModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
239
265
  * only the yes/no mount decision has to live here.
240
266
  */
241
267
  attachmentsUpload?: boolean;
268
+ /**
269
+ * Which half of the module this process mounts (see {@link AgentSurface}). Same static-wiring
270
+ * reasoning as `attachmentsUpload`/`durable` above — `useFactory` resolves too late to decide
271
+ * which controllers exist. Omit → `'both'`, today's behavior with zero change.
272
+ */
273
+ surface?: AgentSurface;
242
274
  }
243
275
 
244
276
  /** The synthesized `agent`-kind tool name an orchestrator uses to hand off to `target`. */
@@ -267,4 +299,4 @@ declare class AgentDepsFactory {
267
299
  private followUpsCount;
268
300
  }
269
301
 
270
- export { AgentDepsFactory as A, type FunctionalTool as F, type AgentModuleOptions as a, type AgentModuleAsyncOptions as b, AGENT_TOOL_BRAND as c, type AgentAttachmentsOptions as d, type AgentDeps as e, delegateToolName as f, provideAgentTool as p, utcDay as u };
302
+ export { AgentDepsFactory as A, type FunctionalTool as F, type AgentSurface as a, type AgentModuleOptions as b, type AgentModuleAsyncOptions as c, AGENT_TOOL_BRAND as d, type AgentAttachmentsOptions as e, type AgentDeps as f, delegateToolName as g, provideAgentTool as p, utcDay as u };
@@ -56,6 +56,26 @@ interface FunctionalTool {
56
56
  declare function provideAgentTool(tool: FunctionalTool): Provider;
57
57
  declare function provideAgentTool(factory: (...deps: never[]) => FunctionalTool, inject?: FactoryProvider['inject']): Provider;
58
58
 
59
+ /**
60
+ * Which half of the module this process mounts — the split that lets an API pod and a WORKER pod
61
+ * each load `AgentModule` without either one doing the other's job:
62
+ *
63
+ * - `'both'` (default): today's behavior, unconditionally — every controller AND the full durable
64
+ * engine wiring (when `durable: true`). Zero-config single-pod deployments never touch this.
65
+ * - `'http'`: mounts every controller (chat/threads/tool-call/quota/agents/attachments) so the HTTP
66
+ * surface is fully functional, but under `durable: true` this pod must never EXECUTE agent work —
67
+ * see `AgentDurableModuleOptions.surface` (`@dudousxd/nestjs-agent/durable`), which is where the
68
+ * actual step-handler exclusion happens; this flag only controls the controllers here.
69
+ * - `'engine'`: mounts NO controllers (an API pod's routes have no business existing on a worker
70
+ * pod that never receives HTTP traffic) — everything else (registry/tools/model/store/sinks, the
71
+ * `agent.run` workflow, and its dispatched steps under `AgentDurableModule`) stays wired exactly
72
+ * as `'both'`.
73
+ *
74
+ * A durable deployment mirrors this on `AgentDurableModule.forRoot({ surface })` too — see that
75
+ * module's doc for why the split can't be inferred from this option alone (Nest's static module
76
+ * metadata can't be computed from a runtime-injected value).
77
+ */
78
+ type AgentSurface = 'http' | 'engine' | 'both';
59
79
  /**
60
80
  * `AgentModuleOptions.attachments` — bounds and content-type allowlist for the optional
61
81
  * `POST /agent/attachments` upload controller (see `attachments.upload`).
@@ -188,6 +208,12 @@ interface AgentModuleOptions {
188
208
  guards?: Type<CanActivate>[];
189
209
  /** Bounds/allowlist for the optional attachment-upload controller. Omit → 20 MiB, the documented default types, not mounted. */
190
210
  attachments?: AgentAttachmentsOptions;
211
+ /**
212
+ * Which half of the module this process mounts (see {@link AgentSurface}). Omit → `'both'`,
213
+ * today's behavior with zero change. STATIC top-level field (like `durable`/`path`) — it decides
214
+ * which controllers exist at module-build time, not something a request can flip.
215
+ */
216
+ surface?: AgentSurface;
191
217
  }
192
218
  interface AgentModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
193
219
  inject?: Array<InjectionToken | OptionalFactoryDependency>;
@@ -239,6 +265,12 @@ interface AgentModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
239
265
  * only the yes/no mount decision has to live here.
240
266
  */
241
267
  attachmentsUpload?: boolean;
268
+ /**
269
+ * Which half of the module this process mounts (see {@link AgentSurface}). Same static-wiring
270
+ * reasoning as `attachmentsUpload`/`durable` above — `useFactory` resolves too late to decide
271
+ * which controllers exist. Omit → `'both'`, today's behavior with zero change.
272
+ */
273
+ surface?: AgentSurface;
242
274
  }
243
275
 
244
276
  /** The synthesized `agent`-kind tool name an orchestrator uses to hand off to `target`. */
@@ -267,4 +299,4 @@ declare class AgentDepsFactory {
267
299
  private followUpsCount;
268
300
  }
269
301
 
270
- export { AgentDepsFactory as A, type FunctionalTool as F, type AgentModuleOptions as a, type AgentModuleAsyncOptions as b, AGENT_TOOL_BRAND as c, type AgentAttachmentsOptions as d, type AgentDeps as e, delegateToolName as f, provideAgentTool as p, utcDay as u };
302
+ export { AgentDepsFactory as A, type FunctionalTool as F, type AgentSurface as a, type AgentModuleOptions as b, type AgentModuleAsyncOptions as c, AGENT_TOOL_BRAND as d, type AgentAttachmentsOptions as e, type AgentDeps as f, delegateToolName as g, provideAgentTool as p, utcDay as u };
@@ -31,10 +31,10 @@ __export(durable_exports, {
31
31
  module.exports = __toCommonJS(durable_exports);
32
32
 
33
33
  // src/durable/agent-run.workflow.ts
34
- var import_nestjs_agent_core2 = require("@dudousxd/nestjs-agent-core");
35
- var import_nestjs_durable2 = require("@dudousxd/nestjs-durable");
34
+ var import_nestjs_agent_core = require("@dudousxd/nestjs-agent-core");
35
+ var import_nestjs_durable = require("@dudousxd/nestjs-durable");
36
36
  var import_nestjs_durable_core = require("@dudousxd/nestjs-durable-core");
37
- var import_common2 = require("@nestjs/common");
37
+ var import_common = require("@nestjs/common");
38
38
 
39
39
  // src/agent-deps.ts
40
40
  function utcDay(date = /* @__PURE__ */ new Date()) {
@@ -52,10 +52,10 @@ function childSinkWriter(writer) {
52
52
  }
53
53
  __name(childSinkWriter, "childSinkWriter");
54
54
 
55
- // src/durable/agent-run.steps.ts
56
- var import_nestjs_agent_core = require("@dudousxd/nestjs-agent-core");
57
- var import_nestjs_durable = require("@dudousxd/nestjs-durable");
58
- var import_common = require("@nestjs/common");
55
+ // src/durable/dispatched-steps.token.ts
56
+ var AGENT_DISPATCHED_STEPS = Symbol.for("@dudousxd/nestjs-agent:dispatched-steps");
57
+
58
+ // src/durable/agent-run.workflow.ts
59
59
  function _ts_decorate(decorators, target, key, desc) {
60
60
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
61
61
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -73,122 +73,6 @@ function _ts_param(paramIndex, decorator) {
73
73
  };
74
74
  }
75
75
  __name(_ts_param, "_ts_param");
76
- var AgentRunSteps = class {
77
- static {
78
- __name(this, "AgentRunSteps");
79
- }
80
- factory;
81
- constructor(factory) {
82
- this.factory = factory;
83
- }
84
- /**
85
- * Retryable: every side effect (persist/quota/stream markers) happens loop-side under
86
- * `hooks.step`, so a durable retry only re-runs the model call and re-streams its tokens — never
87
- * double-writes. NEVER call `writer.end()`/`fail()` here: the workflow owns the stream's lifecycle,
88
- * across however many turns/retries it takes.
89
- */
90
- async llm(input) {
91
- const deps = this.factory.forAgent(input.agentName);
92
- const tools = await deps.registry.definitionsFor(input.actor, deps.rolesPolicy, deps.toolAllowList);
93
- const opened = await deps.sink.open(input.sinkRunId);
94
- const writer = input.childSink ? childSinkWriter(opened) : opened;
95
- return (0, import_nestjs_agent_core.traceLlmTurn)(input.runId, input.step, () => deps.model.runTurn({
96
- system: input.system,
97
- messages: input.messages,
98
- tools,
99
- sink: writer
100
- }));
101
- }
102
- /**
103
- * NO DURABLE retries — a tool may not be idempotent (a bare `@Step()` carries none, unlike `llm`
104
- * above). What it does retry, in place, is a CLASSIFIED-TRANSIENT error from the tool's own
105
- * invocation (a DB deadlock, a lock-wait timeout — see `isTransientToolError`): the server rolled
106
- * that work back, so re-invoking it is safe, and doing it here (inside this one step, never a new
107
- * checkpoint) is exactly how the non-dispatched loop path handles it too. The timeout is applied
108
- * HERE, handler-side, via `withToolTimeout` — never as a durable dispatch `timeoutMs`, which
109
- * bounds engine liveness (retryable) rather than the tool's own business outcome — and per
110
- * ATTEMPT, so a retried invocation gets its own fresh timeout window.
111
- */
112
- async tool(input) {
113
- const deps = this.factory.forAgent(input.ctx.agentName);
114
- const ctx = {
115
- ...input.ctx
116
- };
117
- const localClassify = deps.toolTransientRetry !== false ? deps.toolTransientRetry?.classify : void 0;
118
- const transientRetry = input.transientRetry === false ? false : {
119
- attempts: input.transientRetry.attempts,
120
- backoffMs: input.transientRetry.backoffMs,
121
- ...localClassify !== void 0 ? {
122
- classify: localClassify
123
- } : {}
124
- };
125
- return (0, import_nestjs_agent_core.traceToolExecution)(input.ctx.runId, {
126
- toolCallId: input.toolCallId,
127
- toolName: input.toolName,
128
- toolType: input.toolType
129
- }, () => (0, import_nestjs_agent_core.invokeWithTransientRetry)(() => {
130
- const invocation = deps.registry.invoke(input.toolName, input.input, ctx, deps.rolesPolicy);
131
- return input.timeoutMs !== void 0 ? (0, import_nestjs_agent_core.withToolTimeout)(invocation, input.timeoutMs, input.toolName) : invocation;
132
- }, transientRetry, {
133
- onRetry: /* @__PURE__ */ __name((attempt, error) => {
134
- (0, import_nestjs_agent_core.publishAgentToolRetry)({
135
- toolName: input.toolName,
136
- toolCallId: input.toolCallId,
137
- attempt,
138
- message: error instanceof Error ? error.message : String(error)
139
- });
140
- }, "onRetry")
141
- }));
142
- }
143
- };
144
- _ts_decorate([
145
- (0, import_nestjs_durable.Step)({
146
- retries: 3
147
- }),
148
- _ts_metadata("design:type", Function),
149
- _ts_metadata("design:paramtypes", [
150
- typeof DispatchedLlmInput === "undefined" ? Object : DispatchedLlmInput
151
- ]),
152
- _ts_metadata("design:returntype", Promise)
153
- ], AgentRunSteps.prototype, "llm", null);
154
- _ts_decorate([
155
- (0, import_nestjs_durable.Step)(),
156
- _ts_metadata("design:type", Function),
157
- _ts_metadata("design:paramtypes", [
158
- typeof DispatchedToolInput === "undefined" ? Object : DispatchedToolInput
159
- ]),
160
- _ts_metadata("design:returntype", Promise)
161
- ], AgentRunSteps.prototype, "tool", null);
162
- AgentRunSteps = _ts_decorate([
163
- (0, import_common.Injectable)(),
164
- _ts_param(0, (0, import_common.Inject)(import_nestjs_agent_core.AGENT_DEPS_FACTORY)),
165
- _ts_metadata("design:type", Function),
166
- _ts_metadata("design:paramtypes", [
167
- typeof AgentDepsFactory === "undefined" ? Object : AgentDepsFactory
168
- ])
169
- ], AgentRunSteps);
170
-
171
- // src/durable/dispatched-steps.token.ts
172
- var AGENT_DISPATCHED_STEPS = Symbol.for("@dudousxd/nestjs-agent:dispatched-steps");
173
-
174
- // src/durable/agent-run.workflow.ts
175
- function _ts_decorate2(decorators, target, key, desc) {
176
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
177
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
178
- else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
179
- return c > 3 && r && Object.defineProperty(target, key, r), r;
180
- }
181
- __name(_ts_decorate2, "_ts_decorate");
182
- function _ts_metadata2(k, v) {
183
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
184
- }
185
- __name(_ts_metadata2, "_ts_metadata");
186
- function _ts_param2(paramIndex, decorator) {
187
- return function(target, key) {
188
- decorator(target, key, paramIndex);
189
- };
190
- }
191
- __name(_ts_param2, "_ts_param");
192
76
  var AgentRunWorkflow = class _AgentRunWorkflow {
193
77
  static {
194
78
  __name(this, "AgentRunWorkflow");
@@ -206,6 +90,7 @@ var AgentRunWorkflow = class _AgentRunWorkflow {
206
90
  async run(ctx, input) {
207
91
  const day = input.day ?? utcDay();
208
92
  const deps = this.factory.forAgent(input.agentName);
93
+ const dispatchSteps = this.dispatchedSteps ? this.steps : void 0;
209
94
  if (input.sinkRunId !== void 0) {
210
95
  await ctx.localStep("activate", () => this.store.setActiveStream(input.threadId, ctx.runId));
211
96
  }
@@ -243,8 +128,8 @@ var AgentRunWorkflow = class _AgentRunWorkflow {
243
128
  // `ctx.localStep`s, so a turn isn't pinned to this workflow worker for the model call or a tool
244
129
  // execution. `sinkRunId`/`childSink` are sink routing this workflow already resolved above —
245
130
  // core's dispatchLlm signature stays sink-topology-agnostic, so we add them here, not in core.
246
- ...this.dispatchedSteps ? {
247
- dispatchLlm: /* @__PURE__ */ __name((index, envelope) => ctx.step(this.steps.llm, {
131
+ ...dispatchSteps !== void 0 ? {
132
+ dispatchLlm: /* @__PURE__ */ __name((index, envelope) => ctx.step(dispatchSteps.llm, {
248
133
  ...envelope,
249
134
  runId: ctx.runId,
250
135
  step: index,
@@ -256,7 +141,7 @@ var AgentRunWorkflow = class _AgentRunWorkflow {
256
141
  // ('agent'-kind delegations branch to runAgent earlier in the loop), so anything that
257
142
  // isn't 'action' is defensively 'read' — the same posture core takes for an
258
143
  // unresolvable `kind`.
259
- dispatchTool: /* @__PURE__ */ __name((call, envelope) => ctx.step(this.steps.tool, {
144
+ dispatchTool: /* @__PURE__ */ __name((call, envelope) => ctx.step(dispatchSteps.tool, {
260
145
  ...envelope,
261
146
  toolCallId: call.id,
262
147
  toolType: call.kind === "action" ? "action" : "read"
@@ -264,7 +149,7 @@ var AgentRunWorkflow = class _AgentRunWorkflow {
264
149
  } : {}
265
150
  };
266
151
  try {
267
- const result = await (0, import_nestjs_agent_core2.runAgentLoop)({
152
+ const result = await (0, import_nestjs_agent_core.runAgentLoop)({
268
153
  ...deps,
269
154
  day
270
155
  }, input, hooks);
@@ -275,8 +160,8 @@ var AgentRunWorkflow = class _AgentRunWorkflow {
275
160
  throw error;
276
161
  }
277
162
  const message = error instanceof Error ? error.message : String(error);
278
- const code = error instanceof import_nestjs_agent_core2.QuotaExceededError ? "quota_exceeded" : "run_failed";
279
- (0, import_nestjs_agent_core2.publishAgentRunFailed)({
163
+ const code = error instanceof import_nestjs_agent_core.QuotaExceededError ? "quota_exceeded" : "run_failed";
164
+ (0, import_nestjs_agent_core.publishAgentRunFailed)({
280
165
  runId: ctx.runId,
281
166
  code,
282
167
  message
@@ -297,24 +182,141 @@ var AgentRunWorkflow = class _AgentRunWorkflow {
297
182
  }
298
183
  }
299
184
  };
300
- AgentRunWorkflow = _ts_decorate2([
301
- (0, import_common2.Injectable)(),
302
- (0, import_nestjs_durable2.Workflow)({
185
+ AgentRunWorkflow = _ts_decorate([
186
+ (0, import_common.Injectable)(),
187
+ (0, import_nestjs_durable.Workflow)({
303
188
  name: "agent.run",
304
189
  version: "1"
305
190
  }),
306
- _ts_param2(0, (0, import_common2.Inject)(import_nestjs_agent_core2.AGENT_DEPS_FACTORY)),
307
- _ts_param2(1, (0, import_common2.Inject)(import_nestjs_agent_core2.AGENT_STORE)),
308
- _ts_param2(3, (0, import_common2.Inject)(AGENT_DISPATCHED_STEPS)),
309
- _ts_metadata2("design:type", Function),
310
- _ts_metadata2("design:paramtypes", [
191
+ _ts_param(0, (0, import_common.Inject)(import_nestjs_agent_core.AGENT_DEPS_FACTORY)),
192
+ _ts_param(1, (0, import_common.Inject)(import_nestjs_agent_core.AGENT_STORE)),
193
+ _ts_param(2, (0, import_common.Optional)()),
194
+ _ts_param(3, (0, import_common.Inject)(AGENT_DISPATCHED_STEPS)),
195
+ _ts_metadata("design:type", Function),
196
+ _ts_metadata("design:paramtypes", [
311
197
  typeof AgentDepsFactory === "undefined" ? Object : AgentDepsFactory,
312
198
  typeof AgentStore === "undefined" ? Object : AgentStore,
313
- typeof AgentRunSteps === "undefined" ? Object : AgentRunSteps,
199
+ Object,
314
200
  Boolean
315
201
  ])
316
202
  ], AgentRunWorkflow);
317
203
 
204
+ // src/durable/agent-run.steps.ts
205
+ var import_nestjs_agent_core2 = require("@dudousxd/nestjs-agent-core");
206
+ var import_nestjs_durable2 = require("@dudousxd/nestjs-durable");
207
+ var import_common2 = require("@nestjs/common");
208
+ function _ts_decorate2(decorators, target, key, desc) {
209
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
210
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
211
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
212
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
213
+ }
214
+ __name(_ts_decorate2, "_ts_decorate");
215
+ function _ts_metadata2(k, v) {
216
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
217
+ }
218
+ __name(_ts_metadata2, "_ts_metadata");
219
+ function _ts_param2(paramIndex, decorator) {
220
+ return function(target, key) {
221
+ decorator(target, key, paramIndex);
222
+ };
223
+ }
224
+ __name(_ts_param2, "_ts_param");
225
+ var AgentRunSteps = class {
226
+ static {
227
+ __name(this, "AgentRunSteps");
228
+ }
229
+ factory;
230
+ constructor(factory) {
231
+ this.factory = factory;
232
+ }
233
+ /**
234
+ * Retryable: every side effect (persist/quota/stream markers) happens loop-side under
235
+ * `hooks.step`, so a durable retry only re-runs the model call and re-streams its tokens — never
236
+ * double-writes. NEVER call `writer.end()`/`fail()` here: the workflow owns the stream's lifecycle,
237
+ * across however many turns/retries it takes.
238
+ */
239
+ async llm(input) {
240
+ const deps = this.factory.forAgent(input.agentName);
241
+ const tools = await deps.registry.definitionsFor(input.actor, deps.rolesPolicy, deps.toolAllowList);
242
+ const opened = await deps.sink.open(input.sinkRunId);
243
+ const writer = input.childSink ? childSinkWriter(opened) : opened;
244
+ return (0, import_nestjs_agent_core2.traceLlmTurn)(input.runId, input.step, () => deps.model.runTurn({
245
+ system: input.system,
246
+ messages: input.messages,
247
+ tools,
248
+ sink: writer
249
+ }));
250
+ }
251
+ /**
252
+ * NO DURABLE retries — a tool may not be idempotent (a bare `@Step()` carries none, unlike `llm`
253
+ * above). What it does retry, in place, is a CLASSIFIED-TRANSIENT error from the tool's own
254
+ * invocation (a DB deadlock, a lock-wait timeout — see `isTransientToolError`): the server rolled
255
+ * that work back, so re-invoking it is safe, and doing it here (inside this one step, never a new
256
+ * checkpoint) is exactly how the non-dispatched loop path handles it too. The timeout is applied
257
+ * HERE, handler-side, via `withToolTimeout` — never as a durable dispatch `timeoutMs`, which
258
+ * bounds engine liveness (retryable) rather than the tool's own business outcome — and per
259
+ * ATTEMPT, so a retried invocation gets its own fresh timeout window.
260
+ */
261
+ async tool(input) {
262
+ const deps = this.factory.forAgent(input.ctx.agentName);
263
+ const ctx = {
264
+ ...input.ctx
265
+ };
266
+ const localClassify = deps.toolTransientRetry !== false ? deps.toolTransientRetry?.classify : void 0;
267
+ const transientRetry = input.transientRetry === false ? false : {
268
+ attempts: input.transientRetry.attempts,
269
+ backoffMs: input.transientRetry.backoffMs,
270
+ ...localClassify !== void 0 ? {
271
+ classify: localClassify
272
+ } : {}
273
+ };
274
+ return (0, import_nestjs_agent_core2.traceToolExecution)(input.ctx.runId, {
275
+ toolCallId: input.toolCallId,
276
+ toolName: input.toolName,
277
+ toolType: input.toolType
278
+ }, () => (0, import_nestjs_agent_core2.invokeWithTransientRetry)(() => {
279
+ const invocation = deps.registry.invoke(input.toolName, input.input, ctx, deps.rolesPolicy);
280
+ return input.timeoutMs !== void 0 ? (0, import_nestjs_agent_core2.withToolTimeout)(invocation, input.timeoutMs, input.toolName) : invocation;
281
+ }, transientRetry, {
282
+ onRetry: /* @__PURE__ */ __name((attempt, error) => {
283
+ (0, import_nestjs_agent_core2.publishAgentToolRetry)({
284
+ toolName: input.toolName,
285
+ toolCallId: input.toolCallId,
286
+ attempt,
287
+ message: error instanceof Error ? error.message : String(error)
288
+ });
289
+ }, "onRetry")
290
+ }));
291
+ }
292
+ };
293
+ _ts_decorate2([
294
+ (0, import_nestjs_durable2.Step)({
295
+ retries: 3
296
+ }),
297
+ _ts_metadata2("design:type", Function),
298
+ _ts_metadata2("design:paramtypes", [
299
+ typeof DispatchedLlmInput === "undefined" ? Object : DispatchedLlmInput
300
+ ]),
301
+ _ts_metadata2("design:returntype", Promise)
302
+ ], AgentRunSteps.prototype, "llm", null);
303
+ _ts_decorate2([
304
+ (0, import_nestjs_durable2.Step)(),
305
+ _ts_metadata2("design:type", Function),
306
+ _ts_metadata2("design:paramtypes", [
307
+ typeof DispatchedToolInput === "undefined" ? Object : DispatchedToolInput
308
+ ]),
309
+ _ts_metadata2("design:returntype", Promise)
310
+ ], AgentRunSteps.prototype, "tool", null);
311
+ AgentRunSteps = _ts_decorate2([
312
+ (0, import_common2.Injectable)(),
313
+ _ts_param2(0, (0, import_common2.Inject)(import_nestjs_agent_core2.AGENT_DEPS_FACTORY)),
314
+ _ts_metadata2("design:type", Function),
315
+ _ts_metadata2("design:paramtypes", [
316
+ typeof AgentDepsFactory === "undefined" ? Object : AgentDepsFactory
317
+ ])
318
+ ], AgentRunSteps);
319
+
318
320
  // src/durable/durable-agent-runner.ts
319
321
  var import_node_crypto = require("crypto");
320
322
  var import_nestjs_durable3 = require("@dudousxd/nestjs-durable");
@@ -452,16 +454,68 @@ function _ts_decorate4(decorators, target, key, desc) {
452
454
  }
453
455
  __name(_ts_decorate4, "_ts_decorate");
454
456
  var logger = new import_common4.Logger("AgentDurableModule");
457
+ function runnerProviders() {
458
+ return [
459
+ DurableAgentRunner,
460
+ {
461
+ provide: import_nestjs_agent_core4.AGENT_DURABLE_RUNNER,
462
+ useExisting: DurableAgentRunner
463
+ },
464
+ {
465
+ provide: AGENT_DISPATCHED_STEPS,
466
+ useFactory: /* @__PURE__ */ __name((options, sink) => {
467
+ const enabled = options.durable === true && options.dispatchedSteps !== false;
468
+ if (enabled && sink instanceof InProcessTokenStreamSink) {
469
+ logger.warn("Dispatched steps are active (the default under durable: true) with the default InProcessTokenStreamSink, which only buffers tokens in-process. A dispatched `llm` step served by a different worker cannot stream into this buffer. Wire a cross-process TokenStreamSink (e.g. a Redis pub/sub sink) via AgentModule.forRoot({ sink }) before running multi-pod, or set dispatchedSteps: false to keep the turn in-process localSteps.");
470
+ }
471
+ return enabled;
472
+ }, "useFactory"),
473
+ inject: [
474
+ import_nestjs_agent_core4.AGENT_OPTIONS,
475
+ import_nestjs_agent_core4.AGENT_SINK
476
+ ]
477
+ }
478
+ ];
479
+ }
480
+ __name(runnerProviders, "runnerProviders");
481
+ var AgentDurableHttpModule = class AgentDurableHttpModule2 {
482
+ static {
483
+ __name(this, "AgentDurableHttpModule");
484
+ }
485
+ };
486
+ AgentDurableHttpModule = _ts_decorate4([
487
+ (0, import_common4.Global)(),
488
+ (0, import_common4.Module)({
489
+ providers: [
490
+ AgentRunWorkflow,
491
+ ...runnerProviders()
492
+ ],
493
+ exports: [
494
+ import_nestjs_agent_core4.AGENT_DURABLE_RUNNER,
495
+ AgentRunWorkflow,
496
+ AGENT_DISPATCHED_STEPS
497
+ ]
498
+ })
499
+ ], AgentDurableHttpModule);
455
500
  var AgentDurableModule = class _AgentDurableModule {
456
501
  static {
457
502
  __name(this, "AgentDurableModule");
458
503
  }
459
504
  /**
460
- * The dynamic form used by the `agentDurable` helper. The module needs no configuration of its
461
- * own (its providers come from the decorator above), so this just yields the module reference —
462
- * importing `AgentDurableModule` directly stays equivalent.
505
+ * `surface: 'http'` returns the reduced `AgentDurableHttpModule` wiring (no `AgentRunSteps`);
506
+ * everything else (omitted, `'engine'`, `'both'`) returns this module's own full wiring — the
507
+ * class's `@Module` providers above merge in regardless of which DynamicModule shape a caller
508
+ * imports (Nest merges a class's static provider list with whatever a self-referencing
509
+ * `DynamicModule` adds), which is exactly why the reduced case has to be a DIFFERENT module class
510
+ * rather than a conditional subset of this one's own decorator.
463
511
  */
464
- static forRoot() {
512
+ static forRoot(options) {
513
+ if (options?.surface === "http") {
514
+ return {
515
+ module: AgentDurableHttpModule,
516
+ global: true
517
+ };
518
+ }
465
519
  return {
466
520
  module: _AgentDurableModule,
467
521
  global: true
@@ -474,25 +528,7 @@ AgentDurableModule = _ts_decorate4([
474
528
  providers: [
475
529
  AgentRunWorkflow,
476
530
  AgentRunSteps,
477
- DurableAgentRunner,
478
- {
479
- provide: import_nestjs_agent_core4.AGENT_DURABLE_RUNNER,
480
- useExisting: DurableAgentRunner
481
- },
482
- {
483
- provide: AGENT_DISPATCHED_STEPS,
484
- useFactory: /* @__PURE__ */ __name((options, sink) => {
485
- const enabled = options.durable === true && options.dispatchedSteps !== false;
486
- if (enabled && sink instanceof InProcessTokenStreamSink) {
487
- logger.warn("Dispatched steps are active (the default under durable: true) with the default InProcessTokenStreamSink, which only buffers tokens in-process. A dispatched `llm` step served by a different worker cannot stream into this buffer. Wire a cross-process TokenStreamSink (e.g. a Redis pub/sub sink) via AgentModule.forRoot({ sink }) before running multi-pod, or set dispatchedSteps: false to keep the turn in-process localSteps.");
488
- }
489
- return enabled;
490
- }, "useFactory"),
491
- inject: [
492
- import_nestjs_agent_core4.AGENT_OPTIONS,
493
- import_nestjs_agent_core4.AGENT_SINK
494
- ]
495
- }
531
+ ...runnerProviders()
496
532
  ],
497
533
  exports: [
498
534
  import_nestjs_agent_core4.AGENT_DURABLE_RUNNER,
@@ -2279,6 +2315,10 @@ function controllersFor(mountAttachments) {
2279
2315
  ] : BASE_CONTROLLERS;
2280
2316
  }
2281
2317
  __name(controllersFor, "controllersFor");
2318
+ function controllersForSurface(surface, mountAttachments) {
2319
+ return surface === "engine" ? [] : controllersFor(mountAttachments);
2320
+ }
2321
+ __name(controllersForSurface, "controllersForSurface");
2282
2322
  var GUARDS_METADATA = "__guards__";
2283
2323
  function applyGuards(guards) {
2284
2324
  const resolved = guards ?? [];
@@ -2324,7 +2364,7 @@ var AgentModule = class _AgentModule {
2324
2364
  import_core3.DiscoveryModule,
2325
2365
  routerFor(path)
2326
2366
  ],
2327
- controllers: controllersFor(options.attachments?.upload === true),
2367
+ controllers: controllersForSurface(options.surface, options.attachments?.upload === true),
2328
2368
  providers: [
2329
2369
  {
2330
2370
  provide: import_nestjs_agent_core17.AGENT_OPTIONS,
@@ -2349,17 +2389,21 @@ var AgentModule = class _AgentModule {
2349
2389
  routerFor(path),
2350
2390
  ...options.imports ?? []
2351
2391
  ],
2352
- controllers: controllersFor(options.attachmentsUpload === true),
2392
+ controllers: controllersForSurface(options.surface, options.attachmentsUpload === true),
2353
2393
  providers: [
2354
2394
  {
2355
2395
  provide: import_nestjs_agent_core17.AGENT_OPTIONS,
2356
- // Stamp the STATIC wiring flags onto the factory result: `durable`/`dispatchedSteps` are
2357
- // authoritative on the async config object (they decided the actual wiring above), and
2358
- // `AgentDurableModule` derives the effective dispatched-steps value from AGENT_OPTIONS —
2359
- // without this stamp, an async host would get in-process localSteps despite durable: true.
2396
+ // Stamp the STATIC wiring flags onto the factory result: `durable`/`dispatchedSteps`/
2397
+ // `surface` are authoritative on the async config object (they decided the actual wiring
2398
+ // above), and `AgentDurableModule` derives the effective dispatched-steps value from
2399
+ // AGENT_OPTIONS — without this stamp, an async host would get in-process localSteps
2400
+ // despite durable: true. `surface` isn't read back by anything today (AgentDurableModule
2401
+ // takes its own mirror option — see its doc for why), but it's stamped here too so the
2402
+ // EFFECTIVE value is always observable off AGENT_OPTIONS, sync or async alike.
2360
2403
  useFactory: /* @__PURE__ */ __name(async (...args) => ({
2361
2404
  ...await options.useFactory(...args),
2362
2405
  durable: options.durable ?? false,
2406
+ surface: options.surface ?? "both",
2363
2407
  ...options.dispatchedSteps !== void 0 ? {
2364
2408
  dispatchedSteps: options.dispatchedSteps
2365
2409
  } : {}
@@ -2385,7 +2429,9 @@ function agentDurable(options) {
2385
2429
  ...options,
2386
2430
  durable: true
2387
2431
  }),
2388
- AgentDurableModule.forRoot()
2432
+ AgentDurableModule.forRoot(options.surface !== void 0 ? {
2433
+ surface: options.surface
2434
+ } : {})
2389
2435
  ];
2390
2436
  }
2391
2437
  __name(agentDurable, "agentDurable");