mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-11 01:51:34 -10:00
workflow: make the shared token budget opt-in instead of a silent 120k cap
[workflow].default_token_budget defaulted to 120_000 and was applied to a whole run plus all of its children. A fan-out doing honest recon burns ~30k per child, so runs died at the limit with no hand-back — the failure mode is strictly worse than overrun, and the model never asked for the cap (#6189). The parent turn loop already treats its token budget as advisory; the default now matches that policy instead of splitting it. 0 now means "no shared cap" end to end: the launch-time fallback already read 0 as unset, so the only real work is in the approval layer, where the threshold did double duty as the "high budget" baseline. The flag now only exists when a baseline is configured, and the card's budget label reports the effective value (requested, else configured default, else "unbounded") rather than the requested one. Explicit budgets — tool `token_budget`, spec `budget.max_tokens`, or a configured default — still enforce. Tests: cargo test -p codewhale-config -p codewhale-workflow -> 704 + 265 passed, 0 failed scripts/dev-test.sh crates/tui/src/tools/workflow_plan_approval.rs -> tools:: 2196 passed, 1 failed (pre-change assert on the old default; updated, re-ran: workflow_plan_approval 13 passed, 0 failed) cargo check -p codewhale-tui --lib clean
This commit is contained in:
1 parent
74703422f2
commit
a06d229652
7 files changed
+45
-15
No files matched your search
@@ -2475,6 +2475,8 @@ pub struct WorkflowConfigToml {
|
||||
#[serde(default = "default_workflow_max_depth")]
|
||||
pub max_depth: u32,
|
||||
/// Default shared token budget for a Workflow run and its children.
|
||||
/// `0` applies no shared cap — the run is advisory-only like the parent
|
||||
/// turn loop — while any positive value is enforced across the run.
|
||||
#[serde(default = "default_workflow_default_token_budget")]
|
||||
pub default_token_budget: u64,
|
||||
/// How many parallel write children may share the parent worktree without
|
||||
@@ -2521,7 +2523,11 @@ fn default_workflow_max_depth() -> u32 {
|
||||
}
|
||||
|
||||
fn default_workflow_default_token_budget() -> u64 {
|
||||
120_000
|
||||
// Off by default: a cap the caller never asked for must not throttle a
|
||||
// run — a 120k default silently killed real fan-outs mid-task (#6189).
|
||||
// Spend discipline stays available as an explicit opt-in (tool
|
||||
// `token_budget`, spec `budget.max_tokens`, or a configured value here).
|
||||
0
|
||||
}
|
||||
|
||||
fn default_workflow_max_parallel_writes_without_worktree() -> u32 {
|
||||
|
||||
@@ -8252,7 +8252,9 @@ fn workflow_config_defaults_match_product_surface() {
|
||||
assert_eq!(defaults.max_children, 1000);
|
||||
assert_eq!(defaults.max_concurrent, 16);
|
||||
assert_eq!(defaults.max_depth, 5);
|
||||
assert_eq!(defaults.default_token_budget, 120_000);
|
||||
// 0 = no shared cap; budgets are opt-in, matching the parent turn loop's
|
||||
// advisory policy (#6189).
|
||||
assert_eq!(defaults.default_token_budget, 0);
|
||||
assert_eq!(defaults.max_parallel_writes_without_worktree, 0);
|
||||
assert!(defaults.persist_completed_activity);
|
||||
assert!(defaults.persist_completed_across_restarts);
|
||||
|
||||
@@ -275,7 +275,7 @@ pub(in crate::commands) fn workflow_settings(app: &App) -> CommandResult {
|
||||
cfg.max_children, cfg.max_concurrent, cfg.max_depth
|
||||
),
|
||||
format!(
|
||||
"default_token_budget = {} · shared admission hint for a run and its children",
|
||||
"default_token_budget = {} · shared admission cap for a run and its children (0 = none)",
|
||||
cfg.default_token_budget
|
||||
),
|
||||
format!(
|
||||
|
||||
@@ -1119,7 +1119,7 @@ impl ToolSpec for WorkflowTool {
|
||||
"token_budget": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"description": "Optional shared Workflow admission hint. Usage is reconciled when children report completion; already-running parallel children can take aggregate spent past the hint, while later and descendant spawns are rejected once exhausted."
|
||||
"description": "Optional shared Workflow admission cap; omit for no cap. Usage is reconciled when children report completion; already-running parallel children can take aggregate spent past the cap, while later and descendant spawns are rejected once exhausted."
|
||||
},
|
||||
"wait": {
|
||||
"type": "boolean",
|
||||
|
||||
@@ -184,7 +184,12 @@ pub fn analyze_workflow_plan_approval_with_config(
|
||||
let network = script_suggests_network(script);
|
||||
let worktree = script.contains("worktree") || script.contains("isolation");
|
||||
let token_budget = optional_u64(input, "token_budget");
|
||||
let high_budget = token_budget.is_some_and(|b| b > config.default_token_budget);
|
||||
// The card reports what the run will actually get: the caller's budget,
|
||||
// else the configured default when one is set — 0 means none applies.
|
||||
let effective_budget = token_budget
|
||||
.filter(|b| *b > 0)
|
||||
.or((config.default_token_budget > 0).then_some(config.default_token_budget));
|
||||
let high_budget = is_high_budget(token_budget, config);
|
||||
// Unknown script authority: always elevated so the card is required.
|
||||
let elevated = true;
|
||||
let mut reasons = vec!["script_or_source".to_string()];
|
||||
@@ -224,7 +229,7 @@ pub fn analyze_workflow_plan_approval_with_config(
|
||||
high_budget,
|
||||
broader_authority: false,
|
||||
token_budget,
|
||||
budget_label: budget_label(token_budget, high_budget),
|
||||
budget_label: budget_label(effective_budget, high_budget),
|
||||
elevated,
|
||||
reasons,
|
||||
}
|
||||
@@ -432,7 +437,7 @@ fn analyze_plan_object(
|
||||
worktree = true;
|
||||
}
|
||||
|
||||
let high_budget = token_budget.is_some_and(|b| b > config.default_token_budget);
|
||||
let high_budget = is_high_budget(token_budget, config);
|
||||
let mut reasons = Vec::new();
|
||||
if writes {
|
||||
reasons.push("writes".into());
|
||||
@@ -709,11 +714,17 @@ fn optional_u64(value: &Value, key: &str) -> Option<u64> {
|
||||
value.get(key).and_then(Value::as_u64)
|
||||
}
|
||||
|
||||
/// A budget is "high" only against a configured baseline; when
|
||||
/// `[workflow].default_token_budget` is 0 (the default) nothing is high.
|
||||
fn is_high_budget(token_budget: Option<u64>, config: &WorkflowConfigToml) -> bool {
|
||||
config.default_token_budget > 0 && token_budget.is_some_and(|b| b > config.default_token_budget)
|
||||
}
|
||||
|
||||
fn budget_label(token_budget: Option<u64>, high_budget: bool) -> String {
|
||||
match token_budget {
|
||||
Some(n) if high_budget => format!("{n} tokens (high)"),
|
||||
Some(n) => format!("{n} tokens"),
|
||||
None => "default".to_string(),
|
||||
None => "unbounded".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -934,12 +945,20 @@ mod tests {
|
||||
"children": [{ "prompt": "scan", "type": "explore" }]
|
||||
}
|
||||
});
|
||||
let summary = analyze_workflow_plan_approval(&input);
|
||||
// With no configured baseline (the default) nothing is "high" — the
|
||||
// flag only exists relative to an operator-set cap (#6189).
|
||||
let uncapped = analyze_workflow_plan_approval(&input);
|
||||
assert!(!uncapped.high_budget, "{uncapped:?}");
|
||||
let config = WorkflowConfigToml {
|
||||
default_token_budget: 120_000,
|
||||
..WorkflowConfigToml::default()
|
||||
};
|
||||
let summary = analyze_workflow_plan_approval_with_config(&input, &config);
|
||||
assert!(summary.high_budget, "{summary:?}");
|
||||
assert!(summary.elevated);
|
||||
assert!(summary.budget_label.contains("high"));
|
||||
assert_eq!(
|
||||
workflow_approval_requirement_for(&input, &config()),
|
||||
workflow_approval_requirement_for(&input, &config),
|
||||
ApprovalRequirement::Required
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,8 +11,9 @@ use crate::{
|
||||
leaf_is_write_capable, leaf_wants_worktree,
|
||||
};
|
||||
|
||||
/// Default soft token budget from product config (`[workflow].default_token_budget`).
|
||||
/// Plans requesting more than this are treated as high-budget.
|
||||
/// Fallback high-budget flag threshold for approval display when a caller
|
||||
/// does not thread `[workflow].default_token_budget` through. Display-only:
|
||||
/// it never caps a run.
|
||||
pub const DEFAULT_HIGH_BUDGET_THRESHOLD: u64 = 120_000;
|
||||
|
||||
/// Options that refine elevation assessment beyond the IR itself.
|
||||
@@ -20,7 +21,8 @@ pub const DEFAULT_HIGH_BUDGET_THRESHOLD: u64 = 120_000;
|
||||
pub struct ElevationOptions {
|
||||
/// Token budget declared on the tool call (may outrank `spec.budget`).
|
||||
pub token_budget: Option<u64>,
|
||||
/// Threshold above which a token budget is considered high.
|
||||
/// Threshold above which a token budget is considered high. `0` disables
|
||||
/// the flag — with no configured baseline there is nothing to exceed.
|
||||
pub high_budget_threshold: u64,
|
||||
/// Whether the parent session currently allows writes.
|
||||
pub parent_allows_write: bool,
|
||||
@@ -143,7 +145,8 @@ pub fn assess_workflow_elevation(
|
||||
.token_budget
|
||||
.or(spec.budget.max_tokens)
|
||||
.filter(|n| *n > 0);
|
||||
let high_budget = effective_tokens.is_some_and(|n| n > options.high_budget_threshold);
|
||||
let high_budget = options.high_budget_threshold > 0
|
||||
&& effective_tokens.is_some_and(|n| n > options.high_budget_threshold);
|
||||
|
||||
let broader_authority =
|
||||
(!options.parent_allows_write && writes) || (!options.parent_allows_network && network);
|
||||
|
||||
@@ -49,7 +49,7 @@ request orchestration explicitly.
|
||||
| `require_approval_for_writes` | `true` | Gates the plan-approval card for writes / elevated starts |
|
||||
| `auto_start_child_limit` | `16` | Soft cap on automatic child count |
|
||||
| `max_children` / `max_concurrent` / `max_depth` | `1000` / `16` / `5` | Task count, concurrent children, and plan structure ceilings |
|
||||
| `default_token_budget` | `120000` | Shared admission hint; not an exact mid-stream cutoff |
|
||||
| `default_token_budget` | `0` | Shared admission cap for a run and its children; `0` = none — set it or pass `token_budget` on the call to bound spend |
|
||||
| `persist_completed_activity` | `true` | Keep completed panel/history activity |
|
||||
|
||||
Elevated work (writes, shell beyond read-only, network, secrets, worktrees, high
|
||||
|
||||
Reference in new issue
Block a user