@dudousxd/nestjs-agent 1.12.1 → 1.13.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/README.md CHANGED
@@ -297,7 +297,8 @@ through `AgentService.subscribeAs(actor, runId)`.
297
297
  ### Resuming a dropped stream
298
298
 
299
299
  Every event frame of `POST /agent/chat` and `GET /agent/chat/:runId/stream` carries an SSE `id:` —
300
- its 1-based position in the run, the same on every attach. `GET …/stream?after=<n>` (or the
300
+ its 1-based position in the run, the same on every attach (a runner of your own may number with gaps,
301
+ as long as ids only increase — see *Numbering a stream you rebuild* in the protocol doc). `GET …/stream?after=<n>` (or the
301
302
  `Last-Event-ID` header) skips what a reconnecting client already has; the React transport uses it to
302
303
  continue a message after a network drop. See docs/stream-protocol.md.
303
304
 
@@ -346,8 +347,9 @@ turn rather than answering on another. `staticModelCatalog(view)` wraps a fixed
346
347
 
347
348
  ### Budgets: `GET /agent/quota`
348
349
 
349
- `GET /agent/quota` reports `{ windows: [{ period: 'day' | 'month', usedTokens, limitTokens?,
350
- usedUsd, limitUsd?, resetsAt? }], blocked? }` from a `QuotaProvider`. The default,
350
+ `GET /agent/quota` reports `{ windows: [{ period: 'day' | 'month', usedTokens?, limitTokens?,
351
+ usedUsd, limitUsd?, resetsAt?, warnAt? }], blocked?, warning? }` from a `QuotaProvider` (a USD-only
352
+ budget leaves `usedTokens` out; `quota: { limits, warnAt: 0.8 }` adds a soft-limit `warning`). The default,
351
353
  `LedgerQuotaProvider`, reads the usage ledger (day, plus month when the store has `usageBetween`)
352
354
  and only reports. Configure it and it also gates sends — a `blocked` report refuses `POST
353
355
  /agent/chat` with `429 { code: 'quota_exceeded', period }` before the turn starts:
@@ -103,6 +103,14 @@ interface QuotaLimits {
103
103
  day?: QuotaWindowLimits;
104
104
  month?: QuotaWindowLimits;
105
105
  }
106
+ /** {@link LedgerQuotaProvider}'s tuning beyond the ceilings. */
107
+ interface LedgerQuotaOptions {
108
+ /**
109
+ * The soft limit: the share of a ceiling (`0..1`) past which the report carries a `warning`.
110
+ * Stamped on every window that has a ceiling. Omit → no warnings.
111
+ */
112
+ warnAt?: number;
113
+ }
106
114
  /**
107
115
  * The default {@link QuotaProvider}: windows read off the usage ledger the loop already writes.
108
116
  *
@@ -115,7 +123,8 @@ interface QuotaLimits {
115
123
  declare class LedgerQuotaProvider implements QuotaProvider {
116
124
  private readonly store;
117
125
  private readonly limits;
118
- constructor(store: AgentStore, limits?: QuotaLimits);
126
+ private readonly options;
127
+ constructor(store: AgentStore, limits?: QuotaLimits, options?: LedgerQuotaOptions);
119
128
  report(query: QuotaQuery): Promise<QuotaReport>;
120
129
  private dayWindow;
121
130
  private window;
@@ -233,7 +242,8 @@ interface AgentModuleOptions {
233
242
  sink?: TokenStreamSink;
234
243
  /**
235
244
  * The caller's budget. `{ limits: { day?: { tokens?, usd? }, month?: { tokens?, usd? } } }` puts
236
- * ceilings on the built-in ledger provider; a {@link QuotaProvider} of your own replaces it. Either
245
+ * ceilings on the built-in ledger provider, and `warnAt` (`0..1`, e.g. `0.8`) a soft limit past
246
+ * which the report carries a `warning`; a {@link QuotaProvider} of your own replaces it. Either
237
247
  * way `GET <base>/quota` reports it and a send whose report comes back `blocked` is refused with
238
248
  * `429` before the turn starts. Omit → reported (usage off the ledger), never enforced — for a
239
249
  * public (anonymous) deployment that means unbounded model spend, so set limits there; they apply
@@ -241,6 +251,7 @@ interface AgentModuleOptions {
241
251
  */
242
252
  quota?: {
243
253
  limits: QuotaLimits;
254
+ warnAt?: number;
244
255
  } | QuotaProvider;
245
256
  /**
246
257
  * Which models a caller may pick — `GET <base>/models`, a send's `model`, a thread's pinned model
@@ -103,6 +103,14 @@ interface QuotaLimits {
103
103
  day?: QuotaWindowLimits;
104
104
  month?: QuotaWindowLimits;
105
105
  }
106
+ /** {@link LedgerQuotaProvider}'s tuning beyond the ceilings. */
107
+ interface LedgerQuotaOptions {
108
+ /**
109
+ * The soft limit: the share of a ceiling (`0..1`) past which the report carries a `warning`.
110
+ * Stamped on every window that has a ceiling. Omit → no warnings.
111
+ */
112
+ warnAt?: number;
113
+ }
106
114
  /**
107
115
  * The default {@link QuotaProvider}: windows read off the usage ledger the loop already writes.
108
116
  *
@@ -115,7 +123,8 @@ interface QuotaLimits {
115
123
  declare class LedgerQuotaProvider implements QuotaProvider {
116
124
  private readonly store;
117
125
  private readonly limits;
118
- constructor(store: AgentStore, limits?: QuotaLimits);
126
+ private readonly options;
127
+ constructor(store: AgentStore, limits?: QuotaLimits, options?: LedgerQuotaOptions);
119
128
  report(query: QuotaQuery): Promise<QuotaReport>;
120
129
  private dayWindow;
121
130
  private window;
@@ -233,7 +242,8 @@ interface AgentModuleOptions {
233
242
  sink?: TokenStreamSink;
234
243
  /**
235
244
  * The caller's budget. `{ limits: { day?: { tokens?, usd? }, month?: { tokens?, usd? } } }` puts
236
- * ceilings on the built-in ledger provider; a {@link QuotaProvider} of your own replaces it. Either
245
+ * ceilings on the built-in ledger provider, and `warnAt` (`0..1`, e.g. `0.8`) a soft limit past
246
+ * which the report carries a `warning`; a {@link QuotaProvider} of your own replaces it. Either
237
247
  * way `GET <base>/quota` reports it and a send whose report comes back `blocked` is refused with
238
248
  * `429` before the turn starts. Omit → reported (usage off the ledger), never enforced — for a
239
249
  * public (anonymous) deployment that means unbounded model spend, so set limits there; they apply
@@ -241,6 +251,7 @@ interface AgentModuleOptions {
241
251
  */
242
252
  quota?: {
243
253
  limits: QuotaLimits;
254
+ warnAt?: number;
244
255
  } | QuotaProvider;
245
256
  /**
246
257
  * Which models a caller may pick — `GET <base>/models`, a send's `model`, a thread's pinned model
@@ -1219,8 +1219,22 @@ var AgentService = class {
1219
1219
  }
1220
1220
  return entry.id;
1221
1221
  }
1222
- /** The model a turn runs on: the send's own, else the thread's pinned one, else none. */
1222
+ /**
1223
+ * The model a turn runs on: the send's own (that turn only — it is never stored on the thread),
1224
+ * else the thread's pinned one, else none. An agent the catalog locks to one model runs on it
1225
+ * whatever was sent or pinned, and a send naming another model is refused.
1226
+ */
1223
1227
  async resolveModel(actor, agent, requested, threadId) {
1228
+ const locked = this.models !== void 0 ? (await this.models.list({
1229
+ actor,
1230
+ agent
1231
+ })).locked : void 0;
1232
+ if (locked !== void 0) {
1233
+ if (requested !== void 0 && requested !== locked.model) {
1234
+ throw new import_common6.BadRequestException(`model "${requested}" cannot be selected: ${locked.reason ?? `this agent always uses "${locked.model}"`}`);
1235
+ }
1236
+ return locked.model;
1237
+ }
1224
1238
  const model = requested ?? (threadId !== void 0 ? await this.threadModel(threadId) : null);
1225
1239
  if (model === null || model === void 0) {
1226
1240
  return void 0;
@@ -1369,12 +1383,15 @@ var AgentService = class {
1369
1383
  * "the user picked exactly the defaults" persist as the same answers, and no client has to
1370
1384
  * re-send what it was shown.
1371
1385
  */
1372
- async answer(actor, toolCallId2, answers2 = {}) {
1386
+ async answer(actor, toolCallId2, answers2 = {}, opts = {}) {
1373
1387
  await this.assertOwnsToolCall(actor, toolCallId2);
1374
1388
  await this.assertAnswersFit(toolCallId2, answers2);
1375
1389
  const reply = {
1376
1390
  answers: answers2,
1377
- answeredByRef: actor.id
1391
+ answeredByRef: actor.id,
1392
+ ...opts.via !== void 0 ? {
1393
+ answeredVia: opts.via
1394
+ } : {}
1378
1395
  };
1379
1396
  return this.signalToolCall(toolCallId2, reply);
1380
1397
  }
@@ -1383,12 +1400,15 @@ var AgentService = class {
1383
1400
  * them: the run records this as a rejection, so a reader auditing what the agent was told can
1384
1401
  * tell a choice the user made from one they refused to make.
1385
1402
  */
1386
- async skip(actor, toolCallId2) {
1403
+ async skip(actor, toolCallId2, opts = {}) {
1387
1404
  await this.assertOwnsToolCall(actor, toolCallId2);
1388
1405
  const reply = {
1389
1406
  answers: {},
1390
1407
  skipped: true,
1391
- answeredByRef: actor.id
1408
+ answeredByRef: actor.id,
1409
+ ...opts.via !== void 0 ? {
1410
+ answeredVia: opts.via
1411
+ } : {}
1392
1412
  };
1393
1413
  return this.signalToolCall(toolCallId2, reply);
1394
1414
  }
@@ -3250,7 +3270,9 @@ var ToolCallController = class {
3250
3270
  */
3251
3271
  async answer(req, body) {
3252
3272
  const actor = await this.actorResolver.resolve(req);
3253
- await this.agent.answer(actor, toolCallId(body.toolCallId), answers(body.answers));
3273
+ await this.agent.answer(actor, toolCallId(body.toolCallId), answers(body.answers), {
3274
+ via: via(body.via)
3275
+ });
3254
3276
  return {
3255
3277
  ok: true
3256
3278
  };
@@ -3258,7 +3280,9 @@ var ToolCallController = class {
3258
3280
  /** Decline to answer and let the agent proceed on the answers it pre-picked. */
3259
3281
  async skip(req, body) {
3260
3282
  const actor = await this.actorResolver.resolve(req);
3261
- await this.agent.skip(actor, toolCallId(body.toolCallId));
3283
+ await this.agent.skip(actor, toolCallId(body.toolCallId), {
3284
+ via: via(body.via)
3285
+ });
3262
3286
  return {
3263
3287
  ok: true
3264
3288
  };
@@ -3894,9 +3918,11 @@ var LedgerQuotaProvider = class {
3894
3918
  }
3895
3919
  store;
3896
3920
  limits;
3897
- constructor(store, limits = {}) {
3921
+ options;
3922
+ constructor(store, limits = {}, options = {}) {
3898
3923
  this.store = store;
3899
3924
  this.limits = limits;
3925
+ this.options = options;
3900
3926
  }
3901
3927
  async report(query) {
3902
3928
  const now = query.now ?? /* @__PURE__ */ new Date();
@@ -3910,10 +3936,14 @@ var LedgerQuotaProvider = class {
3910
3936
  windows.push(this.window("month", used.usedTokens, used.costUsd, range.resetsAt));
3911
3937
  }
3912
3938
  const blocked = (0, import_nestjs_agent_core25.exhaustedWindow)(windows);
3939
+ const warning = (0, import_nestjs_agent_core25.quotaWarning)(windows);
3913
3940
  return {
3914
3941
  windows,
3915
3942
  ...blocked !== void 0 ? {
3916
3943
  blocked
3944
+ } : {},
3945
+ ...warning !== void 0 ? {
3946
+ warning
3917
3947
  } : {}
3918
3948
  };
3919
3949
  }
@@ -3934,7 +3964,10 @@ var LedgerQuotaProvider = class {
3934
3964
  ...limits.usd !== void 0 ? {
3935
3965
  limitUsd: limits.usd
3936
3966
  } : {},
3937
- resetsAt
3967
+ resetsAt,
3968
+ ...this.options.warnAt !== void 0 && (limits.tokens !== void 0 || limits.usd !== void 0) ? {
3969
+ warnAt: this.options.warnAt
3970
+ } : {}
3938
3971
  };
3939
3972
  }
3940
3973
  };
@@ -4539,7 +4572,9 @@ function sharedProviders(durable) {
4539
4572
  // `quota` is either a provider of the host's own or ceilings for the built-in ledger one;
4540
4573
  // omitted, the ledger provider still reports usage, with no ceiling to block on.
4541
4574
  provide: import_nestjs_agent_core28.AGENT_QUOTA_PROVIDER,
4542
- useFactory: /* @__PURE__ */ __name((o, store) => isQuotaProvider(o.quota) ? o.quota : new LedgerQuotaProvider(store, o.quota?.limits), "useFactory"),
4575
+ useFactory: /* @__PURE__ */ __name((o, store) => isQuotaProvider(o.quota) ? o.quota : new LedgerQuotaProvider(store, o.quota?.limits, o.quota?.warnAt !== void 0 ? {
4576
+ warnAt: o.quota.warnAt
4577
+ } : {}), "useFactory"),
4543
4578
  inject: [
4544
4579
  import_nestjs_agent_core28.AGENT_OPTIONS,
4545
4580
  import_nestjs_agent_core28.AGENT_STORE