Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
97 commits
Select commit Hold shift + click to select a range
ac6dc1d
feat(core): introduce decoupled ContextManager and Sidecar architecture
joshualitt Apr 6, 2026
aa0deb0
not working
joshualitt Apr 6, 2026
fcaa6c5
broken but working through build changes
joshualitt Apr 6, 2026
6867a96
building now
joshualitt Apr 6, 2026
e601563
working in progress
joshualitt Apr 6, 2026
7c21355
working
joshualitt Apr 6, 2026
2e80fad
refactor environment
joshualitt Apr 6, 2026
d7433dd
mapper tidy
joshualitt Apr 6, 2026
c1b06fe
token counting service
joshualitt Apr 6, 2026
1774abe
token calculation service
joshualitt Apr 6, 2026
dd7190b
speculative partial fix for typed configs
joshualitt Apr 6, 2026
a4b6372
fixes
joshualitt Apr 6, 2026
cf6866c
use fakes in the tracer
joshualitt Apr 6, 2026
fc4439c
refactoring continued
joshualitt Apr 6, 2026
fbcfa40
next steps
joshualitt Apr 6, 2026
d3d6b94
next steps
joshualitt Apr 6, 2026
f423aff
testing
joshualitt Apr 7, 2026
81c8dac
next steps
joshualitt Apr 7, 2026
0dc8efb
nearly building
joshualitt Apr 7, 2026
54e901b
lints v0
joshualitt Apr 7, 2026
61dacec
lint burn down
joshualitt Apr 7, 2026
63e8b82
burndown
joshualitt Apr 7, 2026
94c5940
burndown
joshualitt Apr 7, 2026
a9cc613
next
joshualitt Apr 7, 2026
64b8a6f
thread around registry
joshualitt Apr 7, 2026
1754797
fixing
joshualitt Apr 7, 2026
370e2b9
fix disables
joshualitt Apr 7, 2026
256a7a8
broken refactor
joshualitt Apr 7, 2026
1383200
gcbackstop trigger
joshualitt Apr 7, 2026
e548cd6
tidy triggers
joshualitt Apr 7, 2026
5fad1f4
schema cleanup
joshualitt Apr 7, 2026
1c76cae
docs: formalize pure functional context ir
joshualitt Apr 7, 2026
19c3988
docs: add migration checklist for pure functional IR
joshualitt Apr 7, 2026
cd14eb4
refactor(context): transition IR to immutable Concrete/Logical split
joshualitt Apr 7, 2026
b1d62a0
refactor(context): define pure functional ContextPatch and ProcessArgs
joshualitt Apr 7, 2026
5381a5c
docs: tracking checklist progress
joshualitt Apr 7, 2026
229d570
refactor(context): update Orchestrator and toIr to support pure funct…
joshualitt Apr 7, 2026
ee0123a
refactor(context): update projector, tokenCalculator and mapper to us…
joshualitt Apr 7, 2026
0c48e5f
docs: mark Phase 5 complete
joshualitt Apr 7, 2026
1f2ca6d
docs: formalize Autonomous Buffer, Proposals, and Inbox in V2 IR blue…
joshualitt Apr 8, 2026
23fce46
docs: formalize Linear Temporal Progression model over Variant Voting
joshualitt Apr 8, 2026
60ba97e
not building
joshualitt Apr 8, 2026
84868b4
speculative broken
joshualitt Apr 8, 2026
631053b
further changes
joshualitt Apr 8, 2026
95a175d
initial worker pr, debugging
joshualitt Apr 8, 2026
57f13a1
home stretch!
joshualitt Apr 8, 2026
4202227
hmm speculative
joshualitt Apr 8, 2026
f726de1
building and tests green
joshualitt Apr 8, 2026
9287159
lints
joshualitt Apr 8, 2026
0df3521
next batch
joshualitt Apr 8, 2026
b39b74e
closer!
joshualitt Apr 8, 2026
9b9ed0c
closer!
joshualitt Apr 8, 2026
aa71d59
decentralized nodes
joshualitt Apr 8, 2026
fd5a703
check-in(broken)
joshualitt Apr 8, 2026
6e79876
checkin
joshualitt Apr 8, 2026
4a34f64
snapshotter
joshualitt Apr 9, 2026
f7b67ec
complete snapshotter
joshualitt Apr 9, 2026
775b36e
tidy names
joshualitt Apr 9, 2026
1d25931
building
joshualitt Apr 9, 2026
0179a14
further tidies
joshualitt Apr 9, 2026
28bd094
coverage
joshualitt Apr 9, 2026
8fd4396
lints step 1
joshualitt Apr 9, 2026
46c20c6
lints clean, but still need further tidies
joshualitt Apr 9, 2026
10ef9a6
last minute tidies
joshualitt Apr 9, 2026
9bd9c0f
next batch
joshualitt Apr 9, 2026
c01ed6f
further tidies
joshualitt Apr 9, 2026
8cbdbda
mock llm
joshualitt Apr 9, 2026
264fffb
format and push
joshualitt Apr 9, 2026
2f8ea41
continue tidying
joshualitt Apr 9, 2026
87ccf70
cleanup continue
joshualitt Apr 9, 2026
68e7e93
cwb complete
joshualitt Apr 9, 2026
17c9b43
format
joshualitt Apr 9, 2026
2a5b169
delete old design docs
joshualitt Apr 9, 2026
e2c3d07
refactor(context): migrate context wiring to TS Profiles and convert …
joshualitt Apr 9, 2026
0ebc369
test: restore and migrate context processor unit tests to functional …
joshualitt Apr 9, 2026
be4bee5
refactor(context): implement Named Processor Configuration architectu…
joshualitt Apr 10, 2026
a52ded7
refactor(context): unify processors and workers into sync/async pipel…
joshualitt Apr 10, 2026
ae899ac
refactor(context): completely purge legacy worker terminology and reo…
joshualitt Apr 10, 2026
abd46a8
formatting
joshualitt Apr 10, 2026
60e9bc8
further tidying
joshualitt Apr 10, 2026
1000976
cleanup docs for now
joshualitt Apr 10, 2026
d9bb3b7
test(context): inject IdGenerator into irMapper to restore snapshot d…
joshualitt Apr 10, 2026
ad4e391
refactor(context): inline lightweight summarization logic inside roll…
joshualitt Apr 10, 2026
f77aaa6
fix(context): make incremental GC purely node-based instead of token-…
joshualitt Apr 10, 2026
70b7e66
tweaks
joshualitt Apr 10, 2026
fb978e7
address feedback
joshualitt Apr 13, 2026
60ce7a2
refactor(context): remove file system and id generator mocks/interfaces
joshualitt Apr 13, 2026
1ae65bd
refactor(context): rename IR to ContextGraph / Node
joshualitt Apr 13, 2026
e10484b
applying feedback
joshualitt Apr 13, 2026
1616370
fix(context): prevent memory leaks in context caches
joshualitt Apr 13, 2026
290af4a
further tidies
joshualitt Apr 13, 2026
483758e
format
joshualitt Apr 13, 2026
e66c2b1
refactor(context): strictly type processor schemas using AJV JSONSche…
joshualitt Apr 13, 2026
bbbd215
better typing for schemas.
joshualitt Apr 13, 2026
0c9ecc7
fix gc
joshualitt Apr 13, 2026
0dde584
a bit more last minute feedback
joshualitt Apr 13, 2026
68f1347
some more tweaks
joshualitt Apr 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/core/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,7 @@ export interface ConfigParameters {
experimentalJitContext?: boolean;
autoDistillation?: boolean;
experimentalMemoryManager?: boolean;
experimentalContextManagementConfig?: string;
experimentalAgentHistoryTruncation?: boolean;
experimentalAgentHistoryTruncationThreshold?: number;
experimentalAgentHistoryRetainedMessages?: number;
Expand Down Expand Up @@ -940,6 +941,7 @@ export class Config implements McpContext, AgentLoopContext {
private readonly adminSkillsEnabled: boolean;
private readonly experimentalJitContext: boolean;
private readonly experimentalMemoryManager: boolean;
private readonly experimentalContextManagementConfig?: string;
private readonly memoryBoundaryMarkers: readonly string[];
private readonly topicUpdateNarration: boolean;
private readonly disableLLMCorrection: boolean;
Expand Down Expand Up @@ -1151,6 +1153,8 @@ export class Config implements McpContext, AgentLoopContext {

this.experimentalJitContext = params.experimentalJitContext ?? false;
this.experimentalMemoryManager = params.experimentalMemoryManager ?? false;
this.experimentalContextManagementConfig =
params.experimentalContextManagementConfig;
this.memoryBoundaryMarkers = params.memoryBoundaryMarkers ?? ['.git'];
this.contextManagement = {
enabled: params.contextManagement?.enabled ?? false,
Expand Down Expand Up @@ -2413,6 +2417,10 @@ export class Config implements McpContext, AgentLoopContext {
return this.experimentalMemoryManager;
}

getExperimentalContextManagementConfig(): string | undefined {
return this.experimentalContextManagementConfig;
}

getContextManagementConfig(): ContextManagementConfig {
return this.contextManagement;
}
Expand Down
94 changes: 94 additions & 0 deletions packages/core/src/context/config/configLoader.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { loadContextManagementConfig } from './configLoader.js';
import { defaultContextProfile } from './profiles.js';
import { ContextProcessorRegistry } from './registry.js';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import * as os from 'node:os';
import type { Config } from '../../config/config.js';
import type { JSONSchemaType } from 'ajv';

describe('SidecarLoader (Real FS)', () => {
let tmpDir: string;
let registry: ContextProcessorRegistry;
let sidecarPath: string;
let mockConfig: Config;

beforeEach(async () => {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gemini-sidecar-test-'));
sidecarPath = path.join(tmpDir, 'sidecar.json');
registry = new ContextProcessorRegistry();
registry.registerProcessor({
id: 'NodeTruncation',
schema: {
type: 'object',
properties: { maxTokens: { type: 'number' } },
required: ['maxTokens'],
} as unknown as JSONSchemaType<{ maxTokens: number }>,
});

mockConfig = {
getExperimentalContextManagementConfig: () => sidecarPath,
} as unknown as Config;
});

afterEach(async () => {
await fs.rm(tmpDir, { recursive: true, force: true });
});

it('returns default profile if file does not exist', async () => {
const result = await loadContextManagementConfig(mockConfig, registry);
expect(result).toBe(defaultContextProfile);
});

it('returns default profile if file exists but is 0 bytes', async () => {
await fs.writeFile(sidecarPath, '');
const result = await loadContextManagementConfig(mockConfig, registry);
expect(result).toBe(defaultContextProfile);
});

it('returns parsed config if file is valid', async () => {
const validConfig = {
budget: { retainedTokens: 1000, maxTokens: 2000 },
processorOptions: {
myTruncation: {
type: 'NodeTruncation',
options: { maxTokens: 500 },
},
},
};
await fs.writeFile(sidecarPath, JSON.stringify(validConfig));
const result = await loadContextManagementConfig(mockConfig, registry);
expect(result.config.budget?.maxTokens).toBe(2000);
expect(result.config.processorOptions?.['myTruncation']).toBeDefined();
});

it('throws validation error if processorOptions contains invalid data for the schema', async () => {
const invalidConfig = {
budget: { retainedTokens: 1000, maxTokens: 2000 },
processorOptions: {
myTruncation: {
type: 'NodeTruncation',
options: { maxTokens: 'this should be a number' },
},
},
};
await fs.writeFile(sidecarPath, JSON.stringify(invalidConfig));
await expect(
loadContextManagementConfig(mockConfig, registry),
).rejects.toThrow('Validation error');
});

it('throws validation error if file is empty whitespace', async () => {
await fs.writeFile(sidecarPath, ' \n ');
await expect(
loadContextManagementConfig(mockConfig, registry),
).rejects.toThrow('Unexpected end of JSON input');
});
});
90 changes: 90 additions & 0 deletions packages/core/src/context/config/configLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* @license
Comment thread
joshualitt marked this conversation as resolved.
Resolved
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import type { Config } from '../../config/config.js';
import * as fsSync from 'node:fs';
import * as fs from 'node:fs/promises';
import type { ContextManagementConfig } from './types.js';
import { defaultContextProfile, type ContextProfile } from './profiles.js';
import { SchemaValidator } from '../../utils/schemaValidator.js';
import { getContextManagementConfigSchema } from './schema.js';
import type { ContextProcessorRegistry } from './registry.js';
import { getErrorMessage } from '../../utils/errors.js';

/**
* Loads and validates a sidecar config from a specific file path.
* Throws an error if the file cannot be read, parsed, or fails schema validation.
*/
async function loadConfigFromFile(
sidecarPath: string,
registry: ContextProcessorRegistry,
): Promise<ContextProfile> {
const fileContent = await fs.readFile(sidecarPath, 'utf8');
let parsed: unknown;
try {
parsed = JSON.parse(fileContent);
} catch (error) {
throw new Error(
`Failed to parse Sidecar configuration file at ${sidecarPath}: ${getErrorMessage(
error,
)}`,
);
}

// Validate the complete structure, including deep options
const validationError = SchemaValidator.validate(
getContextManagementConfigSchema(registry),
parsed,
);

if (validationError) {
throw new Error(
`Invalid sidecar configuration in ${sidecarPath}. Validation error: ${validationError}`,
);
}

// Extract strictly what we need.
// Why this unsafe cast is acceptable:
// SchemaValidator just ran \`getSidecarConfigSchema(registry)\` against \`parsed\`.
// That function dynamically maps the \`processorOptions\` to strict JSON schema definitions,
// so we know with absolute certainty at runtime that \`parsed\` conforms to this shape.
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const validConfig = parsed as ContextManagementConfig;
return {
...defaultContextProfile,
config: {
...defaultContextProfile.config,
...(validConfig.budget ? { budget: validConfig.budget } : {}),
...(validConfig.processorOptions
? { processorOptions: validConfig.processorOptions }
: {}),
},
};
}

/**
* Generates a Sidecar JSON graph from the experimental config file path or defaults.
* If a config file is present but invalid, this will THROW to prevent silent misconfiguration.
*/
export async function loadContextManagementConfig(
config: Config,
registry: ContextProcessorRegistry,
): Promise<ContextProfile> {
const sidecarPath = config.getExperimentalContextManagementConfig();

if (sidecarPath && fsSync.existsSync(sidecarPath)) {
const size = fsSync.statSync(sidecarPath).size;
// If the file exists but is completely empty (0 bytes), it's safe to fallback.
if (size === 0) {
return defaultContextProfile;
}

// If the file has content, enforce strict validation and throw on failure.
return loadConfigFromFile(sidecarPath, registry);
}

return defaultContextProfile;
}
145 changes: 145 additions & 0 deletions packages/core/src/context/config/profiles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import type {
AsyncPipelineDef,
ContextManagementConfig,
PipelineDef,
} from './types.js';
import type { ContextEnvironment } from '../pipeline/environment.js';

// Import factories
import { createToolMaskingProcessor } from '../processors/toolMaskingProcessor.js';
import { createBlobDegradationProcessor } from '../processors/blobDegradationProcessor.js';
import { createNodeTruncationProcessor } from '../processors/nodeTruncationProcessor.js';
import { createNodeDistillationProcessor } from '../processors/nodeDistillationProcessor.js';
import { createStateSnapshotProcessor } from '../processors/stateSnapshotProcessor.js';
import { createStateSnapshotAsyncProcessor } from '../processors/stateSnapshotAsyncProcessor.js';

/**
* Helper to safely merge static default options with dynamically loaded
* JSON overrides from the SidecarConfig.
*
* Why the unsafe cast is acceptable here:
* Before the \`config\` object ever reaches this function, \`SidecarLoader.ts\`
* passes the raw JSON through \`SchemaValidator\`. The schema dynamically generates
* a \`oneOf\` map linking every \`type\` discriminator to its corresponding processor
* schema definition. By the time we access \`options\` here, its shape has been
* strictly validated against the corresponding Zod/JSONSchema definition at runtime,
* making the generic cast to \`<T>\` structurally safe.
*/
function resolveProcessorOptions<T>(
config: ContextManagementConfig | undefined,
id: string,
defaultOptions: T,
): T {
if (config?.processorOptions && config.processorOptions[id]) {
return {
...defaultOptions,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
...(config.processorOptions[id].options as T),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Open extensibility is nice but I'd honestly prefer type safety here even if that means that config schema knows about the set of features.

};
}
return defaultOptions;
}

export interface ContextProfile {
config: ContextManagementConfig;
buildPipelines: (
env: ContextEnvironment,
config?: ContextManagementConfig,
) => PipelineDef[];
buildAsyncPipelines: (
env: ContextEnvironment,
config?: ContextManagementConfig,
) => AsyncPipelineDef[];
}

/**
* The standard default context management profile.
* Optimized for safety, precision, and reliable summarization.
*/
export const defaultContextProfile: ContextProfile = {
config: {
budget: {
retainedTokens: 65000,
maxTokens: 150000,
},
},

buildPipelines: (
env: ContextEnvironment,
config?: ContextManagementConfig,
): PipelineDef[] =>
// Helper to merge default options with dynamically loaded processorOptions by ID
[
{
name: 'Immediate Sanitization',
triggers: ['new_message'],
processors: [
createToolMaskingProcessor(
'ToolMasking',
env,
resolveProcessorOptions(config, 'ToolMasking', {
stringLengthThresholdTokens: 8000,
}),
),
createBlobDegradationProcessor('BlobDegradation', env), // No options
],
},
{
name: 'Normalization',
triggers: ['retained_exceeded'],
processors: [
createNodeTruncationProcessor(
'NodeTruncation',
env,
resolveProcessorOptions(config, 'NodeTruncation', {
maxTokensPerNode: 3000,
}),
),
createNodeDistillationProcessor(
'NodeDistillation',
env,
resolveProcessorOptions(config, 'NodeDistillation', {
nodeThresholdTokens: 5000,
}),
),
],
},
{
name: 'Emergency Backstop',
triggers: ['gc_backstop'],
processors: [
createStateSnapshotProcessor(
'StateSnapshotSync',
env,
resolveProcessorOptions(config, 'StateSnapshotSync', {
target: 'max',
}),
),
],
},
],
buildAsyncPipelines: (
env: ContextEnvironment,
config?: ContextManagementConfig,
): AsyncPipelineDef[] => [
{
name: 'Async Background GC',
triggers: ['nodes_aged_out'],
processors: [
createStateSnapshotAsyncProcessor(
'StateSnapshotAsync',
env,
resolveProcessorOptions(config, 'StateSnapshotAsync', {
type: 'accumulate',
}),
),
],
},
],
};
Loading
Loading