-
Notifications
You must be signed in to change notification settings - Fork 540
Fix zero token usage in the logs MCP tool run schema #60424
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
839998c
63c1522
812bf64
619eb59
2b71844
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| # ADR-60424: Ensure Logs Tool Downloads Usage Artifacts | ||
|
|
||
| **Date**: 2026-09-12 | ||
| **Status**: Draft | ||
| **Deciders**: gh-aw maintainers | ||
|
|
||
| --- | ||
|
|
||
| ### Context | ||
|
|
||
| The PR fixes a bug where the `logs` MCP tool reported `TokenUsage == 0` for every run because its default artifact selection downloaded only the `info` artifact set and never fetched the compact `usage` artifact containing `token_usage.jsonl` and `agent_usage.json`. The PR description and diff show that downstream reports average token usage from per-run records, so omitting usage artifacts silently produced incorrect fleet analytics and even removed the `token_usage` key entirely because of `omitempty`. The implementation changes the logs tool schema defaults, normalizes explicit artifact selections, and adjusts JSON serialization so consumers can distinguish `0` from an absent field. The architectural question is how the logs tool should guarantee availability of per-run token metrics without forcing callers to understand internal artifact dependencies. | ||
|
|
||
| ### Decision | ||
|
|
||
| We will make the `logs` MCP tool always include the compact `usage` artifact set in its effective artifact selection and always serialize `token_usage` and `aic` on each run record. We decided to default the tool to `info,usage` and to append `usage` to explicit artifact selections unless the caller already requested `usage` or `all`, because token metrics are part of the tool's contract and should not disappear due to an incomplete artifact list. This keeps report consumers aligned with the tool schema and restores correct token-usage analytics with minimal download overhead. | ||
|
|
||
| ### Alternatives Considered | ||
|
|
||
| #### Alternative 1: Keep `usage` optional and require callers to request it explicitly | ||
|
|
||
| This matched the previous behavior where the tool could be invoked with only `info` or another narrow artifact subset. It was considered because it gives callers maximal control over artifact downloads. It was not chosen because the PR evidence shows callers and downstream reports treated `token_usage` as a normal part of run data, so leaving `usage` optional caused silent data corruption rather than an explicit opt-in trade-off. | ||
|
|
||
| #### Alternative 2: Infer token usage from other downloaded artifacts or omit the field when unavailable | ||
|
|
||
| Another option would be to keep existing artifact behavior and either derive token usage from heavier artifacts or continue omitting `token_usage` and `aic` when the data is missing. This was considered because it avoids modifying artifact defaults. It was not chosen because the PR description states the compact `usage` artifact is the authoritative, cheap source of those metrics, and omitting the fields made consumers interpret missing data as zero or fail to discover the metric at all. | ||
|
|
||
| #### Alternative 3: Change only JSON serialization so `token_usage: 0` is always present | ||
|
|
||
| The diff also removes `omitempty` from `token_usage` and `aic`, so one possible narrower decision would be to serialize zero values without changing artifact selection. This was considered because it improves schema discoverability. It was not chosen on its own because the root problem is absent usage data; always serializing `0` without downloading `usage` would preserve an incorrect metric rather than restore real token counts. | ||
|
|
||
| ### Consequences | ||
|
|
||
| #### Positive | ||
| - Per-run `token_usage` and `aic` are reliably present, so fleet analytics and logs reports can aggregate real usage metrics again. | ||
| - The logs tool contract becomes safer for callers because explicit artifact selections still retain the compact data needed for token accounting. | ||
| - Regression tests now guard both default and explicit artifact-selection paths, reducing the chance of silently reintroducing zeroed token metrics. | ||
|
|
||
| #### Negative | ||
| - The logs tool now downloads the `usage` artifact even when a caller requested a narrower set, slightly reducing strict caller control over artifact selection. | ||
| - The implementation adds artifact-normalization logic and special handling for `all`/`usage`, increasing behavior complexity around schema defaults. | ||
| - Future artifact-set changes must preserve this implicit dependency or update the tool contract and tests accordingly. | ||
|
|
||
| #### Neutral | ||
| - The change does not alter the external CLI shape beyond artifact defaults and documented behavior; callers still pass artifact-set names in the same way. | ||
| - Run records with legitimately unavailable usage data now emit `token_usage: 0`, making missing-versus-zero semantics explicit at the JSON layer. | ||
| - The patch frames `usage` as a compact dependency of the logs tool rather than as an optional reporting enhancement. | ||
|
|
||
| --- | ||
|
|
||
| *ADR created by [adr-writer agent]. Review and finalize before changing status from Draft to Accepted.* |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -150,11 +150,13 @@ type RunData struct { | |
| // turns were observed, or job metadata shows agent=success followed | ||
| // by a failed safe_outputs job. | ||
| // "" – the run did not fail (success), or turn data was unavailable for classification. | ||
| FailureKind string `json:"failure_kind,omitempty" console:"-"` | ||
| Duration string `json:"duration,omitempty" console:"header:Duration,omitempty"` | ||
| ActionMinutes float64 `json:"action_minutes,omitempty" console:"header:Action Minutes,omitempty"` | ||
| TokenUsage int `json:"token_usage,omitempty" console:"header:Tokens,format:number,omitempty"` | ||
| AIC float64 `json:"aic,omitempty"` | ||
| FailureKind string `json:"failure_kind,omitempty" console:"-"` | ||
| Duration string `json:"duration,omitempty" console:"header:Duration,omitempty"` | ||
| ActionMinutes float64 `json:"action_minutes,omitempty" console:"header:Action Minutes,omitempty"` | ||
| // TokenUsage is always emitted (even when 0) so consumers of the run list can | ||
| // discover the field and distinguish "no tokens recorded" from "field absent". | ||
| TokenUsage int `json:"token_usage" console:"header:Tokens,format:number,omitempty"` | ||
| AIC float64 `json:"aic"` | ||
|
Comment on lines
+156
to
+159
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added |
||
| AmbientContext *AmbientContextMetrics `json:"ambient_context,omitempty" console:"-"` | ||
| WorkingSet *WorkingSetMetrics `json:"working_set,omitempty" console:"-"` | ||
| WSRF string `json:"-" console:"header:WSRF,omitempty"` // Working-Set Rebuild Factor, pre-formatted for table display | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ import ( | |
| "fmt" | ||
| "os" | ||
| "os/exec" | ||
| "slices" | ||
| "strconv" | ||
| "strings" | ||
| "time" | ||
|
|
@@ -116,7 +117,29 @@ type logsArgs struct { | |
| MaxStorageMB int `json:"max_storage,omitempty" jsonschema:"Maximum logs storage in MB after pruning non-essential cache data (0 means unlimited)."` | ||
| PruneOlderRuns bool `json:"prune_older_runs,omitempty" jsonschema:"Remove oldest completed runs when non-essential cache pruning cannot satisfy max_storage."` | ||
| MaxTokens int `json:"max_tokens,omitempty" jsonschema:"Deprecated: accepted for backward compatibility but ignored. Output is always written to a file."` | ||
| Artifacts []string `json:"artifacts,omitempty" jsonschema:"Artifact sets to download (default: usage). Valid sets: all, activation, agent, detection, evals, experiment, firewall, github-api, graders, mcp, usage"` | ||
| Artifacts []string `json:"artifacts,omitempty" jsonschema:"Artifact sets to download (default: info,usage). Valid sets: all, activation, agent, detection, evals, experiment, firewall, github-api, graders, info, mcp, usage. The compact usage set is always added so per-run token_usage is populated."` | ||
| } | ||
|
|
||
| // defaultMCPLogsToolArtifacts is the artifact selection used when the caller does | ||
| // not request specific sets. The compact "info" artifact supplies run metadata and | ||
| // the compact "usage" artifact supplies per-run token usage (token_usage.jsonl / | ||
| // agent_usage.json). Without the usage set every run reports zero tokens, which | ||
| // silently breaks fleet-analytics reports that aggregate token usage from the run list. | ||
| var defaultMCPLogsToolArtifacts = []string{string(ArtifactSetInfo), string(ArtifactSetUsage)} | ||
|
|
||
| // effectiveMCPLogsToolArtifacts resolves the artifact sets the logs tool downloads. | ||
| // Callers that omit the parameter get the defaults; callers that request specific | ||
| // sets always get the compact usage set added so token usage stays populated. | ||
| func effectiveMCPLogsToolArtifacts(artifacts []string) []string { | ||
| if len(artifacts) == 0 { | ||
| return slices.Clone(defaultMCPLogsToolArtifacts) | ||
|
Comment on lines
+133
to
+135
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 2b71844: added |
||
| } | ||
| for _, set := range artifacts { | ||
| if ArtifactSet(set) == ArtifactSetAll || ArtifactSet(set) == ArtifactSetUsage { | ||
| return artifacts | ||
| } | ||
| } | ||
| return append(append([]string(nil), artifacts...), string(ArtifactSetUsage)) | ||
| } | ||
|
|
||
| func defaultMCPLogsToolTimeoutMinutesForCount(count int) int { | ||
|
|
@@ -171,7 +194,7 @@ func registerLogsTool(server *mcp.Server, execCmd execCmdFunc, actor string, val | |
| logsSchema, err := generateSchemaWithDefaults[logsArgs](map[string]any{ | ||
| "count": defaultMCPLogsToolCount, | ||
| "max_tokens": 12000, | ||
| "artifacts": []string{"info"}, | ||
| "artifacts": defaultMCPLogsToolArtifacts, | ||
| }) | ||
| if err != nil { | ||
| mcpLog.Printf("Failed to generate logs tool schema: %v", err) | ||
|
|
@@ -213,7 +236,12 @@ When results are incomplete, the tool response also sets "partial": true and rep | |
| "continuation" cursor inline, so partial results can be detected without reading the file. | ||
|
|
||
| The continuation field includes all necessary parameters (before_run_id, etc.) to resume fetching | ||
| from where the previous request stopped due to timeout.` | ||
| from where the previous request stopped due to timeout. | ||
|
|
||
| Each run record includes a "token_usage" field (input+output tokens) and an "aic" field. | ||
| Both are always present, so aggregating them across runs yields real fleet-level metrics. | ||
| The compact "usage" artifact set is downloaded by default (and added to any explicit | ||
| artifact selection) because it carries the per-run token usage data.` | ||
|
|
||
| // newLogsToolHandler builds the handler for the logs tool. | ||
| func newLogsToolHandler(execCmd execCmdFunc, actor string, validateActor bool) func(context.Context, *mcp.CallToolRequest, logsArgs) (*mcp.CallToolResult, any, error) { | ||
|
|
@@ -382,8 +410,8 @@ func appendLogsFilterArgs(cmdArgs []string, args logsArgs) []string { | |
| if args.PruneOlderRuns { | ||
| cmdArgs = append(cmdArgs, "--prune-older-runs") | ||
| } | ||
| if len(args.Artifacts) > 0 { | ||
| cmdArgs = append(cmdArgs, "--artifacts", strings.Join(args.Artifacts, ",")) | ||
| if artifacts := effectiveMCPLogsToolArtifacts(args.Artifacts); len(artifacts) > 0 { | ||
| cmdArgs = append(cmdArgs, "--artifacts", strings.Join(artifacts, ",")) | ||
| } | ||
| return cmdArgs | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[/diagnosing-bugs] Removing
omitemptyhere makestoken_usage/aicrequired in the generated JSON Schema, butschemas/logs.schema.jsonandschemas/logs-jsonl.schema.jsonweren't regenerated —make schemas(ormake recompile) still produces a diff adding"token_usage"and"aic"to therequiredarray.TestGeneratedOutputSchemasAreCurrentfails on this branch.💡 Verification
Same delta for
logs-jsonl.schema.json. Runmake schemas(ormake recompile) and commit the regenerated files.@copilot please address this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regenerated in 2b71844 via
make schemas;TestGeneratedOutputSchemasAreCurrentpasses now.