| 1 | #!/usr/bin/env python3 |
| 2 | """ |
| 3 | darwin-emit - POST a contract-§1 event to the Darwin Deck event bus. |
| 4 | |
| 5 | The forward hook for true `agents-of-agents` nesting: any process (a spawner, |
| 6 | a scheduler, a remote agent, a shell hook) can self-report into the live tree. |
| 7 | A spawner emits a `child_spawned` carrying its own --parent-id so the child |
| 8 | nests under it to any depth. |
| 9 | |
| 10 | Deck endpoint (default): http://REDACTED-IP:9893/event |
| 11 | Contract: /shared/projects/darwin/deck/CONTRACTS.md §1 |
| 12 | |
| 13 | USAGE |
| 14 | darwin-emit --agent-id ID [options] |
| 15 | darwin-emit -a ID -t child_spawned -p PARENT_ID -l "Build gateway" -m glm --host ct247 |
| 16 | |
| 17 | OPTIONS (env var fallback in parens; CLI wins) |
| 18 | -a, --agent-id ID stable id for this agent run (DARWIN_AGENT_ID) [required] |
| 19 | -p, --parent-id ID spawner id -> builds the tree edge (DARWIN_PARENT_ID) |
| 20 | -t, --type T spawned|tool_call|child_spawned|returned|error|token_tick |
| 21 | (DARWIN_TYPE, default: spawned) |
| 22 | -l, --label S human label (DARWIN_LABEL) |
| 23 | -m, --model S sonnet|opus|glm-5.2|codex|gpt-5.5|… (DARWIN_MODEL, default: sonnet) |
| 24 | -s, --status S running|done|error|paused (DARWIN_STATUS, default: running) |
| 25 | --host S ct215|ct241|ct247|… (DARWIN_HOST, default: ct215) |
| 26 | --tokens N cumulative token count (DARWIN_TOKENS, default: 0) |
| 27 | --tool S tool name (for tool_call) (DARWIN_TOOL) |
| 28 | --session S session id (DARWIN_SESSION) |
| 29 | --deck-url URL override Deck base url (DARWIN_DECK_URL) |
| 30 | -q, --quiet no stdout on success |
| 31 | |
| 32 | EXAMPLES |
| 33 | # a spawner registers a freshly-spawned child under itself |
| 34 | darwin-emit -a child_abc -p $MY_AGENT_ID -t child_spawned -l "scan repo" -m glm --host ct247 |
| 35 | |
| 36 | # heartbeat / token tick |
| 37 | darwin-emit -a child_abc -t token_tick --tokens 53542 |
| 38 | |
| 39 | # mark done |
| 40 | darwin-emit -a child_abc -t returned -s done |
| 41 | |
| 42 | EXIT 0 ok · 2 bad args · 3 POST failed |
| 43 | """ |
| 44 | import argparse |
| 45 | import json |
| 46 | import os |
| 47 | import sys |
| 48 | import urllib.request |
| 49 | from datetime import datetime, timezone |
| 50 | |
| 51 | |
| 52 | def main() -> int: |
| 53 | p = argparse.ArgumentParser(add_help=True, description="Emit a Darwin Deck event.") |
| 54 | p.add_argument("-a", "--agent-id", default=os.environ.get("DARWIN_AGENT_ID")) |
| 55 | p.add_argument("-p", "--parent-id", default=os.environ.get("DARWIN_PARENT_ID")) |
| 56 | p.add_argument("-t", "--type", default=os.environ.get("DARWIN_TYPE", "spawned")) |
| 57 | p.add_argument("-l", "--label", default=os.environ.get("DARWIN_LABEL", "")) |
| 58 | p.add_argument("-m", "--model", default=os.environ.get("DARWIN_MODEL", "sonnet")) |
| 59 | p.add_argument("-s", "--status", default=os.environ.get("DARWIN_STATUS", "running")) |
| 60 | p.add_argument("--host", default=os.environ.get("DARWIN_HOST", "ct215")) |
| 61 | p.add_argument("--tokens", type=int, default=int(os.environ.get("DARWIN_TOKENS", "0"))) |
| 62 | p.add_argument("--tool", default=os.environ.get("DARWIN_TOOL")) |
| 63 | p.add_argument("--session", default=os.environ.get("DARWIN_SESSION", "")) |
| 64 | p.add_argument("--deck-url", default=os.environ.get("DARWIN_DECK_URL", "http://REDACTED-IP:9893")) |
| 65 | p.add_argument("-q", "--quiet", action="store_true") |
| 66 | args = p.parse_args() |
| 67 | |
| 68 | if not args.agent_id: |
| 69 | sys.stderr.write("darwin-emit: --agent-id (or DARWIN_AGENT_ID) is required\n") |
| 70 | return 2 |
| 71 | |
| 72 | event = { |
| 73 | "ts": datetime.now(timezone.utc).isoformat(), |
| 74 | "agent_id": args.agent_id, |
| 75 | "parent_id": args.parent_id, |
| 76 | "type": args.type, |
| 77 | "label": args.label, |
| 78 | "model": args.model, |
| 79 | "status": args.status, |
| 80 | "tokens": args.tokens, |
| 81 | "host": args.host, |
| 82 | "session": args.session, |
| 83 | } |
| 84 | if args.tool: |
| 85 | event["tool"] = args.tool |
| 86 | |
| 87 | url = args.deck_url.rstrip("/") + "/event" |
| 88 | data = json.dumps(event).encode() |
| 89 | req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"}, method="POST") |
| 90 | try: |
| 91 | with urllib.request.urlopen(req, timeout=5) as resp: |
| 92 | body = resp.read().decode() |
| 93 | except Exception as e: # noqa: BLE001 - deck down must never break a spawner |
| 94 | sys.stderr.write(f"darwin-emit: POST failed: {e}\n") |
| 95 | return 3 |
| 96 | |
| 97 | if not args.quiet: |
| 98 | print(body) |
| 99 | return 0 |
| 100 | |
| 101 | |
| 102 | if __name__ == "__main__": |
| 103 | sys.exit(main()) |