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>
61 lines
2.0 KiB
TypeScript
61 lines
2.0 KiB
TypeScript
import type { FastifyInstance } from 'fastify';
|
|
import type { Sql } from '../db.js';
|
|
import type { Broker } from '../services/broker.js';
|
|
import type { Message } from '../types/api.js';
|
|
|
|
export function registerWebSocket(
|
|
app: FastifyInstance,
|
|
sql: Sql,
|
|
broker: Broker
|
|
): void {
|
|
app.get<{ Params: { id: string } }>(
|
|
'/api/ws/sessions/:id',
|
|
{ websocket: true },
|
|
async (socket, req) => {
|
|
const sessionId = req.params.id;
|
|
|
|
const session = await sql<{ id: string }[]>`SELECT id FROM sessions WHERE id = ${sessionId}`;
|
|
if (session.length === 0) {
|
|
socket.send(JSON.stringify({ type: 'error', error: 'session not found' }));
|
|
socket.close(1008, 'session not found');
|
|
return;
|
|
}
|
|
|
|
const messages = await sql<Message[]>`
|
|
SELECT id, session_id, chat_id, role, content, kind, tool_calls, tool_results, status, last_seq,
|
|
tokens_used, ctx_used, ctx_max, started_at, finished_at, created_at, metadata
|
|
FROM messages
|
|
WHERE session_id = ${sessionId}
|
|
ORDER BY created_at ASC, id ASC
|
|
`;
|
|
socket.send(JSON.stringify({ type: 'snapshot', messages }));
|
|
|
|
const unsubscribe = broker.subscribe(sessionId, (frame) => {
|
|
if (socket.readyState !== socket.OPEN) return;
|
|
try {
|
|
socket.send(JSON.stringify(frame));
|
|
} catch (err) {
|
|
app.log.warn({ err, sessionId }, 'ws send failed');
|
|
}
|
|
});
|
|
|
|
socket.on('close', () => unsubscribe());
|
|
socket.on('error', () => unsubscribe());
|
|
}
|
|
);
|
|
|
|
app.get('/api/ws/user', { websocket: true }, async (socket) => {
|
|
const user = 'default';
|
|
const unsubscribe = broker.subscribeUser(user, (frame) => {
|
|
if (socket.readyState !== socket.OPEN) return;
|
|
try {
|
|
socket.send(JSON.stringify(frame));
|
|
} catch (err) {
|
|
app.log.warn({ err, user }, 'user ws send failed');
|
|
}
|
|
});
|
|
socket.on('close', () => unsubscribe());
|
|
socket.on('error', () => unsubscribe());
|
|
});
|
|
}
|