Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/chatSessions/claude/node/sessionParser/claudeSessionSchema.ts
13406 views
1
/*---------------------------------------------------------------------------------------------
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
/**
7
* Schema Validators for Claude Code Session Files
8
*
9
* This module provides type-safe validators for parsing Claude Code session JSONL files.
10
* It uses a composable validator pattern that ensures runtime validation matches static types.
11
*
12
* ## Session File Format
13
* Claude Code stores sessions in JSONL format with several entry types:
14
* - QueueOperationEntry: Session queue state (dequeue operations)
15
* - UserMessageEntry: User messages with optional tool results
16
* - AssistantMessageEntry: Assistant responses including tool use, thinking blocks
17
* - SummaryEntry: Session summaries for display labels
18
* - ChainNode: Generic linked list node for parent-chain resolution
19
*
20
* ## Validation Approach
21
* - Every JSON.parse result goes through validators before use
22
* - No type assertions (`as`) - all types are inferred from validators
23
* - Detailed error messages for debugging schema mismatches
24
*
25
* @see CLAUDE.md for complete format documentation
26
*/
27
28
import type Anthropic from '@anthropic-ai/sdk';
29
import {
30
IValidator,
31
ValidationError,
32
ValidatorType,
33
vArray,
34
vBoolean,
35
vEnum,
36
vLiteral,
37
vNullable,
38
vNumber,
39
vObj,
40
vObjAny,
41
vRequired,
42
vString,
43
vUnchecked,
44
vUndefined,
45
vUnion,
46
vUnknown,
47
} from '../../../../../platform/configuration/common/validator';
48
49
// Re-export validator utilities for convenience
50
export { IValidator, ValidationError, ValidatorType };
51
52
// #region Primitive Validators
53
54
/**
55
* Validates ISO 8601 timestamp strings (e.g., "2026-01-31T00:34:50.025Z").
56
* Does not validate that the date is semantically valid, only format.
57
*/
58
export function vIsoTimestamp(): IValidator<string> {
59
return {
60
validate(content: unknown) {
61
if (typeof content !== 'string') {
62
return { content: undefined, error: { message: `Expected ISO timestamp string, got ${typeof content}` } };
63
}
64
// Basic ISO 8601 format check (YYYY-MM-DDTHH:MM:SS with optional fractional seconds and Z)
65
const isoPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?$/;
66
if (!isoPattern.test(content)) {
67
return { content: undefined, error: { message: `Invalid ISO timestamp format: ${content}` } };
68
}
69
return { content, error: undefined };
70
},
71
toSchema() {
72
return { type: 'string', format: 'date-time' };
73
}
74
};
75
}
76
77
/**
78
* Validates UUID-like strings.
79
* We use a lenient approach since real session data may have variations
80
* like agent IDs (e.g., "a139fcf") or other identifier formats.
81
*
82
* Strict UUID format: "6762c0b9-ee55-42cc-8998-180da7f37462"
83
* But we accept any non-empty string to handle edge cases.
84
*/
85
export function vUuid(): IValidator<string> {
86
return {
87
validate(content: unknown) {
88
if (typeof content !== 'string') {
89
return { content: undefined, error: { message: `Expected UUID string, got ${typeof content}` } };
90
}
91
if (content.length === 0) {
92
return { content: undefined, error: { message: 'Expected non-empty UUID string' } };
93
}
94
return { content, error: undefined };
95
},
96
toSchema() {
97
return { type: 'string', format: 'uuid' };
98
}
99
};
100
}
101
102
// #endregion
103
104
// #region Message Content Validators
105
// These validators parse session file content and output SDK-compatible types.
106
// Using SDK types directly ensures compile-time errors when the SDK changes.
107
108
/**
109
* Compile-time assertion that validator output is assignable to SDK type.
110
* If SDK changes in incompatible ways, this will fail and remind us to update the validator.
111
* Direction: Validator -> SDK (validator output can be used as SDK type)
112
*/
113
function assertValidatorAssignable<_TValidator extends TSDKType, TSDKType>(): void { }
114
115
/**
116
* Text content block in assistant messages.
117
* Matches Anthropic.TextBlock from the SDK.
118
*/
119
export const vTextBlock = vObj({
120
type: vRequired(vLiteral('text')),
121
text: vRequired(vString()),
122
citations: vNullable(vArray(vUnchecked<Anthropic.TextCitation>())),
123
});
124
assertValidatorAssignable<ValidatorType<typeof vTextBlock>, Anthropic.TextBlock>();
125
export type TextBlock = Anthropic.TextBlock;
126
127
/**
128
* Thinking content block in assistant messages.
129
* Matches Anthropic.ThinkingBlock from the SDK.
130
*/
131
export const vThinkingBlock = vObj({
132
type: vRequired(vLiteral('thinking')),
133
thinking: vRequired(vString()),
134
signature: vRequired(vString()),
135
});
136
assertValidatorAssignable<ValidatorType<typeof vThinkingBlock>, Anthropic.ThinkingBlock>();
137
export type ThinkingBlock = Anthropic.ThinkingBlock;
138
139
/**
140
* Tool use content block in assistant messages.
141
* Matches Anthropic.Beta.Messages.BetaToolUseBlock from the SDK.
142
*/
143
export const vToolUseBlock = vObj({
144
type: vRequired(vLiteral('tool_use')),
145
id: vRequired(vString()),
146
name: vRequired(vString()),
147
input: vRequired(vUnknown()),
148
});
149
assertValidatorAssignable<ValidatorType<typeof vToolUseBlock>, Anthropic.Beta.Messages.BetaToolUseBlock>();
150
export type ToolUseBlock = Anthropic.Beta.Messages.BetaToolUseBlock;
151
152
/**
153
* Tool result content block in user messages (response to tool use).
154
* Matches Anthropic.ToolResultBlockParam from the SDK.
155
*
156
* Note: Content array elements use vUnchecked because:
157
* - Data originates from Claude's API (trusted source)
158
* - Full validators for each block param type would add significant maintenance cost
159
* - The outer structure is validated; malformed inner content surfaces at consumption time
160
*/
161
export const vToolResultBlock = vObj({
162
type: vRequired(vLiteral('tool_result')),
163
tool_use_id: vRequired(vString()),
164
content: vUnion(
165
vString(),
166
vArray(vUnchecked<Anthropic.TextBlockParam | Anthropic.ImageBlockParam | Anthropic.SearchResultBlockParam | Anthropic.DocumentBlockParam>()),
167
vUndefined()
168
),
169
is_error: vBoolean(),
170
});
171
assertValidatorAssignable<ValidatorType<typeof vToolResultBlock>, Anthropic.ToolResultBlockParam>();
172
export type ToolResultBlock = Anthropic.ToolResultBlockParam;
173
174
/**
175
* Base64 image source with inline data.
176
* Matches Anthropic.Base64ImageSource from the SDK.
177
*/
178
const vBase64ImageSource = vObj({
179
type: vRequired(vLiteral('base64')),
180
media_type: vRequired(vEnum('image/jpeg', 'image/png', 'image/gif', 'image/webp')),
181
data: vRequired(vString()),
182
});
183
184
/**
185
* URL image source with a remote URL.
186
* Matches Anthropic.URLImageSource from the SDK.
187
*/
188
const vURLImageSource = vObj({
189
type: vRequired(vLiteral('url')),
190
url: vRequired(vString()),
191
});
192
193
/**
194
* Image content block in user messages.
195
* Matches Anthropic.ImageBlockParam from the SDK.
196
*
197
* Source is validated as a discriminated union of base64 and url shapes,
198
* ensuring required fields (type, media_type/data or url) are present.
199
*/
200
export const vImageBlock = vObj({
201
type: vRequired(vLiteral('image')),
202
source: vRequired(vUnion(vBase64ImageSource, vURLImageSource)),
203
});
204
assertValidatorAssignable<ValidatorType<typeof vImageBlock>, Anthropic.ImageBlockParam>();
205
export type ImageBlock = Anthropic.ImageBlockParam;
206
207
/**
208
* Unknown content block type for forward compatibility.
209
* Allows parsing of new block types the SDK may introduce.
210
*/
211
export const vUnknownContentBlock = vObj({
212
type: vRequired(vString()),
213
});
214
export type UnknownContentBlock = { type: string };
215
216
/**
217
* Union of all known content block types.
218
* For assistant messages, use ContentBlock (excludes ToolResultBlock).
219
* For user messages, content may also include ToolResultBlock.
220
*/
221
export const vContentBlock = vUnion(
222
vTextBlock,
223
vThinkingBlock,
224
vToolUseBlock,
225
vToolResultBlock,
226
vImageBlock,
227
vUnknownContentBlock
228
);
229
export type ContentBlock = TextBlock | ThinkingBlock | ToolUseBlock | ToolResultBlock | ImageBlock | UnknownContentBlock;
230
231
// #endregion
232
233
// #region Message Usage Validators
234
235
/**
236
* Cache creation details for token usage.
237
*/
238
export const vCacheCreation = vObj({
239
ephemeral_1h_input_tokens: vNumber(),
240
ephemeral_5m_input_tokens: vNumber(),
241
});
242
export type CacheCreation = ValidatorType<typeof vCacheCreation>;
243
244
/**
245
* Token usage information for API calls.
246
*/
247
export const vUsage = vObj({
248
cache_creation: vNullable(vCacheCreation),
249
cache_creation_input_tokens: vNumber(),
250
cache_read_input_tokens: vNumber(),
251
input_tokens: vNumber(),
252
output_tokens: vNumber(),
253
});
254
export type Usage = ValidatorType<typeof vUsage>;
255
256
// #endregion
257
258
// #region Role-Specific Message Validators
259
260
/**
261
* User message content (from Anthropic SDK MessageParam type).
262
*/
263
export const vUserMessageContent = vObj({
264
role: vRequired(vLiteral('user')),
265
content: vRequired(vUnion(vString(), vArray(vContentBlock))),
266
});
267
export type UserMessageContent = ValidatorType<typeof vUserMessageContent>;
268
269
/**
270
* Assistant message content (from Anthropic SDK BetaMessage type).
271
*/
272
export const vAssistantMessageContent = vObj({
273
role: vRequired(vLiteral('assistant')),
274
content: vRequired(vArray(vContentBlock)),
275
id: vString(),
276
model: vString(),
277
type: vString(),
278
stop_reason: vNullable(vString()),
279
stop_sequence: vNullable(vString()),
280
usage: vUsage,
281
parent_tool_use_id: vNullable(vString()),
282
});
283
export type AssistantMessageContent = ValidatorType<typeof vAssistantMessageContent>;
284
285
/**
286
* System message content — a simple text entry produced by the runtime
287
* (e.g., "Conversation compacted" from a compact boundary).
288
*/
289
interface SystemMessageContent {
290
readonly role: 'system';
291
readonly content: string;
292
}
293
294
/**
295
* Model ID used by the SDK for synthetic messages (e.g., "No response requested." from abort).
296
* These messages should be filtered out from display and processing.
297
*/
298
export const SYNTHETIC_MODEL_ID = '<synthetic>';
299
300
// #endregion
301
302
// #region Session Entry Validators
303
304
/**
305
* Queue operation entry - represents session queue state changes.
306
* Example: { "type": "queue-operation", "operation": "dequeue", "timestamp": "...", "sessionId": "..." }
307
*/
308
export const vQueueOperationEntry = vObj({
309
type: vRequired(vLiteral('queue-operation')),
310
operation: vRequired(vEnum('dequeue', 'enqueue')),
311
timestamp: vRequired(vIsoTimestamp()),
312
sessionId: vRequired(vUuid()),
313
});
314
export type QueueOperationEntry = ValidatorType<typeof vQueueOperationEntry>;
315
316
/**
317
* Common fields shared between user and assistant message entries.
318
*/
319
const vCommonMessageFields = {
320
uuid: vRequired(vUuid()),
321
sessionId: vRequired(vUuid()),
322
timestamp: vRequired(vIsoTimestamp()),
323
parentUuid: vNullable(vUuid()),
324
isSidechain: vBoolean(),
325
userType: vString(),
326
cwd: vString(),
327
version: vString(),
328
gitBranch: vString(),
329
slug: vString(),
330
agentId: vString(),
331
};
332
333
/**
334
* User message entry - represents a user turn in the conversation.
335
* May contain plain text or tool results.
336
*/
337
export const vUserMessageEntry = vObj({
338
...vCommonMessageFields,
339
type: vRequired(vLiteral('user')),
340
message: vRequired(vUserMessageContent),
341
toolUseResult: vUnion(vString(), vObjAny()),
342
sourceToolAssistantUUID: vString(),
343
isCompactSummary: vBoolean(),
344
});
345
export type UserMessageEntry = ValidatorType<typeof vUserMessageEntry>;
346
347
/**
348
* Assistant message entry - represents an assistant turn in the conversation.
349
* May contain text, thinking, tool use blocks.
350
*/
351
export const vAssistantMessageEntry = vObj({
352
...vCommonMessageFields,
353
type: vRequired(vLiteral('assistant')),
354
message: vRequired(vAssistantMessageContent),
355
});
356
export type AssistantMessageEntry = ValidatorType<typeof vAssistantMessageEntry>;
357
358
/**
359
* Summary entry - provides a label for the session based on conversation.
360
* Example: { "type": "summary", "summary": "Implementing dark mode", "leafUuid": "..." }
361
*/
362
export const vSummaryEntry = vObj({
363
type: vRequired(vLiteral('summary')),
364
summary: vRequired(vString()),
365
leafUuid: vRequired(vUuid()),
366
});
367
export type SummaryEntry = ValidatorType<typeof vSummaryEntry>;
368
369
/**
370
* Custom title entry - user-assigned session name via /rename command.
371
* Example: { "type": "custom-title", "customTitle": "omega-3", "sessionId": "..." }
372
* Takes highest priority over summary and first-message labels.
373
*/
374
export const vCustomTitleEntry = vObj({
375
type: vRequired(vLiteral('custom-title')),
376
customTitle: vRequired(vString()),
377
sessionId: vRequired(vUuid()),
378
});
379
export type CustomTitleEntry = ValidatorType<typeof vCustomTitleEntry>;
380
381
/**
382
* Minimal validator for extracting chain metadata from any UUID-bearing entry.
383
* Used by the linked list parser (layer 2) to build the session chain without
384
* classifying entries into buckets. Every entry with a `uuid` becomes a ChainNode.
385
*/
386
export const vChainNodeFields = vObj({
387
uuid: vRequired(vUuid()),
388
parentUuid: vNullable(vUuid()),
389
logicalParentUuid: vNullable(vUuid()),
390
});
391
392
// #endregion
393
394
// #region Union Validators
395
396
export const vMessageEntry = vUnion(
397
vUserMessageEntry,
398
vAssistantMessageEntry
399
);
400
export type MessageEntry = ValidatorType<typeof vMessageEntry>;
401
402
// #endregion
403
404
// #region Type Guards
405
406
export type ImageMediaType = Anthropic.Messages.Base64ImageSource['media_type'];
407
408
// Record ensures a compile error if the SDK adds a new media type we haven't covered.
409
const SUPPORTED_IMAGE_MEDIA_TYPES: Record<ImageMediaType, true> = {
410
'image/jpeg': true,
411
'image/png': true,
412
'image/gif': true,
413
'image/webp': true,
414
};
415
416
function isImageMediaType(value: string): value is ImageMediaType {
417
return Object.hasOwn(SUPPORTED_IMAGE_MEDIA_TYPES, value);
418
}
419
420
/**
421
* Normalizes a MIME type string to a supported Anthropic image media type.
422
* Handles variations like 'image/jpg' → 'image/jpeg'.
423
* Returns undefined for unsupported types.
424
*/
425
export function toAnthropicImageMediaType(mimeType: string): ImageMediaType | undefined {
426
const normalized = mimeType.toLowerCase() === 'image/jpg' ? 'image/jpeg' : mimeType.toLowerCase();
427
return isImageMediaType(normalized) ? normalized : undefined;
428
}
429
430
/**
431
* Checks if a user message represents a genuine user request (not a tool result).
432
* Tool results have content that is solely tool_result blocks; genuine requests
433
* have string content or contain at least one non-tool_result block.
434
*/
435
export function isUserRequest(content: UserMessageContent['content']): boolean {
436
if (typeof content === 'string') {
437
return true;
438
}
439
if (!Array.isArray(content)) {
440
return false;
441
}
442
return content.some(block => block.type !== 'tool_result');
443
}
444
445
// #endregion
446
447
// #region Session Output Types
448
449
/**
450
* A node in the session linked list. Holds raw parsed data and chain metadata.
451
* Built by layer 2 (parseSessionFileContent) and consumed by layer 3 (buildSessions).
452
*/
453
export interface ChainNode {
454
readonly uuid: string;
455
readonly parentUuid: string | null;
456
readonly raw: Record<string, unknown>;
457
readonly lineNumber: number;
458
}
459
460
/**
461
* A stored message with revived timestamp (Date instead of string).
462
*/
463
export interface StoredMessage {
464
readonly uuid: string;
465
readonly sessionId: string;
466
readonly timestamp: Date;
467
readonly parentUuid: string | null;
468
readonly type: 'user' | 'assistant' | 'system';
469
readonly message: UserMessageContent | AssistantMessageContent | SystemMessageContent;
470
readonly isSidechain?: boolean;
471
readonly userType?: string;
472
readonly cwd?: string;
473
readonly version?: string;
474
readonly gitBranch?: string;
475
readonly slug?: string;
476
readonly agentId?: string;
477
}
478
479
/**
480
* A subagent session spawned by the main session.
481
* These are parallel task executions (e.g., Task tool agents).
482
*/
483
export interface ISubagentSession {
484
readonly agentId: string;
485
readonly parentToolUseId?: string;
486
readonly messages: readonly StoredMessage[];
487
readonly timestamp: Date;
488
}
489
490
/**
491
* A parsed Claude Code session ready for use.
492
*/
493
export interface IClaudeCodeSession extends IClaudeCodeSessionInfo {
494
readonly messages: readonly StoredMessage[];
495
readonly subagents: readonly ISubagentSession[];
496
}
497
498
/**
499
* Lightweight session metadata for listing sessions.
500
* Contains only the information needed for ChatSessionItem display.
501
* Does not include full message content to reduce memory usage.
502
*
503
* Timestamps are in milliseconds elapsed since January 1, 1970 00:00:00 UTC,
504
* matching the ChatSessionItem.timing API contract.
505
*/
506
export interface IClaudeCodeSessionInfo {
507
readonly id: string;
508
readonly label: string;
509
/** Timestamp when the session was created (first message) in ms since epoch. */
510
readonly created: number;
511
/** Timestamp when the most recent user request started in ms since epoch. */
512
readonly lastRequestStarted?: number;
513
/** Timestamp when the most recent request completed (last message) in ms since epoch. */
514
readonly lastRequestEnded?: number;
515
/** Basename of the workspace folder this session belongs to (for badge display) */
516
readonly folderName?: string;
517
/** Current working directory of the session */
518
readonly cwd?: string;
519
/** Git branch of the session */
520
readonly gitBranch?: string;
521
}
522
523
// #endregion
524
525
// #endregion
526
527