263 lines
8.0 KiB
Markdown
263 lines
8.0 KiB
Markdown
# JSON Schema State Reducers Implementation Plan
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** Make `Workflow.state_schema` a normal JSON Schema object, with `reducer` as an explicit workflow extension keyword on field schemas.
|
|
|
|
**Architecture:** `StateSchema` should validate as JSON Schema first, then expose helper indexes for workflow runtime metadata. Runtime reducer lookup should compile from `properties` paths instead of requiring a separate path declaration list. Legacy `fields` inputs remain parse-only compatibility during the transition.
|
|
|
|
**Tech Stack:** Python, Pydantic v2, `jsonschema`, `wf_core` path models, pytest, basedpyright, ruff.
|
|
|
|
---
|
|
|
|
### Task 1: Add Canonical State Schema Tests
|
|
|
|
**Files:**
|
|
|
|
- Modify: `tests/core/test_nested_state_paths.py`
|
|
- Modify: `tests/core/test_schema_validation.py`
|
|
|
|
- [ ] **Step 1: Add a test for JSON Schema property reducers**
|
|
|
|
```python
|
|
def test_state_schema_uses_json_schema_properties_as_canonical_shape() -> None:
|
|
schema = StateSchema.model_validate(
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"person": {
|
|
"type": "object",
|
|
"properties": {
|
|
"name": {
|
|
"type": "string",
|
|
"description": "Display name",
|
|
"reducer": "wf.std.replace",
|
|
}
|
|
},
|
|
},
|
|
"count": {"type": "integer", "reducer": "wf.std.add"},
|
|
},
|
|
}
|
|
)
|
|
|
|
fields = schema.field_map()
|
|
assert fields["person.name"].validation_schema.type == "string"
|
|
assert fields["person.name"].reducer == ReducerRef(name="wf.std.replace")
|
|
assert fields["count"].reducer == ReducerRef(name="wf.std.add")
|
|
```
|
|
|
|
- [ ] **Step 2: Add a dump test proving the canonical output is still JSON Schema**
|
|
|
|
```python
|
|
def test_state_schema_dumps_canonical_json_schema_with_reducer_keyword() -> None:
|
|
schema = StateSchema.model_validate(
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"count": {"type": "integer", "reducer": "wf.std.add"}
|
|
},
|
|
}
|
|
)
|
|
|
|
dumped = schema.model_dump(mode="json")
|
|
assert dumped["type"] == "object"
|
|
assert dumped["properties"]["count"]["type"] == "integer"
|
|
assert dumped["properties"]["count"]["reducer"] == "wf.std.add"
|
|
Draft202012Validator.check_schema(dumped)
|
|
```
|
|
|
|
- [ ] **Step 3: Add a runtime reducer lookup test from canonical schema**
|
|
|
|
```python
|
|
def test_exact_nested_state_path_uses_reducer_from_json_schema_property() -> None:
|
|
workflow = _workflow_from_state_schema(
|
|
StateSchema.model_validate(
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"person": {
|
|
"type": "object",
|
|
"properties": {
|
|
"tags": {"type": "array", "reducer": "wf.std.append"}
|
|
},
|
|
}
|
|
},
|
|
}
|
|
)
|
|
)
|
|
state = {"person": {"tags": ["seed"]}}
|
|
|
|
write_state_value(workflow, state, "state.person.tags", ["next"])
|
|
|
|
assert state["person"]["tags"] == ["seed", "next"]
|
|
```
|
|
|
|
- [ ] **Step 4: Run focused tests and confirm failures**
|
|
|
|
Run: `uv run --with pytest pytest tests/core/test_nested_state_paths.py tests/core/test_schema_validation.py -q`
|
|
|
|
Expected: new tests fail because `StateSchema` still serializes as `fields: [...]` and reducer lookup is compiled from field declarations only.
|
|
|
|
### Task 2: Implement JSON-Schema-Native `StateSchema`
|
|
|
|
**Files:**
|
|
|
|
- Modify: `src/wf_core/models/schemas.py`
|
|
|
|
- [ ] **Step 1: Make `StateSchema` inherit JSON Schema fields directly**
|
|
|
|
`StateSchema` should expose common JSON Schema object fields:
|
|
|
|
```python
|
|
title: str | None = None
|
|
type: str | list[str] | None = "object"
|
|
properties: dict[str, Any] = Field(default_factory=dict)
|
|
required: list[str] = Field(default_factory=list)
|
|
```
|
|
|
|
- [ ] **Step 2: Preserve legacy `fields` as parse-only input**
|
|
|
|
Keep accepting:
|
|
|
|
```json
|
|
{
|
|
"fields": [
|
|
{ "path": "state.count", "type": "integer", "reducer": "wf.std.add" }
|
|
]
|
|
}
|
|
```
|
|
|
|
and:
|
|
|
|
```json
|
|
{ "fields": { "count": { "type": "integer", "reducer": "wf.std.add" } } }
|
|
```
|
|
|
|
by converting both into:
|
|
|
|
```json
|
|
{
|
|
"type": "object",
|
|
"properties": { "count": { "type": "integer", "reducer": "wf.std.add" } }
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 3: Add `field_map()` as an internal compiled index**
|
|
|
|
`field_map()` should walk explicit object `properties` and return `StateFieldDecl` values keyed by rootless state path. It must:
|
|
|
|
- include every explicit property path
|
|
- parse `reducer` with `ReducerRef`
|
|
- default missing reducer to `wf.std.replace`
|
|
- preserve `trace` and `default` workflow extension keywords
|
|
- remove workflow extension keywords from `StateFieldDecl.validation_schema`
|
|
|
|
- [ ] **Step 4: Validate JSON Schema and extension keyword types**
|
|
|
|
Use `SchemaRef`/`jsonschema` validation for the complete state schema. Add explicit validation that:
|
|
|
|
- `reducer` is a string or `ReducerRef`-compatible object
|
|
- `trace` is a boolean when present
|
|
- `default` is allowed as JSON Schema/default metadata
|
|
|
|
### Task 3: Update Artifact Reducer Extraction
|
|
|
|
**Files:**
|
|
|
|
- Modify: `src/wf_artifacts/factory.py`
|
|
|
|
- [ ] **Step 1: Extract reducer dependencies from `state_schema.properties`**
|
|
|
|
Add a helper that walks explicit JSON Schema properties and yields reducer payloads from every property schema.
|
|
|
|
- [ ] **Step 2: Keep legacy `fields` extraction only as compatibility**
|
|
|
|
If `state_schema.fields` exists in old artifacts, continue reading it. Prefer canonical `properties` when present.
|
|
|
|
- [ ] **Step 3: Add tests through existing workflow surface/artifact tests**
|
|
|
|
Use an existing artifact/dependency test and assert a reducer declared at:
|
|
|
|
```json
|
|
state_schema.properties.count.reducer
|
|
```
|
|
|
|
is included in required capabilities.
|
|
|
|
### Task 4: Update Authoring Conversion
|
|
|
|
**Files:**
|
|
|
|
- Modify: `src/wf_authoring/schemas.py`
|
|
- Modify: `tests/authoring/test_schemas.py`
|
|
|
|
- [ ] **Step 1: Attach reducer metadata directly to generated property schemas**
|
|
|
|
When `state_schema_from(BaseModel)` sees `Annotated[..., state_field(reducer=...)]`, inject `reducer` and `trace` into that property schema instead of building a separate field map.
|
|
|
|
- [ ] **Step 2: Preserve model JSON Schema as the state schema**
|
|
|
|
Return `StateSchema.model_validate(schema_with_reducer_keywords)` so generated state schema remains JSON Schema-shaped.
|
|
|
|
### Task 5: Update Docs and Examples
|
|
|
|
**Files:**
|
|
|
|
- Modify: `docs/core_state_mapping_and_merge.md`
|
|
- Modify: `docs/workflow_drafts.md`
|
|
- Modify: `docs/wf_mcp_operator_manual.md`
|
|
- Modify: `docs/wf_mcp_end_to_end_runbook.md`
|
|
- Modify: `examples/raw_canonical_workflow.py`
|
|
|
|
- [ ] **Step 1: Replace canonical `fields: [...]` examples**
|
|
|
|
Use JSON Schema:
|
|
|
|
```json
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"count": {
|
|
"type": "integer",
|
|
"description": "Counter value",
|
|
"reducer": "wf.std.add"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Document extension semantics**
|
|
|
|
State clearly that `reducer` is not standard JSON Schema behavior. JSON Schema validators ignore it; `wf_core` reads it for workflow state writes.
|
|
|
|
### Task 6: Verification
|
|
|
|
**Files:**
|
|
|
|
- All touched files
|
|
|
|
- [ ] **Step 1: Run focused tests**
|
|
|
|
Run: `uv run --with pytest pytest tests/core/test_nested_state_paths.py tests/core/test_schema_validation.py tests/authoring/test_schemas.py -q`
|
|
|
|
- [ ] **Step 2: Run full tests**
|
|
|
|
Run: `uv run --with pytest pytest -q`
|
|
|
|
- [ ] **Step 3: Run static checks**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uvx ruff check
|
|
uv run basedpyright --level error
|
|
```
|
|
|
|
---
|
|
|
|
## Self-Review
|
|
|
|
- Spec coverage: covers canonical JSON Schema state shape, reducer extension keyword, compatibility, runtime lookup, artifact dependency extraction, authoring generation, docs, and verification.
|
|
- Placeholder scan: no placeholders remain.
|
|
- Type consistency: `StateSchema`, `StateFieldDecl`, `ReducerRef`, and `SchemaRef` names match current code.
|