Files
FarmHouse-Holdings/engine/protocol.py
T
fez5587andClaude Fable 5 9b6e7a23f6 Slice 1: dispatcher loop + objective decomposition — first live objective completed
- engine/dispatcher.py: 10s tick, bounded in-flight (3), ready->kanban dispatch
  with budget mapping (goal_max_turns/max_runtime_seconds), poll->AgentMessage
  extraction->events, wall-clock cap with run termination, blocked triage state.
- engine/protocol.py: schema-enforced pool calls — raw report -> AgentMessage,
  objective brief -> task decomposition (3.6s live).
- engine/main.py: POST /companies/{slug}/objectives (manager decompose ->
  child work items, role-matched round-robin), GET work-items.
- schema: work_item.hermes_task_id/dispatched_at, employee.role.
- Hermes integration fixes discovered live:
  * profiles must use the pool custom endpoint (provider config written into
    per-profile config; 'ollama' provider = Ollama Cloud, wants API key)
  * kanban toolset was globally disabled in deployed Hermes config — removed
    from agent.disabled_toolsets (backup in scratchpad)
  * no_skills profiles (73 bundled skills drowned 9B tool discipline)
  * souls spell out kanban_show first, kanban_complete/kanban_block with
    required args last — after this, tasks complete reliably.
- Live result: GasWatch objective decomposed into 3 tasks, all completed by
  qwen3.5:9b employees (mvp_features.md, onepager.md, report.md), 4 cost
  entries totaling ~24 min wall, full event trail.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 09:49:26 -05:00

94 lines
3.5 KiB
Python

"""Structured extraction from free-form agent output.
Kanban runs return free text; the protocol requires AgentMessage. Rather than
trusting the worker to format correctly mid-run, the engine converts each raw
report with one schema-enforced pool call (qwen3.5:9b scored 100% validity
under `format` enforcement — benchmarks/RESULTS.md).
"""
import json
from typing import Any
import httpx
from engine.config import settings
from engine.schemas.events import AGENT_MESSAGE_FORMAT, AgentMessage
MODEL = "qwen3.5:9b"
EXTRACT_SYSTEM = (
"You convert an AI employee's raw work report into exactly one structured "
"protocol message. Choose the message_type that matches what actually "
"happened:\n"
"- task.completed: work finished, evidence given\n"
"- task.progressed: partial progress, more to do\n"
"- task.blocked: cannot continue for technical reasons\n"
"- clarification.requested: needs a decision/answer from a human; include "
"the options given\n"
"- approval.requested: needs authority the employee lacks\n"
"- incident.declared: something broke that affects others\n"
"Copy concrete facts (files, commits, test counts) into summary/detail. "
"Do not invent facts not present in the report."
)
DECOMPOSE_FORMAT: dict[str, Any] = {
"type": "object",
"properties": {
"tasks": {
"type": "array",
"items": {
"type": "object",
"properties": {
"title": {"type": "string"},
"description": {"type": "string"},
"role": {"type": "string", "enum": ["engineer", "qa", "manager"]},
"acceptance_criteria": {"type": "array", "items": {"type": "string"}},
},
"required": ["title", "description", "role", "acceptance_criteria"],
},
},
"clarification_needed": {"type": "string"},
},
"required": ["tasks"],
}
async def _chat_structured(system: str, user: str, fmt: dict) -> dict:
async with httpx.AsyncClient(timeout=180) as client:
r = await client.post(
f"{settings.ollama_pool_url}/api/chat",
json={
"model": MODEL,
"messages": [
{"role": "system", "content": system},
{"role": "user", "content": user},
],
"stream": False,
"think": False,
"format": fmt,
},
)
r.raise_for_status()
return json.loads(r.json()["message"]["content"])
async def extract_agent_message(work_item_id: str, raw_report: str) -> AgentMessage:
data = await _chat_structured(
EXTRACT_SYSTEM,
f"Work item ID: {work_item_id}\n\nRaw report:\n{raw_report[:6000]}",
AGENT_MESSAGE_FORMAT,
)
data["work_item_id"] = work_item_id # never trust the model with the ID
return AgentMessage.model_validate(data)
async def decompose_objective(brief: str, company_name: str) -> dict:
"""Manager-style decomposition into 2-5 small tasks. Returns validated dict."""
system = (
f"You are the engineering manager at {company_name}. Decompose the "
"objective into 2-5 small, independently completable tasks, each with "
"concrete acceptance criteria. Assign each to the right role. If the "
"objective is materially ambiguous, fill clarification_needed with the "
"question and give your best-guess decomposition anyway."
)
return await _chat_structured(system, brief, DECOMPOSE_FORMAT)