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:
@@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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' }] } },
|
||||||
|
|||||||
Reference in New Issue
Block a user