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: 2 additions & 0 deletions examples/05_llm_integration_pattern.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
1 change: 1 addition & 0 deletions examples/06_step_sequence_and_state_restore.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions examples/08_controller_preview_diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down
2 changes: 2 additions & 0 deletions examples/_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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=(",", ":"))
Expand Down
10 changes: 9 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
35 changes: 35 additions & 0 deletions src/context_compiler/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,36 +8,48 @@


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
state: State


class PreviewResult(TypedDict):
"""Return the dry-run outcome of one controller preview evaluation."""

output_version: Literal[1]
mode: Literal["preview"]
decision: Decision
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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.
Expand Down
10 changes: 10 additions & 0 deletions src/context_compiler/decision_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
34 changes: 34 additions & 0 deletions src/context_compiler/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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",
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions src/context_compiler/grammar.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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]
Expand Down
12 changes: 12 additions & 0 deletions src/context_compiler/repl.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Command-line and REPL entry points for interacting with the engine."""

import json
import sys
from typing import TextIO
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

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

Loading