@dudousxd/nestjs-agent 1.2.0 → 1.4.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
  }
@@ -508,6 +528,7 @@ var AgentService = class {
508
528
  */
509
529
  async answer(actor, toolCallId2, answers2 = {}) {
510
530
  await this.assertOwnsToolCall(actor, toolCallId2);
531
+ await this.assertAnswersFit(toolCallId2, answers2);
511
532
  const reply = {
512
533
  answers: answers2,
513
534
  answeredByRef: actor.id
@@ -537,6 +558,7 @@ var AgentService = class {
537
558
  * not the thread's own actor).
538
559
  */
539
560
  async signalToolCall(toolCallId2, reply) {
561
+ await this.assertNotExpired(toolCallId2);
540
562
  const runId = await this.resolveRunForToolCall(toolCallId2);
541
563
  return this.runner.signal(runId, toolCallId2, reply);
542
564
  }
@@ -775,6 +797,68 @@ var AgentService = class {
775
797
  }
776
798
  }
777
799
  /**
800
+ * Check a reply against the questions it answers before it is signalled: every submitted value a
801
+ * question's own rules accept (a typed input's type, bounds and pattern; a pick from the offered
802
+ * options), and every `required` question answered — by the reply, or by the defaults an omitted
803
+ * question falls back to. A reply the loop would have to drop parts of is refused here instead, so
804
+ * the person who typed it hears why. Needs the store to hand back the call's recorded questions;
805
+ * without that seam the loop's own filtering is the only check.
806
+ */
807
+ async assertAnswersFit(toolCallId2, answers2) {
808
+ if (this.store.toolCallInput === void 0) {
809
+ return;
810
+ }
811
+ const questions = (0, import_nestjs_agent_core3.readElicitationQuestions)(await this.store.toolCallInput(toolCallId2));
812
+ for (const question of questions) {
813
+ const submitted = Object.hasOwn(answers2, question.id) ? answers2[question.id] : void 0;
814
+ const problem = (0, import_nestjs_agent_core3.validateElicitationAnswer)(question, submitted ?? question.defaults ?? []);
815
+ if (problem !== null) {
816
+ throw new import_common2.BadRequestException(`answers["${question.id}"] ${problem}`);
817
+ }
818
+ }
819
+ }
820
+ /**
821
+ * Authorization seam for approve/reject. The call's recorded approver decides who may settle it:
822
+ * the requester (the default, and every call recorded before approvers existed) is the thread's
823
+ * own actor — the ownership check this always was; any other approver goes through the policy's
824
+ * `canDecide`, which by default asks whether the actor holds that role.
825
+ */
826
+ async assertMayDecide(actor, toolCallId2) {
827
+ const approval = await this.store.toolCallApproval?.(toolCallId2);
828
+ const approver = approval?.approver ?? import_nestjs_agent_core3.REQUESTER_APPROVER;
829
+ if (approver === import_nestjs_agent_core3.REQUESTER_APPROVER) {
830
+ return this.assertOwnsToolCall(actor, toolCallId2);
831
+ }
832
+ const requesterRef = await this.store.ownerOfToolCall(toolCallId2);
833
+ if (requesterRef === null) {
834
+ throw new import_common2.NotFoundException(`tool call ${toolCallId2} not found`);
835
+ }
836
+ const allowed = await (0, import_nestjs_agent_core3.mayDecideApproval)(this.deps.forAgent().approvalPolicy, actor, {
837
+ toolCallId: toolCallId2,
838
+ approver,
839
+ requesterRef
840
+ });
841
+ if (!allowed) {
842
+ throw new import_common2.ForbiddenException(`this approval is for ${approver}`);
843
+ }
844
+ }
845
+ /**
846
+ * Refuse a decision on a request that has already lapsed — recorded `expired`, or past its
847
+ * `expiresAt` with the run's own timer about to fire. Signalling it anyway would race the timeout:
848
+ * a durable runtime buffers a signal nobody is waiting on, and a late approval must not be what a
849
+ * later replay of the run reads back.
850
+ */
851
+ async assertNotExpired(toolCallId2) {
852
+ const approval = await this.store.toolCallApproval?.(toolCallId2);
853
+ if (approval === null || approval === void 0) {
854
+ return;
855
+ }
856
+ const lapsed = approval.status === "expired" || approval.status === "pending_approval" && approval.expiresAt !== null && Date.parse(approval.expiresAt) <= Date.now();
857
+ if (lapsed) {
858
+ throw new import_common2.GoneException(`the approval request for tool call ${toolCallId2} has expired`);
859
+ }
860
+ }
861
+ /**
778
862
  * Resolve the run awaiting `toolCallId`. Derived server-side from the tool call alone — the client
779
863
  * never knows or supplies a runId, and that run is the sub-agent's own child run when the pending
780
864
  * call belongs to a delegated agent. The store answers from the CALL'S OWN `runId` where it has
@@ -852,6 +936,12 @@ var AgentApprovalPortAdapter = class {
852
936
  approved: true,
853
937
  ...opts?.executedByRef !== void 0 ? {
854
938
  executedByRef: opts.executedByRef
939
+ } : {},
940
+ ...opts?.remember === true ? {
941
+ remember: true
942
+ } : {},
943
+ ...opts?.decidedVia !== void 0 ? {
944
+ decidedVia: opts.decidedVia
855
945
  } : {}
856
946
  });
857
947
  }
@@ -863,6 +953,9 @@ var AgentApprovalPortAdapter = class {
863
953
  } : {},
864
954
  ...opts?.reason !== void 0 ? {
865
955
  reason: opts.reason
956
+ } : {},
957
+ ...opts?.decidedVia !== void 0 ? {
958
+ decidedVia: opts.decidedVia
866
959
  } : {}
867
960
  });
868
961
  }
@@ -1782,6 +1875,8 @@ var MAX_ANSWERED_QUESTIONS = 100;
1782
1875
  var MAX_VALUES_PER_QUESTION = 100;
1783
1876
  var MAX_ANSWER_VALUE_LENGTH = 4096;
1784
1877
  var MAX_REJECT_REASON_LENGTH = 4096;
1878
+ var MAX_VIA_LENGTH = 64;
1879
+ var DEFAULT_VIA = "web";
1785
1880
  function toolCallId(claimed) {
1786
1881
  if (typeof claimed !== "string" || claimed.length === 0) {
1787
1882
  throw new import_common11.BadRequestException("toolCallId must be a non-empty string");
@@ -1802,6 +1897,26 @@ function rejectReason(claimed) {
1802
1897
  return claimed;
1803
1898
  }
1804
1899
  __name(rejectReason, "rejectReason");
1900
+ function remember(claimed) {
1901
+ if (claimed === void 0) {
1902
+ return false;
1903
+ }
1904
+ if (typeof claimed !== "boolean") {
1905
+ throw new import_common11.BadRequestException("remember must be a boolean");
1906
+ }
1907
+ return claimed;
1908
+ }
1909
+ __name(remember, "remember");
1910
+ function via(claimed) {
1911
+ if (claimed === void 0) {
1912
+ return DEFAULT_VIA;
1913
+ }
1914
+ if (typeof claimed !== "string" || claimed.length === 0 || claimed.length > MAX_VIA_LENGTH) {
1915
+ throw new import_common11.BadRequestException(`via must be a string of 1-${MAX_VIA_LENGTH} characters`);
1916
+ }
1917
+ return claimed;
1918
+ }
1919
+ __name(via, "via");
1805
1920
  function answers(claimed) {
1806
1921
  if (claimed === void 0) {
1807
1922
  return void 0;
@@ -1846,14 +1961,19 @@ var ToolCallController = class {
1846
1961
  }
1847
1962
  async approve(req, body) {
1848
1963
  const actor = await this.actorResolver.resolve(req);
1849
- await this.agent.approve(actor, toolCallId(body.toolCallId));
1964
+ await this.agent.approve(actor, toolCallId(body.toolCallId), {
1965
+ remember: remember(body.remember),
1966
+ via: via(body.via)
1967
+ });
1850
1968
  return {
1851
1969
  ok: true
1852
1970
  };
1853
1971
  }
1854
1972
  async reject(req, body) {
1855
1973
  const actor = await this.actorResolver.resolve(req);
1856
- await this.agent.reject(actor, toolCallId(body.toolCallId), rejectReason(body.reason));
1974
+ await this.agent.reject(actor, toolCallId(body.toolCallId), rejectReason(body.reason), {
1975
+ via: via(body.via)
1976
+ });
1857
1977
  return {
1858
1978
  ok: true
1859
1979
  };
@@ -1919,7 +2039,7 @@ _ts_decorate11([
1919
2039
  _ts_metadata11("design:type", Function),
1920
2040
  _ts_metadata11("design:paramtypes", [
1921
2041
  typeof Request === "undefined" ? Object : Request,
1922
- typeof ApproveBody === "undefined" ? Object : ApproveBody
2042
+ Object
1923
2043
  ]),
1924
2044
  _ts_metadata11("design:returntype", Promise)
1925
2045
  ], ToolCallController.prototype, "skip", null);
@@ -2785,6 +2905,36 @@ var InlineAgentRunner = class _InlineAgentRunner {
2785
2905
  * shape it is delivering — the loop asked for one and only ever gets what the caller sent. A
2786
2906
  * cancel settles the wait too, by rejecting it.
2787
2907
  */
2908
+ /**
2909
+ * {@link park} for an approval, bounded by the policy's time to live when it has one: the timer
2910
+ * settles the wait as `expired` — the same Decision the durable runner builds from its signal
2911
+ * timeout — and a decision that arrives first disarms it.
2912
+ */
2913
+ parkApproval(runId, toolCallId2, timeoutMs) {
2914
+ const waiting = this.park(runId, toolCallId2);
2915
+ if (timeoutMs === void 0) {
2916
+ return waiting;
2917
+ }
2918
+ const key = `${runId}:${toolCallId2}`;
2919
+ let timer;
2920
+ const lapse = new Promise((resolve) => {
2921
+ timer = setTimeout(() => {
2922
+ const entry = this.pending.get(key);
2923
+ if (entry !== void 0) {
2924
+ this.pending.delete(key);
2925
+ resolve({
2926
+ approved: false,
2927
+ expired: true
2928
+ });
2929
+ }
2930
+ }, timeoutMs);
2931
+ timer.unref?.();
2932
+ });
2933
+ return Promise.race([
2934
+ waiting,
2935
+ lapse
2936
+ ]).finally(() => clearTimeout(timer));
2937
+ }
2788
2938
  park(runId, toolCallId2) {
2789
2939
  return new Promise((resolve, reject) => {
2790
2940
  this.pending.set(`${runId}:${toolCallId2}`, {
@@ -2798,7 +2948,7 @@ var InlineAgentRunner = class _InlineAgentRunner {
2798
2948
  return {
2799
2949
  runId,
2800
2950
  openSink: /* @__PURE__ */ __name(() => deps.sink.open(runId), "openSink"),
2801
- awaitApproval: /* @__PURE__ */ __name((call) => this.park(runId, call.id), "awaitApproval"),
2951
+ awaitApproval: /* @__PURE__ */ __name((call, _ctx, opts) => this.parkApproval(runId, call.id, opts?.timeoutMs), "awaitApproval"),
2802
2952
  awaitAnswers: /* @__PURE__ */ __name((request) => this.park(runId, request.id), "awaitAnswers"),
2803
2953
  step: /* @__PURE__ */ __name((_name, fn) => fn(), "step"),
2804
2954
  parallel: import_nestjs_agent_core18.settleAll,
@@ -2849,7 +2999,7 @@ var InlineAgentRunner = class _InlineAgentRunner {
2849
2999
  const hooks = {
2850
3000
  runId,
2851
3001
  openSink: /* @__PURE__ */ __name(() => deps.sink.open(runId), "openSink"),
2852
- awaitApproval: /* @__PURE__ */ __name((call) => this.park(runId, call.id), "awaitApproval"),
3002
+ awaitApproval: /* @__PURE__ */ __name((call, _ctx, opts) => this.parkApproval(runId, call.id, opts?.timeoutMs), "awaitApproval"),
2853
3003
  awaitAnswers: /* @__PURE__ */ __name((request) => this.park(runId, request.id), "awaitAnswers"),
2854
3004
  step: /* @__PURE__ */ __name((_name, fn) => fn(), "step"),
2855
3005
  parallel: import_nestjs_agent_core18.settleAll,
@@ -2953,7 +3103,7 @@ var InlineAgentRunner = class _InlineAgentRunner {
2953
3103
  runId,
2954
3104
  // Forward into the top-level stream; the top-level run owns end/fail on that shared sink.
2955
3105
  openSink: /* @__PURE__ */ __name(async () => childSinkWriter(await deps.sink.open(sinkRunId)), "openSink"),
2956
- awaitApproval: /* @__PURE__ */ __name((call) => this.park(runId, call.id), "awaitApproval"),
3106
+ awaitApproval: /* @__PURE__ */ __name((call, _ctx, opts) => this.parkApproval(runId, call.id, opts?.timeoutMs), "awaitApproval"),
2957
3107
  awaitAnswers: /* @__PURE__ */ __name((request) => this.park(runId, request.id), "awaitAnswers"),
2958
3108
  step: /* @__PURE__ */ __name((_name, fn) => fn(), "step"),
2959
3109
  parallel: import_nestjs_agent_core18.settleAll,