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
130 lines
4.0 KiB
Python
130 lines
4.0 KiB
Python
import io
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from nisse_dev.canonical import (
|
|
AssistantText,
|
|
DelegationCompleted,
|
|
DelegationStarted,
|
|
)
|
|
from nisse_dev.evals import UNMARKED, collect, summarise, to_jsonl
|
|
from nisse_dev.oplog import OpLog
|
|
from nisse_dev.state import StateStore
|
|
|
|
|
|
@pytest.fixture
|
|
def store(tmp_path: Path) -> StateStore:
|
|
return StateStore(tmp_path / "state.db", oplog=OpLog("nisse-dev", io.StringIO()))
|
|
|
|
|
|
def record_pair(
|
|
store: StateStore,
|
|
*,
|
|
brief: str = "add greet",
|
|
model: str = "claude-haiku-4-5",
|
|
role: str = "helper",
|
|
status: str = "handback",
|
|
) -> int:
|
|
store.transcript("t", "helper").append(
|
|
DelegationStarted(brief=brief, role=role, model=model, caps={"turns": 5})
|
|
)
|
|
store.transcript("t", "helper").append(
|
|
DelegationCompleted(
|
|
handback="what changed: greet.py",
|
|
artifacts=["greet.py"],
|
|
tokens=1200,
|
|
duration_s=9.5,
|
|
exit_status=status,
|
|
)
|
|
)
|
|
return store.all_events()[-1].id
|
|
|
|
|
|
def test_pairs_a_start_with_its_completion(store: StateStore) -> None:
|
|
record_pair(store)
|
|
[record] = collect(store)
|
|
assert record.brief == "add greet"
|
|
assert record.model == "claude-haiku-4-5"
|
|
assert record.exit_status == "handback"
|
|
assert record.artifacts == ["greet.py"]
|
|
assert record.verdict == UNMARKED
|
|
|
|
|
|
def test_unpaired_start_is_skipped(store: StateStore) -> None:
|
|
# A crash mid-delegation leaves a start with no completion; a half-formed
|
|
# record would pollute the set L9 exists to make trustworthy.
|
|
store.transcript("t", "helper").append(
|
|
DelegationStarted(brief="died", role="helper", model="m", caps={})
|
|
)
|
|
assert collect(store) == []
|
|
|
|
|
|
def test_unrelated_events_between_the_pair_are_ignored(store: StateStore) -> None:
|
|
store.transcript("t", "helper").append(
|
|
DelegationStarted(brief="b", role="helper", model="m", caps={})
|
|
)
|
|
store.transcript("t", "helper").append(AssistantText(text="working", model="m"))
|
|
store.transcript("t", "helper").append(
|
|
DelegationCompleted(
|
|
handback="done",
|
|
artifacts=[],
|
|
tokens=1,
|
|
duration_s=1.0,
|
|
exit_status="handback",
|
|
)
|
|
)
|
|
assert len(collect(store)) == 1
|
|
|
|
|
|
def test_verdicts_attach_to_the_completion(store: StateStore) -> None:
|
|
event_id = record_pair(store)
|
|
store.mark_outcome(event_id, "good", "clean handback, tests passed")
|
|
[record] = collect(store)
|
|
assert record.verdict == "good"
|
|
assert "tests passed" in record.note
|
|
|
|
|
|
def test_a_verdict_can_be_revised(store: StateStore) -> None:
|
|
# Judgements get revised; the log they describe does not.
|
|
event_id = record_pair(store)
|
|
store.mark_outcome(event_id, "good")
|
|
store.mark_outcome(event_id, "bad", "regressed later")
|
|
[record] = collect(store)
|
|
assert record.verdict == "bad"
|
|
assert record.note == "regressed later"
|
|
|
|
|
|
def test_consults_and_delegations_share_the_set(store: StateStore) -> None:
|
|
record_pair(store, role="helper")
|
|
record_pair(store, role="expert", brief="which approach?")
|
|
roles = [r.role for r in collect(store)]
|
|
assert roles == ["helper", "expert"]
|
|
|
|
|
|
def test_collect_can_start_after_a_marker(store: StateStore) -> None:
|
|
record_pair(store, brief="old")
|
|
marker = store.all_events()[-1].id
|
|
record_pair(store, brief="new")
|
|
assert [r.brief for r in collect(store, after=marker)] == ["new"]
|
|
|
|
|
|
def test_jsonl_round_trips(store: StateStore) -> None:
|
|
record_pair(store)
|
|
line = to_jsonl(collect(store))
|
|
assert json.loads(line)["model"] == "claude-haiku-4-5"
|
|
|
|
|
|
def test_summary_groups_by_model(store: StateStore) -> None:
|
|
good = record_pair(store, model="haiku")
|
|
record_pair(store, model="haiku", status="capped")
|
|
record_pair(store, model="opus")
|
|
store.mark_outcome(good, "good")
|
|
|
|
summary = summarise(collect(store))
|
|
assert summary["haiku"]["total"] == 2
|
|
assert summary["haiku"]["good"] == 1
|
|
assert summary["haiku"][UNMARKED] == 1
|
|
assert summary["opus"]["total"] == 1
|