chp-adapter-planning 0.24.0__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.
@@ -0,0 +1,16 @@
1
+ node_modules/
2
+ dist/
3
+ *.egg-info/
4
+ __pycache__/
5
+ .pytest_cache/
6
+ .chp/
7
+ .DS_Store
8
+ *.tgz
9
+ .env
10
+ .env.*
11
+ *.pyc
12
+ .python-version
13
+ .coverage
14
+ .hypothesis/
15
+ .chp-agent/
16
+ *.sqlite
@@ -0,0 +1,40 @@
1
+ Metadata-Version: 2.4
2
+ Name: chp-adapter-planning
3
+ Version: 0.24.0
4
+ Summary: CHP capability adapter — observable agent cognition (planning and reflection)
5
+ Author: Auxo
6
+ License: Apache-2.0
7
+ Keywords: adapter,capability-host-protocol,chp,cognition,planning
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Requires-Python: >=3.10
18
+ Requires-Dist: chp-core>=0.7.0
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
21
+ Requires-Dist: pytest>=8.0; extra == 'dev'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # chp-adapter-planning
25
+
26
+ CHP capability adapter for observable agent cognition: planning and reflection.
27
+
28
+ ## Capabilities
29
+
30
+ | Capability | Risk | Description |
31
+ |---|---|---|
32
+ | `chp.adapters.planning.create_plan` | medium | Declare an agent plan with ordered steps |
33
+ | `chp.adapters.planning.step_update` | low | Record step started / completed / failed |
34
+ | `chp.adapters.planning.revise` | medium | Record a plan revision with optional new steps |
35
+ | `chp.adapters.planning.reflect` | low | Structured reflection with optional evaluation score |
36
+
37
+ ## Evidence Events
38
+
39
+ `plan_created`, `plan_step_started`, `plan_step_completed`, `plan_revised`,
40
+ `reflection_started`, `outcome_scored`, `reflection_completed`
@@ -0,0 +1,17 @@
1
+ # chp-adapter-planning
2
+
3
+ CHP capability adapter for observable agent cognition: planning and reflection.
4
+
5
+ ## Capabilities
6
+
7
+ | Capability | Risk | Description |
8
+ |---|---|---|
9
+ | `chp.adapters.planning.create_plan` | medium | Declare an agent plan with ordered steps |
10
+ | `chp.adapters.planning.step_update` | low | Record step started / completed / failed |
11
+ | `chp.adapters.planning.revise` | medium | Record a plan revision with optional new steps |
12
+ | `chp.adapters.planning.reflect` | low | Structured reflection with optional evaluation score |
13
+
14
+ ## Evidence Events
15
+
16
+ `plan_created`, `plan_step_started`, `plan_step_completed`, `plan_revised`,
17
+ `reflection_started`, `outcome_scored`, `reflection_completed`
@@ -0,0 +1,3 @@
1
+ from chp_adapter_planning.adapter import PlanningAdapter, PlanningConfig
2
+
3
+ __all__ = ["PlanningAdapter", "PlanningConfig"]
@@ -0,0 +1,275 @@
1
+ """PlanningAdapter — observable agent cognition as CHP capabilities.
2
+
3
+ Evidence hygiene (MUST PRESERVE):
4
+ * Plan intent and step descriptions — included (they are declared agent cognition,
5
+ not secrets or diff content).
6
+ * Revision reasons — included (declared governance metadata).
7
+ * Reflection content — included (declared agent reasoning; the whole point of
8
+ the capability is to make cognition observable).
9
+
10
+ Four capabilities:
11
+
12
+ * ``planning.create_plan`` — declare a plan with ordered steps; emits plan_created
13
+ * ``planning.step_update`` — record step started / completed / failed
14
+ * ``planning.revise`` — record a plan revision with optional added steps
15
+ * ``planning.reflect`` — record a structured reflection with optional score
16
+ """
17
+ from __future__ import annotations
18
+
19
+ from dataclasses import dataclass
20
+ from typing import Any
21
+
22
+ from chp_core import BaseAdapter, capability
23
+ from chp_core.types import (
24
+ EvaluationResult,
25
+ PlanDescriptor,
26
+ PlanStep,
27
+ new_id,
28
+ )
29
+
30
+
31
+ @dataclass
32
+ class PlanningConfig:
33
+ """Configuration for PlanningAdapter (currently stateless; reserved for future use)."""
34
+
35
+
36
+ class PlanningAdapter(BaseAdapter):
37
+ """Observable agent cognition — planning and reflection as governed capabilities."""
38
+
39
+ def __init__(self, config: PlanningConfig | None = None) -> None:
40
+ self._config = config or PlanningConfig()
41
+
42
+ @capability(
43
+ id="chp.adapters.planning.create_plan",
44
+ emits=['plan_created'],
45
+ version="0.1.0",
46
+ category="agent_operations",
47
+ risk="medium",
48
+ description=(
49
+ "Declare an agent plan: intent + ordered steps. "
50
+ "Emits plan_created evidence. Returns the full PlanDescriptor."
51
+ ),
52
+ input_schema={
53
+ "type": "object",
54
+ "required": ["intent"],
55
+ "properties": {
56
+ "plan_id": {
57
+ "type": "string",
58
+ "description": "Stable plan identifier; auto-generated if omitted.",
59
+ },
60
+ "intent": {"type": "string", "minLength": 1},
61
+ "steps": {
62
+ "type": "array",
63
+ "items": {
64
+ "type": "object",
65
+ "required": ["step_id", "description"],
66
+ "properties": {
67
+ "step_id": {"type": "string"},
68
+ "description": {"type": "string"},
69
+ "capability_id": {"type": ["string", "null"]},
70
+ },
71
+ "additionalProperties": False,
72
+ },
73
+ "default": [],
74
+ },
75
+ "parent_correlation_id": {"type": "string"},
76
+ "metadata": {"type": "object"},
77
+ },
78
+ "additionalProperties": False,
79
+ },
80
+ )
81
+ async def create_plan(self, ctx: Any, payload: dict) -> dict:
82
+ plan_id = payload.get("plan_id") or new_id("plan")
83
+ intent = payload["intent"]
84
+ raw_steps = payload.get("steps") or []
85
+ steps = [
86
+ PlanStep(
87
+ step_id=s["step_id"],
88
+ description=s["description"],
89
+ capability_id=s.get("capability_id"),
90
+ )
91
+ for s in raw_steps
92
+ ]
93
+ descriptor = PlanDescriptor(
94
+ plan_id=plan_id,
95
+ intent=intent,
96
+ steps=steps,
97
+ parent_correlation_id=payload.get("parent_correlation_id"),
98
+ metadata=dict(payload.get("metadata") or {}),
99
+ )
100
+ ctx.emit("plan_created", {
101
+ "plan_id": plan_id,
102
+ "intent": intent,
103
+ "step_count": len(steps),
104
+ "steps": [
105
+ {"step_id": s.step_id, "description": s.description}
106
+ for s in steps
107
+ ],
108
+ })
109
+ return descriptor.to_dict()
110
+
111
+ @capability(
112
+ id="chp.adapters.planning.step_update",
113
+ version="0.1.0",
114
+ category="agent_operations",
115
+ risk="low",
116
+ description=(
117
+ "Record a plan step transition: started → completed or failed. "
118
+ "Emits plan_step_started or plan_step_completed."
119
+ ),
120
+ input_schema={
121
+ "type": "object",
122
+ "required": ["plan_id", "step_id", "status"],
123
+ "properties": {
124
+ "plan_id": {"type": "string"},
125
+ "step_id": {"type": "string"},
126
+ "status": {
127
+ "type": "string",
128
+ "enum": ["started", "completed", "failed"],
129
+ },
130
+ "result": {
131
+ "type": "object",
132
+ "description": "Structured result (for status=completed).",
133
+ },
134
+ "error": {
135
+ "type": "string",
136
+ "description": "Error message (for status=failed).",
137
+ },
138
+ },
139
+ "additionalProperties": False,
140
+ },
141
+ )
142
+ async def step_update(self, ctx: Any, payload: dict) -> dict:
143
+ plan_id = payload["plan_id"]
144
+ step_id = payload["step_id"]
145
+ status = payload["status"]
146
+
147
+ if status == "started":
148
+ ctx.emit("plan_step_started", {"plan_id": plan_id, "step_id": step_id})
149
+ else:
150
+ ev: dict[str, Any] = {
151
+ "plan_id": plan_id,
152
+ "step_id": step_id,
153
+ "status": status,
154
+ }
155
+ if status == "completed" and (result := payload.get("result")):
156
+ ev["result"] = result
157
+ if status == "failed" and (error := payload.get("error")):
158
+ ev["error"] = error
159
+ ctx.emit("plan_step_completed", ev)
160
+
161
+ return {"plan_id": plan_id, "step_id": step_id, "status": status}
162
+
163
+ @capability(
164
+ id="chp.adapters.planning.revise",
165
+ version="0.1.0",
166
+ category="agent_operations",
167
+ risk="medium",
168
+ description=(
169
+ "Record a plan revision: why it changed and any added steps. "
170
+ "Emits plan_revised evidence."
171
+ ),
172
+ input_schema={
173
+ "type": "object",
174
+ "required": ["plan_id", "reason"],
175
+ "properties": {
176
+ "plan_id": {"type": "string"},
177
+ "reason": {"type": "string", "minLength": 1},
178
+ "added_steps": {
179
+ "type": "array",
180
+ "items": {
181
+ "type": "object",
182
+ "required": ["step_id", "description"],
183
+ "properties": {
184
+ "step_id": {"type": "string"},
185
+ "description": {"type": "string"},
186
+ },
187
+ "additionalProperties": False,
188
+ },
189
+ "default": [],
190
+ },
191
+ },
192
+ "additionalProperties": False,
193
+ },
194
+ )
195
+ async def revise(self, ctx: Any, payload: dict) -> dict:
196
+ plan_id = payload["plan_id"]
197
+ reason = payload["reason"]
198
+ added_steps = payload.get("added_steps") or []
199
+ ctx.emit("plan_revised", {
200
+ "plan_id": plan_id,
201
+ "reason": reason,
202
+ "added_step_count": len(added_steps),
203
+ "added_steps": [
204
+ {"step_id": s["step_id"], "description": s["description"]}
205
+ for s in added_steps
206
+ ],
207
+ })
208
+ return {"plan_id": plan_id, "added_step_count": len(added_steps)}
209
+
210
+ @capability(
211
+ id="chp.adapters.planning.reflect",
212
+ version="0.1.0",
213
+ category="agent_operations",
214
+ risk="low",
215
+ description=(
216
+ "Record a structured reflection. Emits reflection_started, "
217
+ "outcome_scored (if score provided), and reflection_completed."
218
+ ),
219
+ input_schema={
220
+ "type": "object",
221
+ "required": ["subject", "content"],
222
+ "properties": {
223
+ "subject": {"type": "string", "minLength": 1},
224
+ "content": {"type": "string"},
225
+ "score": {
226
+ "type": "number",
227
+ "minimum": 0.0,
228
+ "maximum": 1.0,
229
+ "description": "Normalised evaluation score 0–1.",
230
+ },
231
+ "rubric": {"type": "string"},
232
+ "evaluator": {
233
+ "type": "string",
234
+ "description": "'model' | 'human' | 'automated'",
235
+ "default": "model",
236
+ },
237
+ "evidence_refs": {
238
+ "type": "array",
239
+ "items": {"type": "string"},
240
+ "description": "Event IDs cited by this evaluation.",
241
+ },
242
+ },
243
+ "additionalProperties": False,
244
+ },
245
+ )
246
+ async def reflect(self, ctx: Any, payload: dict) -> dict:
247
+ subject = payload["subject"]
248
+ content = payload["content"]
249
+ score = payload.get("score")
250
+ rubric = payload.get("rubric", "")
251
+ evaluator = payload.get("evaluator", "model")
252
+ evidence_refs = list(payload.get("evidence_refs") or [])
253
+
254
+ ctx.emit("reflection_started", {"subject": subject})
255
+
256
+ if score is not None:
257
+ evaluation = EvaluationResult(
258
+ score=score,
259
+ rubric=rubric,
260
+ evaluator=evaluator,
261
+ evidence_refs=evidence_refs,
262
+ )
263
+ ctx.emit("outcome_scored", {"subject": subject, **evaluation.to_dict()})
264
+
265
+ ctx.emit("reflection_completed", {
266
+ "subject": subject,
267
+ "content": content,
268
+ "content_parts": 1,
269
+ })
270
+
271
+ return {
272
+ "subject": subject,
273
+ "content_parts": 1,
274
+ "scored": score is not None,
275
+ }
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.25"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "chp-adapter-planning"
7
+ version = "0.24.0"
8
+ description = "CHP capability adapter — observable agent cognition (planning and reflection)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "Apache-2.0" }
12
+ authors = [{ name = "Auxo" }]
13
+ keywords = ["chp", "capability-host-protocol", "planning", "cognition", "adapter"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: Apache Software License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Topic :: Software Development :: Libraries :: Python Modules",
24
+ ]
25
+ dependencies = [
26
+ "chp-core>=0.7.0",
27
+ ]
28
+
29
+ [project.entry-points."chp.adapters"]
30
+ planning = "chp_adapter_planning:PlanningAdapter"
31
+
32
+ [project.optional-dependencies]
33
+ dev = ["pytest>=8.0", "pytest-asyncio>=0.23"]
34
+
35
+ [tool.pytest.ini_options]
36
+ testpaths = ["tests"]
37
+ pythonpath = ["."]
38
+ asyncio_mode = "auto"
39
+
40
+ [tool.hatch.build.targets.wheel]
41
+ packages = ["chp_adapter_planning"]
@@ -0,0 +1,335 @@
1
+ """Tests for PlanningAdapter.
2
+
3
+ All tests are stateless — no subprocess or external backend required.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import pytest
8
+ from chp_core import LocalCapabilityHost, register_adapter
9
+ from chp_core.store import SQLiteEvidenceStore
10
+
11
+ from chp_adapter_planning import PlanningAdapter, PlanningConfig
12
+
13
+
14
+ # ---------------------------------------------------------------------------
15
+ # Helpers
16
+ # ---------------------------------------------------------------------------
17
+
18
+ def _make_host() -> LocalCapabilityHost:
19
+ adapter = PlanningAdapter()
20
+ host = LocalCapabilityHost(store=SQLiteEvidenceStore(":memory:"))
21
+ register_adapter(host, adapter)
22
+ return host
23
+
24
+
25
+ def _events(host: LocalCapabilityHost) -> list[dict]:
26
+ return [e for e in host.store.all() if "capability_uri" not in e.get("payload", {})]
27
+
28
+
29
+ def _event_types(host: LocalCapabilityHost) -> list[str]:
30
+ return [e["event_type"] for e in _events(host)]
31
+
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # Capability registration
35
+ # ---------------------------------------------------------------------------
36
+
37
+ class TestRegistration:
38
+ async def test_four_capabilities_registered(self):
39
+ host = _make_host()
40
+ # keys are "id:version" — check id prefix
41
+ keys = " ".join(host._capabilities.keys())
42
+ assert "chp.adapters.planning.create_plan" in keys
43
+ assert "chp.adapters.planning.step_update" in keys
44
+ assert "chp.adapters.planning.revise" in keys
45
+ assert "chp.adapters.planning.reflect" in keys
46
+
47
+
48
+ # ---------------------------------------------------------------------------
49
+ # create_plan
50
+ # ---------------------------------------------------------------------------
51
+
52
+ class TestCreatePlan:
53
+ async def test_success(self):
54
+ host = _make_host()
55
+ result = await host.ainvoke("chp.adapters.planning.create_plan", {
56
+ "intent": "implement Tier 3 adapters",
57
+ "steps": [
58
+ {"step_id": "s1", "description": "write planning adapter"},
59
+ {"step_id": "s2", "description": "write delegation adapter"},
60
+ ],
61
+ })
62
+ assert result.outcome == "success"
63
+ assert result.data["intent"] == "implement Tier 3 adapters"
64
+ assert len(result.data["steps"]) == 2
65
+
66
+ async def test_autogenerated_plan_id(self):
67
+ host = _make_host()
68
+ result = await host.ainvoke("chp.adapters.planning.create_plan", {
69
+ "intent": "do something",
70
+ })
71
+ assert result.outcome == "success"
72
+ assert result.data["plan_id"].startswith("plan_")
73
+
74
+ async def test_explicit_plan_id_preserved(self):
75
+ host = _make_host()
76
+ result = await host.ainvoke("chp.adapters.planning.create_plan", {
77
+ "plan_id": "my-plan-001",
78
+ "intent": "do something",
79
+ })
80
+ assert result.data["plan_id"] == "my-plan-001"
81
+
82
+ async def test_emits_plan_created(self):
83
+ host = _make_host()
84
+ await host.ainvoke("chp.adapters.planning.create_plan", {
85
+ "intent": "do something",
86
+ "steps": [{"step_id": "a", "description": "first step"}],
87
+ })
88
+ types = _event_types(host)
89
+ assert "plan_created" in types
90
+
91
+ async def test_plan_created_contains_step_count(self):
92
+ host = _make_host()
93
+ await host.ainvoke("chp.adapters.planning.create_plan", {
94
+ "intent": "x",
95
+ "steps": [
96
+ {"step_id": "s1", "description": "d1"},
97
+ {"step_id": "s2", "description": "d2"},
98
+ {"step_id": "s3", "description": "d3"},
99
+ ],
100
+ })
101
+ ev = next(e for e in _events(host) if e["event_type"] == "plan_created")
102
+ assert ev["payload"]["step_count"] == 3
103
+
104
+ async def test_no_steps_allowed(self):
105
+ host = _make_host()
106
+ result = await host.ainvoke("chp.adapters.planning.create_plan", {
107
+ "intent": "minimal plan",
108
+ })
109
+ assert result.outcome == "success"
110
+ assert result.data["steps"] == []
111
+
112
+ async def test_missing_intent_denied(self):
113
+ host = _make_host()
114
+ result = await host.ainvoke("chp.adapters.planning.create_plan", {})
115
+ assert result.outcome == "denied"
116
+
117
+ async def test_unknown_field_denied(self):
118
+ host = _make_host()
119
+ result = await host.ainvoke("chp.adapters.planning.create_plan", {
120
+ "intent": "x",
121
+ "bogus": True,
122
+ })
123
+ assert result.outcome == "denied"
124
+
125
+ async def test_parent_correlation_id_round_trips(self):
126
+ host = _make_host()
127
+ result = await host.ainvoke("chp.adapters.planning.create_plan", {
128
+ "intent": "child plan",
129
+ "parent_correlation_id": "corr_abc123",
130
+ })
131
+ assert result.data["parent_correlation_id"] == "corr_abc123"
132
+
133
+
134
+ # ---------------------------------------------------------------------------
135
+ # step_update
136
+ # ---------------------------------------------------------------------------
137
+
138
+ class TestStepUpdate:
139
+ async def test_started_emits_plan_step_started(self):
140
+ host = _make_host()
141
+ result = await host.ainvoke("chp.adapters.planning.step_update", {
142
+ "plan_id": "p1",
143
+ "step_id": "s1",
144
+ "status": "started",
145
+ })
146
+ assert result.outcome == "success"
147
+ assert "plan_step_started" in _event_types(host)
148
+
149
+ async def test_completed_emits_plan_step_completed(self):
150
+ host = _make_host()
151
+ await host.ainvoke("chp.adapters.planning.step_update", {
152
+ "plan_id": "p1",
153
+ "step_id": "s1",
154
+ "status": "completed",
155
+ "result": {"files_written": 3},
156
+ })
157
+ types = _event_types(host)
158
+ assert "plan_step_started" not in types
159
+ assert "plan_step_completed" in types
160
+
161
+ async def test_failed_emits_plan_step_completed_with_error(self):
162
+ host = _make_host()
163
+ await host.ainvoke("chp.adapters.planning.step_update", {
164
+ "plan_id": "p1",
165
+ "step_id": "s1",
166
+ "status": "failed",
167
+ "error": "subprocess exited 1",
168
+ })
169
+ ev = next(e for e in _events(host) if e["event_type"] == "plan_step_completed")
170
+ assert ev["payload"]["status"] == "failed"
171
+ assert ev["payload"]["error"] == "subprocess exited 1"
172
+
173
+ async def test_returns_ids_and_status(self):
174
+ host = _make_host()
175
+ result = await host.ainvoke("chp.adapters.planning.step_update", {
176
+ "plan_id": "p1", "step_id": "s2", "status": "completed",
177
+ })
178
+ assert result.data == {"plan_id": "p1", "step_id": "s2", "status": "completed"}
179
+
180
+ async def test_invalid_status_denied(self):
181
+ host = _make_host()
182
+ result = await host.ainvoke("chp.adapters.planning.step_update", {
183
+ "plan_id": "p1", "step_id": "s1", "status": "unknown",
184
+ })
185
+ assert result.outcome == "denied"
186
+
187
+ async def test_missing_plan_id_denied(self):
188
+ host = _make_host()
189
+ result = await host.ainvoke("chp.adapters.planning.step_update", {
190
+ "step_id": "s1", "status": "started",
191
+ })
192
+ assert result.outcome == "denied"
193
+
194
+
195
+ # ---------------------------------------------------------------------------
196
+ # revise
197
+ # ---------------------------------------------------------------------------
198
+
199
+ class TestRevise:
200
+ async def test_emits_plan_revised(self):
201
+ host = _make_host()
202
+ result = await host.ainvoke("chp.adapters.planning.revise", {
203
+ "plan_id": "p1",
204
+ "reason": "scope changed after retro",
205
+ "added_steps": [
206
+ {"step_id": "s_new", "description": "add safety tests"},
207
+ ],
208
+ })
209
+ assert result.outcome == "success"
210
+ assert "plan_revised" in _event_types(host)
211
+
212
+ async def test_added_step_count_in_event(self):
213
+ host = _make_host()
214
+ await host.ainvoke("chp.adapters.planning.revise", {
215
+ "plan_id": "p1",
216
+ "reason": "found gap",
217
+ "added_steps": [
218
+ {"step_id": "x1", "description": "new step 1"},
219
+ {"step_id": "x2", "description": "new step 2"},
220
+ ],
221
+ })
222
+ ev = next(e for e in _events(host) if e["event_type"] == "plan_revised")
223
+ assert ev["payload"]["added_step_count"] == 2
224
+
225
+ async def test_reason_in_event(self):
226
+ host = _make_host()
227
+ await host.ainvoke("chp.adapters.planning.revise", {
228
+ "plan_id": "p1",
229
+ "reason": "scope changed",
230
+ })
231
+ ev = next(e for e in _events(host) if e["event_type"] == "plan_revised")
232
+ assert ev["payload"]["reason"] == "scope changed"
233
+
234
+ async def test_no_added_steps_allowed(self):
235
+ host = _make_host()
236
+ result = await host.ainvoke("chp.adapters.planning.revise", {
237
+ "plan_id": "p1",
238
+ "reason": "minor adjustment",
239
+ })
240
+ assert result.outcome == "success"
241
+ assert result.data["added_step_count"] == 0
242
+
243
+ async def test_missing_reason_denied(self):
244
+ host = _make_host()
245
+ result = await host.ainvoke("chp.adapters.planning.revise", {
246
+ "plan_id": "p1",
247
+ })
248
+ assert result.outcome == "denied"
249
+
250
+
251
+ # ---------------------------------------------------------------------------
252
+ # reflect
253
+ # ---------------------------------------------------------------------------
254
+
255
+ class TestReflect:
256
+ async def test_emits_started_and_completed(self):
257
+ host = _make_host()
258
+ result = await host.ainvoke("chp.adapters.planning.reflect", {
259
+ "subject": "Tier 3 implementation",
260
+ "content": "All three adapters built and tested successfully.",
261
+ })
262
+ assert result.outcome == "success"
263
+ types = _event_types(host)
264
+ assert "reflection_started" in types
265
+ assert "reflection_completed" in types
266
+
267
+ async def test_no_outcome_scored_without_score(self):
268
+ host = _make_host()
269
+ await host.ainvoke("chp.adapters.planning.reflect", {
270
+ "subject": "x",
271
+ "content": "y",
272
+ })
273
+ assert "outcome_scored" not in _event_types(host)
274
+
275
+ async def test_outcome_scored_emitted_when_score_provided(self):
276
+ host = _make_host()
277
+ await host.ainvoke("chp.adapters.planning.reflect", {
278
+ "subject": "code quality",
279
+ "content": "Adapter follows established patterns.",
280
+ "score": 0.9,
281
+ "rubric": "adherence to CHP adapter pattern",
282
+ "evaluator": "model",
283
+ })
284
+ types = _event_types(host)
285
+ assert "outcome_scored" in types
286
+
287
+ async def test_outcome_scored_has_score_value(self):
288
+ host = _make_host()
289
+ await host.ainvoke("chp.adapters.planning.reflect", {
290
+ "subject": "x",
291
+ "content": "y",
292
+ "score": 0.75,
293
+ })
294
+ ev = next(e for e in _events(host) if e["event_type"] == "outcome_scored")
295
+ assert ev["payload"]["score"] == pytest.approx(0.75)
296
+
297
+ async def test_returns_scored_flag(self):
298
+ host = _make_host()
299
+ result = await host.ainvoke("chp.adapters.planning.reflect", {
300
+ "subject": "x", "content": "y", "score": 0.5,
301
+ })
302
+ assert result.data["scored"] is True
303
+
304
+ async def test_returns_scored_false_when_no_score(self):
305
+ host = _make_host()
306
+ result = await host.ainvoke("chp.adapters.planning.reflect", {
307
+ "subject": "x", "content": "y",
308
+ })
309
+ assert result.data["scored"] is False
310
+
311
+ async def test_missing_content_denied(self):
312
+ host = _make_host()
313
+ result = await host.ainvoke("chp.adapters.planning.reflect", {
314
+ "subject": "x",
315
+ })
316
+ assert result.outcome == "denied"
317
+
318
+ async def test_score_out_of_range_denied(self):
319
+ host = _make_host()
320
+ result = await host.ainvoke("chp.adapters.planning.reflect", {
321
+ "subject": "x",
322
+ "content": "y",
323
+ "score": 1.5,
324
+ })
325
+ assert result.outcome == "denied"
326
+
327
+ async def test_evidence_refs_accepted(self):
328
+ host = _make_host()
329
+ result = await host.ainvoke("chp.adapters.planning.reflect", {
330
+ "subject": "x",
331
+ "content": "y",
332
+ "score": 0.8,
333
+ "evidence_refs": ["ev_001", "ev_002"],
334
+ })
335
+ assert result.outcome == "success"