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
62 changes: 62 additions & 0 deletions actions/setup/js/fixtures/goose_ci_sessions.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// @ts-check

// Sanitized stream shapes from github/gh-aw smoke-goose agent/agent-stdio.log:
// success https://github.com/github/gh-aw/actions/runs/37865761051
// failed shell https://github.com/github/gh-aw/actions/runs/37266019863
// terminal error https://github.com/github/gh-aw/actions/runs/37264898160
// extension startup https://github.com/github/gh-aw/actions/runs/37248341114
// IDs, timestamps, text, arguments and accounting values are replaced.
const message = (id, role, content) => ({
type: "message",
message: {
id,
role,
created: 1,
content,
metadata: { userVisible: true, agentVisible: true, ...(role === "assistant" ? { inference: { provider: "openai", requestedModel: "gpt-5.4" } } : {}) },
},
});
const request = (id, name, arguments_, extension, index) => ({
type: "toolRequest",
id,
toolCall: { status: "success", value: { name, arguments: arguments_ } },
_meta: { "goose.toolCall.providerIndex": index, goose_extension: extension },
});
const response = (id, value) => ({ type: "toolResponse", id, toolResult: { status: "success", value } });
const complete = { type: "complete", total_tokens: 12, input_tokens: 10, output_tokens: 2, cache_read_input_tokens: 4, cache_write_input_tokens: 0, cost_usd: 0 };

const success = [
message("sanitized-assistant-1", "assistant", [
request("sanitized-github-call", "github__list_pull_requests", { owner: "sanitized", repo: "repo", perPage: 2 }, "github", 0),
request("sanitized-shell-call", "shell", { command: "printf sanitized", timeout_secs: 30 }, "developer", 1),
]),
message("sanitized-user-1", "user", [response("sanitized-github-call", { content: [{ type: "text", text: "[]" }] })]),
message("sanitized-user-2", "user", [response("sanitized-shell-call", { resultType: "complete", content: [{ type: "text", text: "sanitized" }], structuredContent: { stdout: "sanitized", stderr: "", exit_code: 0 }, isError: false })]),
message("sanitized-assistant-2", "assistant", [{ type: "text", text: "Done" }]),
message("sanitized-assistant-2", "assistant", [{ type: "text", text: "." }]),
complete,
];

const failedTool = [
message("sanitized-assistant-1", "assistant", [request("sanitized-shell-call", "shell", { command: "false", timeout_secs: 30 }, "developer", 0)]),
message("sanitized-user-1", "user", [
response("sanitized-shell-call", {
resultType: "complete",
content: [{ type: "text", text: "sanitized failure\n\nCommand exited with code 1", annotations: { priority: 0 } }],
structuredContent: { stdout: "sanitized failure", stderr: "", exit_code: 1 },
isError: true,
}),
]),
complete,
];

const terminalFailure = [
success[0],
success[1],
success[2],
{ type: "error", error: "Ran into this error: Authentication error: Status: 403 Forbidden. Response: Maximum AI credits exceeded.\n\nPlease retry if you think this is a transient or recoverable error." },
];

const startupFailure = "Warning: Failed to start extension 'github' (sanitized transport failure)\nWarning: Failed to start extension 'safeoutputs' (sanitized transport failure)";

module.exports = { success, failedTool, terminalFailure, startupFailure };
67 changes: 43 additions & 24 deletions actions/setup/js/parse_goose_log.cjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// @ts-check

const { createEngineLogParser, generateCopilotCliStyleSummary, buildStepSummaryDetailsSection } = require("./log_parser_shared.cjs");
const { createSessionEvent, isSessionEvent, isTokenCount, isMetric } = require("./agent_session.cjs");
const { createSessionEvent, isSessionEvent, isTokenCount, isMetric, reconcileSessionUsage } = require("./agent_session.cjs");

const main = createEngineLogParser({ parserName: "Goose", parseFunction: parseGooseLog, supportsDirectories: false });

