Skip to content

Commit f484bd4

Browse files
Copilotpelikhan
andauthored
feat: update usage file paths to /tmp/gh-aw/usage/{agent,detection}/token_usage.jsonl
- Update JavaScript constants to use new aggregated paths - Remove obsolete Go constants for old usage file names - Update notify_comment.go to remove references to old paths - Add existsSync logging in parseAICreditsFromUsageJsonl - Update test expectations to match new paths - All tests passing Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
1 parent c188cf8 commit f484bd4

5 files changed

Lines changed: 45 additions & 33 deletions

File tree

actions/setup/js/send_otlp_span.cjs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1438,18 +1438,18 @@ const OTLP_EXPORT_ERROR_DETAILS_PATH = "/tmp/gh-aw/otlp-export-errors.jsonl";
14381438
const FAILURE_CATEGORIES_PATH = "/tmp/gh-aw/failure_categories.json";
14391439

14401440
/**
1441-
* Path to the per-agent AI usage JSONL file written to the temp folder.
1441+
* Path to the aggregated agent job AI usage JSONL file in the usage artifact directory.
14421442
* Read by the conclusion job post-step before /tmp/gh-aw/ is deleted.
14431443
* @type {string}
14441444
*/
1445-
const AGENTS_USAGE_JSONL_PATH = "/tmp/gh-aw/agent_usage.jsonl";
1445+
const AGENTS_USAGE_JSONL_PATH = "/tmp/gh-aw/usage/agent/token_usage.jsonl";
14461446

14471447
/**
1448-
* Path to the detection job AI usage JSONL file written to the temp folder.
1448+
* Path to the aggregated detection job AI usage JSONL file in the usage artifact directory.
14491449
* Read by the conclusion job post-step before /tmp/gh-aw/ is deleted.
14501450
* @type {string}
14511451
*/
1452-
const DETECTION_USAGE_JSONL_PATH = "/tmp/gh-aw/detection_usage.jsonl";
1452+
const DETECTION_USAGE_JSONL_PATH = "/tmp/gh-aw/usage/detection/token_usage.jsonl";
14531453

14541454
/**
14551455
* Path to the agent stdio log file.
@@ -1768,15 +1768,23 @@ function normalizeRuntimeTokenUsage(rawUsage) {
17681768
* non-negative numeric AI-credits field are silently skipped.
17691769
*
17701770
* Used by the conclusion job post-step to read AI usage data from
1771-
* `/tmp/gh-aw/agent_usage.jsonl` and `/tmp/gh-aw/detection_usage.jsonl`
1772-
* before the temp folder is deleted.
1771+
* `/tmp/gh-aw/usage/agent/token_usage.jsonl` and
1772+
* `/tmp/gh-aw/usage/detection/token_usage.jsonl` before the temp folder is deleted.
17731773
*
17741774
* @param {string} filePath - Absolute path to the JSONL usage file
17751775
* @returns {number} Total AI credits summed across all valid entries (0 when the
17761776
* file is absent, empty, or contains no entries with a valid AI-credits field)
17771777
*/
17781778
function parseAICreditsFromUsageJsonl(filePath) {
17791779
try {
1780+
// Check if file exists and log it
1781+
if (fs.existsSync(filePath)) {
1782+
core.info(`[otlp] parseAICreditsFromUsageJsonl: file exists at ${filePath}`);
1783+
} else {
1784+
core.info(`[otlp] parseAICreditsFromUsageJsonl: file not found at ${filePath}`);
1785+
return 0;
1786+
}
1787+
17801788
const content = fs.readFileSync(filePath, "utf8");
17811789
if (!content.trim()) return 0;
17821790
let total = 0;
@@ -1787,7 +1795,8 @@ function parseAICreditsFromUsageJsonl(filePath) {
17871795
if (typeof parsed === "number") total += parsed;
17881796
}
17891797
return total;
1790-
} catch {
1798+
} catch (error) {
1799+
core.info(`[otlp] parseAICreditsFromUsageJsonl: error reading ${filePath}: ${error.message}`);
17911800
return 0;
17921801
}
17931802
}

