Skip to content
Closed
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
Next Next commit
Improve Copilot unified session message normalization
Normalize scoped message and tool evidence, reasoning streams, interrupted tool requests, and essential conversation metadata using sampled Copilot run artifacts.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
  • Loading branch information
pelikhan and Copilot committed Oct 8, 2026
commit 42f3a6b2304ddcb9ba858833032872a2747de707
1 change: 1 addition & 0 deletions actions/setup/js/agent_session.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const USAGE_ALIASES = {
input_tokens: "inputTokens",
output_tokens: "outputTokens",
total_tokens: "totalTokens",
reasoning_output_tokens: "reasoningOutputTokens",
cache_creation_input_tokens: "cacheCreationInputTokens",
cache_read_input_tokens: "cacheReadInputTokens",
};
Expand Down
11 changes: 8 additions & 3 deletions actions/setup/js/agent_session_render.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,12 @@ function collapseStreamedMessages(events) {
output.push(entry);
continue;
}
if (source.type === "assistant.message_delta" || source.type === "claude.assistant_snapshot" || (source.type === "gemini.message_snapshot" && source.data?.role !== "user" && source.data?.type !== "user")) {
if (
source.type === "assistant.message_delta" ||
source.type === "assistant.reasoning_delta" ||
source.type === "claude.assistant_snapshot" ||
(source.type === "gemini.message_snapshot" && source.data?.role !== "user" && source.data?.type !== "user")
) {
if (source.type === "gemini.message_snapshot") geminiSnapshot = { scope, id: source.data?.messageId ?? source.data?.message_id ?? source.messageId ?? source.message_id, content: entry.data?.content };
output.push(entry);
continue;
Expand All @@ -58,13 +63,13 @@ function collapseStreamedMessages(events) {
const type = legacy ? (block?.type === "text" ? "assistant.message" : block?.type === "thinking" ? "assistant.reasoning" : undefined) : source.type;
const field = block?.type === "thinking" ? "thinking" : "text";
const text = legacy ? block?.[field] : entry.data?.content;
const messageId = source.data?.messageId ?? source.data?.message_id ?? source.messageId ?? source.message_id ?? source.message?.id;
const messageId = (type === "assistant.reasoning" ? source.data?.reasoningId : undefined) ?? source.data?.messageId ?? source.data?.message_id ?? source.messageId ?? source.message_id ?? source.message?.id;
const claude = claudeMessages.get(scope);
const snapshotBlocks = source.message?.content;
const matchingBlocks = Array.isArray(snapshotBlocks) ? snapshotBlocks.map((value, index) => ({ value, index })).filter(({ value }) => value?.type === (type === "assistant.reasoning" ? "thinking" : "text")) : [];
const claudeContinuation = claude?.id !== undefined && messageId === claude.id && matchingBlocks.length === 1;
const position = native?.index ?? source.data?.contentIndex ?? (claudeContinuation ? matchingBlocks[0].index : undefined);
const delta = source.delta === true || source.data?.delta === true || source.copilotProjection === "assistant.message_delta";
const delta = source.delta === true || source.data?.delta === true || ["assistant.message_delta", "assistant.reasoning_delta"].includes(source.copilotProjection);
const partial = source.partial === true || source.data?.partial === true;
const key = JSON.stringify([scope, type, messageId ?? claude?.id, claude?.sequence, position]);
const previousText = previous?.legacy ? previous.entry.message.content[0][previous.field] : previous?.entry.data.content;
Expand Down
25 changes: 22 additions & 3 deletions actions/setup/js/codex_session.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,31 @@ function normalizeCodexSession(records, model = null) {
if (!Object.hasOwn(report, "cache_creation_input_tokens") && Object.hasOwn(report, "cache_write_input_tokens")) contribution.cache_creation_input_tokens = report.cache_write_input_tokens;
accumulateSessionUsage(usage, contribution);
for (const [field, value] of Object.entries(report)) {
if (["cached_input_tokens", "cache_write_input_tokens", "reasoning_output_tokens", "total_tokens"].includes(field)) {
if (["cached_input_tokens", "cache_write_input_tokens", "total_tokens"].includes(field)) {
if (isTokenCount(value)) usage[field] = (usage[field] ?? 0) + value;
} else if (!["input_tokens", "output_tokens", "cache_read_input_tokens", "cache_creation_input_tokens", "inputTokens", "outputTokens", "cacheReadInputTokens", "cacheCreationInputTokens"].includes(field))
} else if (
![
"input_tokens",
"output_tokens",
"reasoning_output_tokens",
"cache_read_input_tokens",
"cache_creation_input_tokens",
"inputTokens",
"outputTokens",
"reasoningOutputTokens",
"cacheReadInputTokens",
"cacheCreationInputTokens",
].includes(field)
)
usage[field] = structuredClone(value);
}
for (const [alias, field] of Object.entries({ inputTokens: "input_tokens", outputTokens: "output_tokens", cacheReadInputTokens: "cache_read_input_tokens", cacheCreationInputTokens: "cache_creation_input_tokens" })) {
for (const [alias, field] of Object.entries({
inputTokens: "input_tokens",
outputTokens: "output_tokens",
reasoningOutputTokens: "reasoning_output_tokens",
cacheReadInputTokens: "cache_read_input_tokens",
cacheCreationInputTokens: "cache_creation_input_tokens",
})) {
if ((Object.hasOwn(report, alias) || Object.hasOwn(usage, alias)) && isTokenCount(usage[field])) usage[alias] = usage[field];
}
}
Expand Down
75 changes: 48 additions & 27 deletions actions/setup/js/copilot_session.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
const { normalizeAgentSession, createSessionEvent, accumulateSessionUsage, isTokenCount, isMetric } = require("./agent_session.cjs");
const { isDeepStrictEqual } = require("node:util");

const COPILOT_CONVERSATION_EVENT_TYPES = new Set(["assistant.message", "assistant.message_delta", "user.message", "tool.execution_start", "tool.execution_complete"]);
const COPILOT_CONVERSATION_EVENT_TYPES = new Set(["assistant.message", "assistant.message_delta", "assistant.reasoning", "assistant.reasoning_delta", "assistant.refusal", "user.message", "tool.execution_start", "tool.execution_complete"]);

/**
* @param {Array<any>} events
Expand Down Expand Up @@ -73,18 +73,6 @@ function normalizeCopilotSession(entries) {
bucket.push({ evidence: projectionEvidence(event), consumed: false });
projections.set(key, bucket);
}
const deltaText = new Map();
for (const event of source) {
if (event.type === "assistant.message_delta" && event.data.messageId !== undefined && typeof event.data.deltaContent === "string") {
deltaText.set(event.data.messageId, (deltaText.get(event.data.messageId) ?? "") + event.data.deltaContent);
}
}
const snapshots = new Set(
source
.filter(event => event.type === "assistant.message" && !event.copilotProjection && typeof event.data.content === "string" && deltaText.has(event.data.messageId) && event.data.content.startsWith(deltaText.get(event.data.messageId)))
.map(event => event.data.messageId)
.filter(id => id !== undefined)
);
const summaryScopes = [];
const sourceSessions = new Map();
const originScopes = new Map();
Expand All @@ -94,10 +82,12 @@ function normalizeCopilotSession(entries) {
const retainedSummaries = new Set();
for (const [index, event] of source.entries()) {
const provenance = event.provenance && typeof event.provenance === "object" ? event.provenance : {};
const sourceKey = JSON.stringify(["phase" in provenance ? provenance.phase : undefined, "path" in provenance ? provenance.path : undefined, event.agentId]);
const sourceKey = JSON.stringify(["component" in provenance ? provenance.component : undefined, "phase" in provenance ? provenance.phase : undefined, "path" in provenance ? provenance.path : undefined]);
if (["session.start", "session.init"].includes(event.type) && event.data.sessionId !== undefined) sourceSessions.set(sourceKey, event.data.sessionId);
const originKey = JSON.stringify([sourceKey, event.id, event.timestamp]);
const observedScope = JSON.stringify([sourceKey, event.session_id ?? sourceSessions.get(sourceKey)]);
const agentId = event.agentId ?? event.data.agentId;
const parentToolId = event.parent_tool_use_id ?? event.parentToolUseId ?? event.parentToolCallId ?? event.data.parentToolUseId ?? event.data.parent_tool_use_id ?? event.data.parentToolCallId;
const originKey = JSON.stringify([sourceKey, agentId, parentToolId, event.id, event.timestamp]);
const observedScope = JSON.stringify([sourceKey, event.session_id ?? event.sessionId ?? event.data.sessionId ?? event.data.session_id ?? sourceSessions.get(sourceKey), agentId, parentToolId]);
const scope = event.copilotProjection && event.id !== undefined ? (originScopes.get(originKey) ?? observedScope) : observedScope;
summaryScopes[index] = scope;
if (!event.copilotProjection && event.id !== undefined) originScopes.set(originKey, scope);
Expand All @@ -120,7 +110,23 @@ function normalizeCopilotSession(entries) {
if (!emittedMessages.has(scope)) emittedMessages.set(scope, new Set());
emittedMessages.get(scope).add(content);
}
let sessionId;
const correlationKey = (scope, id) => JSON.stringify([scope, id]);
const streamKey = (scope, event) => JSON.stringify([scope, event.type.startsWith("assistant.reasoning") ? "reasoning" : "message", event.data.reasoningId ?? event.data.messageId]);
const deltaText = new Map();
const nativeStarts = new Set();
for (const [index, event] of source.entries()) {
const scope = summaryScopes[index];
if (["assistant.message_delta", "assistant.reasoning_delta"].includes(event.type) && (event.data.messageId !== undefined || event.data.reasoningId !== undefined) && typeof event.data.deltaContent === "string") {
const key = streamKey(scope, event);
deltaText.set(key, (deltaText.get(key) ?? "") + event.data.deltaContent);
}
if (event.type === "tool.execution_start" && !event.copilotProjection && event.data.toolCallId !== undefined) nativeStarts.add(correlationKey(scope, event.data.toolCallId));
}
const snapshots = new Set();
for (const [index, event] of source.entries()) {
const key = streamKey(summaryScopes[index], event);
if (["assistant.message", "assistant.reasoning"].includes(event.type) && !event.copilotProjection && typeof event.data.content === "string" && deltaText.has(key) && event.data.content.startsWith(deltaText.get(key))) snapshots.add(key);
}
let finalizedTurns = 0;
let hasTurnResult = false;

Expand All @@ -129,6 +135,8 @@ function normalizeCopilotSession(entries) {
/** @type {Record<string, any>} */
const data = event.data;
const summaryScope = summaryScopes[index];
if ((event.copilotProjection === "assistant.message_delta" || event.copilotProjection === "assistant.reasoning_delta") && snapshots.has(streamKey(summaryScope, event))) continue;
if (event.type === "tool.execution_start" && event.copilotProjection === "assistant.toolRequests" && data.toolCallId !== undefined && nativeStarts.has(correlationKey(summaryScope, data.toolCallId))) continue;
if (event.type === "assistant.message" && event.copilotProjection === "session.task_complete") {
const identity = JSON.stringify([summaryScope, data.content]);
if (emittedMessages.get(summaryScope)?.has(data.content) || retainedSummaries.has(identity)) continue;
Expand Down Expand Up @@ -160,7 +168,6 @@ function normalizeCopilotSession(entries) {
};

if (event.type === "session.start") {
sessionId = data.sessionId;
if (!Object.hasOwn(data, "model") && Object.hasOwn(data, "selectedModel")) data.model = data.selectedModel;
if (!Object.hasOwn(data, "cwd") && data.context && Object.hasOwn(data.context, "cwd")) data.cwd = data.context.cwd;
project("session.init", {
Expand All @@ -172,15 +179,28 @@ function normalizeCopilotSession(entries) {
if (!Object.hasOwn(data, "input") && !Object.hasOwn(data, "parameters") && Object.hasOwn(data, "arguments")) {
event.data.input = structuredClone(data.arguments);
}
if (data.toolCallId !== undefined) tools.set(data.toolCallId, data);
if (data.toolCallId !== undefined) tools.set(correlationKey(summaryScope, data.toolCallId), data);
} else if (event.type === "tool.execution_complete") {
const start = tools.get(data.toolCallId);
if (!Object.hasOwn(data, "toolName") && start?.toolName !== undefined) data.toolName = start.toolName;
const start = data.toolCallId !== undefined ? tools.get(correlationKey(summaryScope, data.toolCallId)) : undefined;
for (const key of ["toolName", "mcpServerName"]) {
if (!Object.hasOwn(data, key) && start?.[key] !== undefined) data[key] = structuredClone(start[key]);
}
if (!Object.hasOwn(data, "exitCode") && Number.isSafeInteger(data.shellExecution?.exitCode)) data.exitCode = data.shellExecution.exitCode;
} else if ((event.type === "assistant.message" || event.type === "assistant.refusal") && typeof data.reasoningText === "string") {
project("assistant.reasoning", { content: data.reasoningText });
} else if (event.type === "assistant.message_delta" && typeof data.deltaContent === "string" && !snapshots.has(data.messageId)) {
project("assistant.message", { content: data.deltaContent });
} else if (event.type === "assistant.message" || event.type === "assistant.refusal") {
if (typeof data.reasoningText === "string") project("assistant.reasoning", { content: data.reasoningText });
for (const request of Array.isArray(data.toolRequests) ? data.toolRequests : []) {
if (!request || typeof request !== "object" || Array.isArray(request) || typeof request.name !== "string") continue;
if (request.toolCallId !== undefined && nativeStarts.has(correlationKey(summaryScope, request.toolCallId))) continue;
const fields = {
...request,
toolName: request.name,
...(!Object.hasOwn(request, "input") && !Object.hasOwn(request, "parameters") && Object.hasOwn(request, "arguments") ? { input: structuredClone(request.arguments) } : {}),
};
project("tool.execution_start", fields, "assistant.toolRequests");
if (request.toolCallId !== undefined) tools.set(correlationKey(summaryScope, request.toolCallId), fields);
}
} else if (["assistant.message_delta", "assistant.reasoning_delta"].includes(event.type) && typeof data.deltaContent === "string" && !snapshots.has(streamKey(summaryScope, event))) {
project(event.type === "assistant.reasoning_delta" ? "assistant.reasoning" : "assistant.message", { content: data.deltaContent, delta: true });
} else if (event.type === "session.task_complete" && typeof data.summary === "string" && data.summary.trim()) {
const messages = summaryMessages.get(summaryScope) ?? new Set();
if (!messages.has(data.summary)) {
Expand All @@ -190,13 +210,13 @@ function normalizeCopilotSession(entries) {
retainedSummaries.add(JSON.stringify([summaryScope, data.summary]));
}
} else if (event.type === "assistant.turn_end") {
const identity = data.turnId !== undefined ? JSON.stringify([sessionId, event.agentId, data.turnId]) : event.id;
const identity = data.turnId !== undefined || event.id !== undefined ? correlationKey(summaryScope, data.turnId ?? event.id) : undefined;
if (identity === undefined || !turns.has(identity)) {
finalizedTurns++;
if (identity !== undefined) turns.add(identity);
}
} else if (event.type === "assistant.usage") {
const identity = data.apiCallId ?? event.id;
const identity = data.apiCallId !== undefined || event.id !== undefined ? correlationKey(summaryScope, data.apiCallId ?? event.id) : undefined;
if (identity === undefined || !responses.has(identity)) {
if (identity !== undefined) responses.add(identity);
accumulateSessionUsage(usage, copilotUsage(data));
Expand Down Expand Up @@ -251,6 +271,7 @@ function copilotUsage(data) {
["outputTokens", "output_tokens"],
["cacheReadTokens", "cache_read_input_tokens"],
["cacheWriteTokens", "cache_creation_input_tokens"],
["reasoningTokens", "reasoning_output_tokens"],
]) {
if (isTokenCount(data?.[nativeKey])) usage[key] = data[nativeKey];
}
Expand Down
Loading