@dudousxd/nestjs-agent 1.36.2 → 1.38.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,5 +1,5 @@
1
1
  import { A2uiStreamOptions, a2uiThreadReplay } from '@dudousxd/nestjs-agent-core/a2ui';
2
- import { AgentAttachmentConfig, AttachmentStagingStore, AgentStore, Actor, ListActionProposals, ActionProposalView, ActionProposalMutationView, TextActionProposalVocabulary, AgentRunner, ModelCatalog, QuotaProvider, ActionApprovalMode, MessageAttachment, QuotaReport, ModelCatalogView, AgentHostContext, AttachmentRef, PageContext, ChatQueueState, SubscribeOptions, HumanReply, ThreadSummary, ThreadDetail, UpdateThreadInput, MessageFeedbackValue, MessageFeedback, StagedAttachment, ActorResolver } from '@dudousxd/nestjs-agent-core';
2
+ import { AgentAttachmentConfig, AttachmentStagingStore, AgentStore, Actor, ListActionProposals, ActionProposalView, ActionProposalMutationView, TextActionProposalVocabulary, AgentRunner, ModelCatalog, QuotaProvider, ActionApprovalMode, MessageAttachment, QuotaReport, ModelCatalogView, AgentHostContext, AttachmentRef, PageContext, ChatQueueState, SubscribeOptions, HumanReply, ThreadSummary, ThreadDetail, UpdateThreadInput, MessageFeedbackValue, MessageFeedback, StagedAttachment, AiUsageInput, AiUsageOptions, AiUsageResult, ActorResolver } from '@dudousxd/nestjs-agent-core';
3
3
  import { Request, Response } from 'express';
4
4
  import { UiCapabilities } from '@dudousxd/nestjs-agent-core/genui';
5
5
  import { b as AgentModuleOptions, A as AgentDepsFactory, C as ChatQueueService, d as AgentProtocolAdapter } from './chat-queue.service-fopUN313.cjs';
