From 4481c5afa90151172d01b7e1c8c37ae67f7f7444 Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Sun, 31 May 2026 05:01:22 -0400 Subject: [PATCH 1/4] docs: clarify state helper usage and preprocessor safety guidance --- README.md | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b880884..a6e693e 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Directive examples: - Controller APIs for step envelopes, preview/dry-run, and structural state diffs. - Decision constants for host-side checks. - Experimental preprocessor module exposed through a package subpath import. -- Fixture parity aligned with the Python 0.7.3 fixture snapshot. +- Fixture parity synced from the Python source-of-truth fixture corpus. ## Not Included Yet @@ -74,14 +74,28 @@ npm install @rlippmann/context-compiler ## Quick Start ```ts -import { createEngine, getClarifyPrompt, isClarify, isPassthrough, isUpdate } from '@rlippmann/context-compiler'; +import { + createEngine, + getClarifyPrompt, + getDecisionState, + getPolicyItems, + getPremiseValue, + isClarify, + isPassthrough, + isUpdate +} from '@rlippmann/context-compiler'; const engine = createEngine(); const decision = engine.step('set premise concise replies'); if (isUpdate(decision)) { - // Use the updated stored rules from the engine. - console.log(engine.state); + const state = getDecisionState(decision); + if (state) { + console.log({ + premise: getPremiseValue(state), + policies: getPolicyItems(state) + }); + } } else if (isClarify(decision)) { console.log(getClarifyPrompt(decision)); } else if (isPassthrough(decision)) { @@ -89,6 +103,9 @@ if (isUpdate(decision)) { } ``` +State snapshots are intentionally opaque. Prefer helpers such as +`getPremiseValue(state)` and `getPolicyItems(state)` for value reads. + ## Public API - `createEngine(init?)` -> create an engine instance. @@ -108,14 +125,18 @@ if (isUpdate(decision)) { ## Experimental Preprocessor -The optional preprocessor can recognize natural-language rule updates before they reach the engine. +The preprocessor is an optional host-side layer that can recognize some +natural-language rule updates before they reach the engine. For example: - "keep replies concise" - "don't suggest docker" - "forget that previous policy" -Preprocessor output should always be parsed/validated before passing it to the engine. +Safety guidance: +- Always validate preprocessor output before applying a directive to the engine. +- If `engine.hasPendingClarification()` is true, bypass preprocessing and pass raw input directly to `engine.step(...)`. +- Boundary behavior is conservative and false-negative-preferred: abstain rather than risk unsafe mutation. Experimental preprocessor APIs are available via package subpath: From 7785016714de77fa326031dc8451a398e197ae0f Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Sun, 31 May 2026 05:10:04 -0400 Subject: [PATCH 2/4] test: add optional dependency preflight for vercel smoke --- .../vercel_ai_sdk_structured_output/README.md | 12 ++++++++- ...l_ai_sdk_structured_output_example.test.ts | 26 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/examples/integrations/vercel_ai_sdk_structured_output/README.md b/examples/integrations/vercel_ai_sdk_structured_output/README.md index 53e4d94..8fd1577 100644 --- a/examples/integrations/vercel_ai_sdk_structured_output/README.md +++ b/examples/integrations/vercel_ai_sdk_structured_output/README.md @@ -73,8 +73,18 @@ They assert: Optional smoke test (real model call) is env-gated: +Required optional packages (from repository root): + +```bash +npm install --no-save ai @ai-sdk/openai zod +``` + +Smoke command (from repository root): + ```bash -CONTEXT_COMPILER_RUN_VERCEL_AI_SMOKE=1 OPENAI_API_KEY=... npm test +CONTEXT_COMPILER_RUN_VERCEL_AI_SMOKE=1 OPENAI_API_KEY=... npm test -- --run tests/vercel_ai_sdk_structured_output_example.test.ts ``` +`OPENAI_API_KEY` is required when `CONTEXT_COMPILER_RUN_VERCEL_AI_SMOKE=1`. + When the smoke flag is off, deterministic tests still validate host wiring. diff --git a/tests/vercel_ai_sdk_structured_output_example.test.ts b/tests/vercel_ai_sdk_structured_output_example.test.ts index 9147d8b..7ab3901 100644 --- a/tests/vercel_ai_sdk_structured_output_example.test.ts +++ b/tests/vercel_ai_sdk_structured_output_example.test.ts @@ -6,6 +6,20 @@ import { selectStructuredSchemasFromState } from '../examples/integrations/vercel_ai_sdk_structured_output/index.js'; +async function findMissingOptionalDeps(packages: string[]): Promise { + const checks = await Promise.all( + packages.map(async (pkg) => { + try { + await import(pkg); + return null; + } catch { + return pkg; + } + }) + ); + return checks.filter((pkg): pkg is string => pkg !== null); +} + describe('vercel ai sdk structured output example', () => { it('selects python_script and excludes shell_command from compiler state', () => { const engine = createEngine(); @@ -32,6 +46,18 @@ describe('vercel ai sdk structured output example', () => { const RUN_SMOKE = process.env.CONTEXT_COMPILER_RUN_VERCEL_AI_SMOKE === '1'; it.skipIf(!RUN_SMOKE)('optional smoke: can call Vercel AI SDK generateObject with selected schema', async () => { + const missingDeps = await findMissingOptionalDeps(['ai', '@ai-sdk/openai', 'zod']); + if (missingDeps.length > 0) { + throw new Error( + [ + 'Optional smoke dependencies are missing:', + ` ${missingDeps.join(', ')}`, + 'Install them from repository root with:', + ' npm install --no-save ai @ai-sdk/openai zod' + ].join('\n') + ); + } + const apiKey = process.env.OPENAI_API_KEY; if (!apiKey || apiKey.trim() === '') { throw new Error('OPENAI_API_KEY is required when CONTEXT_COMPILER_RUN_VERCEL_AI_SMOKE=1.'); From 930a023878172616422b70bd10a10a29a36bd9ac Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Sun, 31 May 2026 05:13:37 -0400 Subject: [PATCH 3/4] chore: bump version to 0.7.3 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0b4a78f..0796915 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@rlippmann/context-compiler", - "version": "0.7.2", + "version": "0.7.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@rlippmann/context-compiler", - "version": "0.7.2", + "version": "0.7.3", "license": "Apache-2.0", "devDependencies": { "typescript": "^5.9.3", diff --git a/package.json b/package.json index 60dcf80..ea89ac4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rlippmann/context-compiler", - "version": "0.7.2", + "version": "0.7.3", "description": "Store AI rules and corrections separately from chat history so they stay consistent across turns.", "keywords": [ "llm", From 37a4c0ab1f792fc92c41607e726e5f1ae6640baf Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Sun, 31 May 2026 05:21:58 -0400 Subject: [PATCH 4/4] fix: support sourceInput alias for preprocessor options --- README.md | 24 ++++++++++++++++++++++ examples/integrations/node-basic/server.ts | 2 +- src/experimental/preprocessor/index.ts | 11 +++++++--- tests/api_aliases.test.ts | 13 ++++++++++++ 4 files changed, 46 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a6e693e..0e8a7bb 100644 --- a/README.md +++ b/README.md @@ -144,4 +144,28 @@ Experimental preprocessor APIs are available via package subpath: import { preprocessHeuristic, parsePreprocessorOutput, validatePreprocessorOutput } from '@rlippmann/context-compiler/experimental/preprocessor'; ``` +### Experimental Preprocessor Quick Start + +```ts +function stepWithOptionalPreprocessor(engine: ReturnType, userInput: string) { + if (engine.hasPendingClarification()) { + return engine.step(userInput); + } + + const heuristic = preprocessHeuristic(userInput); + let engineInput = userInput; + + if (heuristic.classification === 'directive' && heuristic.output !== null) { + const parsed = parsePreprocessorOutput(heuristic.output, { sourceInput: userInput }); + if (parsed !== null) { + engineInput = parsed; + } + } + + return engine.step(engineInput); +} +``` + +The preprocessor is a convenience layer. The engine remains the authoritative source of state changes. + This module is intentionally experimental and separate from the deterministic core engine API. diff --git a/examples/integrations/node-basic/server.ts b/examples/integrations/node-basic/server.ts index a619113..be0ae33 100644 --- a/examples/integrations/node-basic/server.ts +++ b/examples/integrations/node-basic/server.ts @@ -66,7 +66,7 @@ function minimalRecentContext(history: ChatBody['history']) { function normalizeInputWithPreprocessor(input: string): string { const heuristic = preprocessHeuristic(input); if (heuristic.classification === 'directive' && heuristic.output !== null) { - const parsed = parsePreprocessorOutput(heuristic.output, { source_input: input }); + const parsed = parsePreprocessorOutput(heuristic.output, { sourceInput: input }); if (parsed !== null) { return parsed; } diff --git a/src/experimental/preprocessor/index.ts b/src/experimental/preprocessor/index.ts index cd1f956..e4b3394 100644 --- a/src/experimental/preprocessor/index.ts +++ b/src/experimental/preprocessor/index.ts @@ -18,6 +18,11 @@ export const PreprocessResult = { output: null } as const; +type PreprocessorSourceOptions = { + source_input?: string; + sourceInput?: string; +}; + const PROMPT_TOKEN_NULL_OR_VALUE = ''; const PROMPT_TOKEN_POLICY_SET = ''; @@ -303,11 +308,11 @@ function validateTextOutput(rawOutput: string): PreprocessResultType { export function validate_preprocessor_output( rawOutput: unknown, - opts?: { source_input?: string } + opts?: PreprocessorSourceOptions ): PreprocessResultType { const validated = typeof rawOutput === 'string' ? validateTextOutput(rawOutput) : validateStructuredOutput(rawOutput); - const sourceInput = opts?.source_input; + const sourceInput = opts?.sourceInput ?? opts?.source_input; if ( sourceInput != null && validated.classification === PREPROCESS_OUTCOME_DIRECTIVE && @@ -322,7 +327,7 @@ export function validate_preprocessor_output( export const validatePreprocessorOutput = validate_preprocessor_output; -export function parse_preprocessor_output(rawOutput: unknown, opts?: { source_input?: string }): string | null { +export function parse_preprocessor_output(rawOutput: unknown, opts?: PreprocessorSourceOptions): string | null { const validated = validate_preprocessor_output(rawOutput, opts); return validated.classification === PREPROCESS_OUTCOME_DIRECTIVE ? validated.output : null; } diff --git a/tests/api_aliases.test.ts b/tests/api_aliases.test.ts index 14a30a6..93969d7 100644 --- a/tests/api_aliases.test.ts +++ b/tests/api_aliases.test.ts @@ -27,4 +27,17 @@ describe('preprocessor API aliases', () => { expect(pre.preprocessHeuristic).toBe(pre.preprocess_heuristic); expect(pre.renderPrompt).toBe(pre.render_prompt); }); + + it('supports both source_input and sourceInput options with identical behavior', () => { + const rawOutput = 'use docker'; + const sourceInput = 'can you use docker?'; + + const validatedSnake = pre.validatePreprocessorOutput(rawOutput, { source_input: sourceInput }); + const validatedCamel = pre.validatePreprocessorOutput(rawOutput, { sourceInput }); + expect(validatedCamel).toEqual(validatedSnake); + + const parsedSnake = pre.parsePreprocessorOutput(rawOutput, { source_input: sourceInput }); + const parsedCamel = pre.parsePreprocessorOutput(rawOutput, { sourceInput }); + expect(parsedCamel).toEqual(parsedSnake); + }); });