context-compiler 0.4.2__tar.gz → 0.4.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. {context_compiler-0.4.2 → context_compiler-0.4.4}/AGENTS.md +1 -0
  2. {context_compiler-0.4.2 → context_compiler-0.4.4}/PKG-INFO +29 -80
  3. {context_compiler-0.4.2 → context_compiler-0.4.4}/README.md +28 -79
  4. {context_compiler-0.4.2 → context_compiler-0.4.4}/demos/04_llm_tool_governance.py +2 -3
  5. {context_compiler-0.4.2 → context_compiler-0.4.4}/demos/06_context_compaction.py +2 -3
  6. {context_compiler-0.4.2 → context_compiler-0.4.4}/demos/common.py +3 -4
  7. {context_compiler-0.4.2 → context_compiler-0.4.4}/docs/DirectiveGrammarSpec.md +13 -3
  8. {context_compiler-0.4.2 → context_compiler-0.4.4}/examples/01_persistent_guardrails.py +2 -3
  9. {context_compiler-0.4.2 → context_compiler-0.4.4}/examples/04_tool_governance_denylist.py +2 -3
  10. {context_compiler-0.4.2 → context_compiler-0.4.4}/pyproject.toml +1 -1
  11. context_compiler-0.4.4/src/context_compiler/__init__.py +25 -0
  12. {context_compiler-0.4.2 → context_compiler-0.4.4}/src/context_compiler/engine.py +11 -10
  13. {context_compiler-0.4.2 → context_compiler-0.4.4}/tests/test_engine.py +64 -19
  14. {context_compiler-0.4.2 → context_compiler-0.4.4}/uv.lock +1 -1
  15. context_compiler-0.4.2/src/context_compiler/__init__.py +0 -7
  16. {context_compiler-0.4.2 → context_compiler-0.4.4}/.github/workflows/ci.yml +0 -0
  17. {context_compiler-0.4.2 → context_compiler-0.4.4}/.github/workflows/publish-pypi.yml +0 -0
  18. {context_compiler-0.4.2 → context_compiler-0.4.4}/.gitignore +0 -0
  19. {context_compiler-0.4.2 → context_compiler-0.4.4}/.pre-commit-config.yaml +0 -0
  20. {context_compiler-0.4.2 → context_compiler-0.4.4}/CONTRIBUTING.md +0 -0
  21. {context_compiler-0.4.2 → context_compiler-0.4.4}/LICENSE +0 -0
  22. {context_compiler-0.4.2 → context_compiler-0.4.4}/demos/01_llm_ambiguity_block.py +0 -0
  23. {context_compiler-0.4.2 → context_compiler-0.4.4}/demos/02_llm_constraint_drift.py +0 -0
  24. {context_compiler-0.4.2 → context_compiler-0.4.4}/demos/03_llm_correction_replacement.py +0 -0
  25. {context_compiler-0.4.2 → context_compiler-0.4.4}/demos/05_llm_prompt_drift.py +0 -0
  26. {context_compiler-0.4.2 → context_compiler-0.4.4}/demos/07_llm_prompt_engineering_comparison.py +0 -0
  27. {context_compiler-0.4.2 → context_compiler-0.4.4}/demos/README.md +0 -0
  28. {context_compiler-0.4.2 → context_compiler-0.4.4}/demos/__init__.py +0 -0
  29. {context_compiler-0.4.2 → context_compiler-0.4.4}/demos/llm_client.py +0 -0
  30. {context_compiler-0.4.2 → context_compiler-0.4.4}/demos/run_demo.py +0 -0
  31. {context_compiler-0.4.2 → context_compiler-0.4.4}/docs/DescriptionAndMilestones.md +0 -0
  32. {context_compiler-0.4.2 → context_compiler-0.4.4}/docs/README.md +0 -0
  33. {context_compiler-0.4.2 → context_compiler-0.4.4}/examples/02_configuration_and_correction.py +0 -0
  34. {context_compiler-0.4.2 → context_compiler-0.4.4}/examples/03_ambiguity_with_clarification.py +0 -0
  35. {context_compiler-0.4.2 → context_compiler-0.4.4}/examples/05_llm_integration_pattern.py +0 -0
  36. {context_compiler-0.4.2 → context_compiler-0.4.4}/examples/06_transcript_replay.py +0 -0
  37. {context_compiler-0.4.2 → context_compiler-0.4.4}/examples/README.md +0 -0
  38. {context_compiler-0.4.2 → context_compiler-0.4.4}/examples/_util.py +0 -0
  39. {context_compiler-0.4.2 → context_compiler-0.4.4}/src/context_compiler/const.py +0 -0
  40. {context_compiler-0.4.2 → context_compiler-0.4.4}/src/context_compiler/repl.py +0 -0
  41. {context_compiler-0.4.2 → context_compiler-0.4.4}/tests/test_04_grammar_edge_cases.py +0 -0
  42. {context_compiler-0.4.2 → context_compiler-0.4.4}/tests/test_04_llm_tool_governance.py +0 -0
  43. {context_compiler-0.4.2 → context_compiler-0.4.4}/tests/test_07_llm_prompt_engineering_comparison.py +0 -0
  44. {context_compiler-0.4.2 → context_compiler-0.4.4}/tests/test_examples.py +0 -0
  45. {context_compiler-0.4.2 → context_compiler-0.4.4}/tests/test_llm_client.py +0 -0
  46. {context_compiler-0.4.2 → context_compiler-0.4.4}/tests/test_llm_demos.py +0 -0
  47. {context_compiler-0.4.2 → context_compiler-0.4.4}/tests/test_properties.py +0 -0
  48. {context_compiler-0.4.2 → context_compiler-0.4.4}/tests/test_repl.py +0 -0
  49. {context_compiler-0.4.2 → context_compiler-0.4.4}/tests/test_repl_properties.py +0 -0
  50. {context_compiler-0.4.2 → context_compiler-0.4.4}/tests/test_run_demo.py +0 -0
  51. {context_compiler-0.4.2 → context_compiler-0.4.4}/tests/test_smoke.py +0 -0
  52. {context_compiler-0.4.2 → context_compiler-0.4.4}/tests/test_transcript_replay.py +0 -0
