mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-07-10 01:29:41 +00:00
Populate OpenCode skills and subagents breakdowns (#557)
The Skills & Agents panel was always empty for OpenCode sessions even when skills and subagents were used. buildAssistantCall (shared by the OpenCode SQLite and file-based parsers, and Kilo Code) counted the skill/task tool invocations but never read the skill name or subagent type from the tool-call input, so the dedicated breakdowns had no names to aggregate. In OpenCode's part files the identifier lives in state.input: the skill tool carries input.name and the task tool carries input.subagent_type. Extract both in buildAssistantCall and surface them on ParsedProviderCall.skills / .subagentTypes, then stop hard-coding skills: [] in providerCallToTurn and providerCallToCachedCall so the classifier's subCategory and the subagent breakdown receive the data. Verified against real OpenCode data: the previously empty skills[] and subagents[] now populate (pipeline-investigation, plan-spec, splunk, ... and explore/general subagents). Fixes #556 Co-authored-by: Kevin Addison <kevin.addison3@tesco.com>
This commit is contained in:
parent
7cd1f90631
commit
d5002f1deb
4 changed files with 61 additions and 3 deletions
|
|
@ -1707,7 +1707,7 @@ function providerCallToTurn(call: ParsedProviderCall): ParsedTurn {
|
|||
costUSD: call.costUSD,
|
||||
tools,
|
||||
mcpTools: extractMcpTools(tools),
|
||||
skills: [],
|
||||
skills: call.skills ?? [],
|
||||
subagentTypes: call.subagentTypes ?? [],
|
||||
hasAgentSpawn: tools.includes('Agent'),
|
||||
hasPlanMode: tools.includes('EnterPlanMode'),
|
||||
|
|
@ -1746,7 +1746,7 @@ function providerCallToCachedCall(call: ParsedProviderCall): CachedCall {
|
|||
timestamp: call.timestamp,
|
||||
tools: call.tools,
|
||||
bashCommands: call.bashCommands,
|
||||
skills: [],
|
||||
skills: call.skills ?? [],
|
||||
subagentTypes: call.subagentTypes ?? [],
|
||||
deduplicationKey: call.deduplicationKey,
|
||||
project: call.project,
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export type PartData = {
|
|||
type: string
|
||||
text?: string
|
||||
tool?: string
|
||||
state?: { input?: { command?: string } }
|
||||
state?: { input?: { command?: string; name?: string; subagent_type?: string } }
|
||||
}
|
||||
|
||||
const toolNameMap: Record<string, string> = {
|
||||
|
|
@ -117,6 +117,18 @@ export function buildAssistantCall(opts: {
|
|||
.filter((p) => p.tool === 'bash' && typeof p.state?.input?.command === 'string')
|
||||
.flatMap((p) => extractBashCommands(p.state!.input!.command!))
|
||||
|
||||
// The skill/subagent name lives in the tool-call input, not the tool name, so
|
||||
// the Skills & Agents breakdown needs these extracted alongside the tool list.
|
||||
const skills = toolParts
|
||||
.filter((p) => p.tool === 'skill' && typeof p.state?.input?.name === 'string')
|
||||
.map((p) => p.state!.input!.name!)
|
||||
.filter(Boolean)
|
||||
|
||||
const subagentTypes = toolParts
|
||||
.filter((p) => p.tool === 'task' && typeof p.state?.input?.subagent_type === 'string')
|
||||
.map((p) => p.state!.input!.subagent_type!)
|
||||
.filter(Boolean)
|
||||
|
||||
const model = data.modelID ?? data.model ?? 'unknown'
|
||||
let costUSD = calculateCost(
|
||||
model,
|
||||
|
|
@ -144,6 +156,8 @@ export function buildAssistantCall(opts: {
|
|||
costUSD,
|
||||
tools,
|
||||
bashCommands,
|
||||
skills,
|
||||
subagentTypes,
|
||||
timestamp: parseTimestamp(opts.timeCreatedMs),
|
||||
speed: 'standard',
|
||||
deduplicationKey: opts.dedupKey,
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@ export type ParsedProviderCall = {
|
|||
// Subagent types spawned in this call (e.g. 'general-purpose'). Feeds the
|
||||
// Skills & Agents breakdown; optional since most providers don't expose it.
|
||||
subagentTypes?: string[]
|
||||
// Skill names invoked in this call (e.g. 'commit'). Feeds the Skills & Agents
|
||||
// breakdown; optional since most providers don't expose it.
|
||||
skills?: string[]
|
||||
timestamp: string
|
||||
speed: 'standard' | 'fast'
|
||||
deduplicationKey: string
|
||||
|
|
|
|||
|
|
@ -138,6 +138,47 @@ describe('opencode file-based provider - parsing', () => {
|
|||
expect(c.deduplicationKey).toBe('opencode:ses_test1:msg_a')
|
||||
})
|
||||
|
||||
it('extracts skill names and subagent types from skill/task tool parts', async () => {
|
||||
await writeSession({
|
||||
messages: [{
|
||||
id: 'msg_a',
|
||||
data: {
|
||||
role: 'assistant', modelID: 'gpt-5.3-codex-spark', cost: 0,
|
||||
tokens: { input: 100, output: 20, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
time: { created: 1 },
|
||||
},
|
||||
parts: [
|
||||
{ type: 'tool', tool: 'skill', state: { input: { name: 'commit' } } },
|
||||
{ type: 'tool', tool: 'task', state: { input: { description: 'find files', subagent_type: 'explore' } } },
|
||||
{ type: 'text', text: 'done' },
|
||||
],
|
||||
}],
|
||||
})
|
||||
const calls = await parseAll()
|
||||
expect(calls).toHaveLength(1)
|
||||
const c = calls[0]!
|
||||
expect(c.tools).toEqual(['Skill', 'Agent'])
|
||||
expect(c.skills).toEqual(['commit'])
|
||||
expect(c.subagentTypes).toEqual(['explore'])
|
||||
})
|
||||
|
||||
it('leaves skills and subagentTypes empty when no skill/task parts are present', async () => {
|
||||
await writeSession({
|
||||
messages: [{
|
||||
id: 'msg_a',
|
||||
data: {
|
||||
role: 'assistant', modelID: 'gpt-5.3-codex-spark', cost: 0,
|
||||
tokens: { input: 100, output: 20, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
time: { created: 1 },
|
||||
},
|
||||
parts: [{ type: 'tool', tool: 'bash', state: { input: { command: 'ls' } } }],
|
||||
}],
|
||||
})
|
||||
const calls = await parseAll()
|
||||
expect(calls[0]!.skills).toEqual([])
|
||||
expect(calls[0]!.subagentTypes).toEqual([])
|
||||
})
|
||||
|
||||
it('skips an errored or empty assistant turn (all-zero tokens, no parts)', async () => {
|
||||
await writeSession({
|
||||
messages: [{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue