Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 8 additions & 0 deletions .changeset/patch-unified-tool-structured-content.md

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

6 changes: 6 additions & 0 deletions actions/setup/js/parse_goose_log.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ describe("Goose unified session parser", () => {
success: !native.isError,
isError: native.isError,
output: native.content,
structuredContent: native.structuredContent,
exitCode: native.structuredContent.exit_code,
});
expect(parsed.logEntries.at(-1).data.status).toBe("completed");
Expand Down Expand Up @@ -345,6 +346,11 @@ describe("Goose unified session parser", () => {
expect(validateSession(serializeSessionArtifact(collected.events), "unified")).toBe(collected.events.length);
const unified = collected.events.filter(event => event.type === "tool.execution_complete");
expect(unified.map(event => event.data)).toEqual(parsed.filter(event => event.type === "tool.execution_complete").map(event => normalizeUnifiedSessionEvent(event).data));
for (const event of unified) {
const nativeResult = fixture.flatMap(record => record.message?.content ?? []).find(item => item.type === "toolResponse" && item.id === event.data.toolCallId).toolResult.value;
if (Object.hasOwn(nativeResult, "structuredContent")) expect(event.data.structuredContent).toEqual(nativeResult.structuredContent);
else expect(event.data).not.toHaveProperty("structuredContent");
}
const nativeText = fixture
.flatMap(event => event.message?.content ?? [])
.filter(item => item.type === "text")
Expand Down
2 changes: 1 addition & 1 deletion actions/setup/js/session_artifact.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ const sessionPayloadFields = new Map([
["assistant.reasoning", ["content"]],
["assistant.refusal", ["content", "explanation"]],
["tool.execution_start", ["input", "parameters", "command"]],
["tool.execution_complete", ["output", "result", "error", "fileEdits"]],
["tool.execution_complete", ["output", "structuredContent", "result", "error", "fileEdits"]],
]);

/** @param {unknown} original @param {unknown} redacted @param {string[]} maskedValues @param {string} eventType @param {string[]} [fields] */
Expand Down
1 change: 1 addition & 0 deletions actions/setup/js/types/agent_session.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export interface ToolExecutionCompleteData {
workflowRunId?: string;
success?: boolean;
output?: JsonValue;
structuredContent?: JsonValue;
result?: JsonValue;
error?: JsonValue;
fileEdits?: JsonValue;
Expand Down
19 changes: 18 additions & 1 deletion actions/setup/js/types/unified_session.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -852,7 +852,24 @@ export interface UnifiedSessionEventDataMap {
} & UnifiedSessionContextData;
"tool.execution_complete": Pick<
ToolExecutionCompleteData,
"toolCallId" | "stepIndex" | "toolName" | "success" | "output" | "result" | "error" | "fileEdits" | "durationMs" | "exitCode" | "status" | "mcpServerName" | "isError" | "taskId" | "taskType" | "workflowName" | "workflowRunId"
| "toolCallId"
| "stepIndex"
| "toolName"
| "success"
| "output"
| "structuredContent"
| "result"
| "error"
| "fileEdits"
| "durationMs"
| "exitCode"
| "status"
| "mcpServerName"
| "isError"
| "taskId"
| "taskType"
| "workflowName"
| "workflowRunId"
> &
UnifiedSessionContextData;
"dynamicWorkflows.task_started": DynamicWorkflowObservationData;
Expand Down
1 change: 1 addition & 0 deletions actions/setup/js/unified_session_payload.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ const EVENT_FIELDS = {
...TOOL_FIELDS,
success: ["success"],
output: ["output"],
structuredContent: ["structuredContent"],
result: ["result"],
error: ["error"],
fileEdits: ["fileEdits"],
Expand Down
53 changes: 53 additions & 0 deletions actions/setup/js/unified_session_payload.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,59 @@ import { failure as claudeFailures } from "./fixtures/claude_ci_sessions.cjs";
import { createSessionLookups } from "./grader_session.cjs";

describe("essential unified session payloads", () => {
it.each([null, false, 0, "", [], {}, { stdout: 'line\n"quoted"\\path \u2603', stderr: "", exit_code: 0, nested: [false, 0, null, ""] }])(
"retains explicitly supplied structured tool content %j without restoring provider envelopes",
structuredContent => {
const event = {
type: "tool.execution_complete",
id: "completion",
data: {
toolCallId: "shell",
success: false,
output: [],
structuredContent,
nativeEnvelope: { omitted: true },
},
};
const original = structuredClone(event);
const projected = normalizeUnifiedSessionEvent(event);
expect(projected).toEqual({ type: event.type, id: event.id, data: { toolCallId: "shell", success: false, output: [], structuredContent } });
expect(normalizeUnifiedSessionEvent(projected)).toEqual(projected);
expect(event).toEqual(original);
const [unified] = mergeSessionSources([{ component: "agent", phase: "agent", path: "agent-session.jsonl", events: [event] }]);
const published = JSON.parse(serializeSessionArtifact([unified]).trim());
expect(published.data.structuredContent).toEqual(structuredContent);
const validate = createSessionValidator("unified").event;
expect(validate(published), JSON.stringify(validate.errors)).toBe(true);
}
);

it("does not invent structured tool content or retain it in compact RPC observations", () => {
expect(normalizeUnifiedSessionEvent({ type: "tool.execution_complete", data: { output: [] } }).data).toEqual({ output: [] });
const rpc = normalizeUnifiedSessionEvent({
type: "mcp.rpc.response",
data: { id: 1, result: { structuredContent: { omitted: true }, content: [{ type: "text", text: "omitted" }] } },
});
expect(JSON.stringify(rpc)).not.toContain("structuredContent");
expect(JSON.stringify(rpc)).not.toContain("omitted");
});

it("redacts structured tool payload values and keys without rejecting them as structural metadata", () => {
const event = {
type: "tool.execution_complete",
data: {
toolCallId: "shell",
structuredContent: { stdout: "SANITIZED_MASK\n", stderr: "", exit_code: 0, nested: { SANITIZED_MASK: "SANITIZED_MASK" } },
},
};
for (const events of [[event], mergeSessionSources([{ component: "agent", phase: "agent", path: "agent-session.jsonl", events: [event] }])]) {
const serialized = serializeSessionArtifact(events, ["SANITIZED_MASK"], { rejectStructuralRedaction: true });
expect(serialized).not.toContain("SANITIZED_MASK");
expect(JSON.parse(serialized.trim()).data.structuredContent).toEqual({ stdout: "***\n", stderr: "", exit_code: 0, nested: { "***": "***" } });
}
expect(() => serializeSessionArtifact([{ ...event, id: "SANITIZED_MASK" }], ["SANITIZED_MASK"], { rejectStructuralRedaction: true })).toThrow("Session redaction changed structural fields");
});

it("preserves observed Codex reconnect progress separately from harness attempts", () => {
const native = parseEngineSession("Reconnecting... 2/5 (connection interrupted)\n", "codex").find(event => event.type === "session.result");
expect(native).toMatchObject({ reconnectAttempt: 2, maxReconnects: 5 });
Expand Down
1 change: 1 addition & 0 deletions docs/public/schemas/agent-session.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@
"workflowRunId": { "type": "string" },
"success": { "type": "boolean" },
"output": { "$ref": "#/definitions/JsonValue" },
"structuredContent": { "$ref": "#/definitions/JsonValue" },
"result": { "$ref": "#/definitions/JsonValue" },
"error": { "$ref": "#/definitions/JsonValue" },
"fileEdits": { "$ref": "#/definitions/JsonValue" },
Expand Down
1 change: 1 addition & 0 deletions docs/public/schemas/unified-session.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@
"toolName": { "type": "string" },
"success": { "type": "boolean" },
"output": { "$ref": "#/definitions/JsonValue" },
"structuredContent": { "$ref": "#/definitions/JsonValue" },
"result": { "$ref": "#/definitions/JsonValue" },
"error": { "$ref": "#/definitions/JsonValue" },
"fileEdits": { "$ref": "#/definitions/JsonValue" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ opaque because its essential fields are not defined by this specification.
| Agent messages and reasoning | Exact `content`, without duplicate text blocks or the original message envelope. |
| Agent session errors | Observed diagnostic fields, including `errorType`, `code`, and `statusCode`; absent status codes remain absent, and unrelated native error context is omitted. |
| Agent policy refusals | Structured `reason`, exact available `content`, `policyCategory`, `explanation`, and streaming `partial` flag; no duplicated provider envelope. |
| Agent tool lifecycle | Correlation IDs, tool/server names, one `input` or `output` field, command, outcome/error signals, duration, and exit code. Background tool updates retain observed `taskId` and `taskType`; progress and task notifications do not imply a tool completion. |
| Agent tool lifecycle | Correlation IDs, tool/server names, one `input` or `output` field, separately supplied `structuredContent`, command, outcome/error signals, duration, and exit code. Background tool updates retain observed `taskId` and `taskType`; progress and task notifications do not imply a tool completion. |
| Agent accounting | Turns, duration, cost, observed terminal `status` and `sourceType`, normalized `usage` including reported reasoning tokens, errors, and permission denials. |
| Agent execution diagnostics | Unique harness/compiler `categories`, native `errorCodes` and `errorTypes`, and the observed final process `exitCode`; no diagnostic messages or duplicated engine envelopes. |
| MCP | Server, direction, RPC/call/request IDs, method, tool name, duration, sizes, status, reason, and error code/message. Failed calls reporting HTTP 429 also retain `httpStatus: 429` and the sanitized `retryHint`, including retry durations or `Retry-After` seconds/HTTP-date. No RPC arguments, response bodies, echoed queries/URLs in rate-limit diagnostics, or error context. |
Expand Down Expand Up @@ -765,12 +765,18 @@ Native `parameters` remains a reader alias for `input`. If both are present, rea
| `toolName` | Source-dependent native name, or a name recovered from an exact matching start. |
| `success` | Boolean when the source completion establishes an outcome; omitted when unknown. |
| `output` | Source-dependent JSON-compatible output, not restricted to text. |
| `structuredContent` | Optional separately supplied JSON-compatible tool result, retained alongside `output` without flattening nested fields or replacing explicit empty or falsy values. |
| `durationMs` | Source-dependent finite nonnegative duration in milliseconds, including zero. |
| `error` | Optional native error string, object, or other supported error value. |
| `result` | Optional native result value, including Copilot `result.content` arrays or JSON content blocks. |

`output` and `result` can coexist. A reader uses an explicitly present `output` for the primary output view, otherwise the native `result`; neither is destroyed. Source-specific result envelopes remain available to readers.

The full unified artifact retains `structuredContent` independently of the primary
output view, including shell `stdout`, `stderr`, and `exit_code` fields. It uses
the same registered-secret redaction as other tool payloads. This does not retain
duplicated provider envelopes or change the compact MCP RPC projection.

**T-UAS-014 — Explicit failures.** A mapping MUST represent an explicit failed status, error outcome, `is_error: true`, `isError: true`, or nonzero command exit code as a failure. Absence of an error is not, by itself, evidence of completion or success. A source protocol's documented successful completion or default non-error tool-result semantics MAY establish `success: true`; a start alone MUST NOT. If preserved native signals conflict, readers MUST treat an explicit failure as controlling and retain the contradictory source evidence.

### 4.6 `session.result`
Expand Down
Loading