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
8 changes: 4 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ requires-python = ">=3.11"
license = { text = "Apache-2.0" }
authors = [{ name = "Robert Lippmann" }]
dependencies = [
"context-compiler>=0.8.3",
"context-compiler==0.9.0dev7",
]
keywords = [
"context-compiler",
Expand Down Expand Up @@ -47,12 +47,12 @@ Issues = "https://github.com/rlippmann/context-compiler-example-integrations/iss
[project.optional-dependencies]
all = [
"chromadb",
"context-compiler-directive-drafter>=0.1.2",
"context-compiler-directive-drafter==0.2.0dev0",
"fastapi",
"litellm",
]
drafter = [
"context-compiler-directive-drafter>=0.1.2",
"context-compiler-directive-drafter==0.2.0dev0",
]
fastapi = [
"fastapi",
Expand All @@ -67,7 +67,7 @@ retrieval = [
[dependency-groups]
dev = [
"chromadb",
"context-compiler-directive-drafter>=0.1.2",
"context-compiler-directive-drafter==0.2.0dev0",
"fastapi",
"httpx2>=2.5.0",
"httpx>=0.28.1",
Expand Down
50 changes: 19 additions & 31 deletions python/examples/checkpoint_continuation/README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
# Checkpoint continuation
# State Persistence

Restoring a saved checkpoint changes whether a fresh host process can resume
and apply a pending itinerary change. This example shows checkpoint
continuation in a generic Python travel-booking flow.
Persisting authoritative compiler state lets a fresh host process recover the
same premise and policy decisions without recreating them from model output or
conversation history. This example shows state persistence in a generic Python
travel-booking flow.

## Domain

The domain is a small travel-booking change flow.

The user requests a change from the current itinerary to a new itinerary.
That change requires confirmation before the host applies it.
The user selects a new itinerary, the compiler records that selection in
authoritative state, and the host later applies the booking change from a
restored engine.

## Runtime

Expand All @@ -24,43 +26,29 @@ It does not use directive drafter.
Context Compiler owns:

- authoritative policy state
- the pending confirmation continuation state
- the checkpoint that captures both

In this example, the pending checkpoint state is what makes the resumed
confirmation meaningful.

Restoring authoritative state alone is not enough to resume the pending change.
- serialization of that state through `export_json()`
- restoration of that state through `import_json()`

## What the host owns

The host owns:

- the booking record
- checkpoint persistence
- request/process boundaries
- persisted state storage
- process boundaries
- the runtime behavior that actually applies the itinerary change

The host reads authoritative Context Compiler state after confirmation and
decides whether to apply the booking change.

## Why this is not prompt reinjection

This example does not re-send hidden instructions to a model.

The observable behavior change is host-side: the booking record changes only
after a restored engine resumes the pending confirmation and authoritative
state changes.
The host reads restored authoritative Context Compiler state and decides whether
to apply the booking change.

## Example behavior

1. The host starts with a booking on `boston_trip`.
2. The user initiates a switch to `chicago_trip`.
3. Context Compiler enters a pending confirmation state.
4. The host exports and persists the checkpoint.
5. A fresh host process restores that checkpoint into a new engine.
6. If the user confirms, the host applies the itinerary change.
7. If the user rejects or sends unrelated text, the booking remains unchanged.
2. The user selects `chicago_trip`.
3. Context Compiler updates authoritative state.
4. The host persists that state JSON.
5. A fresh host process restores the saved state into a new engine.
6. The host applies the booking change from the restored authoritative state.

## Run

Expand Down
144 changes: 70 additions & 74 deletions python/examples/checkpoint_continuation/example.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"""Minimal checkpoint-continuation example for a travel booking change."""
"""Minimal persistence example for a travel booking change."""

from collections.abc import Mapping
from dataclasses import dataclass, field
from typing import Literal, TypedDict, cast
from typing import Literal, TypedDict

from context_compiler import POLICY_USE, State, create_engine, get_policy_items
from context_compiler.engine import Checkpoint, Engine, State as EngineState
from context_compiler import POLICY_USE, DecisionKind, PolicyValue, create_engine
from context_compiler.engine import Engine


class BookingRecord(TypedDict):
Expand All @@ -15,25 +16,26 @@ class BookingRecord(TypedDict):
class BookingChangeRuntimeResult(TypedDict):
compiler_input: str
decision_kind: Literal["clarify", "update", "passthrough"]
prompt_to_user: str | None
checkpoint_pending: bool
active_itinerary: str
message_to_user: str | None
persisted_state_json: str
selected_itinerary: str | None
host_applied_change: bool
active_itinerary: str


@dataclass
class CheckpointStore:
"""Host-owned persistence for serialized engine checkpoints."""
class EnginePersistenceStore:
"""Host-owned persistence for serialized authoritative compiler state."""

saved_checkpoint: Checkpoint | None = None
saved_state_json: str | None = None

def save(self, checkpoint: Checkpoint) -> None:
self.saved_checkpoint = checkpoint
def save(self, state_json: str) -> None:
self.saved_state_json = state_json

def load(self) -> Checkpoint:
if self.saved_checkpoint is None:
raise ValueError("no checkpoint saved")
return self.saved_checkpoint
def load(self) -> str:
if self.saved_state_json is None:
raise ValueError("no saved state")
return self.saved_state_json


@dataclass
Expand All @@ -43,8 +45,8 @@ class BookingHost:
booking: BookingRecord
applied_changes: list[str] = field(default_factory=list)

def apply_selected_itinerary(self, state: State) -> bool:
selected_itinerary = select_itinerary_from_state(state)
def apply_selected_itinerary(self, policies: Mapping[str, PolicyValue]) -> bool:
selected_itinerary = select_itinerary_from_policies(policies)
if selected_itinerary is None:
return False

Expand All @@ -53,13 +55,13 @@ def apply_selected_itinerary(self, state: State) -> bool:
return True


def select_itinerary_from_state(state: State) -> str | None:
def select_itinerary_from_policies(policies: Mapping[str, PolicyValue]) -> str | None:
"""Select the host-visible itinerary from authoritative state."""

use_items = list(get_policy_items(state, POLICY_USE))
if not use_items:
return None
return use_items[0]
for item, kind in policies.items():
if kind == POLICY_USE:
return item
return None


def _decision_kind_name(
Expand All @@ -69,99 +71,93 @@ def _decision_kind_name(
raise ValueError("unexpected decision shape")

kind = decision.get("kind")
kind_name = getattr(kind, "value", None)
if kind_name not in {"clarify", "update", "passthrough"}:
raise ValueError(f"unexpected decision kind: {kind_name}")
return cast(Literal["clarify", "update", "passthrough"], kind_name)
if kind == DecisionKind.ERROR:
return "clarify"
if kind == DecisionKind.UPDATE:
return "update"
if kind == DecisionKind.NO_DIRECTIVE:
return "passthrough"
raise ValueError(f"unexpected decision kind: {kind}")


def initiate_itinerary_change(
def persist_itinerary_selection(
engine: Engine,
*,
current_itinerary: str,
requested_itinerary: str,
) -> BookingChangeRuntimeResult:
"""Ask Context Compiler to hold a travel change behind confirmation."""
"""Persist authoritative state after selecting an itinerary."""

compiler_input = f"use {requested_itinerary} instead of {current_itinerary}"
compiler_input = f"use {requested_itinerary}"
decision = engine.step(compiler_input)
persisted_state_json = engine.export_json()
selected_itinerary = select_itinerary_from_policies(engine.policies)

return {
"compiler_input": compiler_input,
"decision_kind": _decision_kind_name(decision),
"prompt_to_user": decision.get("prompt_to_user"),
"checkpoint_pending": engine.has_pending_clarification(),
"active_itinerary": select_itinerary_from_state(engine.state)
or current_itinerary,
"message_to_user": decision["message"],
"persisted_state_json": persisted_state_json,
"selected_itinerary": selected_itinerary,
"host_applied_change": False,
"active_itinerary": requested_itinerary
if selected_itinerary is not None
else "boston_trip",
}


def restore_engine_from_checkpoint(checkpoint: Checkpoint) -> Engine:
"""Restore both authoritative state and pending continuation state."""
def restore_engine_from_persisted_state(state_json: str) -> Engine:
"""Restore authoritative compiler state into a fresh engine."""

engine = create_engine()
engine.import_checkpoint(checkpoint)
engine.import_json(state_json)
return engine


def restore_engine_from_authoritative_state_only(
checkpoint: Checkpoint,
) -> Engine:
"""Restore only authoritative state, without pending continuation state."""

authoritative_state = cast(EngineState, checkpoint["authoritative_state"])
return create_engine(state=authoritative_state)


def continue_itinerary_change(
engine: Engine,
host: BookingHost,
user_input: str,
def apply_restored_itinerary(
engine: Engine, host: BookingHost
) -> BookingChangeRuntimeResult:
"""Resume a pending change and apply host behavior only after confirmation."""
"""Apply host behavior from restored authoritative compiler state."""

decision = engine.step(user_input)
host_applied_change = False
if _decision_kind_name(decision) == "update":
host_applied_change = host.apply_selected_itinerary(engine.state)
host_applied_change = host.apply_selected_itinerary(engine.policies)
selected_itinerary = select_itinerary_from_policies(engine.policies)

return {
"compiler_input": user_input,
"decision_kind": _decision_kind_name(decision),
"prompt_to_user": decision.get("prompt_to_user"),
"checkpoint_pending": engine.has_pending_clarification(),
"active_itinerary": host.booking["active_itinerary"],
"compiler_input": "",
"decision_kind": "update" if host_applied_change else "passthrough",
"message_to_user": None,
"persisted_state_json": engine.export_json(),
"selected_itinerary": selected_itinerary,
"host_applied_change": host_applied_change,
"active_itinerary": host.booking["active_itinerary"],
}


def run_demo() -> dict[str, BookingChangeRuntimeResult | Checkpoint]:
"""Run a deterministic checkpoint-continuation demonstration."""
def run_demo() -> dict[str, BookingChangeRuntimeResult | str]:
"""Run a deterministic persistence demonstration."""

initial_booking: BookingRecord = {
"booking_id": "booking-100",
"active_itinerary": "boston_trip",
}
first_host = BookingHost(booking=initial_booking.copy())
first_engine = create_engine()
checkpoint_store = CheckpointStore()
engine_persistence_store = EnginePersistenceStore()

pending_result = initiate_itinerary_change(
persisted_result = persist_itinerary_selection(
first_engine,
current_itinerary=first_host.booking["active_itinerary"],
requested_itinerary="chicago_trip",
)
checkpoint_store.save(first_engine.export_checkpoint())
engine_persistence_store.save(persisted_result["persisted_state_json"])

resumed_engine = restore_engine_from_checkpoint(checkpoint_store.load())
resumed_host = BookingHost(booking=first_host.booking.copy())
confirmed_result = continue_itinerary_change(resumed_engine, resumed_host, "yes")
restored_engine = restore_engine_from_persisted_state(
engine_persistence_store.load()
)
restored_host = BookingHost(booking=initial_booking.copy())
applied_result = apply_restored_itinerary(restored_engine, restored_host)

return {
"pending_result": pending_result,
"confirmed_result": confirmed_result,
"saved_checkpoint": checkpoint_store.load(),
"persisted_result": persisted_result,
"applied_result": applied_result,
"saved_state_json": engine_persistence_store.load(),
}


Expand Down
Loading
Loading