v1.8.2: tool loop cap-hit summary + tool call UI compaction
Old hardcoded MAX_TOOL_LOOP_DEPTH=15 replaced by per-agent max_tool_calls (1-100, AGENTS.md frontmatter) with defaults: 30 for read-only-only agents, 10 for agents that include any non-read-only tool, 15 for raw chat. When the loop hits cap, fire one final summary call with tools disabled, stream the wrap-up into the in-flight assistant message, then insert a system sentinel with metadata.kind='cap_hit'. The sentinel renders an amber bubble with a Continue button (latest sentinel only) that POSTs to a new /api/chats/:id/continue route to extend. Hard ceiling: 3 cap-hits per chat (2 continues max) — third sentinel reports can_continue=false. Error frames carry a machine-readable reason code alongside human error text. Failed messages persist the reason via metadata.kind='error' so the bubble renders specifics on reload (WS error frame is one-shot). Tool call UI rewired: ToolCallLine renders inline (↳ name args spinner/check/✗, expand-on-tap for args+result); ToolCallGroup collapses 3+ consecutive same-tool runs into a compact card. MessageList owns a three-pass pre-render (flatten + fold tool results onto matching runs by id + group same-tool runs + number sentinels). MessageBubble drops tool rendering and adds the sentinel / error-reason branches. ToolCallCard deleted. Roadmap follow-up logged: add explicit max_tool_calls: 30 to the 6 agents in /data/AGENTS.md and /opt/boocode/AGENTS.md post-ship for discoverability (defaults handle behavior identically). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,9 @@ interface ParsedFrontmatter {
|
||||
tools?: string[];
|
||||
description?: string;
|
||||
model?: string;
|
||||
// v1.8.2: optional per-agent tool-loop budget. Absent → inference resolves
|
||||
// from the agent's toolset at runtime.
|
||||
max_tool_calls?: number;
|
||||
}
|
||||
|
||||
function stripQuotes(s: string): string {
|
||||
@@ -89,6 +92,21 @@ function parseFrontmatter(yaml: string): { data: ParsedFrontmatter; errors: stri
|
||||
data.description = stripQuotes(valueRaw);
|
||||
} else if (key === 'model') {
|
||||
data.model = stripQuotes(valueRaw);
|
||||
} else if (key === 'max_tool_calls') {
|
||||
// v1.8.2: 1..100 inclusive integer. Out-of-range values are skipped
|
||||
// with a warning rather than throwing — agents shouldn't be unusable
|
||||
// because of a typo on a defaulted field. Non-numeric or non-integer
|
||||
// still hard-fails the block, matching `temperature` behavior.
|
||||
const n = Number(valueRaw);
|
||||
if (Number.isInteger(n) && n >= 1 && n <= 100) {
|
||||
data.max_tool_calls = n;
|
||||
} else if (Number.isInteger(n)) {
|
||||
console.warn(
|
||||
`agents: max_tool_calls ${n} out of range 1-100, ignoring (falling back to default)`,
|
||||
);
|
||||
} else {
|
||||
errors.push(`max_tool_calls must be an integer 1-100 (got "${valueRaw}")`);
|
||||
}
|
||||
}
|
||||
// Unknown keys silently ignored — forward-compat.
|
||||
}
|
||||
@@ -177,6 +195,7 @@ function parseAgentSection(section: RawSection): Omit<Agent, 'source'> {
|
||||
temperature: typeof fm.temperature === 'number' ? fm.temperature : DEFAULT_TEMPERATURE,
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user