actions/setup/js/send_otlp_span.test.cjs

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6783,92 +6783,107 @@ describe("sendJobConclusionSpan does not emit OTLP metrics", () => {
67836783

67846784
describe("parseAICreditsFromUsageJsonl", () => {
67856785
let readFileSpy;
6786+
let existsSyncSpy;
67866787

67876788
beforeEach(() => {
67886789
readFileSpy = vi.spyOn(fs, "readFileSync").mockImplementation(() => {
67896790
throw Object.assign(new Error("ENOENT"), { code: "ENOENT" });
67906791
});
6792+
existsSyncSpy = vi.spyOn(fs, "existsSync").mockReturnValue(false);
67916793
});
67926794

67936795
afterEach(() => {
67946796
readFileSpy.mockRestore();
6797+
existsSyncSpy.mockRestore();
67956798
});
67966799

67976800
it("returns 0 when the file does not exist", () => {
67986801
expect(parseAICreditsFromUsageJsonl("/tmp/gh-aw/agent_usage.jsonl")).toBe(0);
67996802
});
68006803

68016804
it("returns 0 for an empty file", () => {
6805+
existsSyncSpy.mockReturnValue(true);
68026806
readFileSpy.mockImplementation(() => "");
68036807
expect(parseAICreditsFromUsageJsonl("/tmp/gh-aw/agent_usage.jsonl")).toBe(0);
68046808
});
68056809

68066810
it("returns 0 for a file with only whitespace", () => {
6811+
existsSyncSpy.mockReturnValue(true);
68076812
readFileSpy.mockImplementation(() => " \n \n");
68086813
expect(parseAICreditsFromUsageJsonl("/tmp/gh-aw/agent_usage.jsonl")).toBe(0);
68096814
});
68106815

68116816
it("sums ai_credits from a single-entry file", () => {
6817+
existsSyncSpy.mockReturnValue(true);
68126818
readFileSpy.mockImplementation(() => JSON.stringify({ ai_credits: 1.5, model: "gpt-4o" }) + "\n");
68136819
expect(parseAICreditsFromUsageJsonl("/tmp/gh-aw/agent_usage.jsonl")).toBe(1.5);
68146820
});
68156821

68166822
it("sums ai_credits across multiple entries", () => {
6823+
existsSyncSpy.mockReturnValue(true);
68176824
const lines = [JSON.stringify({ ai_credits: 1.0, model: "gpt-4o" }), JSON.stringify({ ai_credits: 0.5, model: "claude-3-5-sonnet" }), JSON.stringify({ ai_credits: 0.25 })].join("\n");
68186825
readFileSpy.mockImplementation(() => lines);
68196826
expect(parseAICreditsFromUsageJsonl("/tmp/gh-aw/agent_usage.jsonl")).toBe(1.75);
68206827
});
68216828

68226829
it("supports camelCase aiCredits field", () => {
6830+
existsSyncSpy.mockReturnValue(true);
68236831
readFileSpy.mockImplementation(() => JSON.stringify({ aiCredits: 2.0 }) + "\n");
68246832
expect(parseAICreditsFromUsageJsonl("/tmp/gh-aw/agent_usage.jsonl")).toBe(2.0);
68256833
});
68266834

68276835
it("prefers snake_case ai_credits over camelCase aiCredits when both are present", () => {
6836+
existsSyncSpy.mockReturnValue(true);
68286837
readFileSpy.mockImplementation(() => JSON.stringify({ ai_credits: 1.0, aiCredits: 9.0 }) + "\n");
68296838
expect(parseAICreditsFromUsageJsonl("/tmp/gh-aw/agent_usage.jsonl")).toBe(1.0);
68306839
});
68316840

68326841
it("skips entries without an ai_credits or aiCredits field", () => {
6842+
existsSyncSpy.mockReturnValue(true);
68336843
const lines = [JSON.stringify({ model: "gpt-4o", input_tokens: 100 }), JSON.stringify({ ai_credits: 0.75 })].join("\n");
68346844
readFileSpy.mockImplementation(() => lines);
68356845
expect(parseAICreditsFromUsageJsonl("/tmp/gh-aw/agent_usage.jsonl")).toBe(0.75);
68366846
});
68376847

68386848
it("skips malformed JSON lines without throwing", () => {
6849+
existsSyncSpy.mockReturnValue(true);
68396850
const lines = ["{not valid json}", JSON.stringify({ ai_credits: 1.0 }), "also bad"].join("\n");
68406851
readFileSpy.mockImplementation(() => lines);
68416852
expect(parseAICreditsFromUsageJsonl("/tmp/gh-aw/agent_usage.jsonl")).toBe(1.0);
68426853
});
68436854

68446855
it("skips entries with negative ai_credits", () => {
6856+
existsSyncSpy.mockReturnValue(true);
68456857
const lines = [JSON.stringify({ ai_credits: -0.5 }), JSON.stringify({ ai_credits: 1.0 })].join("\n");
68466858
readFileSpy.mockImplementation(() => lines);
68476859
expect(parseAICreditsFromUsageJsonl("/tmp/gh-aw/agent_usage.jsonl")).toBe(1.0);
68486860
});
68496861

68506862
it("skips entries where ai_credits is a non-numeric string", () => {
6863+
existsSyncSpy.mockReturnValue(true);
68516864
const lines = [JSON.stringify({ ai_credits: "invalid" }), JSON.stringify({ ai_credits: 0.5 })].join("\n");
68526865
readFileSpy.mockImplementation(() => lines);
68536866
expect(parseAICreditsFromUsageJsonl("/tmp/gh-aw/agent_usage.jsonl")).toBe(0.5);
68546867
});
68556868

68566869
it("accepts ai_credits expressed as a numeric string", () => {
6870+
existsSyncSpy.mockReturnValue(true);
68576871
readFileSpy.mockImplementation(() => JSON.stringify({ ai_credits: "1.234" }) + "\n");
68586872
expect(parseAICreditsFromUsageJsonl("/tmp/gh-aw/agent_usage.jsonl")).toBeCloseTo(1.234);
68596873
});
68606874

68616875
it("accepts ai_credits of zero", () => {
6876+
existsSyncSpy.mockReturnValue(true);
68626877
readFileSpy.mockImplementation(() => JSON.stringify({ ai_credits: 0 }) + "\n");
68636878
expect(parseAICreditsFromUsageJsonl("/tmp/gh-aw/agent_usage.jsonl")).toBe(0);
68646879
});
68656880

68666881
it("uses the AGENTS_USAGE_JSONL_PATH constant for the agents file", () => {
6867-
expect(AGENTS_USAGE_JSONL_PATH).toBe("/tmp/gh-aw/agent_usage.jsonl");
6882+
expect(AGENTS_USAGE_JSONL_PATH).toBe("/tmp/gh-aw/usage/agent/token_usage.jsonl");
68686883
});
68696884

68706885
it("uses the DETECTION_USAGE_JSONL_PATH constant for the detection file", () => {
6871-
expect(DETECTION_USAGE_JSONL_PATH).toBe("/tmp/gh-aw/detection_usage.jsonl");
6886+
expect(DETECTION_USAGE_JSONL_PATH).toBe("/tmp/gh-aw/usage/detection/token_usage.jsonl");
68726887
});
68736888
});
68746889

@@ -6880,7 +6895,7 @@ describe("sendJobConclusionSpan conclusion job AI credits from usage files", ()
68806895
/** @type {Record<string, string | undefined>} */
68816896
const savedEnv = {};
68826897
const envKeys = ["GH_AW_OTLP_ENDPOINTS", "INPUT_JOB_NAME", "GH_AW_AIC", "GH_AW_AGENT_CONCLUSION", "GITHUB_RUN_ID", "GITHUB_ACTOR", "GITHUB_REPOSITORY"];
6883-
let mkdirSpy, appendSpy, readFileSpy;
6898+
let mkdirSpy, appendSpy, readFileSpy, existsSyncSpy;
68846899

68856900
beforeEach(() => {
68866901
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: true, status: 200, statusText: "OK" }));
@@ -6890,6 +6905,10 @@ describe("sendJobConclusionSpan conclusion job AI credits from usage files", ()
68906905
}
68916906
mkdirSpy = vi.spyOn(fs, "mkdirSync").mockImplementation(() => {});
68926907
appendSpy = vi.spyOn(fs, "appendFileSync").mockImplementation(() => {});
6908+
existsSyncSpy = vi.spyOn(fs, "existsSync").mockImplementation(filePath => {
6909+
// Return true for paths that will be read by readFileSpy
6910+
return filePath === AGENTS_USAGE_JSONL_PATH || filePath === DETECTION_USAGE_JSONL_PATH;
6911+
});
68936912
readFileSpy = vi.spyOn(fs, "readFileSync").mockImplementation(() => {
68946913
throw Object.assign(new Error("ENOENT"), { code: "ENOENT" });
68956914
});
@@ -6909,6 +6928,7 @@ describe("sendJobConclusionSpan conclusion job AI credits from usage files", ()
69096928
mkdirSpy.mockRestore();
69106929
appendSpy.mockRestore();
69116930
readFileSpy.mockRestore();
6931+
existsSyncSpy.mockRestore();
69126932
});
69136933

69146934
/**
@@ -7004,7 +7024,7 @@ describe("sendJobConclusionSpan conclusion job AI credits from usage files", ()
70047024
it("reads usage files from the fixed /tmp/gh-aw/ paths regardless of GH_AW_AGENT_OUTPUT", async () => {
70057025
process.env.GH_AW_AGENT_OUTPUT = "/custom/path/output.json";
70067026
readFileSpy.mockImplementation(filePath => {
7007-
if (filePath === "/tmp/gh-aw/agent_usage.jsonl") return JSON.stringify({ ai_credits: 2.0 }) + "\n";
7027+
if (filePath === AGENTS_USAGE_JSONL_PATH) return JSON.stringify({ ai_credits: 2.0 }) + "\n";
70087028
throw Object.assign(new Error("ENOENT"), { code: "ENOENT" });
70097029
});
70107030

pkg/constants/job_constants.go

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -112,16 +112,6 @@ const SafeOutputsFilename = "safeoutputs.jsonl"
112112
// consume structured token data without parsing the step summary or GITHUB_OUTPUT.
113113
const TokenUsageFilename = "agent_usage.json"
114114

115-
// AgentUsageJsonlFilename is the filename of the per-run agent AI credit usage JSONL file written to /tmp/gh-aw/.
116-
// Each line is a JSON object with an ai_credits (or aiCredits) field tracking credit consumption.
117-
// This file is read by the conclusion job post-step to aggregate AIC before the temp folder is removed.
118-
const AgentUsageJsonlFilename = "agent_usage.jsonl"
119-
120-
// DetectionUsageJsonlFilename is the filename of the per-run detection job AI credit usage JSONL file written to /tmp/gh-aw/.
121-
// Each line is a JSON object with an ai_credits (or aiCredits) field tracking credit consumption.
122-
// This file is read by the conclusion job post-step to aggregate AIC before the temp folder is removed.
123-
const DetectionUsageJsonlFilename = "detection_usage.jsonl"
124-
125115
// GithubRateLimitsFilename is the filename of the GitHub API rate-limit log written to /tmp/gh-aw/.
126116
// Each line is a JSON object recording the x-ratelimit-* headers (or rate-limit API snapshot)
127117
// captured during github.rest API calls, enabling post-run analysis of rate-limit consumption.

pkg/workflow/notify_comment.go

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -650,7 +650,7 @@ func (c *Compiler) buildConclusionJob(data *WorkflowData, mainJobName string, sa
650650
}
651651

652652
// buildUsageArtifactUploadSteps creates steps that collect and upload a compact usage artifact.
653-
// The artifact includes aw-info.jsonl, agent_usage.jsonl, detection_usage.jsonl, and agent/detection token usage JSONL files (when present).
653+
// The artifact includes aw-info.jsonl and aggregated token usage JSONL files from agent/detection sandbox locations.
654654
func buildUsageArtifactUploadSteps(prefix string, pinAction func(string) string) []string {
655655
usageArtifactName := prefix + "usage"
656656
return []string{
@@ -660,12 +660,10 @@ func buildUsageArtifactUploadSteps(prefix string, pinAction func(string) string)
660660
" run: |\n",
661661
" mkdir -p /tmp/gh-aw/usage/agent /tmp/gh-aw/usage/detection\n",
662662
" echo \"Usage artifact source file status:\"\n",
663-
fmt.Sprintf(" for file in /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/%s /tmp/gh-aw/%s /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl; do\n", constants.AgentUsageJsonlFilename, constants.DetectionUsageJsonlFilename),
663+
" for file in /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl; do\n",
664664
" [ -f \"$file\" ] && echo \"FOUND: $file\" || echo \"MISSING: $file\"\n",
665665
" done\n",
666666
" [ -f /tmp/gh-aw/aw-info.jsonl ] && cp /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/usage/aw-info.jsonl || true\n",
667-
fmt.Sprintf(" [ -f /tmp/gh-aw/%[1]s ] && cp /tmp/gh-aw/%[1]s /tmp/gh-aw/usage/%[1]s || true\n", constants.AgentUsageJsonlFilename),
668-
fmt.Sprintf(" [ -f /tmp/gh-aw/%[1]s ] && cp /tmp/gh-aw/%[1]s /tmp/gh-aw/usage/%[1]s || true\n", constants.DetectionUsageJsonlFilename),
669667
" [ -f /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true\n",
670668
" [ -f /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true\n",
671669
" [ -f /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true\n",
@@ -683,8 +681,6 @@ func buildUsageArtifactUploadSteps(prefix string, pinAction func(string) string)
683681
fmt.Sprintf(" name: %s\n", usageArtifactName),
684682
" path: |\n",
685683
" /tmp/gh-aw/usage/aw-info.jsonl\n",
686-
" /tmp/gh-aw/usage/" + constants.AgentUsageJsonlFilename + "\n",
687-
" /tmp/gh-aw/usage/" + constants.DetectionUsageJsonlFilename + "\n",
688684
" /tmp/gh-aw/usage/agent/token_usage.jsonl\n",
689685
" /tmp/gh-aw/usage/detection/token_usage.jsonl\n",
690686
" if-no-files-found: ignore\n",

pkg/workflow/notify_comment_test.go

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1186,15 +1186,12 @@ func TestConclusionJobIncludesUsageArtifactSteps(t *testing.T) {
11861186
if !strings.Contains(allSteps, "/tmp/gh-aw/usage/aw-info.jsonl") {
11871187
t.Errorf("Expected usage artifact to include aw-info.jsonl path.\nGenerated steps:\n%s", allSteps)
11881188
}
1189-
if !strings.Contains(allSteps, "/tmp/gh-aw/usage/agent_usage.jsonl") {
1190-
t.Errorf("Expected usage artifact to include agent_usage.jsonl path.\nGenerated steps:\n%s", allSteps)
1191-
}
1192-
if !strings.Contains(allSteps, "/tmp/gh-aw/usage/detection_usage.jsonl") {
1193-
t.Errorf("Expected usage artifact to include detection_usage.jsonl path.\nGenerated steps:\n%s", allSteps)
1194-
}
11951189
if !strings.Contains(allSteps, "/tmp/gh-aw/usage/agent/token_usage.jsonl") {
11961190
t.Errorf("Expected usage artifact to include agent token usage path.\nGenerated steps:\n%s", allSteps)
11971191
}
1192+
if !strings.Contains(allSteps, "/tmp/gh-aw/usage/detection/token_usage.jsonl") {
1193+
t.Errorf("Expected usage artifact to include detection token usage path.\nGenerated steps:\n%s", allSteps)
1194+
}
11981195
if !strings.Contains(allSteps, "/tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl") {
11991196
t.Errorf("Expected usage artifact collection to include firewall audit token usage path for agent.\nGenerated steps:\n%s", allSteps)
12001197
}

0 commit comments

Comments
 (0)