Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
57 changes: 51 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -74,21 +74,38 @@ 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)) {
// passthrough
}
```

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.
Expand All @@ -108,19 +125,47 @@ 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:

```ts
import { preprocessHeuristic, parsePreprocessorOutput, validatePreprocessorOutput } from '@rlippmann/context-compiler/experimental/preprocessor';
```

### Experimental Preprocessor Quick Start

```ts
function stepWithOptionalPreprocessor(engine: ReturnType<typeof createEngine>, 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.
2 changes: 1 addition & 1 deletion examples/integrations/node-basic/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
12 changes: 11 additions & 1 deletion examples/integrations/vercel_ai_sdk_structured_output/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
11 changes: 8 additions & 3 deletions src/experimental/preprocessor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ export const PreprocessResult = {
output: null
} as const;

type PreprocessorSourceOptions = {
source_input?: string;
sourceInput?: string;
};

const PROMPT_TOKEN_NULL_OR_VALUE = '<NULL_OR_VALUE>';
const PROMPT_TOKEN_POLICY_SET = '<SET OF CURRENT POLICY ITEMS>';

Expand Down Expand Up @@ -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 &&
Expand All @@ -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;
}
Expand Down
13 changes: 13 additions & 0 deletions tests/api_aliases.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
26 changes: 26 additions & 0 deletions tests/vercel_ai_sdk_structured_output_example.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@ import {
selectStructuredSchemasFromState
} from '../examples/integrations/vercel_ai_sdk_structured_output/index.js';

async function findMissingOptionalDeps(packages: string[]): Promise<string[]> {
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();
Expand All @@ -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.');
Expand Down
Loading