@dudousxd/nestjs-agent 1.2.0 → 1.3.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.
@@ -1,6 +1,6 @@
1
1
  import { AgentStore, AgentRunInput, AgentLoopResult, LlmStepEnvelope, BufferedModelTurnResult, ToolStepEnvelope, AgentRunner, TokenStreamSink, HumanReply } from '@dudousxd/nestjs-agent-core';
2
2
  import { RunGateway, WorkflowCtx } from '@dudousxd/nestjs-durable-core';
3
- import { A as AgentDepsFactory, a as AgentSurface, b as AgentModuleOptions } from '../agent-deps.factory-OjrLyAZo.cjs';
3
+ import { A as AgentDepsFactory, a as AgentSurface, b as AgentModuleOptions } from '../agent-deps.factory-CUyVPsa1.cjs';
4
4
  import { WorkflowService } from '@dudousxd/nestjs-durable';
5
5
  import { DynamicModule } from '@nestjs/common';
6
6
  import '@standard-schema/spec';
@@ -1,6 +1,6 @@
1
1
  import { AgentStore, AgentRunInput, AgentLoopResult, LlmStepEnvelope, BufferedModelTurnResult, ToolStepEnvelope, AgentRunner, TokenStreamSink, HumanReply } from '@dudousxd/nestjs-agent-core';
2
2
  import { RunGateway, WorkflowCtx } from '@dudousxd/nestjs-durable-core';
3
- import { A as AgentDepsFactory, a as AgentSurface, b as AgentModuleOptions } from '../agent-deps.factory-OjrLyAZo.js';
3
+ import { A as AgentDepsFactory, a as AgentSurface, b as AgentModuleOptions } from '../agent-deps.factory-CUyVPsa1.js';
4
4
  import { WorkflowService } from '@dudousxd/nestjs-durable';
5
5
  import { DynamicModule } from '@nestjs/common';
6
6
  import '@standard-schema/spec';
@@ -276,7 +276,21 @@ var AgentRunWorkflow = class _AgentRunWorkflow {
276
276
  runId: ctx.runId,
277
277
  // A child forwards into the top-level sink but must not end/fail it (the top-level run owns it).
278
278
  openSink: /* @__PURE__ */ __name(async () => input.sinkRunId !== void 0 ? childSinkWriter(await deps.sink.open(sinkRunId)) : deps.sink.open(ctx.runId), "openSink"),
279
- awaitApproval: /* @__PURE__ */ __name((call) => ctx.waitForSignal(`tool:${ctx.runId}:${call.id}`), "awaitApproval"),
279
+ // A policy's time to live becomes the signal wait's own timeout: the runtime journals the
280
+ // deadline on the first call (a position of its own, reached only for a call the journal says
281
+ // has a ttl) and wakes the run when it passes. The lapse comes back as a Decision rather than a
282
+ // throw, so the loop settles the call `expired` on its ordinary rejection checkpoint.
283
+ awaitApproval: /* @__PURE__ */ __name((call, _toolCtx, opts) => opts?.timeoutMs === void 0 ? ctx.waitForSignal(`tool:${ctx.runId}:${call.id}`) : ctx.waitForSignal(`tool:${ctx.runId}:${call.id}`, {
284
+ timeoutMs: opts.timeoutMs
285
+ }).catch((error) => {
286
+ if (isSignalTimeout(error)) {
287
+ return {
288
+ approved: false,
289
+ expired: true
290
+ };
291
+ }
292
+ throw error;
293
+ }), "awaitApproval"),
280
294
  // The same wait, on the same signal key, carrying a typed answer instead of a yes/no — so a
281
295
  // question set and an approval reach a parked run through one path, and the HTTP surface that
282
296
  // settles either one is the same `POST /agent/tool-call/*` family.
@@ -450,6 +464,10 @@ AgentRunWorkflow = _ts_decorate2([
450
464
  typeof RunGateway === "undefined" ? Object : RunGateway
451
465
  ])
452
466
  ], AgentRunWorkflow);