@@ -61,6 +61,7 @@ Prefer modern typing syntax:
61
61
  - PR descriptions should include:
62
62
  - what changed
63
63
  - why the change was needed
64
+ - A dedicated "Validation" section in PR text is optional and not required.
64
65
  - Keep PR scope aligned to the requested task; if scope grows, ask for guidance before expanding.
65
66
 
66
67
  ## CI
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: context-compiler
3
- Version: 0.4.2
3
+ Version: 0.4.4
4
4
  Summary: Deterministic conversational state engine for LLM applications.
5
5
  Project-URL: Homepage, https://github.com/rlippmann/context-compiler
6
6
  Project-URL: Repository, https://github.com/rlippmann/context-compiler
@@ -73,19 +73,7 @@ User sets a constraint once:
73
73
  User: don't use peanuts
74
74
  ```
75
75
 
76
- State becomes:
77
-
78
- ```json
79
- {
80
- "facts": {
81
- "focus.primary": null
82
- },
83
- "policies": {
84
- "prohibit": ["peanuts"]
85
- },
86
- "version": 1
87
- }
88
- ```
76
+ Outcome: prohibited items now include `"peanuts"`.
89
77
 
90
78
  Later in the conversation:
91
79
 
@@ -157,35 +145,35 @@ else:
157
145
 
158
146
  | API | Description |
159
147
  |---|---|
160
- | `create_engine(...)` | Create a new compiler engine, optionally with replacement initial state. |
148
+ | `create_engine(state=None)` | Create a new compiler engine; optional `state` provides initial authoritative state (validated/canonicalized). |
161
149
  | `step(user_input)` | Parse one user turn and return a deterministic `Decision`. |
162
150
  | `compile_transcript(messages)` | Replay a transcript from a fresh engine and return either final state or a confirmation prompt. |
163
151
  | `engine.apply_transcript(messages)` | Replay a transcript onto the current engine state and return either final state or a confirmation prompt. |
164
- | `engine.state` | Read or replace full in-memory authoritative state. |
152
+ | `engine.state` | Read current authoritative in-memory state snapshot. |
153
+ | `get_focus_value(state)` | Read the current focus value from a state snapshot. |
154
+ | `get_prohibited_items(state)` | Read prohibited items from a state snapshot. |
165
155
  | `export_json()` | Export current state as JSON for persistence/transport. |
166
- | `import_json(payload)` | Load state from exported JSON payload. |
156
+ | `import_json(payload)` | Load/restore state from exported JSON payload. |
167
157
 
168
158
  ---
169
159
 
170
160
  ## State Model
171
161
 
172
- The compiler maintains an authoritative state:
173
-
174
- ```json
175
- {
176
- "facts": {
177
- "focus.primary": null
178
- },
179
- "policies": {
180
- "prohibit": []
181
- },
182
- "version": 1
183
- }
184
- ```
162
+ The compiler maintains an authoritative state snapshot.
163
+ Hosts should treat this state as structured application data and avoid coupling
164
+ to internal field names or nested layout.
185
165
 
186
166
  ## State Access and Persistence
187
167
 
188
- Hosts may inspect or replace in-memory state (`engine.state`) or persist it using `export_json()` and `import_json()`. State changes occur only through directives processed by `step()`. Storage is managed by the host application.
168
+ Hosts can provide initial state at engine creation (`create_engine(state=...)`),
169
+ read current in-memory state via `engine.state`, and persist/restore via
170
+ `export_json()` and `import_json()`. Semantic state mutations occur through
171
+ directives processed by `step()`. Storage is managed by the host application.
172
+
173
+ Use the returned state snapshot as structured host input for prompt
174
+ construction, policy enforcement, or replay/storage workflows.
175
+ For host code that needs typed reads without direct nested key lookups, use
176
+ `get_focus_value(state)` and `get_prohibited_items(state)`.
189
177
 
190
178
  ### Transcript Replay
191
179
 
@@ -200,7 +188,7 @@ Transcript replay compiles conversational history by reusing the same determinis
200
188
 
201
189
  ### Fact Schema
202
190
 
203
- The current schema contains a single exclusive slot: `facts["focus.primary"]`.
191
+ The current behavior includes one exclusive focus value.
204
192
  This demonstrates deterministic fact replacement and correction behavior.
205
193
  Richer schemas may be introduced in future releases.
206
194
 
@@ -221,9 +209,9 @@ User: use corn oil
221
209
  ```
