@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.
package/dist/index.cjs CHANGED
@@ -184,6 +184,9 @@ var AgentDepsFactory2 = class {
184
184
  store: this.store,
185
185
  sink: this.sink,
186
186
  rolesPolicy: this.rolesPolicy,
187
+ ...this.options.approvalPolicy !== void 0 ? {
188
+ approvalPolicy: this.options.approvalPolicy
189
+ } : {},
187
190
  registry: this.registry,
188
191
  promptContributors: this.promptContributors,
189
192
  systemPrompt: definition?.systemPrompt ?? "You are a helpful assistant.",
@@ -485,18 +488,35 @@ var AgentService = class {
485
488
  await this.assertOwnsActiveStream(actor, runId);
486
489
  return this.subscribe(runId);
487
490
  }
488
- async approve(actor, toolCallId2) {
489
- await this.assertOwnsToolCall(actor, toolCallId2);
491
+ /**
492
+ * Approve a parked action call as `actor`. Who may is the call's recorded approver's business
493
+ * (see {@link assertMayDecide}); `remember` approves later calls of the same tool in the same
494
+ * thread, and `via` names the surface the decision came through (`'web'`, `'slack'`, …) — both
495
+ * persisted with the call.
496
+ */
497
+ async approve(actor, toolCallId2, opts = {}) {
498
+ await this.assertMayDecide(actor, toolCallId2);
490
499
  return this.signalToolCall(toolCallId2, {
491
- approved: true
500
+ approved: true,
501
+ executedByRef: actor.id,
502
+ ...opts.remember === true ? {
503
+ remember: true
504
+ } : {},
505
+ ...opts.via !== void 0 ? {
506
+ decidedVia: opts.via
507
+ } : {}
492
508
  });
493
509
  }
494
- async reject(actor, toolCallId2, reason) {
495
- await this.assertOwnsToolCall(actor, toolCallId2);
510
+ async reject(actor, toolCallId2, reason, opts = {}) {
511
+ await this.assertMayDecide(actor, toolCallId2);
496
512
  return this.signalToolCall(toolCallId2, {
497
513
  approved: false,
514
+ executedByRef: actor.id,
498
515
  ...reason !== void 0 ? {
499
516
  reason
517
+ } : {},
518
+ ...opts.via !== void 0 ? {
519
+ decidedVia: opts.via
500
520
  } : {}
501
521
  });
502
522
  }
@@ -537,6 +557,7 @@ var AgentService = class {
537
557
  * not the thread's own actor).
538
558
  */
539
559
  async signalToolCall(toolCallId2, reply) {
560
+ await this.assertNotExpired(toolCallId2);
540
561
  const runId = await this.resolveRunForToolCall(toolCallId2);
541
562
  return this.runner.signal(runId, toolCallId2, reply);
542
563
  }
@@ -775,6 +796,47 @@ var AgentService = class {
775
796
  }
776
797
  }
777
798
  /**
799
+ * Authorization seam for approve/reject. The call's recorded approver decides who may settle it:
800
+ * the requester (the default, and every call recorded before approvers existed) is the thread's
801
+ * own actor — the ownership check this always was; any other approver goes through the policy's
802
+ * `canDecide`, which by default asks whether the actor holds that role.
803
+ */
804
+ async assertMayDecide(actor, toolCallId2) {
805
+ const approval = await this.store.toolCallApproval?.(toolCallId2);
806
+ const approver = approval?.approver ?? import_nestjs_agent_core3.REQUESTER_APPROVER;
807
+ if (approver === import_nestjs_agent_core3.REQUESTER_APPROVER) {
808
+ return this.assertOwnsToolCall(actor, toolCallId2);
809
+ }
810
+ const requesterRef = await this.store.ownerOfToolCall(toolCallId2);
811
+ if (requesterRef === null) {
812
+ throw new import_common2.NotFoundException(`tool call ${toolCallId2} not found`);
813
+ }
814
+ const allowed = await (0, import_nestjs_agent_core3.mayDecideApproval)(this.deps.forAgent().approvalPolicy, actor, {
815
+ toolCallId: toolCallId2,
816
+ approver,
817
+ requesterRef
818
+ });
819
+ if (!allowed) {
820
+ throw new import_common2.ForbiddenException(`this approval is for ${approver}`);
821
+ }
822
+ }
823
+ /**
824
+ * Refuse a decision on a request that has already lapsed — recorded `expired`, or past its
825
+ * `expiresAt` with the run's own timer about to fire. Signalling it anyway would race the timeout:
826
+ * a durable runtime buffers a signal nobody is waiting on, and a late approval must not be what a
827
+ * later replay of the run reads back.
828
+ */
829
+ async assertNotExpired(toolCallId2) {
830
+ const approval = await this.store.toolCallApproval?.(toolCallId2);
831
+ if (approval === null || approval === void 0) {
832
+ return;
833
+ }
834
+ const lapsed = approval.status === "expired" || approval.status === "pending_approval" && approval.expiresAt !== null && Date.parse(approval.expiresAt) <= Date.now();
835
+ if (lapsed) {
836
+ throw new import_common2.GoneException(`the approval request for tool call ${toolCallId2} has expired`);
837
+ }
838
+ }
839
+ /**
778
840
  * Resolve the run awaiting `toolCallId`. Derived server-side from the tool call alone — the client
779
841
  * never knows or supplies a runId, and that run is the sub-agent's own child run when the pending
780
842
  * call belongs to a delegated agent. The store answers from the CALL'S OWN `runId` where it has
@@ -852,6 +914,12 @@ var AgentApprovalPortAdapter = class {
852
914
  approved: true,
853
915
  ...opts?.executedByRef !== void 0 ? {
854
916
  executedByRef: opts.executedByRef
917
+ } : {},
918
+ ...opts?.remember === true ? {
919
+ remember: true
920
+ } : {},
921
+ ...opts?.decidedVia !== void 0 ? {
922
+ decidedVia: opts.decidedVia
855
923
  } : {}
856
924
  });
857
925
  }
@@ -863,6 +931,9 @@ var AgentApprovalPortAdapter = class {
863
931
  } : {},
864
932
  ...opts?.reason !== void 0 ? {
865
933
  reason: opts.reason
934
+ } : {},
935
+ ...opts?.decidedVia !== void 0 ? {
936
+ decidedVia: opts.decidedVia
866
937
  } : {}
867
938
  });
868
939
  }
@@ -1782,6 +1853,8 @@ var MAX_ANSWERED_QUESTIONS = 100;
1782
1853
  var MAX_VALUES_PER_QUESTION = 100;
1783
1854
  var MAX_ANSWER_VALUE_LENGTH = 4096;
1784
1855
  var MAX_REJECT_REASON_LENGTH = 4096;
1856
+ var MAX_VIA_LENGTH = 64;
1857
+ var DEFAULT_VIA = "web";
1785
1858
  function toolCallId(claimed) {
1786
1859
  if (typeof claimed !== "string" || claimed.length === 0) {
1787
1860
  throw new import_common11.BadRequestException("toolCallId must be a non-empty string");
@@ -1802,6 +1875,26 @@ function rejectReason(claimed) {
1802
1875
  return claimed;
1803
1876
  }
1804
1877
  __name(rejectReason, "rejectReason");
1878
+ function remember(claimed) {
1879
+ if (claimed === void 0) {
1880
+ return false;
1881
+ }
1882
+ if (typeof claimed !== "boolean") {
1883
+ throw new import_common11.BadRequestException("remember must be a boolean");
1884
+ }
1885
+ return claimed;
1886
+ }
1887
+ __name(remember, "remember");
1888
+ function via(claimed) {
1889
+ if (claimed === void 0) {
1890
+ return DEFAULT_VIA;
1891
+ }
1892
+ if (typeof claimed !== "string" || claimed.length === 0 || claimed.length > MAX_VIA_LENGTH) {
1893
+ throw new import_common11.BadRequestException(`via must be a string of 1-${MAX_VIA_LENGTH} characters`);
1894
+ }
1895
+ return claimed;
1896
+ }
1897
+ __name(via, "via");
1805
1898
  function answers(claimed) {
1806
1899
  if (claimed === void 0) {
1807
1900
  return void 0;
@@ -1846,14 +1939,19 @@ var ToolCallController = class {
1846
1939
  }
1847
1940
  async approve(req, body) {
1848
1941
  const actor = await this.actorResolver.resolve(req);
1849
- await this.agent.approve(actor, toolCallId(body.toolCallId));
1942
+ await this.agent.approve(actor, toolCallId(body.toolCallId), {
1943
+ remember: remember(body.remember),
1944
+ via: via(body.via)
1945
+ });
1850
1946
  return {
1851
1947
  ok: true
1852
1948
  };
1853
1949
  }
1854
1950
  async reject(req, body) {
1855
1951
  const actor = await this.actorResolver.resolve(req);
1856
- await this.agent.reject(actor, toolCallId(body.toolCallId), rejectReason(body.reason));
1952
+ await this.agent.reject(actor, toolCallId(body.toolCallId), rejectReason(body.reason), {
1953
+ via: via(body.via)
1954
+ });
1857
1955
  return {
1858
1956
  ok: true
1859
1957
  };
@@ -1919,7 +2017,7 @@ _ts_decorate11([
1919
2017
  _ts_metadata11("design:type", Function),
1920
2018
  _ts_metadata11("design:paramtypes", [
1921
2019
  typeof Request === "undefined" ? Object : Request,
1922
- typeof ApproveBody === "undefined" ? Object : ApproveBody
2020
+ Object
1923
2021
  ]),
1924
2022
  _ts_metadata11("design:returntype", Promise)
1925
2023
  ], ToolCallController.prototype, "skip", null);
@@ -2785,6 +2883,36 @@ var InlineAgentRunner = class _InlineAgentRunner {
2785
2883
  * shape it is delivering — the loop asked for one and only ever gets what the caller sent. A
2786
2884
  * cancel settles the wait too, by rejecting it.
2787
2885
  */
2886
+ /**
2887
+ * {@link park} for an approval, bounded by the policy's time to live when it has one: the timer
2888
+ * settles the wait as `expired` — the same Decision the durable runner builds from its signal
2889
+ * timeout — and a decision that arrives first disarms it.
2890
+ */
2891
+ parkApproval(runId, toolCallId2, timeoutMs) {
2892
+ const waiting = this.park(runId, toolCallId2);
2893
+ if (timeoutMs === void 0) {
2894
+ return waiting;
2895
+ }
2896
+ const key = `${runId}:${toolCallId2}`;
2897
+ let timer;
2898
+ const lapse = new Promise((resolve) => {
2899
+ timer = setTimeout(() => {
2900
+ const entry = this.pending.get(key);
2901
+ if (entry !== void 0) {
2902
+ this.pending.delete(key);
2903
+ resolve({
2904
+ approved: false,
2905
+ expired: true
2906
+ });
2907
+ }
2908
+ }, timeoutMs);
2909
+ timer.unref?.();
2910
+ });
2911
+ return Promise.race([
2912
+ waiting,
2913
+ lapse
2914
+ ]).finally(() => clearTimeout(timer));
2915
+ }
2788
2916
  park(runId, toolCallId2) {
2789
2917
  return new Promise((resolve, reject) => {
2790
2918
  this.pending.set(`${runId}:${toolCallId2}`, {
@@ -2798,7 +2926,7 @@ var InlineAgentRunner = class _InlineAgentRunner {
2798
2926
  return {
2799
2927
  runId,
2800
2928
  openSink: /* @__PURE__ */ __name(() => deps.sink.open(runId), "openSink"),
2801
- awaitApproval: /* @__PURE__ */ __name((call) => this.park(runId, call.id), "awaitApproval"),
2929
+ awaitApproval: /* @__PURE__ */ __name((call, _ctx, opts) => this.parkApproval(runId, call.id, opts?.timeoutMs), "awaitApproval"),
2802
2930
  awaitAnswers: /* @__PURE__ */ __name((request) => this.park(runId, request.id), "awaitAnswers"),
2803
2931
  step: /* @__PURE__ */ __name((_name, fn) => fn(), "step"),
2804
2932
  parallel: import_nestjs_agent_core18.settleAll,
@@ -2849,7 +2977,7 @@ var InlineAgentRunner = class _InlineAgentRunner {
2849
2977
  const hooks = {
2850
2978
  runId,
2851
2979
  openSink: /* @__PURE__ */ __name(() => deps.sink.open(runId), "openSink"),
2852
- awaitApproval: /* @__PURE__ */ __name((call) => this.park(runId, call.id), "awaitApproval"),
2980
+ awaitApproval: /* @__PURE__ */ __name((call, _ctx, opts) => this.parkApproval(runId, call.id, opts?.timeoutMs), "awaitApproval"),
2853
2981
  awaitAnswers: /* @__PURE__ */ __name((request) => this.park(runId, request.id), "awaitAnswers"),
2854
2982
  step: /* @__PURE__ */ __name((_name, fn) => fn(), "step"),
2855
2983
  parallel: import_nestjs_agent_core18.settleAll,
@@ -2953,7 +3081,7 @@ var InlineAgentRunner = class _InlineAgentRunner {
2953
3081
  runId,
2954
3082
  // Forward into the top-level stream; the top-level run owns end/fail on that shared sink.
2955
3083
  openSink: /* @__PURE__ */ __name(async () => childSinkWriter(await deps.sink.open(sinkRunId)), "openSink"),
2956
- awaitApproval: /* @__PURE__ */ __name((call) => this.park(runId, call.id), "awaitApproval"),
3084
+ awaitApproval: /* @__PURE__ */ __name((call, _ctx, opts) => this.parkApproval(runId, call.id, opts?.timeoutMs), "awaitApproval"),
2957
3085
  awaitAnswers: /* @__PURE__ */ __name((request) => this.park(runId, request.id), "awaitAnswers"),
2958
3086
  step: /* @__PURE__ */ __name((_name, fn) => fn(), "step"),
2959
3087
  parallel: import_nestjs_agent_core18.settleAll,