@dudousxd/nestjs-agent 1.12.1 → 1.14.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
  };
@@ -3360,19 +3384,34 @@ var ToolsController = class {
3360
3384
  this.agents = agents;
3361
3385
  }
3362
3386
  async list(req, agent) {
3363
- if (agent !== void 0 && this.agents.get(agent) === void 0) {
3387
+ if (agent !== void 0 && agent !== import_nestjs_agent_core20.ALL_AGENTS && this.agents.get(agent) === void 0) {
3364
3388
  throw new import_common19.NotFoundException(`No agent named "${agent}"`);
3365
3389
  }
3366
3390
  const actor = await this.actorResolver.resolve(req);
3367
- const deps = this.depsFactory.forAgent(agent);
3368
- const specs = await deps.registry.visibleSpecs(actor, deps.rolesPolicy, deps.toolAllowList);
3369
- return specs.map((spec) => ({
3370
- name: spec.name,
3371
- kind: spec.kind,
3372
- ...spec.presentation !== void 0 ? {
3373
- presentation: spec.presentation
3374
- } : {}
3375
- }));
3391
+ const names = agent === import_nestjs_agent_core20.ALL_AGENTS ? [
3392
+ void 0,
3393
+ ...this.agents.list().map((definition) => definition.name)
3394
+ ] : [
3395
+ agent
3396
+ ];
3397
+ const entries = /* @__PURE__ */ new Map();
3398
+ for (const name of names) {
3399
+ const deps = this.depsFactory.forAgent(name);
3400
+ const specs = await deps.registry.visibleSpecs(actor, deps.rolesPolicy, deps.toolAllowList);
3401
+ for (const spec of specs) {
3402
+ if (entries.has(spec.name)) continue;
3403
+ entries.set(spec.name, {
3404
+ name: spec.name,
3405
+ kind: spec.kind,
3406
+ ...spec.presentation !== void 0 ? {
3407
+ presentation: spec.presentation
3408
+ } : {}
3409
+ });
3410
+ }
3411
+ }
3412
+ return [
3413
+ ...entries.values()
3414
+ ];
3376
3415
  }
3377
3416
  };
3378
3417
  _ts_decorate19([
@@ -3894,9 +3933,11 @@ var LedgerQuotaProvider = class {
3894
3933
  }
3895
3934
  store;
3896
3935
  limits;
3897
- constructor(store, limits = {}) {
3936
+ options;
3937
+ constructor(store, limits = {}, options = {}) {
3898
3938
  this.store = store;
3899
3939
  this.limits = limits;
3940
+ this.options = options;
3900
3941
  }
3901
3942
  async report(query) {
3902
3943
  const now = query.now ?? /* @__PURE__ */ new Date();
@@ -3910,10 +3951,14 @@ var LedgerQuotaProvider = class {
3910
3951
  windows.push(this.window("month", used.usedTokens, used.costUsd, range.resetsAt));
3911
3952
  }
3912
3953
  const blocked = (0, import_nestjs_agent_core25.exhaustedWindow)(windows);
3954
+ const warning = (0, import_nestjs_agent_core25.quotaWarning)(windows);
3913
3955
  return {
3914
3956
  windows,
3915
3957
  ...blocked !== void 0 ? {
3916
3958
  blocked
3959
+ } : {},
3960
+ ...warning !== void 0 ? {
3961
+ warning
3917
3962
  } : {}
3918
3963
  };
3919
3964
  }
@@ -3934,7 +3979,10 @@ var LedgerQuotaProvider = class {
3934
3979
  ...limits.usd !== void 0 ? {
3935
3980
  limitUsd: limits.usd
3936
3981
  } : {},
3937
- resetsAt
3982
+ resetsAt,
3983
+ ...this.options.warnAt !== void 0 && (limits.tokens !== void 0 || limits.usd !== void 0) ? {
3984
+ warnAt: this.options.warnAt
3985
+ } : {}
3938
3986
  };
3939
3987
  }
3940
3988
  };
@@ -4539,7 +4587,9 @@ function sharedProviders(durable) {
4539
4587
  // `quota` is either a provider of the host's own or ceilings for the built-in ledger one;
4540
4588
  // omitted, the ledger provider still reports usage, with no ceiling to block on.
4541
4589
  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"),
4590
+ useFactory: /* @__PURE__ */ __name((o, store) => isQuotaProvider(o.quota) ? o.quota : new LedgerQuotaProvider(store, o.quota?.limits, o.quota?.warnAt !== void 0 ? {
4591
+ warnAt: o.quota.warnAt
4592
+ } : {}), "useFactory"),
4543
4593
  inject: [
4544
4594
  import_nestjs_agent_core28.AGENT_OPTIONS,
4545
4595
  import_nestjs_agent_core28.AGENT_STORE