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:
@@ -152,6 +152,13 @@ export const api = {
|
||||
`/api/chats/${chatId}/force_send`,
|
||||
{ method: 'POST', body: JSON.stringify({ content }) }
|
||||
),
|
||||
// v1.8.2: extend an inference that hit the tool budget. `sentinelMessageId`
|
||||
// is the cap-hit sentinel message the user clicked Continue on.
|
||||
continue: (chatId: string, sentinelMessageId: string) =>
|
||||
request<{ assistant_message_id: string }>(
|
||||
`/api/chats/${chatId}/continue`,
|
||||
{ method: 'POST', body: JSON.stringify({ sentinel_message_id: sentinelMessageId }) }
|
||||
),
|
||||
fork: (chatId: string, body: { messageId: string; name?: string }) =>
|
||||
request<Chat>(`/api/chats/${chatId}/fork`, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -44,6 +44,10 @@ export interface Agent {
|
||||
tools: string[];
|
||||
model: string | null;
|
||||
source: AgentSource;
|
||||
// v1.8.2: per-agent tool-loop budget. null means resolve at runtime from
|
||||
// the agent's toolset (30 for all read-only, 10 otherwise) or 15 for raw
|
||||
// chat with no agent.
|
||||
max_tool_calls: number | null;
|
||||
}
|
||||
|
||||
export interface AgentParseError {
|
||||
@@ -89,6 +93,32 @@ export interface ToolResult {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// v1.8.2: structured reason codes that flow through error frames / metadata.
|
||||
// `error` text stays human; `reason` is the discriminator the UI matches on.
|
||||
export type ErrorReason =
|
||||
| 'llm_provider_error'
|
||||
| 'tool_execution_failed'
|
||||
| 'summary_after_cap_failed';
|
||||
|
||||
// v1.8.2: shapes stored in Message.metadata. Discriminated on `kind`.
|
||||
// cap_hit — sentinel emitted when the tool budget is hit; carries the
|
||||
// budget + agent name + whether Continue is still allowed.
|
||||
// error — attached to a failed assistant message so the bubble can show
|
||||
// a specific reason on reload (WS error frame is one-shot).
|
||||
export type MessageMetadata =
|
||||
| {
|
||||
kind: 'cap_hit';
|
||||
used: number;
|
||||
limit: number;
|
||||
agent_name: string | null;
|
||||
can_continue: boolean;
|
||||
}
|
||||
| {
|
||||
kind: 'error';
|
||||
error_reason: ErrorReason;
|
||||
error_text: string;
|
||||
};
|
||||
|
||||
export interface Message {
|
||||
id: string;
|
||||
session_id: string;
|
||||
@@ -106,6 +136,9 @@ export interface Message {
|
||||
started_at: string | null;
|
||||
finished_at: string | null;
|
||||
created_at: string;
|
||||
// v1.8.2: per-message metadata; see MessageMetadata. null for the vast
|
||||
// majority of messages.
|
||||
metadata: MessageMetadata | null;
|
||||
}
|
||||
|
||||
export interface ModelInfo {
|
||||
@@ -225,7 +258,13 @@ export type WsFrame =
|
||||
ctx_max?: number | null;
|
||||
started_at?: string | null;
|
||||
finished_at?: string | null;
|
||||
// v1.8.2: piggybacks the persisted metadata onto the terminal frame so
|
||||
// cap-hit sentinels (and any future stamped-on-complete metadata) flow
|
||||
// to the client without a refetch.
|
||||
metadata?: MessageMetadata | null;
|
||||
}
|
||||
| { type: 'messages_deleted'; message_ids: string[]; chat_id?: string }
|
||||
| { type: 'chat_renamed'; chat_id: string; name: string }
|
||||
| { type: 'error'; message_id?: string; chat_id?: string; error: string };
|
||||
// v1.8.2: `reason` discriminates structured failures (the UI prefers it
|
||||
// over `error` text when present).
|
||||
| { type: 'error'; message_id?: string; chat_id?: string; error: string; reason?: ErrorReason };
|
||||
|
||||
Reference in New Issue
Block a user