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
26 changes: 17 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ Use Context Compiler in your host application first:
```python
from context_compiler import (
create_engine,
get_error_prompt,
get_error_message,
is_error,
is_update,
)
Expand All @@ -148,7 +148,7 @@ user_input = "set premise current project uses uv"
decision = engine.step(user_input)

if is_error(decision):
show_to_user(get_error_prompt(decision))
show_to_user(get_error_message(decision))
elif is_update(decision):
messages = build_messages(engine.state, user_input)
render(call_llm(messages))
Expand Down Expand Up @@ -247,10 +247,18 @@ uv run pytest
Each user message produces a `Decision`.

```python
class Decision(TypedDict):
kind: Literal["no_directive", "update", "error"]
state: dict | None
prompt_to_user: str | None
class NoDirectiveDecision(TypedDict):
kind: Literal["no_directive"]

class UpdateDecision(TypedDict):
kind: Literal["update"]
state: State

class ErrorDecision(TypedDict):
kind: Literal["error"]
message: str

Decision = NoDirectiveDecision | UpdateDecision | ErrorDecision
```

Meaning:
Expand All @@ -259,10 +267,10 @@ Meaning:
| --- | --- |
| no_directive | no canonical directive recognized; no authoritative state change; host decides what to do next |
| update | authoritative state mutated; host may use updated state downstream |
| error | show `prompt_to_user` and do not continue normal downstream processing yet |
| error | show `message` and do not continue normal downstream processing yet |

For normal app code, prefer the exported decision helpers (`is_error`,
`is_update`, `is_no_directive`, `get_error_prompt`, `get_decision_state`)
`is_update`, `is_no_directive`, `get_error_message`, `get_decision_state`)
instead of direct key traversal.

See [docs/api-reference.md](docs/api-reference.md) for the full public API
Expand All @@ -274,7 +282,7 @@ Common API entry points:
`engine.premise`, `engine.policies`, `engine.export_json(...)`,
`engine.import_json(...)`
- decision helpers: `is_error(...)`, `is_update(...)`, `is_no_directive(...)`,
`get_error_prompt(...)`, `get_decision_state(...)`
`get_error_message(...)`, `get_decision_state(...)`
- state transport: `engine.export_json(...)`, `engine.import_json(...)`
- controller API: `step(...)`
- audit APIs: `preview(...)`, `state_diff(...)`
Expand Down
8 changes: 4 additions & 4 deletions demos/01_llm_contradiction_error.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Demo 1: compiler blocks contradictory directives before model call."""

from context_compiler import create_engine
from context_compiler import create_engine, is_error
from demos.common import (
build_baseline_messages,
build_mediated_messages_from_transcript,
Expand Down Expand Up @@ -62,9 +62,9 @@ def main() -> None:
reinjected_output = complete_messages(reinjected_messages)
print_model_output("Reinjected-state", reinjected_output)

if second["kind"] == "error":
if is_error(second):
print_messages("compiler-mediated (full)", [])
mediated_output = f"[no call] error required: {second['prompt_to_user']}\nACTION:error"
mediated_output = f"[no call] error required: {second['message']}\nACTION:error"
print_model_output("Compiler-mediated (full)", mediated_output)
else:
mediated_messages = build_mediated_messages_from_transcript(engine.state, user_inputs)
Expand All @@ -89,7 +89,7 @@ def main() -> None:
compact_action = extract_tag_value(compact_output, "ACTION")
baseline_respects = baseline_action is not None and baseline_action.lower() == "error"
reinjected_respects = reinjected_action is not None and reinjected_action.lower() == "error"
compiler_host_blocked = second["kind"] == "error"
compiler_host_blocked = is_error(second)
mediated_respects = compiler_host_blocked
compact_respects = compacted_prompt is not None or (
compact_action is not None and compact_action.lower() == "error"
Expand Down
5 changes: 2 additions & 3 deletions demos/06_llm_context_compaction.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""Demo 6: host-side prompt replacement from authoritative step-derived state."""

from context_compiler import create_engine
from context_compiler.engine import DecisionKind
from context_compiler import DECISION_UPDATE, create_engine
from demos.common import compact_user_turns, is_verbose, print_info_report

DEMO_NAME = "06_context_compaction — superseded directives eliminated"
Expand Down Expand Up @@ -44,7 +43,7 @@ def _compile_premise(turns: list[str]) -> str:
engine = create_engine()
for turn in turns:
decision = engine.step(turn)
assert decision["kind"] == DecisionKind.UPDATE
assert decision["kind"] == DECISION_UPDATE
compiled_premise = engine.premise
assert compiled_premise is not None
return compiled_premise
Expand Down
8 changes: 4 additions & 4 deletions demos/08_llm_replacement_precondition.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Demo 8: missing-source replacement applies deterministically from authoritative state."""

from context_compiler import DECISION_UPDATE, State, create_engine
from context_compiler import State, create_engine, is_update
from demos.common import (
build_baseline_messages,
build_reinjected_messages,
Expand Down Expand Up @@ -61,7 +61,7 @@ def main() -> None:
reinjected_output = complete_messages(reinjected_messages)
print_model_output("Reinjected-state", reinjected_output)

if decision["kind"] == DECISION_UPDATE:
if is_update(decision):
print_messages("compiler-mediated (full)", [])
mediated_output = "[no call] authoritative state applied deterministic replacement update"
print_model_output("Compiler-mediated (full)", mediated_output)
Expand All @@ -86,7 +86,7 @@ def main() -> None:

baseline_has_authoritative_precondition = False
reinjected_has_authoritative_precondition = False
compiler_pass = decision["kind"] == DECISION_UPDATE and state_applied
compiler_pass = is_update(decision) and state_applied
compact_pass = compacted_prompt is None and compact_state_applied and compact_no_pending

print_host_check(
Expand All @@ -101,7 +101,7 @@ def main() -> None:
)
print_host_check(
"COMPILER_BLOCKED_INVALID_REPLACEMENT",
yes_no(decision["kind"] == DECISION_UPDATE),
yes_no(is_update(decision)),
context="compiler-mediated",
)
print_host_check(
Expand Down
12 changes: 5 additions & 7 deletions demos/09_llm_confirmation_no_directive.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"""Demo 9: confirmation-style followups remain ordinary no_directive."""

from context_compiler import (
DECISION_NO_DIRECTIVE,
DECISION_UPDATE,
State,
create_engine,
is_no_directive,
is_update,
)
from demos.common import (
build_baseline_messages,
Expand Down Expand Up @@ -98,11 +98,9 @@ def main() -> None:
)
print_model_output("Compiler-mediated + compact", compact_output)

deterministic_initial_update = first["kind"] == DECISION_UPDATE and state_applied_after_first
unrelated_followup_no_directive = (
second["kind"] == DECISION_NO_DIRECTIVE and state_preserved_after_second
)
confirmation_token_not_consumed = third["kind"] == DECISION_NO_DIRECTIVE
deterministic_initial_update = is_update(first) and state_applied_after_first
unrelated_followup_no_directive = is_no_directive(second) and state_preserved_after_second
confirmation_token_not_consumed = is_no_directive(third)
deterministic_final_state = _has_podman_use(engine.state)

baseline_has_confirmation_state_machine = False
Expand Down
26 changes: 13 additions & 13 deletions demos/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
from typing import Literal, NotRequired, TypedDict

from context_compiler import (
DECISION_ERROR,
DECISION_UPDATE,
Decision,
State,
create_engine,
is_error,
is_update,
)
from demos.llm_client import Message

Expand Down Expand Up @@ -80,14 +80,14 @@ def print_decision(title: str, decision: Decision, state: State) -> None:
if not is_verbose():
return
print(f"Compiler decision ({title}):")
if decision["kind"] == DECISION_UPDATE:
if is_update(decision):
print("result: updated")
_print_state_summary(state)
elif decision["kind"] == DECISION_ERROR:
elif is_error(decision):
print("result: error")
prompt = decision["prompt_to_user"]
if prompt:
_print_multiline_prompt("error prompt", prompt)
message = decision["message"]
if message:
_print_multiline_prompt("error message", message)
_print_state_summary(state)
else:
print("result: no_directive")
Expand Down Expand Up @@ -255,24 +255,24 @@ def compact_user_turns(
- drop update lines
- keep no_directive lines
- keep first error line and stop
- return prompt_to_user for error, else None
- return message for error, else None
- returned state is engine state at stop point
"""

engine = create_engine()
compacted_turns: list[str] = []
prompt_to_user: str | None = None
message: str | None = None

for turn in user_turns:
decision = engine.step(turn)
if decision["kind"] == DECISION_UPDATE:
if is_update(decision):
continue
compacted_turns.append(turn)
if decision["kind"] == DECISION_ERROR:
prompt_to_user = decision["prompt_to_user"]
if is_error(decision):
message = decision["message"]
break

return compacted_turns, engine.state, prompt_to_user
return compacted_turns, engine.state, message


def build_mediated_messages_from_transcript(
Expand Down
16 changes: 12 additions & 4 deletions docs/DirectiveGrammarSpec.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,18 @@ The host:
## 4. Decision API Contract

```python
class Decision(TypedDict):
kind: Literal["no_directive", "update", "error"]
state: dict | None
prompt_to_user: str | None
class NoDirectiveDecision(TypedDict):
kind: Literal["no_directive"]

