Path: blob/main/extensions/copilot/src/extension/chatSessions/claude/node/sessionParser/claudeSessionSchema.ts
13406 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45/**6* Schema Validators for Claude Code Session Files7*8* This module provides type-safe validators for parsing Claude Code session JSONL files.9* It uses a composable validator pattern that ensures runtime validation matches static types.10*11* ## Session File Format12* Claude Code stores sessions in JSONL format with several entry types:13* - QueueOperationEntry: Session queue state (dequeue operations)14* - UserMessageEntry: User messages with optional tool results15* - AssistantMessageEntry: Assistant responses including tool use, thinking blocks16* - SummaryEntry: Session summaries for display labels17* - ChainNode: Generic linked list node for parent-chain resolution18*19* ## Validation Approach20* - Every JSON.parse result goes through validators before use21* - No type assertions (`as`) - all types are inferred from validators22* - Detailed error messages for debugging schema mismatches23*24* @see CLAUDE.md for complete format documentation25*/2627import type Anthropic from '@anthropic-ai/sdk';28import {29IValidator,30ValidationError,31ValidatorType,32vArray,33vBoolean,34vEnum,35vLiteral,36vNullable,37vNumber,38vObj,39vObjAny,40vRequired,41vString,42vUnchecked,43vUndefined,44vUnion,45vUnknown,46} from '../../../../../platform/configuration/common/validator';4748// Re-export validator utilities for convenience49export { IValidator, ValidationError, ValidatorType };5051// #region Primitive Validators5253/**54* Validates ISO 8601 timestamp strings (e.g., "2026-01-31T00:34:50.025Z").55* Does not validate that the date is semantically valid, only format.56*/57export function vIsoTimestamp(): IValidator<string> {58return {59validate(content: unknown) {60if (typeof content !== 'string') {61return { content: undefined, error: { message: `Expected ISO timestamp string, got ${typeof content}` } };62}63// Basic ISO 8601 format check (YYYY-MM-DDTHH:MM:SS with optional fractional seconds and Z)64const isoPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?$/;65if (!isoPattern.test(content)) {66return { content: undefined, error: { message: `Invalid ISO timestamp format: ${content}` } };67}68return { content, error: undefined };69},70toSchema() {71return { type: 'string', format: 'date-time' };72}73};74}7576/**77* Validates UUID-like strings.78* We use a lenient approach since real session data may have variations79* like agent IDs (e.g., "a139fcf") or other identifier formats.80*81* Strict UUID format: "6762c0b9-ee55-42cc-8998-180da7f37462"82* But we accept any non-empty string to handle edge cases.83*/84export function vUuid(): IValidator<string> {85return {86validate(content: unknown) {87if (typeof content !== 'string') {88return { content: undefined, error: { message: `Expected UUID string, got ${typeof content}` } };89}90if (content.length === 0) {91return { content: undefined, error: { message: 'Expected non-empty UUID string' } };92}93return { content, error: undefined };94},95toSchema() {96return { type: 'string', format: 'uuid' };97}98};99}100101// #endregion102103// #region Message Content Validators104// These validators parse session file content and output SDK-compatible types.105// Using SDK types directly ensures compile-time errors when the SDK changes.106107/**108* Compile-time assertion that validator output is assignable to SDK type.109* If SDK changes in incompatible ways, this will fail and remind us to update the validator.110* Direction: Validator -> SDK (validator output can be used as SDK type)111*/112function assertValidatorAssignable<_TValidator extends TSDKType, TSDKType>(): void { }113114/**115* Text content block in assistant messages.116* Matches Anthropic.TextBlock from the SDK.117*/118export const vTextBlock = vObj({119type: vRequired(vLiteral('text')),120text: vRequired(vString()),121citations: vNullable(vArray(vUnchecked<Anthropic.TextCitation>())),122});123assertValidatorAssignable<ValidatorType<typeof vTextBlock>, Anthropic.TextBlock>();124export type TextBlock = Anthropic.TextBlock;125126/**127* Thinking content block in assistant messages.128* Matches Anthropic.ThinkingBlock from the SDK.129*/130export const vThinkingBlock = vObj({131type: vRequired(vLiteral('thinking')),132thinking: vRequired(vString()),133signature: vRequired(vString()),134});135assertValidatorAssignable<ValidatorType<typeof vThinkingBlock>, Anthropic.ThinkingBlock>();136export type ThinkingBlock = Anthropic.ThinkingBlock;137138/**139* Tool use content block in assistant messages.140* Matches Anthropic.Beta.Messages.BetaToolUseBlock from the SDK.141*/142export const vToolUseBlock = vObj({143type: vRequired(vLiteral('tool_use')),144id: vRequired(vString()),145name: vRequired(vString()),146input: vRequired(vUnknown()),147});148assertValidatorAssignable<ValidatorType<typeof vToolUseBlock>, Anthropic.Beta.Messages.BetaToolUseBlock>();149export type ToolUseBlock = Anthropic.Beta.Messages.BetaToolUseBlock;150151/**152* Tool result content block in user messages (response to tool use).153* Matches Anthropic.ToolResultBlockParam from the SDK.154*155* Note: Content array elements use vUnchecked because:156* - Data originates from Claude's API (trusted source)157* - Full validators for each block param type would add significant maintenance cost158* - The outer structure is validated; malformed inner content surfaces at consumption time159*/160export const vToolResultBlock = vObj({161type: vRequired(vLiteral('tool_result')),162tool_use_id: vRequired(vString()),163content: vUnion(164vString(),165vArray(vUnchecked<Anthropic.TextBlockParam | Anthropic.ImageBlockParam | Anthropic.SearchResultBlockParam | Anthropic.DocumentBlockParam>()),166vUndefined()167),168is_error: vBoolean(),169});170assertValidatorAssignable<ValidatorType<typeof vToolResultBlock>, Anthropic.ToolResultBlockParam>();171export type ToolResultBlock = Anthropic.ToolResultBlockParam;172173/**174* Base64 image source with inline data.175* Matches Anthropic.Base64ImageSource from the SDK.176*/177const vBase64ImageSource = vObj({178type: vRequired(vLiteral('base64')),179media_type: vRequired(vEnum('image/jpeg', 'image/png', 'image/gif', 'image/webp')),180data: vRequired(vString()),181});182183/**184* URL image source with a remote URL.185* Matches Anthropic.URLImageSource from the SDK.186*/187const vURLImageSource = vObj({188type: vRequired(vLiteral('url')),189url: vRequired(vString()),190});191192/**193* Image content block in user messages.194* Matches Anthropic.ImageBlockParam from the SDK.195*196* Source is validated as a discriminated union of base64 and url shapes,197* ensuring required fields (type, media_type/data or url) are present.198*/199export const vImageBlock = vObj({200type: vRequired(vLiteral('image')),201source: vRequired(vUnion(vBase64ImageSource, vURLImageSource)),202});203assertValidatorAssignable<ValidatorType<typeof vImageBlock>, Anthropic.ImageBlockParam>();204export type ImageBlock = Anthropic.ImageBlockParam;205206/**207* Unknown content block type for forward compatibility.208* Allows parsing of new block types the SDK may introduce.209*/210export const vUnknownContentBlock = vObj({211type: vRequired(vString()),212});213export type UnknownContentBlock = { type: string };214215/**216* Union of all known content block types.217* For assistant messages, use ContentBlock (excludes ToolResultBlock).218* For user messages, content may also include ToolResultBlock.219*/220export const vContentBlock = vUnion(221vTextBlock,222vThinkingBlock,223vToolUseBlock,224vToolResultBlock,225vImageBlock,226vUnknownContentBlock227);228export type ContentBlock = TextBlock | ThinkingBlock | ToolUseBlock | ToolResultBlock | ImageBlock | UnknownContentBlock;229230// #endregion231232// #region Message Usage Validators233234/**235* Cache creation details for token usage.236*/237export const vCacheCreation = vObj({238ephemeral_1h_input_tokens: vNumber(),239ephemeral_5m_input_tokens: vNumber(),240});241export type CacheCreation = ValidatorType<typeof vCacheCreation>;242243/**244* Token usage information for API calls.245*/246export const vUsage = vObj({247cache_creation: vNullable(vCacheCreation),248cache_creation_input_tokens: vNumber(),249cache_read_input_tokens: vNumber(),250input_tokens: vNumber(),251output_tokens: vNumber(),252});253export type Usage = ValidatorType<typeof vUsage>;254255// #endregion256257// #region Role-Specific Message Validators258259/**260* User message content (from Anthropic SDK MessageParam type).261*/262export const vUserMessageContent = vObj({263role: vRequired(vLiteral('user')),264content: vRequired(vUnion(vString(), vArray(vContentBlock))),265});266export type UserMessageContent = ValidatorType<typeof vUserMessageContent>;267268/**269* Assistant message content (from Anthropic SDK BetaMessage type).270*/271export const vAssistantMessageContent = vObj({272role: vRequired(vLiteral('assistant')),273content: vRequired(vArray(vContentBlock)),274id: vString(),275model: vString(),276type: vString(),277stop_reason: vNullable(vString()),278stop_sequence: vNullable(vString()),279usage: vUsage,280parent_tool_use_id: vNullable(vString()),281});282export type AssistantMessageContent = ValidatorType<typeof vAssistantMessageContent>;283284/**285* System message content — a simple text entry produced by the runtime286* (e.g., "Conversation compacted" from a compact boundary).287*/288interface SystemMessageContent {289readonly role: 'system';290readonly content: string;291}292293/**294* Model ID used by the SDK for synthetic messages (e.g., "No response requested." from abort).295* These messages should be filtered out from display and processing.296*/297export const SYNTHETIC_MODEL_ID = '<synthetic>';298299// #endregion300301// #region Session Entry Validators302303/**304* Queue operation entry - represents session queue state changes.305* Example: { "type": "queue-operation", "operation": "dequeue", "timestamp": "...", "sessionId": "..." }306*/307export const vQueueOperationEntry = vObj({308type: vRequired(vLiteral('queue-operation')),309operation: vRequired(vEnum('dequeue', 'enqueue')),310timestamp: vRequired(vIsoTimestamp()),311sessionId: vRequired(vUuid()),312});313export type QueueOperationEntry = ValidatorType<typeof vQueueOperationEntry>;314315/**316* Common fields shared between user and assistant message entries.317*/318const vCommonMessageFields = {319uuid: vRequired(vUuid()),320sessionId: vRequired(vUuid()),321timestamp: vRequired(vIsoTimestamp()),322parentUuid: vNullable(vUuid()),323isSidechain: vBoolean(),324userType: vString(),325cwd: vString(),326version: vString(),327gitBranch: vString(),328slug: vString(),329agentId: vString(),330};331332/**333* User message entry - represents a user turn in the conversation.334* May contain plain text or tool results.335*/336export const vUserMessageEntry = vObj({337...vCommonMessageFields,338type: vRequired(vLiteral('user')),339message: vRequired(vUserMessageContent),340toolUseResult: vUnion(vString(), vObjAny()),341sourceToolAssistantUUID: vString(),342isCompactSummary: vBoolean(),343});344export type UserMessageEntry = ValidatorType<typeof vUserMessageEntry>;345346/**347* Assistant message entry - represents an assistant turn in the conversation.348* May contain text, thinking, tool use blocks.349*/350export const vAssistantMessageEntry = vObj({351...vCommonMessageFields,352type: vRequired(vLiteral('assistant')),353message: vRequired(vAssistantMessageContent),354});355export type AssistantMessageEntry = ValidatorType<typeof vAssistantMessageEntry>;356357/**358* Summary entry - provides a label for the session based on conversation.359* Example: { "type": "summary", "summary": "Implementing dark mode", "leafUuid": "..." }360*/361export const vSummaryEntry = vObj({362type: vRequired(vLiteral('summary')),363summary: vRequired(vString()),364leafUuid: vRequired(vUuid()),365});366export type SummaryEntry = ValidatorType<typeof vSummaryEntry>;367368/**369* Custom title entry - user-assigned session name via /rename command.370* Example: { "type": "custom-title", "customTitle": "omega-3", "sessionId": "..." }371* Takes highest priority over summary and first-message labels.372*/373export const vCustomTitleEntry = vObj({374type: vRequired(vLiteral('custom-title')),375customTitle: vRequired(vString()),376sessionId: vRequired(vUuid()),377});378export type CustomTitleEntry = ValidatorType<typeof vCustomTitleEntry>;379380/**381* Minimal validator for extracting chain metadata from any UUID-bearing entry.382* Used by the linked list parser (layer 2) to build the session chain without383* classifying entries into buckets. Every entry with a `uuid` becomes a ChainNode.384*/385export const vChainNodeFields = vObj({386uuid: vRequired(vUuid()),387parentUuid: vNullable(vUuid()),388logicalParentUuid: vNullable(vUuid()),389});390391// #endregion392393// #region Union Validators394395export const vMessageEntry = vUnion(396vUserMessageEntry,397vAssistantMessageEntry398);399export type MessageEntry = ValidatorType<typeof vMessageEntry>;400401// #endregion402403// #region Type Guards404405export type ImageMediaType = Anthropic.Messages.Base64ImageSource['media_type'];406407// Record ensures a compile error if the SDK adds a new media type we haven't covered.408const SUPPORTED_IMAGE_MEDIA_TYPES: Record<ImageMediaType, true> = {409'image/jpeg': true,410'image/png': true,411'image/gif': true,412'image/webp': true,413};414415function isImageMediaType(value: string): value is ImageMediaType {416return Object.hasOwn(SUPPORTED_IMAGE_MEDIA_TYPES, value);417}418419/**420* Normalizes a MIME type string to a supported Anthropic image media type.421* Handles variations like 'image/jpg' → 'image/jpeg'.422* Returns undefined for unsupported types.423*/424export function toAnthropicImageMediaType(mimeType: string): ImageMediaType | undefined {425const normalized = mimeType.toLowerCase() === 'image/jpg' ? 'image/jpeg' : mimeType.toLowerCase();426return isImageMediaType(normalized) ? normalized : undefined;427}428429/**430* Checks if a user message represents a genuine user request (not a tool result).431* Tool results have content that is solely tool_result blocks; genuine requests432* have string content or contain at least one non-tool_result block.433*/434export function isUserRequest(content: UserMessageContent['content']): boolean {435if (typeof content === 'string') {436return true;437}438if (!Array.isArray(content)) {439return false;440}441return content.some(block => block.type !== 'tool_result');442}443444// #endregion445446// #region Session Output Types447448/**449* A node in the session linked list. Holds raw parsed data and chain metadata.450* Built by layer 2 (parseSessionFileContent) and consumed by layer 3 (buildSessions).451*/452export interface ChainNode {453readonly uuid: string;454readonly parentUuid: string | null;455readonly raw: Record<string, unknown>;456readonly lineNumber: number;457}458459/**460* A stored message with revived timestamp (Date instead of string).461*/462export interface StoredMessage {463readonly uuid: string;464readonly sessionId: string;465readonly timestamp: Date;466readonly parentUuid: string | null;467readonly type: 'user' | 'assistant' | 'system';468readonly message: UserMessageContent | AssistantMessageContent | SystemMessageContent;469readonly isSidechain?: boolean;470readonly userType?: string;471readonly cwd?: string;472readonly version?: string;473readonly gitBranch?: string;474readonly slug?: string;475readonly agentId?: string;476}477478/**479* A subagent session spawned by the main session.480* These are parallel task executions (e.g., Task tool agents).481*/482export interface ISubagentSession {483readonly agentId: string;484readonly parentToolUseId?: string;485readonly messages: readonly StoredMessage[];486readonly timestamp: Date;487}488489/**490* A parsed Claude Code session ready for use.491*/492export interface IClaudeCodeSession extends IClaudeCodeSessionInfo {493readonly messages: readonly StoredMessage[];494readonly subagents: readonly ISubagentSession[];495}496497/**498* Lightweight session metadata for listing sessions.499* Contains only the information needed for ChatSessionItem display.500* Does not include full message content to reduce memory usage.501*502* Timestamps are in milliseconds elapsed since January 1, 1970 00:00:00 UTC,503* matching the ChatSessionItem.timing API contract.504*/505export interface IClaudeCodeSessionInfo {506readonly id: string;507readonly label: string;508/** Timestamp when the session was created (first message) in ms since epoch. */509readonly created: number;510/** Timestamp when the most recent user request started in ms since epoch. */511readonly lastRequestStarted?: number;512/** Timestamp when the most recent request completed (last message) in ms since epoch. */513readonly lastRequestEnded?: number;514/** Basename of the workspace folder this session belongs to (for badge display) */515readonly folderName?: string;516/** Current working directory of the session */517readonly cwd?: string;518/** Git branch of the session */519readonly gitBranch?: string;520}521522// #endregion523524// #endregion525526527