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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
env:
PY_FIXTURE_REF: 6cedcd955fcf4b4e687ea85771ebbc5302e5fe01
PY_FIXTURE_REF: caf37689ee6c1c99ddf0067f6486daf6cc9bb0ed
PY_FIXTURE_CHECKOUT: /tmp/context-compiler-source

steps:
Expand Down
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,14 @@ State snapshots are intentionally opaque. Prefer helpers such as
- `getPremiseValue(state)` / `getPolicyItems(state, value?)` -> read helpers for state.
- `step(engine, input)` -> controller step envelope (`output_version`, `mode`, `decision`, `state`).
- `preview(engine, input)` -> dry-run step envelope with `state_before`, `state_after`, `diff`, and `would_mutate` (live engine state is restored).
- `getStepDecision(stepResult)` / `getStepState(stepResult)` -> read helpers for controller step results.
- `getPreviewDecision(previewResult)` / `getPreviewStateAfter(previewResult)` / `previewWouldMutate(previewResult)` -> read helpers for controller preview results.
- `diffHasChanges(diff)` -> read helper for the structural diff `changed` flag.
- `stateDiff(before, after)` -> structural state diff used by preview.
- `DECISION_PASSTHROUGH` / `DECISION_UPDATE` / `DECISION_CLARIFY` -> decision kind constants.

Prefer the controller helper accessors over direct controller result property reads in TypeScript examples and app code.

## Experimental Preprocessor

The preprocessor is an optional host-side layer that can recognize some
Expand All @@ -141,7 +146,12 @@ Safety guidance:
Experimental preprocessor APIs are available via package subpath:

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

