@sweny-ai/core 0.1.191 → 0.1.193

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/dist/budget.d.ts CHANGED
@@ -59,7 +59,7 @@ export declare function spendOf(u: NodeUsage | undefined): {
59
59
  export declare function describeOverrun(o: BudgetOverrun, nodeId?: string): string;
60
60
  /**
61
61
  * Tracks spend for one run. `node()` opens a visit to a node; `attempt()` opens
62
- * one agent run inside it. Spend is committed when an attempt finishes.
62
+ * one agent run inside it. Spend is committed once, when an attempt settles.
63
63
  */
64
64
  export declare class BudgetGuard {
65
65
  readonly runLimits: SpendLimits;
@@ -102,6 +102,7 @@ export declare class BudgetAttempt {
102
102
  private live;
103
103
  private liveUsage;
104
104
  private stopped;
105
+ private settled;
105
106
  private readonly onOuterAbort;
106
107
  constructor(guard: BudgetGuard, node: NodeBudget, outer?: AbortSignal | undefined);
107
108
  get signal(): AbortSignal;
@@ -113,11 +114,20 @@ export declare class BudgetAttempt {
113
114
  readonly onUsage: (u: NodeUsage) => void;
114
115
  private check;
115
116
  /**
116
- * The attempt is over. Commit its spend (the larger of what the result
117
- * reports and what was seen live) and return the breach, if any: the live
118
- * stop, else a crossing visible only in the final usage.
117
+ * The attempt is over: commit its spend exactly once (#549). The spend is the
118
+ * larger of `finalUsage` and what was seen live, per unit. The breach (the
119
+ * live stop, else a crossing visible only in the final usage) is judged
120
+ * against the totals before this commit. Later calls change nothing and
121
+ * return the first call's breach, so the executor settles as soon as the
122
+ * harness returns, before any fallible work, and every exit path may settle
123
+ * again without double counting.
119
124
  */
125
+ settle(finalUsage?: NodeUsage): BudgetOverrun | undefined;
126
+ /** Settle with the attempt's final usage and return the breach, if any. */
120
127
  finish(finalUsage: NodeUsage | undefined): BudgetOverrun | undefined;
121
- /** Release the outer-signal listener when the run threw before `finish`. */
128
+ /**
129
+ * Every exit path ends here: an attempt that threw still commits what it
130
+ * reported live. A no-op once settled.
131
+ */
122
132
  dispose(): void;
123
133
  }
package/dist/budget.js CHANGED
@@ -69,7 +69,7 @@ export function describeOverrun(o, nodeId) {
69
69
  }
70
70
  /**
71
71
  * Tracks spend for one run. `node()` opens a visit to a node; `attempt()` opens
72
- * one agent run inside it. Spend is committed when an attempt finishes.
72
+ * one agent run inside it. Spend is committed once, when an attempt settles.
73
73
  */
74
74
  export class BudgetGuard {
75
75
  runLimits;
@@ -139,6 +139,7 @@ export class BudgetAttempt {
139
139
  live = { tokens: 0, costUsd: 0 };
140
140
  liveUsage;
141
141
  stopped;
142
+ settled;
142
143
  onOuterAbort = () => this.controller.abort();
143
144
  constructor(guard, node, outer) {
144
145
  this.guard = guard;
@@ -184,23 +185,37 @@ export class BudgetAttempt {
184
185
  breachOf("run", this.guard.runLimits, { tokens: r.tokens + attempt.tokens, costUsd: r.costUsd + attempt.costUsd }, true));
185
186
  }
186
187
  /**
187
- * The attempt is over. Commit its spend (the larger of what the result
188
- * reports and what was seen live) and return the breach, if any: the live
189
- * stop, else a crossing visible only in the final usage.
188
+ * The attempt is over: commit its spend exactly once (#549). The spend is the
189
+ * larger of `finalUsage` and what was seen live, per unit. The breach (the
190
+ * live stop, else a crossing visible only in the final usage) is judged
191
+ * against the totals before this commit. Later calls change nothing and
192
+ * return the first call's breach, so the executor settles as soon as the
193
+ * harness returns, before any fallible work, and every exit path may settle
194
+ * again without double counting.
190
195
  */
191
- finish(finalUsage) {
196
+ settle(finalUsage) {
197
+ if (this.settled)
198
+ return this.settled.breach;
192
199
  this.outer?.removeEventListener("abort", this.onOuterAbort);
193
200
  const f = spendOf(finalUsage);
194
201
  const spent = {
195
202
  tokens: Math.max(this.live.tokens, f.tokens ?? 0),
196
203
  costUsd: Math.max(this.live.costUsd, f.costUsd ?? 0),
197
204
  };
198
- const b = this.stopped ?? this.check(spent);
205
+ const breach = this.stopped ?? this.check(spent);
206
+ this.settled = { breach };
199
207
  this.node.commit(spent);
200
- return b;
208
+ return breach;
209
+ }
210
+ /** Settle with the attempt's final usage and return the breach, if any. */
211
+ finish(finalUsage) {
212
+ return this.settle(finalUsage);
201
213
  }
202
- /** Release the outer-signal listener when the run threw before `finish`. */
214
+ /**
215
+ * Every exit path ends here: an attempt that threw still commits what it
216
+ * reported live. A no-op once settled.
217
+ */
203
218
  dispose() {
204
- this.outer?.removeEventListener("abort", this.onOuterAbort);
219
+ this.settle();
205
220
  }
206
221
  }
package/dist/executor.js CHANGED
@@ -598,6 +598,8 @@ async function executeRun(workflow, input, options, results, trace, at) {
598
598
  safeObserve(observer, { type: "node:progress", node: currentId, message }, logger);
599
599
  },
600
600
  });
601
+ // Commit the attempt's spend before anything here can throw (#549); `finish` below reads the breach.
602
+ attemptBudget?.settle(result.usage);
601
603
  }
602
604
  catch (err) {
603
605
  if (journalFault)
@@ -608,8 +610,9 @@ async function executeRun(workflow, input, options, results, trace, at) {
608
610
  throw err;
609
611
  }
610
612
  finally {
611
- parentSignal?.removeEventListener("abort", onParentAbort);
613
+ // A throw from the harness still commits the spend it reported live; a no-op once settled.
612
614
  attemptBudget?.dispose();
615
+ parentSignal?.removeEventListener("abort", onParentAbort);
613
616
  }
614
617
  if (journalFault)
615
618
  throw journalFault;
@@ -632,8 +635,8 @@ async function executeRun(workflow, input, options, results, trace, at) {
632
635
  result = { ...result, degraded: [...new Set([...(result.degraded ?? []), ...budgetCheck.degraded])] };
633
636
  }
634
637
  if (attemptBudget) {
635
- // Commit this attempt's spend; a live stop, or a crossing visible only
636
- // in the final usage, fails the node whatever else it did.
638
+ // The spend was committed when the harness returned; a live stop, or a
639
+ // crossing visible only in the final usage, fails the node whatever else it did.
637
640
  const breach = attemptBudget.finish(result.usage);
638
641
  if (breach) {
639
642
  const error = describeOverrun(breach, currentId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sweny-ai/core",
3
- "version": "0.1.191",
3
+ "version": "0.1.193",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sweny": "./dist/cli/main.js"