@stigg/agent-sdk-js 0.7.5 → 0.7.6

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.
@@ -122,9 +122,14 @@ export type EntityPlacement = {
122
122
  /**
123
123
  * Set the budget only when the assignment does not exist yet. An existing one keeps its limit
124
124
  * and cadence, so a raise made in the dashboard survives the next run. Mutually exclusive with
125
- * `usageLimit` - it costs one extra read, which is why it is not the default.
125
+ * `usageLimit` - it costs one extra read, which is why a number is never seeded unless asked for.
126
+ *
127
+ * `null` seeds an unlimited assignment - the metering-only one, which puts the agent in the tree
128
+ * and tracks its usage without its own assignment refusing it. It is what a budget with no limit
129
+ * sends, so that case pays the read on every call. Unlike a numeric seed it still applies the
130
+ * caller's `cadence` to an existing assignment, as a budget with no limit always has.
126
131
  */
127
- usageLimitOnCreate?: number;
132
+ usageLimitOnCreate?: number | null;
128
133
  cadence?: string;
129
134
  /** The entities the budget is scoped to - its own assignment beside the agent's whole one. */
130
135
  scopeEntityIds?: string[];
@@ -16,6 +16,8 @@ const errors_1 = require("../errors");
16
16
  const types_1 = require("../types");
17
17
  const sdk_identity_1 = require("../sdk-identity");
18
18
  const report_1 = require("./report");
19
+ /** The window an unlimited assignment is created with when the caller named none. */
20
+ const TRACKING_CADENCE = 'P1M';
19
21
  /** The governance check endpoint is `/api/v1-beta/` and flagged experimental. */
20
22
  class RestGovernanceEngine {
21
23
  /**
@@ -116,11 +118,16 @@ class RestGovernanceEngine {
116
118
  const scope = scopeOf(placement.scopeEntityIds);
117
119
  // Every page: the one with this scope can sit past the first, and missing it would overwrite
118
120
  // an admin's limit with the seed.
121
+ const tracking = placement.usageLimitOnCreate === null;
119
122
  for await (const row of this.client.v1Beta.customers.assignments.list(customerId, { entityId, ...capability })) {
120
- // Already assigned: leave the limit and its window exactly as they are.
123
+ // Already assigned: leave the limit and its window exactly as they are - except that a
124
+ // metering-only call still moves the window it named, as a budget with no limit always did.
121
125
  if (scopeOf(row.scopeEntityIds) === scope)
122
- return {};
126
+ return tracking ? cadence : {};
123
127
  }
128
+ // Governance requires a window on create, and a metering-only caller has no reason to pick one.
129
+ if (tracking)
130
+ return { usageLimit: null, cadence: placement.cadence ?? TRACKING_CADENCE };
124
131
  return { usageLimit: placement.usageLimitOnCreate, ...cadence };
125
132
  }
126
133
  async check(customerId, { entitlement, requestedUsage, dimensions }) {
@@ -152,7 +152,8 @@ function toPlacement(input) {
152
152
  if (onCreate && typeof input.budget.limit !== 'number') {
153
153
  throw new Error(`Agent ${input.agentId} asked to seed a budget with no limit to seed. ` +
154
154
  "`budget.apply: 'on-create'` writes a cap the first time and never again, so there is " +
155
- 'nothing to write without `budget.limit`.');
155
+ 'nothing to write without `budget.limit`. To seed an unlimited, metering-only budget, ' +
156
+ 'leave out both `limit` and `apply`.');
156
157
  }
157
158
  // Each entry names an entity, and a blank one names none: the budget would be scoped to an
158
159
  // entity no request resolves, and apply to nothing.
@@ -160,12 +161,15 @@ function toPlacement(input) {
160
161
  throw new Error(`Agent ${input.agentId} has a budget.scope that is not a list of entity ids: ${JSON.stringify(scope)}. ` +
161
162
  'Leave it out, or pass [], for the whole budget.');
162
163
  }
164
+ // No limit at all is metering only: an agent with no assignment is in nobody's tree and its usage
165
+ // is counted against nothing, so it gets an unlimited one - seeded, so a cap set later survives.
166
+ const tracking = !onCreate && input.budget.limit === undefined;
163
167
  return {
164
168
  parentId,
165
169
  entitlement,
166
170
  cadence,
167
171
  usageLimit: onCreate ? undefined : input.budget.limit,
168
- usageLimitOnCreate: onCreate ? (input.budget.limit ?? undefined) : undefined,
172
+ usageLimitOnCreate: tracking ? null : onCreate ? (input.budget.limit ?? undefined) : undefined,
169
173
  scopeEntityIds: scope?.length ? scope : undefined,
170
174
  };
171
175
  }
package/dist/types.d.ts CHANGED
@@ -509,10 +509,21 @@ export type AgentBudget = {
509
509
  * A scoped budget governs only the requests it names: one naming no model, or a model with no
510
510
  * budget, is limited by the agent's whole budget and its ancestors' alone - with neither, by
511
511
  * nothing. Register the whole budget too.
512
+ *
513
+ * A scope with no `limit` meters that scope only: it is created unlimited, like a whole budget
514
+ * with no limit, so it shows that scope's usage and refuses nothing.
512
515
  */
513
516
  scope?: string[];
514
517
  } & ({
515
- /** The limit, written on every call. `null` clears whatever is assigned. */
518
+ /**
519
+ * The limit, written on every call. `null` clears whatever is assigned.
520
+ *
521
+ * Omitted, the agent is metering only: the first call creates an unlimited assignment, so its
522
+ * usage is tracked in the governance tree and its own assignment never refuses it - its
523
+ * ancestors' budgets and the customer's entitlement still can. Later calls leave the limit
524
+ * assigned by then - a cap set in the dashboard included - untouched, though a `cadence` they
525
+ * pass is still applied. `cadence` defaults to `'P1M'` on that create.
526
+ */
516
527
  limit?: number | null;
517
528
  /** Writes the limit every time. The default. */
518
529
  apply?: 'always';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stigg/agent-sdk-js",
3
- "version": "0.7.5",
3
+ "version": "0.7.6",
4
4
  "description": "Stigg agent SDK (JS) - run-lifecycle governance for agentic stacks, with examples per layer",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",