222
210
 
223
211
  Result:
224
- facts.focus.primary = "corn oil"
212
+ the current focus value becomes `"corn oil"`
225
213
 
226
- Because `focus.primary` is an exclusive slot, later `use ...` directives replace earlier values.
214
+ Because the focus value is exclusive (last write wins), later `use ...` directives replace earlier values.
227
215
 
228
216
  This may differ from human expectations, where the intent may be interpreted as additive (e.g., ingredient + cooking medium). The current schema models a single focus value. See [issue #45](https://github.com/rlippmann/context-compiler/issues/45) for discussion.
229
217
 
@@ -238,14 +226,7 @@ User: don't use peanuts
238
226
  ```
239
227
 
240
228
  Result:
241
-
242
- ```json
243
- {
244
- "policies": {
245
- "prohibit": ["peanuts"]
246
- }
247
- }
248
- ```
229
+ prohibited items include `"peanuts"`.
249
230
 
250
231
  Fact configuration:
251
232
 
@@ -254,10 +235,7 @@ User: use vegetarian curry
254
235
  ```
255
236
 
256
237
  State update:
257
-
258
- ```text
259
- facts.focus.primary = "vegetarian curry"
260
- ```
238
+ the current focus value becomes `"vegetarian curry"`
261
239
 
262
240
  Correction:
263
241
 
@@ -266,10 +244,7 @@ User: actually vegan curry
266
244
  ```
267
245
 
268
246
  Result:
269
-
270
- ```text
271
- facts.focus.primary = "vegan curry"
272
- ```
247
+ the current focus value becomes `"vegan curry"`
273
248
 
274
249
  Ambiguous mutation:
275
250
 
@@ -289,40 +264,14 @@ No state mutation occurs until confirmation.
289
264
 
290
265
  Two explicit reset commands are supported:
291
266
 
292
- - `reset policies` clears `policies.prohibit` but preserves the current fact (`facts["focus.primary"]`)
267
+ - `reset policies` clears prohibited items but preserves the current focus value
293
268
  - `clear state` resets the full state to initial values
294
269
 
295
270
  Example:
296
271
 
297
- Before:
298
-
299
- ```json
300
- {
301
- "facts": {"focus.primary": "vegetarian curry"},
302
- "policies": {"prohibit": ["peanuts"]},
303
- "version": 1
304
- }
305
- ```
306
-
307
- After `reset policies`:
308
-
309
- ```json
310
- {
311
- "facts": {"focus.primary": "vegetarian curry"},
312
- "policies": {"prohibit": []},
313
- "version": 1
314
- }
315
- ```
316
-
317
- After `clear state`:
318
-
319
- ```json
320
- {
321
- "facts": {"focus.primary": null},
322
- "policies": {"prohibit": []},
323
- "version": 1
324
- }
325
- ```
272
+ - If current focus is `"vegetarian curry"` and prohibited items include `"peanuts"`:
273
+ - after `reset policies`, prohibited items are empty and focus remains `"vegetarian curry"`.
274
+ - after `clear state`, both focus and prohibited items return to initial defaults.
326
275
 
327
276
  ---
328
277
 
@@ -44,19 +44,7 @@ User sets a constraint once:
44
44
  User: don't use peanuts
45
45
  ```
46
46
 
47
- State becomes:
48
-
49
- ```json
50
- {
51
- "facts": {
52
- "focus.primary": null
53
- },
54
- "policies": {
55
- "prohibit": ["peanuts"]
56
- },
57
- "version": 1
58
- }
59
- ```
47
+ Outcome: prohibited items now include `"peanuts"`.
60
48
 
61
49
  Later in the conversation:
62
50
 
@@ -128,35 +116,35 @@ else:
128
116
 
129
117
  | API | Description |
130
118
  |---|---|
131
- | `create_engine(...)` | Create a new compiler engine, optionally with replacement initial state. |
119
+ | `create_engine(state=None)` | Create a new compiler engine; optional `state` provides initial authoritative state (validated/canonicalized). |
132
120
  | `step(user_input)` | Parse one user turn and return a deterministic `Decision`. |
133
121
  | `compile_transcript(messages)` | Replay a transcript from a fresh engine and return either final state or a confirmation prompt. |
134
122
  | `engine.apply_transcript(messages)` | Replay a transcript onto the current engine state and return either final state or a confirmation prompt. |
135
- | `engine.state` | Read or replace full in-memory authoritative state. |
123
+ | `engine.state` | Read current authoritative in-memory state snapshot. |
124
+ | `get_focus_value(state)` | Read the current focus value from a state snapshot. |
125
+ | `get_prohibited_items(state)` | Read prohibited items from a state snapshot. |
136
126
  | `export_json()` | Export current state as JSON for persistence/transport. |
137
- | `import_json(payload)` | Load state from exported JSON payload. |
127
+ | `import_json(payload)` | Load/restore state from exported JSON payload. |
138
128
 
139
129
  ---
140
130
 
141
131
  ## State Model
142
132
 
143
- The compiler maintains an authoritative state:
144
-
145
- ```json
146
- {
147
- "facts": {
148
- "focus.primary": null
149
- },
150
- "policies": {
151
- "prohibit": []
152
- },
153
- "version": 1
154
- }
155
- ```
133
+ The compiler maintains an authoritative state snapshot.
134
+ Hosts should treat this state as structured application data and avoid coupling
135
+ to internal field names or nested layout.
156
136
 
157
137
  ## State Access and Persistence
158
138
 
159
- Hosts may inspect or replace in-memory state (`engine.state`) or persist it using `export_json()` and `import_json()`. State changes occur only through directives processed by `step()`. Storage is managed by the host application.
139
+ Hosts can provide initial state at engine creation (`create_engine(state=...)`),
140
+ read current in-memory state via `engine.state`, and persist/restore via
141
+ `export_json()` and `import_json()`. Semantic state mutations occur through
142
+ directives processed by `step()`. Storage is managed by the host application.
143
+
144
+ Use the returned state snapshot as structured host input for prompt
145
+ construction, policy enforcement, or replay/storage workflows.
146
+ For host code that needs typed reads without direct nested key lookups, use
147
+ `get_focus_value(state)` and `get_prohibited_items(state)`.
160
148
 
161
149
  ### Transcript Replay
162
150
 
@@ -171,7 +159,7 @@ Transcript replay compiles conversational history by reusing the same determinis
171
159
 
172
160
  ### Fact Schema
173
161
 
174
- The current schema contains a single exclusive slot: `facts["focus.primary"]`.
162
+ The current behavior includes one exclusive focus value.
175
163
  This demonstrates deterministic fact replacement and correction behavior.
176
164
  Richer schemas may be introduced in future releases.
177
165
 
@@ -192,9 +180,9 @@ User: use corn oil
192
180
  ```
193
181
 
194
182
  Result:
195
- facts.focus.primary = "corn oil"
183
+ the current focus value becomes `"corn oil"`
196
184
 
197
- Because `focus.primary` is an exclusive slot, later `use ...` directives replace earlier values.
185
+ Because the focus value is exclusive (last write wins), later `use ...` directives replace earlier values.
198
186
 
199
187
  This may differ from human expectations, where the intent may be interpreted as additive (e.g., ingredient + cooking medium). The current schema models a single focus value. See [issue #45](https://github.com/rlippmann/context-compiler/issues/45) for discussion.
200
188
 
@@ -209,14 +197,7 @@ User: don't use peanuts
209
197
  ```
210
198
 
211
199
  Result:
212
-
213
- ```json
214
- {
215
- "policies": {
216
- "prohibit": ["peanuts"]
217
- }
218
- }
219
- ```
200
+ prohibited items include `"peanuts"`.
220
201
 
221
202
  Fact configuration:
222
203
 
@@ -225,10 +206,7 @@ User: use vegetarian curry
225
206
  ```
226
207
 
227
208
  State update:
228
-
229
- ```text
230
- facts.focus.primary = "vegetarian curry"
231
- ```
209
+ the current focus value becomes `"vegetarian curry"`
232
210
 
233
211
  Correction:
234
212
 
@@ -237,10 +215,7 @@ User: actually vegan curry
237
215
  ```
238
216
 
239
217
  Result:
240
-
241
- ```text
242
- facts.focus.primary = "vegan curry"
243
- ```
218
+ the current focus value becomes `"vegan curry"`
244
219
 
245
220
  Ambiguous mutation:
246
221
 
@@ -260,40 +235,14 @@ No state mutation occurs until confirmation.
260
235
 
261
236
  Two explicit reset commands are supported:
262
237
 
263
- - `reset policies` clears `policies.prohibit` but preserves the current fact (`facts["focus.primary"]`)
238
+ - `reset policies` clears prohibited items but preserves the current focus value
264
239
  - `clear state` resets the full state to initial values
265
240
 
266
241
  Example:
267
242
 
268
- Before:
269
-
270
- ```json
271
- {
272
- "facts": {"focus.primary": "vegetarian curry"},
273
- "policies": {"prohibit": ["peanuts"]},
274
- "version": 1
275
- }
276
- ```
277
-
278
- After `reset policies`:
279
-
280
- ```json
281
- {
282
- "facts": {"focus.primary": "vegetarian curry"},
283
- "policies": {"prohibit": []},
284
- "version": 1
285
- }
286
- ```
287
-
288
- After `clear state`:
289
-
290
- ```json
291
- {
292
- "facts": {"focus.primary": null},
293
- "policies": {"prohibit": []},
294
- "version": 1
295
- }
296
- ```
243
+ - If current focus is `"vegetarian curry"` and prohibited items include `"peanuts"`:
244
+ - after `reset policies`, prohibited items are empty and focus remains `"vegetarian curry"`.
245
+ - after `clear state`, both focus and prohibited items return to initial defaults.
297
246
 
298
247
  ---
299
248
 
@@ -2,8 +2,7 @@
2
2
 
3
3
  import re
4
4
 
5
- from context_compiler import create_engine
6
- from context_compiler.const import POLICY_PROHIBIT, STATE_POLICIES
5
+ from context_compiler import create_engine, get_prohibited_items
7
6
  from demos.common import (
8
7
  build_baseline_messages,
9
8
  build_mediated_messages,
@@ -72,7 +71,7 @@ def main() -> None:
72
71
  baseline_output = complete_messages(baseline_messages)
73
72
  print_model_output("Baseline", baseline_output)
74
73
 
75
- prohibited = engine.state[STATE_POLICIES][POLICY_PROHIBIT]
74
+ prohibited = get_prohibited_items(engine.state)
76
75
  candidate_tools = ["docker", "kubectl"]
77
76
  filtered_tools = [tool for tool in candidate_tools if tool not in prohibited]
78
77
  if is_verbose():
@@ -1,7 +1,6 @@
1
1
  """Demo 6: host-side prompt replacement from authoritative compiled state."""
2
2
 
3
- from context_compiler import compile_transcript
4
- from context_compiler.const import FOCUS_PRIMARY, STATE_FACTS
3
+ from context_compiler import compile_transcript, get_focus_value
5
4
  from demos.common import is_verbose, print_info_report
6
5
 
7
6
  DEMO_NAME = "06_context_compaction — superseded directives eliminated"
@@ -44,7 +43,7 @@ def _compile_focus(turns: list[str]) -> str:
44
43
  messages: list[dict[str, object]] = [{"role": "user", "content": turn} for turn in turns]
45
44
  result = compile_transcript(messages)
46
45
  assert result["kind"] == "state"
47
- compiled_focus = result["state"][STATE_FACTS][FOCUS_PRIMARY]
46
+ compiled_focus = get_focus_value(result["state"])
48
47
  assert compiled_focus is not None
49
48
  return compiled_focus
50
49
 
@@ -5,8 +5,7 @@ import os
5
5
  import re
6
6
  from typing import Any, TypedDict
7
7
 
8
- from context_compiler import Decision, State
9
- from context_compiler.const import FOCUS_PRIMARY, POLICY_PROHIBIT, STATE_FACTS, STATE_POLICIES
8
+ from context_compiler import Decision, State, get_focus_value, get_prohibited_items
10
9
  from demos.llm_client import Message
11
10
 
12
11
  VERBOSE_ENV_VAR = "CONTEXT_COMPILER_DEMO_VERBOSE"
@@ -179,8 +178,8 @@ def consume_last_info_report() -> InfoReport | None:
179
178
 
180
179
 
181
180
  def build_compiled_system_prompt(state: State) -> str:
182
- focus_value = state[STATE_FACTS][FOCUS_PRIMARY]
183
- prohibit = state[STATE_POLICIES][POLICY_PROHIBIT]
181
+ focus_value = get_focus_value(state)
182
+ prohibit = get_prohibited_items(state)
184
183
  prohibit_text = ", ".join(prohibit) if prohibit else "(none)"
185
184
  focus_text = focus_value if focus_value is not None else "(unset)"
186
185
  return (
@@ -43,6 +43,15 @@ The host:
43
43
  - Displays clarification prompts
44
44
  - Calls the LLM when allowed
45
45
  - Formats prompts using provided state
46
+ - May read state snapshots directly, but should prefer public helper accessors where available.
47
+
48
+ Current helpers:
49
+ - `get_focus_value(state)`
50
+ - `get_prohibited_items(state)`
51
+
52
+ These helpers are read-only conveniences for state snapshots to reduce direct
53
+ coupling to nested layout. They do not modify compiler state and are not
54
+ semantic/compiler primitives.
46
55
 
47
56
  ### 4. Decision API Contract
48
57
 
@@ -291,11 +300,12 @@ directive parsing, and does not mutate state.
291
300
  Adding duplicate policy is a no-op.
292
301
  Policies stored in sorted lexical order.
293
302
 
294
- Administrative state replacement is also supported through public host APIs:
295
- - `engine.state = ...` (object replacement)
303
+ Administrative state initialization/replacement is supported through:
304
+ - constructor input (`create_engine(state=...)` / `Engine(state=...)`) for initial load
296
305
  - `engine.import_json(payload)` (JSON replacement)
297
306
 
298
- Both replacement paths clear pending clarification state and must behave like live state for subsequent `step()` calls.
307
+ Import-based replacement clears pending clarification state and must behave like
308
+ live state for subsequent `step()` calls.
299
309
 
300
310
  ### 10. Context Serialization Contract
301
311
 
@@ -2,12 +2,11 @@
2
2
 
3
3
  from _util import print_json
4
4
 
5
- from context_compiler import State, create_engine
6
- from context_compiler.const import POLICY_PROHIBIT, STATE_POLICIES
5
+ from context_compiler import State, create_engine, get_prohibited_items
7
6
 
8
7
 
9
8
  def build_prompt(state: State, user_input: str) -> str:
10
- prohibit = state[STATE_POLICIES][POLICY_PROHIBIT]
9
+ prohibit = get_prohibited_items(state)
11
10
  prohibit_text = ", ".join(prohibit) if prohibit else "(none)"
12
11
  return (
13
12
  "System: Follow authoritative conversation state.\n"
@@ -4,8 +4,7 @@ from dataclasses import dataclass
4
4
 
5
5
  from _util import print_json
6
6
 
7
- from context_compiler import create_engine
8
- from context_compiler.const import POLICY_PROHIBIT, STATE_POLICIES
7
+ from context_compiler import create_engine, get_prohibited_items
9
8
 
10
9
 
11
10
  @dataclass
@@ -35,7 +34,7 @@ def main() -> None:
35
34
  print()
36
35
 
37
36
  print("Host-side tool denylist behavior:")
38
- prohibit = state[STATE_POLICIES][POLICY_PROHIBIT]
37
+ prohibit = get_prohibited_items(state)
39
38
  tools = [Tool("docker"), Tool("kubectl")]
40
39
  for tool in tools:
41
40
  if tool.name in prohibit:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "context-compiler"
7
- version = "0.4.2"
7
+ version = "0.4.4"
8
8
  description = "Deterministic conversational state engine for LLM applications."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -0,0 +1,25 @@
1
+ from importlib.metadata import version
2
+
3
+ from .engine import (
4
+ ApplyResult,
5
+ Decision,
6
+ Engine,
7
+ State,
8
+ compile_transcript,
9
+ create_engine,
10
+ get_focus_value,
11
+ get_prohibited_items,
12
+ )
13
+
14
+ __version__ = version("context-compiler")
15
+
16
+ __all__ = [
17
+ "ApplyResult",
18
+ "Decision",
19
+ "Engine",
20
+ "State",
21
+ "compile_transcript",
22
+ "create_engine",
23
+ "get_focus_value",
24
+ "get_prohibited_items",
25
+ ]
@@ -123,6 +123,16 @@ def compile_transcript(messages: list[dict[str, object]]) -> ApplyResult:
123
123
  return engine.apply_transcript(messages)
124
124
 
125
125
 
126
+ def get_focus_value(state: State) -> str | None:
127
+ """Return the current exclusive focus value from a state snapshot."""
128
+ return state[STATE_FACTS][FOCUS_PRIMARY]
129
+
130
+
131
+ def get_prohibited_items(state: State) -> list[str]:
132
+ """Return prohibited items from a state snapshot as a defensive list copy."""
133
+ return list(state[STATE_POLICIES][POLICY_PROHIBIT])
134
+
135
+
126
136
  class Engine:
127
137
  """Deterministic state engine implementing directive semantics.
128
138
 
@@ -132,7 +142,7 @@ class Engine:
132
142
  directive input to ``step()``.
133
143
  - Host code should not rely on imperative helpers such as
134
144
  ``reset_policies()`` or ``clear_state()``.
135
- - State may be administratively replaced via ``engine.state = ...`` and
145
+ - State may be administratively replaced via constructor input and
136
146
  ``engine.import_json(...)``.
137
147
  """
138
148
 
@@ -153,15 +163,6 @@ class Engine:
153
163
  """Return a defensive copy of the current authoritative in-memory state."""
154
164
  return deepcopy(self._state)
155
165
 
156
- @state.setter
157
- def state(self, value: State) -> None:
158
- """Replace authoritative in-memory state from a supplied object.
159
-
160
- The supplied value is validated and canonicalized. Replacement is full,
161
- and pending clarification state is cleared.
162
- """
163
- self._replace_state(_load_state_obj(value))
164
-
165
166
  def export_json(self) -> str:
166
167
  """Serialize authoritative state for persistence or transport."""
167
168
  return json.dumps(self._state, sort_keys=True, separators=(",", ":"))
@@ -2,7 +2,7 @@ import json
2
2
 
3
3
  import pytest
4
4
 
5
- from context_compiler import create_engine
5
+ from context_compiler import create_engine, get_focus_value, get_prohibited_items
6
6
  from context_compiler.engine import DecisionKind, Engine
7
7
 
8
8
 
@@ -20,6 +20,23 @@ def test_state_getter_returns_defensive_copy() -> None:
20
20
  assert engine.state["facts"]["focus.primary"] is None
21
21
 
22
22
 
23
+ def test_get_focus_value_reads_current_focus_from_state_snapshot() -> None:
24
+ engine = create_engine()
25
+ engine.step("use Nord Stage 4")
26
+
27
+ assert get_focus_value(engine.state) == "Nord Stage 4"
28
+
29
+
30
+ def test_get_prohibited_items_returns_defensive_list_copy() -> None:
31
+ engine = create_engine()
32
+ engine.step("don't use docker")
33
+
34
+ prohibited = get_prohibited_items(engine.state)
35
+ prohibited.append("kubernetes")
36
+
37
+ assert get_prohibited_items(engine.state) == ["docker"]
38
+
39
+
23
40
  def test_export_json_returns_complete_representation_of_state() -> None:
24
41
  engine = create_engine()
25
42
  engine.step("use Nord Stage 4")
@@ -195,7 +212,7 @@ def test_create_engine_with_state_initializes_from_normalized_state() -> None:
195
212
  },
196
213
  ),
197
214
  (
198
- "setter",
215
+ "import_json",
199
216
  {
200
217
  "facts": {"focus.primary": None},
201
218
  "policies": {"prohibit": "docker"},
@@ -212,22 +229,36 @@ def test_object_state_replacement_paths_reject_invalid_state(path: str, bad_stat
212
229
 
213
230
  engine = create_engine()
214
231
  with pytest.raises(ValueError):
215
- engine.state = bad_state
232
+ engine.import_json(json.dumps(bad_state))
216
233
 
217
234
 
218
- def test_state_setter_replaces_state_and_clears_pending_clarification() -> None:
235
+ def test_state_property_is_read_only() -> None:
236
+ engine = create_engine()
237
+ with pytest.raises(AttributeError):
238
+ engine.state = {
239
+ "facts": {"focus.primary": None},
240
+ "policies": {"prohibit": []},
241
+ "version": 1,
242
+ }
243
+
244
+
245
+ def test_import_json_replaces_state_and_clears_pending_clarification() -> None:
219
246
  engine = create_engine()
220
247
  decision = engine.step("no use docker")
221
248
  assert decision["kind"] == "clarify"
222
249
 
223
- engine.state = {
224
- "facts": {"focus.primary": "Nord Stage 4"},
225
- "policies": {"prohibit": ["kubernetes", "docker", "docker"]},
226
- "version": 1,
227
- }
250
+ engine.import_json(
251
+ json.dumps(
252
+ {
253
+ "facts": {"focus.primary": "Nord Stage 4"},
254
+ "policies": {"prohibit": ["kubernetes", "docker", "docker"]},
255
+ "version": 1,
256
+ }
257
+ )
258
+ )
228
259
 
229
- decision_after_setter = engine.step("yes")
230
- assert decision_after_setter["kind"] == "passthrough"
260
+ decision_after_import = engine.step("yes")
261
+ assert decision_after_import["kind"] == "passthrough"
231
262
  assert engine.state == {
232
263
  "facts": {"focus.primary": "Nord Stage 4"},
233
264
  "policies": {"prohibit": ["docker", "kubernetes"]},
@@ -235,7 +266,7 @@ def test_state_setter_replaces_state_and_clears_pending_clarification() -> None:
235
266
  }
236
267
 
237
268
 
238
- def test_constructor_setter_and_import_json_share_normalization_behavior() -> None:
269
+ def test_constructor_and_import_json_share_normalization_behavior() -> None:
239
270
  raw_state = {
240
271
  "facts": {"focus.primary": " MacBook M3` "},
241
272
  "policies": {"prohibit": ["kubernetes", "docker", "docker"]},
@@ -244,13 +275,10 @@ def test_constructor_setter_and_import_json_share_normalization_behavior() -> No
244
275
 
245
276
  from_ctor = Engine(state=json.loads(json.dumps(raw_state)))
246
277
 
247
- from_setter = create_engine()
248
- from_setter.state = json.loads(json.dumps(raw_state))
249
-
250
278
  from_import = create_engine()
251
279
  from_import.import_json(json.dumps(raw_state))
252
280
 
253
- assert from_ctor.state == from_setter.state == from_import.state
281
+ assert from_ctor.state == from_import.state
254
282
 
255
283
 
256
284
  def test_import_json_clears_pending_clarification_state() -> None:
@@ -881,9 +909,9 @@ def test_pending_clarification_rejected_by_explicit_no_is_passthrough() -> None:
881
909
  ("path", "bad_state"),
882
910
  [
883
911
  ("constructor", []),
884
- ("setter", "not-a-dict"),
912
+ ("import_json", "not-a-dict"),
885
913
  ("constructor", {"facts": [], "policies": {"prohibit": []}, "version": 1}),
886
- ("setter", {"facts": {"focus.primary": None}, "policies": [], "version": 1}),
914
+ ("import_json", {"facts": {"focus.primary": None}, "policies": [], "version": 1}),
887
915
  (
888
916
  "constructor",
889
917
  {"facts": {"focus.primary": 123}, "policies": {"prohibit": []}, "version": 1},
@@ -898,7 +926,7 @@ def test_object_state_paths_reject_malformed_state_inputs(path: str, bad_state:
898
926
 
899
927
  engine = create_engine()
900
928
  with pytest.raises(ValueError):
901
- engine.state = bad_state
929
+ engine.import_json(json.dumps(bad_state))
902
930
 
903
931
 
904
932
  def test_allow_suffix_removes_existing_prohibition() -> None:
@@ -909,3 +937,20 @@ def test_allow_suffix_removes_existing_prohibition() -> None:
909
937
 
910
938
  assert decision["kind"] == "update"
911
939
  assert engine.state["policies"]["prohibit"] == []
940
+
941
+
942
+ def test_correction_payload_that_invokes_other_directive_family_is_clarified_without_mutation() -> (
943
+ None
944
+ ):
945
+ engine = create_engine()
946
+ engine.step("use Nord Stage 4")
947
+ before = engine.state
948
+
949
+ decision = engine.step("actually don't use docker")
950
+
951
+ assert decision["kind"] == "clarify"
952
+ assert (
953
+ decision["prompt_to_user"]
954
+ == "Your directive mixes multiple directive types. Please provide one."
955
+ )
956
+ assert engine.state == before
@@ -53,7 +53,7 @@ wheels = [
53
53
 
54
54
  [[package]]
55
55
  name = "context-compiler"
56
- version = "0.4.2"
56
+ version = "0.4.4"
57
57
  source = { editable = "." }
58
58
 
59
59
  [package.optional-dependencies]
@@ -1,7 +0,0 @@
1
- from importlib.metadata import version
2
-
3
- from .engine import ApplyResult, Decision, Engine, State, compile_transcript, create_engine
4
-
5
- __version__ = version("context-compiler")
6
-
7
- __all__ = ["ApplyResult", "Decision", "Engine", "State", "compile_transcript", "create_engine"]