@sweny-ai/core 0.1.157 → 0.1.158
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 +115 -0
- package/dist/budget.js +191 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +5 -1
- package/dist/cli/run-output.d.ts +10 -0
- package/dist/cli/run-output.js +20 -1
- package/dist/cli/workflow-input.d.ts +9 -0
- package/dist/cli/workflow-input.js +21 -0
- package/dist/executor.d.ts +12 -0
- package/dist/executor.js +93 -28
- package/dist/harness/acp.js +4 -1
- package/dist/harness/capabilities.d.ts +10 -4
- package/dist/harness/capabilities.js +12 -6
- package/dist/harness/claude-code.js +25 -1
- package/dist/harness/index.d.ts +1 -1
- package/dist/harness/index.js +1 -1
- package/dist/harness/policy.d.ts +13 -0
- package/dist/harness/policy.js +37 -0
- package/dist/harness/types.d.ts +7 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/schema.d.ts +135 -1
- package/dist/schema.js +54 -0
- package/dist/types.d.ts +25 -0
- package/package.json +1 -1
package/dist/harness/acp.js
CHANGED
|
@@ -437,8 +437,10 @@ export class AcpHarness {
|
|
|
437
437
|
// `used` and `size` are context window occupancy, not billed tokens: not mapped.
|
|
438
438
|
const cost = u.cost;
|
|
439
439
|
if (isObj(cost) && typeof cost.amount === "number" && Number.isFinite(cost.amount)) {
|
|
440
|
-
if (typeof cost.currency === "string" && cost.currency.toUpperCase() === "USD")
|
|
440
|
+
if (typeof cost.currency === "string" && cost.currency.toUpperCase() === "USD") {
|
|
441
441
|
out.costUsd = cost.amount;
|
|
442
|
+
o.onUsage?.({ costUsd: cost.amount });
|
|
443
|
+
}
|
|
442
444
|
else if (!costWarned) {
|
|
443
445
|
costWarned = true;
|
|
444
446
|
this.logger.debug(`[acp] usage_update cost is in ${String(cost.currency)}, not USD: not recorded`);
|
|
@@ -789,6 +791,7 @@ export class AcpHarness {
|
|
|
789
791
|
signal: req.signal,
|
|
790
792
|
maxToolCalls: maxTurns,
|
|
791
793
|
onProgress: req.onProgress,
|
|
794
|
+
onUsage: req.onUsage,
|
|
792
795
|
});
|
|
793
796
|
}
|
|
794
797
|
finally {
|
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
* them without pulling in an agent SDK.
|
|
4
4
|
*/
|
|
5
5
|
import type { HarnessCapabilities } from "./types.js";
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* Claude Code enforces every sweny opinion natively, so `policyGate` never degrades or refuses it.
|
|
8
|
+
* usage: tokens live (per assistant message, a lower bound until the terminal
|
|
9
|
+
* `result` message gives the authoritative total); cost only at the end of the
|
|
10
|
+
* node (the SDK prices the run in `total_cost_usd` on `result`).
|
|
11
|
+
*/
|
|
7
12
|
export declare const CLAUDE_CODE_CAPABILITIES: HarnessCapabilities;
|
|
8
13
|
/**
|
|
9
14
|
* Codex (`codex exec --json`), declared from the openai/codex source at
|
|
@@ -81,9 +86,10 @@ export declare const PI_CAPABILITIES: HarnessCapabilities;
|
|
|
81
86
|
* - readOnly none: modes are agent-defined. `policyGate` counts read-only as
|
|
82
87
|
* enforced only with the wrapper's read-only mount.
|
|
83
88
|
* - turnLimit watchdog: no turn limit in the protocol; sweny counts tool calls.
|
|
84
|
-
* - usage costUsd only: `usage_update {used, size, cost?}` is the stable
|
|
85
|
-
* usage
|
|
86
|
-
*
|
|
89
|
+
* - usage costUsd only, live: `usage_update {used, size, cost?}` is the stable
|
|
90
|
+
* usage and arrives while the turn runs, so a cost budget stops the agent
|
|
91
|
+
* mid-node. `used` and `size` are context window occupancy, not billed
|
|
92
|
+
* tokens, so they are not mapped; `cost` (cumulative, ISO 4217 currency) is, when it
|
|
87
93
|
* is USD. Per-turn token usage is behind an unstable flag.
|
|
88
94
|
* - cancel rpc: `session/cancel`, then kill.
|
|
89
95
|
* - resume false: sweny starts a fresh session per node.
|
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
* Declared capabilities per adapter. No runtime imports, so the executor can use
|
|
3
3
|
* them without pulling in an agent SDK.
|
|
4
4
|
*/
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* Claude Code enforces every sweny opinion natively, so `policyGate` never degrades or refuses it.
|
|
7
|
+
* usage: tokens live (per assistant message, a lower bound until the terminal
|
|
8
|
+
* `result` message gives the authoritative total); cost only at the end of the
|
|
9
|
+
* node (the SDK prices the run in `total_cost_usd` on `result`).
|
|
10
|
+
*/
|
|
6
11
|
export const CLAUDE_CODE_CAPABILITIES = {
|
|
7
12
|
structuredOutput: "native",
|
|
8
13
|
toolTrace: "full",
|
|
@@ -11,7 +16,7 @@ export const CLAUDE_CODE_CAPABILITIES = {
|
|
|
11
16
|
sandbox: { fs: true, network: true },
|
|
12
17
|
readOnly: "native",
|
|
13
18
|
turnLimit: "native",
|
|
14
|
-
usage: { tokens: true, costUsd: true, live:
|
|
19
|
+
usage: { tokens: true, costUsd: true, live: true, liveUnits: ["tokens"] },
|
|
15
20
|
cancel: "signal",
|
|
16
21
|
resume: false,
|
|
17
22
|
};
|
|
@@ -115,9 +120,10 @@ export const PI_CAPABILITIES = {
|
|
|
115
120
|
* - readOnly none: modes are agent-defined. `policyGate` counts read-only as
|
|
116
121
|
* enforced only with the wrapper's read-only mount.
|
|
117
122
|
* - turnLimit watchdog: no turn limit in the protocol; sweny counts tool calls.
|
|
118
|
-
* - usage costUsd only: `usage_update {used, size, cost?}` is the stable
|
|
119
|
-
* usage
|
|
120
|
-
*
|
|
123
|
+
* - usage costUsd only, live: `usage_update {used, size, cost?}` is the stable
|
|
124
|
+
* usage and arrives while the turn runs, so a cost budget stops the agent
|
|
125
|
+
* mid-node. `used` and `size` are context window occupancy, not billed
|
|
126
|
+
* tokens, so they are not mapped; `cost` (cumulative, ISO 4217 currency) is, when it
|
|
121
127
|
* is USD. Per-turn token usage is behind an unstable flag.
|
|
122
128
|
* - cancel rpc: `session/cancel`, then kill.
|
|
123
129
|
* - resume false: sweny starts a fresh session per node.
|
|
@@ -130,7 +136,7 @@ export const ACP_CAPABILITIES = {
|
|
|
130
136
|
sandbox: { fs: false, network: false },
|
|
131
137
|
readOnly: "none",
|
|
132
138
|
turnLimit: "watchdog",
|
|
133
|
-
usage: { tokens: false, costUsd: true, live:
|
|
139
|
+
usage: { tokens: false, costUsd: true, live: true, liveUnits: ["costUsd"] },
|
|
134
140
|
cancel: "rpc",
|
|
135
141
|
resume: false,
|
|
136
142
|
};
|
|
@@ -315,7 +315,7 @@ export class ClaudeCodeHarness {
|
|
|
315
315
|
return { ...result, harness: this.info(), degraded: gate.degraded };
|
|
316
316
|
}
|
|
317
317
|
async runQuery(opts) {
|
|
318
|
-
const { instruction, context, tools, outputSchema, onProgress, maxTurns, model, timeoutMs, signal, readOnly, agentAccess, } = opts;
|
|
318
|
+
const { instruction, context, tools, outputSchema, onProgress, maxTurns, model, timeoutMs, signal, readOnly, agentAccess, onUsage, } = opts;
|
|
319
319
|
// Dry run (#380): external MCP servers cannot be classified per tool, so
|
|
320
320
|
// they are unknown, and unknown means write. Drop them, and disallow the
|
|
321
321
|
// built-ins that can change the workspace or shell out.
|
|
@@ -361,6 +361,11 @@ export class ClaudeCodeHarness {
|
|
|
361
361
|
// why FIFO-by-name is wrong.
|
|
362
362
|
const toolCalls = [];
|
|
363
363
|
const pendingByUseId = new Map();
|
|
364
|
+
// Live token tally (#449). Each assistant message carries its API
|
|
365
|
+
// message's usage; the SDK repeats one API message across its content
|
|
366
|
+
// blocks, so keep the latest per message id and sum. A lower bound until
|
|
367
|
+
// the terminal `result` message, whose total is authoritative.
|
|
368
|
+
const liveByMessage = new Map();
|
|
364
369
|
// Convert core tools to SDK MCP tools. The wrapper invokes the user's
|
|
365
370
|
// handler and returns `content` via the MCP transport. We do not push
|
|
366
371
|
// to toolCalls from the wrapper - the stream's tool_result is the sole
|
|
@@ -468,6 +473,25 @@ export class ClaudeCodeHarness {
|
|
|
468
473
|
// Tool_use blocks start a ToolCall record. Status + output are
|
|
469
474
|
// filled in when the matching user tool_result arrives.
|
|
470
475
|
const am = message;
|
|
476
|
+
if (onUsage) {
|
|
477
|
+
const u = am.message?.usage;
|
|
478
|
+
if (u && typeof u === "object") {
|
|
479
|
+
const key = typeof am.message?.id === "string" ? am.message.id : `#${liveByMessage.size}`;
|
|
480
|
+
const num = (v) => (typeof v === "number" && Number.isFinite(v) ? v : 0);
|
|
481
|
+
const prev = liveByMessage.get(key);
|
|
482
|
+
liveByMessage.set(key, {
|
|
483
|
+
input: Math.max(prev?.input ?? 0, num(u.input_tokens)),
|
|
484
|
+
output: Math.max(prev?.output ?? 0, num(u.output_tokens)),
|
|
485
|
+
});
|
|
486
|
+
let inputTokens = 0;
|
|
487
|
+
let outputTokens = 0;
|
|
488
|
+
for (const v of liveByMessage.values()) {
|
|
489
|
+
inputTokens += v.input;
|
|
490
|
+
outputTokens += v.output;
|
|
491
|
+
}
|
|
492
|
+
onUsage({ inputTokens, outputTokens });
|
|
493
|
+
}
|
|
494
|
+
}
|
|
471
495
|
if (am.message?.content && Array.isArray(am.message.content)) {
|
|
472
496
|
for (const block of am.message.content) {
|
|
473
497
|
if (block.type === "tool_use") {
|
package/dist/harness/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { AcpHarness, type AcpHarnessOptions } from "./acp.js";
|
|
|
8
8
|
import { PiHarness, type PiHarnessOptions } from "./pi.js";
|
|
9
9
|
export { claudeCompat, asClaude } from "./compat.js";
|
|
10
10
|
export type { AgentHarness, HarnessCapabilities, HarnessCompleteRequest, HarnessId, HarnessInfo, HarnessRunRequest, HarnessRunResult, NodePolicy, PolicyGateResult, PolicyWrappers, ToolClass, } from "./types.js";
|
|
11
|
-
export { policyGate, nativeDenyClasses, resolveHarnessPolicy, isToolClass, TOOL_CLASSES } from "./policy.js";
|
|
11
|
+
export { policyGate, budgetGate, nativeDenyClasses, resolveHarnessPolicy, isToolClass, TOOL_CLASSES, } from "./policy.js";
|
|
12
12
|
export type { HarnessPolicyMode } from "./policy.js";
|
|
13
13
|
export { ask, evaluate, buildAskPrompt, buildEvaluatePrompt } from "./prompts.js";
|
|
14
14
|
export { ClaudeCodeHarness, CLAUDE_CODE_CAPABILITIES } from "./claude-code.js";
|
package/dist/harness/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { AcpHarness } from "./acp.js";
|
|
|
8
8
|
import { PiHarness } from "./pi.js";
|
|
9
9
|
import { parseAcpAgent, unsupportedAgentError } from "./agents.js";
|
|
10
10
|
export { claudeCompat, asClaude } from "./compat.js";
|
|
11
|
-
export { policyGate, nativeDenyClasses, resolveHarnessPolicy, isToolClass, TOOL_CLASSES } from "./policy.js";
|
|
11
|
+
export { policyGate, budgetGate, nativeDenyClasses, resolveHarnessPolicy, isToolClass, TOOL_CLASSES, } from "./policy.js";
|
|
12
12
|
export { ask, evaluate, buildAskPrompt, buildEvaluatePrompt } from "./prompts.js";
|
|
13
13
|
export { ClaudeCodeHarness, CLAUDE_CODE_CAPABILITIES } from "./claude-code.js";
|
|
14
14
|
export { CodexHarness, CODEX_CAPABILITIES, MIN_CODEX_VERSION } from "./codex.js";
|
package/dist/harness/policy.d.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* - wrapped: sweny keeps the opinion itself, but not natively (for example a
|
|
13
13
|
* watchdog in place of a native turn limit). Reported, never refused.
|
|
14
14
|
*/
|
|
15
|
+
import type { SpendLimits } from "../budget.js";
|
|
15
16
|
import type { HarnessCapabilities, NodePolicy, PolicyGateResult, PolicyWrappers, ToolClass } from "./types.js";
|
|
16
17
|
/** `strict` refuses a node whose opinions the harness cannot honor; `warn` runs it and reports `degraded`. */
|
|
17
18
|
export type HarnessPolicyMode = "strict" | "warn";
|
|
@@ -30,3 +31,15 @@ export declare function isToolClass(name: string): name is ToolClass;
|
|
|
30
31
|
/** The tool classes a harness can deny natively. */
|
|
31
32
|
export declare function nativeDenyClasses(caps: HarnessCapabilities): ToolClass[];
|
|
32
33
|
export declare function policyGate(caps: HarnessCapabilities, policy: NodePolicy, wrappers?: PolicyWrappers): PolicyGateResult;
|
|
34
|
+
/**
|
|
35
|
+
* budgetGate (#449): can this harness keep a spend budget? Pure; the executor
|
|
36
|
+
* calls it once per node that has a token or cost limit (node or run).
|
|
37
|
+
*
|
|
38
|
+
* Per budgeted unit:
|
|
39
|
+
* - the harness cannot report the unit at all (Codex has no cost): nothing can
|
|
40
|
+
* be enforced. Reported as `budget_<unit>`; `strict` refuses the node.
|
|
41
|
+
* - the harness reports it only when the node ends: enforced between nodes, so
|
|
42
|
+
* one node can overrun before it is stopped. Reported as `budget_live`,
|
|
43
|
+
* never refused.
|
|
44
|
+
*/
|
|
45
|
+
export declare function budgetGate(caps: HarnessCapabilities, limits: SpendLimits, strict: boolean): PolicyGateResult;
|
package/dist/harness/policy.js
CHANGED
|
@@ -111,3 +111,40 @@ export function policyGate(caps, policy, wrappers = {}) {
|
|
|
111
111
|
}
|
|
112
112
|
return { degraded };
|
|
113
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* budgetGate (#449): can this harness keep a spend budget? Pure; the executor
|
|
116
|
+
* calls it once per node that has a token or cost limit (node or run).
|
|
117
|
+
*
|
|
118
|
+
* Per budgeted unit:
|
|
119
|
+
* - the harness cannot report the unit at all (Codex has no cost): nothing can
|
|
120
|
+
* be enforced. Reported as `budget_<unit>`; `strict` refuses the node.
|
|
121
|
+
* - the harness reports it only when the node ends: enforced between nodes, so
|
|
122
|
+
* one node can overrun before it is stopped. Reported as `budget_live`,
|
|
123
|
+
* never refused.
|
|
124
|
+
*/
|
|
125
|
+
export function budgetGate(caps, limits, strict) {
|
|
126
|
+
const unenforced = [];
|
|
127
|
+
const wrapped = [];
|
|
128
|
+
const units = [
|
|
129
|
+
{ unit: "tokens", key: "tokens", limit: limits.tokens, reports: caps.usage.tokens },
|
|
130
|
+
{ unit: "cost_usd", key: "costUsd", limit: limits.costUsd, reports: caps.usage.costUsd },
|
|
131
|
+
];
|
|
132
|
+
const notLive = [];
|
|
133
|
+
for (const u of units) {
|
|
134
|
+
if (u.limit === undefined)
|
|
135
|
+
continue;
|
|
136
|
+
if (!u.reports) {
|
|
137
|
+
unenforced.push(`budget_${u.unit}: harness cannot report ${u.unit === "tokens" ? "token" : "cost"} usage, so this budget is not enforced`);
|
|
138
|
+
}
|
|
139
|
+
else if (!caps.usage.live || (caps.usage.liveUnits !== undefined && !caps.usage.liveUnits.includes(u.key))) {
|
|
140
|
+
notLive.push(u.unit);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (notLive.length > 0) {
|
|
144
|
+
wrapped.push(`budget_live: harness reports ${notLive.join(" and ")} only when a node ends; the budget is enforced between nodes, so a node can overrun before it is stopped`);
|
|
145
|
+
}
|
|
146
|
+
const degraded = [...unenforced, ...wrapped];
|
|
147
|
+
if (unenforced.length > 0 && strict)
|
|
148
|
+
return { degraded, refuse: `strict policy: ${unenforced.join("; ")}` };
|
|
149
|
+
return { degraded };
|
|
150
|
+
}
|
package/dist/harness/types.d.ts
CHANGED
|
@@ -36,10 +36,17 @@ export interface HarnessCapabilities {
|
|
|
36
36
|
};
|
|
37
37
|
readOnly: "native" | "tool-allowlist" | "none";
|
|
38
38
|
turnLimit: "native" | "watchdog" | "none";
|
|
39
|
+
/**
|
|
40
|
+
* What the harness reports. `live`: usage arrives while the node runs (through
|
|
41
|
+
* `HarnessRunRequest.onUsage`), so a spend budget stops the agent mid-node.
|
|
42
|
+
* `liveUnits` narrows which of the reported units arrive live; absent means
|
|
43
|
+
* every reported unit. Without `live`, budgets are enforced between nodes.
|
|
44
|
+
*/
|
|
39
45
|
usage: {
|
|
40
46
|
tokens: boolean;
|
|
41
47
|
costUsd: boolean;
|
|
42
48
|
live: boolean;
|
|
49
|
+
liveUnits?: Array<"tokens" | "costUsd">;
|
|
43
50
|
};
|
|
44
51
|
cancel: "signal" | "rpc" | "kill";
|
|
45
52
|
resume: boolean;
|
package/dist/index.d.ts
CHANGED
|
@@ -27,6 +27,8 @@ export { EVALUATOR_KINDS, EVAL_POLICIES, REQUIRES_ON_FAIL, NODE_ON_FAIL, MCP_TRA
|
|
|
27
27
|
export { consoleLogger } from "./types.js";
|
|
28
28
|
export { execute, RouteEvaluationError } from "./executor.js";
|
|
29
29
|
export type { ExecuteOptions } from "./executor.js";
|
|
30
|
+
export { BudgetGuard, describeOverrun, minLimits, toLimits } from "./budget.js";
|
|
31
|
+
export type { Budget, BudgetOverrun, BudgetUnit, SpendLimits } from "./budget.js";
|
|
30
32
|
export { resolveNodePermissions, buildNodePolicy } from "./node-policy.js";
|
|
31
33
|
export type { ResolvedPermissions } from "./node-policy.js";
|
|
32
34
|
export { applySafeOutputs, createEmitOutputTool, resolveActor, EMIT_OUTPUT_TOOL } from "./safe-outputs.js";
|
package/dist/index.js
CHANGED
|
@@ -28,6 +28,7 @@ export { consoleLogger } from "./types.js";
|
|
|
28
28
|
// Executor
|
|
29
29
|
export { execute, RouteEvaluationError } from "./executor.js";
|
|
30
30
|
// Least-privilege nodes and safe outputs (#365)
|
|
31
|
+
export { BudgetGuard, describeOverrun, minLimits, toLimits } from "./budget.js";
|
|
31
32
|
export { resolveNodePermissions, buildNodePolicy } from "./node-policy.js";
|
|
32
33
|
export { applySafeOutputs, createEmitOutputTool, resolveActor, EMIT_OUTPUT_TOOL } from "./safe-outputs.js";
|
|
33
34
|
// Agent harness (the seam) and its adapters: Claude Code, Codex and pi
|
package/dist/schema.d.ts
CHANGED
|
@@ -848,6 +848,26 @@ export declare const nodePermissionsZ: z.ZodUnion<[z.ZodEnum<["read", "write"]>,
|
|
|
848
848
|
deny?: ("shell" | "write" | "edit" | "net" | "subagent")[] | undefined;
|
|
849
849
|
access?: "write" | "read" | undefined;
|
|
850
850
|
}>]>;
|
|
851
|
+
/**
|
|
852
|
+
* Spend ceiling (#449): input + output tokens and/or reported USD. The object
|
|
853
|
+
* must declare at least one of the two.
|
|
854
|
+
*/
|
|
855
|
+
export declare const budgetZ: z.ZodEffects<z.ZodObject<{
|
|
856
|
+
tokens: z.ZodOptional<z.ZodNumber>;
|
|
857
|
+
cost_usd: z.ZodOptional<z.ZodNumber>;
|
|
858
|
+
}, "strict", z.ZodTypeAny, {
|
|
859
|
+
tokens?: number | undefined;
|
|
860
|
+
cost_usd?: number | undefined;
|
|
861
|
+
}, {
|
|
862
|
+
tokens?: number | undefined;
|
|
863
|
+
cost_usd?: number | undefined;
|
|
864
|
+
}>, {
|
|
865
|
+
tokens?: number | undefined;
|
|
866
|
+
cost_usd?: number | undefined;
|
|
867
|
+
}, {
|
|
868
|
+
tokens?: number | undefined;
|
|
869
|
+
cost_usd?: number | undefined;
|
|
870
|
+
}>;
|
|
851
871
|
/** One typed write intent a node may emit (#365). */
|
|
852
872
|
export declare const safeOutputDeclarationZ: z.ZodObject<{
|
|
853
873
|
type: z.ZodEnum<["comment", "issue", "pr", "label", "issue_state"]>;
|
|
@@ -940,6 +960,22 @@ export declare const nodeZ: z.ZodObject<{
|
|
|
940
960
|
skills: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
941
961
|
output: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
942
962
|
max_turns: z.ZodOptional<z.ZodNumber>;
|
|
963
|
+
budget: z.ZodOptional<z.ZodEffects<z.ZodObject<{
|
|
964
|
+
tokens: z.ZodOptional<z.ZodNumber>;
|
|
965
|
+
cost_usd: z.ZodOptional<z.ZodNumber>;
|
|
966
|
+
}, "strict", z.ZodTypeAny, {
|
|
967
|
+
tokens?: number | undefined;
|
|
968
|
+
cost_usd?: number | undefined;
|
|
969
|
+
}, {
|
|
970
|
+
tokens?: number | undefined;
|
|
971
|
+
cost_usd?: number | undefined;
|
|
972
|
+
}>, {
|
|
973
|
+
tokens?: number | undefined;
|
|
974
|
+
cost_usd?: number | undefined;
|
|
975
|
+
}, {
|
|
976
|
+
tokens?: number | undefined;
|
|
977
|
+
cost_usd?: number | undefined;
|
|
978
|
+
}>>;
|
|
943
979
|
disallowed_tools: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
944
980
|
tools: z.ZodOptional<z.ZodEffects<z.ZodObject<{
|
|
945
981
|
allow: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
@@ -1426,6 +1462,10 @@ export declare const nodeZ: z.ZodObject<{
|
|
|
1426
1462
|
model?: string | undefined;
|
|
1427
1463
|
on_fail?: "halt" | "continue" | undefined;
|
|
1428
1464
|
max_turns?: number | undefined;
|
|
1465
|
+
budget?: {
|
|
1466
|
+
tokens?: number | undefined;
|
|
1467
|
+
cost_usd?: number | undefined;
|
|
1468
|
+
} | undefined;
|
|
1429
1469
|
disallowed_tools?: string[] | undefined;
|
|
1430
1470
|
fail_soft?: boolean | undefined;
|
|
1431
1471
|
permissions?: "write" | "read" | {
|
|
@@ -1540,6 +1580,10 @@ export declare const nodeZ: z.ZodObject<{
|
|
|
1540
1580
|
on_fail?: "halt" | "continue" | undefined;
|
|
1541
1581
|
skills?: string[] | undefined;
|
|
1542
1582
|
max_turns?: number | undefined;
|
|
1583
|
+
budget?: {
|
|
1584
|
+
tokens?: number | undefined;
|
|
1585
|
+
cost_usd?: number | undefined;
|
|
1586
|
+
} | undefined;
|
|
1543
1587
|
disallowed_tools?: string[] | undefined;
|
|
1544
1588
|
fail_soft?: boolean | undefined;
|
|
1545
1589
|
permissions?: "write" | "read" | {
|
|
@@ -1698,6 +1742,22 @@ export declare const workflowZ: z.ZodObject<{
|
|
|
1698
1742
|
skills: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
1699
1743
|
output: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
1700
1744
|
max_turns: z.ZodOptional<z.ZodNumber>;
|
|
1745
|
+
budget: z.ZodOptional<z.ZodEffects<z.ZodObject<{
|
|
1746
|
+
tokens: z.ZodOptional<z.ZodNumber>;
|
|
1747
|
+
cost_usd: z.ZodOptional<z.ZodNumber>;
|
|
1748
|
+
}, "strict", z.ZodTypeAny, {
|
|
1749
|
+
tokens?: number | undefined;
|
|
1750
|
+
cost_usd?: number | undefined;
|
|
1751
|
+
}, {
|
|
1752
|
+
tokens?: number | undefined;
|
|
1753
|
+
cost_usd?: number | undefined;
|
|
1754
|
+
}>, {
|
|
1755
|
+
tokens?: number | undefined;
|
|
1756
|
+
cost_usd?: number | undefined;
|
|
1757
|
+
}, {
|
|
1758
|
+
tokens?: number | undefined;
|
|
1759
|
+
cost_usd?: number | undefined;
|
|
1760
|
+
}>>;
|
|
1701
1761
|
disallowed_tools: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
1702
1762
|
tools: z.ZodOptional<z.ZodEffects<z.ZodObject<{
|
|
1703
1763
|
allow: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
@@ -2184,6 +2244,10 @@ export declare const workflowZ: z.ZodObject<{
|
|
|
2184
2244
|
model?: string | undefined;
|
|
2185
2245
|
on_fail?: "halt" | "continue" | undefined;
|
|
2186
2246
|
max_turns?: number | undefined;
|
|
2247
|
+
budget?: {
|
|
2248
|
+
tokens?: number | undefined;
|
|
2249
|
+
cost_usd?: number | undefined;
|
|
2250
|
+
} | undefined;
|
|
2187
2251
|
disallowed_tools?: string[] | undefined;
|
|
2188
2252
|
fail_soft?: boolean | undefined;
|
|
2189
2253
|
permissions?: "write" | "read" | {
|
|
@@ -2298,6 +2362,10 @@ export declare const workflowZ: z.ZodObject<{
|
|
|
2298
2362
|
on_fail?: "halt" | "continue" | undefined;
|
|
2299
2363
|
skills?: string[] | undefined;
|
|
2300
2364
|
max_turns?: number | undefined;
|
|
2365
|
+
budget?: {
|
|
2366
|
+
tokens?: number | undefined;
|
|
2367
|
+
cost_usd?: number | undefined;
|
|
2368
|
+
} | undefined;
|
|
2301
2369
|
disallowed_tools?: string[] | undefined;
|
|
2302
2370
|
fail_soft?: boolean | undefined;
|
|
2303
2371
|
permissions?: "write" | "read" | {
|
|
@@ -2547,6 +2615,22 @@ export declare const workflowZ: z.ZodObject<{
|
|
|
2547
2615
|
judge_model: z.ZodOptional<z.ZodString>;
|
|
2548
2616
|
model: z.ZodOptional<z.ZodString>;
|
|
2549
2617
|
judge_budget: z.ZodOptional<z.ZodNumber>;
|
|
2618
|
+
budget: z.ZodOptional<z.ZodEffects<z.ZodObject<{
|
|
2619
|
+
tokens: z.ZodOptional<z.ZodNumber>;
|
|
2620
|
+
cost_usd: z.ZodOptional<z.ZodNumber>;
|
|
2621
|
+
}, "strict", z.ZodTypeAny, {
|
|
2622
|
+
tokens?: number | undefined;
|
|
2623
|
+
cost_usd?: number | undefined;
|
|
2624
|
+
}, {
|
|
2625
|
+
tokens?: number | undefined;
|
|
2626
|
+
cost_usd?: number | undefined;
|
|
2627
|
+
}>, {
|
|
2628
|
+
tokens?: number | undefined;
|
|
2629
|
+
cost_usd?: number | undefined;
|
|
2630
|
+
}, {
|
|
2631
|
+
tokens?: number | undefined;
|
|
2632
|
+
cost_usd?: number | undefined;
|
|
2633
|
+
}>>;
|
|
2550
2634
|
context_mode: z.ZodOptional<z.ZodEnum<["bounded", "full"]>>;
|
|
2551
2635
|
inputs: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEffects<z.ZodObject<{
|
|
2552
2636
|
type: z.ZodEnum<["string", "number", "boolean", "string[]"]>;
|
|
@@ -2658,6 +2742,10 @@ export declare const workflowZ: z.ZodObject<{
|
|
|
2658
2742
|
model?: string | undefined;
|
|
2659
2743
|
on_fail?: "halt" | "continue" | undefined;
|
|
2660
2744
|
max_turns?: number | undefined;
|
|
2745
|
+
budget?: {
|
|
2746
|
+
tokens?: number | undefined;
|
|
2747
|
+
cost_usd?: number | undefined;
|
|
2748
|
+
} | undefined;
|
|
2661
2749
|
disallowed_tools?: string[] | undefined;
|
|
2662
2750
|
fail_soft?: boolean | undefined;
|
|
2663
2751
|
permissions?: "write" | "read" | {
|
|
@@ -2762,6 +2850,10 @@ export declare const workflowZ: z.ZodObject<{
|
|
|
2762
2850
|
}[];
|
|
2763
2851
|
entry: string;
|
|
2764
2852
|
model?: string | undefined;
|
|
2853
|
+
budget?: {
|
|
2854
|
+
tokens?: number | undefined;
|
|
2855
|
+
cost_usd?: number | undefined;
|
|
2856
|
+
} | undefined;
|
|
2765
2857
|
permissions?: "write" | "read" | {
|
|
2766
2858
|
strict?: boolean | undefined;
|
|
2767
2859
|
deny?: ("shell" | "write" | "edit" | "net" | "subagent")[] | undefined;
|
|
@@ -2824,6 +2916,10 @@ export declare const workflowZ: z.ZodObject<{
|
|
|
2824
2916
|
on_fail?: "halt" | "continue" | undefined;
|
|
2825
2917
|
skills?: string[] | undefined;
|
|
2826
2918
|
max_turns?: number | undefined;
|
|
2919
|
+
budget?: {
|
|
2920
|
+
tokens?: number | undefined;
|
|
2921
|
+
cost_usd?: number | undefined;
|
|
2922
|
+
} | undefined;
|
|
2827
2923
|
disallowed_tools?: string[] | undefined;
|
|
2828
2924
|
fail_soft?: boolean | undefined;
|
|
2829
2925
|
permissions?: "write" | "read" | {
|
|
@@ -2942,6 +3038,10 @@ export declare const workflowZ: z.ZodObject<{
|
|
|
2942
3038
|
env?: Record<string, string> | undefined;
|
|
2943
3039
|
} | undefined;
|
|
2944
3040
|
}> | undefined;
|
|
3041
|
+
budget?: {
|
|
3042
|
+
tokens?: number | undefined;
|
|
3043
|
+
cost_usd?: number | undefined;
|
|
3044
|
+
} | undefined;
|
|
2945
3045
|
permissions?: "write" | "read" | {
|
|
2946
3046
|
strict?: boolean | undefined;
|
|
2947
3047
|
deny?: ("shell" | "write" | "edit" | "net" | "subagent")[] | undefined;
|
|
@@ -3032,6 +3132,10 @@ export declare function parseWorkflow(raw: unknown): {
|
|
|
3032
3132
|
model?: string | undefined;
|
|
3033
3133
|
on_fail?: "halt" | "continue" | undefined;
|
|
3034
3134
|
max_turns?: number | undefined;
|
|
3135
|
+
budget?: {
|
|
3136
|
+
tokens?: number | undefined;
|
|
3137
|
+
cost_usd?: number | undefined;
|
|
3138
|
+
} | undefined;
|
|
3035
3139
|
disallowed_tools?: string[] | undefined;
|
|
3036
3140
|
fail_soft?: boolean | undefined;
|
|
3037
3141
|
permissions?: "write" | "read" | {
|
|
@@ -3136,6 +3240,10 @@ export declare function parseWorkflow(raw: unknown): {
|
|
|
3136
3240
|
}[];
|
|
3137
3241
|
entry: string;
|
|
3138
3242
|
model?: string | undefined;
|
|
3243
|
+
budget?: {
|
|
3244
|
+
tokens?: number | undefined;
|
|
3245
|
+
cost_usd?: number | undefined;
|
|
3246
|
+
} | undefined;
|
|
3139
3247
|
permissions?: "write" | "read" | {
|
|
3140
3248
|
strict?: boolean | undefined;
|
|
3141
3249
|
deny?: ("shell" | "write" | "edit" | "net" | "subagent")[] | undefined;
|
|
@@ -3178,7 +3286,7 @@ export declare function parseWorkflow(raw: unknown): {
|
|
|
3178
3286
|
} | undefined;
|
|
3179
3287
|
};
|
|
3180
3288
|
export interface WorkflowError {
|
|
3181
|
-
code: "MISSING_ENTRY" | "UNKNOWN_EDGE_SOURCE" | "UNKNOWN_EDGE_TARGET" | "UNREACHABLE_NODE" | "UNKNOWN_SKILL" | "SELF_LOOP" | "UNBOUNDED_CYCLE" | "AMBIGUOUS_EDGES" | "UNSUPPORTED_EVAL_POLICY" | "INVALID_INLINE_SKILL" | "EDGE_ITERATIONS_EXCEEDED" | "RETRY_MAX_EXCEEDED" | "PERMISSION_CEILING" | "OUTPUT_NOT_ALLOWED" | "DUPLICATE_OUTPUT" | "UNSUPPORTED_OUTPUT";
|
|
3289
|
+
code: "MISSING_ENTRY" | "UNKNOWN_EDGE_SOURCE" | "UNKNOWN_EDGE_TARGET" | "UNREACHABLE_NODE" | "UNKNOWN_SKILL" | "SELF_LOOP" | "UNBOUNDED_CYCLE" | "AMBIGUOUS_EDGES" | "UNSUPPORTED_EVAL_POLICY" | "INVALID_INLINE_SKILL" | "EDGE_ITERATIONS_EXCEEDED" | "RETRY_MAX_EXCEEDED" | "PERMISSION_CEILING" | "BUDGET_CEILING" | "OUTPUT_NOT_ALLOWED" | "DUPLICATE_OUTPUT" | "UNSUPPORTED_OUTPUT";
|
|
3182
3290
|
message: string;
|
|
3183
3291
|
nodeId?: string;
|
|
3184
3292
|
}
|
|
@@ -3438,6 +3546,24 @@ export declare const workflowJsonSchema: {
|
|
|
3438
3546
|
};
|
|
3439
3547
|
}];
|
|
3440
3548
|
};
|
|
3549
|
+
readonly Budget: {
|
|
3550
|
+
readonly type: "object";
|
|
3551
|
+
readonly additionalProperties: false;
|
|
3552
|
+
readonly minProperties: 1;
|
|
3553
|
+
readonly description: "A spend ceiling. 'tokens' counts input plus output tokens as the harness reports them; 'cost_usd' is the harness-reported cost (never estimated). A crossing stops the agent and fails the node; the run halts.";
|
|
3554
|
+
readonly properties: {
|
|
3555
|
+
readonly tokens: {
|
|
3556
|
+
readonly type: "integer";
|
|
3557
|
+
readonly minimum: 1;
|
|
3558
|
+
readonly description: "Max input plus output tokens.";
|
|
3559
|
+
};
|
|
3560
|
+
readonly cost_usd: {
|
|
3561
|
+
readonly type: "number";
|
|
3562
|
+
readonly exclusiveMinimum: 0;
|
|
3563
|
+
readonly description: "Max reported cost in USD.";
|
|
3564
|
+
};
|
|
3565
|
+
};
|
|
3566
|
+
};
|
|
3441
3567
|
readonly SafeOutput: {
|
|
3442
3568
|
readonly type: "object";
|
|
3443
3569
|
readonly required: readonly ["type"];
|
|
@@ -3614,6 +3740,10 @@ export declare const workflowJsonSchema: {
|
|
|
3614
3740
|
readonly minimum: 0;
|
|
3615
3741
|
readonly description: "Soft cap on expected judge calls per workflow run. Executor warns at load time if exceeded; not a hard runtime cap in v1.";
|
|
3616
3742
|
};
|
|
3743
|
+
readonly budget: {
|
|
3744
|
+
readonly $ref: "#/$defs/Budget";
|
|
3745
|
+
readonly description: "Spend ceiling for the whole run and for every node. A node's own budget may only narrow it. The CLI's --max-tokens and --max-cost tighten it further.";
|
|
3746
|
+
};
|
|
3617
3747
|
readonly context_mode: {
|
|
3618
3748
|
readonly type: "string";
|
|
3619
3749
|
readonly enum: readonly ["bounded", "full"];
|
|
@@ -3779,6 +3909,10 @@ export declare const workflowJsonSchema: {
|
|
|
3779
3909
|
readonly minimum: 1;
|
|
3780
3910
|
readonly description: "Max AI model turns for this node. When absent, the executor's default applies.";
|
|
3781
3911
|
};
|
|
3912
|
+
readonly budget: {
|
|
3913
|
+
readonly $ref: "#/$defs/Budget";
|
|
3914
|
+
readonly description: "Spend ceiling for one visit to this node, retry attempts included. Never above the workflow's budget. Crossing it stops the agent and fails the node (fail_soft and on_fail: continue do not apply) and the run halts.";
|
|
3915
|
+
};
|
|
3782
3916
|
readonly disallowed_tools: {
|
|
3783
3917
|
readonly type: "array";
|
|
3784
3918
|
readonly items: {
|
package/dist/schema.js
CHANGED
|
@@ -262,6 +262,19 @@ export const nodePermissionsZ = z.union([
|
|
|
262
262
|
message: "permissions must declare at least one of access, deny, strict",
|
|
263
263
|
}),
|
|
264
264
|
]);
|
|
265
|
+
/**
|
|
266
|
+
* Spend ceiling (#449): input + output tokens and/or reported USD. The object
|
|
267
|
+
* must declare at least one of the two.
|
|
268
|
+
*/
|
|
269
|
+
export const budgetZ = z
|
|
270
|
+
.object({
|
|
271
|
+
tokens: z.number().int().min(1).optional(),
|
|
272
|
+
cost_usd: z.number().positive().optional(),
|
|
273
|
+
})
|
|
274
|
+
.strict()
|
|
275
|
+
.refine((b) => b.tokens !== undefined || b.cost_usd !== undefined, {
|
|
276
|
+
message: "budget must declare at least one of tokens, cost_usd",
|
|
277
|
+
});
|
|
265
278
|
/** One typed write intent a node may emit (#365). */
|
|
266
279
|
export const safeOutputDeclarationZ = z
|
|
267
280
|
.object({
|
|
@@ -296,6 +309,7 @@ export const nodeZ = z
|
|
|
296
309
|
skills: z.array(z.string()).default([]),
|
|
297
310
|
output: jsonSchemaZ.optional(),
|
|
298
311
|
max_turns: z.number().int().min(1).optional(),
|
|
312
|
+
budget: budgetZ.optional(),
|
|
299
313
|
disallowed_tools: z.array(z.string().min(1)).optional(),
|
|
300
314
|
tools: nodeToolsZ.optional(),
|
|
301
315
|
fail_soft: z.boolean().optional(),
|
|
@@ -353,6 +367,7 @@ export const workflowZ = z.object({
|
|
|
353
367
|
judge_model: z.string().min(1).optional(),
|
|
354
368
|
model: z.string().min(1).optional(),
|
|
355
369
|
judge_budget: z.number().int().min(0).optional(),
|
|
370
|
+
budget: budgetZ.optional(),
|
|
356
371
|
context_mode: z.enum(CONTEXT_MODES).optional(),
|
|
357
372
|
inputs: workflowInputsZ.optional(),
|
|
358
373
|
permissions: nodePermissionsZ.optional(),
|
|
@@ -611,6 +626,27 @@ export function validateWorkflow(workflow, knownSkills) {
|
|
|
611
626
|
nodeId,
|
|
612
627
|
});
|
|
613
628
|
}
|
|
629
|
+
// Budgets (#449): the workflow's `budget` is the ceiling for every node.
|
|
630
|
+
const wfBudget = workflow.budget;
|
|
631
|
+
const nodeBudget = node.budget;
|
|
632
|
+
if (wfBudget && nodeBudget) {
|
|
633
|
+
if (wfBudget.tokens !== undefined && nodeBudget.tokens !== undefined && nodeBudget.tokens > wfBudget.tokens) {
|
|
634
|
+
errors.push({
|
|
635
|
+
code: "BUDGET_CEILING",
|
|
636
|
+
message: `Node "${nodeId}" declares budget.tokens ${nodeBudget.tokens}, above the workflow's budget.tokens ${wfBudget.tokens}`,
|
|
637
|
+
nodeId,
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
if (wfBudget.cost_usd !== undefined &&
|
|
641
|
+
nodeBudget.cost_usd !== undefined &&
|
|
642
|
+
nodeBudget.cost_usd > wfBudget.cost_usd) {
|
|
643
|
+
errors.push({
|
|
644
|
+
code: "BUDGET_CEILING",
|
|
645
|
+
message: `Node "${nodeId}" declares budget.cost_usd ${nodeBudget.cost_usd}, above the workflow's budget.cost_usd ${wfBudget.cost_usd}`,
|
|
646
|
+
nodeId,
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
}
|
|
614
650
|
const seenTypes = new Set();
|
|
615
651
|
for (const out of node.outputs ?? []) {
|
|
616
652
|
if (seenTypes.has(out.type)) {
|
|
@@ -844,6 +880,16 @@ export const workflowJsonSchema = {
|
|
|
844
880
|
},
|
|
845
881
|
],
|
|
846
882
|
},
|
|
883
|
+
Budget: {
|
|
884
|
+
type: "object",
|
|
885
|
+
additionalProperties: false,
|
|
886
|
+
minProperties: 1,
|
|
887
|
+
description: "A spend ceiling. 'tokens' counts input plus output tokens as the harness reports them; 'cost_usd' is the harness-reported cost (never estimated). A crossing stops the agent and fails the node; the run halts.",
|
|
888
|
+
properties: {
|
|
889
|
+
tokens: { type: "integer", minimum: 1, description: "Max input plus output tokens." },
|
|
890
|
+
cost_usd: { type: "number", exclusiveMinimum: 0, description: "Max reported cost in USD." },
|
|
891
|
+
},
|
|
892
|
+
},
|
|
847
893
|
SafeOutput: {
|
|
848
894
|
type: "object",
|
|
849
895
|
required: ["type"],
|
|
@@ -984,6 +1030,10 @@ export const workflowJsonSchema = {
|
|
|
984
1030
|
// applies the soft-cap default at use-time. See judge_model above.
|
|
985
1031
|
description: "Soft cap on expected judge calls per workflow run. Executor warns at load time if exceeded; not a hard runtime cap in v1.",
|
|
986
1032
|
},
|
|
1033
|
+
budget: {
|
|
1034
|
+
$ref: "#/$defs/Budget",
|
|
1035
|
+
description: "Spend ceiling for the whole run and for every node. A node's own budget may only narrow it. The CLI's --max-tokens and --max-cost tighten it further.",
|
|
1036
|
+
},
|
|
987
1037
|
context_mode: {
|
|
988
1038
|
type: "string",
|
|
989
1039
|
enum: [...CONTEXT_MODES],
|
|
@@ -1100,6 +1150,10 @@ export const workflowJsonSchema = {
|
|
|
1100
1150
|
minimum: 1,
|
|
1101
1151
|
description: "Max AI model turns for this node. When absent, the executor's default applies.",
|
|
1102
1152
|
},
|
|
1153
|
+
budget: {
|
|
1154
|
+
$ref: "#/$defs/Budget",
|
|
1155
|
+
description: "Spend ceiling for one visit to this node, retry attempts included. Never above the workflow's budget. Crossing it stops the agent and fails the node (fail_soft and on_fail: continue do not apply) and the run halts.",
|
|
1156
|
+
},
|
|
1103
1157
|
disallowed_tools: {
|
|
1104
1158
|
type: "array",
|
|
1105
1159
|
items: { type: "string", minLength: 1 },
|