@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.
@@ -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, readElicitationQuestions, validateElicitationAnswer } 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
  }
@@ -1186,6 +1224,7 @@ var AgentService = class {
1186
1224
  */
1187
1225
  async answer(actor, toolCallId2, answers2 = {}) {
1188
1226
  await this.assertOwnsToolCall(actor, toolCallId2);
1227
+ await this.assertAnswersFit(toolCallId2, answers2);
1189
1228
  const reply = {
1190
1229
  answers: answers2,
1191
1230
  answeredByRef: actor.id
@@ -1215,6 +1254,7 @@ var AgentService = class {
1215
1254
  * not the thread's own actor).
1216
1255
  */
1217
1256
  async signalToolCall(toolCallId2, reply) {
1257
+ await this.assertNotExpired(toolCallId2);
1218
1258
  const runId = await this.resolveRunForToolCall(toolCallId2);
1219
1259
  return this.runner.signal(runId, toolCallId2, reply);
1220
1260
  }
@@ -1453,6 +1493,68 @@ var AgentService = class {
1453
1493
  }
1454
1494
  }
1455
1495
  /**
1496
+ * Check a reply against the questions it answers before it is signalled: every submitted value a
1497
+ * question's own rules accept (a typed input's type, bounds and pattern; a pick from the offered
1498
+ * options), and every `required` question answered — by the reply, or by the defaults an omitted
1499
+ * question falls back to. A reply the loop would have to drop parts of is refused here instead, so
1500
+ * the person who typed it hears why. Needs the store to hand back the call's recorded questions;
1501
+ * without that seam the loop's own filtering is the only check.
1502
+ */
1503
+ async assertAnswersFit(toolCallId2, answers2) {
1504
+ if (this.store.toolCallInput === void 0) {
1505
+ return;
1506
+ }
1507
+ const questions = readElicitationQuestions(await this.store.toolCallInput(toolCallId2));
1508
+ for (const question of questions) {
1509
+ const submitted = Object.hasOwn(answers2, question.id) ? answers2[question.id] : void 0;
1510
+ const problem = validateElicitationAnswer(question, submitted ?? question.defaults ?? []);
1511
+ if (problem !== null) {
1512
+ throw new BadRequestException(`answers["${question.id}"] ${problem}`);
1513
+ }
1514
+ }
1515
+ }
1516
+ /**
1517
+ * Authorization seam for approve/reject. The call's recorded approver decides who may settle it:
1518
+ * the requester (the default, and every call recorded before approvers existed) is the thread's
1519
+ * own actor — the ownership check this always was; any other approver goes through the policy's
1520
+ * `canDecide`, which by default asks whether the actor holds that role.
1521
+ */
1522
+ async assertMayDecide(actor, toolCallId2) {
1523
+ const approval = await this.store.toolCallApproval?.(toolCallId2);
1524
+ const approver = approval?.approver ?? REQUESTER_APPROVER;
1525
+ if (approver === REQUESTER_APPROVER) {
1526
+ return this.assertOwnsToolCall(actor, toolCallId2);
1527
+ }
1528
+ const requesterRef = await this.store.ownerOfToolCall(toolCallId2);
1529
+ if (requesterRef === null) {
1530
+ throw new NotFoundException(`tool call ${toolCallId2} not found`);
1531
+ }
1532
+ const allowed = await mayDecideApproval(this.deps.forAgent().approvalPolicy, actor, {
1533
+ toolCallId: toolCallId2,
1534
+ approver,
1535
+ requesterRef
1536
+ });
1537
+ if (!allowed) {
1538
+ throw new ForbiddenException(`this approval is for ${approver}`);
1539
+ }
1540
+ }
1541
+ /**
1542
+ * Refuse a decision on a request that has already lapsed — recorded `expired`, or past its
1543
+ * `expiresAt` with the run's own timer about to fire. Signalling it anyway would race the timeout:
1544
+ * a durable runtime buffers a signal nobody is waiting on, and a late approval must not be what a
1545
+ * later replay of the run reads back.
1546
+ */
1547
+ async assertNotExpired(toolCallId2) {
1548
+ const approval = await this.store.toolCallApproval?.(toolCallId2);
1549
+ if (approval === null || approval === void 0) {
1550
+ return;
1551
+ }
1552
+ const lapsed = approval.status === "expired" || approval.status === "pending_approval" && approval.expiresAt !== null && Date.parse(approval.expiresAt) <= Date.now();
1553
+ if (lapsed) {
1554
+ throw new GoneException(`the approval request for tool call ${toolCallId2} has expired`);
1555
+ }
1556
+ }
1557
+ /**
1456
1558
  * Resolve the run awaiting `toolCallId`. Derived server-side from the tool call alone — the client
1457
1559
  * never knows or supplies a runId, and that run is the sub-agent's own child run when the pending
1458
1560
  * call belongs to a delegated agent. The store answers from the CALL'S OWN `runId` where it has
@@ -1530,6 +1632,12 @@ var AgentApprovalPortAdapter = class {
1530
1632
  approved: true,
1531
1633
  ...opts?.executedByRef !== void 0 ? {
1532
1634
  executedByRef: opts.executedByRef
1635
+ } : {},
1636
+ ...opts?.remember === true ? {
1637
+ remember: true
1638
+ } : {},
1639
+ ...opts?.decidedVia !== void 0 ? {
1640
+ decidedVia: opts.decidedVia
1533
1641
  } : {}
1534
1642
  });
1535
1643
  }
@@ -1541,6 +1649,9 @@ var AgentApprovalPortAdapter = class {
1541
1649
  } : {},
1542
1650
  ...opts?.reason !== void 0 ? {
1543
1651
  reason: opts.reason
1652
+ } : {},
1653
+ ...opts?.decidedVia !== void 0 ? {
1654
+ decidedVia: opts.decidedVia
1544
1655
  } : {}
1545
1656
  });
1546
1657
  }
@@ -2460,6 +2571,8 @@ var MAX_ANSWERED_QUESTIONS = 100;
2460
2571
  var MAX_VALUES_PER_QUESTION = 100;
2461
2572
  var MAX_ANSWER_VALUE_LENGTH = 4096;
2462
2573
  var MAX_REJECT_REASON_LENGTH = 4096;
2574
+ var MAX_VIA_LENGTH = 64;
2575
+ var DEFAULT_VIA = "web";
2463
2576
  function toolCallId(claimed) {
2464
2577
  if (typeof claimed !== "string" || claimed.length === 0) {
2465
2578
  throw new BadRequestException4("toolCallId must be a non-empty string");
@@ -2480,6 +2593,26 @@ function rejectReason(claimed) {
2480
2593
  return claimed;
2481
2594
  }
2482
2595
  __name(rejectReason, "rejectReason");
2596
+ function remember(claimed) {
2597
+ if (claimed === void 0) {
2598
+ return false;
2599
+ }
2600
+ if (typeof claimed !== "boolean") {
2601
+ throw new BadRequestException4("remember must be a boolean");
2602
+ }
2603
+ return claimed;
2604
+ }
2605
+ __name(remember, "remember");
2606
+ function via(claimed) {
2607
+ if (claimed === void 0) {
2608
+ return DEFAULT_VIA;
2609
+ }
2610
+ if (typeof claimed !== "string" || claimed.length === 0 || claimed.length > MAX_VIA_LENGTH) {
2611
+ throw new BadRequestException4(`via must be a string of 1-${MAX_VIA_LENGTH} characters`);
2612
+ }
2613
+ return claimed;
2614
+ }
2615
+ __name(via, "via");
2483
2616
  function answers(claimed) {
2484
2617
  if (claimed === void 0) {
2485
2618
  return void 0;
@@ -2524,14 +2657,19 @@ var ToolCallController = class {
2524
2657
  }
2525
2658
  async approve(req, body) {
2526
2659
  const actor = await this.actorResolver.resolve(req);
2527
- await this.agent.approve(actor, toolCallId(body.toolCallId));
2660
+ await this.agent.approve(actor, toolCallId(body.toolCallId), {
2661
+ remember: remember(body.remember),
2662
+ via: via(body.via)
2663
+ });
2528
2664
  return {
2529
2665
  ok: true
2530
2666
  };
2531
2667
  }
2532
2668
  async reject(req, body) {
2533
2669
  const actor = await this.actorResolver.resolve(req);
2534
- await this.agent.reject(actor, toolCallId(body.toolCallId), rejectReason(body.reason));
2670
+ await this.agent.reject(actor, toolCallId(body.toolCallId), rejectReason(body.reason), {
2671
+ via: via(body.via)
2672
+ });
2535
2673
  return {
2536
2674
  ok: true
2537
2675
  };
@@ -2597,7 +2735,7 @@ _ts_decorate15([
2597
2735
  _ts_metadata14("design:type", Function),
2598
2736
  _ts_metadata14("design:paramtypes", [
2599
2737
  typeof Request === "undefined" ? Object : Request,
2600
- typeof ApproveBody === "undefined" ? Object : ApproveBody
2738
+ Object
2601
2739
  ]),
2602
2740
  _ts_metadata14("design:returntype", Promise)
2603
2741
  ], ToolCallController.prototype, "skip", null);
@@ -3342,6 +3480,36 @@ var InlineAgentRunner = class _InlineAgentRunner {
3342
3480
  * shape it is delivering — the loop asked for one and only ever gets what the caller sent. A
3343
3481
  * cancel settles the wait too, by rejecting it.
3344
3482
  */
3483
+ /**
3484
+ * {@link park} for an approval, bounded by the policy's time to live when it has one: the timer
3485
+ * settles the wait as `expired` — the same Decision the durable runner builds from its signal
3486
+ * timeout — and a decision that arrives first disarms it.
3487
+ */
3488
+ parkApproval(runId, toolCallId2, timeoutMs) {
3489
+ const waiting = this.park(runId, toolCallId2);
3490
+ if (timeoutMs === void 0) {
3491
+ return waiting;
3492
+ }
3493
+ const key = `${runId}:${toolCallId2}`;
3494
+ let timer;
3495
+ const lapse = new Promise((resolve) => {
3496
+ timer = setTimeout(() => {
3497
+ const entry = this.pending.get(key);
3498
+ if (entry !== void 0) {
3499
+ this.pending.delete(key);
3500
+ resolve({
3501
+ approved: false,
3502
+ expired: true
3503
+ });
3504
+ }
3505
+ }, timeoutMs);
3506
+ timer.unref?.();
3507
+ });
3508
+ return Promise.race([
3509
+ waiting,
3510
+ lapse
3511
+ ]).finally(() => clearTimeout(timer));
3512
+ }
3345
3513
  park(runId, toolCallId2) {
3346
3514
  return new Promise((resolve, reject) => {
3347
3515
  this.pending.set(`${runId}:${toolCallId2}`, {
@@ -3355,7 +3523,7 @@ var InlineAgentRunner = class _InlineAgentRunner {
3355
3523
  return {
3356
3524
  runId,
3357
3525
  openSink: /* @__PURE__ */ __name(() => deps.sink.open(runId), "openSink"),
3358
- awaitApproval: /* @__PURE__ */ __name((call) => this.park(runId, call.id), "awaitApproval"),
3526
+ awaitApproval: /* @__PURE__ */ __name((call, _ctx, opts) => this.parkApproval(runId, call.id, opts?.timeoutMs), "awaitApproval"),
3359
3527
  awaitAnswers: /* @__PURE__ */ __name((request) => this.park(runId, request.id), "awaitAnswers"),
3360
3528
  step: /* @__PURE__ */ __name((_name, fn) => fn(), "step"),
3361
3529
  parallel: settleAll2,
@@ -3406,7 +3574,7 @@ var InlineAgentRunner = class _InlineAgentRunner {
3406
3574
  const hooks = {
3407
3575
  runId,
3408
3576
  openSink: /* @__PURE__ */ __name(() => deps.sink.open(runId), "openSink"),
3409
- awaitApproval: /* @__PURE__ */ __name((call) => this.park(runId, call.id), "awaitApproval"),
3577
+ awaitApproval: /* @__PURE__ */ __name((call, _ctx, opts) => this.parkApproval(runId, call.id, opts?.timeoutMs), "awaitApproval"),
3410
3578
  awaitAnswers: /* @__PURE__ */ __name((request) => this.park(runId, request.id), "awaitAnswers"),
3411
3579
  step: /* @__PURE__ */ __name((_name, fn) => fn(), "step"),
3412
3580
  parallel: settleAll2,
@@ -3510,7 +3678,7 @@ var InlineAgentRunner = class _InlineAgentRunner {
3510
3678
  runId,
3511
3679
  // Forward into the top-level stream; the top-level run owns end/fail on that shared sink.
3512
3680
  openSink: /* @__PURE__ */ __name(async () => childSinkWriter(await deps.sink.open(sinkRunId)), "openSink"),
3513
- awaitApproval: /* @__PURE__ */ __name((call) => this.park(runId, call.id), "awaitApproval"),
3681
+ awaitApproval: /* @__PURE__ */ __name((call, _ctx, opts) => this.parkApproval(runId, call.id, opts?.timeoutMs), "awaitApproval"),
3514
3682
  awaitAnswers: /* @__PURE__ */ __name((request) => this.park(runId, request.id), "awaitAnswers"),
3515
3683
  step: /* @__PURE__ */ __name((_name, fn) => fn(), "step"),
3516
3684
  parallel: settleAll2,