Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Retain metrics-only sub-agent attribution
Co-authored-by: SivaKesava1 <11771739+SivaKesava1@users.noreply.github.com>
  • Loading branch information
Copilot and SivaKesava1 authored Oct 8, 2026
commit 2d4290883afd936ced6542e078e513fbabeab52a
4 changes: 3 additions & 1 deletion pkg/cli/token_usage_subagent.go
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,7 @@ func isPiSubagentModel(entry TokenUsageEntry, models []string, resolver *modelId

func accumulatePiMainUsage(main *AgentUsageBreakdown, entry TokenUsageEntry) {
credits := tokenUsageEntryCredits(entry)
servedModel := modelNameWithoutProvider(entry.Model)
main.Requests++
main.InputTokens += entry.InputTokens
main.OutputTokens += entry.OutputTokens
Expand All @@ -469,8 +470,9 @@ func accumulatePiMainUsage(main *AgentUsageBreakdown, entry TokenUsageEntry) {
main.ReasoningTokens += entry.ReasoningTokens
main.AIC += credits
main.TotalApiDurationMs += entry.DurationMs
main.ServedModels = appendUnique(main.ServedModels, servedModel)
main.Models = appendAgentModelUsage(main.Models, AgentModelUsage{
Model: entry.Model, ResolvedModel: normalizeModelIdentity(modelNameWithoutProvider(entry.Model)),
Model: entry.Model, ResolvedModel: normalizeModelIdentity(servedModel),
Requests: 1, TokenCoreMetrics: TokenCoreMetrics{
InputTokens: entry.InputTokens, OutputTokens: entry.OutputTokens,
CacheReadTokens: entry.CacheReadTokens, CacheWriteTokens: entry.CacheWriteTokens,
Expand Down
41 changes: 33 additions & 8 deletions pkg/cli/token_usage_subagent_session.go
Original file line number Diff line number Diff line change
Expand Up @@ -571,16 +571,24 @@ func (models *subagentSessionModels) observeMetrics(data sessionSubagentData) er
}

func (models *subagentSessionModels) observeAgentMetric(agentID string, metric sessionSubagentAgentMetric) error {
if agentID != "main" && models.agents[agentID] == nil && metric.AgentName != "" {
name := firstNonEmptyModel(metric.AgentDisplayName, metric.AgentName)
models.agents[agentID] = &SubagentModelRequest{AgentName: name, InvocationCount: 1}
}
name := firstNonEmptyModel(metric.AgentDisplayName, metric.AgentName)
role := "subagent"
if agentID == "main" {
name, role = "main", "main"
} else if name == "" && models.agents[agentID] != nil {
name = models.agents[agentID].AgentName
} else {
if name == "" {
name = agentID
}
row := models.agents[agentID]
if row == nil {
row = &SubagentModelRequest{AgentName: name}
models.agents[agentID] = row
} else if row.AgentName == "" {
row.AgentName = name
}
if row.RequestedModel == "" {
row.RequestedModel = agentMetricRequestedModel(metric)
}
}
agentUsage := models.agentUsage[agentID]
if agentUsage == nil {
Expand All @@ -602,6 +610,23 @@ func (models *subagentSessionModels) observeAgentMetric(agentID string, metric s
return models.addAgentMetricModelUsage(agentID, metric, agentUsage)
}

func agentMetricRequestedModel(metric sessionSubagentAgentMetric) string {
models := make([]string, 0, len(metric.ModelMetrics))
for model := range metric.ModelMetrics {
models = append(models, model)
}
slices.Sort(models)
for _, model := range models {
if metric.ModelMetrics[model].Requests.Count != nil && *metric.ModelMetrics[model].Requests.Count > 0 {
return model
}
}
if len(models) > 0 {
return models[0]
}
return ""
}

func (models *subagentSessionModels) addAgentMetricModelUsage(agentID string, metric sessionSubagentAgentMetric, agentUsage *AgentUsageBreakdown) error {
counts := make(map[string]int)
actuals := make(map[string]SubagentModelActual)
Expand Down Expand Up @@ -1022,8 +1047,8 @@ func addSubagentRequestGroup(grouped map[subagentModelKey]SubagentModelRequest,
current := *row
current.EffectiveModel = ""
current.ReasonCode = ""
combined := grouped[key]
if combined.InvocationCount == 0 {
combined, exists := grouped[key]
if !exists {
combined = current
} else {
combined.InvocationCount += current.InvocationCount
Expand Down
43 changes: 43 additions & 0 deletions pkg/cli/token_usage_subagent_session_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,43 @@
require.Zero(t, summary.AgentUsage[1].AIC)
}

func TestCopilotAgentMetricsWithoutLifecycleRetainAttribution(t *testing.T) {
root := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(root, "usage"), 0o755))
content := subagentSessionHeader +
subagentSessionRecord(`{"type":"session.init","data":{"sourceEngine":"copilot","sessionId":"metrics-only"}}`) +
subagentSessionRecord(`{"type":"session.shutdown","data":{"agentMetrics":`+strings.ReplaceAll(copilotSubagentIntegrationMetrics, "\n", "")+`}}`)

Check failure on line 468 in pkg/cli/token_usage_subagent_session_test.go

View workflow job for this annotation

GitHub Actions / verify-integration-build

undefined: copilotSubagentIntegrationMetrics
require.NoError(t, os.WriteFile(filepath.Join(root, "usage", "aw_session.jsonl"), []byte(content), 0o644))

_, _, parsedAgents, found, err := readSessionSubagentModelsDetailed(root)
require.NoError(t, err)
require.True(t, found)
require.Len(t, parsedAgents, 4)
summary := &TokenUsageSummary{}
augmentSubagentModelAttribution(root, summary)

require.Len(t, summary.AgentUsage, 4)
agents := make(map[string]AgentUsageBreakdown, len(summary.AgentUsage))
for _, agent := range summary.AgentUsage {
agents[agent.AgentName] = agent
}
require.InDelta(t, 389.122, agents["main"].AIC, 0.000001)
require.InDelta(t, 494.207, agents["subagent-research"].AIC, 0.000001)
require.InDelta(t, 40.977, agents["awf-routing"].AIC, 0.000001)
require.InDelta(t, 9.141, agents["ghaw-issues"].AIC, 0.000001)
require.Len(t, summary.SubagentModelRequests, 3)
for _, request := range summary.SubagentModelRequests {
require.Zero(t, request.InvocationCount)
require.Zero(t, request.CompletedCount)
require.Zero(t, request.FailedCount)
require.Zero(t, request.IncompleteCount)
require.Equal(t, "opus", request.RequestedModel)
}
require.Len(t, summary.SubagentModelActuals, 1)
require.Equal(t, 60, summary.SubagentModelActuals[0].Requests)
require.Equal(t, TokenCoreMetrics{InputTokens: 6000, OutputTokens: 600, CacheReadTokens: 60, CacheWriteTokens: 120}, summary.SubagentModelActuals[0].TokenCoreMetrics)
}

func TestPiUnifiedFixtureAttributesInterleavedRequestsFromFirewallEvents(t *testing.T) {
fixture := filepath.Join("testdata", "subagent_attribution", "pi-interleaved")
_, actuals, agents, found, err := readSessionSubagentModelsDetailed(fixture)
Expand All @@ -476,22 +513,28 @@
require.Equal(t, "z-1", entries[0].RequestID)

resolver := newModelIdentityResolver("")
agents = resolveAgentUsageModels(agents, resolver, tokenUsageEntryModels(entries)...)

Check failure on line 516 in pkg/cli/token_usage_subagent_session_test.go

View workflow job for this annotation

GitHub Actions / verify-integration-build

undefined: tokenUsageEntryModels
summary := &TokenUsageSummary{AgentUsage: agents, AICFound: true, TotalAIC: 2.35}
matchPiAgentUsageCredits(summary, entries, resolver)
reconcileAgentUsageCredits(summary, entries)

creditsByAgent := make(map[string]float64)
requestsByAgent := make(map[string]int)
var mainAgent *AgentUsageBreakdown
for _, agent := range summary.AgentUsage {
creditsByAgent[agent.AgentName] = agent.AIC
requestsByAgent[agent.AgentName] = agent.Requests
if agent.AgentType == "main" {
mainAgent = &agent
}
}
require.InDelta(t, 0.9, creditsByAgent["a-reader"], 0.000001)
require.InDelta(t, 1.2, creditsByAgent["z-reader"], 0.000001)
require.InDelta(t, 0.25, creditsByAgent["main"], 0.000001)
require.Equal(t, 2, requestsByAgent["a-reader"])
require.Equal(t, 2, requestsByAgent["z-reader"])
require.Equal(t, 1, requestsByAgent["main"])
require.NotNil(t, mainAgent)
require.Equal(t, []string{"gpt-5.6-luna"}, mainAgent.ServedModels)
require.Empty(t, summary.Warnings)
}
Loading