Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
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
4 changes: 4 additions & 0 deletions .github/workflows/smoke-codex-auto.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 12 additions & 6 deletions actions/setup/js/agent_session.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,15 @@ function isMetric(value) {
function normalizeSessionUsage(usage) {
if (!usage || typeof usage !== "object" || Array.isArray(usage)) return undefined;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Smoke test review comment (Run 37138576096): The normalizeSessionUsage function correctly handles the object spread — consider adding a comment explaining why arrays are explicitly excluded.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a concise explanation in f5d8d90: usage is a named-field object, so arrays cannot represent its token fields.

const result = { ...usage };
delete result.overflowedTokens;
for (const [key, alias] of Object.entries(USAGE_ALIASES)) {
const value = Object.hasOwn(usage, key) ? usage[key] : usage[alias];
if (isTokenCount(value)) result[key] = value;
else delete result[key];
}
if (Array.isArray(usage.overflowed_tokens)) {
result.overflowed_tokens = [...new Set(usage.overflowed_tokens.filter(key => typeof key === "string" && Object.hasOwn(USAGE_ALIASES, key)))];
const overflowed = usage.overflowedTokens ?? usage.overflowed_tokens;
Comment thread
Copilot marked this conversation as resolved.
Resolved
if (Array.isArray(overflowed)) {
result.overflowed_tokens = [...new Set(overflowed.map(key => Object.entries(USAGE_ALIASES).find(([name, alias]) => key === name || key === alias)?.[0]).filter(Boolean))];
for (const key of result.overflowed_tokens) {
delete result[key];
delete result[USAGE_ALIASES[key]];
Expand Down Expand Up @@ -335,10 +337,14 @@ function transformFlatSessionEntries(records) {
/** @param {any} usage @returns {number|undefined} */
function sessionTokenTotal(usage) {
if (!usage) return undefined;
if (isTokenCount(usage.total_tokens)) return usage.total_tokens;
if (!isTokenCount(usage.input_tokens) && !isTokenCount(usage.output_tokens)) return undefined;
const cache = usage.input_tokens_include_cache === false ? (usage.cache_creation_input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) : 0;
const total = (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0) + cache;
const totalTokens = usage.totalTokens ?? usage.total_tokens;
const inputTokens = usage.inputTokens ?? usage.input_tokens;
const outputTokens = usage.outputTokens ?? usage.output_tokens;
if (isTokenCount(totalTokens)) return totalTokens;
if (!isTokenCount(inputTokens) && !isTokenCount(outputTokens)) return undefined;
const cache =
(usage.inputTokensIncludeCache ?? usage.input_tokens_include_cache) === false ? (usage.cacheCreationInputTokens ?? usage.cache_creation_input_tokens ?? 0) + (usage.cacheReadInputTokens ?? usage.cache_read_input_tokens ?? 0) : 0;
const total = (inputTokens ?? 0) + (outputTokens ?? 0) + cache;
return isTokenCount(total) ? total : undefined;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f5d8d90: usage normalization and total calculation now prefer snake_case fields by own-property presence, including zero and invalid canonical values.

}

Expand Down
4 changes: 2 additions & 2 deletions actions/setup/js/log_parser_shared.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const { unfenceMarkdown } = require("./markdown_unfencing.cjs");
const { ERR_PARSE } = require("./error_codes.cjs");
const createLogParserFormatters = require("./log_parser_format.cjs");
const { buildStepSummaryDetailsSection } = require("./log_parser_step_summary_builder.cjs");
const { isSessionEvent, normalizeAgentSession, projectSessionResult, projectSessionInitialization, sessionOutputText, sessionToolSuccess, sessionTokenTotal, isMetric } = require("./agent_session.cjs");
const { isSessionEvent, normalizeAgentSession, normalizeSessionUsage, projectSessionResult, projectSessionInitialization, sessionOutputText, sessionToolSuccess, sessionTokenTotal, isMetric } = require("./agent_session.cjs");
const { escapeSummaryText, toolInventoryName, displayArgument } = require("./agent_session_render.cjs");

/**
Expand Down Expand Up @@ -313,7 +313,7 @@ function generateInformationSection(lastEntry, options = {}) {
}

if (lastEntry.usage) {
const usage = lastEntry.usage;
const usage = normalizeSessionUsage(lastEntry.usage) ?? lastEntry.usage;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f5d8d90: statistics rendering now uses the shared normalizeSessionUsage projection, so camelCase totals, token components, and cache semantics render consistently.

const totalTokens = sessionTokenTotal(usage);
if (totalTokens !== undefined || usage.cache_creation_input_tokens !== undefined || usage.cache_read_input_tokens !== undefined) {
markdown += `**Token Usage:**\n`;
Expand Down
2 changes: 1 addition & 1 deletion actions/setup/js/unified_session.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ function mergeSessionSources(sources) {
const events = sources.flatMap(source =>
source.events.map((event, index) => {
const timestampMs = sessionTimestamp(event, source.timestampUnit);
const normalized = normalizeUnifiedSessionEvent(event);
const normalized = normalizeUnifiedSessionEvent(event, source.phase);
if (event.type === "detection.result" && source.path !== "usage/detection/detection_result.json") {
delete normalized.data.reason;
}
Expand Down
37 changes: 34 additions & 3 deletions actions/setup/js/unified_session.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ describe("Unified conclusion session", () => {
engine_id: "copilot",
agent_version: "v2.3.4",
model: "fixture",
event_name: "workflow_dispatch",
secret: "omit",
});
const { events } = collectUnifiedSession({ rootDir: root });
Expand All @@ -101,14 +102,44 @@ describe("Unified conclusion session", () => {
mcpgVersion: "v0.6.7",
engineId: "copilot",
agentVersion: "v2.3.4",
engine: "copilot",
model: "fixture",
requestedModel: "fixture",
triggerType: "workflow_dispatch",
},
provenance: { path: "aw_info.json", component: "workflow" },
});
expect(events.find(event => event.type === "workflow.info").data).not.toHaveProperty("secret");
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] New test covers the happy paths for resolveUsageAic (zero tokens, unknown model, explicit ai_credits/ai_credits_this_response) but not the guard-clause edge cases inside the function itself: negative/non-finite token values, and a usage record where only cache tokens are present (cacheReadInputTokens/cacheCreationInputTokens > 0 but inputTokens/outputTokens are 0 or absent).

💡 Suggested additional cases
// cache-only usage should still compute AIC
write("evals/evals_token_usage.jsonl", [
  { provider: "openai", model: "gpt-4o-mini", cache_read_input_tokens: 500 },
]);

// negative/non-finite tokens should be rejected, not silently coerced
write("threat-detection/detection_usage.jsonl", [
  { provider: "openai", model: "gpt-4o-mini", input_tokens: -5 },
]);

Without these, a regression in the tokens.some(...)/tokens.every(...) guard in resolveUsageAic (unified_session_payload.cjs:183-184) could silently start producing negative or NaN AIC values without any test failing.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in f5d8d90: tests cover cache-only pricing, negative and non-finite token rejection, and leaving other phases unestimated.

it("resolves detection and eval AIC per observation without adding overlapping checkpoints", () => {
write("threat-detection/detection_usage.jsonl", [
{ provider: "openai", model: "gpt-4o-mini", input_tokens: 1000, output_tokens: 100 },
{ provider: "openai", model: "unknown-model", input_tokens: 10 },
{ provider: "openai", model: "gpt-4o-mini", ai_credits: 0, input_tokens: 100 },
{ usage: { provider: "openai", model: "gpt-4o-mini", ai_credits: 0, input_tokens: 100 } },
]);
write("evals/evals_token_usage.jsonl", [
{ provider: "openai", model: "gpt-4o-mini", ai_credits_this_response: 0.25, input_tokens: 100 },
{ provider: "openai", model: "gpt-4o-mini", input_tokens: 100, output_tokens: 0 },
]);
const { events } = collectUnifiedSession({ rootDir: root });
const detection = events.filter(event => event.type === "usage.report" && event.provenance.phase === "detection");
const evals = events.filter(event => event.type === "usage.report" && event.provenance.phase === "evals");
expect(detection[0].data.aic).toBeGreaterThan(0);
expect(detection[0].data.usage).toEqual({ inputTokens: 1000, outputTokens: 100 });
expect(detection[1].data).not.toHaveProperty("aic");
expect(detection[2].data).toMatchObject({ totalAic: 0 });
expect(detection[2].data).not.toHaveProperty("aic");
expect(detection[3].data).toMatchObject({ provider: "openai", model: "gpt-4o-mini", totalAic: 0, usage: { inputTokens: 100 } });
expect(detection[3].data).not.toHaveProperty("aic");
expect(evals[0].data).toMatchObject({ aic: 0.25 });
expect(evals[1].data.aic).toBeGreaterThan(0);
expect(evals[1].data.usage.outputTokens).toBe(0);
for (const event of [...detection, ...evals]) {
expect(Object.keys(event.data).every(key => !key.includes("_"))).toBe(true);
expect(Object.keys(event.data.usage ?? {}).every(key => !key.includes("_"))).toBe(true);
}
});

it("retains native provenance, equal-time source order, and invalid/absent timestamps without mutation", () => {
const sources = [
{
Expand Down Expand Up @@ -184,7 +215,7 @@ describe("Unified conclusion session", () => {
status: "completed",
sourceType: "turn.completed",
numTurns: 1,
usage: { input_tokens: 16847, output_tokens: 167, cache_read_input_tokens: 8576, cache_creation_input_tokens: 0, reasoning_output_tokens: 0 },
usage: { inputTokens: 16847, outputTokens: 167, cacheReadInputTokens: 8576, cacheCreationInputTokens: 0, reasoningOutputTokens: 0 },
});
expect(agent.at(-1)).not.toHaveProperty("usage");
expect(agent.every(event => event.provenance.path === (canonicalPresent ? "agent-session.jsonl" : "agent-stdio.log"))).toBe(true);
Expand Down
64 changes: 49 additions & 15 deletions actions/setup/js/unified_session_payload.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,18 @@

/** @typedef {import("./types/agent_session").SessionEvent} SessionEvent */
/** @typedef {Record<string, string[]>} Fields */

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Smoke test review comment (Run 37138576096): Good use of named imports from model_costs.cjs — this keeps the dependency explicit and tree-shakeable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No change was needed in f5d8d90; the import from model_costs.cjs remains a named import.

const { computeInferenceAIC, findModelPricing } = require("./model_costs.cjs");

/** @type {Fields} */
const USAGE_FIELDS = {
total_tokens: ["total_tokens", "totalTokens"],
input_tokens: ["input_tokens", "inputTokens"],
output_tokens: ["output_tokens", "outputTokens"],
reasoning_output_tokens: ["reasoning_output_tokens"],
cache_read_input_tokens: ["cache_read_input_tokens", "cacheReadInputTokens", "cache_read_tokens"],
cache_creation_input_tokens: ["cache_creation_input_tokens", "cacheCreationInputTokens", "cache_write_tokens"],
input_tokens_include_cache: ["input_tokens_include_cache"],
overflowed_tokens: ["overflowed_tokens"],
totalTokens: ["totalTokens", "total_tokens"],
inputTokens: ["inputTokens", "input_tokens"],
outputTokens: ["outputTokens", "output_tokens"],
reasoningOutputTokens: ["reasoningOutputTokens", "reasoning_output_tokens", "reasoning_tokens"],
cacheReadInputTokens: ["cacheReadInputTokens", "cache_read_input_tokens", "cache_read_tokens"],
cacheCreationInputTokens: ["cacheCreationInputTokens", "cache_creation_input_tokens", "cache_write_tokens"],
inputTokensIncludeCache: ["inputTokensIncludeCache", "input_tokens_include_cache"],
overflowedTokens: ["overflowedTokens", "overflowed_tokens"],
};

/** @type {Fields} */
Expand Down Expand Up @@ -121,8 +122,8 @@ const EVENT_FIELDS = {
model: ["model"],
requestId: ["requestId", "request_id"],
status: ["status"],
aic: ["aic", "ai_credits_this_response"],
totalAic: ["totalAic", "ai_credits_total", "ai_credits"],
aic: ["aic", "ai_credits_this_response", "aiCreditsThisResponse"],
totalAic: ["totalAic", "ai_credits_total", "ai_credits", "aiCredits"],

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f5d8d90: aiCreditsTotal is now accepted as an authoritative totalAic alias and suppresses per-request estimation.

premiumRequests: ["premiumRequests", "premium_requests"],
durationMs: ["durationMs", "duration_ms"],
},
Expand All @@ -143,13 +144,13 @@ const EVENT_FIELDS = {
maliciousPatch: ["maliciousPatch", "malicious_patch"],
},
"workflow.info": {
engine: ["engine", "engine_id"],
engineId: ["engineId", "engine_id"],
engineId: ["engineId", "engine_id", "engine"],
agentVersion: ["agentVersion", "agent_version"],
cliVersion: ["cliVersion", "cli_version"],
awfVersion: ["awfVersion", "awf_version"],
mcpgVersion: ["mcpgVersion", "awmg_version"],
model: ["model"],
mcpgVersion: ["mcpgVersion", "mcpg_version", "awmg_version"],
requestedModel: ["requestedModel", "model"],
triggerType: ["triggerType", "event_name"],
workflow: ["workflow", "workflow_name"],
repository: ["repository"],
runId: ["runId", "run_id"],
Expand All @@ -173,13 +174,35 @@ function selectFields(value, fields) {
return result;
}

/** @param {any} source @param {Record<string, any>} data @param {Record<string, any>} usage */
function resolveUsageAic(source, data, usage) {
if (data.aic !== undefined || data.totalAic !== undefined) return;
const provider = data.provider ?? source.usage?.provider;
const model = data.model ?? source.usage?.model;
if (typeof provider !== "string" || typeof model !== "string" || !findModelPricing(provider, model)) return;
const tokens = [usage.inputTokens, usage.outputTokens, usage.cacheReadInputTokens, usage.cacheCreationInputTokens, usage.reasoningOutputTokens];
if (!tokens.some(value => typeof value === "number" && value > 0) || tokens.some(value => value !== undefined && (typeof value !== "number" || !Number.isFinite(value) || value < 0))) return;
const aic = computeInferenceAIC({
provider,
model,
inputTokens: usage.inputTokens ?? 0,
outputTokens: usage.outputTokens ?? 0,
cacheReadTokens: usage.cacheReadInputTokens ?? 0,
cacheWriteTokens: usage.cacheCreationInputTokens ?? 0,
reasoningTokens: usage.reasoningOutputTokens ?? 0,
...(typeof usage.inputTokensIncludeCache === "boolean" ? { inputTokensIncludeCache: usage.inputTokensIncludeCache } : {}),
});
if (Number.isFinite(aic)) data.aic = aic;
}

/**
* Compact known payloads without truncating text or changing native parser traces.
* Unknown extension data remains opaque for forward compatibility.
* @param {SessionEvent} event
* @param {string} [phase]
* @returns {SessionEvent}
*/
function normalizeUnifiedSessionEvent(event) {
function normalizeUnifiedSessionEvent(event, phase) {
/** @type {any} */
const source = event.data;
const known = Object.hasOwn(EVENT_FIELDS, event.type);
Expand All @@ -193,8 +216,19 @@ function normalizeUnifiedSessionEvent(event) {
}
if (["session.result", "firewall.token_usage", "usage.report"].includes(event.type)) {
const usage = selectFields(event.type === "session.result" ? source.usage : (source.usage ?? source), USAGE_FIELDS);
if (Array.isArray(usage.overflowedTokens)) {
const aliases = Object.entries(USAGE_FIELDS);
usage.overflowedTokens = usage.overflowedTokens.map(name => aliases.find(([, names]) => names.includes(name))?.[0] ?? name);
}
if (Object.keys(usage).length || (source.usage && typeof source.usage === "object")) data.usage = usage;
else if (source.usage === null) data.usage = null;
if (event.type === "usage.report" && source.usage && typeof source.usage === "object") {
const nested = selectFields(source.usage, EVENT_FIELDS["usage.report"]);
for (const key of ["provider", "model", "aic", "totalAic", "premiumRequests"]) {
if (data[key] === undefined && nested[key] !== undefined) data[key] = nested[key];
}
}
if (event.type === "usage.report" && (phase === "detection" || phase === "evals")) resolveUsageAic(source, data, usage);
}
if (event.type === "tool.execution_complete" && (source.is_error === true || source.result?.isError === true || source.result?.is_error === true)) data.isError = true;
if (event.type === "experiment.assignment") {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] resolveUsageAic is gated by a hardcoded phase allowlist (phase === "detection" || phase === "evals") buried inside normalizeUnifiedSessionEvent, far from the EVENT_FIELDS/USAGE_FIELDS tables that define the rest of the accounting shape.

💡 Suggestion

If a future phase (e.g. a new eval/detection source, or conclusion) needs pricing-derived AIC, this silently skips it. Consider hoisting the phase list to a named constant (e.g. AIC_RESOLVABLE_PHASES) next to USAGE_FIELDS so the coupling between phase and pricing resolution is visible and easy to extend/test.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f5d8d90: the detection/evals pricing phases are now named in AIC_RESOLVABLE_PHASES beside USAGE_FIELDS.

Expand Down
17 changes: 11 additions & 6 deletions actions/setup/js/unified_session_payload.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { mergeSessionSources } from "./unified_session.cjs";
import { generatePlainTextSummary, generateCopilotCliStyleSummary } from "./log_parser_shared.cjs";
import { serializeSessionArtifact } from "./session_artifact.cjs";
import { normalizeCodexSession } from "./codex_session.cjs";
import { selectSessionResult, sessionTokenTotal } from "./agent_session.cjs";
import { reconcileSessionUsage, selectSessionResult, sessionTokenTotal } from "./agent_session.cjs";

describe("essential unified session payloads", () => {
it("removes duplicated engine envelopes without trimming significant message text", () => {
Expand Down Expand Up @@ -84,25 +84,30 @@ describe("essential unified session payloads", () => {
data: { engine_id: "custom", agent_version: "v2", version: "v3", cli_version: "", awf_version: "", awmg_version: "", token: "omit" },
};
expect(normalizeUnifiedSessionEvent(source).data).toEqual({
engine: "custom",
engineId: "custom",
agentVersion: "v2",
cliVersion: "",
awfVersion: "",
mcpgVersion: "",
});
expect(normalizeUnifiedSessionEvent({ type: "workflow.info", data: { engine_id: "custom", version: "v3" } }).data).toEqual({
engine: "custom",
engineId: "custom",
});
const metadata = normalizeUnifiedSessionEvent({ type: "workflow.info", data: { engine: "claude", model: "requested", event_name: "schedule", cli_version: "1.0", mcpg_version: "2.0" } });
expect(metadata.data).toEqual({ engineId: "claude", cliVersion: "1.0", mcpgVersion: "2.0", requestedModel: "requested", triggerType: "schedule" });
expect(normalizeUnifiedSessionEvent(metadata)).toEqual(metadata);
});

it("uses one accounting shape without losing zero values or adding overlapping totals", () => {
const data = { provider: "copilot", inputTokens: 0, output_tokens: 2, cache_read_tokens: 0, cache_write_tokens: 0, ai_credits_this_response: 0, ai_credits_total: 0, duration_ms: 0, opaque: "omit" };
const runtime = normalizeUnifiedSessionEvent({ type: "firewall.token_usage", data });
expect(runtime.data).toEqual({ provider: "copilot", aic: 0, totalAic: 0, durationMs: 0, usage: { input_tokens: 0, output_tokens: 2, cache_read_input_tokens: 0, cache_creation_input_tokens: 0 } });
expect(runtime.data).toEqual({ provider: "copilot", aic: 0, totalAic: 0, durationMs: 0, usage: { inputTokens: 0, outputTokens: 2, cacheReadInputTokens: 0, cacheCreationInputTokens: 0 } });
const agent = normalizeUnifiedSessionEvent({ type: "session.result", data: { num_turns: 0, usage: { inputTokens: 0, output_tokens: 2, overflowed_tokens: ["cache_read_input_tokens"], unused: 10 } } });
expect(agent.data).toEqual({ numTurns: 0, usage: { input_tokens: 0, output_tokens: 2, overflowed_tokens: ["cache_read_input_tokens"] } });
expect(agent.data).toEqual({ numTurns: 0, usage: { inputTokens: 0, outputTokens: 2, overflowedTokens: ["cacheReadInputTokens"] } });
const reconciled = reconcileSessionUsage({ cache_read_input_tokens: 4 }, agent.data.usage);
expect(reconciled.cache_read_input_tokens).toBeUndefined();
expect(reconciled.cacheReadInputTokens).toBeUndefined();
expect(reconciled.overflowed_tokens).toEqual(["cache_read_input_tokens"]);
expect(normalizeUnifiedSessionEvent(runtime)).toEqual(runtime);
});

Expand All @@ -117,7 +122,7 @@ describe("essential unified session payloads", () => {
status: "completed",
sourceType: "turn.completed",
numTurns: 1,
usage: { input_tokens: 10, output_tokens: 5, reasoning_output_tokens: reasoning, cache_read_input_tokens: 4, cache_creation_input_tokens: 0 },
usage: { inputTokens: 10, outputTokens: 5, reasoningOutputTokens: reasoning, cacheReadInputTokens: 4, cacheCreationInputTokens: 0 },
},
},
]);
Expand Down
4 changes: 2 additions & 2 deletions actions/setup/js/unified_session_render.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ function eventDetail(event) {
return fields(data, ["event", "message", "reason"]);
case "firewall.token_usage":
case "usage.report":
return fields(data, ["provider", "model", "aic", "totalAic", "premiumRequests", "durationMs"]) + " " + fields(data.usage, ["input_tokens", "output_tokens", "cache_read_input_tokens", "cache_creation_input_tokens"]);
return fields(data, ["provider", "model", "aic", "totalAic", "premiumRequests", "durationMs"]) + " " + fields(data.usage, ["inputTokens", "outputTokens", "cacheReadInputTokens", "cacheCreationInputTokens"]);
case "mcp.event":
case "firewall.event":
return fields(data, ["event", "level", "status"]);
Expand All @@ -125,7 +125,7 @@ function eventDetail(event) {
case "detection.result":
return fields(data, ["jobResult", "conclusion", "reason", "promptInjection", "secretLeak", "maliciousPatch"]);
case "workflow.info":
return fields(data, ["engine", "model", "workflow", "repository", "runId"]);
return fields(data, ["engineId", "requestedModel", "triggerType", "cliVersion", "awfVersion", "mcpgVersion", "agentVersion", "workflow", "repository", "runId"]);
case "session.collection_warning":
return fields(data, ["path", "line", "code"]);
case "session.collection":
Expand Down
Loading
Loading