467
+ function isSignalTimeout(error) {
468
+ return error instanceof Error && error.name === "SignalTimeoutError";
469
+ }
470
+ __name(isSignalTimeout, "isSignalTimeout");
453
471
 
454
472
  // src/durable/durable-agent-runner.ts
455
473
  import { randomUUID } from "crypto";
@@ -880,6 +898,9 @@ var AgentDepsFactory2 = class {
880
898
  store: this.store,
881
899
  sink: this.sink,
882
900
  rolesPolicy: this.rolesPolicy,
901
+ ...this.options.approvalPolicy !== void 0 ? {
902
+ approvalPolicy: this.options.approvalPolicy
903
+ } : {},
883
904
  registry: this.registry,
884
905
  promptContributors: this.promptContributors,
885
906
  systemPrompt: definition?.systemPrompt ?? "You are a helpful assistant.",
@@ -1027,8 +1048,8 @@ AgentDepsFactory2 = _ts_decorate5([
1027
1048
  ], AgentDepsFactory2);
1028
1049
 
1029
1050
  // src/agent.service.ts
1030
- import { AGENT_ATTACHMENT_STAGING, AGENT_DEPS_FACTORY as AGENT_DEPS_FACTORY3, AGENT_QUOTA_STORE as AGENT_QUOTA_STORE2, AGENT_RUNNER, AGENT_STORE as AGENT_STORE4 } from "@dudousxd/nestjs-agent-core";
1031
- import { BadRequestException, ForbiddenException, Inject as Inject5, Injectable as Injectable5, NotFoundException, NotImplementedException, Optional as Optional3 } from "@nestjs/common";
1051
+ import { AGENT_ATTACHMENT_STAGING, AGENT_DEPS_FACTORY as AGENT_DEPS_FACTORY3, AGENT_QUOTA_STORE as AGENT_QUOTA_STORE2, AGENT_RUNNER, AGENT_STORE as AGENT_STORE4, REQUESTER_APPROVER, mayDecideApproval } from "@dudousxd/nestjs-agent-core";
1052
+ import { BadRequestException, ForbiddenException, GoneException, Inject as Inject5, Injectable as Injectable5, NotFoundException, NotImplementedException, Optional as Optional3 } from "@nestjs/common";
1032
1053
  function _ts_decorate6(decorators, target, key, desc) {
1033
1054
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1034
1055
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
@@ -1163,18 +1184,35 @@ var AgentService = class {
1163
1184
  await this.assertOwnsActiveStream(actor, runId);
1164
1185
  return this.subscribe(runId);
1165
1186
  }
1166
- async approve(actor, toolCallId2) {
1167
- await this.assertOwnsToolCall(actor, toolCallId2);
1187
+ /**
1188
+ * Approve a parked action call as `actor`. Who may is the call's recorded approver's business
1189
+ * (see {@link assertMayDecide}); `remember` approves later calls of the same tool in the same
1190
+ * thread, and `via` names the surface the decision came through (`'web'`, `'slack'`, …) — both
1191
+ * persisted with the call.
1192
+ */
1193
+ async approve(actor, toolCallId2, opts = {}) {
1194
+ await this.assertMayDecide(actor, toolCallId2);
1168
1195
  return this.signalToolCall(toolCallId2, {
1169
- approved: true
1196
+ approved: true,
1197
+ executedByRef: actor.id,
1198
+ ...opts.remember === true ? {
1199
+ remember: true
1200
+ } : {},
1201
+ ...opts.via !== void 0 ? {
1202
+ decidedVia: opts.via
1203
+ } : {}
1170
1204
  });
1171
1205
  }
1172
- async reject(actor, toolCallId2, reason) {
1173
- await this.assertOwnsToolCall(actor, toolCallId2);
1206
+ async reject(actor, toolCallId2, reason, opts = {}) {
1207
+ await this.assertMayDecide(actor, toolCallId2);
1174
1208
  return this.signalToolCall(toolCallId2, {
1175
1209
  approved: false,
1210
+ executedByRef: actor.id,
1176
1211
  ...reason !== void 0 ? {
1177
1212
  reason
1213
+ } : {},
1214
+ ...opts.via !== void 0 ? {
1215
+ decidedVia: opts.via
1178
1216
  } : {}
1179
1217
  });
1180
1218
  }
@@ -1215,6 +1253,7 @@ var AgentService = class {
1215
1253
  * not the thread's own actor).
1216
1254
  */
1217
1255
  async signalToolCall(toolCallId2, reply) {
1256
+ await this.assertNotExpired(toolCallId2);
1218
1257
  const runId = await this.resolveRunForToolCall(toolCallId2);
1219
1258
  return this.runner.signal(runId, toolCallId2, reply);
1220
1259
  }
@@ -1453,6 +1492,47 @@ var AgentService = class {
1453
1492
  }
1454
1493
  }
1455
1494
  /**
1495
+ * Authorization seam for approve/reject. The call's recorded approver decides who may settle it:
1496
+ * the requester (the default, and every call recorded before approvers existed) is the thread's
1497
+ * own actor — the ownership check this always was; any other approver goes through the policy's
1498
+ * `canDecide`, which by default asks whether the actor holds that role.
1499
+ */
1500
+ async assertMayDecide(actor, toolCallId2) {
1501
+ const approval = await this.store.toolCallApproval?.(toolCallId2);
1502
+ const approver = approval?.approver ?? REQUESTER_APPROVER;
1503
+ if (approver === REQUESTER_APPROVER) {
1504
+ return this.assertOwnsToolCall(actor, toolCallId2);
1505
+ }
1506
+ const requesterRef = await this.store.ownerOfToolCall(toolCallId2);
1507
+ if (requesterRef === null) {
1508
+ throw new NotFoundException(`tool call ${toolCallId2} not found`);
1509
+ }
1510
+ const allowed = await mayDecideApproval(this.deps.forAgent().approvalPolicy, actor, {
1511
+ toolCallId: toolCallId2,
1512
+ approver,
1513
+ requesterRef
1514
+ });
1515
+ if (!allowed) {
1516
+ throw new ForbiddenException(`this approval is for ${approver}`);
1517
+ }
1518
+ }
1519
+ /**
1520
+ * Refuse a decision on a request that has already lapsed — recorded `expired`, or past its
1521
+ * `expiresAt` with the run's own timer about to fire. Signalling it anyway would race the timeout:
1522
+ * a durable runtime buffers a signal nobody is waiting on, and a late approval must not be what a
1523
+ * later replay of the run reads back.
1524
+ */
1525
+ async assertNotExpired(toolCallId2) {
1526
+ const approval = await this.store.toolCallApproval?.(toolCallId2);
1527
+ if (approval === null || approval === void 0) {
1528
+ return;
1529
+ }
1530
+ const lapsed = approval.status === "expired" || approval.status === "pending_approval" && approval.expiresAt !== null && Date.parse(approval.expiresAt) <= Date.now();
1531
+ if (lapsed) {
1532
+ throw new GoneException(`the approval request for tool call ${toolCallId2} has expired`);
1533
+ }
1534
+ }
1535
+ /**
1456
1536
  * Resolve the run awaiting `toolCallId`. Derived server-side from the tool call alone — the client
1457
1537
  * never knows or supplies a runId, and that run is the sub-agent's own child run when the pending
1458
1538
  * call belongs to a delegated agent. The store answers from the CALL'S OWN `runId` where it has
@@ -1530,6 +1610,12 @@ var AgentApprovalPortAdapter = class {
1530
1610
  approved: true,
1531
1611
  ...opts?.executedByRef !== void 0 ? {
1532
1612
  executedByRef: opts.executedByRef
1613
+ } : {},
1614
+ ...opts?.remember === true ? {
1615
+ remember: true
1616
+ } : {},
1617
+ ...opts?.decidedVia !== void 0 ? {
1618
+ decidedVia: opts.decidedVia
1533
1619
  } : {}
1534
1620
  });
1535
1621
  }
@@ -1541,6 +1627,9 @@ var AgentApprovalPortAdapter = class {
1541
1627
  } : {},
1542
1628
  ...opts?.reason !== void 0 ? {
1543
1629
  reason: opts.reason
1630
+ } : {},
1631
+ ...opts?.decidedVia !== void 0 ? {
1632
+ decidedVia: opts.decidedVia
1544
1633
  } : {}
1545
1634
  });
1546
1635
  }
@@ -2460,6 +2549,8 @@ var MAX_ANSWERED_QUESTIONS = 100;
2460
2549
  var MAX_VALUES_PER_QUESTION = 100;
2461
2550
  var MAX_ANSWER_VALUE_LENGTH = 4096;
2462
2551
  var MAX_REJECT_REASON_LENGTH = 4096;
2552
+ var MAX_VIA_LENGTH = 64;
2553
+ var DEFAULT_VIA = "web";
2463
2554
  function toolCallId(claimed) {
2464
2555
  if (typeof claimed !== "string" || claimed.length === 0) {
2465
2556
  throw new BadRequestException4("toolCallId must be a non-empty string");
@@ -2480,6 +2571,26 @@ function rejectReason(claimed) {
2480
2571
  return claimed;
2481
2572
  }
2482
2573
  __name(rejectReason, "rejectReason");
2574
+ function remember(claimed) {
2575
+ if (claimed === void 0) {
2576
+ return false;
2577
+ }
2578
+ if (typeof claimed !== "boolean") {
2579
+ throw new BadRequestException4("remember must be a boolean");
2580
+ }
2581
+ return claimed;
2582
+ }
2583
+ __name(remember, "remember");
2584
+ function via(claimed) {
2585
+ if (claimed === void 0) {
2586
+ return DEFAULT_VIA;
2587
+ }
2588
+ if (typeof claimed !== "string" || claimed.length === 0 || claimed.length > MAX_VIA_LENGTH) {
2589
+ throw new BadRequestException4(`via must be a string of 1-${MAX_VIA_LENGTH} characters`);
2590
+ }
2591
+ return claimed;
2592
+ }
2593
+ __name(via, "via");
2483
2594
  function answers(claimed) {
2484
2595
  if (claimed === void 0) {
2485
2596
  return void 0;
@@ -2524,14 +2635,19 @@ var ToolCallController = class {
2524
2635
  }
2525
2636
  async approve(req, body) {
2526
2637
  const actor = await this.actorResolver.resolve(req);
2527
- await this.agent.approve(actor, toolCallId(body.toolCallId));
2638
+ await this.agent.approve(actor, toolCallId(body.toolCallId), {
2639
+ remember: remember(body.remember),
2640
+ via: via(body.via)
2641
+ });
2528
2642
  return {
2529
2643
  ok: true
2530
2644
  };
2531
2645
  }
2532
2646
  async reject(req, body) {
2533
2647
  const actor = await this.actorResolver.resolve(req);
2534
- await this.agent.reject(actor, toolCallId(body.toolCallId), rejectReason(body.reason));
2648
+ await this.agent.reject(actor, toolCallId(body.toolCallId), rejectReason(body.reason), {
2649
+ via: via(body.via)
2650
+ });
2535
2651
  return {
2536
2652
  ok: true
2537
2653
  };
@@ -2597,7 +2713,7 @@ _ts_decorate15([
2597
2713
  _ts_metadata14("design:type", Function),
2598
2714
  _ts_metadata14("design:paramtypes", [
2599
2715
  typeof Request === "undefined" ? Object : Request,
2600
- typeof ApproveBody === "undefined" ? Object : ApproveBody
2716
+ Object
2601
2717
  ]),
2602
2718
  _ts_metadata14("design:returntype", Promise)
2603
2719
  ], ToolCallController.prototype, "skip", null);
@@ -3342,6 +3458,36 @@ var InlineAgentRunner = class _InlineAgentRunner {
3342
3458
  * shape it is delivering — the loop asked for one and only ever gets what the caller sent. A
3343
3459
  * cancel settles the wait too, by rejecting it.
3344
3460
  */
3461
+ /**
3462
+ * {@link park} for an approval, bounded by the policy's time to live when it has one: the timer
3463
+ * settles the wait as `expired` — the same Decision the durable runner builds from its signal
3464
+ * timeout — and a decision that arrives first disarms it.
3465
+ */
3466
+ parkApproval(runId, toolCallId2, timeoutMs) {
3467
+ const waiting = this.park(runId, toolCallId2);
3468
+ if (timeoutMs === void 0) {
3469
+ return waiting;
3470
+ }
3471
+ const key = `${runId}:${toolCallId2}`;
3472
+ let timer;
3473
+ const lapse = new Promise((resolve) => {
3474
+ timer = setTimeout(() => {
3475
+ const entry = this.pending.get(key);
3476
+ if (entry !== void 0) {
3477
+ this.pending.delete(key);
3478
+ resolve({
3479
+ approved: false,
3480
+ expired: true
3481
+ });
3482
+ }
3483
+ }, timeoutMs);
3484
+ timer.unref?.();
3485
+ });
3486
+ return Promise.race([
3487
+ waiting,
3488
+ lapse
3489
+ ]).finally(() => clearTimeout(timer));
3490
+ }
3345
3491
  park(runId, toolCallId2) {
3346
3492
  return new Promise((resolve, reject) => {
3347
3493
  this.pending.set(`${runId}:${toolCallId2}`, {
@@ -3355,7 +3501,7 @@ var InlineAgentRunner = class _InlineAgentRunner {
3355
3501
  return {
3356
3502
  runId,
3357
3503
  openSink: /* @__PURE__ */ __name(() => deps.sink.open(runId), "openSink"),
3358
- awaitApproval: /* @__PURE__ */ __name((call) => this.park(runId, call.id), "awaitApproval"),
3504
+ awaitApproval: /* @__PURE__ */ __name((call, _ctx, opts) => this.parkApproval(runId, call.id, opts?.timeoutMs), "awaitApproval"),
3359
3505
  awaitAnswers: /* @__PURE__ */ __name((request) => this.park(runId, request.id), "awaitAnswers"),
3360
3506
  step: /* @__PURE__ */ __name((_name, fn) => fn(), "step"),
3361
3507
  parallel: settleAll2,
@@ -3406,7 +3552,7 @@ var InlineAgentRunner = class _InlineAgentRunner {
3406
3552
  const hooks = {
3407
3553
  runId,
3408
3554
  openSink: /* @__PURE__ */ __name(() => deps.sink.open(runId), "openSink"),
3409
- awaitApproval: /* @__PURE__ */ __name((call) => this.park(runId, call.id), "awaitApproval"),
3555
+ awaitApproval: /* @__PURE__ */ __name((call, _ctx, opts) => this.parkApproval(runId, call.id, opts?.timeoutMs), "awaitApproval"),
3410
3556
  awaitAnswers: /* @__PURE__ */ __name((request) => this.park(runId, request.id), "awaitAnswers"),
3411
3557
  step: /* @__PURE__ */ __name((_name, fn) => fn(), "step"),
3412
3558
  parallel: settleAll2,
@@ -3510,7 +3656,7 @@ var InlineAgentRunner = class _InlineAgentRunner {
3510
3656
  runId,
3511
3657
  // Forward into the top-level stream; the top-level run owns end/fail on that shared sink.
3512
3658
  openSink: /* @__PURE__ */ __name(async () => childSinkWriter(await deps.sink.open(sinkRunId)), "openSink"),
3513
- awaitApproval: /* @__PURE__ */ __name((call) => this.park(runId, call.id), "awaitApproval"),
3659
+ awaitApproval: /* @__PURE__ */ __name((call, _ctx, opts) => this.parkApproval(runId, call.id, opts?.timeoutMs), "awaitApproval"),
3514
3660
  awaitAnswers: /* @__PURE__ */ __name((request) => this.park(runId, request.id), "awaitAnswers"),
3515
3661
  step: /* @__PURE__ */ __name((_name, fn) => fn(), "step"),
3516
3662
  parallel: settleAll2,