diff --git a/packages/core/src/context/graph/toGraph.test.ts b/packages/core/src/context/graph/toGraph.test.ts index 5f16e0cdf4..9f221339bf 100644 --- a/packages/core/src/context/graph/toGraph.test.ts +++ b/packages/core/src/context/graph/toGraph.test.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, vi } from 'vitest'; import { ContextGraphBuilder } from './toGraph.js'; import type { Content } from '@google/genai'; import type { BaseConcreteNode } from './types.js'; @@ -38,6 +38,8 @@ describe('ContextGraphBuilder', () => { }); it('should generate completely deterministic graph structure and UUIDs across JSON serialization cycles', () => { + vi.spyOn(Date, 'now').mockReturnValue(0); + const complexHistory: Content[] = [ { role: 'user', parts: [{ text: 'Step 1: complex analysis' }] }, { @@ -86,11 +88,14 @@ describe('ContextGraphBuilder', () => { nodes1.forEach((node, index) => { expect(node.id).toBeDefined(); expect(node.id).toBe(nodes2[index].id); + expect(node.timestamp).toBe(0); if ('turnId' in node) { expect(node.turnId).toBeDefined(); expect(node.turnId).toBe((nodes2[index] as BaseConcreteNode).turnId); } }); + + vi.restoreAllMocks(); }); }); }); diff --git a/packages/core/src/context/graph/toGraph.ts b/packages/core/src/context/graph/toGraph.ts index f47aa8c7ee..8bf9459789 100644 --- a/packages/core/src/context/graph/toGraph.ts +++ b/packages/core/src/context/graph/toGraph.ts @@ -62,6 +62,26 @@ function isFunctionResponsePart( ); } +function isExecutableCodePart( + part: Part, +): part is Part & { executableCode: { code: string } } { + return ( + typeof part.executableCode === 'object' && + part.executableCode !== null && + typeof part.executableCode.code === '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' + ); +} + /** * Generates a stable ID for an object reference using a WeakMap. * Falls back to content-based hashing for Part-like objects to ensure @@ -116,6 +136,16 @@ export function getStableId( ) .digest('hex'); id = `resp_h_${contentHash}_${turnSalt}_${partIdx}`; + } else if (isExecutableCodePart(part)) { + contentHash = createHash('sha256') + .update(`exec:${part.executableCode.code}`) + .digest('hex'); + id = `exec_${contentHash}_${turnSalt}_${partIdx}`; + } else if (isCodeExecutionResultPart(part)) { + contentHash = createHash('sha256') + .update(`result:${part.codeExecutionResult.output}`) + .digest('hex'); + id = `result_${contentHash}_${turnSalt}_${partIdx}`; } if (contentHash) { @@ -194,7 +224,7 @@ export class ContextGraphBuilder { apiId || getStableId(part, this.nodeIdentityMap, turnSalt, partIdx); const node: ConcreteNode = { id, - timestamp: 0, // Using 0 for deterministic structural equality. Actual time is applied by orchestrator. + timestamp: Date.now(), type: isFunctionResponsePart(part) ? NodeType.TOOL_EXECUTION : NodeType.USER_PROMPT, @@ -215,7 +245,7 @@ export class ContextGraphBuilder { apiId || getStableId(part, this.nodeIdentityMap, turnSalt, partIdx); const node: ConcreteNode = { id, - timestamp: 0, // Using 0 for deterministic structural equality. Actual time is applied by orchestrator. + timestamp: Date.now(), type: isFunctionCallPart(part) ? NodeType.TOOL_EXECUTION : NodeType.AGENT_THOUGHT,