@dudousxd/nestjs-agent 1.19.2 → 1.19.3

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/README.md CHANGED
@@ -156,6 +156,8 @@ per-message cap — so a client never repeats them.
156
156
  it leaves media with nothing pointing at it. Two optional methods make that findable — `list` on
157
157
  your `AttachmentStagingStore` (you stored the bytes, so only you can enumerate them) and
158
158
  `referencedMediaIds` on your `AgentStore` (only it can see which media a live message carries).
159
+ A message waiting in a thread's queue — sent while a turn ran, or into a paused queue — counts as
160
+ live: its files are kept until it runs, is removed, or has its attachments edited away.
159
161
 
160
162
  ```ts
161
163
  // a job the host runs; nothing here is reachable over HTTP
@@ -282,12 +282,60 @@ var AgentRunWorkflow = class _AgentRunWorkflow {
282
282
  store;
283
283
  runs;
284
284
  queue;
285
+ workflows;
285
286
  logger = new import_common2.Logger(_AgentRunWorkflow.name);
286
- constructor(factory, store, runs, queue) {
287
+ constructor(factory, store, runs, queue, workflows) {
287
288
  this.factory = factory;
288
289
  this.store = store;
289
290
  this.runs = runs;
290
291
  this.queue = queue;
292
+ this.workflows = workflows;
293
+ }
294
+ /**
295
+ * Start a detached delegation as a TOP-LEVEL run, under an id derived from this run and the call.
296
+ *
297
+ * Not `ctx.startChild`, because the runtime treats a `spawn:` child as part of its parent: a Stop
298
+ * on the parent cascades to every run it spawned. A detached run is exactly the one the person did
299
+ * NOT ask to stop — they stopped the turn they were watching, and the work it handed off is meant
300
+ * to outlive that turn (it is meant to outlive it finishing, after all). Cascaded, it died parked
301
+ * on its approval, where its body never runs again, so nothing settled the delegation and the card
302
+ * said "started" for ever. The agent-level edge stays: the child's input still carries
303
+ * `parentRunId`, which is what the run rows record.
304
+ *
305
+ * Journaled as one `detach:<toolCallId>` localStep whose body runs outside the ambient workflow
306
+ * ctx (so the start goes to the engine and writes no position in this run). Its recorded result
307
+ * keeps a replay from starting it again, and a body re-run after a crash between the start and
308
+ * the checkpoint asks for the same id, which the runtime answers with the run it already has.
309
+ * The child is stamped with this run's namespace, as `ctx.startChild` would have stamped it, so an
310
+ * operator re-driving a tenant's turn does not move its delegate off that tenant's workers.
311
+ */
312
+ startDetached(ctx, toolCallId2, childInput) {
313
+ return stepOf(ctx)(`detach:${toolCallId2}`, async () => {
314
+ const workflows = this.workflows;
315
+ if (workflows === void 0) {
316
+ throw new Error("A detached delegation needs WorkflowService (DurableModule) to start its run, and none is bound.");
317
+ }
318
+ const childRunId = `${ctx.runId}.detached.${toolCallId2}`;
319
+ const namespace = await this.namespaceOf(ctx.runId);
320
+ try {
321
+ await workflows.start(_AgentRunWorkflow, childInput, childRunId, namespace !== void 0 ? {
322
+ namespace
323
+ } : void 0);
324
+ } catch (error) {
325
+ if (!(0, import_nestjs_durable_core2.isWorkflowControlFlowSignal)(error)) {
326
+ throw error;
327
+ }
328
+ }
329
+ return childRunId;
330
+ });
331
+ }
332
+ /** The namespace this run is stamped with, where the runtime can say. */
333
+ async namespaceOf(runId) {
334
+ try {
335
+ return (await this.runs?.getRunDetail(runId))?.run.namespace;
336
+ } catch {
337
+ return void 0;
338
+ }
291
339
  }
292
340
  /**
293
341
  * Move the thread past this settling turn — to the next queued message, started here as a
@@ -488,6 +536,10 @@ var AgentRunWorkflow = class _AgentRunWorkflow {
488
536
  // OWN runId, and that is what `runForToolCall` answers with.
489
537
  // - `deliverTo` instead. The parent's tool result is a receipt, so the answer needs an
490
538
  // address of its own, and by the time it exists nobody is holding one.
539
+ // - no runtime parent. A Stop on this turn must not reach the work it handed off, so the
540
+ // run is started on its own (`startDetached`), not as a `spawn:` child. A run that
541
+ // journaled the `spawn:` before this release replays it: `ctx.patched` answers false at a
542
+ // position that already holds one.
491
543
  startAgent: /* @__PURE__ */ __name(async ({ agentName, task, toolCallId: toolCallId2 }) => {
492
544
  const subThreadId = await stepOf(ctx)(`subthread:${agentName}`, async () => {
493
545
  const thread = await this.store.createThread({
@@ -496,7 +548,7 @@ var AgentRunWorkflow = class _AgentRunWorkflow {
496
548
  });
497
549
  return thread.id;
498
550
  });
499
- const childRunId = await ctx.startChild(_AgentRunWorkflow, {
551
+ const childInput = {
500
552
  agentName,
501
553
  threadId: subThreadId,
502
554
  actor: input.actor,
@@ -509,9 +561,14 @@ var AgentRunWorkflow = class _AgentRunWorkflow {
509
561
  threadId: input.threadId,
510
562
  toolCallId: toolCallId2
511
563
  }
512
- });
564
+ };
565
+ if (!await ctx.patched("agent:detached-unlinked")) {
566
+ return {
567
+ runId: await ctx.startChild(_AgentRunWorkflow, childInput)
568
+ };
569
+ }
513
570
  return {
514
- runId: childRunId
571
+ runId: await this.startDetached(ctx, toolCallId2, childInput)
515
572
  };
516
573
  }, "startAgent"),
517
574
  // The two long steps as engine-dispatched `ctx.step`s, so a turn isn't pinned to this
@@ -624,12 +681,14 @@ AgentRunWorkflow = _ts_decorate2([
624
681
  _ts_param2(2, (0, import_common2.Inject)(import_nestjs_durable2.RUN_GATEWAY)),
625
682
  _ts_param2(3, (0, import_common2.Optional)()),
626
683
  _ts_param2(3, (0, import_common2.Inject)(AGENT_CHAT_QUEUE)),
684
+ _ts_param2(4, (0, import_common2.Optional)()),
627
685
  _ts_metadata2("design:type", Function),
628
686
  _ts_metadata2("design:paramtypes", [
629
687
  typeof AgentDepsFactory === "undefined" ? Object : AgentDepsFactory,
630
688
  typeof AgentStore === "undefined" ? Object : AgentStore,
631
689
  typeof RunGateway === "undefined" ? Object : RunGateway,
632
- typeof ChatQueueService === "undefined" ? Object : ChatQueueService
690
+ typeof ChatQueueService === "undefined" ? Object : ChatQueueService,
691
+ typeof import_nestjs_durable2.WorkflowService === "undefined" ? Object : import_nestjs_durable2.WorkflowService
633
692
  ])
634
693
  ], AgentRunWorkflow);
635
694
  function isSignalTimeout(error) {
@@ -750,6 +809,20 @@ function isThreadTurn(input) {
750
809
  return typeof input === "object" && input !== null && input.sinkRunId === void 0 && input.deliverTo === void 0;
751
810
  }
752
811
  __name(isThreadTurn, "isThreadTurn");
812
+ function deliveryOf(input) {
813
+ if (typeof input !== "object" || input === null) {
814
+ return void 0;
815
+ }
816
+ const { deliverTo, agentName } = input;
817
+ if (typeof deliverTo !== "object" || deliverTo === null || typeof deliverTo.threadId !== "string" || typeof deliverTo.toolCallId !== "string") {
818
+ return void 0;
819
+ }
820
+ return {
821
+ delivery: deliverTo,
822
+ agent: typeof agentName === "string" ? agentName : "default"
823
+ };
824
+ }
825
+ __name(deliveryOf, "deliveryOf");
753
826
  var DurableAgentRunner = class _DurableAgentRunner {
754
827
  static {
755
828
  __name(this, "DurableAgentRunner");
@@ -829,11 +902,16 @@ var DurableAgentRunner = class _DurableAgentRunner {
829
902
  */
830
903
  async cancel(runId) {
831
904
  this.logger.log(`cancelling agent run ${runId}`);
832
- const input = await this.inputOf(runId);
905
+ const detail = await this.detailOf(runId);
906
+ const input = detail?.run.input;
833
907
  const threadId = threadOfRun(input);
834
908
  await this.runs.cancel(runId, {
835
909
  compensate: true
836
910
  });
911
+ await this.settleCancelledDelegation(runId, input);
912
+ for (const childId of detail?.children ?? []) {
913
+ await this.settleCascadedDetached(childId);
914
+ }
837
915
  const writer = await this.sink.open(runId);
838
916
  if (threadId !== void 0) {
839
917
  if (this.queue?.supported === true && isThreadTurn(input)) {
@@ -864,7 +942,57 @@ var DurableAgentRunner = class _DurableAgentRunner {
864
942
  });
865
943
  }
866
944
  /**
867
- * The input a run was started with (which thread it was streaming), or `undefined` where the gateway cannot say.
945
+ * Settle the delegation a DETACHED run was started for, once it has been stopped. A run parked on
946
+ * a human never runs its body again — the runtime settles it from outside — so the body's own
947
+ * `deliver:detached:unsettled` is not reached, and without this the card in the calling thread says
948
+ * "started" for ever. Settles once (see `settleUnsettledDelegation`), so a body that did observe
949
+ * the Stop and a run that had already answered are both left as they are. A no-op for any run
950
+ * that does not deliver into another thread.
951
+ */
952
+ async settleCancelledDelegation(runId, input) {
953
+ const target = deliveryOf(input);
954
+ if (target === void 0) {
955
+ return;
956
+ }
957
+ try {
958
+ await (0, import_nestjs_agent_core4.settleUnsettledDelegation)({
959
+ store: this.store,
960
+ delivery: target.delivery,
961
+ agent: target.agent,
962
+ runId,
963
+ status: "cancelled"
964
+ });
965
+ } catch (error) {
966
+ this.logger.error(`could not settle the delegation of cancelled run ${runId}: ${error instanceof Error ? error.message : String(error)}`);
967
+ }
968
+ }
969
+ /**
970
+ * A detached child the runtime cancelled along with its parent (a `spawn:` journaled before
971
+ * detached runs were started on their own): end it the way a Stop on its own id would have.
972
+ */
973
+ async settleCascadedDetached(childId) {
974
+ const child = await this.detailOf(childId);
975
+ const status = child?.run.status;
976
+ if (child === void 0 || deliveryOf(child.run.input) === void 0 || status !== "cancelled" && status !== "cancelling") {
977
+ return;
978
+ }
979
+ const subThreadId = threadOfRun(child.run.input);
980
+ if (subThreadId !== void 0) {
981
+ await (0, import_nestjs_agent_core4.releaseThreadRun)(this.store, subThreadId, childId);
982
+ }
983
+ await this.settleCancelledDelegation(childId, child.run.input);
984
+ const writer = await this.sink.open(childId);
985
+ await writer.write((0, import_nestjs_agent_core4.encodeStreamEvent)({
986
+ kind: "cancelled"
987
+ }));
988
+ await writer.end();
989
+ await this.store.recordRunEnd?.({
990
+ runId: childId,
991
+ status: "cancelled"
992
+ });
993
+ }
994
+ /**
995
+ * What the runtime recorded for a run — the input it was started with (which thread it was streaming), its status and the runs it spawned — or `undefined` where the gateway cannot say.
868
996
  *
869
997
  * A read failure answers `undefined` instead of propagating, the same posture `AgentRunWorkflow`
870
998
  * takes for its own cancel observation: failing to ask is not an answer worth failing a cancel
@@ -873,9 +1001,9 @@ var DurableAgentRunner = class _DurableAgentRunner {
873
1001
  * with a subscriber holding a stream that never settles, from the one call whose entire job is to
874
1002
  * make a run stop.
875
1003
  */
876
- async inputOf(runId) {
1004
+ async detailOf(runId) {
877
1005
  try {
878
- return (await this.runs.getRunDetail(runId))?.run.input;
1006
+ return await this.runs.getRunDetail(runId) ?? void 0;
879
1007
  } catch {
880
1008
  return void 0;
881
1009
  }