class UpdateDecision(TypedDict):
kind: Literal["update"]
state: State

class ErrorDecision(TypedDict):
kind: Literal["error"]
message: str

Decision = NoDirectiveDecision | UpdateDecision | ErrorDecision
```

Semantics:
Expand Down
26 changes: 17 additions & 9 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,18 @@ state through the returned mapping.
Each user message produces a `Decision`.

```python
class Decision(TypedDict):
kind: Literal["no_directive", "update", "error"]
state: dict | None
prompt_to_user: str | None
class NoDirectiveDecision(TypedDict):
kind: Literal["no_directive"]

class UpdateDecision(TypedDict):
kind: Literal["update"]
state: State

class ErrorDecision(TypedDict):
kind: Literal["error"]
message: str

Decision = NoDirectiveDecision | UpdateDecision | ErrorDecision
```

Decision kinds:
Expand All @@ -138,25 +146,25 @@ Decision kinds:
| --- | --- |
| `no_directive` | no canonical directive recognized; no authoritative state change; host decides what to do next |
| `update` | authoritative state changed; host may apply downstream behavior using updated state |
| `error` | show `prompt_to_user`; do not continue normal downstream processing yet |
| `error` | show `message`; do not continue normal downstream processing yet |

Helper functions:

- `is_no_directive(decision)`
- `is_update(decision)`
- `is_error(decision)`
- `get_error_prompt(decision)`
- `get_error_message(decision)`
- `get_decision_state(decision)`

Typical use:

```python
from context_compiler import get_error_prompt, is_error, is_update
from context_compiler import get_error_message, is_error, is_update

