diff --git a/README.md b/README.md index 763c669..1952f5a 100644 --- a/README.md +++ b/README.md @@ -311,7 +311,7 @@ instead of direct key traversal. | `step(user_input)` | Parse one user turn and return a deterministic `Decision`. | | `compile_transcript(messages: Transcript)` | Replay a transcript from a fresh engine and return either final state or a confirmation prompt. | | `engine.apply_transcript(messages: Transcript)` | Replay a transcript onto the current engine state and return either final state or a confirmation prompt. | -| `engine.state` | Read current authoritative in-memory state snapshot. | +| `engine.state` | Read the current opaque authoritative in-memory state snapshot; for normal host reads, prefer `get_premise_value(state)` and `get_policy_items(state, ...)`. | | `engine.has_pending_clarification()` | Return whether a confirmation-required clarification is currently pending. | | `get_premise_value(state)` | Read the current premise value from a state snapshot. | | `get_policy_items(state, value=None)` | Read policy items from a state snapshot (all, `use`, or `prohibit`). | @@ -535,6 +535,9 @@ It is designed to be conservative and must be used with validation: - no directive grammar expansion - raw outputs must not be passed directly to the compiler +If `engine.has_pending_clarification()` is true, bypass preprocessing and pass raw input directly to `engine.step(...)`. +Boundary policy is false-negative-preferred: abstain rather than risk unsafe state mutation. + See [LLM preprocessor](docs/llm-preprocessor.md) and [`experimental/preprocessor/`](experimental/preprocessor/) for details. diff --git a/demos/09_llm_pending_clarification.py b/demos/09_llm_pending_clarification.py index f1076e3..08173ba 100644 --- a/demos/09_llm_pending_clarification.py +++ b/demos/09_llm_pending_clarification.py @@ -1,6 +1,13 @@ """Demo 9: pending clarification requires confirmation-only continuation.""" -from context_compiler import DECISION_CLARIFY, DECISION_UPDATE, State, create_engine +from context_compiler import ( + DECISION_CLARIFY, + DECISION_UPDATE, + POLICY_USE, + State, + create_engine, + get_policy_items, +) from demos.common import ( build_baseline_messages, build_reinjected_messages, @@ -19,17 +26,15 @@ TURN_1 = "use podman instead of docker" TURN_2 = "maybe" TURN_3 = "yes" +INITIAL_AUTHORITATIVE_STATE = create_engine().state def _has_podman_use(state: State) -> bool: - policies = state.get("policies") - if not isinstance(policies, dict): - return False - return policies.get("podman") == "use" + return "podman" in get_policy_items(state, POLICY_USE) def _is_initial_authoritative_state(state: State) -> bool: - return state == {"premise": None, "policies": {}, "version": 2} + return state == INITIAL_AUTHORITATIVE_STATE def main() -> None: diff --git a/examples/integrations/litellm/README.md b/examples/integrations/litellm/README.md index 8c2d194..22f0523 100644 --- a/examples/integrations/litellm/README.md +++ b/examples/integrations/litellm/README.md @@ -137,6 +137,7 @@ choose different rendering behavior. - If heuristic returns a directive, that directive is passed to `engine.step(...)`. - If heuristic does not produce a directive (`no_directive` or `unknown`), LLM fallback prompt conversion runs. - If fallback yields nothing usable or errors, behavior safely remains equivalent to basic. + - If `engine.has_pending_clarification()` is true, bypass preprocessing and pass raw input directly to `engine.step(...)`. - Behavior is reject-first and does not broaden the directive grammar. Decision flow in both examples: diff --git a/examples/integrations/litellm_proxy/README.md b/examples/integrations/litellm_proxy/README.md index 3aa7954..11df998 100644 --- a/examples/integrations/litellm_proxy/README.md +++ b/examples/integrations/litellm_proxy/README.md @@ -97,6 +97,7 @@ Preprocessor-enabled variant behavior: - Only the latest user transcript message is preprocessed for compiler replay input. - Heuristic runs first; if no directive is found, LLM fallback is attempted. +- If `engine.has_pending_clarification()` is true, bypass preprocessing and pass raw input directly to `engine.step(...)`. - Forwarded upstream request messages are not rewritten (except injected compiler system message). Optional env vars for preprocessor fallback: diff --git a/examples/integrations/openwebui/README.md b/examples/integrations/openwebui/README.md index 67f7fb1..1a1c21d 100644 --- a/examples/integrations/openwebui/README.md +++ b/examples/integrations/openwebui/README.md @@ -106,6 +106,8 @@ Decision flow in both pipes: - `clarify`: show `prompt_to_user`; do not change saved state. - `update`: state changed; render local acknowledgment for directive-only input, or call downstream model with updated state injected. +For the preprocessor pipe, if `engine.has_pending_clarification()` is true, bypass preprocessing and pass raw input directly to `engine.step(...)`. + ## Behavioral comparisons **Case 1** diff --git a/pyproject.toml b/pyproject.toml index 4f2e924..8f18134 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "context-compiler" -version = "0.7.7" +version = "0.7.8" description = "Deterministic conversational state engine for LLM applications." readme = "README.md" requires-python = ">=3.11" diff --git a/uv.lock b/uv.lock index ad4a45c..30a4795 100644 --- a/uv.lock +++ b/uv.lock @@ -468,7 +468,7 @@ wheels = [ [[package]] name = "context-compiler" -version = "0.7.7" +version = "0.7.8" source = { editable = "." } [package.optional-dependencies]