Download packages/core/src/context/graph/toGraph.ts from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 8.29 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/core/src/context/graph/toGraph.ts
- Command line
-
hf download hf://SaylorTwift/gemini-cli/packages/core/src/context/graph/toGraph.ts
-
curl -L -o toGraph.ts https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/core/src/context/graph/toGraph.ts
8.29 kB
| /** | |
| * @license | |
| * Copyright 2026 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import type { Part } from '@google/genai'; | |
| import { type ConcreteNode, NodeType } from './types.js'; | |
| import { createHash } from 'node:crypto'; | |
| import { debugLogger } from '../../utils/debugLogger.js'; | |
| import type { NodeIdService } from './nodeIdService.js'; | |
| import type { HistoryTurn } from '../../core/agentChatHistory.js'; | |
| import { isSnapshotState } from '../utils/snapshotGenerator.js'; | |
| import { deriveStableId } from '../../utils/cryptoUtils.js'; | |
| import { ensureStableToolIds } from '../../utils/sessionUtils.js'; | |
| // Global WeakMap to cache hashes for Part objects. | |
| // This optimizes getStableId by avoiding redundant stringify/hash operations | |
| // on the same object instances across multiple management passes. | |
| const PART_HASH_CACHE = new WeakMap<object, string>(); | |
| function isTextPart(part: Part): part is Part & { text: string } { | |
| return typeof part.text === 'string'; | |
| } | |
| function isInlineDataPart( | |
| part: Part, | |
| ): part is Part & { inlineData: { data: string } } { | |
| return ( | |
| typeof part.inlineData === 'object' && | |
| part.inlineData !== null && | |
| typeof part.inlineData.data === 'string' | |
| ); | |
| } | |
| function isFileDataPart( | |
| part: Part, | |
| ): part is Part & { fileData: { fileUri: string } } { | |
| return ( | |
| typeof part.fileData === 'object' && | |
| part.fileData !== null && | |
| typeof part.fileData.fileUri === 'string' | |
| ); | |
| } | |
| function isFunctionCallPart(part: Part): part is Part & { | |
| functionCall: { id?: string; name: string; args: Record<string, unknown> }; | |
| } { | |
| return ( | |
| typeof part.functionCall === 'object' && | |
| part.functionCall !== null && | |
| typeof part.functionCall.name === 'string' | |
| ); | |
| } | |
| function isFunctionResponsePart(part: Part): part is Part & { | |
| functionResponse: { | |
| id?: string; | |
| name: string; | |
| response: Record<string, unknown>; | |
| }; | |
| } { | |
| return ( | |
| typeof part.functionResponse === 'object' && | |
| part.functionResponse !== null && | |
| typeof part.functionResponse.name === 'string' | |
| ); | |
| } | |
| function isExecutableCodePart( | |
| part: Part, | |
| ): part is Part & { executableCode: { code: string; language: string } } { | |
| return ( | |
| typeof part.executableCode === 'object' && | |
| part.executableCode !== null && | |
| typeof part.executableCode.code === 'string' && | |
| typeof part.executableCode.language === 'string' | |
| ); | |
| } | |
| function isCodeExecutionResultPart( | |
| part: Part, | |
| ): part is Part & { codeExecutionResult: { outcome: string; output: string } } { | |
| return ( | |
| typeof part.codeExecutionResult === 'object' && | |
| part.codeExecutionResult !== null && | |
| typeof part.codeExecutionResult.output === 'string' && | |
| typeof part.codeExecutionResult.outcome === 'string' | |
| ); | |
| } | |
| /** | |
| * Generates a stable ID for an object reference using a NodeIdService. | |
| * Falls back to content-based hashing for Part-like objects to ensure | |
| * stability across object re-creations (e.g. during history mapping). | |
| */ | |
| export function getStableId( | |
| obj: object, | |
| idService: NodeIdService, | |
| turnSalt: string = '', | |
| partIdx: number = 0, | |
| ): string { | |
| let id = idService.get(obj); | |
| if (id) return id; | |
| const cachedHash = PART_HASH_CACHE.get(obj); | |
| if (cachedHash) { | |
| id = `${cachedHash}_${turnSalt}_${partIdx}`; | |
| idService.set(obj, id); | |
| return id; | |
| } | |
| const part = obj as Part; | |
| let contentHash: string | undefined; | |
| if (isTextPart(part)) { | |
| contentHash = createHash('sha256').update(part.text).digest('hex'); | |
| id = `text_${contentHash}_${turnSalt}_${partIdx}`; | |
| } else if (isInlineDataPart(part)) { | |
| contentHash = createHash('sha256') | |
| .update(part.inlineData.data) | |
| .digest('hex'); | |
| id = `media_${contentHash}_${turnSalt}_${partIdx}`; | |
| } else if (isFileDataPart(part)) { | |
| contentHash = createHash('sha256') | |
| .update(part.fileData.fileUri) | |
| .digest('hex'); | |
| id = `file_${contentHash}_${turnSalt}_${partIdx}`; | |
| } else if (isFunctionCallPart(part)) { | |
| if (part.functionCall.id) { | |
| id = `call_${part.functionCall.id}`; | |
| } else { | |
| contentHash = createHash('sha256') | |
| .update( | |
| `call:${part.functionCall.name}:${JSON.stringify(part.functionCall.args)}`, | |
| ) | |
| .digest('hex'); | |
| id = `call_h_${contentHash}_${turnSalt}_${partIdx}`; | |
| } | |
| } else if (isFunctionResponsePart(part)) { | |
| if (part.functionResponse.id) { | |
| id = `resp_${part.functionResponse.id}`; | |
| } else { | |
| contentHash = createHash('sha256') | |
| .update( | |
| `resp:${part.functionResponse.name}:${JSON.stringify(part.functionResponse.response)}`, | |
| ) | |
| .digest('hex'); | |
| id = `resp_h_${contentHash}_${turnSalt}_${partIdx}`; | |
| } | |
| } else if (isExecutableCodePart(part)) { | |
| contentHash = createHash('sha256') | |
| .update( | |
| `exec:${part.executableCode.language}:${part.executableCode.code}`, | |
| ) | |
| .digest('hex'); | |
| id = `exec_${contentHash}_${turnSalt}_${partIdx}`; | |
| } else if (isCodeExecutionResultPart(part)) { | |
| contentHash = createHash('sha256') | |
| .update( | |
| `result:${part.codeExecutionResult.outcome}:${part.codeExecutionResult.output}`, | |
| ) | |
| .digest('hex'); | |
| id = `result_${contentHash}_${turnSalt}_${partIdx}`; | |
| } | |
| if (contentHash) { | |
| PART_HASH_CACHE.set(obj, contentHash); | |
| } | |
| if (!id) { | |
| if (turnSalt && partIdx === -1) { | |
| id = `turn_${turnSalt}`; | |
| } else { | |
| id = `${turnSalt}_f_${partIdx}`; | |
| } | |
| } | |
| idService.set(obj, id); | |
| return id; | |
| } | |
| /** | |
| * Builds a 1:1 Mirror Graph from Chat History. | |
| * Every Part in history is mapped to exactly one ConcreteNode. | |
| */ | |
| export class ContextGraphBuilder { | |
| constructor(private readonly idService: NodeIdService) {} | |
| processHistory(history: readonly HistoryTurn[]): ConcreteNode[] { | |
| // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion | |
| ensureStableToolIds(history as HistoryTurn[]); | |
| const nodes: ConcreteNode[] = []; | |
| for (let turnIdx = 0; turnIdx < history.length; turnIdx++) { | |
| const turn = history[turnIdx]; | |
| const msg = turn.content; | |
| if (!msg.parts) continue; | |
| const hasEnvHeader = msg.parts?.some( | |
| (p) => isTextPart(p) && p.text.trim().startsWith('<session_context>'), | |
| ); | |
| const turnSalt = | |
| hasEnvHeader && turnIdx === 0 | |
| ? deriveStableId(['environment-context']) | |
| : turn.id; | |
| const turnId = turnSalt.startsWith('turn_') | |
| ? turnSalt | |
| : `turn_${turnSalt}`; | |
| if (msg.role === 'user') { | |
| for (let partIdx = 0; partIdx < msg.parts.length; partIdx++) { | |
| const part = msg.parts[partIdx]; | |
| // Skip legacy session context headers if they appear later in history (after Turn 0). | |
| if ( | |
| isTextPart(part) && | |
| part.text.trim().startsWith('<session_context>') && | |
| turnIdx > 0 | |
| ) { | |
| debugLogger.log( | |
| '[ContextGraphBuilder] Skipping legacy environment header turn from graph.', | |
| ); | |
| continue; | |
| } | |
| const isSnapshot = isTextPart(part) && isSnapshotState(part.text); | |
| const id = getStableId(part, this.idService, turnSalt, partIdx); | |
| const node: ConcreteNode = { | |
| id, | |
| timestamp: Date.now(), | |
| type: isFunctionResponsePart(part) | |
| ? NodeType.TOOL_EXECUTION | |
| : isSnapshot | |
| ? NodeType.SNAPSHOT | |
| : NodeType.USER_PROMPT, | |
| role: 'user', | |
| payload: part, | |
| turnId, | |
| }; | |
| nodes.push(node); | |
| } | |
| } else if (msg.role === 'model') { | |
| for (let partIdx = 0; partIdx < msg.parts.length; partIdx++) { | |
| const part = msg.parts[partIdx]; | |
| const id = getStableId(part, this.idService, turnSalt, partIdx); | |
| const node: ConcreteNode = { | |
| id, | |
| timestamp: Date.now(), | |
| type: isFunctionCallPart(part) | |
| ? NodeType.TOOL_EXECUTION | |
| : NodeType.AGENT_THOUGHT, | |
| role: 'model', | |
| payload: part, | |
| turnId, | |
| }; | |
| nodes.push(node); | |
| } | |
| } | |
| } | |
| debugLogger.log( | |
| `[ContextGraphBuilder] Mirror Graph built with ${nodes.length} nodes.`, | |
| ); | |
| return nodes; | |
| } | |
| } | |