Work items in the state store, with a transcript bound to each (M1.1) #87

Merged
dongar merged 2 commits from work-items into main 2026-10-01 23:36:36 +00:00
Collaborator

Development Plan M1.1, as discussed: work items in the state store, a transcript bound to one work item and one actor, a fresh v4 schema, and the role renames. Two commits: the rename first, then the work items.

1. Roles renamed (L9): delegate → helper, consultant → expert, in RoleMap, config keys, recorded events and tests. planner and executor stay distinct until M2.3 merges them into driver. Configs need the new keys; the example and test configs are updated here, and so is this box's bring-up config.

2. Work items (L1, L7)

  • Schema v4: a work_items table (id, scope, title, status, outcome, position, data) and an agent row whose active_item points at one; NULL means idle.
    • Canonical events carry work_item and actor, and intents carry work_item.
    • Older stores are refused with a clear message: there's no production data, so no migration. The migration mechanism stays; synthetic-version tests still exercise it.
  • store.transcript(item, actor) returns a view that reads only that work item and writes with both.
    • The planner, executor, delegation and expert desk receive one per call from the orchestrator, and no longer hold the store. A component therefore can't read another task's events (#78's planner read all of them) or write without attribution.
    • Delegation keeps the store only for boot recovery, and finds an interrupted delegation's work item from its intent.
    • store.all_events() remains for readers that span items: evaluation capture and dumps.
  • The orchestrator:
    • opens a work item on use, and closes it with its outcome (completed or abandoned);
    • carries what the operator said while idle into the new item, as copies noting where they came from (the log is never edited). So "task first, then use", run 2's flow, still reaches the planner (D1).
    • control.py keeps its state machine through get_state() / transition(), now backed by the active work item, until M2.6 retires it.
  • Expert consult budget is counted per work item; there's a new test for it.
  • nisse_dev.dump shows the work items, plus each event's item and actor.

Tests

  • test_work_items.py (10): transcript scoping, attribution, an unknown actor refused, intent ownership, items and the active pointer surviving a restart, outcomes, idle messages carried with their origin, and at the orchestrator level: a second task never sees the first, each task its own item with its outcome, and a task sent before use reaching the planner.
  • The existing suites now pass a transcript per call, or read through all_events() where they check across items. The oplog literal-format test now documents the work_item.open / work_item.switch lines.
  • Results: 514 unit tests and the integration suite (19 passed, 7 skipped) pass; lint and format clean; Quint scenarios pass. The model doesn't change, since the loop's behaviour doesn't.
  • Smoke run: scripts/bringup.sh start booted the agent on a fresh v4 store, with the v3 store set aside.

Not in this PR (per the plan): parking and switching between items (the schema allows it), and per-role views with fact events, which are M1.2.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE

Development Plan **M1.1**, as discussed: work items in the state store, a transcript bound to one work item and one actor, a fresh v4 schema, and the role renames. Two commits: the rename first, then the work items. **1. Roles renamed** (L9): `delegate` → `helper`, `consultant` → `expert`, in `RoleMap`, config keys, recorded events and tests. `planner` and `executor` stay distinct until M2.3 merges them into `driver`. Configs need the new keys; the example and test configs are updated here, and so is this box's bring-up config. **2. Work items** (L1, L7) - **Schema v4:** a `work_items` table (id, scope, title, status, outcome, position, data) and an `agent` row whose `active_item` points at one; `NULL` means idle. - Canonical events carry `work_item` and `actor`, and intents carry `work_item`. - **Older stores are refused** with a clear message: there's no production data, so no migration. The migration mechanism stays; synthetic-version tests still exercise it. - **`store.transcript(item, actor)`** returns a view that reads only that work item and writes with both. - The planner, executor, delegation and expert desk **receive one per call** from the orchestrator, and no longer hold the store. A component therefore can't read another task's events (#78's planner read all of them) or write without attribution. - Delegation keeps the store only for boot recovery, and finds an interrupted delegation's work item from its intent. - `store.all_events()` remains for readers that span items: evaluation capture and dumps. - **The orchestrator:** - opens a work item on `use`, and closes it with its outcome (**completed** or **abandoned**); - carries what the operator said while idle into the new item, as copies noting where they came from (the log is never edited). So "task first, then `use`", run 2's flow, still reaches the planner (D1). - `control.py` keeps its state machine through `get_state()` / `transition()`, now backed by the active work item, until M2.6 retires it. - **Expert consult budget** is counted per work item; there's a new test for it. - **`nisse_dev.dump`** shows the work items, plus each event's item and actor. **Tests** - **`test_work_items.py`** (10): transcript scoping, attribution, an unknown actor refused, intent ownership, items and the active pointer surviving a restart, outcomes, idle messages carried with their origin, and at the orchestrator level: **a second task never sees the first**, each task its own item with its outcome, and a task sent before `use` reaching the planner. - **The existing suites** now pass a transcript per call, or read through `all_events()` where they check across items. The oplog literal-format test now documents the `work_item.open` / `work_item.switch` lines. - **Results:** 514 unit tests and the integration suite (19 passed, 7 skipped) pass; lint and format clean; Quint scenarios pass. The model doesn't change, since the loop's behaviour doesn't. - **Smoke run:** `scripts/bringup.sh start` booted the agent on a fresh v4 store, with the v3 store set aside. **Not in this PR** (per the plan): parking and switching between items (the schema allows it), and per-role views with `fact` events, which are M1.2. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
dev-agent added 2 commits 2026-10-01 23:21:38 +00:00
Architecture L9's role names, for the two roles with a direct
counterpart: the delegation role is now 'helper' and the ask_expert role
'expert', in RoleMap, configuration (roles.helper, roles.expert),
recorded events and tests. 'planner' and 'executor' stay distinct for
now; they become 'driver' when the new loop merges them (Development
Plan M2.3).

Configurations must rename their keys: nisse-dev.example.yaml and
tests/integration/nisse-test.yaml are updated here.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
Put work items in the state store, and bind the log to them (M1.1)
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m14s
83804fe02a
Development Plan M1.1, as discussed.

- Schema v4: a work_items table (id, scope, title, status, outcome,
  position, data) and an agent row whose active_item points at one
  (NULL: idle). Canonical events and intents carry their work item, and
  events their actor. Older stores are refused with a clear message:
  there was no production data, so no migration (the mechanism stays).
- The log is reached through a Transcript bound to one work item and
  one actor: store.transcript(item, actor) reads only that item and
  writes with both. The planner, executor, delegation and expert desk
  get one per call from the orchestrator instead of holding the store,
  so a component can neither read another task's events (the planner
  read every earlier task's, #78) nor write without attribution.
  store.all_events() is kept for readers that span items (evaluation
  capture, dumps). Intents are begun through the transcript or with
  work_item=, so each belongs to its item.
- The orchestrator opens a work item on 'use', closes it with its
  outcome (completed or abandoned), and carries what the operator said
  while idle into the new item as copies with their origin, so a task
  sent before 'use' is still planned from (D1). control.py keeps its
  state machine through get_state()/transition(), now over the active
  work item, until M2.6 retires it.
- nisse_dev.dump shows work items and each event's item and actor.

Tests: test_work_items.py (scoping, attribution, intent ownership,
restart, outcomes, carried messages, a second task never seeing the
first); the suites updated for the per-call transcript.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
dongar approved these changes 2026-10-01 23:36:32 +00:00
dongar merged commit 51e144e26b into main 2026-10-01 23:36:36 +00:00
dongar deleted branch work-items 2026-10-01 23:36:36 +00:00
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dev/nisse#87