chore(context): polish toGraph determinism and fix part type coverage

This commit is contained in:
Your Name
2026-05-13 22:03:14 +00:00
parent e2de05bc00
commit e9723cacc9
2 changed files with 38 additions and 3 deletions

View File

@@ -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();
});
});
});

View File

@@ -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,