Skip to content
Merged
Next Next commit
fix: prefer unified sessions for subagent attribution
Preserve Copilot subagent metadata and per-agent accounting snapshots in unified sessions. Read structured attribution before legacy logs and limit heuristic parsing to CLI dispatch lines.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
  • Loading branch information
pelikhan and Copilot committed Oct 8, 2026
commit dfe18013924cc2cf144cd74e56b1053af1008079
5 changes: 5 additions & 0 deletions .changeset/patch-unified-subagent-sessions.md

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

55 changes: 55 additions & 0 deletions actions/setup/js/copilot_dynamic_workflow.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { normalizeUnifiedSessionEvent } from "./unified_session_payload.cjs";
import { generatePlainTextSummary, generateCopilotCliStyleSummary } from "./log_parser_shared.cjs";
import { runWithCopilotSDK } from "./copilot_sdk_session.cjs";
import { projectSessionResult } from "./agent_session.cjs";
import { createSessionValidator } from "./scripts/validate_session.cjs";

const runId = "dynamic-smoke-run";
const nativePath = "sandbox/agent/logs/copilot-session-state/root/events.jsonl";
Expand Down Expand Up @@ -40,6 +41,60 @@ describe("Copilot dynamic workflow sessions", () => {
return root;
};

it("collects exact per-agent accounting and attribution metadata into the unified file", () => {
const root = temporaryRoot();
const modelMetric = (count, totalNanoAiu) => ({
requests: { count, cost: 0 },
usage: { inputTokens: 100, outputTokens: 10, cacheReadTokens: 20, cacheWriteTokens: 30 },
totalNanoAiu,
});
const agentMetrics = {
main: { totalNanoAiu: 389122000000, modelMetrics: { opus: modelMetric(41, 389122000000) } },
research: { agentName: "research", agentDisplayName: "routing-research", totalNanoAiu: 494207000000, modelMetrics: { opus: modelMetric(45, 494207000000) } },
explore: { agentName: "explore", agentDisplayName: "routing-explore", totalNanoAiu: 9141000000, modelMetrics: { opus: modelMetric(3, 9141000000) } },
};
const native = [
{ type: "session.start", data: { sessionId: "root", selectedModel: "opus", reasoningEffort: "xhigh" } },
{
type: "subagent.started",
agentId: "research",
data: {
toolCallId: "research-call",
agentName: "research",
agentDisplayName: "routing-research",
agentType: "built-in",
model: "opus",
modelSelectionSource: "agent_definition_default",
executionMode: "background",
agentDescription: "PRIVATE_PROMPT",
},
},
{ type: "subagent.configured", agentId: "research", data: { model: "opus", reasoningEffort: "xhigh" } },
{ type: "subagent.started", agentId: "explore", data: { parentId: "research", toolCallId: "explore-call", agentName: "explore", agentDisplayName: "routing-explore", model: "opus", executionMode: "sync" } },
{ type: "subagent.configured", agentId: "explore", data: { model: "opus", reasoningEffort: "low" } },
{ type: "session.model_change", agentId: "explore", data: { newModel: "opus", reasoningEffort: "low", source: "agent" } },
{ type: "assistant.message", agentId: "explore", data: { content: "Done", model: "opus", apiCallId: "api-1", interactionId: "interaction-1", turnId: "turn-1", parentToolCallId: "explore-call", toolRequests: [] } },
{ type: "subagent.completed", agentId: "explore", data: { model: "opus", firstDispatchedModel: "opus", totalTokens: 110, totalToolCalls: 1, durationMs: 100 } },
{ type: "session.shutdown", data: { agentMetrics } },
];
const parsed = parseCopilotLog(serialize(native)).logEntries;
expect(parsed.find(event => event.type === "session.result").data.agentMetrics).toEqual(agentMetrics);
const file = path.join(root, nativePath);
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, serialize(native));
const events = JSON.parse(JSON.stringify(collectUnifiedSession({ rootDir: root, engine: "copilot" }).events));
expect(events.find(event => event.type === "session.init").data).toMatchObject({ model: "opus", reasoningEffort: "xhigh" });
expect(events.find(event => event.type === "subagent.started" && event.agentId === "research").data).toMatchObject({ agentType: "built-in", modelSelectionSource: "agent_definition_default", executionMode: "background" });
expect(events.find(event => event.type === "subagent.started" && event.agentId === "explore").data.parentId).toBe("research");
expect(events.find(event => event.type === "subagent.configured" && event.agentId === "explore").data.reasoningEffort).toBe("low");
expect(events.find(event => event.type === "assistant.message").data).toMatchObject({ model: "opus", apiCallId: "api-1", interactionId: "interaction-1", turnId: "turn-1", parentToolCallId: "explore-call" });
expect(events.find(event => event.type === "subagent.completed").data.firstDispatchedModel).toBe("opus");
expect(events.find(event => event.type === "session.result").data.agentMetrics).toEqual(agentMetrics);
expect(events.find(event => event.type === "subagent.started").data).not.toHaveProperty("agentDescription");
const validate = createSessionValidator("unified").event;
for (const event of events) expect(validate(event), JSON.stringify(validate.errors)).toBe(true);
});

