@dudousxd/nestjs-agent 1.23.0 → 1.24.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
@@ -673,6 +673,31 @@ function childSinkWriter(writer) {
673
673
  }
674
674
  __name(childSinkWriter, "childSinkWriter");
675
675
 
676
+ // src/attachment-limits.ts
677
+ var DEFAULT_MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
678
+ var DEFAULT_ALLOWED_ATTACHMENT_CONTENT_TYPES = [
679
+ "image/png",
680
+ "image/jpeg",
681
+ "image/gif",
682
+ "image/webp",
683
+ "application/pdf",
684
+ "text/plain",
685
+ "text/csv"
686
+ ];
687
+ var MAX_ATTACHMENTS_PER_MESSAGE = 10;
688
+ function attachmentLimits(options, staging) {
689
+ const declared = staging?.describe?.();
690
+ const own = options?.attachments;
691
+ return {
692
+ enabled: staging !== void 0,
693
+ upload: staging === void 0 ? null : declared?.upload ?? "multipart",
694
+ maxBytes: declared?.maxBytes ?? own?.maxBytes ?? DEFAULT_MAX_ATTACHMENT_BYTES,
695
+ allowedContentTypes: declared?.allowedContentTypes ?? own?.allowedContentTypes ?? DEFAULT_ALLOWED_ATTACHMENT_CONTENT_TYPES,
696
+ maxPerMessage: MAX_ATTACHMENTS_PER_MESSAGE
697
+ };
698
+ }
699
+ __name(attachmentLimits, "attachmentLimits");
700
+
676
701
  // src/proposals/action-proposal.service.ts
677
702
  var import_nestjs_agent_core5 = require("@dudousxd/nestjs-agent-core");
678
703
  var import_common3 = require("@nestjs/common");
@@ -829,6 +854,10 @@ var ActionProposalService = class {
829
854
  text: this.reply(result, decision)
830
855
  };
831
856
  }
857
+ /** The configured words a text decision is made of (`actionProposalText.vocabulary`). */
858
+ textVocabulary() {
859
+ return this.vocabulary;
860
+ }
832
861
  /** The configured reply to a decision the store answered with `result`. */
833
862
  reply(result, decision) {
834
863
  return (0, import_nestjs_agent_core5.textActionProposalReply)(result, decision, this.replies);
@@ -1455,6 +1484,65 @@ var AgentService = class {
1455
1484
  };
1456
1485
  return this.proposals.handleTextDecision(threadId, actor, text);
1457
1486
  }
1487
+ /** How `action` tools wait for their approval here — `AgentModule.forRoot({ actionApprovalMode })`. */
1488
+ actionApprovalMode() {
1489
+ return this.options?.actionApprovalMode ?? "blocking";
1490
+ }
1491
+ /**
1492
+ * The words a text decision is made of (`actionProposalText.vocabulary`, English by default) — what
1493
+ * a surface without buttons tells the person to reply.
1494
+ */
1495
+ actionProposalVocabulary() {
1496
+ return this.proposals?.textVocabulary() ?? import_nestjs_agent_core9.DEFAULT_TEXT_ACTION_PROPOSAL_VOCABULARY;
1497
+ }
1498
+ /** The configured reply (`actionProposalText.replies`) to a decision the store answered with `result`. */
1499
+ actionProposalReply(result, decision) {
1500
+ return this.proposals?.reply(result, decision) ?? (0, import_nestjs_agent_core9.textActionProposalReply)(result, decision);
1501
+ }
1502
+ /**
1503
+ * The proposals of a thread this actor may see — the thread's own actor, or one the policy lets
1504
+ * decide them; what `GET <base>/threads/:threadId/action-proposals` serves (its first page). `404`
1505
+ * when independent approvals are not on.
1506
+ */
1507
+ async listActionProposals(actor, threadId) {
1508
+ if (this.options?.actionApprovalMode !== "independent" || !this.proposals) {
1509
+ throw new import_common6.NotFoundException("Proposal not found");
1510
+ }
1511
+ return this.proposals.list(threadId, actor);
1512
+ }
1513
+ /**
1514
+ * The attachment rules in force — what `GET <base>/config` serves and `POST <base>/attachments`
1515
+ * enforces. `enabled: false` when no `AGENT_ATTACHMENT_STAGING` provider is bound.
1516
+ */
1517
+ attachmentLimits() {
1518
+ return attachmentLimits(this.options, this.staging);
1519
+ }
1520
+ /**
1521
+ * Stage a file for a later turn, held to the same limits as `POST <base>/attachments` — for a
1522
+ * surface that receives files itself (a messaging webhook). Refuses with `501` when attachments are
1523
+ * off, `415` for a type outside the allowlist and `413` past the size cap; send the returned
1524
+ * `mediaId` as `attachments: [{ mediaId }]`.
1525
+ */
1526
+ async stageAttachment(actor, file) {
1527
+ if (this.staging === void 0) {
1528
+ throw new import_common6.NotImplementedException("Attachments are not configured on this server: bind AGENT_ATTACHMENT_STAGING (e.g. import AgentMediaAttachmentsModule).");
1529
+ }
1530
+ const { allowedContentTypes, maxBytes } = this.attachmentLimits();
1531
+ const contentType = file.contentType.split(";")[0]?.trim().toLowerCase() ?? "";
1532
+ if (!allowedContentTypes.includes(contentType)) {
1533
+ throw new import_common6.UnsupportedMediaTypeException(`content type "${contentType}" is not allowed (allowed: ${allowedContentTypes.join(", ")})`);
1534
+ }
1535
+ if (file.data.byteLength > maxBytes) {
1536
+ throw new import_common6.PayloadTooLargeException(`file exceeds the ${maxBytes}-byte limit`);
1537
+ }
1538
+ return this.staging.stage({
1539
+ data: file.data,
1540
+ filename: file.filename,
1541
+ contentType,
1542
+ sizeBytes: file.data.byteLength,
1543
+ actor
1544
+ });
1545
+ }
1458
1546
  /** The queue service when the bound store can hold a queue, else `undefined`. */
