Repository navigation
feat(core): introduce decoupled ContextManager and Sidecar architecture #24752
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
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 aa0deb0
not working
joshualitt fcaa6c5
broken but working through build changes
joshualitt 6867a96
building now
joshualitt e601563
working in progress
joshualitt 7c21355
working
joshualitt 2e80fad
refactor environment
joshualitt d7433dd
mapper tidy
joshualitt c1b06fe
token counting service
joshualitt 1774abe
token calculation service
joshualitt dd7190b
speculative partial fix for typed configs
joshualitt a4b6372
fixes
joshualitt cf6866c
use fakes in the tracer
joshualitt fc4439c
refactoring continued
joshualitt fbcfa40
next steps
joshualitt d3d6b94
next steps
joshualitt f423aff
testing
joshualitt 81c8dac
next steps
joshualitt 0dc8efb
nearly building
joshualitt 54e901b
lints v0
joshualitt 61dacec
lint burn down
joshualitt 63e8b82
burndown
joshualitt 94c5940
burndown
joshualitt a9cc613
next
joshualitt 64b8a6f
thread around registry
joshualitt 1754797
fixing
joshualitt 370e2b9
fix disables
joshualitt 256a7a8
broken refactor
joshualitt 1383200
gcbackstop trigger
joshualitt e548cd6
tidy triggers
joshualitt 5fad1f4
schema cleanup
joshualitt 1c76cae
docs: formalize pure functional context ir
joshualitt 19c3988
docs: add migration checklist for pure functional IR
joshualitt cd14eb4
refactor(context): transition IR to immutable Concrete/Logical split
joshualitt b1d62a0
refactor(context): define pure functional ContextPatch and ProcessArgs
joshualitt 5381a5c
docs: tracking checklist progress
joshualitt 229d570
refactor(context): update Orchestrator and toIr to support pure funct…
joshualitt ee0123a
refactor(context): update projector, tokenCalculator and mapper to us…
joshualitt 0c48e5f
docs: mark Phase 5 complete
joshualitt 1f2ca6d
docs: formalize Autonomous Buffer, Proposals, and Inbox in V2 IR blue…
joshualitt 23fce46
docs: formalize Linear Temporal Progression model over Variant Voting
joshualitt 60ba97e
not building
joshualitt 84868b4
speculative broken
joshualitt 631053b
further changes
joshualitt 95a175d
initial worker pr, debugging
joshualitt 57f13a1
home stretch!
joshualitt 4202227
hmm speculative
joshualitt f726de1
building and tests green
joshualitt 9287159
lints
joshualitt 0df3521
next batch
joshualitt b39b74e
closer!
joshualitt 9b9ed0c
closer!
joshualitt aa71d59
decentralized nodes
joshualitt fd5a703
check-in(broken)
joshualitt 6e79876
checkin
joshualitt 4a34f64
snapshotter
joshualitt f7b67ec
complete snapshotter
joshualitt 775b36e
tidy names
joshualitt 1d25931
building
joshualitt 0179a14
further tidies
joshualitt 28bd094
coverage
joshualitt 8fd4396
lints step 1
joshualitt 46c20c6
lints clean, but still need further tidies
joshualitt 10ef9a6
last minute tidies
joshualitt 9bd9c0f
next batch
joshualitt c01ed6f
further tidies
joshualitt 8cbdbda
mock llm
joshualitt 264fffb
format and push
joshualitt 2f8ea41
continue tidying
joshualitt 87ccf70
cleanup continue
joshualitt 68e7e93
cwb complete
joshualitt 17c9b43
format
joshualitt 2a5b169
delete old design docs
joshualitt e2c3d07
refactor(context): migrate context wiring to TS Profiles and convert …
joshualitt 0ebc369
test: restore and migrate context processor unit tests to functional …
joshualitt be4bee5
refactor(context): implement Named Processor Configuration architectu…
joshualitt a52ded7
refactor(context): unify processors and workers into sync/async pipel…
joshualitt ae899ac
refactor(context): completely purge legacy worker terminology and reo…
joshualitt abd46a8
formatting
joshualitt 60e9bc8
further tidying
joshualitt 1000976
cleanup docs for now
joshualitt d9bb3b7
test(context): inject IdGenerator into irMapper to restore snapshot d…
joshualitt ad4e391
refactor(context): inline lightweight summarization logic inside roll…
joshualitt f77aaa6
fix(context): make incremental GC purely node-based instead of token-…
joshualitt 70b7e66
tweaks
joshualitt fb978e7
address feedback
joshualitt 60ce7a2
refactor(context): remove file system and id generator mocks/interfaces
joshualitt 1ae65bd
refactor(context): rename IR to ContextGraph / Node
joshualitt e10484b
applying feedback
joshualitt 1616370
fix(context): prevent memory leaks in context caches
joshualitt 290af4a
further tidies
joshualitt 483758e
format
joshualitt e66c2b1
refactor(context): strictly type processor schemas using AJV JSONSche…
joshualitt bbbd215
better typing for schemas.
joshualitt 0c9ecc7
fix gc
joshualitt 0dde584
a bit more last minute feedback
joshualitt 68f1347
some more tweaks
joshualitt File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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'); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| /** | ||
| * @license | ||
| * 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; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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), | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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', | ||
| }), | ||
| ), | ||
| ], | ||
| }, | ||
| ], | ||
| }; | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.