fix(observabilidade): parser extrai skills de tool_result.content (string e array)

Antes: skills_invoked vazio em 1608/1608 sessões porque detectSkillInvoked
apenas era aplicado ao text extraído de content[type=text]. A string
'Launching skill: X' vive dentro de tool_result.content (string ou array
de text blocks), que era ignorada.

Fix: adicionar helper extractResultText(r) que trata ambos os casos e
aplicar detectSkillInvoked + detectHook também ao tool_result. Após
re-indexação full, 526/1616 sessões têm agora skills detectadas e o
detector de padrões devolve 6 padrões (vs 2 baseline), incluindo
skills_with_high_error_rate reais.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-23 02:20:24 +01:00
parent 1eb4f246de
commit ac4e9c6f35
2 changed files with 59 additions and 3 deletions
+21 -3
View File
@@ -19,6 +19,21 @@ function detectHook(text: string | null): string | null {
return m ? m[1] : null return m ? m[1] : null
} }
function extractResultText(r: unknown): string | null {
if (r == null) return null
if (typeof r === 'string') return r
if (Array.isArray(r)) {
const parts: string[] = []
for (const p of r) {
if (p && typeof p === 'object' && 'text' in p && typeof (p as { text: unknown }).text === 'string') {
parts.push((p as { text: string }).text)
}
}
return parts.length ? parts.join('\n') : null
}
return null
}
function extractText(rawMsg: unknown): string | null { function extractText(rawMsg: unknown): string | null {
if (!rawMsg || typeof rawMsg !== 'object') return null if (!rawMsg || typeof rawMsg !== 'object') return null
const msg = rawMsg as { content?: unknown } const msg = rawMsg as { content?: unknown }
@@ -132,9 +147,12 @@ export async function parseSessionFile(jsonlPath: string): Promise<ParseResult>
} }
} }
const resultText = extractResultText(toolResult)
const skill = detectSkillInvoked(text) const skill = detectSkillInvoked(text)
if (skill) skillsInvoked.add(skill) const skillFromResult = detectSkillInvoked(resultText)
const hook = detectHook(text) const finalSkill = skill ?? skillFromResult
if (finalSkill) skillsInvoked.add(finalSkill)
const hook = detectHook(text) ?? detectHook(resultText)
events.push({ events.push({
index: idx++, index: idx++,
@@ -145,7 +163,7 @@ export async function parseSessionFile(jsonlPath: string): Promise<ParseResult>
tool_name: toolName, tool_name: toolName,
tool_input: toolInput, tool_input: toolInput,
tool_result: toolResult, tool_result: toolResult,
skill_invoked: skill, skill_invoked: finalSkill,
hook_name: hook, hook_name: hook,
}) })
} }
+38
View File
@@ -67,6 +67,44 @@ describe('parseSessionFile', () => {
expect(result.meta.skills_invoked).toContain('superpowers:brainstorming') expect(result.meta.skills_invoked).toContain('superpowers:brainstorming')
}) })
it('detecta skill invocation em tool_result.content (string)', async () => {
const path = writeJsonl([
{
type: 'user',
timestamp: '2026-04-23T10:00:00Z',
message: {
role: 'user',
content: [
{ type: 'tool_result', tool_use_id: 'abc', content: 'Launching skill: infraestrutura:easypanel-monitor\nOther log output' },
],
},
},
])
const result = await parseSessionFile(path)
expect(result.meta.skills_invoked).toContain('infraestrutura:easypanel-monitor')
})
it('detecta skill invocation em tool_result.content (array de text blocks)', async () => {
const path = writeJsonl([
{
type: 'user',
timestamp: '2026-04-23T10:00:00Z',
message: {
role: 'user',
content: [
{
type: 'tool_result',
tool_use_id: 'abc',
content: [{ type: 'text', text: 'Launching skill: superpowers:brainstorming' }],
},
],
},
},
])
const result = await parseSessionFile(path)
expect(result.meta.skills_invoked).toContain('superpowers:brainstorming')
})
it('ignora linhas JSON inválidas silenciosamente', async () => { it('ignora linhas JSON inválidas silenciosamente', async () => {
const path = writeJsonl([ const path = writeJsonl([
{ type: 'user', message: { role: 'user', content: [{ type: 'text', text: 'válido' }] } }, { type: 'user', message: { role: 'user', content: [{ type: 'text', text: 'válido' }] } },