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

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
This commit is contained in:
dongarandClaude Opus 5.5 committed 2026-10-01 23:20:29 +00:00
1 parent 3cf4cefacd
commit 83804fe02a
40 files changed
+1026 -363

No files matched your search

+53 -25
View File
@@ -19,12 +19,13 @@ from nisse_dev.delegation import (
DELEGATION_INTENT,
Caps,
Delegation,
DelegationResult,
delegation_reconciler,
)
from nisse_dev.git import GitRepo
from nisse_dev.llm import BudgetExhausted, LLMError, UpstreamUnavailable
from nisse_dev.oplog import OpLog
from nisse_dev.state import StateStore
from nisse_dev.state import StateStore, Transcript
from nisse_dev.tools import Workspace
@@ -86,8 +87,34 @@ def ws(repo: GitRepo) -> Workspace:
return Workspace(repo.root, timeout_s=10)
ITEM = "t" # the work item these delegations run in
def log(store: StateStore) -> Transcript:
return store.transcript(ITEM, "helper")
class Bound:
"""A delegation run in the test's work item, as the executor runs it."""
def __init__(self, delegation: Delegation, store: StateStore) -> None:
self.delegation = delegation
self.store = store
def run(self, **kw: Any) -> DelegationResult:
return self.delegation.run(log(self.store), **kw)
def resume(self, intent: Any) -> DelegationResult:
return self.delegation.resume(intent)
def recover(self, notify: Any) -> DelegationResult | None:
return self.delegation.recover(notify)
def build(client: Any, store: StateStore, ws: Workspace, repo: GitRepo, **kw: Any):
return Delegation(client, store, ws, repo, model="claude-haiku-4-5", **kw)
return Bound(
Delegation(client, store, ws, repo, model="claude-haiku-4-5", **kw), store
)
def test_successful_delegation_returns_a_structured_handback(
@@ -169,7 +196,7 @@ def test_stop_discards_the_branch_and_settles_the_intent(
# The unrun call still has a result, so the transcript renders.
skipped = [
e.event
for e in store.read_events()
for e in log(store).read()
if isinstance(e.event, ToolResult) and e.event.result == STOPPED_RESULT
]
assert len(skipped) == 1
@@ -225,7 +252,7 @@ def test_events_are_checkpointed_as_they_arrive(
)
build(client, store, ws, repo).run(brief="add a", branch="d")
kinds = [type(e.event).__name__ for e in store.read_events()]
kinds = [type(e.event).__name__ for e in log(store).read()]
assert kinds == [
"DelegationStarted",
"ToolCall",
@@ -245,7 +272,7 @@ def test_composite_events_record_the_brief_caps_and_outcome(
caps = Caps(turns=7, tokens=1234, wall_clock_s=42.0)
build(client, store, ws, repo, caps=caps).run(brief="the brief", branch="d")
events = [e.event for e in store.read_events()]
events = [e.event for e in log(store).read()]
started = next(e for e in events if isinstance(e, DelegationStarted))
completed = next(e for e in events if isinstance(e, DelegationCompleted))
assert started.brief == "the brief"
@@ -356,6 +383,7 @@ def pending_delegation_intent(store: StateStore, branch: str = "d") -> Any:
DELEGATION_INTENT,
{"branch": branch, "base": "main", "brief": "finish it"},
cause="test",
work_item=ITEM, # owned by the work item it ran in
)
return store.pending_intents()[-1]
@@ -385,9 +413,9 @@ def test_clean_cut_after_a_tool_result_is_resumable(
repo.checkout_new_branch("d")
(repo.root / "partial.py").write_text("x")
repo.commit_all("partial work")
store.append_event(DelegationStarted(brief="b", role="helper", model="m", caps={}))
store.append_event(ToolCall(call_id="c", tool="write_file", arguments={}))
store.append_event(ToolResult(call_id="c", result="wrote", success=True))
log(store).append(DelegationStarted(brief="b", role="helper", model="m", caps={}))
log(store).append(ToolCall(call_id="c", tool="write_file", arguments={}))
log(store).append(ToolResult(call_id="c", result="wrote", success=True))
intent = pending_delegation_intent(store)
assert delegation_reconciler(store, repo)(intent) is True
@@ -398,8 +426,8 @@ def test_dangling_tool_call_gates(
) -> None:
# The effect of the call is unknown and not verifiable by inspection.
repo.checkout_new_branch("d")
store.append_event(DelegationStarted(brief="b", role="helper", model="m", caps={}))
store.append_event(ToolCall(call_id="c", tool="run_command", arguments={}))
log(store).append(DelegationStarted(brief="b", role="helper", model="m", caps={}))
log(store).append(ToolCall(call_id="c", tool="run_command", arguments={}))
intent = pending_delegation_intent(store)
assert delegation_reconciler(store, repo)(intent) is None
@@ -408,8 +436,8 @@ def test_dangling_tool_call_gates(
def test_missing_branch_means_nothing_started(
store: StateStore, ws: Workspace, repo: GitRepo
) -> None:
store.append_event(DelegationStarted(brief="b", role="helper", model="m", caps={}))
store.append_event(ToolResult(call_id="c", result="ok", success=True))
log(store).append(DelegationStarted(brief="b", role="helper", model="m", caps={}))
log(store).append(ToolResult(call_id="c", result="ok", success=True))
intent = pending_delegation_intent(store, branch="never-created")
assert delegation_reconciler(store, repo)(intent) is False
@@ -419,8 +447,8 @@ def test_a_finished_delegation_settles_rather_than_resumes(
store: StateStore, ws: Workspace, repo: GitRepo
) -> None:
repo.checkout_new_branch("d")
store.append_event(DelegationStarted(brief="b", role="helper", model="m", caps={}))
store.append_event(
log(store).append(DelegationStarted(brief="b", role="helper", model="m", caps={}))
log(store).append(
DelegationCompleted(
handback="done",
artifacts=[],
@@ -440,8 +468,8 @@ def test_resume_continues_the_same_delegation(
# Interrupt after one turn, then resume: no second delegation_started,
# because L7 defines a session as the delegation, not the process.
repo.checkout_new_branch("d")
store.append_event(DelegationStarted(brief="b", role="helper", model="m", caps={}))
store.append_event(ToolResult(call_id="c", result="wrote a.py", success=True))
log(store).append(DelegationStarted(brief="b", role="helper", model="m", caps={}))
log(store).append(ToolResult(call_id="c", result="wrote a.py", success=True))
intent = pending_delegation_intent(store)
client = ScriptedClient(
@@ -450,7 +478,7 @@ def test_resume_continues_the_same_delegation(
result = build(client, store, ws, repo).resume(intent)
assert result.usable
starts = [e for e in store.read_events() if isinstance(e.event, DelegationStarted)]
starts = [e for e in log(store).read() if isinstance(e.event, DelegationStarted)]
assert len(starts) == 1
assert store.pending_intents() == []
assert (repo.root / "b.py").exists()
@@ -474,8 +502,8 @@ def test_recover_resumes_a_clean_cut_with_a_notice(
store: StateStore, ws: Workspace, repo: GitRepo
) -> None:
repo.checkout_new_branch("d")
store.append_event(DelegationStarted(brief="b", role="helper", model="m", caps={}))
store.append_event(ToolResult(call_id="c", result="wrote a.py", success=True))
log(store).append(DelegationStarted(brief="b", role="helper", model="m", caps={}))
log(store).append(ToolResult(call_id="c", result="wrote a.py", success=True))
pending_delegation_intent(store)
notices: list[str] = []
@@ -493,8 +521,8 @@ def test_recover_settles_a_finished_delegation_without_resuming(
store: StateStore, ws: Workspace, repo: GitRepo
) -> None:
repo.checkout_new_branch("d")
store.append_event(DelegationStarted(brief="b", role="helper", model="m", caps={}))
store.append_event(
log(store).append(DelegationStarted(brief="b", role="helper", model="m", caps={}))
log(store).append(
DelegationCompleted(
handback="done",
artifacts=[],
@@ -514,7 +542,7 @@ def test_recover_settles_a_finished_delegation_without_resuming(
def test_recover_discards_an_intent_whose_branch_never_existed(
store: StateStore, ws: Workspace, repo: GitRepo
) -> None:
store.append_event(ToolResult(call_id="c", result="ok", success=True))
log(store).append(ToolResult(call_id="c", result="ok", success=True))
pending_delegation_intent(store, branch="never-made")
assert build(ScriptedClient([]), store, ws, repo).recover(lambda _m: None) is None
@@ -525,8 +553,8 @@ def test_recover_leaves_an_unclean_cut_for_the_boot_gate(
store: StateStore, ws: Workspace, repo: GitRepo
) -> None:
repo.checkout_new_branch("d")
store.append_event(DelegationStarted(brief="b", role="helper", model="m", caps={}))
store.append_event(ToolCall(call_id="c", tool="run_command", arguments={}))
log(store).append(DelegationStarted(brief="b", role="helper", model="m", caps={}))
log(store).append(ToolCall(call_id="c", tool="run_command", arguments={}))
pending_delegation_intent(store)
assert build(ScriptedClient([]), store, ws, repo).recover(lambda _m: None) is None
@@ -581,7 +609,7 @@ def test_tokens_spent_are_reported_not_left_at_zero(
assert result.tokens == 2000 # two calls, 1000 each
completed = next(
e.event for e in store.read_events() if isinstance(e.event, DelegationCompleted)
e.event for e in log(store).read() if isinstance(e.event, DelegationCompleted)
)
# L9 evaluates cost against outcome, which needs a real number.
assert completed.tokens == 2000