feat(observer): differentiate error events by tool + summary
Closes brain-retro 2026-05-20 #7 — each tool_result.is_error now emits { kind:'error', tool:<name>, summary:<first 80 chars> }. Allows aggregation by tool (Bash/Edit/Read) + cause prefix (ENOENT/timeout/ 'String to replace not found'). Required updating existing 'emits error events for tool_result with is_error' test assertion (old shape had bare 'message' field). 4 new vitest tests + 1 existing relaxed, 286/286 GREEN. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -127,7 +127,16 @@ export function classifyTask(text) {
|
||||
function collectToolUse(entries) {
|
||||
const skills = [];
|
||||
const counts = {};
|
||||
let errorCount = 0;
|
||||
const errors = [];
|
||||
const idToTool = {};
|
||||
// First pass — build id→tool name map (tool_results may reference tools across messages)
|
||||
for (const e of entries) {
|
||||
const content = e && e.message && Array.isArray(e.message.content) ? e.message.content : [];
|
||||
for (const b of content) {
|
||||
if (b && b.type === 'tool_use') idToTool[b.id] = b.name || 'unknown';
|
||||
}
|
||||
}
|
||||
// Second pass — accumulate counts + per-error attribution
|
||||
for (const e of entries) {
|
||||
const content = e && e.message && Array.isArray(e.message.content) ? e.message.content : [];
|
||||
for (const block of content) {
|
||||
@@ -139,11 +148,15 @@ function collectToolUse(entries) {
|
||||
skills.push((block.input && block.input.skill) || 'unknown');
|
||||
}
|
||||
} else if (block.type === 'tool_result' && block.is_error === true) {
|
||||
errorCount += 1;
|
||||
const tool = idToTool[block.tool_use_id] || 'unknown';
|
||||
const c = block.content;
|
||||
const text = typeof c === 'string' ? c
|
||||
: (Array.isArray(c) ? c.map((b) => (b && typeof b.text === 'string') ? b.text : '').join(' ') : '');
|
||||
errors.push({ tool, summary: text.slice(0, 80) });
|
||||
}
|
||||
}
|
||||
}
|
||||
return { skills, counts, errorCount };
|
||||
return { skills, counts, errors };
|
||||
}
|
||||
|
||||
const FILE_TOOLS = new Set(['Read', 'Edit', 'Write', 'MultiEdit', 'NotebookEdit']);
|
||||
@@ -557,13 +570,13 @@ export function parseTranscript(transcriptText, fallbackSessionId = null) {
|
||||
const ended_at = stamps[stamps.length - 1] || started_at;
|
||||
const durationMs = new Date(ended_at) - new Date(started_at);
|
||||
|
||||
const { skills, counts, errorCount } = collectToolUse(turn);
|
||||
const { skills, counts, errors } = collectToolUse(turn);
|
||||
|
||||
const events = [];
|
||||
for (const skill of skills) events.push({ kind: 'skill_invoked', skill });
|
||||
if (Object.keys(counts).length > 0) events.push({ kind: 'tool_summary', counts });
|
||||
for (let i = 0; i < errorCount; i++) {
|
||||
events.push({ kind: 'error', message: 'tool_result reported is_error' });
|
||||
for (const err of errors) {
|
||||
events.push({ kind: 'error', tool: err.tool, summary: err.summary });
|
||||
}
|
||||
events.push(...extractProcessEvents(turn, broken, total, durationMs));
|
||||
events.push(...extractAskUserQuestionEvents(turn));
|
||||
|
||||
Reference in New Issue
Block a user