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.
- chp_adapter_planning-0.24.0/.gitignore +16 -0
- chp_adapter_planning-0.24.0/PKG-INFO +40 -0
- chp_adapter_planning-0.24.0/README.md +17 -0
- chp_adapter_planning-0.24.0/chp_adapter_planning/__init__.py +3 -0
- chp_adapter_planning-0.24.0/chp_adapter_planning/adapter.py +275 -0
- chp_adapter_planning-0.24.0/pyproject.toml +41 -0
- chp_adapter_planning-0.24.0/tests/test_planning.py +335 -0
|
@@ -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,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"
|