decision = engine.step(user_input)

if is_error(decision):
show_to_user(get_error_prompt(decision))
show_to_user(get_error_message(decision))
elif is_update(decision):
apply_runtime_rules()
```
Expand Down Expand Up @@ -294,4 +302,4 @@ Public result and data object names exported at package root include:

These names are part of the public package surface. For the exact portable API
export contract used by tests and ports, see
[tests/fixtures/conformance/api/public-api-v1.json](../tests/fixtures/conformance/api/public-api-v1.json).
[tests/fixtures/conformance/api/public-api-v2.json](../tests/fixtures/conformance/api/public-api-v2.json).
6 changes: 3 additions & 3 deletions evals/swe-bench/swe-bench.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from pathlib import Path
from typing import Any, cast

from context_compiler import create_engine
from context_compiler import create_engine, is_error

RUBRIC_WEIGHTS: dict[str, int] = {
"Correct fix locus": 2,
Expand Down Expand Up @@ -701,12 +701,12 @@ def main() -> None:
error_result: dict[str, Any] | None = None
for index, directive in enumerate(task.directives):
decision = engine.step(directive)
if str(decision["kind"]) == "error":
if is_error(decision):
error_result = {
"error": "compiler_lane_error",
"directive_index": index,
"directive": directive,
"prompt_to_user": decision.get("prompt_to_user"),
"message": decision["message"],
}
break

Expand Down
4 changes: 2 additions & 2 deletions examples/03_ambiguity_with_error.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from _util import print_decision_summary, print_state_summary

from context_compiler import create_engine, get_error_prompt, is_error
from context_compiler import create_engine, get_error_message, is_error


def fake_llm(user_input: str) -> str:
Expand All @@ -25,7 +25,7 @@ def main() -> None:

if is_error(decision2):
print("Host behavior: error returned, do NOT call LLM.")
print(f"Error prompt: {get_error_prompt(decision2)}")
print(f"Error message: {get_error_message(decision2)}")
else:
fake_llm("use peanuts")
print()
Expand Down
4 changes: 2 additions & 2 deletions examples/05_llm_integration_pattern.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
State,
create_engine,
get_decision_state,
get_error_prompt,
get_error_message,
is_error,
is_no_directive,
is_update,
Expand Down Expand Up @@ -38,7 +38,7 @@ def handle_turn(engine_input: str, engine: Engine) -> None:
fake_llm(get_decision_state(decision), engine_input)
elif is_error(decision):
print("Host action: error -> show prompt, DO NOT call LLM")
print("error prompt:", get_error_prompt(decision))
print("error message:", get_error_message(decision))
print()


Expand Down
Loading