@@ -167,6 +167,22 @@ interface ChatParams {
167
167
  interface ThreadDefaultAgentReader {
168
168
  defaultAgentForThread(threadId: string): Promise<string | null>;
169
169
  }
170
+ /** What {@link AgentService.forkThread} takes past the thread and the message to copy up to. */
171
+ interface ForkThreadParams {
172
+ /**
173
+ * The HOST already decided this actor may read the thread — "continue this conversation" from a
174
+ * link its owner shared. Skips the ownership check, and the copy belongs to the CALLER, not the
175
+ * source's owner; the original is left as it was. Never set it from a request's own claim: the
176
+ * `POST /threads/:id/fork-from/:messageId` route never does, so it stays owner-only.
177
+ */
178
+ authorized?: boolean;
179
+ /**
180
+ * The copy's tenant — see `ForkThreadOptions.tenantRef`. Absent keeps the SOURCE's, even when the
181
+ * copy now belongs to someone else; forking across tenants, pass the caller's
182
+ * (`actor.tenantRef ?? null`). `null` clears it.
183
+ */
184
+ tenantRef?: string | null;
185
+ }
170
186
  /** Like {@link ThreadDefaultAgentReader}, for the thread's pinned model. */
171
187
  interface ThreadModelReader {
172
188
  modelForThread(threadId: string): Promise<string | null>;
@@ -430,7 +446,12 @@ declare class AgentService {
430
446
  listThreads(actorRef: string): Promise<ThreadSummary[]>;
431
447
  getThread(actor: Actor, threadId: string): Promise<ThreadDetail | null>;
432
448
  deleteThread(actor: Actor, threadId: string): Promise<void>;
433
- forkThread(actor: Actor, threadId: string, fromMessageId: string): Promise<ThreadSummary>;
449
+ /**
450
+ * Copy a thread up to `fromMessageId` (inclusive) into a new one. By default only the thread's
451
+ * owner may, and the copy is theirs. With `params.authorized`, the host has vouched for `actor`
452
+ * (a share link it checked) and the copy is `actor`'s — see {@link ForkThreadParams}.
453
+ */
454
+ forkThread(actor: Actor, threadId: string, fromMessageId: string, params?: ForkThreadParams): Promise<ThreadSummary>;
434
455
  /** Kept for source compatibility — a thin wrapper over the more general `updateThread`. */
435
456
  renameThread(actor: Actor, threadId: string, title: string): Promise<void>;
436
457
  /**
@@ -567,6 +588,22 @@ declare class AgentService {
567
588
  private resolveRunForToolCall;
568
589
  /** Authorization seam for `cancel`: the caller must own the thread currently streaming this run. */
569
590
  private assertOwnsActiveStream;
591
+ /**
592
+ * Record AI spend that did not come from an agent turn — a transcription, a TTS render, a batch of
593
+ * embeddings, a direct OpenAI/OpenRouter SDK call — on the same ledger the agent's turns write, so
594
+ * cost, quota and the dashboard see one figure.
595
+ *
596
+ * Never throws into the caller's flow by default: a failed write or an invalid input is logged and
597
+ * answered with `{ recorded: false, error }`. Pass `{ throwOnError: true }` where losing the row
598
+ * must fail the caller.
599
+ *
600
+ * Counted against `actorRef` like an agent turn: the ledger is the budget (`LedgerQuotaProvider`),
601
+ * so its tokens count in the token windows (a row with no tokens adds 0 there) and its `costUsd` in
602
+ * the USD windows. A `QuotaStore` counter is bumped only where loop deps carry one (`AgentModule`
603
+ * wires none). `actorRef: null` is platform cost, recorded under `PLATFORM_ACTOR_REF` and counted
604
+ * against nobody.
605
+ */
606
+ recordUsage(input: AiUsageInput, options?: AiUsageOptions): Promise<AiUsageResult>;
570
607
  }
571
608
 
572
609
  interface A2uiAdapterOptions extends A2uiStreamOptions {
@@ -647,4 +684,4 @@ declare class A2uiRunHandler {
647
684
  */
648
685
  declare function a2uiAdapter(options?: A2uiAdapterOptions): AgentProtocolAdapter;
649
686
 
650
- export { AgentService as A, type ChatParams as C, DEFAULT_ALLOWED_ATTACHMENT_CONTENT_TYPES as D, MAX_ATTACHMENTS_PER_MESSAGE as M, NO_USER_MESSAGE_CODE as N, type QueuedSend as Q, type ThreadDefaultAgentReader as T, ActionProposalService as a, type A2uiAdapterOptions as b, A2uiRunHandler as c, type AttachmentLimits as d, type ChatSendMode as e, type ChatSendResult as f, DEFAULT_MAX_ATTACHMENT_BYTES as g, type ThreadModelReader as h, a2uiAdapter as i, attachmentLimits as j };
687
+ export { AgentService as A, type ChatParams as C, DEFAULT_ALLOWED_ATTACHMENT_CONTENT_TYPES as D, type ForkThreadParams as F, MAX_ATTACHMENTS_PER_MESSAGE as M, NO_USER_MESSAGE_CODE as N, type QueuedSend as Q, type ThreadDefaultAgentReader as T, ActionProposalService as a, type A2uiAdapterOptions as b, A2uiRunHandler as c, type AttachmentLimits as d, type ChatSendMode as e, type ChatSendResult as f, DEFAULT_MAX_ATTACHMENT_BYTES as g, type ThreadModelReader as h, a2uiAdapter as i, attachmentLimits as j };
@@ -1,5 +1,5 @@
1
1
  import { A2uiStreamOptions, a2uiThreadReplay } from '@dudousxd/nestjs-agent-core/a2ui';
2
- import { AgentAttachmentConfig, AttachmentStagingStore, AgentStore, Actor, ListActionProposals, ActionProposalView, ActionProposalMutationView, TextActionProposalVocabulary, AgentRunner, ModelCatalog, QuotaProvider, ActionApprovalMode, MessageAttachment, QuotaReport, ModelCatalogView, AgentHostContext, AttachmentRef, PageContext, ChatQueueState, SubscribeOptions, HumanReply, ThreadSummary, ThreadDetail, UpdateThreadInput, MessageFeedbackValue, MessageFeedback, StagedAttachment, ActorResolver } from '@dudousxd/nestjs-agent-core';
2
+ import { AgentAttachmentConfig, AttachmentStagingStore, AgentStore, Actor, ListActionProposals, ActionProposalView, ActionProposalMutationView, TextActionProposalVocabulary, AgentRunner, ModelCatalog, QuotaProvider, ActionApprovalMode, MessageAttachment, QuotaReport, ModelCatalogView, AgentHostContext, AttachmentRef, PageContext, ChatQueueState, SubscribeOptions, HumanReply, ThreadSummary, ThreadDetail, UpdateThreadInput, MessageFeedbackValue, MessageFeedback, StagedAttachment, AiUsageInput, AiUsageOptions, AiUsageResult, ActorResolver } from '@dudousxd/nestjs-agent-core';
3
3
  import { Request, Response } from 'express';
4
4
  import { UiCapabilities } from '@dudousxd/nestjs-agent-core/genui';
5
5
  import { b as AgentModuleOptions, A as AgentDepsFactory, C as ChatQueueService, d as AgentProtocolAdapter } from './chat-queue.service-YjiKVTKE.js';
@@ -167,6 +167,22 @@ interface ChatParams {
167
167
  interface ThreadDefaultAgentReader {
168
168
  defaultAgentForThread(threadId: string): Promise<string | null>;
169
169
  }
170
+ /** What {@link AgentService.forkThread} takes past the thread and the message to copy up to. */
171
+ interface ForkThreadParams {
172
+ /**
173
+ * The HOST already decided this actor may read the thread — "continue this conversation" from a
174
+ * link its owner shared. Skips the ownership check, and the copy belongs to the CALLER, not the
175
+ * source's owner; the original is left as it was. Never set it from a request's own claim: the
176
+ * `POST /threads/:id/fork-from/:messageId` route never does, so it stays owner-only.
177
+ */
178
+ authorized?: boolean;
179
+ /**
180
+ * The copy's tenant — see `ForkThreadOptions.tenantRef`. Absent keeps the SOURCE's, even when the
181
+ * copy now belongs to someone else; forking across tenants, pass the caller's
182
+ * (`actor.tenantRef ?? null`). `null` clears it.
183
+ */
184
+ tenantRef?: string | null;
185
+ }
170
186
  /** Like {@link ThreadDefaultAgentReader}, for the thread's pinned model. */
171
187
  interface ThreadModelReader {
172
188
  modelForThread(threadId: string): Promise<string | null>;
@@ -430,7 +446,12 @@ declare class AgentService {
430
446
  listThreads(actorRef: string): Promise<ThreadSummary[]>;
431
447
  getThread(actor: Actor, threadId: string): Promise<ThreadDetail | null>;
432
448
  deleteThread(actor: Actor, threadId: string): Promise<void>;
433
- forkThread(actor: Actor, threadId: string, fromMessageId: string): Promise<ThreadSummary>;
449
+ /**
450
+ * Copy a thread up to `fromMessageId` (inclusive) into a new one. By default only the thread's
451
+ * owner may, and the copy is theirs. With `params.authorized`, the host has vouched for `actor`
452
+ * (a share link it checked) and the copy is `actor`'s — see {@link ForkThreadParams}.
453
+ */
454
+ forkThread(actor: Actor, threadId: string, fromMessageId: string, params?: ForkThreadParams): Promise<ThreadSummary>;
434
455
  /** Kept for source compatibility — a thin wrapper over the more general `updateThread`. */
435
456
  renameThread(actor: Actor, threadId: string, title: string): Promise<void>;
436
457
  /**
@@ -567,6 +588,22 @@ declare class AgentService {
567
588
  private resolveRunForToolCall;
568
589
  /** Authorization seam for `cancel`: the caller must own the thread currently streaming this run. */
569
590
  private assertOwnsActiveStream;
591
+ /**
592
+ * Record AI spend that did not come from an agent turn — a transcription, a TTS render, a batch of
593
+ * embeddings, a direct OpenAI/OpenRouter SDK call — on the same ledger the agent's turns write, so
594
+ * cost, quota and the dashboard see one figure.
595
+ *
596
+ * Never throws into the caller's flow by default: a failed write or an invalid input is logged and
597
+ * answered with `{ recorded: false, error }`. Pass `{ throwOnError: true }` where losing the row
598
+ * must fail the caller.
599
+ *
600
+ * Counted against `actorRef` like an agent turn: the ledger is the budget (`LedgerQuotaProvider`),
601
+ * so its tokens count in the token windows (a row with no tokens adds 0 there) and its `costUsd` in
602
+ * the USD windows. A `QuotaStore` counter is bumped only where loop deps carry one (`AgentModule`
603
+ * wires none). `actorRef: null` is platform cost, recorded under `PLATFORM_ACTOR_REF` and counted
604
+ * against nobody.
605
+ */
606
+ recordUsage(input: AiUsageInput, options?: AiUsageOptions): Promise<AiUsageResult>;
570
607
  }
571
608
 
572
609
  interface A2uiAdapterOptions extends A2uiStreamOptions {
@@ -647,4 +684,4 @@ declare class A2uiRunHandler {
647
684
  */
648
685
  declare function a2uiAdapter(options?: A2uiAdapterOptions): AgentProtocolAdapter;
649
686
 
650
- export { AgentService as A, type ChatParams as C, DEFAULT_ALLOWED_ATTACHMENT_CONTENT_TYPES as D, MAX_ATTACHMENTS_PER_MESSAGE as M, NO_USER_MESSAGE_CODE as N, type QueuedSend as Q, type ThreadDefaultAgentReader as T, ActionProposalService as a, type A2uiAdapterOptions as b, A2uiRunHandler as c, type AttachmentLimits as d, type ChatSendMode as e, type ChatSendResult as f, DEFAULT_MAX_ATTACHMENT_BYTES as g, type ThreadModelReader as h, a2uiAdapter as i, attachmentLimits as j };
687
+ export { AgentService as A, type ChatParams as C, DEFAULT_ALLOWED_ATTACHMENT_CONTENT_TYPES as D, type ForkThreadParams as F, MAX_ATTACHMENTS_PER_MESSAGE as M, NO_USER_MESSAGE_CODE as N, type QueuedSend as Q, type ThreadDefaultAgentReader as T, ActionProposalService as a, type A2uiAdapterOptions as b, A2uiRunHandler as c, type AttachmentLimits as d, type ChatSendMode as e, type ChatSendResult as f, DEFAULT_MAX_ATTACHMENT_BYTES as g, type ThreadModelReader as h, a2uiAdapter as i, attachmentLimits as j };
package/dist/index.cjs CHANGED
@@ -1723,6 +1723,7 @@ function _ts_param4(paramIndex, decorator) {
1723
1723
  };
1724
1724
  }
1725
1725
  __name(_ts_param4, "_ts_param");
1726
+ var usageLogger = new import_common6.Logger("AgentService");
1726
1727
  var NO_USER_MESSAGE_CODE = "no_user_message";
1727
1728
  function sendableParams(params) {
1728
1729
  const message = params.message;
@@ -2550,9 +2551,28 @@ var AgentService2 = class {
2550
2551
  await this.assertOwnsThread(actor, threadId);
2551
2552
  return this.store.softDeleteThread(threadId);
2552
2553
  }
2553
- async forkThread(actor, threadId, fromMessageId) {
2554
- await this.assertOwnsThread(actor, threadId);
2555
- return this.store.forkThread(threadId, fromMessageId);
2554
+ /**
2555
+ * Copy a thread up to `fromMessageId` (inclusive) into a new one. By default only the thread's
2556
+ * owner may, and the copy is theirs. With `params.authorized`, the host has vouched for `actor`
2557
+ * (a share link it checked) and the copy is `actor`'s — see {@link ForkThreadParams}.
2558
+ */
2559
+ async forkThread(actor, threadId, fromMessageId, params = {}) {
2560
+ if (params.authorized === true) {
2561
+ if (await this.store.ownerOfThread(threadId) === null) {
2562
+ throw new import_common6.NotFoundException(`thread ${threadId} not found`);
2563
+ }
2564
+ } else {
2565
+ await this.assertOwnsThread(actor, threadId);
2566
+ }
2567
+ const options = {
2568
+ ...params.authorized === true ? {
2569
+ actorRef: actor.id
2570
+ } : {},
2571
+ ...params.tenantRef !== void 0 ? {
2572
+ tenantRef: params.tenantRef
2573
+ } : {}
2574
+ };
2575
+ return this.store.forkThread(threadId, fromMessageId, options);
2556
2576
  }
2557
2577
  /** Kept for source compatibility — a thin wrapper over the more general `updateThread`. */
2558
2578
  async renameThread(actor, threadId, title) {
@@ -2947,6 +2967,31 @@ var AgentService2 = class {
2947
2967
  throw new import_common6.ForbiddenException("run belongs to another actor");
2948
2968
  }
2949
2969
  }
2970
+ /**
2971
+ * Record AI spend that did not come from an agent turn — a transcription, a TTS render, a batch of
2972
+ * embeddings, a direct OpenAI/OpenRouter SDK call — on the same ledger the agent's turns write, so
2973
+ * cost, quota and the dashboard see one figure.
2974
+ *
2975
+ * Never throws into the caller's flow by default: a failed write or an invalid input is logged and
2976
+ * answered with `{ recorded: false, error }`. Pass `{ throwOnError: true }` where losing the row
2977
+ * must fail the caller.
2978
+ *
2979
+ * Counted against `actorRef` like an agent turn: the ledger is the budget (`LedgerQuotaProvider`),
2980
+ * so its tokens count in the token windows (a row with no tokens adds 0 there) and its `costUsd` in
2981
+ * the USD windows. A `QuotaStore` counter is bumped only where loop deps carry one (`AgentModule`
2982
+ * wires none). `actorRef: null` is platform cost, recorded under `PLATFORM_ACTOR_REF` and counted
2983
+ * against nobody.
2984
+ */
2985
+ recordUsage(input, options = {}) {
2986
+ return (0, import_nestjs_agent_core9.recordAiUsage)({
2987
+ store: this.store,
2988
+ quota: this.deps.forAgent().quota,
2989
+ day: utcDay(),
2990
+ input,
2991
+ options,
2992
+ warn: /* @__PURE__ */ __name((message) => usageLogger.warn(message), "warn")
2993
+ });
2994
+ }
2950
2995
  };
2951
2996
  AgentService2 = _ts_decorate5([
2952
2997
  (0, import_common6.Injectable)(),
@@ -6157,10 +6202,12 @@ var InlineAgentRunner = class _InlineAgentRunner {
6157
6202
  }
6158
6203
  async start(input, options = {}) {
6159
6204
  const runId = options.runId ?? crypto.randomUUID();
6205
+ const acceptedAt = Date.now();
6160
6206
  const day = input.day ?? utcDay();
6161
6207
  const deps = this.factory.forAgent(input.agentName);
6162
6208
  this.live.add(runId);
6163
6209
  const hooks = this.topLevelHooks({
6210
+ acceptedAt,
6164
6211
  runId,
6165
6212
  deps,
6166
6213
  actor: input.actor,
@@ -6182,8 +6229,9 @@ var InlineAgentRunner = class _InlineAgentRunner {
6182
6229
  const timed = durationMs !== void 0 ? {
6183
6230
  durationMs
6184
6231
  } : {};
6232
+ const measured = (0, import_nestjs_agent_core32.runEndTimings)(error);
6185
6233
  if (error instanceof import_nestjs_agent_core32.RunCancelledError) {
6186
- await this.settleCancelled(runId, input, deps, durationMs);
6234
+ await this.settleCancelled(runId, input, deps, durationMs, measured);
6187
6235
  return;
6188
6236
  }
6189
6237
  const message = error instanceof Error ? error.message : String(error);
@@ -6195,6 +6243,7 @@ var InlineAgentRunner = class _InlineAgentRunner {
6195
6243
  code,
6196
6244
  message,
6197
6245
  ...timed,
6246
+ ...measured,
6198
6247
  context: (0, import_nestjs_agent_core32.agentRunContext)(input)
6199
6248
  });
6200
6249
  await this.store.recordRunEnd?.({
@@ -6272,7 +6321,7 @@ var InlineAgentRunner = class _InlineAgentRunner {
6272
6321
  * the stream ENDED after a `cancelled` frame rather than failed, so a client that retries failed
6273
6322
  * streams does not retry this one.
6274
6323
  */
6275
- async settleCancelled(runId, input, deps, durationMs) {
6324
+ async settleCancelled(runId, input, deps, durationMs, measured = {}) {
6276
6325
  this.logger.log(`agent run ${runId} cancelled`);
6277
6326
  const timed = durationMs !== void 0 ? {
6278
6327
  durationMs
@@ -6281,6 +6330,7 @@ var InlineAgentRunner = class _InlineAgentRunner {
6281
6330
  runId,
6282
6331
  threadId: input.threadId,
6283
6332
  ...timed,
6333
+ ...measured,
6284
6334
  context: (0, import_nestjs_agent_core32.agentRunContext)(input)
6285
6335
  });
6286
6336
  const writer = await deps.sink.open(runId);
@@ -6373,9 +6423,10 @@ var InlineAgentRunner = class _InlineAgentRunner {
6373
6423
  });
6374
6424
  }
6375
6425
  topLevelHooks(args) {
6376
- const { runId, deps, actor, day, threadId, chainBelow, input } = args;
6426
+ const { runId, deps, actor, day, threadId, chainBelow, input, acceptedAt } = args;
6377
6427
  return {
6378
6428
  runId,
6429
+ queueMs: /* @__PURE__ */ __name(() => Date.now() - acceptedAt, "queueMs"),
6379
6430
  // The loop ends the stream when the turn completes. Just before, the thread passes to the next
6380
6431
  // queued message (or is released), and the reader is told which — so by the time a client
6381
6432
  // sees the run end, the thread is already free or already running what it queued.
@@ -6500,10 +6551,12 @@ var InlineAgentRunner = class _InlineAgentRunner {
6500
6551
  const timed = durationMs !== void 0 ? {
6501
6552
  durationMs
6502
6553
  } : {};
6554
+ const measured = (0, import_nestjs_agent_core32.runEndTimings)(error);
6503
6555
  const settled = {
6504
6556
  runId,
6505
6557
  threadId: subThread.id,
6506
6558
  ...timed,
6559
+ ...measured,
6507
6560
  context: (0, import_nestjs_agent_core32.agentRunContext)(childInput)
6508
6561
  };
6509
6562
  if (cancelled) {