1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
//! Goal loop orchestrator — the persistent-objective control layer (#3215, and
//! its lineage #891 / #1976 / #2058 / #2029).
//!
//! This is the **Workflow goal layer**: the decision core that turns a one-shot
//! `/goal` into a persistent work loop. Given the durable goal status, the
//! accumulated usage (from the per-goal accounting wired in `crates/state`
//! `record_thread_goal_usage`), and a budget, it decides whether to **continue**
//! (re-dispatch another worker turn toward the objective) or **stop** with a
//! terminal status. It is the orchestrator in the Workflow≈ultracode mapping —
//! the loop that fans work out to workers (`worker_profile`) and verifies before
//! committing.
//!
//! Scope: **decision logic + types**. The engine (`core/engine.rs`) reads the
//! `SharedGoalState` snapshot after each turn and calls `decide_continuation`
//! to decide whether to re-dispatch. For operate-mode goals the only terminal
//! stops are a verified completion, a blocked report, or the continuation
//! backstop (`[goal] max_continuations`); token/time accounting stays visible
//! as telemetry but does not gate continuation — the run is unbounded like
//! grokbuild (`DEFAULT_AGENT_BUDGET` as call cap) and kimicode swarm
//! (`turnBudget` per-task, resumable after budget-reached). Log when the
//! backstop fires.
/// Default automatic cross-turn continuation policy for one goal run (#5052).
///
/// Goals are unlimited by default: completion, blocked status, or explicit
/// user control ends the run. Operators who want a circuit breaker can opt in
/// with `[goal] max_continuations`; `0` keeps the default unlimited behavior.
pub const DEFAULT_MAX_GOAL_CONTINUATIONS: u32 = 0;
/// Terminal or active state of a persistent goal.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GoalRunStatus {
/// Still working toward the objective.
Active,
/// The objective was achieved (the model self-reported done and, ideally, a
/// verifier confirmed — see `GoalGate`).
Completed,
/// The model reported it is blocked and needs the user.
#[allow(dead_code)]
Blocked,
}
/// Why the loop stopped, for a terminal decision.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StopReason {
/// Objective achieved.
Completed,
/// Model reported blocked.
#[allow(dead_code)]
Blocked,
/// Continuation circuit-breaker tripped (too many continuations without a
/// terminal signal).
ContinuationLimit,
}
/// Accumulated, durable progress for a goal run. Mirrors the fields wired by
/// `crates/state` `record_thread_goal_usage` (tokens_used / time_used_seconds)
/// plus a continuation counter the loop maintains.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct GoalProgress {
pub tokens_used: u64,
pub time_used_seconds: u64,
pub continuations: u32,
}
/// The optional token/time bounds on a goal run. `None` fields mean unbounded
/// for that resource; the continuation backstop (`max_continuations`) still
/// applies unless configured to `0`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GoalBudget {
pub token_budget: Option<u64>,
pub time_budget_seconds: Option<u64>,
/// Safety backstop on automatic continuation passes (#5052). `0` disables
/// the backstop: only terminal status stops the run.
pub max_continuations: u32,
}
impl GoalBudget {
/// No token or time cap. Terminal status, user control, and the default
/// continuation backstop still stop the run.
#[allow(dead_code)]
pub const fn unbounded() -> Self {
Self {
token_budget: None,
time_budget_seconds: None,
max_continuations: DEFAULT_MAX_GOAL_CONTINUATIONS,
}
}
/// A token budget for telemetry/UI. It never pauses an unbounded goal.
#[allow(dead_code)]
pub const fn with_token_budget(token_budget: u64) -> Self {
Self {
token_budget: Some(token_budget),
time_budget_seconds: None,
max_continuations: DEFAULT_MAX_GOAL_CONTINUATIONS,
}
}
/// Override the continuation backstop (`0` = unlimited until terminal
/// status).
#[allow(dead_code)]
#[must_use]
pub const fn with_max_continuations(mut self, max_continuations: u32) -> Self {
self.max_continuations = max_continuations;
self
}
}
/// The decision the loop makes after each worker turn.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContinuationDecision {
/// Re-dispatch another turn toward the objective.
Continue,
/// Stop; the goal run is terminal.
Stop(StopReason),
}
/// Decide whether a persistent goal run should continue after a turn.
///
/// Precedence (most authoritative first):
/// 1. A terminal model status (Completed / Blocked) ends the run.
/// 2. The configurable continuation backstop stops a pathological loop
/// (skipped entirely when configured to `0`).
/// 3. Otherwise continue — the loop runs to the completion gate, not to a
/// fixed pass count (#5052). Token/time budgets are advisory telemetry;
/// they are surfaced in the UI but do not stop the run (unbounded).
#[must_use]
pub fn decide_continuation(
status: GoalRunStatus,
progress: GoalProgress,
budget: GoalBudget,
) -> ContinuationDecision {
// 1. Terminal model signal wins.
match status {
GoalRunStatus::Completed => return ContinuationDecision::Stop(StopReason::Completed),
GoalRunStatus::Blocked => return ContinuationDecision::Stop(StopReason::Blocked),
GoalRunStatus::Active => {}
}
// 2. Token/time budgets are advisory only (unbounded). They are
// visible in the Goal chip + /cost but never pause the loop — like
// grokbuild's agent-call budget and kimicode swarm's per-task
// turnBudget with resume. Log if we are over budget, then continue.
if budget
.token_budget
.is_some_and(|limit| progress.tokens_used >= limit)
{
tracing::debug!(
tokens_used = progress.tokens_used,
token_budget = ?budget.token_budget,
"goal over token budget but continuing (unbounded)"
);
}
if let Some(secs) = budget.time_budget_seconds
&& progress.time_used_seconds >= secs
{
tracing::debug!(
time_used_seconds = progress.time_used_seconds,
time_budget_seconds = secs,
"goal over time budget but continuing (unbounded)"
);
}
// 3. Runaway-cost backstop. This deliberately uses the already-durable
// continuation counter instead of adding verifier fingerprints or another
// orchestration subsystem. `0` disables it — budget/terminal stops only.
if budget.max_continuations > 0 && progress.continuations >= budget.max_continuations {
tracing::warn!(
continuations = progress.continuations,
max_continuations = budget.max_continuations,
"goal continuation backstop fired: no terminal signal after the configured \
continuation limit ([goal] max_continuations)"
);
return ContinuationDecision::Stop(StopReason::ContinuationLimit);
}
// 4. Keep going.
ContinuationDecision::Continue
}
/// Whether the durable token usage has reached the active goal's budget.
///
/// Budgets are telemetry-only in unbounded goal mode. Keeping this shared
/// predicate false ensures preview and the live continuation loop agree that
/// crossing a token budget does not close the outbound gate.
#[must_use]
pub const fn token_budget_exhausted(_progress: GoalProgress, _budget: GoalBudget) -> bool {
false
}
/// Whether a stop reason represents success (Completed) vs. an early/forced exit.
/// Useful for the UI/status projection (#2666 token/time visibility).
#[must_use]
#[allow(dead_code)]
pub fn is_success(reason: StopReason) -> bool {
matches!(reason, StopReason::Completed)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn completed_status_stops_with_success() {
let d = decide_continuation(
GoalRunStatus::Completed,
GoalProgress::default(),
GoalBudget::unbounded(),
);
assert_eq!(d, ContinuationDecision::Stop(StopReason::Completed));
assert!(is_success(StopReason::Completed));
}
#[test]
fn blocked_status_stops_without_success() {
let d = decide_continuation(
GoalRunStatus::Blocked,
GoalProgress::default(),
GoalBudget::unbounded(),
);
assert_eq!(d, ContinuationDecision::Stop(StopReason::Blocked));
assert!(!is_success(StopReason::Blocked));
}
#[test]
fn active_under_budget_continues() {
let progress = GoalProgress {
tokens_used: 10,
time_used_seconds: 5,
continuations: 2,
};
let budget = GoalBudget {
token_budget: Some(1000),
time_budget_seconds: Some(600),
max_continuations: DEFAULT_MAX_GOAL_CONTINUATIONS,
};
assert_eq!(
decide_continuation(GoalRunStatus::Active, progress, budget),
ContinuationDecision::Continue
);
}
#[test]
fn default_goal_has_no_continuation_limit() {
let progress = GoalProgress {
continuations: 10_000,
..GoalProgress::default()
};
assert_eq!(
decide_continuation(GoalRunStatus::Active, progress, GoalBudget::unbounded()),
ContinuationDecision::Continue
);
}
#[test]
fn explicit_continuation_limit_stops_run() {
let configured_limit = 100;
let progress = GoalProgress {
continuations: configured_limit,
..GoalProgress::default()
};
let budget = GoalBudget::unbounded().with_max_continuations(configured_limit);
assert_eq!(
decide_continuation(GoalRunStatus::Active, progress, budget),
ContinuationDecision::Stop(StopReason::ContinuationLimit)
);
}
#[test]
fn operate_goal_continues_past_ten_when_budget_remains() {
// #5052 regression: the old hardcoded cap of 10 must not be a terminal
// stop. With no terminal signal, pass 10, 11, and far beyond keep
// continuing because the default has no hidden ceiling.
for continuations in [10, 11, 100, 10_000] {
let progress = GoalProgress {
tokens_used: 5_000,
time_used_seconds: 300,
continuations,
};
let budget = GoalBudget::with_token_budget(1_000_000);
assert_eq!(
decide_continuation(GoalRunStatus::Active, progress, budget),
ContinuationDecision::Continue,
"pass {continuations} must continue toward the completion gate",
);
}
}
#[test]
fn configured_backstop_halts_pathological_loop() {
let backstop = 25;
let progress = GoalProgress {
continuations: backstop,
..GoalProgress::default()
};
let budget = GoalBudget::unbounded().with_max_continuations(backstop);
assert_eq!(
decide_continuation(GoalRunStatus::Active, progress, budget),
ContinuationDecision::Stop(StopReason::ContinuationLimit)
);
}
#[test]
fn zero_backstop_is_unlimited_and_budget_advisory() {
// 0 = unlimited-with-budget-stops: no continuation count ends the run…
let progress = GoalProgress {
continuations: 10_000,
..GoalProgress::default()
};
let budget = GoalBudget::unbounded().with_max_continuations(0);
assert_eq!(
decide_continuation(GoalRunStatus::Active, progress, budget),
ContinuationDecision::Continue
);
// exceeded token budget is advisory — must still continue (unbounded)
let progress = GoalProgress {
tokens_used: 1_000,
continuations: 10_000,
..GoalProgress::default()
};
let budget = GoalBudget::with_token_budget(1_000).with_max_continuations(0);
assert_eq!(
decide_continuation(GoalRunStatus::Active, progress, budget),
ContinuationDecision::Continue,
"budget advisory — must continue even when over budget"
);
}
#[test]
fn token_budget_is_advisory_not_terminal() {
let progress = GoalProgress {
tokens_used: 1000,
continuations: 1,
..GoalProgress::default()
};
let budget = GoalBudget::with_token_budget(1000);
assert_eq!(
decide_continuation(GoalRunStatus::Active, progress, budget),
ContinuationDecision::Continue,
"token budget is advisory — unbounded run must continue"
);
}
#[test]
fn time_budget_is_advisory_not_terminal() {
let progress = GoalProgress {
time_used_seconds: 601,
continuations: 1,
..GoalProgress::default()
};
let budget = GoalBudget {
token_budget: None,
time_budget_seconds: Some(600),
max_continuations: DEFAULT_MAX_GOAL_CONTINUATIONS,
};
assert_eq!(
decide_continuation(GoalRunStatus::Active, progress, budget),
ContinuationDecision::Continue,
"time budget is advisory — unbounded run must continue"
);
}
#[test]
fn terminal_status_outranks_remaining_budget() {
// Completed wins even if there is plenty of budget left.
let progress = GoalProgress::default();
let budget = GoalBudget {
token_budget: Some(1_000_000),
time_budget_seconds: Some(86_400),
max_continuations: DEFAULT_MAX_GOAL_CONTINUATIONS,
};
assert_eq!(
decide_continuation(GoalRunStatus::Completed, progress, budget),
ContinuationDecision::Stop(StopReason::Completed)
);
}
}