src/vs/platform/agentHost/node/copilot/mapSessionEvents.ts

829 LOC · 737 covered · 92 uncovered · 178 ranges · 980 concepts · 52 introducers · 462 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- mapSessionEvents.ts ×12
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import type { AssistantMessageToolRequest, Attachment, SessionEvent, ToolExecutionCompleteContent, ToolExecutionCompleteData } from '@github/copilot-sdk';
7 > import { decodeBase64 } from '../../../../base/common/buffer.js';
8 > import { basename } from '../../../../base/common/path.js';
9 > import { isString } from '../../../../base/common/types.js';
10 > import { URI } from '../../../../base/common/uri.js';
11 > import { generateUuid } from '../../../../base/common/uuid.js';
12 > import { AgentSession } from '../../common/agentService.js';
13 > import { stripRedundantCdPrefix } from '../../common/commandLineHelpers.js';
14 > import { toToolCallMeta, type IToolCallUiMeta } from '../../common/meta/agentToolCallMeta.js';
15 > import { IFileEditRecord, ISessionDatabase } from '../../common/sessionDataService.js';
16 > import { MessageAttachmentKind, type MessageAttachment } from '../../common/state/protocol/state.js';
17 > import { MessageKind, ResponsePartKind, ToolCallConfirmationReason, ToolCallContributorKind, ToolCallStatus, ToolResultContentType, TurnState, buildSubagentSessionUri, type AgentSelection, type Message, type ModelSelection, type ResponsePart, type StringOrMarkdown, type TerminalCommandResult, type ToolCallCompletedState, type ToolResultContent, type ToolResultTerminalContent, type Turn, type UsageInfo } from '../../common/state/sessionState.js';
18 > import { buildNonPtyShellTerminalUri } from './copilotNonPtyShellTerminals.js';
19 > import { getInvocationMessage, getPastTenseMessage, getShellIntention, getShellLanguage, getSubagentMetadata, getTaskCompleteMarkdown, getToolDisplayName, getToolInputString, getToolKind, isEditTool, isHiddenTool, isTaskCompleteTool, synthesizeSkillToolCall } from './copilotToolDisplay.js';
20 > import { buildSessionDbUri } from '../shared/fileEditTracker.js';
21 > import { getMediaMime } from '../../../../base/common/mime.js';
22 > import { buildCopilotSystemNotification } from './copilotSystemNotification.js';
23 > import { buildMcpChannel, buildMcpTopLevelCustomizationId } from '../shared/mcpCustomizationController.js';
24 > import { readSimpleAttachmentDisplayKindFromMimeType } from './copilotAttachmentUtils.js';
25 >
26 > function tryStringify(value: unknown): string | undefined { mapSessionEvents.ts ×3
27 > try {
28 > return JSON.stringify(value);
29 > } catch {
30 return undefined;
31 }
34 > /**
35 > * Returns true if the event is a SDK-injected `user.message` that should not
36 > * be shown to the user (e.g. skill-content injection).
37 > *
38 > * The SDK marks these via a non-`'user'` `source` field. Older sessions
39 > * persisted before `source` existed will not be filtered; that is accepted
40 > * leakage rather than guessed-at content sniffing.
41 > */
42 > function isSyntheticUserMessage(event: SessionEvent): boolean { mapSessionEvents.ts ×12
43 > if (event.type !== 'user.message') {
44 return false;
45 }
46 > const source = event.data.source; mapSessionEvents.ts ×12
47 > return !!source && source.toLowerCase() !== 'user';
48 > }
50 > /**
51 > * Converts SDK `tool.execution_complete` content blocks into AHP tool result
52 > * content. A `shell_exit` block becomes {@link TerminalCommandResult} data on
53 > * the tool call's terminal content block; when no terminal block exists yet
54 > * (e.g. history replay, where no live channel survives) and `terminal` is
55 > * provided, a non-pty terminal block is synthesized so the outcome still
56 > * renders from `result.preview`. Returns the `shell_exit` outcome, if any, so
57 > * the live path can settle the non-pty output channel from it.
58 > */
59 > export interface ISdkShellExit {
60 > readonly shellId: string;
61 > readonly result: TerminalCommandResult;
62 > }
63 >
64 > export function appendSdkToolResultContent(content: ToolResultContent[], sdkContents: readonly ToolExecutionCompleteContent[] | undefined, terminal?: { session: URI | string; toolCallId: string; title: string }): ISdkShellExit | undefined {
65 > let shellExit: ISdkShellExit | undefined; mapSessionEvents.ts ×2
66 > for (const sdkContent of sdkContents ?? []) {
67 > switch (sdkContent.type) { mapSessionEvents.ts ×3
68 > case 'shell_exit': {
69 > const result: TerminalCommandResult = {
70 > exitCode: sdkContent.exitCode,
71 > ...(sdkContent.outputPreview !== undefined ? { preview: sdkContent.outputPreview } : {}),
72 > ...(sdkContent.outputTruncated !== undefined ? { truncated: sdkContent.outputTruncated } : {}),
73 > };
74 > shellExit = { shellId: sdkContent.shellId, result };
75 > const terminalIndex = content.findIndex(c => c.type === ToolResultContentType.Terminal);
76 > if (terminalIndex !== -1) {
77 > const terminalBlock = content[terminalIndex] as ToolResultTerminalContent; mapSessionEvents.ts ×1
78 > content[terminalIndex] = { ...terminalBlock, result };
79 > } else if (terminal) { mapSessionEvents.ts ×3
80 > content.push({ mapSessionEvents.ts ×1
81 > type: ToolResultContentType.Terminal,
82 > resource: buildNonPtyShellTerminalUri(terminal.session, terminal.toolCallId),
83 > title: terminal.title,
84 > isPty: false,
85 > result,
86 > });
87 > }
89 > }
90 > }
91 > }
92 > return shellExit; mapSessionEvents.ts ×2
93 > }
95 > // =============================================================================
96 > // Single-pass turn builder
97 > // =============================================================================
98 >
99 > /** Per-tool-call info captured from `tool.execution_start` and reused at `tool.execution_complete`. */
100 > interface IToolStartInfo {
101 > readonly toolName: string;
102 > readonly displayName: string;
103 > readonly invocationMessage: StringOrMarkdown;
104 > readonly toolInput?: string;
105 > readonly toolKind?: 'terminal' | 'subagent' | 'search';
106 > readonly language?: string;
107 > /** Intention (why the command runs) for shell tools, from their `description` argument. */
108 > readonly intention?: string;
109 > readonly subagentAgentName?: string;
110 > readonly subagentDescription?: string;
111 > readonly parameters: Record<string, unknown> | undefined;
112 > readonly parentToolCallId?: string;
113 > readonly mcpServerName?: string;
114 > readonly mcpToolName?: string;
115 > readonly mcpUiResourceUri?: string;
116 > }
117 >
118 > /** Subagent metadata seen via `subagent.started`, applied to the parent tool call's content at `tool.execution_complete`. */
119 > interface ISubagentInfo {
120 > readonly agentName: string;
121 > readonly agentDisplayName: string;
122 > readonly agentDescription?: string;
123 > }
124 >
125 > /**
126 > * Mutable per-turn state used while iterating events. The parent session
127 > * has one builder; each subagent turn (one per `parentToolCallId`) has its
128 > * own builder so inner events route there directly.
129 > */
130 > interface ITurnBuilder {
131 > id: string;
132 > message: Message;
133 > readonly responseParts: ResponsePart[];
134 > usage: UsageInfo | undefined;
135 > /** Tool starts seen but not yet completed in this turn, keyed by toolCallId. */
136 > readonly pendingTools: Map<string, IToolStartInfo>;
137 > }
138 >
139 > export interface IMapSessionEventsOptions {
140 > readonly workingDirectory?: URI;
141 > readonly model?: ModelSelection;
142 > readonly agent?: AgentSelection;
143 > }
144 >
145 > function newTurnBuilder(id: string, text: string, options?: { attachments?: MessageAttachment[]; model?: ModelSelection; agent?: AgentSelection; origin?: MessageKind }): ITurnBuilder { mapSessionEvents.ts ×12
146 > const message: Message = {
147 > text,
148 > origin: { kind: options?.origin ?? MessageKind.User },
149 > ...(options?.attachments?.length ? { attachments: options.attachments } : {}),
150 > ...(options?.model ? { model: options.model } : {}),
151 > ...(options?.agent ? { agent: options.agent } : {}),
152 > };
153 > return { id, message, responseParts: [], usage: undefined, pendingTools: new Map() };
154 > }
156 > function readStringProperty(source: unknown, key: string): string | undefined { mapSessionEvents.ts ×12
157 > if (!source || typeof source !== 'object' || Array.isArray(source)) {
158 return undefined;
159 }
160 > const value = (source as Record<string, unknown>)[key]; mapSessionEvents.ts ×12
161 > return typeof value === 'string' && value.length > 0 ? value : undefined;
162 > }
164 > function readMcpUiResourceUri(source: unknown): string | undefined { mapSessionEvents.ts ×12
165 > if (!source || typeof source !== 'object' || Array.isArray(source)) {
166 return undefined;
167 }
168 > const toolDescription = (source as Record<string, unknown>)['toolDescription']; mapSessionEvents.ts ×12
169 > if (!toolDescription || typeof toolDescription !== 'object' || Array.isArray(toolDescription)) {
170 > return undefined; mapSessionEvents.ts ×1
171 > }
172 > const meta = (toolDescription as Record<string, unknown>)['_meta']; mapSessionEvents.ts ×4
173 > if (!meta || typeof meta !== 'object' || Array.isArray(meta)) { mapSessionEvents.ts ×12
174 return undefined;
175 }
176 > const ui = (meta as Record<string, unknown>)['ui']; mapSessionEvents.ts ×4
177 > if (!ui || typeof ui !== 'object' || Array.isArray(ui)) { mapSessionEvents.ts ×12
178 return undefined;
179 }
180 > return readStringProperty(ui, 'resourceUri'); mapSessionEvents.ts ×4
181 > }
183 > function makeToolStartInfo(toolName: string, rawArguments: unknown, parentToolCallId: string | undefined, workingDirectory: URI | undefined, source: unknown): IToolStartInfo | undefined { mapSessionEvents.ts ×12
184 > if (isHiddenTool(toolName)) {
185 > return undefined; mapSessionEvents.ts ×3
186 > }
187 > const rawArgs = rawArguments !== undefined ? tryStringify(rawArguments) : undefined; mapSessionEvents.ts ×12
188 > let parameters: Record<string, unknown> | undefined;
189 > if (rawArgs) {
190 > try { parameters = JSON.parse(rawArgs) as Record<string, unknown>; } catch { /* ignore */ } mapSessionEvents.ts ×3
191 > }
192 > // stripRedundantCdPrefix mutates `parameters` and signals via its mapSessionEvents.ts ×12
193 > // return value. We re-stringify only when it changed something so
194 > // `getToolInputString` sees the cleaned command line.
195 > const cleaned = stripRedundantCdPrefix(toolName, parameters, workingDirectory) ? tryStringify(parameters) : undefined;
196 > const toolArgs = cleaned ?? rawArgs;
197 > const toolKind = getToolKind(toolName);
198 > const subagentMeta = toolKind === 'subagent' ? getSubagentMetadata(parameters) : undefined;
199 > const displayName = getToolDisplayName(toolName);
200 > return {
201 > toolName,
202 > displayName,
203 > invocationMessage: getInvocationMessage(toolName, displayName, parameters),
204 > toolInput: getToolInputString(toolName, parameters, toolArgs),
205 > toolKind,
206 > language: toolKind === 'terminal' ? getShellLanguage(toolName) : undefined,
207 > intention: getShellIntention(toolName, parameters),
208 > subagentAgentName: subagentMeta?.agentName,
209 > subagentDescription: subagentMeta?.description,
210 > parameters,
211 > parentToolCallId,
212 > mcpServerName: readStringProperty(source, 'mcpServerName'),
213 > mcpToolName: readStringProperty(source, 'mcpToolName'),
214 > mcpUiResourceUri: readMcpUiResourceUri(source),
215 > };
216 > }
218 > function finalizeTurn(builder: ITurnBuilder, state: TurnState): Turn { mapSessionEvents.ts ×12
219 > return {
220 > id: builder.id,
221 > message: builder.message,
222 > responseParts: builder.responseParts,
223 > usage: builder.usage,
224 > state,
225 > };
226 > }
228 > /**
229 > * Maps raw SDK session events directly into agent-protocol {@link Turn}s
230 > * for the parent session and any subagent child sessions, restoring stored
231 > * file-edit metadata from the session database when available.
232 > *
233 > * Subagent inner events are routed to per-`parentToolCallId` turn builders
234 > * so they appear under their own session view rather than polluting the
235 > * parent transcript. Each subagent's tool calls are returned via
236 > * {@link mapSessionEventsToTurns.subagentTurnsByToolCallId} so callers can
237 > * expose `getSubagentMessages` cheaply.
238 > *
239 > * If `workingDirectory` is provided, redundant `cd <workingDirectory> &&`
240 > * (or PowerShell equivalent) prefixes are stripped from shell tool
241 > * commands so clients see the simplified form.
242 > */
243 > export async function mapSessionEvents( mapSessionEvents.ts ×11
244 > session: URI,
245 > db: ISessionDatabase | undefined,
246 > events: readonly SessionEvent[],
247 > options: URI | IMapSessionEventsOptions | undefined = undefined,
248 > ): Promise<{ turns: Turn[]; subagentTurnsByToolCallId: ReadonlyMap<string, Turn[]> }> {
249 > const workingDirectory = options instanceof URI ? options : options?.workingDirectory;
250 > let currentModel = options instanceof URI ? undefined : options?.model;
251 > let currentAgent = options instanceof URI ? undefined : options?.agent;
252 > // First pass: collect tool-arg info and identify edit tool calls so we
253 > // can batch-load their stored file edits before the second pass needs
254 > // them at `tool.execution_complete` time. We also build the
255 > // `agentId` -> parent tool call id map here so the second pass can route
256 > // sub-agent events without depending on event ordering.
257 > const toolInfoByCallId = new Map<string, IToolStartInfo>();
258 > const editToolCallIds: string[] = [];
259 > const completionsByCallId = new Map<string, ToolExecutionCompleteData>();
260 >
261 > // The SDK tags events that originate from a sub-agent with an
262 > // envelope-level `agentId` (the deprecated `data.parentToolCallId` is no
263 > // longer populated). `subagent.started` carries both the sub-agent's
264 > // `agentId` and the parent tool call id it was spawned from, so we map
265 > // one to the other and resolve every later sub-agent event through it.
266 > const parentToolCallIdByAgentId = new Map<string, string>();
267 > const resolveParentToolCallId = (agentId: string | undefined, deprecatedParentToolCallId: string | undefined): string | undefined => {
268 > const mapped = agentId ? parentToolCallIdByAgentId.get(agentId) : undefined; mapSessionEvents.ts ×20
269 > return mapped ?? deprecatedParentToolCallId;
270 > };
272 > for (const e of events) {
273 > if (e.type === 'subagent.started') { mapSessionEvents.ts ×20
274 > if (e.agentId) { mapSessionEvents.ts ×8
275 > parentToolCallIdByAgentId.set(e.agentId, e.data.toolCallId); mapSessionEvents.ts ×6
276 > }
278 > if (e.type === 'tool.execution_complete') { mapSessionEvents.ts ×20
279 > completionsByCallId.set(e.data.toolCallId, e.data); mapSessionEvents.ts ×12
280 > }
281 > if (e.type === 'tool.execution_start') { mapSessionEvents.ts ×20
282 > const d = e.data; mapSessionEvents.ts ×6
283 > const parentToolCallId = resolveParentToolCallId(e.agentId, d.parentToolCallId);
284 > const info = makeToolStartInfo(d.toolName, d.arguments, parentToolCallId, workingDirectory, d);
285 > if (!info) {
286 continue;
287 }
288 > toolInfoByCallId.set(d.toolCallId, info); mapSessionEvents.ts ×6
289 > const command = isString(info.parameters?.command) ? info.parameters.command : undefined;
290 > if (isEditTool(d.toolName, command)) {
291 editToolCallIds.push(d.toolCallId);
292 }
296 > // Pre-load stored file-edit metadata for all edit tool calls.
297 > let storedEdits: Map<string, IFileEditRecord[]> | undefined;
298 > if (db && editToolCallIds.length > 0) {
299 try {
300 const records = await db.getFileEdits(editToolCallIds);
301 if (records.length > 0) {
302 storedEdits = new Map();
303 for (const r of records) {
304 let list = storedEdits.get(r.toolCallId);
305 if (!list) {
306 list = [];
307 storedEdits.set(r.toolCallId, list);
308 }
309 list.push(r);
310 }
311 }
312 } catch {
313 // Database may not exist yet for new sessions — that's fine.
314 }
315 }
317 > const sessionUriStr = session.toString();
318 > const providerId = session.scheme;
319 > const rawSessionId = AgentSession.id(session);
320 > const turns: Turn[] = [];
321 >
322 > // Subagent state. Each subagent has its own active turn builder; only
323 > // the most recent turn per subagent is built (subagents currently emit
324 > // at most one turn per invocation).
325 > const subagentBuilders = new Map<string, ITurnBuilder>();
326 > const subagentTurnStates = new Map<string, TurnState>();
327 > const subagentTurns = new Map<string, Turn[]>();
328 > const subagentInfoByToolCallId = new Map<string, ISubagentInfo>();
329 >
330 > let parentBuilder: ITurnBuilder | undefined;
331 > let parentTurnState = TurnState.Cancelled;
332 > let parentTurnAborted = false;
333 > let rootAssistantTurnActive = false;
334 > let pendingAutoModeResolved: Extract<SessionEvent, { type: 'session.auto_mode_resolved' }>['data'] | undefined;
335 >
336 > const flushParent = (): void => {
337 > if (!parentBuilder) {
338 > return;
339 > }
340 > turns.push(finalizeTurn(parentBuilder, parentTurnState)); mapSessionEvents.ts ×12
341 > parentBuilder = undefined;
342 > parentTurnState = TurnState.Cancelled;
343 > parentTurnAborted = false;
345 >
346 > const flushSubagent = (parentToolCallId: string): void => {
347 > const builder = subagentBuilders.get(parentToolCallId); mapSessionEvents.ts ×8
348 > if (!builder) {
349 > subagentTurnStates.delete(parentToolCallId); buildSessionEvents.ts ×1
350 > return;
351 > }
352 > subagentBuilders.delete(parentToolCallId); mapSessionEvents.ts ×6
353 > const state = subagentTurnStates.get(parentToolCallId) ?? TurnState.Complete;
354 > subagentTurnStates.delete(parentToolCallId); mapSessionEvents.ts ×8
355 > if (builder.responseParts.length === 0) {
356 return;
357 }
358 > const list = subagentTurns.get(parentToolCallId) ?? []; mapSessionEvents.ts ×6
359 > list.push(finalizeTurn(builder, state)); mapSessionEvents.ts ×8
360 > subagentTurns.set(parentToolCallId, list);
361 > };
363 > const ensureSubagentBuilder = (parentToolCallId: string): ITurnBuilder => {
364 > let builder = subagentBuilders.get(parentToolCallId); mapSessionEvents.ts ×6
365 > if (!builder) {
366 > builder = newTurnBuilder(generateUuid(), '');
367 > subagentBuilders.set(parentToolCallId, builder);
368 > if (!subagentTurnStates.has(parentToolCallId)) {
369 > subagentTurnStates.set(parentToolCallId, TurnState.Complete); mapSessionEvents.ts ×1
370 > }
372 > return builder;
373 > };
375 > const targetBuilderFor = (parentToolCallId: string | undefined): ITurnBuilder | undefined => {
376 > if (parentToolCallId) { mapSessionEvents.ts ×2
377 > return ensureSubagentBuilder(parentToolCallId); mapSessionEvents.ts ×6
378 > }
379 > return parentBuilder; mapSessionEvents.ts ×2
380 > };
382 > for (const e of events) {
383 > switch (e.type) { mapSessionEvents.ts ×20
384 > case 'assistant.turn_start':
385 > if (!e.agentId) { mapSessionEvents.ts ×6
386 > rootAssistantTurnActive = true;
387 > }
388 > break;
389 > case 'assistant.turn_end': mapSessionEvents.ts ×20
390 > if (!e.agentId) { mapSessionEvents.ts ×6
391 > rootAssistantTurnActive = false;
392 > }
393 > break;
394 > case 'session.model_change': { mapSessionEvents.ts ×20
395 > currentModel = { id: e.data.newModel }; mapSessionEvents.ts ×2
396 > break;
397 > }
398 > case 'session.auto_mode_resolved': { mapSessionEvents.ts ×20
399 > if (!e.agentId) { mapSessionEvents.ts ×2
400 > pendingAutoModeResolved = e.data;
401 > }
402 > break;
403 > }
404 > case 'subagent.deselected': { mapSessionEvents.ts ×20
405 if (!e.agentId) {
406 currentAgent = undefined;
407 }
408 break;
409 }
410 > case 'user.message': { mapSessionEvents.ts ×20
411 > if (isSyntheticUserMessage(e)) { mapSessionEvents.ts ×12
412 > continue; mapSessionEvents.ts ×1
413 > }
414 > const d = e.data; mapSessionEvents.ts ×12
415 > const messageId = d.interactionId ?? '';
416 > const content = d.content ?? '';
417 > const attachments = sdkAttachmentsToProtocol(d.attachments);
418 > // User messages carry no deprecated `parentToolCallId`; route
419 > // sub-agent user messages by the envelope `agentId` only.
420 > const parentToolCallId = resolveParentToolCallId(e.agentId, undefined);
421 > if (e.agentId && !parentToolCallId) {
422 > continue; mapSessionEvents.ts ×1
423 > }
424 > if (parentToolCallId) { mapSessionEvents.ts ×12
425 > const builder = ensureSubagentBuilder(parentToolCallId); mapSessionEvents.ts ×1
426 > builder.message = {
427 > ...builder.message,
428 > text: content,
429 > ...(attachments?.length ? { attachments } : {}),
430 > };
431 > } else { mapSessionEvents.ts ×12
432 > // A new top-level user message starts a new parent turn.
433 > // Use the SDK envelope id (the same value
434 > // `setTurnEventId` records as `event_id`) so the restored
435 > // turn id round-trips back to the SDK boundary id that
436 > // fork / truncate RPCs operate on.
437 > flushParent();
438 > const turnId = e.id ?? messageId;
439 > parentBuilder = newTurnBuilder(turnId, content, { attachments, model: currentModel, agent: currentAgent });
440 > if (pendingAutoModeResolved) {
441 > parentBuilder.usage = { mapSessionEvents.ts ×2
442 > model: pendingAutoModeResolved.chosenModel,
443 > _meta: { autoModeResolved: pendingAutoModeResolved },
444 > };
445 > pendingAutoModeResolved = undefined;
446 > }
448 > break;
449 > }
450 > case 'assistant.message': { mapSessionEvents.ts ×20
451 > const d = e.data; mapSessionEvents.ts ×6
452 > const messageId = d.messageId ?? d.interactionId ?? '';
453 > const content = d.content ?? '';
454 > const reasoningText = d.reasoningText;
455 > const hasToolRequests = !!d.toolRequests && d.toolRequests.length > 0;
456 > const parentToolCallId = resolveParentToolCallId(e.agentId, d.parentToolCallId);
457 > if (!content && !reasoningText && !hasToolRequests) {
458 > if (!parentToolCallId && parentBuilder && !parentTurnAborted) { mapSessionEvents.ts ×1
459 > parentTurnState = TurnState.Complete;
460 > }
461 > break;
462 > }
463 > // When this is the first event in a turn (no parent builder mapSessionEvents.ts ×6
464 > // yet), seed the builder with the SDK envelope id so the
465 > // turn id matches `turns.event_id` for fork/truncate
466 > // lookups. See the matching note in the `user.message`
467 > // branch above.
468 > const fallbackTurnId = e.id ?? messageId;
469 > const builder = targetBuilderFor(parentToolCallId)
470 ?? (parentBuilder = newTurnBuilder(fallbackTurnId, ''));
471 > if (reasoningText) { mapSessionEvents.ts ×6
472 > builder.responseParts.push({ mapSessionEvents.ts ×1
473 > kind: ResponsePartKind.Reasoning,
474 > id: generateUuid(),
475 > content: reasoningText,
476 > });
477 > }
478 > if (content) { mapSessionEvents.ts ×6
479 > builder.responseParts.push({ mapSessionEvents.ts ×1
480 > kind: ResponsePartKind.Markdown,
481 > id: generateUuid(),
482 > content,
483 > });
484 > }
485 > if (!parentToolCallId && builder === parentBuilder && !parentTurnAborted) { mapSessionEvents.ts ×6
486 > parentTurnState = hasToolRequests ? TurnState.Cancelled : TurnState.Complete;
487 > }
488 > if (d.toolRequests?.length) {
489 > appendFallbackToolRequests(builder, d.toolRequests, parentToolCallId); mapSessionEvents.ts ×4
490 > }
492 > }
493 > case 'system.notification': { mapSessionEvents.ts ×20
494 > const notification = buildCopilotSystemNotification(e); mapSessionEvents.ts ×6
495 > if (!notification) {
496 break;
497 }
498 > if (rootAssistantTurnActive && parentBuilder) { mapSessionEvents.ts ×6
499 > parentBuilder.responseParts.push({ mapSessionEvents.ts ×1
500 > kind: ResponsePartKind.SystemNotification,
501 > content: notification.messageText,
502 > });
503 > } else if (notification.startsTurn) { mapSessionEvents.ts ×6
504 > flushParent(); mapSessionEvents.ts ×1
505 > parentBuilder = newTurnBuilder(e.id, notification.messageText, { origin: MessageKind.SystemNotification });
506 > }
508 > }
509 > case 'subagent.started': { mapSessionEvents.ts ×20
510 > const d = e.data; mapSessionEvents.ts ×8
511 > subagentInfoByToolCallId.set(d.toolCallId, {
512 > agentName: d.agentName,
513 > agentDisplayName: d.agentDisplayName,
514 > agentDescription: d.agentDescription,
515 > });
516 > break;
517 > }
518 > case 'tool.execution_start': { mapSessionEvents.ts ×20
519 > const parentToolCallId = resolveParentToolCallId(e.agentId, e.data.parentToolCallId); mapSessionEvents.ts ×6
520 > if (!parentToolCallId && parentBuilder) {
521 > parentTurnState = TurnState.Cancelled; mapSessionEvents.ts ×1
522 > }
524 > }
525 > case 'tool.execution_complete': { mapSessionEvents.ts ×20
526 > const d = e.data; mapSessionEvents.ts ×12
527 > const info = toolInfoByCallId.get(d.toolCallId);
528 > if (!info) {
529 > // Orphan complete (no matching start), or hidden tool. mapSessionEvents.ts ×3
530 > continue;
531 > }
532 > toolInfoByCallId.delete(d.toolCallId); mapSessionEvents.ts ×6
533 > const parentToolCallId = resolveParentToolCallId(e.agentId, d.parentToolCallId);
534 > if (isTaskCompleteTool(info.toolName)) {
535 > const builder = targetBuilderFor(parentToolCallId); mapSessionEvents.ts ×3
536 > if (!builder) {
537 > continue; mapSessionEvents.ts ×1
538 > }
539 > const summary = getTaskCompleteMarkdown(info.parameters, d.error?.message ?? d.result?.content); mapSessionEvents.ts ×3
540 > if (summary) {
541 > builder.responseParts.push({ mapSessionEvents.ts ×1
542 > kind: ResponsePartKind.Markdown,
543 > id: generateUuid(),
544 > content: summary,
545 > });
546 > }
547 > if (!parentToolCallId && d.success && builder === parentBuilder && !parentTurnAborted) { mapSessionEvents.ts ×3
548 > parentTurnState = TurnState.Complete; mapSessionEvents.ts ×1
549 > }
550 > continue;
551 > }
552 > const builder = targetBuilderFor(parentToolCallId); mapSessionEvents.ts ×3
553 > if (!builder) {
554 // No active turn to attach this completion to.
555 continue;
556 }
557 > const completedPart = makeCompletedToolCallPart(d, info, sessionUriStr, providerId, rawSessionId, storedEdits, subagentInfoByToolCallId.get(d.toolCallId)); mapSessionEvents.ts ×3
558 > builder.responseParts.push(completedPart);
559 > // When a parent tool call that spawned a subagent completes,
560 > // flush the subagent's accumulated turn.
561 > if (!parentToolCallId && subagentInfoByToolCallId.has(d.toolCallId)) { mapSessionEvents.ts ×12
562 > flushSubagent(d.toolCallId); mapSessionEvents.ts ×8
563 > }
565 > }
566 > case 'skill.invoked': { mapSessionEvents.ts ×20
567 > const synth = synthesizeSkillToolCall(e.data, e.id); mapSessionEvents.ts ×3
568 > const parentToolCallId = resolveParentToolCallId(e.agentId, undefined);
569 > const builder = targetBuilderFor(parentToolCallId)
570 ?? (parentBuilder = newTurnBuilder(generateUuid(), ''));
571 > if (!parentToolCallId && builder === parentBuilder) { mapSessionEvents.ts ×3
572 parentTurnState = TurnState.Cancelled;
573 }
574 > builder.responseParts.push({ mapSessionEvents.ts ×3
575 > kind: ResponsePartKind.ToolCall,
576 > toolCall: {
577 > status: ToolCallStatus.Completed,
578 > toolCallId: synth.toolCallId,
579 > toolName: synth.toolName,
580 > displayName: synth.displayName,
581 > invocationMessage: synth.invocationMessage,
582 > success: true,
583 > pastTenseMessage: synth.pastTenseMessage,
584 > confirmed: ToolCallConfirmationReason.NotNeeded,
585 > } satisfies ToolCallCompletedState,
586 > });
587 > break;
588 > }
589 > case 'abort': { mapSessionEvents.ts ×20
590 > const parentToolCallId = resolveParentToolCallId(e.agentId, undefined); mapSessionEvents.ts ×3
591 > if (parentToolCallId) {
592 > subagentTurnStates.set(parentToolCallId, TurnState.Cancelled); mapSessionEvents.ts ×1
593 > } else { mapSessionEvents.ts ×3
594 > rootAssistantTurnActive = false; mapSessionEvents.ts ×1
595 > if (parentBuilder) {
596 > parentTurnState = TurnState.Cancelled;
597 > parentTurnAborted = true;
598 > }
599 > }
601 > }
602 > default: mapSessionEvents.ts ×20
605 > }
607 > flushParent();
608 > for (const parentToolCallId of [...subagentBuilders.keys()]) {
609 flushSubagent(parentToolCallId);
610 }
612 > return { turns, subagentTurnsByToolCallId: subagentTurns };
613 >
614 > function appendFallbackToolRequests(builder: ITurnBuilder, toolRequests: readonly AssistantMessageToolRequest[], parentToolCallId: string | undefined): void {
615 > for (const request of toolRequests) { mapSessionEvents.ts ×4
616 > const completion = completionsByCallId.get(request.toolCallId);
617 > if (completion && toolInfoByCallId.has(request.toolCallId)) {
618 > continue; mapSessionEvents.ts ×1
619 > }
620 > const info = toolInfoByCallId.get(request.toolCallId) mapSessionEvents.ts ×3
621 > ?? makeToolStartInfo(request.name, request.arguments, parentToolCallId, workingDirectory, request);
622 > if (!info) { mapSessionEvents.ts ×4
623 > continue; mapSessionEvents.ts ×3
624 > }
625 > if (isTaskCompleteTool(info.toolName)) { mapSessionEvents.ts ×3
626 > const summary = getTaskCompleteMarkdown(info.parameters, completion?.error?.message ?? completion?.result?.content); mapSessionEvents.ts ×1
627 > if (summary) {
628 > builder.responseParts.push({
629 > kind: ResponsePartKind.Markdown,
630 > id: generateUuid(),
631 > content: summary,
632 > });
633 > }
634 > if (!parentToolCallId && completion?.success && builder === parentBuilder && !parentTurnAborted) {
635 > parentTurnState = TurnState.Complete;
636 > }
637 > continue;
638 > }
639 > builder.responseParts.push(makeCompletedToolCallPart( mapSessionEvents.ts ×3
640 > completion ?? { toolCallId: request.toolCallId, success: true },
642 > sessionUriStr,
643 > providerId,
644 > rawSessionId,
645 > storedEdits,
646 > subagentInfoByToolCallId.get(request.toolCallId),
647 > ));
648 > }
649 > }
652 > /**
653 > * Translates the SDK's `UserMessageAttachment[]` payload back into the
654 > * agent-protocol {@link MessageAttachment} shape. Text blob attachments
655 > * surface as {@link MessageAttachmentKind.Simple}; other blobs surface as
656 > * inline {@link MessageAttachmentKind.EmbeddedResource} payloads.
657 > * File/directory/selection variants reconstruct local `Resource`
658 > * attachments. We don't try to re-link these to the on-disk snapshots
659 > * produced by the agent host's attachment rewriter — the SDK keeps a
660 > * copy of the bytes / paths it actually saw on send, which is the
661 > * authoritative record for replay.
662 > */
663 > function sdkAttachmentsToProtocol( mapSessionEvents.ts ×12
664 > attachments: readonly Attachment[] | undefined,
665 > ): MessageAttachment[] | undefined {
666 > if (!attachments?.length) {
667 > return undefined; mapSessionEvents.ts ×1
668 > }
669 > const out: MessageAttachment[] = []; mapSessionEvents.ts ×7
670 > for (const a of attachments) {
671 > const converted = sdkAttachmentToProtocol(a);
672 > if (converted) {
673 > out.push(converted);
674 > }
675 > }
676 > return out.length > 0 ? out : undefined; mapSessionEvents.ts ×12
677 > }
679 > function sdkAttachmentToProtocol( mapSessionEvents.ts ×7
680 > attachment: Attachment,
681 > ): MessageAttachment | undefined {
682 > switch (attachment.type) {
683 > case 'file': {
684 > return { mapSessionEvents.ts ×2
685 > type: MessageAttachmentKind.Resource,
686 > uri: URI.file(attachment.path).toString(),
687 > label: attachment.displayName || basename(attachment.path),
688 > displayKind: getMediaMime(attachment.path)?.startsWith('image/') ? 'image' : 'document',
689 > };
690 > }
691 > case 'directory': { mapSessionEvents.ts ×7
692 return {
693 type: MessageAttachmentKind.Resource,
694 uri: URI.file(attachment.path).toString(),
695 label: attachment.displayName || basename(attachment.path),
696 displayKind: 'directory',
697 };
698 }
699 > case 'selection': { mapSessionEvents.ts ×7
700 return {
701 type: MessageAttachmentKind.Resource,
702 uri: URI.file(attachment.filePath).toString(),
703 label: attachment.displayName,
704 displayKind: 'selection',
705 selection: { range: attachment.selection! },
706 };
707 }
708 > case 'blob': { mapSessionEvents.ts ×7
709 > if (typeof attachment.data !== 'string') { copilotAttachmentUtils.ts ×3
710 return undefined;
711 }
712 > if (attachment.mimeType.startsWith('text/plain')) { copilotAttachmentUtils.ts ×3
713 > const displayKind = readSimpleAttachmentDisplayKindFromMimeType(attachment.mimeType);
714 > return {
715 > type: MessageAttachmentKind.Simple,
716 > label: attachment.displayName ?? 'attachment',
717 > modelRepresentation: decodeBase64(attachment.data ?? '').toString(),
718 > ...(displayKind !== undefined ? { displayKind } : {}),
719 > };
720 > }
721 > const displayKind = attachment.mimeType.startsWith('image/') ? 'image' : undefined;
722 > return {
723 > type: MessageAttachmentKind.EmbeddedResource,
724 > label: attachment.displayName ?? 'attachment',
725 > data: attachment.data ?? '',
726 > contentType: attachment.mimeType,
727 > displayKind,
728 > };
729 > }
730 > default: mapSessionEvents.ts ×7
731 return undefined;
733 > }
735 > /**
736 > * Builds a {@link ToolCallCompletedState}-shaped response part from an
737 > * SDK `tool.execution_complete` event. Restores file-edit content
738 > * references from `storedEdits` and merges subagent metadata when the
739 > * tool call spawned a child session.
740 > */
741 > function makeCompletedToolCallPart( mapSessionEvents.ts ×5
742 > d: ToolExecutionCompleteData,
743 > info: IToolStartInfo,
744 > sessionUriStr: string,
745 > providerId: string,
746 > rawSessionId: string,
747 > storedEdits: Map<string, IFileEditRecord[]> | undefined,
748 > subagent: ISubagentInfo | undefined,
749 > ): ResponsePart {
750 > const toolOutput = d.error?.message ?? d.result?.content;
751 > const content: ToolResultContent[] = [];
752 > if (toolOutput !== undefined) {
753 > content.push({ type: ToolResultContentType.Text, text: toolOutput }); mapSessionEvents.ts ×1
754 > }
755 > appendSdkToolResultContent(content, d.result?.contents, { session: sessionUriStr, toolCallId: d.toolCallId, title: info.displayName }); mapSessionEvents.ts ×5
756 >
757 > // Restore file edit content references from the database.
758 > const edits = storedEdits?.get(d.toolCallId);
759 > if (edits) {
760 for (const edit of edits) {
761 const beforeUri = edit.kind === 'rename' && edit.originalPath
762 ? URI.file(edit.originalPath).toString()
763 : URI.file(edit.filePath).toString();
764 const afterUri = URI.file(edit.filePath).toString();
765 const hasBefore = edit.kind !== 'create';
766 const hasAfter = edit.kind !== 'delete';
767 content.push({
768 type: ToolResultContentType.FileEdit,
769 before: hasBefore ? {
770 uri: beforeUri,
771 content: { uri: buildSessionDbUri(sessionUriStr, edit.toolCallId, edit.filePath, 'before') },
772 } : undefined,
773 after: hasAfter ? {
774 uri: afterUri,
775 content: { uri: buildSessionDbUri(sessionUriStr, edit.toolCallId, edit.filePath, 'after') },
776 } : undefined,
777 diff: (edit.addedLines !== undefined || edit.removedLines !== undefined)
778 ? { added: edit.addedLines, removed: edit.removedLines }
779 : undefined,
780 });
781 }
782 }
784 > if (subagent) {
785 > content.push({ mapSessionEvents.ts ×8
786 > type: ToolResultContentType.Subagent,
787 > resource: buildSubagentSessionUri(sessionUriStr, d.toolCallId),
788 > title: subagent.agentDisplayName,
789 > agentName: subagent.agentName,
790 > description: subagent.agentDescription,
791 > });
792 > }
794 > const mcpServerName = info.mcpServerName ?? readStringProperty(d, 'mcpServerName');
795 > const mcpToolName = info.mcpToolName ?? readStringProperty(d, 'mcpToolName');
796 > const mcpUiResourceUri = info.mcpUiResourceUri ?? readMcpUiResourceUri(d);
797 > const mcpUi: IToolCallUiMeta | undefined = mcpUiResourceUri
799 > resourceUri: mcpUiResourceUri,
800 > ...(mcpServerName ? { channel: buildMcpChannel(providerId, rawSessionId, mcpServerName) } : {}),
801 > }
802 > : undefined; mapSessionEvents.ts ×1
804 > const tc: ToolCallCompletedState = {
805 > status: ToolCallStatus.Completed,
806 > toolCallId: d.toolCallId,
807 > toolName: info.toolName,
808 > displayName: info.displayName,
809 > intention: info.intention,
810 > ...(mcpServerName ? { contributor: { kind: ToolCallContributorKind.MCP, customizationId: buildMcpTopLevelCustomizationId(providerId, rawSessionId, mcpServerName) } } : {}),
811 > invocationMessage: info.invocationMessage,
812 > toolInput: info.toolInput,
813 > success: d.success,
814 > pastTenseMessage: getPastTenseMessage(info.toolName, info.displayName, info.parameters, d.success, d.success ? toolOutput : undefined),
815 > content: content.length > 0 ? content : undefined,
816 > error: d.error,
817 > confirmed: ToolCallConfirmationReason.NotNeeded,
818 > _meta: toToolCallMeta({
819 > toolKind: info.toolKind,
820 > language: info.language,
821 > subagentDescription: info.subagentDescription,
822 > subagentAgentName: info.subagentAgentName,
823 > mcpServerName,
824 > mcpToolName,
825 > ui: mcpUi,
826 > }),
827 > };
828 > return { kind: ResponsePartKind.ToolCall, toolCall: tc };
829 > }