diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 091fe52..cbe2809 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 env: - PY_FIXTURE_REF: 0e923284d4c1194a1f390fd75cd361cb48c8efd3 + PY_FIXTURE_REF: 6ca2ad74136b46a4da3b67061ecfd835e0ccf0b8 PY_FIXTURE_CHECKOUT: /tmp/context-compiler-source steps: diff --git a/README.md b/README.md index 8a6f3d7..bb6d978 100644 --- a/README.md +++ b/README.md @@ -73,16 +73,16 @@ npm install @rlippmann/context-compiler ## Quick Start ```ts -import { createEngine } from '@rlippmann/context-compiler'; +import { createEngine, get_clarify_prompt, is_clarify, is_update } from '@rlippmann/context-compiler'; const engine = createEngine(); const decision = engine.step('set premise concise replies'); -if (decision.kind === 'update') { +if (is_update(decision)) { // Use the updated stored rules from the engine. console.log(engine.state); -} else if (decision.kind === 'clarify') { - console.log(decision.prompt_to_user); +} else if (is_clarify(decision)) { + console.log(get_clarify_prompt(decision)); } else { // passthrough } diff --git a/demos/common.ts b/demos/common.ts index 8bc0c55..425c457 100644 --- a/demos/common.ts +++ b/demos/common.ts @@ -1,4 +1,13 @@ -import { createEngine, getPolicyItems, getPremiseValue } from '../src/index.js'; +import { + POLICY_PROHIBIT, + POLICY_USE, + createEngine, + getPolicyItems, + getPremiseValue, + get_clarify_prompt, + is_clarify, + is_update +} from '../src/index.js'; import type { Decision, EngineState } from '../src/index.js'; import type { Message } from './llm_client.js'; @@ -41,8 +50,8 @@ function printStateSummary(state: EngineState): void { const premiseText = premiseValue ?? '(none)'; console.log('compiled state:'); console.log(`- premise: ${premiseText}`); - console.log(`- use policies: ${policyValuesText(state, 'use')}`); - console.log(`- prohibit policies: ${policyValuesText(state, 'prohibit')}`); + console.log(`- use policies: ${policyValuesText(state, POLICY_USE)}`); + console.log(`- prohibit policies: ${policyValuesText(state, POLICY_PROHIBIT)}`); } function printMultilinePrompt(label: string, prompt: string): void { @@ -73,13 +82,14 @@ export function printDecision(title: string, decision: Decision, state: EngineSt return; } console.log(`Compiler decision (${title}):`); - if (decision.kind === 'update') { + if (is_update(decision)) { console.log('result: updated'); printStateSummary(state); - } else if (decision.kind === 'clarify') { + } else if (is_clarify(decision)) { console.log('result: clarify'); - if (decision.prompt_to_user) { - printMultilinePrompt('clarify prompt', decision.prompt_to_user); + const clarifyPrompt = get_clarify_prompt(decision); + if (clarifyPrompt) { + printMultilinePrompt('clarify prompt', clarifyPrompt); } printStateSummary(state); } else { @@ -227,12 +237,12 @@ export function compactUserTurns(userTurns: string[]): { for (const turn of userTurns) { const decision = engine.step(turn); - if (decision.kind === 'update') { + if (is_update(decision)) { continue; } compactedTurns.push(turn); - if (decision.kind === 'clarify') { - promptToUser = decision.prompt_to_user; + if (is_clarify(decision)) { + promptToUser = get_clarify_prompt(decision); break; } } @@ -246,8 +256,8 @@ export function compactUserTurns(userTurns: string[]): { export function buildCompiledSystemPrompt(state: EngineState): string { const premise = getPremiseValue(state) ?? '(unset)'; - const useItems = getPolicyItems(state, 'use'); - const prohibitItems = getPolicyItems(state, 'prohibit'); + const useItems = getPolicyItems(state, POLICY_USE); + const prohibitItems = getPolicyItems(state, POLICY_PROHIBIT); const useText = useItems.length > 0 ? useItems.join(', ') : '(none)'; const prohibitText = prohibitItems.length > 0 ? prohibitItems.join(', ') : '(none)'; diff --git a/examples/03_ambiguity_with_clarification.ts b/examples/03_ambiguity_with_clarification.ts index 0c504fe..3f8252b 100644 --- a/examples/03_ambiguity_with_clarification.ts +++ b/examples/03_ambiguity_with_clarification.ts @@ -1,4 +1,4 @@ -import { createEngine } from '../src/index.js'; +import { createEngine, get_clarify_prompt, is_clarify } from '../src/index.js'; declare const process: { argv: string[] }; @@ -14,7 +14,7 @@ export function runExample03(): { const contradictionDecision = engine.step('use peanuts'); let llmCalled = false; - if (contradictionDecision.kind !== 'clarify') { + if (!is_clarify(contradictionDecision)) { llmCalled = true; } @@ -22,7 +22,7 @@ export function runExample03(): { return { clarifyKind: contradictionDecision.kind, - clarifyPrompt: contradictionDecision.prompt_to_user, + clarifyPrompt: get_clarify_prompt(contradictionDecision), llmCalled, resetKind: resetDecision.kind }; diff --git a/examples/04_tool_governance_denylist.ts b/examples/04_tool_governance_denylist.ts index a02b8cb..bf20988 100644 --- a/examples/04_tool_governance_denylist.ts +++ b/examples/04_tool_governance_denylist.ts @@ -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[] }; @@ -10,7 +10,7 @@ export function runExample04(): { const engine = createEngine(); const decision = engine.step('prohibit docker'); - const prohibited = new Set(getPolicyItems(engine.state, 'prohibit')); + const prohibited = new Set(getPolicyItems(engine.state, POLICY_PROHIBIT)); const tools = ['docker', 'kubectl']; const blockedTools = tools.filter((tool) => prohibited.has(tool)); diff --git a/examples/05_llm_integration_pattern.ts b/examples/05_llm_integration_pattern.ts index 2418e45..7293e97 100644 --- a/examples/05_llm_integration_pattern.ts +++ b/examples/05_llm_integration_pattern.ts @@ -1,4 +1,4 @@ -import { createEngine } from '../src/index.js'; +import { createEngine, is_passthrough, is_update } from '../src/index.js'; declare const process: { argv: string[] }; @@ -6,10 +6,10 @@ type HostAction = 'call_llm_without_state' | 'call_llm_with_state' | 'show_clari function handleTurn(engine: ReturnType, input: string): HostAction { const decision = engine.step(input); - if (decision.kind === 'passthrough') { + if (is_passthrough(decision)) { return 'call_llm_without_state'; } - if (decision.kind === 'update') { + if (is_update(decision)) { return 'call_llm_with_state'; } return 'show_clarify_prompt'; diff --git a/examples/07_single_policy_correction.ts b/examples/07_single_policy_correction.ts index df52f9c..331c62f 100644 --- a/examples/07_single_policy_correction.ts +++ b/examples/07_single_policy_correction.ts @@ -1,4 +1,4 @@ -import { createEngine, getPolicyItems } from '../src/index.js'; +import { POLICY_PROHIBIT, POLICY_USE, createEngine, getPolicyItems } from '../src/index.js'; declare const process: { argv: string[] }; @@ -11,12 +11,12 @@ export function runExample07(): { const decision1 = engine.step('prohibit peanuts'); const decision2 = engine.step('remove policy peanuts'); const decision3 = engine.step('use peanuts'); - const useItems = getPolicyItems(engine.state, 'use'); - const prohibitItems = getPolicyItems(engine.state, 'prohibit'); + const useItems = getPolicyItems(engine.state, POLICY_USE); + const prohibitItems = getPolicyItems(engine.state, POLICY_PROHIBIT); return { stepKinds: [decision1.kind, decision2.kind, decision3.kind], - finalPolicy: useItems.includes('peanuts') ? 'use' : prohibitItems.includes('peanuts') ? 'prohibit' : null + finalPolicy: useItems.includes('peanuts') ? POLICY_USE : prohibitItems.includes('peanuts') ? POLICY_PROHIBIT : null }; } diff --git a/examples/nextjs-basic/app/api/chat/route.ts b/examples/nextjs-basic/app/api/chat/route.ts index aab9333..cfa5536 100644 --- a/examples/nextjs-basic/app/api/chat/route.ts +++ b/examples/nextjs-basic/app/api/chat/route.ts @@ -1,4 +1,12 @@ -import { createEngine, getPolicyItems, getPremiseValue, type EngineState } from '@rlippmann/context-compiler'; +import { + DECISION_CLARIFY, + POLICY_USE, + createEngine, + getPolicyItems, + getPremiseValue, + is_clarify, + type EngineState +} from '@rlippmann/context-compiler'; import { loadSessionState, saveSessionState } from '../../../lib/context-sessions'; type ChatBody = { @@ -8,11 +16,11 @@ type ChatBody = { }; type ChatResponse = - | { kind: 'clarify'; prompt_to_user: string | null } + | { kind: typeof DECISION_CLARIFY; prompt_to_user: string | null } | { kind: 'continue'; output: string }; function stateToSystemPrompt(state: EngineState): string { - const useItems = new Set(getPolicyItems(state, 'use')); + const useItems = new Set(getPolicyItems(state, POLICY_USE)); const policies = getPolicyItems(state) .map((item) => `- ${useItems.has(item) ? 'USE' : 'PROHIBIT'}: ${item}`) .join('\n'); @@ -64,7 +72,7 @@ export async function POST(req: Request): Promise { if (replay.kind === 'confirm') { saveSessionState(sessionId, engine.exportCheckpointJson()); const payload: ChatResponse = { - kind: 'clarify', + kind: DECISION_CLARIFY, prompt_to_user: replay.prompt_to_user }; return Response.json(payload); @@ -75,10 +83,10 @@ export async function POST(req: Request): Promise { const decision = engine.step(input); - if (decision.kind === 'clarify') { + if (is_clarify(decision)) { saveSessionState(sessionId, engine.exportCheckpointJson()); const payload: ChatResponse = { - kind: 'clarify', + kind: DECISION_CLARIFY, prompt_to_user: decision.prompt_to_user }; return Response.json(payload); diff --git a/examples/node-basic/server.ts b/examples/node-basic/server.ts index dfc7d22..7d1b067 100644 --- a/examples/node-basic/server.ts +++ b/examples/node-basic/server.ts @@ -1,5 +1,13 @@ import http from 'node:http'; -import { createEngine, getPolicyItems, getPremiseValue, type EngineState } from '@rlippmann/context-compiler'; +import { + DECISION_CLARIFY, + POLICY_USE, + createEngine, + getPolicyItems, + getPremiseValue, + is_clarify, + type EngineState +} from '@rlippmann/context-compiler'; import { parse_preprocessor_output, preprocess_heuristic @@ -12,7 +20,7 @@ type ChatBody = { }; type ChatResponse = - | { kind: 'clarify'; prompt_to_user: string | null } + | { kind: typeof DECISION_CLARIFY; prompt_to_user: string | null } | { kind: 'continue'; output: string }; const checkpointBySession = new Map(); // sessionId -> engine.exportCheckpointJson() @@ -26,7 +34,7 @@ function saveCheckpoint(sessionId: string, json: string): void { } function stateToSystemPrompt(state: EngineState): string { - const useItems = new Set(getPolicyItems(state, 'use')); + const useItems = new Set(getPolicyItems(state, POLICY_USE)); const policies = getPolicyItems(state) .map((item: string) => `- ${useItems.has(item) ? 'USE' : 'PROHIBIT'}: ${item}`) .join('\n'); @@ -103,7 +111,7 @@ const server = http.createServer(async (req, res) => { const replay = engine.apply_transcript(replayMessages); if (replay.kind === 'confirm') { saveCheckpoint(sessionId, engine.exportCheckpointJson()); - const payload: ChatResponse = { kind: 'clarify', prompt_to_user: replay.prompt_to_user }; + const payload: ChatResponse = { kind: DECISION_CLARIFY, prompt_to_user: replay.prompt_to_user }; sendJson(res, 200, payload); return; } @@ -112,9 +120,9 @@ const server = http.createServer(async (req, res) => { const preprocessedInput = normalizeInputWithPreprocessor(input); const decision = engine.step(preprocessedInput); - if (decision.kind === 'clarify') { + if (is_clarify(decision)) { saveCheckpoint(sessionId, engine.exportCheckpointJson()); - const payload: ChatResponse = { kind: 'clarify', prompt_to_user: decision.prompt_to_user }; + const payload: ChatResponse = { kind: DECISION_CLARIFY, prompt_to_user: decision.prompt_to_user }; sendJson(res, 200, payload); return; } diff --git a/src/index.ts b/src/index.ts index 145ec3b..71615d6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,9 +2,33 @@ export { createEngine, compile_transcript, getPremiseValue, getPolicyItems } fro export { OUTPUT_VERSION, preview, state_diff, 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'; export const DECISION_PASSTHROUGH = 'passthrough' as const; export const DECISION_UPDATE = 'update' as const; export const DECISION_CLARIFY = 'clarify' as const; +export const POLICY_USE = 'use' as const; +export const POLICY_PROHIBIT = 'prohibit' as const; + +export function is_update(decision: Decision): boolean { + return decision.kind === DECISION_UPDATE; +} + +export function is_clarify(decision: Decision): boolean { + return decision.kind === DECISION_CLARIFY; +} + +export function is_passthrough(decision: Decision): boolean { + return decision.kind === DECISION_PASSTHROUGH; +} + +export function get_clarify_prompt(decision: Decision): string | null { + return is_clarify(decision) ? decision.prompt_to_user : null; +} + +export function get_decision_state(decision: Decision): EngineState | null { + return decision.state; +} + export type { ApplyResult, CheckpointPendingReplacement, diff --git a/tests/api_parity.test.ts b/tests/api_parity.test.ts index 75b3558..90f6b38 100644 --- a/tests/api_parity.test.ts +++ b/tests/api_parity.test.ts @@ -22,9 +22,16 @@ const PYTHON_TO_TS_EXPORT_MAP: Record = { compile_transcript: 'compile_transcript', get_premise_value: 'getPremiseValue', get_policy_items: 'getPolicyItems', + is_update: 'is_update', + is_clarify: 'is_clarify', + is_passthrough: 'is_passthrough', + get_clarify_prompt: 'get_clarify_prompt', + get_decision_state: 'get_decision_state', DECISION_PASSTHROUGH: 'DECISION_PASSTHROUGH', DECISION_UPDATE: 'DECISION_UPDATE', DECISION_CLARIFY: 'DECISION_CLARIFY', + POLICY_USE: 'POLICY_USE', + POLICY_PROHIBIT: 'POLICY_PROHIBIT', TranscriptMessage: '__type_only__', Transcript: '__type_only__', ApplyResult: '__type_only__' diff --git a/tests/fixtures/README.md b/tests/fixtures/README.md index 05af512..126e499 100644 --- a/tests/fixtures/README.md +++ b/tests/fixtures/README.md @@ -13,7 +13,7 @@ This directory contains multiple fixture suites with different contracts. ## API contract fixture -[`conformance/api/public-api-v1.json`](conformance/api/public-api-v1.json) defines a small public API presence contract for the Python 0.6 surface that ports must expose. +[`conformance/api/public-api-v1.json`](conformance/api/public-api-v1.json) defines a small public API presence contract for the Python 0.7.x surface that ports must expose. Ports may sync this artifact with conformance fixtures. diff --git a/tests/fixtures/conformance/api/public-api-v1.json b/tests/fixtures/conformance/api/public-api-v1.json index c923c9e..18a84fc 100644 --- a/tests/fixtures/conformance/api/public-api-v1.json +++ b/tests/fixtures/conformance/api/public-api-v1.json @@ -8,9 +8,16 @@ "compile_transcript", "get_premise_value", "get_policy_items", + "is_update", + "is_clarify", + "is_passthrough", + "get_clarify_prompt", + "get_decision_state", "DECISION_PASSTHROUGH", "DECISION_UPDATE", "DECISION_CLARIFY", + "POLICY_USE", + "POLICY_PROHIBIT", "TranscriptMessage", "Transcript", "ApplyResult"