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

279 LOC · 275 covered · 4 uncovered · 36 ranges · 405 concepts · 17 introducers · 181 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 > /*--------------------------------------------------------------------------------------------- buildSessionEvents.ts ×4
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 { SessionEvent } from '@github/copilot-sdk';
7 > import { generateUuid, isUUID } from '../../../../base/common/uuid.js';
8 > import { ResponsePartKind, ToolCallStatus, ToolResultContentType, TurnState, type ToolCallCompletedState, type ToolResultContent, type ToolResultSubagentContent, type Turn } from '../../common/state/sessionState.js';
9 >
10 > /**
11 > * Default schema version stamped on the synthesized `session.start` event.
12 > *
13 > * The Copilot SDK owns the authoritative event-format schema version; this
14 > * value is only meaningful when the resulting `events.jsonl` is actually
15 > * resumed by a CLI. It is irrelevant to the reconstruction performed by
16 > * {@link mapSessionEvents} (which ignores it), so unit tests can rely on the
17 > * default. Callers that write a log for real resume SHOULD pass the version the
18 > * target CLI expects.
19 > */
20 > const DEFAULT_SESSION_EVENT_SCHEMA_VERSION = 1;
21 >
22 > /**
23 > * Producer identifier stamped on the synthesized `session.start` event so a
24 > * migrated event log is attributable to this translation path rather than a
25 > * genuine agent run.
26 > */
27 > const MIGRATION_PRODUCER = 'vscode-copilot-migration';
28 >
29 > /**
30 > * Options controlling how {@link buildSessionEventsFromTurns} synthesizes a
31 > * Copilot SDK event log from VS Code turns.
32 > */
33 > export interface IBuildSessionEventsOptions {
34 > /** The target session id (stamped on the `session.start` event). */
35 > readonly sessionId: string;
36 > /** Working directory of the session, recorded on `session.start` context. */
37 > readonly workingDirectory?: string;
38 > /** Model id to attribute the synthesized assistant messages to, if known. */
39 > readonly model?: string;
40 > /** Copilot application version string for `session.start`. Defaults to `0.0.0`. */
41 > readonly copilotVersion?: string;
42 > /** Event-format schema version for `session.start`. See {@link DEFAULT_SESSION_EVENT_SCHEMA_VERSION}. */
43 > readonly schemaVersion?: number;
44 > /** Base time for the synthesized (monotonically increasing) event timestamps. Defaults to now. */
45 > readonly startTime?: Date;
46 > }
47 >
48 > /**
49 > * Translates a sequence of VS Code {@link Turn}s into a Copilot SDK
50 > * {@link SessionEvent} log (the on-disk `events.jsonl` shape), reversing the
51 > * reconstruction performed by `mapSessionEvents`.
52 > *
53 > * The result is a valid parent-linked event chain: a leading `session.start`
54 > * followed, per turn, by a `user.message` and the turn's response emitted in
55 > * order — assistant markdown/reasoning as `assistant.message` events and each
56 > * completed tool call as a `tool.execution_start` + `tool.execution_complete`
57 > * pair (preceded by a `subagent.started` when the tool call carries sub-agent
58 > * content, so the sub-agent name/description survive a resume). Assistant text
59 > * accumulated before a tool call is flushed as its own `assistant.message` so
60 > * the reconstructed part order matches the original.
61 > *
62 > * Every event envelope id must be a UUID (the Copilot runtime rejects non-UUID
63 > * event ids). A turn whose {@link Turn.id} is already a UUID reuses it as the
64 > * `user.message` envelope id, so the reconstructed turn keeps the same id the
65 > * SDK's fork / truncate RPCs address and the caller can seed matching protocol
66 > * turns; a non-UUID id is replaced with a minted UUID.
67 > *
68 > * Only completed tool calls are translated; streaming / pending tool states and
69 > * file-edit content (which lives in the session database) are not yet emitted.
70 > * A cancelled turn ({@link TurnState.Cancelled}) emits a trailing `abort` event
71 > * so it reconstructs as cancelled. Content refs and system notifications are
72 > * skipped.
73 > */
74 > export function buildSessionEventsFromTurns(turns: readonly Turn[], options: IBuildSessionEventsOptions): SessionEvent[] {
75 > const events: SessionEvent[] = []; buildSessionEvents.ts ×5
76 > let parentId: string | null = null;
77 >
78 > // Synthesize strictly increasing ISO timestamps so the event order on disk
79 > // is unambiguous even for turns that were originally seconds apart.
80 > let clock = (options.startTime ?? new Date()).getTime();
81 > const nextTimestamp = (): string => new Date(clock++).toISOString();
82 >
83 > const push = (event: SessionEvent): void => {
84 > events.push(event);
85 > parentId = event.id;
86 > };
87 >
88 > /** Emits the `tool.execution_start` + `tool.execution_complete` pair for a completed tool call. */
89 > const pushCompletedToolCall = (tc: ToolCallCompletedState): void => {
90 > let toolArguments: Record<string, unknown> | undefined; buildSessionEvents.ts ×5
91 > if (tc.toolInput) {
93 > const parsed = JSON.parse(tc.toolInput);
94 > if (parsed && typeof parsed === 'object') {
95 > toolArguments = parsed as Record<string, unknown>;
96 > }
97 > } catch {
98 // Non-JSON tool input: omit structured arguments (the forward
99 // mapper regenerates the invocation display from the tool name).
100 }
102 > // If the tool call carries sub-agent identity, emit `subagent.started` buildSessionEvents.ts ×5
103 > // first so a resume reconstructs the sub-agent name/description onto the
104 > // parent tool call (the SDK keys this by `toolCallId`). Required fields
105 > // fall back to the title so the event stays well-formed.
106 > const subagent = tc.content?.find((c): c is ToolResultSubagentContent => c.type === ToolResultContentType.Subagent);
107 > if (subagent) {
109 > id: generateUuid(),
110 > parentId,
111 > timestamp: nextTimestamp(),
112 > type: 'subagent.started',
113 > data: {
114 > toolCallId: tc.toolCallId,
115 > agentName: subagent.agentName ?? subagent.title,
116 > agentDisplayName: subagent.title,
117 > agentDescription: subagent.description ?? '',
118 > },
119 > });
120 > }
122 > id: generateUuid(),
123 > parentId,
124 > timestamp: nextTimestamp(),
125 > type: 'tool.execution_start',
126 > data: {
127 > toolCallId: tc.toolCallId,
128 > toolName: tc.toolName,
129 > ...(toolArguments ? { arguments: toolArguments } : {}),
130 > },
131 > });
132 > const resultText = extractToolResultText(tc.content);
133 > push({
134 > id: generateUuid(),
135 > parentId,
136 > timestamp: nextTimestamp(),
137 > type: 'tool.execution_complete',
138 > data: {
139 > toolCallId: tc.toolCallId,
140 > success: tc.success,
141 > ...(tc.success ? { result: { content: resultText } } : {}),
142 > ...(tc.error ? { error: { message: tc.error.message, ...(tc.error.code ? { code: tc.error.code } : {}) } } : {}),
143 > },
144 > });
145 > };
147 > push({
148 > id: generateUuid(),
149 > parentId,
150 > timestamp: nextTimestamp(),
151 > type: 'session.start',
152 > data: {
153 > sessionId: options.sessionId,
154 > copilotVersion: options.copilotVersion ?? '0.0.0',
155 > producer: MIGRATION_PRODUCER,
156 > startTime: nextTimestamp(),
157 > version: options.schemaVersion ?? DEFAULT_SESSION_EVENT_SCHEMA_VERSION,
158 > ...(options.model ? { selectedModel: options.model } : {}),
159 > ...(options.workingDirectory ? { context: { cwd: options.workingDirectory } } : {}),
160 > },
161 > });
162 >
163 > for (const turn of turns) {
164 > // Reuse the turn id as the user-message envelope id when it is already a
165 > // UUID (the runtime rejects non-UUID event ids) so the reconstructed turn
166 > // keeps the id the SDK's fork/truncate RPCs address and a caller can seed
167 > // a matching protocol turn; otherwise mint a fresh UUID.
168 > push({
169 > id: isUUID(turn.id) ? turn.id : generateUuid(),
170 > parentId,
171 > timestamp: nextTimestamp(),
172 > type: 'user.message',
173 > data: {
174 > content: turn.message.text,
175 > source: 'user',
176 > },
177 > });
178 >
179 > let markdown = '';
180 > let reasoning = '';
181 > const flushAssistantMessage = (): void => {
182 > if (!markdown && !reasoning) {
184 > }
186 > id: generateUuid(),
187 > parentId,
188 > timestamp: nextTimestamp(),
189 > type: 'assistant.message',
190 > data: {
191 > content: markdown,
192 > messageId: generateUuid(),
193 > ...(reasoning ? { reasoningText: reasoning } : {}), buildSessionEvents.ts ×5
194 > ...(options.model ? { model: options.model } : {}),
195 > },
196 > });
197 > markdown = '';
198 > reasoning = '';
199 > };
200 >
201 > for (const part of turn.responseParts) {
202 > if (part.kind === ResponsePartKind.Markdown) { buildSessionEvents.ts ×3
203 > // Flush pending reasoning first: the reverse mapper emits reasoning buildSessionEvents.ts ×3
204 > // before content within a single assistant.message, so interleaved
205 > // reasoning/markdown must be split into separate messages to keep
206 > // the original stream order.
207 > if (reasoning) {
208 > flushAssistantMessage(); buildSessionEvents.ts ×3
209 > }
210 > markdown += part.content; buildSessionEvents.ts ×3
211 > } else if (part.kind === ResponsePartKind.Reasoning) { buildSessionEvents.ts ×3
212 > if (markdown) { buildSessionEvents.ts ×3
213 > flushAssistantMessage(); buildSessionEvents.ts ×1
214 > }
215 > reasoning += part.content; buildSessionEvents.ts ×3
216 > } else if (part.kind === ResponsePartKind.ToolCall && part.toolCall.status === ToolCallStatus.Completed) { buildSessionEvents.ts ×1
217 > // Flush accumulated assistant text before the tool call so the buildSessionEvents.ts ×5
218 > // reconstructed part order matches the original interleaving.
219 > flushAssistantMessage();
220 > pushCompletedToolCall(part.toolCall);
221 > }
222 > // Content refs and system notifications are not yet translated. buildSessionEvents.ts ×3
223 > }
224 > flushAssistantMessage(); buildSessionEvents.ts ×5
225 >
226 > // A cancelled turn reconstructs as `TurnState.Cancelled` only if the event
227 > // stream ends without a finalizing assistant message (the reverse mapper
228 > // defaults to cancelled and upgrades to complete on a final message). Emit
229 > // an explicit `abort` after the already-flushed content so the turn is
230 > // marked cancelled while keeping its text.
231 > if (turn.state === TurnState.Cancelled) {
233 > id: generateUuid(),
234 > parentId,
235 > timestamp: nextTimestamp(),
236 > type: 'abort',
237 > data: { reason: 'user_initiated' },
238 > });
239 > }
241 >
242 > return events;
243 > }
245 > /** Concatenates the text of a completed tool call's textual result content blocks. */
246 > function extractToolResultText(content: readonly ToolResultContent[] | undefined): string { buildSessionEvents.ts ×5
247 > if (!content) {
248 > return ''; buildSessionEvents.ts ×1
249 > }
250 > let text = ''; buildSessionEvents.ts ×1
251 > for (const item of content) {
252 > if (item.type === ToolResultContentType.Text) {
253 > text += item.text;
254 > }
255 > }
256 > return text;
257 > }
259 > /**
260 > * Serializes SDK session events into the on-disk `events.jsonl` representation:
261 > * one JSON object per line, terminated by a newline so a subsequent append
262 > * starts on a fresh line. Returns the empty string for an empty event list.
263 > */
264 > export function serializeSessionEventsToJsonl(events: readonly SessionEvent[]): string {
265 > if (events.length === 0) { buildSessionEvents.ts ×2
266 > return ''; buildSessionEvents.ts ×1
267 > }
268 > return events.map(event => JSON.stringify(event)).join('\n') + '\n'; buildSessionEvents.ts ×2
269 > }
271 > /**
272 > * Convenience combining {@link buildSessionEventsFromTurns} and
273 > * {@link serializeSessionEventsToJsonl}: turns the given VS Code turns directly
274 > * into the `events.jsonl` bytes to write for the target session.
275 > */
276 > export function buildSessionEventLogFromTurns(turns: readonly Turn[], options: IBuildSessionEventsOptions): string {
277 > return serializeSessionEventsToJsonl(buildSessionEventsFromTurns(turns, options)); buildSessionEvents.ts ×1
278 > }
279