From 05f36a235999b5a278a870b18d9b50e9df0e70b3 Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Mon, 3 Aug 2026 16:48:13 -0400 Subject: [PATCH 1/3] docs: document public Python API --- src/context_compiler/controller.py | 35 ++++++++++++++++++++++++ src/context_compiler/decision_helpers.py | 10 +++++++ src/context_compiler/engine.py | 34 +++++++++++++++++++++++ src/context_compiler/grammar.py | 6 ++++ src/context_compiler/repl.py | 12 ++++++++ 5 files changed, 97 insertions(+) diff --git a/src/context_compiler/controller.py b/src/context_compiler/controller.py index 8a8b23e..022cb82 100644 --- a/src/context_compiler/controller.py +++ b/src/context_compiler/controller.py @@ -8,29 +8,39 @@ class PremiseDiff(TypedDict): + """Describe how the authoritative premise changed between two states.""" + before: str | None after: str | None changed: bool class ChangedPolicyDiff(TypedDict): + """Capture the old and new value for one policy item.""" + before: Literal["use", "prohibit"] after: Literal["use", "prohibit"] class PoliciesDiff(TypedDict): + """Summarize policy items added, removed, or changed between two states.""" + added: dict[str, Literal["use", "prohibit"]] removed: dict[str, Literal["use", "prohibit"]] changed: dict[str, ChangedPolicyDiff] class StructuralDiff(TypedDict): + """Describe whether and how authoritative state changed structurally.""" + changed: bool premise: PremiseDiff policies: PoliciesDiff class StepResult(TypedDict): + """Return the committed outcome of one controller-driven engine step.""" + output_version: Literal[1] mode: Literal["step"] decision: Decision @@ -38,6 +48,8 @@ class StepResult(TypedDict): class PreviewResult(TypedDict): + """Return the dry-run outcome of one controller preview evaluation.""" + output_version: Literal[1] mode: Literal["preview"] decision: Decision @@ -48,30 +60,44 @@ class PreviewResult(TypedDict): def get_step_decision(step_result: StepResult) -> Decision: + """Return the decision emitted by a committed controller step.""" + return step_result["decision"] def get_step_state(step_result: StepResult) -> State: + """Return the authoritative state after a committed controller step.""" + return step_result["state"] def get_preview_decision(preview_result: PreviewResult) -> Decision: + """Return the decision that preview computed without mutating live state.""" + return preview_result["decision"] def get_preview_state_after(preview_result: PreviewResult) -> State: + """Return the simulated post-transition state from preview.""" + return preview_result["state_after"] def preview_would_mutate(preview_result: PreviewResult) -> bool: + """Return whether preview observed any structural state change.""" + return preview_result["would_mutate"] def diff_has_changes(diff: StructuralDiff) -> bool: + """Return whether a structural diff reports any state change.""" + return diff["changed"] def state_diff(before: State, after: State) -> StructuralDiff: + """Compute a structural diff between two authoritative engine states.""" + before_premise = before["premise"] after_premise = after["premise"] premise_changed = before_premise != after_premise @@ -112,6 +138,8 @@ def state_diff(before: State, after: State) -> StructuralDiff: def step(engine: Engine, user_input: str) -> StepResult: + """Commit one engine transition and package the resulting decision and state.""" + decision = engine.step(user_input) return { "output_version": OUTPUT_VERSION, @@ -122,6 +150,13 @@ def step(engine: Engine, user_input: str) -> StepResult: def preview(engine: Engine, user_input: str) -> PreviewResult: + """Evaluate one transition without mutating the engine's live state. + + Preview uses the same transition evaluation path as committed execution, + then returns the simulated decision, before/after states, and a structural + diff that callers can inspect before deciding whether to step. + """ + state_before = engine.state # Preview intentionally consumes the engine's private evaluator so preview and # committed execution share one transition path without making evaluation public. diff --git a/src/context_compiler/decision_helpers.py b/src/context_compiler/decision_helpers.py index afe78d2..e5f1d7e 100644 --- a/src/context_compiler/decision_helpers.py +++ b/src/context_compiler/decision_helpers.py @@ -13,24 +13,34 @@ def is_update(decision: Decision) -> TypeGuard[UpdateDecision]: + """Return whether a decision represents a successful state update.""" + return decision["kind"] == DECISION_UPDATE def is_error(decision: Decision) -> TypeGuard[ErrorDecision]: + """Return whether a decision represents an error outcome.""" + return decision["kind"] == DECISION_ERROR def is_no_directive(decision: Decision) -> TypeGuard[NoDirectiveDecision]: + """Return whether a decision reports that no directive was recognized.""" + return decision["kind"] == DECISION_NO_DIRECTIVE def get_error_message(decision: Decision) -> str | None: + """Return the error message for an error decision, if present.""" + if not is_error(decision): return None return decision["message"] def get_decision_state(decision: Decision) -> State | None: + """Return the updated authoritative state for an update decision, if present.""" + if not is_update(decision): return None return decision["state"] diff --git a/src/context_compiler/engine.py b/src/context_compiler/engine.py index 78adc88..04e5b60 100644 --- a/src/context_compiler/engine.py +++ b/src/context_compiler/engine.py @@ -33,15 +33,21 @@ class State(TypedDict): class NoDirectiveDecision(TypedDict): + """Report that input did not match any recognized canonical directive.""" + kind: Literal["no_directive"] class UpdateDecision(TypedDict): + """Report a successful transition and the resulting authoritative state.""" + kind: Literal["update"] state: State class ErrorDecision(TypedDict): + """Report a rejected transition together with a user-facing error message.""" + kind: Literal["error"] message: str @@ -51,6 +57,8 @@ class ErrorDecision(TypedDict): @dataclass(frozen=True) class Action: + """Represent one parsed engine action before state validation or mutation.""" + kind: Literal[ "set_premise", "change_premise", @@ -78,33 +86,59 @@ class _EvaluatedTransition: def create_engine(state: State | None = None) -> "Engine": + """Create an engine initialized from validated state or the empty state.""" + return Engine(state=state) class Engine: + """Own the authoritative state and apply one directive transition at a time.""" + def __init__(self, state: State | None = None) -> None: self._state: State self._replace_state(_initial_state() if state is None else _load_state_obj(state)) @property def premise(self) -> str | None: + """Return the current premise from authoritative state.""" + return self._state[STATE_PREMISE] @property def policies(self) -> Mapping[str, PolicyValue]: + """Return a defensive copy of the current policy mapping.""" + return deepcopy(self._state[STATE_POLICIES]) @property def state(self) -> State: + """Return a defensive copy of the full authoritative state.""" + return deepcopy(self._state) def export_json(self) -> str: + """Serialize the current authoritative state to canonical JSON text.""" + return json.dumps(self._state, sort_keys=True, separators=(",", ":")) def import_json(self, payload: str) -> None: + """Replace authoritative state from previously exported JSON text. + + The payload must match the current state schema and is normalized using + the same validation rules applied to other engine state inputs. + """ + self._replace_state(_load_state_json(payload)) def step(self, user_input: str) -> Decision: + """Evaluate and commit one user input against authoritative state. + + Non-directive input returns ``no_directive`` without changing state. + Invalid directives return ``error`` without changing state. Accepted + directives return ``update`` and commit the resulting authoritative + state before the decision is returned. + """ + evaluated = self._evaluate_transition(self._state, user_input) self._replace_state(evaluated.next_state) return evaluated.decision diff --git a/src/context_compiler/grammar.py b/src/context_compiler/grammar.py index 5e33c0f..3b92a2a 100644 --- a/src/context_compiler/grammar.py +++ b/src/context_compiler/grammar.py @@ -8,6 +8,8 @@ class DirectiveKind(StrEnum): + """Enumerate the supported canonical directive families.""" + SET_PREMISE = "set_premise" CHANGE_PREMISE = "change_premise" USE_ITEM = "use_item" @@ -21,12 +23,16 @@ class DirectiveKind(StrEnum): @dataclass(frozen=True, slots=True) class ValidatedDirective: + """Classify text as one canonical directive kind without exposing operands.""" + text: str kind: DirectiveKind @dataclass(frozen=True, slots=True) class CanonicalDirective: + """Represent one parsed canonical directive and its named operands.""" + text: str kind: DirectiveKind operands: MappingProxyType[str, str] diff --git a/src/context_compiler/repl.py b/src/context_compiler/repl.py index a84dc16..d94bad6 100644 --- a/src/context_compiler/repl.py +++ b/src/context_compiler/repl.py @@ -1,3 +1,5 @@ +"""Command-line and REPL entry points for interacting with the engine.""" + import json import sys from typing import TextIO @@ -245,6 +247,14 @@ def run_repl( json_mode: bool = False, engine: Engine | None = None, ) -> None: + """Run the interactive or line-oriented REPL against one engine instance. + + Interactive mode exposes command helpers such as ``state`` and ``preview``. + Non-interactive mode consumes one input line at a time and can optionally + emit NDJSON records. Preview requests are evaluated through the shared + controller preview path and never mutate the live engine state. + """ + active_engine = create_engine() if engine is None else engine if _is_interactive(in_stream, out_stream): @@ -421,6 +431,8 @@ def run_repl( def main() -> int: # pragma: no cover + """Run the command-line entry point and return the process exit status.""" + args = sys.argv[1:] if not args: run_repl(sys.stdin, sys.stdout) From d5d8cfb77d279c26709a43a46a207ef0420bd56e Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Mon, 3 Aug 2026 16:56:21 -0400 Subject: [PATCH 2/3] chore: bump dev version --- pyproject.toml | 10 +++++++++- uv.lock | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6e5cb9b..6153367 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "context-compiler" -version = "0.9.0dev3" +version = "0.9.0dev4" description = "Deterministic conversational state engine for LLM applications." readme = "README.md" requires-python = ">=3.11" @@ -70,6 +70,14 @@ target-version = "py311" [tool.ruff.lint] select = ["E", "F", "I", "UP", "B", "SIM"] +extend-select = ["D100", "D101", "D102", "D103"] + +[tool.ruff.lint.per-file-ignores] +"tests/**/*.py" = ["D100", "D101", "D102", "D103"] +"demos/**/*.py" = ["D100", "D101", "D102", "D103"] +"examples/**/*.py" = ["D100", "D101", "D102", "D103"] +"evals/**/*.py" = ["D100", "D101", "D102", "D103"] +"host_support/**/*.py" = ["D100", "D101", "D102", "D103"] [tool.mypy] python_version = "3.11" diff --git a/uv.lock b/uv.lock index eb86f33..251c126 100644 --- a/uv.lock +++ b/uv.lock @@ -296,7 +296,7 @@ wheels = [ [[package]] name = "context-compiler" -version = "0.9.0.dev3" +version = "0.9.0.dev4" source = { editable = "." } [package.optional-dependencies] From 7dca7822209f690dcc32498c911d87d2eccd77e1 Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Mon, 3 Aug 2026 16:57:26 -0400 Subject: [PATCH 3/3] docs: clarify example intent --- examples/05_llm_integration_pattern.py | 2 ++ examples/06_step_sequence_and_state_restore.py | 1 + examples/08_controller_preview_diff.py | 2 ++ examples/_util.py | 2 ++ 4 files changed, 7 insertions(+) diff --git a/examples/05_llm_integration_pattern.py b/examples/05_llm_integration_pattern.py index d95587f..f252f1e 100644 --- a/examples/05_llm_integration_pattern.py +++ b/examples/05_llm_integration_pattern.py @@ -28,10 +28,12 @@ def handle_turn(engine_input: str, engine: Engine) -> None: print_decision_summary(decision) if is_no_directive(decision): + # Ordinary input stays host-managed; this example forwards it to the model unchanged. print("Host action: no_directive -> core recognized no canonical directive") print("Host choice in this example: call fake_llm() without state") fake_llm(None, engine_input) elif is_update(decision): + # Successful directives produce authoritative state that host code can pass downstream. print("Host action: update -> call fake_llm() with compiled state") fake_llm(decision["state"], engine_input) elif is_error(decision): diff --git a/examples/06_step_sequence_and_state_restore.py b/examples/06_step_sequence_and_state_restore.py index 245a0c7..e8ad37b 100644 --- a/examples/06_step_sequence_and_state_restore.py +++ b/examples/06_step_sequence_and_state_restore.py @@ -19,6 +19,7 @@ def main() -> None: print_decision_summary(engine.step(turn)) print() + # Hosts can persist authoritative state directly instead of replaying prior turns. state_json = engine.export_json() restored = create_engine() restored.import_json(state_json) diff --git a/examples/08_controller_preview_diff.py b/examples/08_controller_preview_diff.py index 0242dd4..aab10fa 100644 --- a/examples/08_controller_preview_diff.py +++ b/examples/08_controller_preview_diff.py @@ -24,11 +24,13 @@ def main() -> None: print_state_summary(state_before, "state before preview") print("\nPreview: prohibit peanuts") + # Preview uses the same transition semantics as apply, but must not mutate live state. preview_result = preview(engine, "prohibit peanuts") print("would_mutate:", preview_would_mutate(preview_result)) print_decision_summary(get_preview_decision(preview_result)) state_after_preview = engine.state + # Compare live state before and after preview to show the dry-run contract directly. diff_after_preview = state_diff(state_before, state_after_preview) print("state changed after preview:", diff_has_changes(diff_after_preview)) diff --git a/examples/_util.py b/examples/_util.py index 3f12592..7642ff6 100644 --- a/examples/_util.py +++ b/examples/_util.py @@ -8,6 +8,8 @@ is_update, ) +# These helpers only format readable example output; they are not part of the library API. + def canonical_json(obj: Any) -> str: return json.dumps(obj, sort_keys=True, separators=(",", ":"))