Skip to content

Commit 1efd445

Browse files
committed
docs(handlers): add scaffold example and cross-links; feat(cli): --strict-validate flag for run_graph to validate handlers+args
1 parent 28e20af commit 1efd445

File tree

3 files changed

+79
-0
lines changed

3 files changed

+79
-0
lines changed

orchestrator/docs/HANDLERS.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,16 @@ This reuses the orchestrator’s persistent MCP clients for `odoo` and `tm`.
128128
- Discover additional handlers via entry points (packaged plugins) if needed.
129129
- Optional lockfile to pin handler name/version for reproducibility.
130130
- Full REST fallback for `github.open_pr` when `gh` is unavailable.
131+
132+
## Scaffolding a custom handler
133+
134+
See `orchestrator/docs/examples/handler_skeleton.py` for a minimal example. It shows:
135+
136+
- Declaring a `name` (e.g., `my.example`)
137+
- Optional `schema()` to validate arguments
138+
- `execute()` reading args, writing a log, optionally raising a `NodeInterrupt`, and
139+
returning an `ExecResult`.
140+
141+
Today, legacy `uses:` steps are auto-mapped to built-ins. In a future iteration, the
142+
registry will support project-local plugins so you can drop a handler in your repo and
143+
reference it by `handler: my.example` in recipes.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from __future__ import annotations
2+
3+
from typing import Any, Dict
4+
5+
from orchestrator.engine.handlers.base import ExecResult, NodeHandler, NodeInterrupt
6+
7+
8+
class MyExampleHandler(NodeHandler):
9+
"""Skeleton for a custom handler.
10+
11+
Register this handler via a local registry (future), or temporarily call it via
12+
`handler: my.example` once the registry supports project-local plugins.
13+
"""
14+
15+
name = "my.example"
16+
17+
def schema(self) -> Dict[str, Any]:
18+
# Optional JSON Schema to validate step arguments
19+
return {
20+
"type": "object",
21+
"properties": {
22+
"message": {"type": "string"},
23+
"interrupt": {"type": "boolean"},
24+
},
25+
"required": ["message"],
26+
"additionalProperties": False,
27+
}
28+
29+
def execute(self, *, args: Dict[str, Any], ctx: Dict[str, Any], **kw) -> ExecResult:
30+
logger = kw.get("logger")
31+
message = str(args.get("message"))
32+
if logger is not None:
33+
logger.write_log("custom/example.log", f"message: {message}\n")
34+
35+
# Example: raise an interrupt to yield control (optional)
36+
if bool(args.get("interrupt")):
37+
raise NodeInterrupt("await-human", {"note": "please continue"})
38+
39+
# Normal completion
40+
return ExecResult(ok=True, output={"echo": message})

orchestrator/main.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ def run_graph(
116116
recipe: str = typer.Option(..., "--recipe", help="Recipe name"),
117117
config: Optional[Path] = typer.Option(None, "--config"),
118118
resume_run: Optional[str] = typer.Option(None, "--resume", help="Resume run_id"),
119+
strict_validate: bool = typer.Option(
120+
False,
121+
"--strict-validate/--no-strict-validate",
122+
help="Validate each step against resolved handler schema before execution",
123+
),
119124
):
120125
cfg = load_config(config)
121126
root = repo_root()
@@ -135,6 +140,27 @@ def run_graph(
135140
cp = Checkpointer(
136141
root / "orchestrator" / ".orchestrator" / "checkpoints" / "cp.sqlite"
137142
)
143+
# Optional strict validation using the handler registry
144+
if strict_validate:
145+
from .engine.registry import HandlerRegistry
146+
147+
reg = HandlerRegistry()
148+
ok = True
149+
for i, step in enumerate(recipe_data.get("steps", []), start=1):
150+
try:
151+
h, args = reg.route_compat(step)
152+
handler = reg.resolve(h)
153+
if not handler:
154+
typer.secho(f"step {i}: handler {h} not found", fg=typer.colors.RED)
155+
ok = False
156+
continue
157+
reg.validate_args(handler, args)
158+
except Exception as e:
159+
typer.secho(f"step {i}: {e}", fg=typer.colors.RED)
160+
ok = False
161+
if not ok:
162+
raise typer.Exit(1)
163+
138164
g = compile_recipe_to_graph(recipe_data)
139165
with MCPRuntime(odoo_cmd=odoo_cmd, tm_cmd=tm_cmd, repo_root=root) as mcp:
140166
execg = GraphExecutor(cfg, logger, root, mcp, cp)

0 commit comments

Comments
 (0)