Expand All @@ -16,13 +16,18 @@ function isGooseEvent(event) {
/** @param {any} event @returns {Record<string, any>} */
function sourceMetadata(event) {
const source = { ...event };
if (typeof event.message?.id === "string") source.id = event.message.id;
if (!Object.hasOwn(source, "id") && typeof event.message?.id === "string") source.id = event.message.id;
// Goose's Message.created is Unix seconds, not milliseconds.
const seconds = event.message?.created;
if (typeof seconds === "number" && Number.isFinite(seconds) && Math.abs(seconds * 1000) <= 8640000000000000) source.timestamp = new Date(seconds * 1000).toISOString();
if (!Object.hasOwn(source, "timestamp") && typeof seconds === "number" && Number.isFinite(seconds) && Math.abs(seconds * 1000) <= 8640000000000000) source.timestamp = new Date(seconds * 1000).toISOString();
Comment thread
pelikhan marked this conversation as resolved.
Resolved
return source;
}

/** @param {unknown} error @returns {boolean} */
function isMaxTurnsError(error) {
return typeof error === "string" && /maximum.*turns|turn limit|max.?turns/i.test(error);
}

/**
* Parse Goose run --output-format stream-json into canonical session events.
* Text records are streaming deltas; complete usage is one cumulative snapshot.
Expand All @@ -47,7 +52,7 @@ function parseGooseLog(content) {
const failure = line.match(/^(?:Warning:|[ \t]*⚠)\s*Failed to start extension '([^']+)'/);
if (failure && !mcpFailures.has(failure[1])) {
mcpFailures.add(failure[1]);
logEntries.push({ type: "goose.mcp_failure", data: { serverName: failure[1] } });
logEntries.push({ type: "mcp.event", data: { event: "extension_start_failure", serverName: failure[1], status: "error" } });
Comment thread
pelikhan marked this conversation as resolved.
Resolved
}
continue;
}
Expand All @@ -61,16 +66,17 @@ function parseGooseLog(content) {
if (isSessionEvent(raw)) {
logEntries.push(structuredClone(raw));
if (raw.type === "session.init") initialized = true;
if (raw.type === "goose.max_turns") maxTurnsHit = true;
if (raw.type === "goose.mcp_failure" && typeof raw.data.serverName === "string") mcpFailures.add(raw.data.serverName);
if (raw.type === "goose.max_turns" || (raw.type === "session.error" && isMaxTurnsError(raw.data.error))) maxTurnsHit = true;
if ((raw.type === "goose.mcp_failure" || (raw.type === "mcp.event" && raw.data.event === "extension_start_failure")) && typeof raw.data.serverName === "string") mcpFailures.add(raw.data.serverName);
continue;
}
if (!isGooseEvent(raw)) continue;
const source = sourceMetadata(raw);
const emit = (type, data) => logEntries.push(createSessionEvent(source, type, data));
if (raw.type === "message") {
if (!initialized) {
emit("session.init", { sourceEngine: "goose", ...(raw.message.metadata?.inference?.requestedModel ? { model: raw.message.metadata.inference.requestedModel } : {}) });
const model = raw.message.metadata?.inference?.requestedModel;
emit("session.init", { sourceEngine: "goose", ...(typeof model === "string" ? { model } : {}) });
initialized = true;
}
if (raw.message.role === "assistant") turns.add(raw.message.id);
Expand All @@ -85,38 +91,52 @@ function parseGooseLog(content) {
const type = item.type === "thinking" ? "assistant.reasoning" : raw.message.role === "assistant" ? "assistant.message" : "user.message";
const key = JSON.stringify([type, raw.message.id]);
const previous = messages.get(key);
if (previous) previous.data.content += text;
const metadataKey = JSON.stringify({ ...source, type: undefined, message: { ...raw.message, content: undefined }, contentMetadata: { ...item, text: undefined, thinking: undefined } });
Comment thread
pelikhan marked this conversation as resolved.
Resolved
if (previous && logEntries.at(-1) === previous.event && previous.metadataKey === metadataKey) previous.event.data.content += text;
else {
const event = createSessionEvent(source, type, { content: text });
messages.set(key, event);
messages.set(key, { event, metadataKey });
logEntries.push(event);
}
} else if (item.type === "toolRequest" && typeof item.id === "string" && !toolStarts.has(item.id)) {
} else if (item.type === "toolRequest") {
const call = item.toolCall;
if (call?.status !== "error" && (call?.status !== "success" || typeof call.value?.name !== "string")) {
emit("session.collection_warning", { code: "malformed_tool_request", toolCallId: item.id });
if (typeof item.id !== "string" || (!(call?.status === "error" && typeof call.error === "string") && (call?.status !== "success" || typeof call.value?.name !== "string"))) {
emit("session.collection_warning", { code: "malformed_tool_request", ...(typeof item.id === "string" ? { toolCallId: item.id } : {}) });
continue;
}
const data = { toolCallId: item.id, ...(call?.status === "success" ? { toolName: call.value.name, input: call.value.arguments ?? {} } : {}) };
if (toolStarts.has(item.id)) continue;
const data = {
toolCallId: item.id,
...(call?.status === "success" ? { toolName: call.value.name, ...(Object.hasOwn(call.value, "arguments") ? { input: call.value.arguments } : {}) } : {}),
...(typeof item._meta?.goose_extension === "string" ? { mcpServerName: item._meta.goose_extension } : {}),
};
toolStarts.set(item.id, data);
emit("tool.execution_start", data);
if (call?.status === "error") emit("session.error", { error: call.error, toolCallId: item.id });
} else if (item.type === "toolResponse" && typeof item.id === "string" && !toolCompletions.has(item.id)) {
} else if (item.type === "toolResponse") {
const result = item.toolResult;
const value = result?.value;
const validSuccess =
result?.status === "success" && value && typeof value === "object" && !Array.isArray(value) && (value.content === undefined || Array.isArray(value.content)) && (value.isError === undefined || typeof value.isError === "boolean");
const validError = result?.status === "error" && typeof result.error === "string";
if (!validSuccess && !validError) {
emit("session.collection_warning", { code: "malformed_tool_response", toolCallId: item.id });
if (typeof item.id !== "string" || (!validSuccess && !validError)) {
emit("session.collection_warning", { code: "malformed_tool_response", ...(typeof item.id === "string" ? { toolCallId: item.id } : {}) });
continue;
}
if (toolCompletions.has(item.id)) continue;
const success = result?.status === "success" && result.value?.isError !== true;
const start = toolStarts.get(item.id);
// Developer shell results expose the process outcome independently of MCP status.
const exitCode = start?.toolName === "shell" ? value?.structuredContent?.exit_code : undefined;
Comment thread
pelikhan marked this conversation as resolved.
Resolved
Comment thread
pelikhan marked this conversation as resolved.
Resolved
emit("tool.execution_complete", {
toolCallId: item.id,
toolName: toolStarts.get(item.id)?.toolName,
...(start?.toolName !== undefined ? { toolName: start.toolName } : {}),
...(typeof item._meta?.goose_extension === "string" ? { mcpServerName: item._meta.goose_extension } : start?.mcpServerName !== undefined ? { mcpServerName: start.mcpServerName } : {}),
success,
...(value?.content !== undefined ? { output: value.content } : value?.structuredContent !== undefined ? { output: value.structuredContent } : {}),
...(Object.hasOwn(value ?? {}, "structuredContent") ? { structuredContent: value.structuredContent } : {}),
...(typeof value?.isError === "boolean" ? { isError: value.isError } : {}),
...(Number.isSafeInteger(exitCode) ? { exitCode } : {}),
...(result?.error !== undefined ? { error: result.error } : {}),
});
toolCompletions.add(item.id);
Expand All @@ -129,22 +149,21 @@ function parseGooseLog(content) {
else if (raw[key] != null) emit("session.collection_warning", { code: "invalid_usage", field: key });
}
if (isTokenCount(raw.cache_write_input_tokens)) usage.cache_creation_input_tokens = raw.cache_write_input_tokens;
else if (raw.cache_write_input_tokens != null) emit("session.collection_warning", { code: "invalid_usage", field: "cache_write_input_tokens" });
if (Object.keys(usage).length) usage.input_tokens_include_cache = true;
resultSource = source;
resultData = {
status: "completed",
...resultData,
status: resultData?.status === "error" ? "error" : "completed",
...(turns.size ? { numTurns: turns.size } : {}),
...(Object.keys(usage).length ? { usage } : {}),
...(Object.keys(usage).length ? { usage: reconcileSessionUsage(resultData?.usage, usage) } : {}),
...(isMetric(raw.cost_usd) ? { totalCostUsd: raw.cost_usd } : {}),
};
} else if (raw.type === "error") {
emit("session.error", { error: raw.error });
if (/maximum.*turns|turn limit|max.?turns/i.test(raw.error)) {
maxTurnsHit = true;
emit("goose.max_turns", { error: raw.error });
}
if (isMaxTurnsError(raw.error)) maxTurnsHit = true;
resultSource = source;
resultData = { status: "error", errors: [raw.error], ...(turns.size ? { numTurns: turns.size } : {}) };
resultData = { ...resultData, status: "error", errors: [...(resultData?.errors ?? []), raw.error], ...(turns.size ? { numTurns: turns.size } : {}) };
}
}
if (resultSource) logEntries.push(createSessionEvent(resultSource, "session.result", resultData));
Expand Down
Loading
Loading