@dudousxd/nestjs-agent 1.21.0 → 1.22.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.js CHANGED
@@ -17,6 +17,7 @@ import { defineCatalog, genuiTools } from "@dudousxd/nestjs-agent-core/genui";
17
17
  import { Inject, Module } from "@nestjs/common";
18
18
 
19
19
  // src/functional-tool.ts
20
+ import { createFunctionalTool } from "@dudousxd/nestjs-agent-core";
20
21
  var AGENT_TOOL_BRAND = /* @__PURE__ */ Symbol.for("@dudousxd/nestjs-agent:functional-tool");
21
22
  function brand(tool) {
22
23
  return {
@@ -53,6 +54,7 @@ function provideAgentTools(factory, inject) {
53
54
  };
54
55
  }
55
56
  __name(provideAgentTools, "provideAgentTools");
57
+ var defineTool = createFunctionalTool;
56
58
 
57
59
  // src/genui/agent-genui.module.ts
58
60
  function _ts_decorate(decorators, target, key, desc) {
@@ -589,7 +591,7 @@ function childSinkWriter(writer) {
589
591
  __name(childSinkWriter, "childSinkWriter");
590
592
 
591
593
  // src/proposals/action-proposal.service.ts
592
- import { AGENT_OPTIONS as AGENT_OPTIONS2, AGENT_STORE as AGENT_STORE2, mayDecideApproval, parseTextActionProposalCommand, resolveTextActionProposalDecision } from "@dudousxd/nestjs-agent-core";
594
+ import { AGENT_OPTIONS as AGENT_OPTIONS2, AGENT_STORE as AGENT_STORE2, DEFAULT_TEXT_ACTION_PROPOSAL_REPLIES, DEFAULT_TEXT_ACTION_PROPOSAL_VOCABULARY, mayDecideApproval, parseTextActionProposalCommand, resolveTextActionProposalDecision, textActionProposalReply, toActionProposalMutationView, toActionProposalView } from "@dudousxd/nestjs-agent-core";
593
595
  import { ForbiddenException, Inject as Inject3, Injectable as Injectable2, NotFoundException, NotImplementedException } from "@nestjs/common";
594
596
  function _ts_decorate3(decorators, target, key, desc) {
595
597
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -626,7 +628,19 @@ var ActionProposalService = class {
626
628
  constructor(store, options) {
627
629
  this.store = store;
628
630
  this.options = options;
631
+ const text = options.actionProposalText;
632
+ this.vocabulary = {
633
+ ...DEFAULT_TEXT_ACTION_PROPOSAL_VOCABULARY,
634
+ ...text?.vocabulary
635
+ };
636
+ this.replies = {
637
+ ...DEFAULT_TEXT_ACTION_PROPOSAL_REPLIES,
638
+ ...text?.replies
639
+ };
629
640
  }
641
+ /** The words a text decision is made of, and what the agent answers — English unless configured. */
642
+ vocabulary;
643
+ replies;
630
644
  capability() {
631
645
  const store = this.store;
632
646
  if (typeof store.getThreadActionProposalScope !== "function" || typeof store.listActionProposals !== "function") throw new NotImplementedException("Action proposals are unavailable");
@@ -655,7 +669,8 @@ var ActionProposalService = class {
655
669
  for (const row of rows) if (scope.actorRef === actor.id || await this.mayDecide(actor, row)) visible.push(row);
656
670
  const last = rows.at(-1);
657
671
  return {
658
- items: visible,
672
+ // The public view: a row carries the worker's lease token and the tool's idempotency key.
673
+ items: visible.map(toActionProposalView),
659
674
  ...rows.length === 1e3 && last ? {
660
675
  next: {
661
676
  createdAt: last.createdAt,
@@ -672,7 +687,7 @@ var ActionProposalService = class {
672
687
  const proposal = await this.capability().getActionProposal(scope, proposalId);
673
688
  if (!proposal) throw new NotFoundException("Proposal not found");
674
689
  if (!await this.mayDecide(actor, proposal)) throw new ForbiddenException("May not decide this proposal");
675
- return this.capability().decideActionProposal(scope, proposalId, {
690
+ const result = await this.capability().decideActionProposal(scope, proposalId, {
676
691
  decision: command.decision,
677
692
  actorRef: actor.id,
678
693
  via: command.via ?? "web",
@@ -683,9 +698,10 @@ var ActionProposalService = class {
683
698
  reason: command.reason
684
699
  } : {}
685
700
  });
701
+ return toActionProposalMutationView(result);
686
702
  }
687
703
  async handleTextDecision(threadId, actor, text) {
688
- const command = parseTextActionProposalCommand(text);
704
+ const command = parseTextActionProposalCommand(text, this.vocabulary);
689
705
  if (command.status === "unmatched") return command;
690
706
  if (command.proposalId !== void 0) {
691
707
  const result2 = await this.decide(threadId, command.proposalId, actor, {
@@ -693,7 +709,7 @@ var ActionProposalService = class {
693
709
  remember: command.remember,
694
710
  via: "text"
695
711
  });
696
- return this.textReceipt(threadId, result2);
712
+ return this.textReceipt(threadId, result2, command.decision);
697
713
  }
698
714
  const candidates = [];
699
715
  const scope = await this.scope(threadId, actor);
@@ -702,30 +718,38 @@ var ActionProposalService = class {
702
718
  limit: 1e3
703
719
  });
704
720
  for (const proposal of pending) if (await this.mayDecide(actor, proposal)) candidates.push(proposal);
705
- const resolution = resolveTextActionProposalDecision(text, candidates);
721
+ const resolution = resolveTextActionProposalDecision(text, candidates, this.vocabulary);
706
722
  if (resolution.status === "unmatched") return resolution;
707
- if (resolution.status === "ambiguous" || resolution.status === "decision" && pending.length === 1e3 && !text.includes("#")) return {
708
- threadId,
709
- proposalDecision: {
710
- status: "ambiguous",
711
- proposalIds: candidates.map(({ id }) => id)
712
- },
713
- text: `Choose the action to decide: ${candidates.map(({ id }) => id).map((id) => `#${id}`).join(", ")}`
714
- };
723
+ const saturated = pending.length === 1e3 && !text.includes("#");
724
+ if (resolution.status === "ambiguous" || saturated) {
725
+ const proposalIds = candidates.map(({ id }) => id);
726
+ return {
727
+ threadId,
728
+ proposalDecision: {
729
+ status: "ambiguous",
730
+ proposalIds
731
+ },
732
+ text: saturated ? this.replies.tooMany : this.replies.ambiguous(proposalIds)
733
+ };
734
+ }
715
735
  const result = await this.decide(threadId, resolution.proposalId, actor, {
716
736
  decision: resolution.decision,
717
737
  remember: resolution.remember,
718
738
  via: "text"
719
739
  });
720
- return this.textReceipt(threadId, result);
740
+ return this.textReceipt(threadId, result, resolution.decision);
721
741
  }
722
- textReceipt(threadId, result) {
742
+ textReceipt(threadId, result, decision) {
723
743
  return {
724
744
  threadId,
725
745
  proposalDecision: result,
726
- text: result.proposal?.decision === "approved" ? "Action approved and queued for execution." : `Action ${result.proposal?.decision ?? result.status}; no execution was performed by this decision.`
746
+ text: this.reply(result, decision)
727
747
  };
728
748
  }
749
+ /** The configured reply to a decision the store answered with `result`. */
750
+ reply(result, decision) {
751
+ return textActionProposalReply(result, decision, this.replies);
752
+ }
729
753
  };
730
754
  ActionProposalService = _ts_decorate3([
731
755
  Injectable2(),
@@ -742,6 +766,33 @@ ActionProposalService = _ts_decorate3([
742
766
  import { AGENT_ATTACHMENT_STAGING, AGENT_DEPS_FACTORY, AGENT_OPTIONS as AGENT_OPTIONS3, AGENT_QUOTA_PROVIDER, AGENT_SINK as AGENT_SINK2, AGENT_STORE as AGENT_STORE3, encodeStreamEvent, isChatQueueStore, queuedMessageView } from "@dudousxd/nestjs-agent-core";
743
767
  import { Inject as Inject4, Injectable as Injectable3, Logger, NotImplementedException as NotImplementedException2, Optional as Optional2 } from "@nestjs/common";
744
768
 
769
+ // src/quota-exceeded.ts
770
+ import { publishAgentQuotaExceeded } from "@dudousxd/nestjs-agent-core";
771
+ function publishQuotaBlocked(actor, report, blocked) {
772
+ const window = report.windows.find((candidate) => candidate.period === blocked.period);
773
+ const payload = {
774
+ actorId: actor.id,
775
+ period: blocked.period,
776
+ ...blocked.reason !== void 0 ? {
777
+ reason: blocked.reason
778
+ } : {},
779
+ ...window?.usedTokens !== void 0 ? {
780
+ usedTokens: window.usedTokens
781
+ } : {},
782
+ ...window?.limitTokens !== void 0 ? {
783
+ limitTokens: window.limitTokens
784
+ } : {},
785
+ ...window !== void 0 ? {
786
+ usedUsd: window.usedUsd
787
+ } : {},
788
+ ...window?.limitUsd !== void 0 ? {
789
+ limitUsd: window.limitUsd
790
+ } : {}
791
+ };
792
+ publishAgentQuotaExceeded(payload);
793
+ }
794
+ __name(publishQuotaBlocked, "publishQuotaBlocked");
795
+
745
796
  // src/thread-persona.ts
746
797
  async function threadPersona(store, threadId) {
747
798
  const projecting = store;
@@ -1085,12 +1136,14 @@ var ChatQueueService = class _ChatQueueService {
1085
1136
  return null;
1086
1137
  }
1087
1138
  try {
1088
- const { blocked } = await this.quotaProvider.report({
1139
+ const report = await this.quotaProvider.report({
1089
1140
  actor
1090
1141
  });
1142
+ const { blocked } = report;
1091
1143
  if (blocked === void 0) {
1092
1144
  return null;
1093
1145
  }
1146
+ publishQuotaBlocked(actor, report, blocked);
1094
1147
  return blocked.reason ?? `The ${blocked.period === "day" ? "daily" : "monthly"} quota is used up`;
1095
1148
  } catch {
1096
1149
  return null;
@@ -1292,6 +1345,21 @@ var AgentService = class {
1292
1345
  this.queue = queue;
1293
1346
  this.proposals = proposals;
1294
1347
  }
1348
+ /**
1349
+ * Decide one independent proposal through the scoped proposal service — what the native
1350
+ * `action-proposals/:id/approve|reject` routes call — and the reply to show for it. `404` when
1351
+ * proposals are not on, `403`/`404` when this actor may not decide it.
1352
+ */
1353
+ async decideActionProposal(actor, threadId, proposalId, command) {
1354
+ if (this.options?.actionApprovalMode !== "independent" || !this.proposals) {
1355
+ throw new NotFoundException2("Proposal not found");
1356
+ }
1357
+ const result = await this.proposals.decide(threadId, proposalId, actor, command);
1358
+ return {
1359
+ proposalDecision: result,
1360
+ text: this.proposals.reply(result, command.decision)
1361
+ };
1362
+ }
1295
1363
  async handleTextDecision(threadId, actor, text) {
1296
1364
  if (this.options?.actionApprovalMode !== "independent" || !this.proposals) return {
1297
1365
  status: "unmatched"
@@ -1339,10 +1407,12 @@ var AgentService = class {
1339
1407
  if (this.options?.quota === void 0 || this.quotaProvider === void 0) {
1340
1408
  return;
1341
1409
  }
1342
- const { blocked } = await this.quotaProvider.report({
1410
+ const report = await this.quotaProvider.report({
1343
1411
  actor
1344
1412
  });
1413
+ const { blocked } = report;
1345
1414
  if (blocked !== void 0) {
1415
+ publishQuotaBlocked(actor, report, blocked);
1346
1416
  throw new HttpException({
1347
1417
  statusCode: HttpStatus.TOO_MANY_REQUESTS,
1348
1418
  code: "quota_exceeded",
@@ -4633,6 +4703,9 @@ var AiToolDiscoveryService = class _AiToolDiscoveryService {
4633
4703
  } : {}
4634
4704
  }, {
4635
4705
  execute: /* @__PURE__ */ __name((input, ctx) => instance.execute(input, ctx), "execute"),
4706
+ ...typeof handler.present === "function" ? {
4707
+ present: /* @__PURE__ */ __name((output, ctx) => instance.present(output, ctx), "present")
4708
+ } : {},
4636
4709
  ...typeof handler.preflight === "function" ? {
4637
4710
  preflight: /* @__PURE__ */ __name((input, ctx, options) => instance.preflight(input, ctx, options), "preflight")
4638
4711
  } : {},
@@ -5746,15 +5819,15 @@ function _ts_decorate26(decorators, target, key, desc) {
5746
5819
  __name(_ts_decorate26, "_ts_decorate");
5747
5820
  var DEFAULT_PATH = "agent";
5748
5821
  var logger = new Logger7("AgentModule");
5749
- function externalStoreWrapper(modules) {
5822
+ function externalWrapper(modules, token) {
5750
5823
  for (const module of modules.values()) {
5751
5824
  if (module.metatype === AgentModule) continue;
5752
- const wrapper = module.providers.get(AGENT_STORE7);
5825
+ const wrapper = module.providers.get(token);
5753
5826
  if (wrapper !== void 0) return wrapper;
5754
5827
  }
5755
5828
  return void 0;
5756
5829
  }
5757
- __name(externalStoreWrapper, "externalStoreWrapper");
5830
+ __name(externalWrapper, "externalWrapper");
5758
5831
  function forwardingStore(wrapper) {
5759
5832
  const target = /* @__PURE__ */ __name(() => {
5760
5833
  const instance = wrapper.instance;
@@ -5777,12 +5850,36 @@ function forwardingStore(wrapper) {
5777
5850
  __name(forwardingStore, "forwardingStore");
5778
5851
  function resolveStore(options, modules) {
5779
5852
  if (options.store !== void 0) return options.store;
5780
- const external = externalStoreWrapper(modules);
5853
+ const external = externalWrapper(modules, AGENT_STORE7);
5781
5854
  if (external !== void 0) return forwardingStore(external);
5782
5855
  logger.warn("No `store` configured and no store module imported \u2014 using the in-memory store. Threads are lost on restart and not shared between processes: not for production. Pass `store`, or import a store module (e.g. MikroOrmAgentStoreModule.forFeature()).");
5783
5856
  return new InMemoryAgentStore();
5784
5857
  }
5785
5858
  __name(resolveStore, "resolveStore");
5859
+ function resolveRolesPolicy(options, modules) {
5860
+ const external = externalWrapper(modules, AGENT_ROLES_POLICY2);
5861
+ if (options.rolesPolicy !== void 0) {
5862
+ if (external !== void 0) {
5863
+ logger.warn("Both a `rolesPolicy` option and a module binding AGENT_ROLES_POLICY (e.g. AgentAuthzModule) are configured \u2014 the `rolesPolicy` option wins.");
5864
+ }
5865
+ return options.rolesPolicy;
5866
+ }
5867
+ if (external !== void 0) return forwardingPolicy(external);
5868
+ return options.emptyRoles === "deny" ? new ClosedRolesPolicy(options.defaultRoles) : new DefaultRolesPolicy(options.defaultRoles);
5869
+ }
5870
+ __name(resolveRolesPolicy, "resolveRolesPolicy");
5871
+ function forwardingPolicy(wrapper) {
5872
+ return {
5873
+ can(actor, tool) {
5874
+ const policy = wrapper.instance;
5875
+ if (policy === void 0 || policy === null) {
5876
+ throw new Error("AgentModule: the AGENT_ROLES_POLICY bound by another module is not ready yet");
5877
+ }
5878
+ return policy.can(actor, tool);
5879
+ }
5880
+ };
5881
+ }
5882
+ __name(forwardingPolicy, "forwardingPolicy");
5786
5883
  function isQuotaProvider(quota) {
5787
5884
  return quota !== void 0 && typeof quota.report === "function";
5788
5885
  }
@@ -5844,9 +5941,10 @@ function sharedProviders(durable) {
5844
5941
  },
5845
5942
  {
5846
5943
  provide: AGENT_ROLES_POLICY2,
5847
- useFactory: /* @__PURE__ */ __name((options) => options.rolesPolicy ?? (options.emptyRoles === "deny" ? new ClosedRolesPolicy(options.defaultRoles) : new DefaultRolesPolicy(options.defaultRoles)), "useFactory"),
5944
+ useFactory: /* @__PURE__ */ __name((options, modules) => resolveRolesPolicy(options, modules), "useFactory"),
5848
5945
  inject: [
5849
- AGENT_OPTIONS9
5946
+ AGENT_OPTIONS9,
5947
+ ModulesContainer
5850
5948
  ]
5851
5949
  },
5852
5950
  {
@@ -6188,6 +6286,19 @@ var AgUiRunHandler = class {
6188
6286
  warnings.push(`The resume entry ${id} answers an interrupt this agent did not raise.`);
6189
6287
  }
6190
6288
  const first = plan.decisions[0];
6289
+ if (first !== void 0 && plan.decisions.every((d) => d.address.kind === "proposal")) {
6290
+ const decided = await this.decideProposals(actor, plan.decisions);
6291
+ this.writeEvents(res, [
6292
+ ...warningEvents(warnings),
6293
+ ...decided.flatMap((outcome, index) => actionProposalDecisionEvents({
6294
+ threadId: input.threadId,
6295
+ runId: index === 0 ? input.runId : `${input.runId}:${index}`,
6296
+ text: outcome.text,
6297
+ proposalDecision: outcome.proposalDecision
6298
+ }))
6299
+ ]);
6300
+ return;
6301
+ }
6191
6302
  if (first !== void 0) {
6192
6303
  const streamRunId = first.address.stream;
6193
6304
  await this.agent.assertResumable(actor, streamRunId);
@@ -6216,11 +6327,7 @@ var AgUiRunHandler = class {
6216
6327
  if (owner !== null && turn.media.length === 0) {
6217
6328
  const decision = await this.agent.handleTextDecision(input.threadId, actor, turn.text);
6218
6329
  if ("proposalDecision" in decision) {
6219
- res.status(200);
6220
- res.setHeader("Content-Type", "text/event-stream");
6221
- res.setHeader("Cache-Control", "no-cache, no-transform");
6222
- res.flushHeaders();
6223
- for (const event of [
6330
+ this.writeEvents(res, [
6224
6331
  ...warningEvents(warnings),
6225
6332
  ...actionProposalDecisionEvents({
6226
6333
  threadId: input.threadId,
@@ -6228,8 +6335,7 @@ var AgUiRunHandler = class {
6228
6335
  text: decision.text,
6229
6336
  proposalDecision: decision.proposalDecision
6230
6337
  })
6231
- ]) res.write(agUiSse(event));
6232
- res.end();
6338
+ ]);
6233
6339
  return;
6234
6340
  }
6235
6341
  }
@@ -6321,9 +6427,11 @@ var AgUiRunHandler = class {
6321
6427
  */
6322
6428
  async settle(actor, decisions) {
6323
6429
  const deliveries = [];
6430
+ const proposals = decisions.filter((decision) => decision.address.kind === "proposal");
6324
6431
  for (const { address, entry } of decisions) {
6325
6432
  const { toolCallId: toolCallId2 } = address;
6326
6433
  const abandoned = entry.status === "cancelled";
6434
+ if (address.kind === "proposal") continue;
6327
6435
  if (address.kind === "approval") {
6328
6436
  await this.agent.checkDecision(actor, toolCallId2);
6329
6437
  const decision = abandoned ? {
@@ -6360,9 +6468,53 @@ var AgUiRunHandler = class {
6360
6468
  via: AG_UI_VIA
6361
6469
  }));
6362
6470
  }
6471
+ if (proposals.length > 0) await this.decideProposals(actor, proposals);
6363
6472
  for (const deliver of deliveries) await deliver();
6364
6473
  }
6365
6474
  /**
6475
+ * Decide independent proposals through the proposal service — the call the native
6476
+ * `action-proposals/:id/approve|reject` routes make — never by signalling the origin run, which
6477
+ * is not waiting. Every payload is checked before the first decision; a refusal is the service's
6478
+ * own `403`/`404`.
6479
+ */
6480
+ async decideProposals(actor, decisions) {
6481
+ const planned = decisions.map(({ address, entry }) => {
6482
+ const decision = entry.status === "cancelled" ? {
6483
+ approved: false
6484
+ } : readApprovalPayload(entry.payload);
6485
+ if (decision === null || address.proposalId === void 0 || address.threadId === void 0) {
6486
+ throw invalid("invalid_resume", "an approval is answered with { approved: boolean, reason?, remember? }");
6487
+ }
6488
+ return {
6489
+ address,
6490
+ decision
6491
+ };
6492
+ });
6493
+ const outcomes = [];
6494
+ for (const { address, decision } of planned) {
6495
+ outcomes.push(await this.agent.decideActionProposal(actor, address.threadId, address.proposalId, {
6496
+ decision: decision.approved ? "approved" : "rejected",
6497
+ via: AG_UI_VIA,
6498
+ ...decision.remember !== void 0 ? {
6499
+ remember: decision.remember
6500
+ } : {},
6501
+ ...decision.reason !== void 0 ? {
6502
+ reason: decision.reason
6503
+ } : {}
6504
+ }));
6505
+ }
6506
+ return outcomes;
6507
+ }
6508
+ /** Answer with a complete list of events, no run behind them. */
6509
+ writeEvents(res, events) {
6510
+ res.status(200);
6511
+ res.setHeader("Content-Type", "text/event-stream");
6512
+ res.setHeader("Cache-Control", "no-cache, no-transform");
6513
+ res.flushHeaders();
6514
+ for (const event of events) res.write(agUiSse(event));
6515
+ res.end();
6516
+ }
6517
+ /**
6366
6518
  * Pipe a run to the client as AG-UI 1.0 over SSE: one event per `data:` line, from `RUN_STARTED`
6367
6519
  * to the run's terminal event. It ends when the run stops to ask — the library run stays parked,
6368
6520
  * and the request that answers re-attaches to it.
@@ -6569,6 +6721,7 @@ export {
6569
6721
  attachmentLimits,
6570
6722
  declaredSkillProvider,
6571
6723
  defaultRequestUserMapper,
6724
+ defineTool,
6572
6725
  delegateToolName,
6573
6726
  provideAgentTool,
6574
6727
  provideAgentTools,