|
| 1 | +# Orchestrator Handlers (Design & Usage) |
| 2 | + |
| 3 | +This document explains how the orchestrator executes recipe steps via pluggable handlers |
| 4 | +(no hardcoded step logic), how legacy `uses:` steps are mapped to handlers, what a |
| 5 | +handler looks like, and how to validate a recipe. |
| 6 | + |
| 7 | +## Why handlers? |
| 8 | + |
| 9 | +- Decouple the engine (graph, checkpoints, interrupts, guardrails) from side‑effects |
| 10 | + (git, GitHub, Odoo, Task Registry, MCP tools). |
| 11 | +- Make it easy to add new step types without touching the engine. |
| 12 | +- Validate step arguments per handler before execution. |
| 13 | + |
| 14 | +## Built‑in handlers |
| 15 | + |
| 16 | +- `git.apply` — checkout/commit with diff guardrails |
| 17 | +- `github.open_pr` — open a PR using `gh` (REST fallback TBD) |
| 18 | +- `odoo.call` — call a tool on the Odoo MCP server (adapter safety still applies) |
| 19 | +- `tm.log_decision` — add a decision note to the current task |
| 20 | +- `tm.set_status` — update the current task status |
| 21 | +- `agent.wait` — write GOAL.md and interrupt to wait for human |
| 22 | +- `mcp.call` — generic MCP caller (server/tool/arguments) |
| 23 | + |
| 24 | +## Mapping legacy `uses:` to handlers |
| 25 | + |
| 26 | +You can keep your existing recipes. The engine maps legacy steps to handlers: |
| 27 | + |
| 28 | +- `uses: git` → `git.apply` (fields: `branch`, `commit`) |
| 29 | +- `uses: github` + `pr:` → `github.open_pr` (fields in `pr`) |
| 30 | +- `uses: odoo` → `odoo.call` (fields: `tool`, `args`) |
| 31 | +- `uses: tm` (`decision:` | `status:`) → `tm.log_decision` | `tm.set_status` |
| 32 | +- `uses: agent` → `agent.wait` |
| 33 | +- `uses: mcp` → `mcp.call` (fields: `server`, `tool`, `arguments`) |
| 34 | + |
| 35 | +You can also call a handler explicitly later (future), but legacy mapping is enough for |
| 36 | +all current recipes. |
| 37 | + |
| 38 | +## Handler interface (for reference) |
| 39 | + |
| 40 | +``` |
| 41 | +# orchestrator/engine/handlers/base.py |
| 42 | +class NodeHandler: |
| 43 | + name: str |
| 44 | + def schema(self) -> dict: ... # JSON Schema for arguments (optional) |
| 45 | + def execute(self, *, args: dict, ctx: dict, **kw) -> ExecResult: ... |
| 46 | +
|
| 47 | +class ExecResult: |
| 48 | + ok: bool |
| 49 | + output: dict |
| 50 | + idempotency_key: Optional[str] |
| 51 | +
|
| 52 | +class NodeInterrupt(Exception): |
| 53 | + reason: str |
| 54 | + payload: dict |
| 55 | +``` |
| 56 | + |
| 57 | +- The engine provides adapters/clients via `**kw` when calling `execute` (e.g. `git`, |
| 58 | + `github`, `odoo`, `tm`, `mcp_clients`, and the `logger`). |
| 59 | +- A handler can raise `NodeInterrupt` to yield control (the engine records a checkpoint |
| 60 | + and returns `status=interrupted`). |
| 61 | + |
| 62 | +## Validation |
| 63 | + |
| 64 | +Handlers can expose a lightweight JSON Schema for their arguments. The doctor command |
| 65 | +uses it to validate recipes: |
| 66 | + |
| 67 | +``` |
| 68 | +python -m orchestrator.main doctor validate-recipe --recipe <name> |
| 69 | +``` |
| 70 | + |
| 71 | +This resolves each legacy step to a handler and checks the arguments against the |
| 72 | +handler’s schema so you catch mistakes early. |
| 73 | + |
| 74 | +## Return to agent on failure (policy) |
| 75 | + |
| 76 | +Any step can bounce control back to an agent when it fails: |
| 77 | + |
| 78 | +``` |
| 79 | +steps: |
| 80 | + - id: agent |
| 81 | + uses: agent |
| 82 | + goal: "Please implement X" |
| 83 | + - id: tests |
| 84 | + uses: odoo |
| 85 | + tool: test_addons |
| 86 | + args: { modules: ["{{ env.module }}"], dbname: "{{ env.db }}" } |
| 87 | + on_error: |
| 88 | + return_to_agent: agent # required |
| 89 | + tm_status: review # optional convenience |
| 90 | +``` |
| 91 | + |
| 92 | +On failure, the engine marks the failing node failed, resets the target agent node in |
| 93 | +checkpoints, and returns `status=interrupted` at that agent. Fix issues, then resume: |
| 94 | + |
| 95 | +``` |
| 96 | +python -m orchestrator.main run_graph --task-id T1 --recipe R --resume <run_id> |
| 97 | +``` |
| 98 | + |
| 99 | +## MCP: generic tool calling |
| 100 | + |
| 101 | +For ad‑hoc tools or new MCP servers, use `mcp.call`: |
| 102 | + |
| 103 | +``` |
| 104 | +steps: |
| 105 | + - uses: mcp |
| 106 | + server: tm |
| 107 | + tool: add_evidence |
| 108 | + arguments: { id: T1, type: note, ref: ok } |
| 109 | +``` |
| 110 | + |
| 111 | +This reuses the orchestrator’s persistent MCP clients for `odoo` and `tm`. |
| 112 | + |
| 113 | +## Parallel steps (sequential for now) |
| 114 | + |
| 115 | +`parallel:` steps execute their `children` sequentially today for safety. The internal |
| 116 | +`GraphExecutor` will fan out in true parallel once repo locking is in place. |
| 117 | + |
| 118 | +## Admin tools |
| 119 | + |
| 120 | +- List handlers: `python -m orchestrator.main doctor list-handlers` |
| 121 | +- Validate handler mappings + args: |
| 122 | + `python -m orchestrator.main doctor validate-recipe --recipe <name>` |
| 123 | +- Reset a node in a run: |
| 124 | + `python -m orchestrator.main doctor reset-node --run-id <id> --node-id <id>` |
| 125 | + |
| 126 | +## Roadmap |
| 127 | + |
| 128 | +- Discover additional handlers via entry points (packaged plugins) if needed. |
| 129 | +- Optional lockfile to pin handler name/version for reproducibility. |
| 130 | +- Full REST fallback for `github.open_pr` when `gh` is unavailable. |
0 commit comments