### Experimental Preprocessor Quick Start
Expand All @@ -155,7 +165,7 @@ function stepWithOptionalPreprocessor(engine: ReturnType<typeof createEngine>, u
const heuristic = preprocessHeuristic(userInput);
let engineInput = userInput;

if (heuristic.classification === 'directive' && heuristic.output !== null) {
if (heuristic.classification === PREPROCESS_OUTCOME_DIRECTIVE && heuristic.output !== null) {
const parsed = parsePreprocessorOutput(heuristic.output, { sourceInput: userInput });
if (parsed !== null) {
engineInput = parsed;
Expand Down
4 changes: 2 additions & 2 deletions demos/04_llm_tool_denylist_guardrail.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createEngine, getPolicyItems } from '../src/index.js';
import { POLICY_PROHIBIT, createEngine, getPolicyItems } from '../src/index.js';
import {
buildBaselineMessages,
buildMediatedMessagesFromTranscript,
Expand Down Expand Up @@ -68,7 +68,7 @@ export async function main(): Promise<void> {
const baselineOutput = await completeMessages(baselineMessages);
printModelOutput('Baseline', baselineOutput);

const prohibited = getPolicyItems(engine.state, 'prohibit');
const prohibited = getPolicyItems(engine.state, POLICY_PROHIBIT);
const candidateTools = ['docker', 'kubectl'];
const filteredTools = candidateTools.filter((tool) => !prohibited.includes(tool));
if (isVerbose()) {
Expand Down
2 changes: 1 addition & 1 deletion demos/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export type InfoReport = {
let lastReport: DemoReport | null = null;
let lastInfoReport: InfoReport | null = null;

function policyValuesText(state: EngineState, value: 'use' | 'prohibit'): string {
function policyValuesText(state: EngineState, value: typeof POLICY_USE | typeof POLICY_PROHIBIT): string {
const items = getPolicyItems(state, value);
return items.length > 0 ? items.join(', ') : '(none)';
}
Expand Down
4 changes: 2 additions & 2 deletions examples/01_persistent_guardrails.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createEngine, getPolicyItems } from '../src/index.js';
import { POLICY_PROHIBIT, createEngine, getPolicyItems } from '../src/index.js';

declare const process: { argv: string[] };

Expand All @@ -15,7 +15,7 @@ export function runExample01(): {
return {
turn1Kind: decision1.kind,
turn2Kind: decision2.kind,
prohibitedPolicies: getPolicyItems(engine.state, 'prohibit')
prohibitedPolicies: getPolicyItems(engine.state, POLICY_PROHIBIT)
};
}

Expand Down
10 changes: 5 additions & 5 deletions examples/03_ambiguity_with_clarification.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { createEngine, getClarifyPrompt, isClarify } from '../src/index.js';
import { DECISION_CLARIFY, DECISION_UPDATE, createEngine, getClarifyPrompt, isClarify } from '../src/index.js';

declare const process: { argv: string[] };

export function runExample03(): {
clarifyKind: string;
clarifyKind: typeof DECISION_CLARIFY;
clarifyPrompt: string | null;
llmCalled: boolean;
resetKind: string;
resetKind: typeof DECISION_UPDATE;
} {
const engine = createEngine();

Expand All @@ -21,10 +21,10 @@ export function runExample03(): {
const resetDecision = engine.step('clear state');

return {
clarifyKind: contradictionDecision.kind,
clarifyKind: DECISION_CLARIFY,
clarifyPrompt: getClarifyPrompt(contradictionDecision),
llmCalled,
resetKind: resetDecision.kind
resetKind: DECISION_UPDATE
};
}

Expand Down
21 changes: 15 additions & 6 deletions examples/08_controller_preview_diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,19 @@ import {
POLICY_PROHIBIT,
POLICY_USE,
createEngine,
diffHasChanges,
getPolicyItems,
getClarifyPrompt,
getPreviewDecision,
getPreviewStateAfter,
getPremiseValue,
getStepDecision,
getStepState,
getDecisionState,
isClarify,
isUpdate,
preview,
previewWouldMutate,
stateDiff,
step,
type Decision,
Expand Down Expand Up @@ -79,21 +85,24 @@ export function runExample08(): {

const previewResult = preview(engine, 'prohibit peanuts');

const stateAfterPreview = engine.state;
const stateAfterPreview = getPreviewStateAfter(previewResult);
const diffAfterPreview = stateDiff(stateBeforePreview, stateAfterPreview);

const stepResult = step(engine, 'prohibit peanuts');
const previewDecision = getPreviewDecision(previewResult);
const stepDecision = getStepDecision(stepResult);
const stepState = getStepState(stepResult);

return {
stateBeforePreview: summarizeState(stateBeforePreview),
preview: {
wouldMutate: previewResult.would_mutate,
decision: summarizeDecision(previewResult.decision)
wouldMutate: previewWouldMutate(previewResult),
decision: summarizeDecision(previewDecision)
},
stateChangedAfterPreview: diffAfterPreview.changed,
stateChangedAfterPreview: diffHasChanges(diffAfterPreview),
apply: {
decision: summarizeDecision(stepResult.decision),
stateAfterStep: summarizeState(stepResult.state)
decision: summarizeDecision(stepDecision),
stateAfterStep: summarizeState(stepState)
}
};
}
Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Demonstrates explicit single-policy correction without `reset policies`:

Shows controller-layer auditability with `preview(engine, input)` and `stateDiff(before, after)`.
Shows that preview does not mutate live engine state, then applies the same input with `step(engine, input)`.
Uses the controller helper accessors such as `getPreviewDecision`, `getPreviewStateAfter`, `previewWouldMutate`, `getStepDecision`, and `getStepState`.

## Integrations

Expand Down
3 changes: 2 additions & 1 deletion examples/integrations/node-basic/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
type EngineState
} from '@rlippmann/context-compiler';
import {
PREPROCESS_OUTCOME_DIRECTIVE,
parsePreprocessorOutput,
preprocessHeuristic
} from '@rlippmann/context-compiler/experimental/preprocessor';
Expand Down Expand Up @@ -65,7 +66,7 @@ function minimalRecentContext(history: ChatBody['history']) {

function normalizeInputWithPreprocessor(input: string): string {
const heuristic = preprocessHeuristic(input);
if (heuristic.classification === 'directive' && heuristic.output !== null) {
if (heuristic.classification === PREPROCESS_OUTCOME_DIRECTIVE && heuristic.output !== null) {
const parsed = parsePreprocessorOutput(heuristic.output, { sourceInput: input });
if (parsed !== null) {
return parsed;
Expand Down
36 changes: 36 additions & 0 deletions src/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ export function state_diff(before: EngineState, after: EngineState): StructuralD

export const stateDiff = state_diff;

export function diff_has_changes(diff: StructuralDiff): boolean {
return diff.changed;
}

export const diffHasChanges = diff_has_changes;

export function step(engine: Engine, user_input: string): StepResult {
const decision = engine.step(user_input);
return {
Expand All @@ -100,6 +106,18 @@ export function step(engine: Engine, user_input: string): StepResult {
};
}

export function get_step_decision(stepResult: StepResult): Decision {
return stepResult.decision;
}

export const getStepDecision = get_step_decision;

export function get_step_state(stepResult: StepResult): EngineState {
return stepResult.state;
}

export const getStepState = get_step_state;

export function preview(engine: Engine, user_input: string): PreviewResult {
const checkpoint = engine.exportCheckpoint();
const stateBefore = engine.state;
Expand All @@ -124,3 +142,21 @@ export function preview(engine: Engine, user_input: string): PreviewResult {
would_mutate: diff.changed
};
}

export function get_preview_decision(previewResult: PreviewResult): Decision {
return previewResult.decision;
}

export const getPreviewDecision = get_preview_decision;

export function get_preview_state_after(previewResult: PreviewResult): EngineState {
return previewResult.state_after;
}

export const getPreviewStateAfter = get_preview_state_after;

export function preview_would_mutate(previewResult: PreviewResult): boolean {
return previewResult.would_mutate;
}

export const previewWouldMutate = preview_would_mutate;
2 changes: 1 addition & 1 deletion src/experimental/preprocessor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ function sourceInputIsStructuredContractDirective(sourceInput: string, directive
}

return (
rec.classification === 'directive' &&
rec.classification === PREPROCESS_OUTCOME_DIRECTIVE &&
typeof rec.output === 'string' &&
rec.output.trim().toLowerCase() === directiveOutput.trim().toLowerCase()
);
Expand Down
20 changes: 19 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,25 @@ export {
getPremiseValue,
getPolicyItems
} from './engine.js';
export { OUTPUT_VERSION, preview, state_diff, stateDiff, step } from './controller.js';
export {
OUTPUT_VERSION,
diff_has_changes,
diffHasChanges,
get_preview_decision,
get_preview_state_after,
get_step_decision,
get_step_state,
getPreviewDecision,
getPreviewStateAfter,
getStepDecision,
getStepState,
preview,
preview_would_mutate,
previewWouldMutate,
state_diff,
stateDiff,
step
} from './controller.js';
export type { Engine, EngineInit } from './engine.js';
export type { PreviewResult, StepResult, StructuralDiff } from './controller.js';
import type { Decision, EngineState } from './types.js';
Expand Down
6 changes: 6 additions & 0 deletions tests/api_aliases.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ describe('root API aliases', () => {
expect(cc.isPassthrough).toBe(cc.is_passthrough);
expect(cc.getClarifyPrompt).toBe(cc.get_clarify_prompt);
expect(cc.getDecisionState).toBe(cc.get_decision_state);
expect(cc.getStepDecision).toBe(cc.get_step_decision);
expect(cc.getStepState).toBe(cc.get_step_state);
expect(cc.getPreviewDecision).toBe(cc.get_preview_decision);
expect(cc.getPreviewStateAfter).toBe(cc.get_preview_state_after);
expect(cc.previewWouldMutate).toBe(cc.preview_would_mutate);
expect(cc.diffHasChanges).toBe(cc.diff_has_changes);
expect(cc.stateDiff).toBe(cc.state_diff);
});

Expand Down
6 changes: 6 additions & 0 deletions tests/api_parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ const PYTHON_TO_TS_EXPORT_MAP: Record<string, string> = {
is_passthrough: 'is_passthrough',
get_clarify_prompt: 'get_clarify_prompt',
get_decision_state: 'get_decision_state',
get_step_decision: 'get_step_decision',
get_step_state: 'get_step_state',
get_preview_decision: 'get_preview_decision',
get_preview_state_after: 'get_preview_state_after',
preview_would_mutate: 'preview_would_mutate',
diff_has_changes: 'diff_has_changes',
DECISION_PASSTHROUGH: 'DECISION_PASSTHROUGH',
DECISION_UPDATE: 'DECISION_UPDATE',
DECISION_CLARIFY: 'DECISION_CLARIFY',
Expand Down
47 changes: 47 additions & 0 deletions tests/controller_helpers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { describe, expect, it } from 'vitest';
import * as cc from '../src/index.js';

describe('controller helper accessors', () => {
it('returns step result fields through camelCase helpers', () => {
const engine = cc.createEngine();
const stepResult = cc.step(engine, 'set premise concise replies');

expect(cc.getStepDecision(stepResult)).toBe(stepResult.decision);
expect(cc.getStepState(stepResult)).toBe(stepResult.state);
});

it('returns preview result fields through camelCase helpers', () => {
const engine = cc.createEngine();
const previewResult = cc.preview(engine, 'use sqlite');

expect(cc.getPreviewDecision(previewResult)).toBe(previewResult.decision);
expect(cc.getPreviewStateAfter(previewResult)).toBe(previewResult.state_after);
expect(cc.previewWouldMutate(previewResult)).toBe(previewResult.would_mutate);
});

it('returns diff changed flag through the camelCase helper', () => {
const diff = cc.stateDiff(
{ premise: null, policies: {}, version: 2 },
{ premise: 'concise replies', policies: {}, version: 2 }
);

expect(cc.diffHasChanges(diff)).toBe(diff.changed);
});

it('keeps snake_case helper aliases behaviorally identical', () => {
const engine = cc.createEngine();
const stepResult = cc.step(engine, 'set premise concise replies');
const previewResult = cc.preview(engine, 'use sqlite');
const diff = cc.stateDiff(
{ premise: null, policies: {}, version: 2 },
{ premise: 'concise replies', policies: {}, version: 2 }
);

expect(cc.get_step_decision(stepResult)).toBe(cc.getStepDecision(stepResult));
expect(cc.get_step_state(stepResult)).toBe(cc.getStepState(stepResult));
expect(cc.get_preview_decision(previewResult)).toBe(cc.getPreviewDecision(previewResult));
expect(cc.get_preview_state_after(previewResult)).toBe(cc.getPreviewStateAfter(previewResult));
expect(cc.preview_would_mutate(previewResult)).toBe(cc.previewWouldMutate(previewResult));
expect(cc.diff_has_changes(diff)).toBe(cc.diffHasChanges(diff));
});
});
6 changes: 6 additions & 0 deletions tests/fixtures/conformance/api/public-api-v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@
"is_passthrough",
"get_clarify_prompt",
"get_decision_state",
"get_step_decision",
"get_step_state",
"get_preview_decision",
"get_preview_state_after",
"preview_would_mutate",
"diff_has_changes",
"DECISION_PASSTHROUGH",
"DECISION_UPDATE",
"DECISION_CLARIFY",
Expand Down
Loading