it("retains native lifecycle signals and correlates subagents without conflating workflow accounting with session usage", () => {
const original = structuredClone(lifecycle);
const parsed = parseCopilotLog(serialize(lifecycle)).logEntries;
Expand Down
1 change: 1 addition & 0 deletions actions/setup/js/copilot_session.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ function normalizeCopilotSession(entries) {
const end = typeof event.timestamp === "string" ? Date.parse(event.timestamp) : event.timestamp;
if (typeof end === "number" && isMetric(data.sessionStartTime) && isMetric(end) && end >= data.sessionStartTime) result.durationMs = end - data.sessionStartTime;
if (data.errorReason !== undefined) result.errors = [data.errorReason];
if (data.agentMetrics !== undefined) result.agentMetrics = structuredClone(data.agentMetrics);
if (Object.keys(result).length) project("session.result", result);
} else if (event.type === "session.error") {
project("session.result", { errors: [data.error !== undefined ? structuredClone(data.error) : structuredClone(data)] });
Expand Down
3 changes: 3 additions & 0 deletions actions/setup/js/copilot_workflow_events.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ const COPILOT_WORKFLOW_EVENT_FIELDS = {
toolCallId: ["toolCallId"],
agentName: ["agentName"],
agentDisplayName: ["agentDisplayName"],
agentType: ["agentType"],
parentId: ["parentId"],
model: ["model"],
modelSelectionSource: ["modelSelectionSource"],
executionMode: ["executionMode"],
},
"subagent.configured": { model: ["model"], contextTier: ["contextTier"], reasoningEffort: ["reasoningEffort"], multiTurn: ["multiTurn"] },
Expand All @@ -27,6 +29,7 @@ const COPILOT_WORKFLOW_EVENT_FIELDS = {
agentName: ["agentName"],
agentDisplayName: ["agentDisplayName"],
model: ["model"],
firstDispatchedModel: ["firstDispatchedModel"],
durationMs: ["durationMs"],
totalTokens: ["totalTokens"],
totalToolCalls: ["totalToolCalls"],
Expand Down
3 changes: 3 additions & 0 deletions actions/setup/js/types/agent_session.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export interface EventMetadata {
export interface SessionInitData {
sourceEngine?: string;
model?: string;
reasoningEffort?: string;
sessionId?: string | null;
cwd?: string;
tools?: JsonValue[];
Expand Down Expand Up @@ -118,6 +119,8 @@ export interface SessionResultData {
usage?: SessionUsage;
errors?: JsonValue[];
permissionDenials?: JsonValue[];
/** Exact per-agent accounting snapshot supplied by the engine, not additive usage. */
agentMetrics?: JsonValue;
[key: string]: unknown;
}

Expand Down
26 changes: 21 additions & 5 deletions actions/setup/js/types/unified_session.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,26 @@ export interface UnifiedSessionUsage {
overflowedTokens?: string[];
}

export interface UnifiedSessionResultData extends Pick<SessionResultData, "numTurns" | "durationMs" | "totalCostUsd" | "status" | "sourceType" | "errors" | "permissionDenials"> {
export interface UnifiedSessionResultData extends Pick<SessionResultData, "numTurns" | "durationMs" | "totalCostUsd" | "status" | "sourceType" | "errors" | "permissionDenials" | "agentMetrics"> {
usage?: UnifiedSessionUsage | null;
}

export interface UnifiedMessageData extends Pick<MessageData, "content"> {
delta?: JsonValue;
partial?: JsonValue;
messageId?: JsonValue;
contentIndex?: JsonValue;
channel?: JsonValue;
sessionId?: JsonValue;
agentId?: JsonValue;
parentToolUseId?: JsonValue;
model?: JsonValue;
apiCallId?: JsonValue;
interactionId?: JsonValue;
turnId?: JsonValue;
parentToolCallId?: JsonValue;
}

export interface DynamicWorkflowTaskData {
taskId?: string;
toolCallId?: string;
Expand Down Expand Up @@ -249,14 +265,14 @@ export interface CollectionData {
export interface UnifiedSessionEventDataMap {
"session.format": SessionFileFormatData;
"agent.execution": AgentExecutionData;
"session.init": Pick<SessionInitData, "sourceEngine" | "model" | "sessionId" | "cwd">;
"session.start": Pick<SessionInitData, "sourceEngine" | "model" | "sessionId" | "cwd">;
"session.init": Pick<SessionInitData, "sourceEngine" | "model" | "sessionId" | "cwd" | "reasoningEffort">;
"session.start": Pick<SessionInitData, "sourceEngine" | "model" | "sessionId" | "cwd" | "reasoningEffort">;
"user.message": Pick<MessageData, "content">;
"prompt.system": Pick<MessageData, "content">;
"prompt.user": Pick<MessageData, "content">;
"assistant.message": Pick<MessageData, "content">;
"assistant.message": UnifiedMessageData;
"assistant.refusal": Pick<AssistantRefusalData, "reason" | "content" | "policyCategory" | "explanation" | "partial">;
"assistant.reasoning": Pick<MessageData, "content">;
"assistant.reasoning": UnifiedMessageData;
"tool.execution_start": Pick<ToolExecutionStartData, "toolCallId" | "toolName" | "input" | "command" | "mcpServerName">;
"tool.execution_complete": Pick<
ToolExecutionCompleteData,
Expand Down
8 changes: 7 additions & 1 deletion actions/setup/js/unified_session_payload.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ const MESSAGE_FIELDS = {
sessionId: ["sessionId", "session_id"],
agentId: ["agentId"],
parentToolUseId: ["parentToolUseId", "parent_tool_use_id"],
model: ["model"],
apiCallId: ["apiCallId"],
interactionId: ["interactionId"],
turnId: ["turnId"],
parentToolCallId: ["parentToolCallId"],
};

/** @type {Fields} */
Expand All @@ -106,7 +111,7 @@ const EVENT_FIELDS = {
...COPILOT_WORKFLOW_EVENT_FIELDS,
"session.format": { version: ["version"] },
"agent.execution": { categories: ["categories"], errorCodes: ["errorCodes"], errorTypes: ["errorTypes"], exitCode: ["exitCode", "exit_code"] },
"session.init": { sourceEngine: ["sourceEngine"], model: ["model"], sessionId: ["sessionId", "session_id"], cwd: ["cwd"] },
"session.init": { sourceEngine: ["sourceEngine"], model: ["model", "selectedModel"], sessionId: ["sessionId", "session_id"], cwd: ["cwd"], reasoningEffort: ["reasoningEffort"] },
"user.message": { content: ["content"] },
"prompt.system": { content: ["content"] },
"prompt.user": { content: ["content"] },
Expand Down Expand Up @@ -136,6 +141,7 @@ const EVENT_FIELDS = {
sourceType: ["sourceType"],
errors: ["errors"],
permissionDenials: ["permissionDenials", "permission_denials"],
agentMetrics: ["agentMetrics"],
},
"mcp.rpc.request": MCP_FIELDS,
"mcp.rpc.response": MCP_FIELDS,
Expand Down
2 changes: 1 addition & 1 deletion actions/setup/js/unified_session_payload.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ describe("essential unified session payloads", () => {
};
const original = structuredClone(message);
const compact = normalizeUnifiedSessionEvent(message);
expect(compact).toEqual({ type: "assistant.message", id: "message", parentId: null, timestamp: message.timestamp, data: { content } });
expect(compact).toEqual({ type: "assistant.message", id: "message", parentId: null, timestamp: message.timestamp, data: { content, model: "duplicated" } });
expect(Buffer.byteLength(JSON.stringify(compact))).toBeLessThan(Buffer.byteLength(JSON.stringify(message)) / 2);
expect(message).toEqual(original);
expect(normalizeUnifiedSessionEvent(compact)).toEqual(compact);
Expand Down
4 changes: 3 additions & 1 deletion docs/public/schemas/agent-session.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
"properties": {
"sourceEngine": { "type": "string" },
"model": { "type": "string" },
"reasoningEffort": { "type": "string" },
"sessionId": { "type": ["string", "null"] },
"cwd": { "type": "string" },
"tools": { "type": "array", "items": { "$ref": "#/definitions/JsonValue" } },
Expand All @@ -101,7 +102,8 @@
"sourceType": { "type": "string" },
"usage": { "$ref": "#/definitions/SessionUsage" },
"errors": { "type": "array", "items": { "$ref": "#/definitions/JsonValue" } },
"permissionDenials": { "type": "array", "items": { "$ref": "#/definitions/JsonValue" } }
"permissionDenials": { "type": "array", "items": { "$ref": "#/definitions/JsonValue" } },
"agentMetrics": { "$ref": "#/definitions/JsonValue", "description": "Exact per-agent accounting snapshot supplied by the engine, not additive usage." }
},
"additionalProperties": {}
},
Expand Down
33 changes: 27 additions & 6 deletions docs/public/schemas/unified-session.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
"allOf": [
{ "$ref": "#/definitions/EventMetadata" },
{ "if": { "properties": { "type": { "const": "agent.execution" } }, "required": ["type"] }, "then": { "properties": { "data": { "$ref": "#/definitions/AgentExecutionData" } } } },
{ "if": { "properties": { "type": { "const": "assistant.message" } }, "required": ["type"] }, "then": { "properties": { "data": { "$ref": "#/definitions/Pick%3CMessageData%2C%22content%22%3E" } } } },
{ "if": { "properties": { "type": { "const": "assistant.reasoning" } }, "required": ["type"] }, "then": { "properties": { "data": { "$ref": "#/definitions/Pick%3CMessageData%2C%22content%22%3E" } } } },
{ "if": { "properties": { "type": { "const": "assistant.message" } }, "required": ["type"] }, "then": { "properties": { "data": { "$ref": "#/definitions/UnifiedMessageData" } } } },
{ "if": { "properties": { "type": { "const": "assistant.reasoning" } }, "required": ["type"] }, "then": { "properties": { "data": { "$ref": "#/definitions/UnifiedMessageData" } } } },
{
"if": { "properties": { "type": { "const": "assistant.refusal" } }, "required": ["type"] },
"then": { "properties": { "data": { "$ref": "#/definitions/Pick%3CAssistantRefusalData%2C(%22reason%22%7C%22content%22%7C%22policyCategory%22%7C%22explanation%22%7C%22partial%22)%3E" } } }
Expand Down Expand Up @@ -52,12 +52,12 @@
{ "if": { "properties": { "type": { "const": "session.format" } }, "required": ["type"] }, "then": { "properties": { "data": { "$ref": "#/definitions/SessionFileFormatData" } } } },
{
"if": { "properties": { "type": { "const": "session.init" } }, "required": ["type"] },
"then": { "properties": { "data": { "$ref": "#/definitions/Pick%3CSessionInitData%2C(%22sourceEngine%22%7C%22model%22%7C%22sessionId%22%7C%22cwd%22)%3E" } } }
"then": { "properties": { "data": { "$ref": "#/definitions/Pick%3CSessionInitData%2C(%22sourceEngine%22%7C%22model%22%7C%22sessionId%22%7C%22cwd%22%7C%22reasoningEffort%22)%3E" } } }
},
{ "if": { "properties": { "type": { "const": "session.result" } }, "required": ["type"] }, "then": { "properties": { "data": { "$ref": "#/definitions/UnifiedSessionResultData" } } } },
{
"if": { "properties": { "type": { "const": "session.start" } }, "required": ["type"] },
"then": { "properties": { "data": { "$ref": "#/definitions/Pick%3CSessionInitData%2C(%22sourceEngine%22%7C%22model%22%7C%22sessionId%22%7C%22cwd%22)%3E" } } }
"then": { "properties": { "data": { "$ref": "#/definitions/Pick%3CSessionInitData%2C(%22sourceEngine%22%7C%22model%22%7C%22sessionId%22%7C%22cwd%22%7C%22reasoningEffort%22)%3E" } } }
},
{
"if": { "properties": { "type": { "const": "tool.execution_complete" } }, "required": ["type"] },
Expand Down Expand Up @@ -347,9 +347,9 @@
"additionalProperties": false
},
"Pick<MessageData,\"content\">": { "type": "object", "properties": { "content": { "$ref": "#/definitions/JsonValue" } }, "additionalProperties": false },
"Pick<SessionInitData,(\"sourceEngine\"|\"model\"|\"sessionId\"|\"cwd\")>": {
"Pick<SessionInitData,(\"sourceEngine\"|\"model\"|\"sessionId\"|\"cwd\"|\"reasoningEffort\")>": {
"type": "object",
"properties": { "sourceEngine": { "type": "string" }, "model": { "type": "string" }, "sessionId": { "type": ["string", "null"] }, "cwd": { "type": "string" } },
"properties": { "sourceEngine": { "type": "string" }, "model": { "type": "string" }, "sessionId": { "type": ["string", "null"] }, "cwd": { "type": "string" }, "reasoningEffort": { "type": "string" } },
"additionalProperties": false
},
"Pick<ToolExecutionCompleteData,(\"toolCallId\"|\"toolName\"|\"success\"|\"output\"|\"error\"|\"durationMs\"|\"exitCode\"|\"status\"|\"mcpServerName\"|\"isError\"|\"taskId\"|\"taskType\"|\"workflowName\"|\"workflowRunId\")>": {
Expand Down Expand Up @@ -446,6 +446,26 @@
"additionalProperties": false,
"description": "Collector metadata is separate from native engine event metadata."
},
"UnifiedMessageData": {
"type": "object",
"properties": {
"content": { "$ref": "#/definitions/JsonValue" },
"delta": { "$ref": "#/definitions/JsonValue" },
"partial": { "$ref": "#/definitions/JsonValue" },
"messageId": { "$ref": "#/definitions/JsonValue" },
"contentIndex": { "$ref": "#/definitions/JsonValue" },
"channel": { "$ref": "#/definitions/JsonValue" },
"sessionId": { "$ref": "#/definitions/JsonValue" },
"agentId": { "$ref": "#/definitions/JsonValue" },
"parentToolUseId": { "$ref": "#/definitions/JsonValue" },
"model": { "$ref": "#/definitions/JsonValue" },
"apiCallId": { "$ref": "#/definitions/JsonValue" },
"interactionId": { "$ref": "#/definitions/JsonValue" },
"turnId": { "$ref": "#/definitions/JsonValue" },
"parentToolCallId": { "$ref": "#/definitions/JsonValue" }
},
"additionalProperties": false
},
"UnifiedSessionResultData": {
"type": "object",
"properties": {
Expand All @@ -456,6 +476,7 @@
"sourceType": { "type": "string" },
"errors": { "type": "array", "items": { "$ref": "#/definitions/JsonValue" } },
"permissionDenials": { "type": "array", "items": { "$ref": "#/definitions/JsonValue" } },
"agentMetrics": { "$ref": "#/definitions/JsonValue", "description": "Exact per-agent accounting snapshot supplied by the engine, not additive usage." },
"usage": { "anyOf": [{ "$ref": "#/definitions/UnifiedSessionUsage" }, { "type": "null" }] }
},
"additionalProperties": false
Expand Down
Loading