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
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:
1 parent
3cf4cefacd
commit
83804fe02a
40 files changed
+1026
-363
No files matched your search
@@ -23,7 +23,7 @@ from nisse_dev.executor import ExecutorOutcome, LLMExecutor
|
||||
from nisse_dev.llm import RoleMap
|
||||
from nisse_dev.oplog import OpLog
|
||||
from nisse_dev.planner import LLMPlanningAgent
|
||||
from nisse_dev.state import StateStore
|
||||
from nisse_dev.state import StateStore, Transcript
|
||||
from nisse_dev.tools import ToolOutcome
|
||||
from nisse_dev.transport import InMemoryTransport
|
||||
from nisse_dev.worker import SerialWorker
|
||||
@@ -36,13 +36,17 @@ ROLES = RoleMap(
|
||||
)
|
||||
|
||||
|
||||
def log(store: StateStore, actor: str = "executor") -> Transcript:
|
||||
return store.transcript("t", actor)
|
||||
|
||||
|
||||
def new_store(path: Path) -> StateStore:
|
||||
return StateStore(path / "state.db", oplog=OpLog("nisse-dev", io.StringIO()))
|
||||
|
||||
|
||||
def user_messages(store: StateStore) -> list[str]:
|
||||
return [
|
||||
e.event.text for e in store.read_events() if isinstance(e.event, UserMessage)
|
||||
e.event.text for e in store.all_events() if isinstance(e.event, UserMessage)
|
||||
]
|
||||
|
||||
|
||||
@@ -58,7 +62,7 @@ class OperatorDuringWork:
|
||||
self.says = says
|
||||
self.transport: InMemoryTransport | None = None
|
||||
|
||||
def run(self, milestone: int) -> ExecutorOutcome:
|
||||
def run(self, transcript: object, milestone: int) -> ExecutorOutcome:
|
||||
assert self.transport is not None
|
||||
self.transport.receive("don", self.says)
|
||||
if self.cancel.requested:
|
||||
@@ -158,6 +162,7 @@ class StopsBeforePlanning:
|
||||
|
||||
def plan(
|
||||
self,
|
||||
transcript: object,
|
||||
gate: str,
|
||||
project: str,
|
||||
feedback: str | None = None,
|
||||
@@ -200,7 +205,7 @@ class BlocksUntilStopped:
|
||||
self.cancel = cancel
|
||||
self.running = threading.Event()
|
||||
|
||||
def run(self, milestone: int) -> ExecutorOutcome:
|
||||
def run(self, transcript: object, milestone: int) -> ExecutorOutcome:
|
||||
self.running.set()
|
||||
for _ in range(500):
|
||||
if self.cancel.requested:
|
||||
@@ -287,10 +292,8 @@ def test_executor_stops_before_a_turn_without_calling_the_model(
|
||||
cancel = Cancel()
|
||||
cancel.set()
|
||||
client = ScriptedClient([])
|
||||
executor = LLMExecutor(
|
||||
client, new_store(tmp_path), ROLES, StopsAfterFirstCall(cancel), cancel=cancel
|
||||
)
|
||||
outcome = executor.run(1)
|
||||
executor = LLMExecutor(client, ROLES, StopsAfterFirstCall(cancel), cancel=cancel)
|
||||
outcome = executor.run(log(new_store(tmp_path)), 1)
|
||||
assert outcome == ExecutorOutcome("stopped", "before turn 1")
|
||||
assert client.calls == 0
|
||||
|
||||
@@ -305,14 +308,14 @@ def test_executor_stops_between_tool_calls_and_records_the_skipped_ones(
|
||||
ToolCall(call_id="a", tool="run_command", arguments={"command": "make"}),
|
||||
ToolCall(call_id="b", tool="write_file", arguments={"path": "x"}),
|
||||
]
|
||||
executor = LLMExecutor(ScriptedClient([calls]), store, ROLES, ws, cancel=cancel)
|
||||
outcome = executor.run(1)
|
||||
executor = LLMExecutor(ScriptedClient([calls]), ROLES, ws, cancel=cancel)
|
||||
outcome = executor.run(log(store), 1)
|
||||
|
||||
assert outcome == ExecutorOutcome("stopped", "during turn 1")
|
||||
assert ws.ran == ["run_command"]
|
||||
results = {
|
||||
e.event.call_id: e.event.result
|
||||
for e in store.read_events()
|
||||
for e in log(store).read()
|
||||
if isinstance(e.event, ToolResult)
|
||||
}
|
||||
# Every call has a result, so the transcript still renders.
|
||||
@@ -320,7 +323,7 @@ def test_executor_stops_between_tool_calls_and_records_the_skipped_ones(
|
||||
|
||||
|
||||
class StoppedDelegate:
|
||||
def run(self, *, brief: str, branch: str) -> DelegationResult:
|
||||
def run(self, transcript: object, *, brief: str, branch: str) -> DelegationResult:
|
||||
return DelegationResult("stopped", "stopped by the operator during turn 2")
|
||||
|
||||
|
||||
@@ -339,12 +342,11 @@ def test_a_stopped_delegation_stops_the_milestone(tmp_path: Path) -> None:
|
||||
|
||||
executor = LLMExecutor(
|
||||
ScriptedClient(failing),
|
||||
new_store(tmp_path),
|
||||
ROLES,
|
||||
Failing(),
|
||||
delegate=StoppedDelegate(),
|
||||
)
|
||||
outcome = executor.run(1)
|
||||
outcome = executor.run(log(new_store(tmp_path)), 1)
|
||||
assert outcome.status == "stopped"
|
||||
assert "during a delegation" in outcome.note
|
||||
|
||||
@@ -353,9 +355,9 @@ def test_planner_does_not_start_a_turn_once_stopped(tmp_path: Path) -> None:
|
||||
cancel = Cancel()
|
||||
cancel.set()
|
||||
client = ScriptedClient([])
|
||||
planner = LLMPlanningAgent(client, new_store(tmp_path), ROLES, cancel=cancel)
|
||||
planner = LLMPlanningAgent(client, ROLES, cancel=cancel)
|
||||
try:
|
||||
planner.plan("arch", "demo")
|
||||
planner.plan(log(new_store(tmp_path), "planner"), "arch", "demo")
|
||||
except PlanningStopped:
|
||||
pass
|
||||
else:
|
||||
|
||||
Reference in new issue
Block a user