@sweny-ai/core 0.1.199 → 0.1.201
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 +77 -11
- package/dist/budget.js +132 -16
- package/dist/cli/config.js +5 -2
- package/dist/cli/deprecated-flags.d.ts +35 -0
- package/dist/cli/deprecated-flags.js +44 -0
- package/dist/cli/renderer.js +5 -0
- package/dist/cli/run-history.d.ts +12 -0
- package/dist/cli/run-history.js +16 -0
- package/dist/cli/run-output.d.ts +16 -1
- package/dist/cli/run-output.js +46 -4
- package/dist/cli/runs.js +38 -1
- package/dist/cli/ticket.js +7 -2
- package/dist/eval/index.d.ts +10 -0
- package/dist/eval/index.js +27 -6
- package/dist/eval/judge.d.ts +3 -0
- package/dist/eval/judge.js +12 -2
- package/dist/executor.js +276 -31
- package/dist/harness/acp.js +4 -1
- package/dist/harness/claude-code.d.ts +3 -1
- package/dist/harness/claude-code.js +4 -1
- package/dist/harness/codex.js +3 -1
- package/dist/harness/compat.js +1 -0
- package/dist/harness/index.d.ts +1 -1
- package/dist/harness/index.js +1 -1
- package/dist/harness/pi.js +3 -1
- package/dist/harness/prompts.d.ts +11 -1
- package/dist/harness/prompts.js +21 -2
- package/dist/harness/types.d.ts +9 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- package/dist/journal.d.ts +48 -10
- package/dist/journal.js +164 -16
- package/dist/safe-outputs.d.ts +9 -0
- package/dist/safe-outputs.js +41 -26
- package/dist/skills/linear.js +3 -1
- package/dist/studio.js +2 -1
- package/dist/types.d.ts +34 -1
- package/package.json +1 -1
package/dist/budget.d.ts
CHANGED
|
@@ -8,8 +8,14 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Tokens are input + output tokens as the harness reports them (cache reads and
|
|
10
10
|
* writes are not counted, matching the run receipt). Cost is the harness's own
|
|
11
|
-
* reported USD, never estimated.
|
|
12
|
-
*
|
|
11
|
+
* reported USD, never estimated. Every model completion the executor itself
|
|
12
|
+
* makes is counted too, under its own `kind` (see {@link SpendKind}): judge
|
|
13
|
+
* scoring, route evaluation, retry reflection and the safe-output screen. A
|
|
14
|
+
* completion counts when its harness reports usage for it; one that reports
|
|
15
|
+
* none adds nothing (the same as a node run without usage). The run totals
|
|
16
|
+
* and the stop are over all kinds together, across a resume too; the split is
|
|
17
|
+
* `trace.spend`. Once a ceiling is reached no further executor completion
|
|
18
|
+
* starts. The decision model (`route_by: decider`) is not metered.
|
|
13
19
|
*
|
|
14
20
|
* Enforcement, per unit the harness can report:
|
|
15
21
|
* - live: the harness calls `onUsage` during the node; crossing a limit aborts
|
|
@@ -17,7 +23,9 @@
|
|
|
17
23
|
* - between nodes: usage on the finished result is checked; a crossing fails the
|
|
18
24
|
* node, and the run halts before the next node starts.
|
|
19
25
|
* - across a resume: the run journal records each attempt's spend (live and
|
|
20
|
-
* final), and
|
|
26
|
+
* final), and each metered completion (judge, route, reflection, screen)
|
|
27
|
+
* as its own record; `sweny workflow resume` seeds the run total with all
|
|
28
|
+
* of it, split by kind.
|
|
21
29
|
*/
|
|
22
30
|
import type { NodeUsage } from "./types.js";
|
|
23
31
|
/** The `budget` field of a node or workflow. */
|
|
@@ -36,6 +44,20 @@ export interface Spend {
|
|
|
36
44
|
tokens: number;
|
|
37
45
|
costUsd: number;
|
|
38
46
|
}
|
|
47
|
+
/** Spend restored on a resume: the total, and its split by kind when the journal has one. */
|
|
48
|
+
export interface SeedSpend extends Spend {
|
|
49
|
+
byKind?: Partial<Record<SpendKind, Spend>>;
|
|
50
|
+
}
|
|
51
|
+
/** What a unit of spend paid for. `node` is an agent node run; the rest are the executor's own completions. */
|
|
52
|
+
export type SpendKind = "node" | "judge" | "route" | "reflect" | "screen";
|
|
53
|
+
/** The order kinds are listed in, wherever the split is shown. */
|
|
54
|
+
export declare const SPEND_KINDS: readonly SpendKind[];
|
|
55
|
+
/** Spend of one {@link SpendKind}. `calls` counts the completions that reported usage. */
|
|
56
|
+
export interface KindSpend extends Spend {
|
|
57
|
+
calls: number;
|
|
58
|
+
}
|
|
59
|
+
/** Spend by kind. A kind with no reported usage is absent. */
|
|
60
|
+
export type SpendBreakdown = Partial<Record<SpendKind, KindSpend>>;
|
|
39
61
|
export type BudgetUnit = "tokens" | "cost_usd";
|
|
40
62
|
/** Recorded on the node whose spend crossed a budget (`NodeResult.budget`). */
|
|
41
63
|
export interface BudgetOverrun {
|
|
@@ -45,6 +67,18 @@ export interface BudgetOverrun {
|
|
|
45
67
|
limit: number;
|
|
46
68
|
/** Spend at the moment of the stop (live) or at the end of the node. */
|
|
47
69
|
spent: number;
|
|
70
|
+
/** The executor completion whose spend crossed it (judge, route, ...). Absent for a node run. */
|
|
71
|
+
kind?: SpendKind;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Thrown into an executor completion (judge, route, reflection, screen) the
|
|
75
|
+
* budget refuses: a ceiling was already reached, so the call is not made.
|
|
76
|
+
* Callers that swallow a failed completion (a judge's parse retry) must let
|
|
77
|
+
* this one through, so a refusal is never mistaken for a model answer.
|
|
78
|
+
*/
|
|
79
|
+
export declare class BudgetRefusal extends Error {
|
|
80
|
+
readonly breach: BudgetOverrun;
|
|
81
|
+
constructor(breach: BudgetOverrun);
|
|
48
82
|
}
|
|
49
83
|
export declare function toLimits(b: Budget | undefined): SpendLimits;
|
|
50
84
|
/** The tighter of two limit sets, per unit. */
|
|
@@ -64,21 +98,43 @@ export declare function describeOverrun(o: BudgetOverrun, nodeId?: string): stri
|
|
|
64
98
|
export declare class BudgetGuard {
|
|
65
99
|
readonly runLimits: SpendLimits;
|
|
66
100
|
private spent;
|
|
101
|
+
private kinds;
|
|
67
102
|
constructor(runLimits: SpendLimits);
|
|
68
103
|
get totals(): Spend;
|
|
104
|
+
/**
|
|
105
|
+
* Spend by kind, in {@link SPEND_KINDS} order. Resumed spend is restored
|
|
106
|
+
* under its kind; a journal without meter records seeds `node` only.
|
|
107
|
+
*/
|
|
108
|
+
breakdown(): SpendBreakdown;
|
|
109
|
+
/** True once any completion other than a node run reported usage. */
|
|
110
|
+
get hasMeteredCalls(): boolean;
|
|
111
|
+
/**
|
|
112
|
+
* Count one executor completion (judge, route, ...) against the run. Usage
|
|
113
|
+
* with no tokens and no cost counts nothing. Returns the breach this report
|
|
114
|
+
* caused, judged against the totals before it, like a live node report.
|
|
115
|
+
*/
|
|
116
|
+
meter(kind: SpendKind, usage: NodeUsage | undefined, nodeBudget?: NodeBudget): BudgetOverrun | undefined;
|
|
69
117
|
/** True when any limit exists at all, so the executor can skip the machinery. */
|
|
70
118
|
active(nodeLimits: SpendLimits): boolean;
|
|
71
119
|
/** The run ceiling is already reached: no further attempt may start. */
|
|
72
120
|
runExhausted(): BudgetOverrun | undefined;
|
|
73
121
|
node(nodeLimits: SpendLimits): NodeBudget;
|
|
74
122
|
/**
|
|
75
|
-
* Spend the run made before this process (a resumed run's journaled usage
|
|
76
|
-
* Counted against the run ceiling like any other
|
|
77
|
-
* resets the whole-run budget.
|
|
123
|
+
* Spend the run made before this process (a resumed run's journaled usage
|
|
124
|
+
* and metered completions). Counted against the run ceiling like any other
|
|
125
|
+
* spend, so a crash never resets the whole-run budget. Restored under its
|
|
126
|
+
* kind when the journal has the split (a journal written before meter
|
|
127
|
+
* records existed has node spend only). Seeded spend is not a call: `calls`
|
|
128
|
+
* is unchanged.
|
|
78
129
|
*/
|
|
79
|
-
seed(s:
|
|
80
|
-
/**
|
|
81
|
-
|
|
130
|
+
seed(s: SeedSpend): void;
|
|
131
|
+
/**
|
|
132
|
+
* @internal `counted`: this commit is one completion that reported usage, so
|
|
133
|
+
* `calls` goes up. A commit that is not (seeded spend, an attempt that
|
|
134
|
+
* reported nothing) adds its spend only, and a zero one leaves no trace, so
|
|
135
|
+
* the split is the same with and without a limit.
|
|
136
|
+
*/
|
|
137
|
+
commit(s: Spend, kind: SpendKind, counted: boolean): void;
|
|
82
138
|
}
|
|
83
139
|
export declare class NodeBudget {
|
|
84
140
|
private readonly guard;
|
|
@@ -88,10 +144,18 @@ export declare class NodeBudget {
|
|
|
88
144
|
/** A limit is already reached before the next attempt (a retry after a spend-heavy attempt, or a spent run). */
|
|
89
145
|
exhausted(): BudgetOverrun | undefined;
|
|
90
146
|
attempt(outer?: AbortSignal): BudgetAttempt;
|
|
147
|
+
/**
|
|
148
|
+
* Spend this visit made before a resume (a checkpointed visit's journaled
|
|
149
|
+
* spend). It counts against the node's own limit only: the run total
|
|
150
|
+
* already holds it from the seed.
|
|
151
|
+
*/
|
|
152
|
+
restore(s: Spend): void;
|
|
91
153
|
/** @internal */
|
|
92
154
|
get committed(): Spend;
|
|
93
|
-
/**
|
|
94
|
-
|
|
155
|
+
/** Count one executor completion made during this visit (judge, retry reflection, screen). */
|
|
156
|
+
meter(kind: SpendKind, usage: NodeUsage | undefined): BudgetOverrun | undefined;
|
|
157
|
+
/** @internal See {@link BudgetGuard.commit}. */
|
|
158
|
+
commit(s: Spend, kind: SpendKind, counted: boolean): void;
|
|
95
159
|
}
|
|
96
160
|
/** One agent run. Its `signal` is the caller's signal plus a budget stop. */
|
|
97
161
|
export declare class BudgetAttempt {
|
|
@@ -101,6 +165,8 @@ export declare class BudgetAttempt {
|
|
|
101
165
|
private readonly controller;
|
|
102
166
|
private live;
|
|
103
167
|
private liveUsage;
|
|
168
|
+
/** A live report carried tokens or cost. */
|
|
169
|
+
private liveReported;
|
|
104
170
|
private stopped;
|
|
105
171
|
private settled;
|
|
106
172
|
private readonly onOuterAbort;
|
package/dist/budget.js
CHANGED
|
@@ -8,8 +8,14 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Tokens are input + output tokens as the harness reports them (cache reads and
|
|
10
10
|
* writes are not counted, matching the run receipt). Cost is the harness's own
|
|
11
|
-
* reported USD, never estimated.
|
|
12
|
-
*
|
|
11
|
+
* reported USD, never estimated. Every model completion the executor itself
|
|
12
|
+
* makes is counted too, under its own `kind` (see {@link SpendKind}): judge
|
|
13
|
+
* scoring, route evaluation, retry reflection and the safe-output screen. A
|
|
14
|
+
* completion counts when its harness reports usage for it; one that reports
|
|
15
|
+
* none adds nothing (the same as a node run without usage). The run totals
|
|
16
|
+
* and the stop are over all kinds together, across a resume too; the split is
|
|
17
|
+
* `trace.spend`. Once a ceiling is reached no further executor completion
|
|
18
|
+
* starts. The decision model (`route_by: decider`) is not metered.
|
|
13
19
|
*
|
|
14
20
|
* Enforcement, per unit the harness can report:
|
|
15
21
|
* - live: the harness calls `onUsage` during the node; crossing a limit aborts
|
|
@@ -17,9 +23,34 @@
|
|
|
17
23
|
* - between nodes: usage on the finished result is checked; a crossing fails the
|
|
18
24
|
* node, and the run halts before the next node starts.
|
|
19
25
|
* - across a resume: the run journal records each attempt's spend (live and
|
|
20
|
-
* final), and
|
|
26
|
+
* final), and each metered completion (judge, route, reflection, screen)
|
|
27
|
+
* as its own record; `sweny workflow resume` seeds the run total with all
|
|
28
|
+
* of it, split by kind.
|
|
21
29
|
*/
|
|
30
|
+
/** The order kinds are listed in, wherever the split is shown. */
|
|
31
|
+
export const SPEND_KINDS = ["node", "judge", "route", "reflect", "screen"];
|
|
32
|
+
/**
|
|
33
|
+
* Thrown into an executor completion (judge, route, reflection, screen) the
|
|
34
|
+
* budget refuses: a ceiling was already reached, so the call is not made.
|
|
35
|
+
* Callers that swallow a failed completion (a judge's parse retry) must let
|
|
36
|
+
* this one through, so a refusal is never mistaken for a model answer.
|
|
37
|
+
*/
|
|
38
|
+
export class BudgetRefusal extends Error {
|
|
39
|
+
breach;
|
|
40
|
+
constructor(breach) {
|
|
41
|
+
super(describeOverrun(breach));
|
|
42
|
+
this.breach = breach;
|
|
43
|
+
this.name = "BudgetRefusal";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
22
46
|
const isNum = (v) => typeof v === "number" && Number.isFinite(v);
|
|
47
|
+
/**
|
|
48
|
+
* Slack for comparing accumulated floats: 0.1 + 0.2 is 0.30000000000000004,
|
|
49
|
+
* which must not read as over a $0.30 limit. Far below a token or a cent.
|
|
50
|
+
*/
|
|
51
|
+
const EPSILON = 1e-9;
|
|
52
|
+
/** Cost as shown: accumulated float noise rounded off (10 decimal places). */
|
|
53
|
+
const roundCost = (n) => Math.round(n * 1e10) / 1e10;
|
|
23
54
|
export function toLimits(b) {
|
|
24
55
|
return {
|
|
25
56
|
...(b?.tokens !== undefined ? { tokens: b.tokens } : {}),
|
|
@@ -50,14 +81,14 @@ export function spendOf(u) {
|
|
|
50
81
|
function over(spent, limit, strictlyOver) {
|
|
51
82
|
if (limit === undefined)
|
|
52
83
|
return false;
|
|
53
|
-
return strictlyOver ? spent > limit : spent >= limit;
|
|
84
|
+
return strictlyOver ? spent > limit + EPSILON : spent >= limit - EPSILON;
|
|
54
85
|
}
|
|
55
86
|
function breachOf(scope, limits, spent, strictlyOver) {
|
|
56
87
|
if (over(spent.tokens, limits.tokens, strictlyOver)) {
|
|
57
88
|
return { scope, unit: "tokens", limit: limits.tokens, spent: spent.tokens };
|
|
58
89
|
}
|
|
59
90
|
if (over(spent.costUsd, limits.costUsd, strictlyOver)) {
|
|
60
|
-
return { scope, unit: "cost_usd", limit: limits.costUsd, spent: spent.costUsd };
|
|
91
|
+
return { scope, unit: "cost_usd", limit: limits.costUsd, spent: roundCost(spent.costUsd) };
|
|
61
92
|
}
|
|
62
93
|
return undefined;
|
|
63
94
|
}
|
|
@@ -74,12 +105,53 @@ export function describeOverrun(o, nodeId) {
|
|
|
74
105
|
export class BudgetGuard {
|
|
75
106
|
runLimits;
|
|
76
107
|
spent = { tokens: 0, costUsd: 0 };
|
|
108
|
+
kinds = new Map();
|
|
77
109
|
constructor(runLimits) {
|
|
78
110
|
this.runLimits = runLimits;
|
|
79
111
|
}
|
|
80
112
|
get totals() {
|
|
81
113
|
return { ...this.spent };
|
|
82
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* Spend by kind, in {@link SPEND_KINDS} order. Resumed spend is restored
|
|
117
|
+
* under its kind; a journal without meter records seeds `node` only.
|
|
118
|
+
*/
|
|
119
|
+
breakdown() {
|
|
120
|
+
const out = {};
|
|
121
|
+
for (const k of SPEND_KINDS) {
|
|
122
|
+
const v = this.kinds.get(k);
|
|
123
|
+
if (v)
|
|
124
|
+
out[k] = { tokens: v.tokens, costUsd: roundCost(v.costUsd), calls: v.calls };
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
/** True once any completion other than a node run reported usage. */
|
|
129
|
+
get hasMeteredCalls() {
|
|
130
|
+
return [...this.kinds.keys()].some((k) => k !== "node");
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Count one executor completion (judge, route, ...) against the run. Usage
|
|
134
|
+
* with no tokens and no cost counts nothing. Returns the breach this report
|
|
135
|
+
* caused, judged against the totals before it, like a live node report.
|
|
136
|
+
*/
|
|
137
|
+
meter(kind, usage, nodeBudget) {
|
|
138
|
+
const u = spendOf(usage);
|
|
139
|
+
if (u.tokens === undefined && u.costUsd === undefined)
|
|
140
|
+
return undefined;
|
|
141
|
+
const s = { tokens: u.tokens ?? 0, costUsd: u.costUsd ?? 0 };
|
|
142
|
+
const r = this.spent;
|
|
143
|
+
const breach = (nodeBudget
|
|
144
|
+
? breachOf("node", nodeBudget.limits, { tokens: nodeBudget.committed.tokens + s.tokens, costUsd: nodeBudget.committed.costUsd + s.costUsd }, true)
|
|
145
|
+
: undefined) ??
|
|
146
|
+
breachOf("run", this.runLimits, { tokens: r.tokens + s.tokens, costUsd: r.costUsd + s.costUsd }, true);
|
|
147
|
+
if (breach && kind !== "node")
|
|
148
|
+
breach.kind = kind;
|
|
149
|
+
if (nodeBudget)
|
|
150
|
+
nodeBudget.commit(s, kind, true);
|
|
151
|
+
else
|
|
152
|
+
this.commit(s, kind, true);
|
|
153
|
+
return breach;
|
|
154
|
+
}
|
|
83
155
|
/** True when any limit exists at all, so the executor can skip the machinery. */
|
|
84
156
|
active(nodeLimits) {
|
|
85
157
|
return hasLimits(this.runLimits) || hasLimits(nodeLimits);
|
|
@@ -92,17 +164,42 @@ export class BudgetGuard {
|
|
|
92
164
|
return new NodeBudget(this, nodeLimits);
|
|
93
165
|
}
|
|
94
166
|
/**
|
|
95
|
-
* Spend the run made before this process (a resumed run's journaled usage
|
|
96
|
-
* Counted against the run ceiling like any other
|
|
97
|
-
* resets the whole-run budget.
|
|
167
|
+
* Spend the run made before this process (a resumed run's journaled usage
|
|
168
|
+
* and metered completions). Counted against the run ceiling like any other
|
|
169
|
+
* spend, so a crash never resets the whole-run budget. Restored under its
|
|
170
|
+
* kind when the journal has the split (a journal written before meter
|
|
171
|
+
* records existed has node spend only). Seeded spend is not a call: `calls`
|
|
172
|
+
* is unchanged.
|
|
98
173
|
*/
|
|
99
174
|
seed(s) {
|
|
100
|
-
const ok = (n) => (Number.isFinite(n) && n > 0 ? n : 0);
|
|
101
|
-
|
|
175
|
+
const ok = (n) => (typeof n === "number" && Number.isFinite(n) && n > 0 ? n : 0);
|
|
176
|
+
if (!s.byKind) {
|
|
177
|
+
this.commit({ tokens: ok(s.tokens), costUsd: ok(s.costUsd) }, "node", false);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
for (const k of SPEND_KINDS) {
|
|
181
|
+
const v = s.byKind[k];
|
|
182
|
+
if (v)
|
|
183
|
+
this.commit({ tokens: ok(v.tokens), costUsd: ok(v.costUsd) }, k, false);
|
|
184
|
+
}
|
|
102
185
|
}
|
|
103
|
-
/**
|
|
104
|
-
|
|
186
|
+
/**
|
|
187
|
+
* @internal `counted`: this commit is one completion that reported usage, so
|
|
188
|
+
* `calls` goes up. A commit that is not (seeded spend, an attempt that
|
|
189
|
+
* reported nothing) adds its spend only, and a zero one leaves no trace, so
|
|
190
|
+
* the split is the same with and without a limit.
|
|
191
|
+
*/
|
|
192
|
+
commit(s, kind, counted) {
|
|
105
193
|
this.spent = { tokens: this.spent.tokens + s.tokens, costUsd: this.spent.costUsd + s.costUsd };
|
|
194
|
+
const k = this.kinds.get(kind);
|
|
195
|
+
if (!k && !counted && s.tokens === 0 && s.costUsd === 0)
|
|
196
|
+
return;
|
|
197
|
+
const prev = k ?? { tokens: 0, costUsd: 0, calls: 0 };
|
|
198
|
+
this.kinds.set(kind, {
|
|
199
|
+
tokens: prev.tokens + s.tokens,
|
|
200
|
+
costUsd: prev.costUsd + s.costUsd,
|
|
201
|
+
calls: prev.calls + (counted ? 1 : 0),
|
|
202
|
+
});
|
|
106
203
|
}
|
|
107
204
|
}
|
|
108
205
|
export class NodeBudget {
|
|
@@ -120,14 +217,27 @@ export class NodeBudget {
|
|
|
120
217
|
attempt(outer) {
|
|
121
218
|
return new BudgetAttempt(this.guard, this, outer);
|
|
122
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* Spend this visit made before a resume (a checkpointed visit's journaled
|
|
222
|
+
* spend). It counts against the node's own limit only: the run total
|
|
223
|
+
* already holds it from the seed.
|
|
224
|
+
*/
|
|
225
|
+
restore(s) {
|
|
226
|
+
const ok = (n) => (Number.isFinite(n) && n > 0 ? n : 0);
|
|
227
|
+
this.spent = { tokens: this.spent.tokens + ok(s.tokens), costUsd: this.spent.costUsd + ok(s.costUsd) };
|
|
228
|
+
}
|
|
123
229
|
/** @internal */
|
|
124
230
|
get committed() {
|
|
125
231
|
return this.spent;
|
|
126
232
|
}
|
|
127
|
-
/**
|
|
128
|
-
|
|
233
|
+
/** Count one executor completion made during this visit (judge, retry reflection, screen). */
|
|
234
|
+
meter(kind, usage) {
|
|
235
|
+
return this.guard.meter(kind, usage, this);
|
|
236
|
+
}
|
|
237
|
+
/** @internal See {@link BudgetGuard.commit}. */
|
|
238
|
+
commit(s, kind, counted) {
|
|
129
239
|
this.spent = { tokens: this.spent.tokens + s.tokens, costUsd: this.spent.costUsd + s.costUsd };
|
|
130
|
-
this.guard.commit(s);
|
|
240
|
+
this.guard.commit(s, kind, counted);
|
|
131
241
|
}
|
|
132
242
|
}
|
|
133
243
|
/** One agent run. Its `signal` is the caller's signal plus a budget stop. */
|
|
@@ -138,6 +248,8 @@ export class BudgetAttempt {
|
|
|
138
248
|
controller = new AbortController();
|
|
139
249
|
live = { tokens: 0, costUsd: 0 };
|
|
140
250
|
liveUsage;
|
|
251
|
+
/** A live report carried tokens or cost. */
|
|
252
|
+
liveReported = false;
|
|
141
253
|
stopped;
|
|
142
254
|
settled;
|
|
143
255
|
onOuterAbort = () => this.controller.abort();
|
|
@@ -167,6 +279,8 @@ export class BudgetAttempt {
|
|
|
167
279
|
return;
|
|
168
280
|
this.liveUsage = { ...this.liveUsage, ...u };
|
|
169
281
|
const s = spendOf(u);
|
|
282
|
+
if (s.tokens !== undefined || s.costUsd !== undefined)
|
|
283
|
+
this.liveReported = true;
|
|
170
284
|
// Reports are cumulative; never let a partial report move spend backwards.
|
|
171
285
|
this.live = {
|
|
172
286
|
tokens: Math.max(this.live.tokens, s.tokens ?? 0),
|
|
@@ -204,7 +318,9 @@ export class BudgetAttempt {
|
|
|
204
318
|
};
|
|
205
319
|
const breach = this.stopped ?? this.check(spent);
|
|
206
320
|
this.settled = { breach };
|
|
207
|
-
|
|
321
|
+
// A call only when the attempt reported usage, live or final: an attempt
|
|
322
|
+
// that reported none is not counted, the same as without a limit.
|
|
323
|
+
this.node.commit(spent, "node", this.liveReported || f.tokens !== undefined || f.costUsd !== undefined);
|
|
208
324
|
return breach;
|
|
209
325
|
}
|
|
210
326
|
/** Settle with the attempt's final usage and return the breach, if any. */
|
package/dist/cli/config.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { execSync } from "node:child_process";
|
|
2
2
|
import { hasCodexLogin } from "../agent-env.js";
|
|
3
|
+
import { registerDeprecatedFlags } from "./deprecated-flags.js";
|
|
3
4
|
import { parseAcpAgent, unsupportedAgentError } from "../harness/agents.js";
|
|
4
5
|
export function registerTriageCommand(program) {
|
|
5
6
|
// NOTE: File-eligible options omit Commander defaults so parseCliInputs() can
|
|
6
7
|
// distinguish "user passed flag" from "not passed" and fall through to .sweny.yml.
|
|
7
|
-
|
|
8
|
+
const cmd = program
|
|
8
9
|
.command("triage")
|
|
9
10
|
.description("Run the SWEny triage workflow")
|
|
10
11
|
.option("--agent <provider>", "Coding agent: claude (default), codex, pi (experimental), or acp:<command> for any ACP agent (experimental)")
|
|
@@ -55,6 +56,7 @@ export function registerTriageCommand(program) {
|
|
|
55
56
|
.option("--workspace-tools <tools>", "Comma-separated workspace tool integrations to enable (slack, notion, pagerduty, monday)")
|
|
56
57
|
.option("--offline", "Skip URL Sources and fail fast if any are referenced", false)
|
|
57
58
|
.option("--file-root [dir]", "Sandbox file: Sources to this directory tree (defaults to the working directory when the flag is given without a value)");
|
|
59
|
+
return registerDeprecatedFlags(cmd);
|
|
58
60
|
}
|
|
59
61
|
export function parseCliInputs(options, fileConfig = {}) {
|
|
60
62
|
const env = process.env;
|
|
@@ -615,7 +617,7 @@ function parseObservabilityCredentials(provider, options, fileConfig = {}) {
|
|
|
615
617
|
}
|
|
616
618
|
}
|
|
617
619
|
export function registerImplementCommand(program) {
|
|
618
|
-
|
|
620
|
+
const cmd = program
|
|
619
621
|
.command("implement <issueId>")
|
|
620
622
|
.description("Implement a fix for a specific issue and open a PR")
|
|
621
623
|
.option("--agent <provider>", "Coding agent: claude (default), codex, pi (experimental), or acp:<command> for any ACP agent (experimental)")
|
|
@@ -634,6 +636,7 @@ export function registerImplementCommand(program) {
|
|
|
634
636
|
.option("--verbose", "Print each tool call's input and output inline (human-readable, truncated). Use --stream for full untruncated NDJSON.", false)
|
|
635
637
|
.option("--offline", "Skip URL Sources and fail fast if any are referenced", false)
|
|
636
638
|
.option("--file-root [dir]", "Sandbox file: Sources to this directory tree (defaults to the working directory when the flag is given without a value)");
|
|
639
|
+
return registerDeprecatedFlags(cmd);
|
|
637
640
|
}
|
|
638
641
|
/**
|
|
639
642
|
* Parse a JSON string into MCP server configs.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type Command } from "commander";
|
|
2
|
+
/**
|
|
3
|
+
* CLI flags that sweny parsed but never read, removed in #528 and restored
|
|
4
|
+
* here as deprecated no-ops so existing callers (the swenyai/triage action
|
|
5
|
+
* passes most of them) keep working through a deprecation period.
|
|
6
|
+
*
|
|
7
|
+
* Each one is accepted, hidden from `--help`, changes nothing, and prints one
|
|
8
|
+
* stderr warning per run. This table is the only place the list lives;
|
|
9
|
+
* `registerDeprecatedFlags` is the only place it is wired in, once per command.
|
|
10
|
+
*
|
|
11
|
+
* Env vars and `.sweny.yml` keys for the same settings are not parsed at all
|
|
12
|
+
* and are never rejected, so they need no handling.
|
|
13
|
+
*
|
|
14
|
+
* Remove an entry (and its command wiring) in a minor release after the
|
|
15
|
+
* deprecation period; the docs-cloud-positioning test keeps these out of
|
|
16
|
+
* `--help` and out of the supported-option docs tables.
|
|
17
|
+
*/
|
|
18
|
+
export interface DeprecatedFlag {
|
|
19
|
+
/** Commander flag spec, e.g. "--cache-dir <path>". */
|
|
20
|
+
flag: string;
|
|
21
|
+
/** Commands that accepted this flag before it was removed. */
|
|
22
|
+
commands: readonly string[];
|
|
23
|
+
/** Why it has no effect (kept for maintainers; not printed). */
|
|
24
|
+
note: string;
|
|
25
|
+
}
|
|
26
|
+
export declare const DEPRECATED_FLAGS: readonly DeprecatedFlag[];
|
|
27
|
+
/** `--cache-dir <path>` -> `--cache-dir`. */
|
|
28
|
+
export declare function deprecatedFlagName(spec: string): string;
|
|
29
|
+
export declare function deprecationWarning(flags: readonly string[]): string;
|
|
30
|
+
/**
|
|
31
|
+
* Accept every deprecated flag that `cmd` used to have. Hidden from help, no
|
|
32
|
+
* effect, and exactly one stderr warning per run naming the flags that were
|
|
33
|
+
* passed.
|
|
34
|
+
*/
|
|
35
|
+
export declare function registerDeprecatedFlags(cmd: Command): Command;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Option } from "commander";
|
|
2
|
+
const TRIAGE_AND_IMPLEMENT = ["triage", "implement"];
|
|
3
|
+
const TRIAGE_ONLY = ["triage"];
|
|
4
|
+
export const DEPRECATED_FLAGS = [
|
|
5
|
+
{ flag: "--cache-dir <path>", commands: TRIAGE_ONLY, note: "no step cache exists" },
|
|
6
|
+
{ flag: "--cache-ttl <seconds>", commands: TRIAGE_ONLY, note: "no step cache exists" },
|
|
7
|
+
{ flag: "--no-cache", commands: TRIAGE_ONLY, note: "no step cache exists" },
|
|
8
|
+
{ flag: "--output-dir <path>", commands: TRIAGE_AND_IMPLEMENT, note: "file providers do not read it" },
|
|
9
|
+
{ flag: "--service-map-path <path>", commands: TRIAGE_ONLY, note: "service maps are not read" },
|
|
10
|
+
{ flag: "--gitlab-base-url <url>", commands: TRIAGE_ONLY, note: "the GitLab provider reads GITLAB_URL" },
|
|
11
|
+
{ flag: "--linear-team-id <id>", commands: TRIAGE_AND_IMPLEMENT, note: "the agent gets team ids from the node" },
|
|
12
|
+
{ flag: "--linear-bug-label-id <id>", commands: TRIAGE_ONLY, note: "never read" },
|
|
13
|
+
{ flag: "--linear-triage-label-id <id>", commands: TRIAGE_ONLY, note: "never read" },
|
|
14
|
+
{ flag: "--linear-state-backlog <name>", commands: TRIAGE_ONLY, note: "never read" },
|
|
15
|
+
{ flag: "--linear-state-in-progress <name>", commands: TRIAGE_AND_IMPLEMENT, note: "never read" },
|
|
16
|
+
{ flag: "--linear-state-peer-review <name>", commands: TRIAGE_AND_IMPLEMENT, note: "never read" },
|
|
17
|
+
];
|
|
18
|
+
/** `--cache-dir <path>` -> `--cache-dir`. */
|
|
19
|
+
export function deprecatedFlagName(spec) {
|
|
20
|
+
return spec.split(/[ ,]/)[0];
|
|
21
|
+
}
|
|
22
|
+
export function deprecationWarning(flags) {
|
|
23
|
+
const list = flags.join(", ");
|
|
24
|
+
const subject = flags.length === 1 ? `${list} is deprecated` : `${list} are deprecated`;
|
|
25
|
+
return `warning: ${subject}, has no effect and will be removed in a future minor version.\n`;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Accept every deprecated flag that `cmd` used to have. Hidden from help, no
|
|
29
|
+
* effect, and exactly one stderr warning per run naming the flags that were
|
|
30
|
+
* passed.
|
|
31
|
+
*/
|
|
32
|
+
export function registerDeprecatedFlags(cmd) {
|
|
33
|
+
const options = DEPRECATED_FLAGS.filter((d) => d.commands.includes(cmd.name())).map((d) => new Option(d.flag).hideHelp());
|
|
34
|
+
for (const option of options)
|
|
35
|
+
cmd.addOption(option);
|
|
36
|
+
cmd.hook("preAction", (_thisCommand, actionCommand) => {
|
|
37
|
+
const used = options
|
|
38
|
+
.filter((o) => actionCommand.getOptionValueSource(o.attributeName()) === "cli")
|
|
39
|
+
.map((o) => o.long);
|
|
40
|
+
if (used.length > 0)
|
|
41
|
+
process.stderr.write(deprecationWarning(used));
|
|
42
|
+
});
|
|
43
|
+
return cmd;
|
|
44
|
+
}
|
package/dist/cli/renderer.js
CHANGED
|
@@ -234,6 +234,11 @@ export class DagRenderer {
|
|
|
234
234
|
s.finishedAt = Date.now();
|
|
235
235
|
break;
|
|
236
236
|
}
|
|
237
|
+
case "node:amended": {
|
|
238
|
+
const s = this.getOrCreate(event.node);
|
|
239
|
+
s.state = event.result.status === "failed" ? "failed" : "completed";
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
237
242
|
// These events don't affect per-node rendering state
|
|
238
243
|
case "workflow:start":
|
|
239
244
|
case "workflow:end":
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* know instead of guessing.
|
|
13
13
|
*/
|
|
14
14
|
import type { ExecutionEvent, ExecutionTrace, NodeResult, RouteRung, Workflow } from "../types.js";
|
|
15
|
+
import { type SpendKind } from "../budget.js";
|
|
15
16
|
export declare const RUN_HISTORY_SCHEMA_VERSION = 1;
|
|
16
17
|
export declare const RUN_HISTORY_DIR: string;
|
|
17
18
|
export declare const RUN_HISTORY_KEEP = 200;
|
|
@@ -58,6 +59,17 @@ export interface RunRecord {
|
|
|
58
59
|
};
|
|
59
60
|
/** Opinions the harness could not honor natively on some node, deduped. Absent when none. */
|
|
60
61
|
degraded?: string[];
|
|
62
|
+
/**
|
|
63
|
+
* Spend by kind (node, judge, route, reflect, screen), present when the
|
|
64
|
+
* executor's own completions reported usage. `totals` is then its sum, so a
|
|
65
|
+
* jump in `totals` that no node explains shows here.
|
|
66
|
+
*/
|
|
67
|
+
spend?: Partial<Record<SpendKind, RunKindSpend>>;
|
|
68
|
+
}
|
|
69
|
+
export interface RunKindSpend {
|
|
70
|
+
tokens: number;
|
|
71
|
+
cost_usd: number;
|
|
72
|
+
calls: number;
|
|
61
73
|
}
|
|
62
74
|
/** sha256 (hex) of the workflow as canonical JSON: keys sorted, undefined dropped. */
|
|
63
75
|
export declare function hashWorkflow(workflow: Workflow): string;
|
package/dist/cli/run-history.js
CHANGED
|
@@ -15,6 +15,7 @@ import crypto from "node:crypto";
|
|
|
15
15
|
import fs from "node:fs";
|
|
16
16
|
import path from "node:path";
|
|
17
17
|
import { summarizeRun } from "./run-output.js";
|
|
18
|
+
import { SPEND_KINDS } from "../budget.js";
|
|
18
19
|
import { openNoFollow } from "../safe-file.js";
|
|
19
20
|
export const RUN_HISTORY_SCHEMA_VERSION = 1;
|
|
20
21
|
export const RUN_HISTORY_DIR = path.join(".sweny", "runs");
|
|
@@ -63,10 +64,15 @@ export function createNodeTimer(now = Date.now) {
|
|
|
63
64
|
entered.set(event.node, now());
|
|
64
65
|
else if (event.type === "node:exit") {
|
|
65
66
|
const start = entered.get(event.node);
|
|
67
|
+
// Cleared on exit: a repeated exit can never count the same visit twice.
|
|
68
|
+
entered.delete(event.node);
|
|
66
69
|
if (start !== undefined)
|
|
67
70
|
durations.set(event.node, (durations.get(event.node) ?? 0) + (now() - start));
|
|
68
71
|
lastResults.set(event.node, event.result);
|
|
69
72
|
}
|
|
73
|
+
else if (event.type === "node:amended") {
|
|
74
|
+
lastResults.set(event.node, event.result);
|
|
75
|
+
}
|
|
70
76
|
},
|
|
71
77
|
};
|
|
72
78
|
}
|
|
@@ -117,8 +123,18 @@ export function buildRunRecord(i) {
|
|
|
117
123
|
},
|
|
118
124
|
...(harness ? { harness: { id: harness.id, version: harness.version } } : {}),
|
|
119
125
|
...(degraded.length > 0 ? { degraded } : {}),
|
|
126
|
+
...(summary.spend ? { spend: recordSpend(summary.spend) } : {}),
|
|
120
127
|
};
|
|
121
128
|
}
|
|
129
|
+
function recordSpend(spend) {
|
|
130
|
+
const out = {};
|
|
131
|
+
for (const k of SPEND_KINDS) {
|
|
132
|
+
const v = spend[k];
|
|
133
|
+
if (v)
|
|
134
|
+
out[k] = { tokens: v.tokens, cost_usd: v.costUsd, calls: v.calls };
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
122
138
|
// ── Write, prune, read ──────────────────────────────────────────
|
|
123
139
|
/** Atomic write: temp file in the same directory, fsync, rename. Returns the path, or null on failure. Never throws. */
|
|
124
140
|
export function writeRunRecord(record, cwd = process.cwd()) {
|
package/dist/cli/run-output.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { type ExecutionTrace, type Logger, type NodeResult, type Workflow } from "../types.js";
|
|
9
9
|
import { type NodeStatus } from "../mermaid.js";
|
|
10
|
+
import { type SpendKind } from "../budget.js";
|
|
10
11
|
export interface RunSummary {
|
|
11
12
|
ok: boolean;
|
|
12
13
|
nodesOk: number;
|
|
@@ -31,6 +32,12 @@ export interface RunSummary {
|
|
|
31
32
|
* knows none of it.
|
|
32
33
|
*/
|
|
33
34
|
policy?: RunPolicy;
|
|
35
|
+
/**
|
|
36
|
+
* Spend by kind (`node`, `judge`, `route`, `reflect`, `screen`), when the executor's own
|
|
37
|
+
* completions reported usage. `tokens` and `costUsd` above are then the sum over every kind
|
|
38
|
+
* (every attempt and visit), not over the node results. Absent otherwise.
|
|
39
|
+
*/
|
|
40
|
+
spend?: ExecutionTrace["spend"];
|
|
34
41
|
/** The spend budget a node crossed (#449). Absent when none was. */
|
|
35
42
|
budget?: {
|
|
36
43
|
node: string;
|
|
@@ -38,6 +45,8 @@ export interface RunSummary {
|
|
|
38
45
|
unit: "tokens" | "cost_usd";
|
|
39
46
|
limit: number;
|
|
40
47
|
spent: number;
|
|
48
|
+
/** The executor completion that crossed it (judge, route, ...); absent for a node run. */
|
|
49
|
+
kind?: SpendKind;
|
|
41
50
|
};
|
|
42
51
|
/**
|
|
43
52
|
* Who decided the run's conditional routes (#357), counts only: a `when`
|
|
@@ -89,8 +98,14 @@ export declare function formatTokenCount(n: number): string;
|
|
|
89
98
|
export declare function formatCost(usd: number): string;
|
|
90
99
|
/** `tokens 52k of 50k`: the overrun, in the budget's unit. */
|
|
91
100
|
export declare function formatBudgetAmount(b: NonNullable<RunSummary["budget"]>): string;
|
|
92
|
-
/** `budget exceeded: tokens 52k of 50k (run, at node gather)
|
|
101
|
+
/** `budget exceeded: tokens 52k of 50k (run, at node gather)`, plus `, judge` when an executor call crossed it. */
|
|
93
102
|
export declare function formatBudgetOverrun(b: NonNullable<RunSummary["budget"]>): string;
|
|
103
|
+
/**
|
|
104
|
+
* `node $0.15, judge $0.03, route <$0.01`: spend by kind in a fixed order
|
|
105
|
+
* (node, judge, route, reflect, screen), only the kinds present. In tokens
|
|
106
|
+
* when no kind reported a cost. Undefined when there is no split to show.
|
|
107
|
+
*/
|
|
108
|
+
export declare function formatSpendSplit(spend: RunSummary["spend"], sep?: string): string | undefined;
|
|
94
109
|
/** `5 (3 decider, 1 expr, 1 agent)`: zero rungs are left out. */
|
|
95
110
|
export declare function formatRouteCounts(r: RouteCounts): string;
|
|
96
111
|
/**
|