feat(engine): the turn runner — prompt builder, sandbox tools, agent loop
Phase 2 of TODO-agent-2: one employee, one turn, real tools in the sandbox. - engine/prompt.py: the stable, cache-friendly prefix in the plan's order (rules → role protocol → toolkit → app profile → memory), a task brief capped at 1200 chars, role-specific stage context (spec for the developer, the diff only for the reviewer, test output for the tester) and a hard prompt cap that drops the oldest tool results first. - engine/tools.py: READ/WRITE/EDIT/LIST_DIR/SEARCH/RUN executed inside the sandbox through SandboxProviderPort, rooted at the worktree's sandbox path. Path escapes, .. and .git are refused; git push/remote/config are refused with "the platform does git"; RUN clamps its timeout (120s default, 600s max) and keeps the tail of its output; every execution writes a tool_calls row so the approvals UI shows real work. - engine/runner.py: the loop — model call, parse tags, execute tool tags, feed results back — until a terminal tag, the tool cap, the turn timeout or a cancellation. The employee's model is checked against ModelGrantPort before the first call (ungranted → error_kind=auth, no spend), errors are classified transient vs fatal, and every model call writes usage events through the one shared record_model_usage path (extracted from WorkforceService so manager chat counts too). Writes engine_turns, run_steps and tool_calls. - engine/events.py: RunFeed batches run.log (≤1 event / 500 ms / stream, ≤200 lines) and emits run.tool and task.gate through the generic envelope. - tests: engine_fakes.py (ScriptedModel, FakeSandbox, FakeGrants, RecordingMemory) plus 54 new tests in test_engine_runner, test_engine_prompt and test_engine_tools. Suite: 648 passed, ruff clean, mypy clean.
This commit is contained in:
@@ -408,14 +408,26 @@ class EngineTurnRepository:
|
||||
return [_turn(r) for r in (await self.session.execute(stmt)).scalars()]
|
||||
|
||||
async def finish(
|
||||
self, turn_id: str, *, ok: bool, tool_calls: int, error_kind: str = ""
|
||||
self,
|
||||
turn_id: str,
|
||||
*,
|
||||
ok: bool,
|
||||
tool_calls: int,
|
||||
error_kind: str = "",
|
||||
reply_excerpt: str | None = None,
|
||||
tags: dict | None = None,
|
||||
) -> None:
|
||||
"""Close a turn: outcome, tool count and the bounded reply excerpt."""
|
||||
row = await self.session.get(EngineTurnRow, turn_id)
|
||||
if row is None:
|
||||
return
|
||||
row.ok = ok
|
||||
row.tool_calls = tool_calls
|
||||
row.error_kind = error_kind
|
||||
if reply_excerpt is not None:
|
||||
row.reply_excerpt = reply_excerpt
|
||||
if tags is not None:
|
||||
row.tags = dict(tags)
|
||||
row.finished_at = utcnow()
|
||||
await self.session.flush()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user