From e02e1eeca2e56df5292bf9e37c49804127a15a0a Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Thu, 5 Feb 2026 15:11:27 +0530 Subject: [PATCH] feat: split heartbeat templates and allow lead agent creation --- backend/app/api/agents.py | 29 +++- backend/app/services/agent_provisioning.py | 15 ++ templates/AGENTS.md | 2 +- templates/HEARTBEAT.md | 24 +-- templates/HEARTBEAT_AGENT.md | 107 ++++++++++++++ templates/HEARTBEAT_LEAD.md | 164 +++++++++++++++++++++ 6 files changed, 317 insertions(+), 24 deletions(-) create mode 100644 templates/HEARTBEAT_AGENT.md create mode 100644 templates/HEARTBEAT_LEAD.md diff --git a/backend/app/api/agents.py b/backend/app/api/agents.py index 0ce2f201..0d2ea5fb 100644 --- a/backend/app/api/agents.py +++ b/backend/app/api/agents.py @@ -189,8 +189,26 @@ def list_agents( async def create_agent( payload: AgentCreate, session: Session = Depends(get_session), - auth: AuthContext = Depends(require_admin_auth), + actor: ActorContext = Depends(require_admin_or_agent), ) -> Agent: + if actor.actor_type == "agent": + if not actor.agent or not actor.agent.is_board_lead: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Only board leads can create agents", + ) + if not actor.agent.board_id: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Board lead must be assigned to a board", + ) + if payload.board_id and payload.board_id != actor.agent.board_id: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Board leads can only create agents in their own board", + ) + payload = AgentCreate(**{**payload.model_dump(), "board_id": actor.agent.board_id}) + board = _require_board(session, payload.board_id) gateway, client_config = _require_gateway(session, board) data = payload.model_dump() @@ -230,7 +248,14 @@ async def create_agent( ) session.commit() try: - await provision_agent(agent, board, gateway, raw_token, auth.user, action="provision") + await provision_agent( + agent, + board, + gateway, + raw_token, + actor.user if actor.actor_type == "user" else None, + action="provision", + ) await _send_wakeup_message(agent, client_config, verb="provisioned") agent.provision_confirm_token_hash = None agent.provision_requested_at = None diff --git a/backend/app/services/agent_provisioning.py b/backend/app/services/agent_provisioning.py index 191dd0e2..216dd1bb 100644 --- a/backend/app/services/agent_provisioning.py +++ b/backend/app/services/agent_provisioning.py @@ -42,6 +42,9 @@ DEFAULT_GATEWAY_FILES = frozenset( } ) +HEARTBEAT_LEAD_TEMPLATE = "HEARTBEAT_LEAD.md" +HEARTBEAT_AGENT_TEMPLATE = "HEARTBEAT_AGENT.md" + def _repo_root() -> Path: return Path(__file__).resolve().parents[3] @@ -80,6 +83,10 @@ def _template_env() -> Environment: ) +def _heartbeat_template_name(agent: Agent) -> str: + return HEARTBEAT_LEAD_TEMPLATE if agent.is_board_lead else HEARTBEAT_AGENT_TEMPLATE + + def _workspace_path(agent_name: str, workspace_root: str) -> str: if not workspace_root: raise ValueError("gateway_workspace_root is required") @@ -219,6 +226,14 @@ def _render_agent_files( if name == "MEMORY.md": rendered[name] = "# MEMORY\n\nBootstrap pending.\n" continue + if name == "HEARTBEAT.md": + heartbeat_template = _heartbeat_template_name(agent) + heartbeat_path = _templates_root() / heartbeat_template + if heartbeat_path.exists(): + rendered[name] = ( + env.get_template(heartbeat_template).render(**context).strip() + ) + continue override = overrides.get(name) if override: rendered[name] = env.from_string(override).render(**context).strip() diff --git a/templates/AGENTS.md b/templates/AGENTS.md index d6d0aa63..59107cf7 100644 --- a/templates/AGENTS.md +++ b/templates/AGENTS.md @@ -29,7 +29,7 @@ Write things down. Do not rely on short-term context. ## Heartbeats - HEARTBEAT.md defines what to do on each heartbeat. -- If **IS_BOARD_LEAD** is true, you are responsible for coordination and must run the Board Lead Loop. +- Lead agents receive a lead-specific HEARTBEAT.md. Follow it exactly. ## Task updates - All task updates MUST be posted to the task comments endpoint. diff --git a/templates/HEARTBEAT.md b/templates/HEARTBEAT.md index 4eb45d4e..9e7edfa9 100644 --- a/templates/HEARTBEAT.md +++ b/templates/HEARTBEAT.md @@ -1,7 +1,9 @@ # HEARTBEAT.md +> This file is provisioned from HEARTBEAT_LEAD.md or HEARTBEAT_AGENT.md. If you see this template directly, follow the agent loop below. + ## Purpose -This file defines the single, authoritative heartbeat loop. Follow it exactly. +This file defines the single, authoritative heartbeat loop for non-lead agents. Follow it exactly. ## Required inputs - BASE_URL (e.g. http://localhost:8000) @@ -30,26 +32,6 @@ If any required input is missing, stop and request a provisioning update. - GET $BASE_URL/api/v1/boards/{BOARD_ID}/tasks must succeed. - If any check fails, stop and retry next heartbeat. -## Board Lead Loop (if IS_BOARD_LEAD == true) -When you are the board lead, run this loop after pre-flight checks and before claiming work: -1) Read board goal context: - - Board: {{ board_name }} ({{ board_type }}) - - Objective: {{ board_objective }} - - Success metrics: {{ board_success_metrics }} - - Target date: {{ board_target_date }} -2) Review recent tasks/comments and board memory: - - GET $BASE_URL/api/v1/boards/{BOARD_ID}/tasks?limit=50 - - GET $BASE_URL/api/v1/boards/{BOARD_ID}/memory?limit=50 -3) Update a short Board Plan Summary in board memory: - - POST $BASE_URL/api/v1/boards/{BOARD_ID}/memory - Body: {"content":"Plan summary + next gaps","tags":["plan","lead"],"source":"lead_heartbeat"} -4) Identify missing steps and propose tasks. -5) For each candidate task, compute confidence (rubric) and check risk/external actions: - - If risky/external or confidence < 80, create an approval: - - POST $BASE_URL/api/v1/boards/{BOARD_ID}/approvals - - Else create the task and assign an agent. -6) Post a brief status update in board memory (1-3 bullets). - ## Heartbeat checklist (run in order) 1) Check in: ```bash diff --git a/templates/HEARTBEAT_AGENT.md b/templates/HEARTBEAT_AGENT.md new file mode 100644 index 00000000..19a7a987 --- /dev/null +++ b/templates/HEARTBEAT_AGENT.md @@ -0,0 +1,107 @@ +# HEARTBEAT_AGENT.md + +## Purpose +This file defines the single, authoritative heartbeat loop for non-lead agents. Follow it exactly. + +## Required inputs +- BASE_URL (e.g. http://localhost:8000) +- AUTH_TOKEN (agent token) +- AGENT_NAME +- AGENT_ID +- BOARD_ID + +If any required input is missing, stop and request a provisioning update. + +## Schedule +- Schedule is controlled by gateway heartbeat config (default: every 10 minutes). +- On first boot, send one immediate check-in before the schedule starts. + +## Non‑negotiable rules +- Task updates go only to task comments (never chat/web). +- Comments must be markdown. Write naturally; be clear and concise. +- Every status change must have a comment within 30 seconds. +- Do not claim a new task if you already have one in progress. + +## Pre‑flight checks (before each heartbeat) +- Confirm BASE_URL, AUTH_TOKEN, and BOARD_ID are set. +- Verify API access: + - GET $BASE_URL/healthz must succeed. + - GET $BASE_URL/api/v1/boards must succeed. + - GET $BASE_URL/api/v1/boards/{BOARD_ID}/tasks must succeed. +- If any check fails, stop and retry next heartbeat. + +## Heartbeat checklist (run in order) +1) Check in: +```bash +curl -s -X POST "$BASE_URL/api/v1/agents/heartbeat" \ + -H "X-Agent-Token: $AUTH_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"name": "'$AGENT_NAME'", "board_id": "'$BOARD_ID'", "status": "online"}' +``` + +2) List boards: +```bash +curl -s "$BASE_URL/api/v1/boards" \ + -H "X-Agent-Token: $AUTH_TOKEN" +``` + +3) For the assigned board, list tasks (use filters to avoid large responses): +```bash +curl -s "$BASE_URL/api/v1/boards/{BOARD_ID}/tasks?status=in_progress&assigned_agent_id=$AGENT_ID&limit=5" \ + -H "X-Agent-Token: $AUTH_TOKEN" +``` +```bash +curl -s "$BASE_URL/api/v1/boards/{BOARD_ID}/tasks?status=inbox&unassigned=true&limit=20" \ + -H "X-Agent-Token: $AUTH_TOKEN" +``` + +4) If you already have an in_progress task, continue working it and do not claim another. + +5) If you do NOT have an in_progress task, claim one inbox task: +- Move it to in_progress AND add a markdown comment describing the update. + +6) Work the task: +- Post progress comments as you go. +- Completion is a two‑step sequence: +6a) Post the full response as a markdown comment using: + POST $BASE_URL/api/v1/boards/{BOARD_ID}/tasks/{TASK_ID}/comments + Example: +```bash +curl -s -X POST "$BASE_URL/api/v1/boards/$BOARD_ID/tasks/$TASK_ID/comments" \ + -H "X-Agent-Token: $AUTH_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"message":"- Update: ...\n- Result: ..."}' +``` + 6b) Move the task to review. + +6b) Move the task to "review": +```bash +curl -s -X PATCH "$BASE_URL/api/v1/boards/{BOARD_ID}/tasks/{TASK_ID}" \ + -H "X-Agent-Token: $AUTH_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"status": "review"}' +``` + +## Definition of Done +- A task is not complete until the draft/response is posted as a task comment. +- Comments must be markdown. + +## Common mistakes (avoid) +- Changing status without posting a comment. +- Posting updates in chat/web instead of task comments. +- Claiming a second task while one is already in progress. +- Moving to review before posting the full response. +- Sending Authorization header instead of X-Agent-Token. + +## Success criteria (when to say HEARTBEAT_OK) +- Check‑in succeeded. +- Tasks were listed successfully. +- If any task was worked, a markdown comment was posted and the task moved to review. +- If any task is inbox or in_progress, do NOT say HEARTBEAT_OK. + +## Status flow +``` +inbox -> in_progress -> review -> done +``` + +Do not say HEARTBEAT_OK if there is inbox work or active in_progress work. diff --git a/templates/HEARTBEAT_LEAD.md b/templates/HEARTBEAT_LEAD.md new file mode 100644 index 00000000..3727c43f --- /dev/null +++ b/templates/HEARTBEAT_LEAD.md @@ -0,0 +1,164 @@ +# HEARTBEAT_LEAD.md + +## Purpose +This file defines the single, authoritative heartbeat loop for the board lead agent. Follow it exactly. + +## Required inputs +- BASE_URL (e.g. http://localhost:8000) +- AUTH_TOKEN (agent token) +- AGENT_NAME +- AGENT_ID +- BOARD_ID + +If any required input is missing, stop and request a provisioning update. + +## Schedule +- Schedule is controlled by gateway heartbeat config (default: every 10 minutes). +- On first boot, send one immediate check-in before the schedule starts. + +## Non‑negotiable rules +- Task updates go only to task comments (never chat/web). +- Comments must be markdown. Write naturally; be clear and concise. +- Every status change must have a comment within 30 seconds. +- Do not claim a new task if you already have one in progress. + +## Pre‑flight checks (before each heartbeat) +- Confirm BASE_URL, AUTH_TOKEN, and BOARD_ID are set. +- Verify API access: + - GET $BASE_URL/healthz must succeed. + - GET $BASE_URL/api/v1/boards must succeed. + - GET $BASE_URL/api/v1/boards/{BOARD_ID}/tasks must succeed. +- If any check fails, stop and retry next heartbeat. + +## Board Lead Loop (run every heartbeat before claiming work) +1) Read board goal context: + - Board: {{ board_name }} ({{ board_type }}) + - Objective: {{ board_objective }} + - Success metrics: {{ board_success_metrics }} + - Target date: {{ board_target_date }} + +2) Review recent tasks/comments and board memory: + - GET $BASE_URL/api/v1/boards/{BOARD_ID}/tasks?limit=50 + - GET $BASE_URL/api/v1/boards/{BOARD_ID}/memory?limit=50 + +3) Update a short Board Plan Summary in board memory: + - POST $BASE_URL/api/v1/boards/{BOARD_ID}/memory + Body: {"content":"Plan summary + next gaps","tags":["plan","lead"],"source":"lead_heartbeat"} + +4) Identify missing steps, blockers, and specialists needed. + +5) For each candidate task, compute confidence and check risk/external actions. + Confidence rubric (max 100): + - clarity 25 + - constraints 20 + - completeness 15 + - risk 20 + - dependencies 10 + - similarity 10 + + If risky/external OR confidence < 80: + - POST approval request to $BASE_URL/api/v1/boards/{BOARD_ID}/approvals + Body example: + {"action_type":"task.create","confidence":75,"payload":{"title":"..."},"rubric_scores":{"clarity":20,"constraints":15,"completeness":10,"risk":10,"dependencies":10,"similarity":10}} + + Else: + - Create the task and assign an agent. + +6) If workload or skills coverage is insufficient, create new agents. + Rule: you may auto‑create agents only when confidence >= 80 and the action is not risky/external. + If the action is risky/external or confidence < 80, create an approval instead. + + Agent create (lead-only): + - POST $BASE_URL/api/v1/agents + Headers: X-Agent-Token: $AUTH_TOKEN + Body example: + { + "name": "Researcher Alpha", + "board_id": "{BOARD_ID}", + "identity_profile": { + "role": "Research", + "communication_style": "concise, structured", + "emoji": ":brain:" + } + } + + Approval example: + {"action_type":"agent.create","confidence":70,"payload":{"role":"Research","reason":"Need specialist"}} + +7) Post a brief status update in board memory (1-3 bullets). + +## Heartbeat checklist (run in order) +1) Check in: +```bash +curl -s -X POST "$BASE_URL/api/v1/agents/heartbeat" \ + -H "X-Agent-Token: $AUTH_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"name": "'$AGENT_NAME'", "board_id": "'$BOARD_ID'", "status": "online"}' +``` + +2) List boards: +```bash +curl -s "$BASE_URL/api/v1/boards" \ + -H "X-Agent-Token: $AUTH_TOKEN" +``` + +3) For the assigned board, list tasks (use filters to avoid large responses): +```bash +curl -s "$BASE_URL/api/v1/boards/{BOARD_ID}/tasks?status=in_progress&assigned_agent_id=$AGENT_ID&limit=5" \ + -H "X-Agent-Token: $AUTH_TOKEN" +``` +```bash +curl -s "$BASE_URL/api/v1/boards/{BOARD_ID}/tasks?status=inbox&unassigned=true&limit=20" \ + -H "X-Agent-Token: $AUTH_TOKEN" +``` + +4) If you already have an in_progress task, continue working it and do not claim another. + +5) If you do NOT have an in_progress task, claim one inbox task: +- Move it to in_progress AND add a markdown comment describing the update. + +6) Work the task: +- Post progress comments as you go. +- Completion is a two‑step sequence: +6a) Post the full response as a markdown comment using: + POST $BASE_URL/api/v1/boards/{BOARD_ID}/tasks/{TASK_ID}/comments + Example: +```bash +curl -s -X POST "$BASE_URL/api/v1/boards/$BOARD_ID/tasks/$TASK_ID/comments" \ + -H "X-Agent-Token: $AUTH_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"message":"- Update: ...\n- Result: ..."}' +``` + 6b) Move the task to review. + +6b) Move the task to "review": +```bash +curl -s -X PATCH "$BASE_URL/api/v1/boards/{BOARD_ID}/tasks/{TASK_ID}" \ + -H "X-Agent-Token: $AUTH_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"status": "review"}' +``` + +## Definition of Done +- A task is not complete until the draft/response is posted as a task comment. +- Comments must be markdown. + +## Common mistakes (avoid) +- Changing status without posting a comment. +- Posting updates in chat/web instead of task comments. +- Claiming a second task while one is already in progress. +- Moving to review before posting the full response. +- Sending Authorization header instead of X-Agent-Token. + +## Success criteria (when to say HEARTBEAT_OK) +- Check‑in succeeded. +- Tasks were listed successfully. +- If any task was worked, a markdown comment was posted and the task moved to review. +- If any task is inbox or in_progress, do NOT say HEARTBEAT_OK. + +## Status flow +``` +inbox -> in_progress -> review -> done +``` + +Do not say HEARTBEAT_OK if there is inbox work or active in_progress work.