v1.14.0-outer-loop: explicit while loop replaces inference recursion
Converts the ad-hoc executeToolPhase → runAssistantTurn recursion into an explicit while (stepNumber < effectiveCap) loop. A step is one stream-and- tool-execute iteration; the loop terminates on non-tool finish, step-cap hit, doom-loop, budget exhaustion, abort, or synthesis success. MAX_STEPS = 200 hard ceiling (4x old effective limit from budget). Per-agent steps: field in AGENTS.md frontmatter sets tighter caps (Refactorer: 5, Architect: 20, others: unset = bounded only by MAX_STEPS). Resolution: effectiveCap = Math.min(agent.steps ?? Infinity, MAX_STEPS). executeToolPhase no longer recurses — returns ToolPhaseResult struct (action: 'continue' | 'paused' | 'synthesis_done') so the caller decides whether to continue or break. steps: 0 handled as "no tool calls allowed" via runTextOnlyTurn (one text-only stream phase, tool calls ignored with warn log). Step-cap hits produce a sentinel summary (reuses cap_hit kind so CapHitSentinel.tsx renders without frontend changes; text distinguishes "Step limit reached" from "Tool budget exhausted"). Doom-loop check migrated to top of loop body — same predicate, same threshold (3), break instead of return. step_start parts are in the schema CHECK but not emitted as message_parts — writing before the stream phase creates a sequence-0 collision with partsFromAssistantMessage. Structured log line emitted instead. Adversarial review caught the collision pre-deploy. 332/332 server tests passing. No frontend changes. No schema changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,10 @@ interface ParsedFrontmatter {
|
||||
// v1.8.2: optional per-agent tool-loop budget. Absent → inference resolves
|
||||
// from the agent's toolset at runtime.
|
||||
max_tool_calls?: number;
|
||||
// v1.14.0: optional per-agent step cap. Absent → bounded only by MAX_STEPS
|
||||
// (200) in the outer loop. Integer ≥ 0; steps: 0 means "no tool calls
|
||||
// allowed" — the model responds text-only.
|
||||
steps?: number;
|
||||
}
|
||||
|
||||
function stripQuotes(s: string): string {
|
||||
@@ -112,6 +116,21 @@ function parseFrontmatter(yaml: string): { data: ParsedFrontmatter; errors: stri
|
||||
} else {
|
||||
errors.push(`max_tool_calls must be an integer 1-100 (got "${valueRaw}")`);
|
||||
}
|
||||
} else if (key === 'steps') {
|
||||
// v1.14.0: per-agent step cap for the outer inference loop. Integer ≥ 0.
|
||||
// steps: 0 means "no tool calls allowed" — model responds text-only.
|
||||
// Non-integer or negative values are warned and ignored (falls back to
|
||||
// MAX_STEPS ceiling), matching the max_tool_calls pattern above.
|
||||
const n = Number(valueRaw);
|
||||
if (Number.isInteger(n) && n >= 0) {
|
||||
data.steps = n;
|
||||
} else if (Number.isInteger(n)) {
|
||||
console.warn(
|
||||
`agents: steps ${n} is negative, ignoring (falling back to default)`,
|
||||
);
|
||||
} else {
|
||||
errors.push(`steps must be a non-negative integer (got "${valueRaw}")`);
|
||||
}
|
||||
}
|
||||
// Unknown keys silently ignored — forward-compat.
|
||||
}
|
||||
@@ -204,6 +223,7 @@ function parseAgentSection(section: RawSection): Omit<Agent, 'source'> {
|
||||
tools: filteredTools,
|
||||
model: typeof fm.model === 'string' && fm.model.length > 0 ? fm.model : null,
|
||||
max_tool_calls: typeof fm.max_tool_calls === 'number' ? fm.max_tool_calls : null,
|
||||
steps: typeof fm.steps === 'number' ? fm.steps : null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user