Add ask_expert and eval capture (A14/A16) — step 7c
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m7s
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m7s
Completes step 7. - expert.py: one stateless call for a question needing no repo access. Structured output is contracted at the prompt level, since vendors differ on JSON enforcement, so the reply is parsed and validated locally with one retry — tolerating a fenced code block, because models wrap JSON habitually, but not missing fields - The per-task budget is counted from the canonical log rather than memory, so it survives a restart and cannot hand a circling executor a fresh allowance. Exhaustion raises rather than answering: a caller out of consults should get on with the work or escalate - Consults are recorded as delegation_started/completed with role consultant. A16 asks for a "consult/delegation eval set", and reusing the composite events gets consult evaluation for free rather than inventing a parallel record - evals.py pairs starts with completions into eval records, with verdicts kept outside the canonical log: the log records what happened, a verdict is a later judgement about it, and judgements get revised. Schema v3 adds the outcomes table, so a v1 store now traverses two migrations — a better exercise of the hook than one - ask_expert is offered to the executor as a tool only when a desk is configured, in addition to the observable escalation triggers and never instead of them (A12). Consulting is not mutating, so a completion claim backed only by consults is still challenged Found live: passing an empty message list and putting the question in the system prompt fails outright — the Messages API requires at least one non-system message. The question now travels as a synthesised user message that is never appended to the log, which keeps the consult stateless while remaining a valid request. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
dd22eda528
commit
2b7651fb37
9 files changed
+846
-8
No files matched your search
@@ -27,7 +27,7 @@ from nisse_dev.canonical import (
|
||||
)
|
||||
from nisse_dev.oplog import OpLog
|
||||
|
||||
CURRENT_SCHEMA_VERSION = 2
|
||||
CURRENT_SCHEMA_VERSION = 3
|
||||
|
||||
Migration = Callable[[sqlite3.Connection], None]
|
||||
|
||||
@@ -41,12 +41,29 @@ CREATE TABLE canonical_events (
|
||||
"""
|
||||
|
||||
|
||||
_EVAL_OUTCOMES_DDL = """
|
||||
CREATE TABLE eval_outcomes (
|
||||
event_id INTEGER PRIMARY KEY,
|
||||
verdict TEXT NOT NULL,
|
||||
note TEXT NOT NULL DEFAULT '',
|
||||
marked_at TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
|
||||
|
||||
def _migrate_v1_add_canonical_events(conn: sqlite3.Connection) -> None:
|
||||
conn.execute(_CANONICAL_EVENTS_DDL)
|
||||
|
||||
|
||||
def _migrate_v2_add_eval_outcomes(conn: sqlite3.Connection) -> None:
|
||||
conn.execute(_EVAL_OUTCOMES_DDL)
|
||||
|
||||
|
||||
# Maps version N to the migration that takes the schema from N to N+1.
|
||||
MIGRATIONS: Mapping[int, Migration] = {1: _migrate_v1_add_canonical_events}
|
||||
MIGRATIONS: Mapping[int, Migration] = {
|
||||
1: _migrate_v1_add_canonical_events,
|
||||
2: _migrate_v2_add_eval_outcomes,
|
||||
}
|
||||
|
||||
|
||||
class StateSchemaError(RuntimeError):
|
||||
@@ -93,6 +110,7 @@ _SCHEMA = [
|
||||
)
|
||||
""",
|
||||
_CANONICAL_EVENTS_DDL,
|
||||
_EVAL_OUTCOMES_DDL,
|
||||
]
|
||||
|
||||
|
||||
@@ -294,6 +312,27 @@ class StateStore:
|
||||
for row in rows
|
||||
]
|
||||
|
||||
def mark_outcome(self, event_id: int, verdict: str, note: str = "") -> None:
|
||||
"""Record whether a delegation or consult was good (A16).
|
||||
|
||||
Kept out of the canonical log on purpose: the log is an append-only
|
||||
record of what happened, and a verdict is a later judgement about it
|
||||
that may be revised.
|
||||
"""
|
||||
self._conn.execute(
|
||||
"INSERT INTO eval_outcomes (event_id, verdict, note, marked_at) "
|
||||
"VALUES (?, ?, ?, ?) ON CONFLICT(event_id) DO UPDATE SET "
|
||||
"verdict = excluded.verdict, note = excluded.note, "
|
||||
"marked_at = excluded.marked_at",
|
||||
(event_id, verdict, note, _now()),
|
||||
)
|
||||
|
||||
def outcomes(self) -> dict[int, tuple[str, str]]:
|
||||
rows = self._conn.execute(
|
||||
"SELECT event_id, verdict, note FROM eval_outcomes"
|
||||
).fetchall()
|
||||
return {r["event_id"]: (r["verdict"], r["note"]) for r in rows}
|
||||
|
||||
def close(self) -> None:
|
||||
self._conn.close()
|
||||
|
||||
|
||||
Reference in new issue
Block a user