1459
1547
  queueing() {
1460
1548
  return this.queue?.supported === true ? this.queue : void 0;
@@ -2738,33 +2826,6 @@ AgentsController = _ts_decorate7([
2738
2826
  var import_nestjs_agent_core11 = require("@dudousxd/nestjs-agent-core");
2739
2827
  var import_common9 = require("@nestjs/common");
2740
2828
  var import_platform_express = require("@nestjs/platform-express");
2741
-
2742
- // src/attachment-limits.ts
2743
- var DEFAULT_MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
2744
- var DEFAULT_ALLOWED_ATTACHMENT_CONTENT_TYPES = [
2745
- "image/png",
2746
- "image/jpeg",
2747
- "image/gif",
2748
- "image/webp",
2749
- "application/pdf",
2750
- "text/plain",
2751
- "text/csv"
2752
- ];
2753
- var MAX_ATTACHMENTS_PER_MESSAGE = 10;
2754
- function attachmentLimits(options, staging) {
2755
- const declared = staging?.describe?.();
2756
- const own = options?.attachments;
2757
- return {
2758
- enabled: staging !== void 0,
2759
- upload: staging === void 0 ? null : declared?.upload ?? "multipart",
2760
- maxBytes: declared?.maxBytes ?? own?.maxBytes ?? DEFAULT_MAX_ATTACHMENT_BYTES,
2761
- allowedContentTypes: declared?.allowedContentTypes ?? own?.allowedContentTypes ?? DEFAULT_ALLOWED_ATTACHMENT_CONTENT_TYPES,
2762
- maxPerMessage: MAX_ATTACHMENTS_PER_MESSAGE
2763
- };
2764
- }
2765
- __name(attachmentLimits, "attachmentLimits");
2766
-
2767
- // src/controller/attachments.controller.ts
2768
2829
  function _ts_decorate8(decorators, target, key, desc) {
2769
2830
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2770
2831
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
@@ -5167,6 +5228,34 @@ var ActionProposalWorkerService = class _ActionProposalWorkerService {
5167
5228
  deps;
5168
5229
  worker;
5169
5230
  logger = new import_common24.Logger(_ActionProposalWorkerService.name);
5231
+ settledListeners = /* @__PURE__ */ new Set();
5232
+ /**
5233
+ * Run `listener` after every proposal this process's worker settles — how a module relays an
5234
+ * outcome somewhere the turn's stream no longer reaches (a text channel's conversation). Next to
5235
+ * `actionProposalWorker.onSettled`; a listener that throws is logged and does not stop the others.
5236
+ * Returns the unsubscribe. Only the replicas that run the worker (not `surface: 'http'`) call it.
5237
+ */
5238
+ onSettled(listener) {
5239
+ this.settledListeners.add(listener);
5240
+ return () => {
5241
+ this.settledListeners.delete(listener);
5242
+ };
5243
+ }
5244
+ async settled(proposal) {
5245
+ const configured = this.options.actionProposalWorker?.onSettled;
5246
+ for (const listener of [
5247
+ ...configured ? [
5248
+ configured
5249
+ ] : [],
5250
+ ...this.settledListeners
5251
+ ]) {
5252
+ try {
5253
+ await listener(proposal);
5254
+ } catch (error) {
5255
+ this.logger.error(error);
5256
+ }
5257
+ }
5258
+ }
5170
5259
  constructor(store, options, deps) {
5171
5260
  this.store = store;
5172
5261
  this.options = options;
@@ -5182,6 +5271,7 @@ var ActionProposalWorkerService = class _ActionProposalWorkerService {
5182
5271
  store: this.store,
5183
5272
  workerId: (0, import_node_crypto2.randomUUID)(),
5184
5273
  ...this.options.actionProposalWorker,
5274
+ onSettled: /* @__PURE__ */ __name((proposal) => this.settled(proposal), "onSettled"),
5185
5275
  executor: new import_nestjs_agent_core29.ActionProposalExecutor({
5186
5276
  resolver,
5187
5277
  resolveExecution: /* @__PURE__ */ __name(async (proposal, actor) => {
@@ -6452,7 +6542,10 @@ var AgUiRunHandler = class {
6452
6542
  if (owner !== null && owner !== actor.id) {
6453
6543
  throw new import_common28.ForbiddenException("thread belongs to another actor");
6454
6544
  }
6455
- if (owner !== null && turn.media.length === 0) {
6545
+ if (forwarded.regenerate === true && owner === null) {
6546
+ throw invalid("invalid_input", "regenerate needs a thread that already has an exchange");
6547
+ }
6548
+ if (owner !== null && forwarded.regenerate !== true && turn.media.length === 0 && turn.staged.length === 0) {
6456
6549
  const decision = await this.agent.handleTextDecision(input.threadId, actor, turn.text);
6457
6550
  if ("proposalDecision" in decision) {
6458
6551
  this.writeEvents(res, [
@@ -6467,7 +6560,7 @@ var AgUiRunHandler = class {
6467
6560
  return;
6468
6561
  }
6469
6562
  }
6470
- const refs = await this.stage(actor, turn.media, warnings);
6563
+ const refs = await this.stage(actor, turn.media, warnings, turn.staged);
6471
6564
  if (turn.text.trim().length === 0 && refs.length === 0) {
6472
6565
  throw invalid("no_user_message", "the user message to answer is empty");
6473
6566
  }
@@ -6503,6 +6596,9 @@ var AgUiRunHandler = class {
6503
6596
  } : {},
6504
6597
  ...refs.length > 0 ? {
6505
6598
  attachments: refs
6599
+ } : {},
6600
+ ...forwarded.regenerate === true ? {
6601
+ regenerate: true
6506
6602
  } : {}
6507
6603
  });
6508
6604
  await this.pipe(res, started.runId, {
@@ -6514,10 +6610,25 @@ var AgUiRunHandler = class {
6514
6610
  preamble: warningEvents(warnings)
6515
6611
  });
6516
6612
  }
6517
- /** Stage the turn's inline media through the bound store, within its limits; say what was not used. */
6518
- async stage(actor, media, warnings) {
6613
+ /**
6614
+ * The turn's attachments: the ones already staged (by mediaId), then its inline media staged
6615
+ * through the bound store — all within the store's limits; say what was not used.
6616
+ */
6617
+ async stage(actor, media, warnings, staged = []) {
6519
6618
  const refs = [];
6520
6619
  const limits = attachmentLimits(this.options, this.staging);
6620
+ for (const ref of staged) {
6621
+ const label = `The attachment ${ref.mediaId} was not used`;
6622
+ if (this.staging === void 0) {
6623
+ warnings.push(`${label}: attachments are not enabled on this agent.`);
6624
+ } else if (refs.length >= limits.maxPerMessage) {
6625
+ warnings.push(`${label}: a message carries at most ${limits.maxPerMessage} attachments.`);
6626
+ } else {
6627
+ refs.push({
6628
+ mediaId: ref.mediaId
6629
+ });
6630
+ }
6631
+ }
6521
6632
  for (const item of media) {
6522
6633
  const label = `The ${item.kind} "${item.filename}" was not used`;
6523
6634
  if (this.staging === void 0) {
@@ -6530,7 +6641,7 @@ var AgUiRunHandler = class {
6530
6641
  warnings.push(`${label}: it is larger than ${limits.maxBytes} bytes.`);
6531
6642
  } else {
6532
6643
  try {
6533
- const staged = await this.staging.stage({
6644
+ const staged2 = await this.staging.stage({
6534
6645
  data: item.data,
6535
6646
  filename: item.filename,
6536
6647
  contentType: item.contentType,
@@ -6538,7 +6649,7 @@ var AgUiRunHandler = class {
6538
6649
  actor
6539
6650
  });
6540
6651
  refs.push({
6541
- mediaId: staged.mediaId
6652
+ mediaId: staged2.mediaId
6542
6653
  });
6543
6654
  } catch {
6544
6655
  warnings.push(`${label}: it could not be stored.`);
@@ -6656,11 +6767,17 @@ var AgUiRunHandler = class {
6656
6767
  res.setHeader("X-Agent-Run-Id", runId);
6657
6768
  res.flushHeaders();
6658
6769
  let terminal = false;
6770
+ const cursor = {
6771
+ seq: 0
6772
+ };
6659
6773
  try {
6660
6774
  const frames = (0, import_ag_ui.agUiFramesFromNdjson)(this.agent.subscribe(runId));
6661
- for await (const event of (0, import_ag_ui.agUiEvents)(frames, options)) {
6775
+ for await (const event of (0, import_ag_ui.agUiEvents)(frames, {
6776
+ ...options,
6777
+ cursor
6778
+ })) {
6662
6779
  if (event.type === "RUN_FINISHED" || event.type === "RUN_ERROR") terminal = true;
6663
- res.write((0, import_ag_ui.agUiSse)(event));
6780
+ res.write((0, import_ag_ui.agUiSse)(event, cursor.seq));
6664
6781
  }
6665
6782
  } catch {
6666
6783
  if (!terminal) {