
Agent Builder
- 36 installs
- 21 repo stars
- Updated August 3, 2026
- starchild-ai-agent/official-skills
Helps with ai & agent building tasks during AI-assisted development.
About
agent-builder is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- agent-builder
- AI & Agent Building
- AI-coding skill
Agent Builder by the numbers
- 36 all-time installs (skills.sh)
- +5 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #8,629 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/starchild-ai-agent/official-skills --skill agent-builderAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 36 |
|---|---|
| repo stars | ★ 21 |
| Last updated | August 3, 2026 |
| Repository | starchild-ai-agent/official-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Agent Builder
Build focused micro-agents that do 1-2 things exceptionally well.
Routing (YOU MUST DECIDE)
When the user asks to create an agent, pick the right path:
`agent_build` (singular) — 1 skill, 1 data source, simple recurring tasks, 1-5 tool calls.
`agent_build` + `always_on=True` — Agent that runs 24/7, polls inbox for messages every 5 min. Like an employee at their desk.
`agent_team` (team) — 2+ skills, parallel data gathering, synthesis needed, 10+ tool calls.
`agent_loop` (long-running) — Open-ended research, iterative accumulation, no fixed endpoint.
`agent_message` — Send a message to any agent's inbox. Works for user→agent and agent→agent.
When unsure: Start singular. Cheapest. User can upgrade later.
Tools
| Tool | Use |
|---|---|
agent_build | Create or update an agent. Set always_on=True for daemon mode. |
agent_task | Add/update/complete/remove/list tasks. Also set_status to pause/archive/reactivate. |
agent_run | Execute agent once (picks highest-priority pending task) |
agent_list | List all agents with status, task counts. Filter by team/status. |
agent_team | Create and run teams (leader + workers with cost-optimized models) |
agent_loop | Run agent in loop mode — iterates until goal satisfied or budget exhausted |
agent_message | Send a message to an agent's inbox. Daemon picks it up on next poll. |
Singular Agent Workflow
1. Create
agent_build(
name="market-scout",
display_name="Market Scout",
role="Crypto market surveillance specialist",
goal="Monitor BTC and ETH prices, flag anomalies",
skills=["coingecko", "chart"],
template="monitor",
mode="research",
schedule="0 8 * * *",
timezone="Asia/Kuala_Lumpur"
)Templates: default, monitor, researcher
Modes:
default— Free thinkingresearch— Hallucination guardrails: must say "I don't know", cite sources, extract quotes before analysis
Schedule: Cron ("0 8 * * *"), interval ("every 30 minutes"), delay ("in 2 hours"), at ("at 2026-05-01 14:00")
2. Add Tasks
agent_task(agent="market-scout", action="add",
title="Check BTC price",
priority="high",
due_date="2026-04-26",
recurring=True,
depends_on=["task-id-of-prerequisite"]
)- Priority:
low(60s, 2 calls),medium(120s, 5 calls),high(180s, 10 calls),critical(300s, 20 calls) - Recurring: Resets to pending after completion, logs history
- depends_on: Task chaining — blocked until dependencies complete
3. Run
agent_run(agent="market-scout")4. Manage
agent_task(agent="market-scout", action="set_status", status="paused")
agent_list(team="market-sentiment", verbose=True)Team Workflow
1. Create Team
agent_team(action="create",
name="market-sentiment",
leader={"name": "analyst", "role": "Synthesize sentiment", "skills": ["chart"]},
workers=[
{"name": "btc-fetcher", "role": "Fetch BTC tweets", "skills": ["twitter"]},
{"name": "eth-fetcher", "role": "Fetch ETH tweets", "skills": ["twitter"]},
]
)Leader: Sonnet ($3/$15). Workers: Haiku ($1/$5). ~67% savings.
2. Run Team
agent_team(action="run", name="market-sentiment",
goal="Cross-chain sentiment comparison for BTC and ETH"
)Leader owns the full pipeline: creates worker tasks → runs workers → waits (bash poll) → reads outputs → synthesizes.
Loop Mode (Long-Running)
For open-ended tasks with no fixed endpoint:
agent_loop(agent="uni-scout",
goal="Find universities with AI programs open to collaboration",
max_iterations=10,
output_file="candidates.json",
timeout=1800
)The agent iterates: read progress → search → add results → evaluate → continue/stop.
Budget regimes adapt strategy as iterations progress:
- EXPLORE (early) → CONVERGE (middle) → FOCUS (late) → FINALIZE (end)
Stopping: Agent says done, max iterations, or 2 consecutive dry iterations.
Always-On Agents (Daemon Mode)
Create agents that run 24/7, polling for messages and tasks:
agent_build(name="ai-scout", display_name="AI Scout",
role="Research AI content creators on Twitter",
goal="Build a database of high-quality AI creators",
skills=["twitter"], template="researcher", mode="research",
always_on=True, team="marketing"
)Then activate the daemon:
scheduled_task(action="activate", job_id="<daemon_job_id from output>")Every 5 minutes, the daemon: 1. Checks inbox.json — new messages? Process them, respond, push to user. 2. Checks tasks.json — pending tasks? Run the highest priority one. 3. Neither? Autonomous work — reads its goal, memory, and existing output, then proactively does the next most valuable thing to advance its goal. Runs every 30 min (cooldown prevents burning tokens every poll).
If the agent has nothing productive to do (goal fully satisfied), it outputs AUTONOMOUS_IDLE and costs $0.00.
Messaging
Send messages to any agent — user to agent or agent to agent:
agent_message(agent="ai-scout", message="Focus on creators with 100K+ followers who post about LLMs")
agent_message(agent="marketing-lead", message="5 new creators found", from_agent="ai-scout")Messages queue in inbox.json. The agent's daemon processes them on next poll (~5 min max). Responses appear in outbox.json and push to user.
Managing Agents
Everything is mutable at any time:
- Pause:
agent_task(agent="x", action="set_status", status="paused")— daemon skips paused agents - Resume:
agent_task(agent="x", action="set_status", status="active") - Kill daemon:
scheduled_task(action="cancel", job_id="<id>")— cancel is permanent, see Daemon Lifecycle below - Change role/skills/mode: Re-run
agent_buildwith new params — overwrites config - Send new instructions:
agent_message(agent="x", message="change focus to...") - Check findings:
read_file("agents/team/agent/output/results.json") - Delete: Remove the directory — agents are just files
Daemon Lifecycle (IMPORTANT)
Cancel is permanent. scheduled_task(action="cancel") deletes the job. You cannot reactivate it. To restart a cancelled daemon: 1. Re-run agent_build with always_on=True — registers a new scheduled task 2. The new task gets a new job_id — the old run.py's JOB_ID is now stale 3. agent_build automatically writes the correct JOB_ID into the new run.py 4. Activate: scheduled_task(action="activate", job_id="<new_id>")
JOB_ID must match the active task. The JOB_ID inside run.py is how push notifications route to the right job. If you manually copy a run.py, update the JOB_ID constant or push notifications silently go to a dead job.
Agent Pre-Action Reasoning
Every agent template now includes a mandatory reasoning framework before tool calls:
- WHO is affected?
- WHAT exactly will you do?
- WHY does this advance your goal?
- RISK — safe / moderate / destructive?
Destructive actions (delete, overwrite, clear) require the agent to STOP and generate a preview before executing. This prevents accidental data loss.
Output Contract
Every agent has a mandatory output contract baked into its prompt:
- Primary output is always a JSON file (
output/results.json) - JSON first, human-readable summary second
- On every run: read existing → append new → write back (never overwrite from scratch)
- Downstream agents and systems read JSON, not markdown
This prevents the "beautiful report but empty database" problem where agents write prose but never update their structured output.
Deduplication
All templates instruct agents to check for duplicates before adding items:
- Match on primary identifier (name, handle, URL, or ID)
- Skip duplicates, log skip count
- Design dedup in from run 1, not as a cleanup pass later
Resource Ownership
One agent owns one resource. Rules baked into every template:
- Agents write ONLY to their own
output/,memory/, andscripts/directories - If multiple agents need to feed the same destination, each writes to their own output — a designated sync agent handles the merge
- No two agents should write to the same file
Targeting Criteria
Researcher agents include explicit targeting bounds:
- Fit score 1-10, only include items scoring 6+
- Reachability — prefer contacts you can actually reach
- Relevance — must directly relate to goal, not adjacent/tangential
- Set explicit upper/lower bounds in task descriptions (follower range, geography, etc.)
Destructive Operation Guards
Agents cannot delete, clear, or overwrite critical data without generating a preview first. The pre-action reasoning framework classifies every action as safe/moderate/destructive. Destructive actions require: 1. A preview of what would change 2. Explicit confirmation before execution
Platform Tool Reliability
Platform tools (send_to_telegram, composio integrations, etc.) can silently fail if the underlying service is misconfigured or pending setup. For daemon agents that depend on external delivery:
- Always verify the tool works manually before relying on it in a daemon
- Build a direct API fallback for critical delivery paths
- Check tool output — silent failures return success with no actual delivery
Loop Mode Stopping Signals
For agent_loop, a "dry iteration" means the agent searched but found zero new items to add to the output file. The agent signals this by writing "new_items": 0 in its score.json for that iteration. Two consecutive dry iterations = stop.
The agent signals completion by outputting the text LOOP_COMPLETE as its final message. If it keeps running after goals are met, add explicit stopping criteria to the task description: "Stop when you have 20+ items" or "Stop when coverage includes US, UK, and Asia."
GUARDRAILS.md
Every agent gets a GUARDRAILS.md file — a living document of learned constraints. Agents should append entries as they discover failure patterns:
## Trigger: Writing to output without reading existing content
Instruction: ALWAYS read the output file before writing
Reason: Previous run overwrote 15 valid entries with 3 new onesDirectory Structure
agents/
├── marketing/ ← team folder
│ ├── team.json ← team config (leader, workers, models)
│ ├── analyst/ ← leader agent
│ │ ├── agent.yaml
│ │ ├── PROMPT.md
│ │ ├── GUARDRAILS.md ← learned constraints
│ │ ├── tasks.json
│ │ ├── references/
│ │ ├── scripts/
│ │ ├── output/results.json ← mandatory JSON output
│ │ ├── inbox.json ← messages (always-on)
│ │ ├── outbox.json
│ │ └── memory/MEMORY.md
│ ├── btc-fetcher/ ← worker
│ └── eth-fetcher/ ← worker
├── btc-watcher/ ← singular agent (flat)
└── hackathon-scout/ ← singular agent (flat)Design Principles
1. Focused — Each agent does 1-2 things. Resist scope creep. 2. Isolated — Each agent owns its own directories. No cross-agent writes. 3. Observable — Tasks have statuses, priorities, due dates, retry counts, history. 4. Composable — Create, pause, archive, delete independently. 5. Progressive — Context loads in layers (L1 metadata → L2 prompt → L3 references on demand). 6. Self-improving — Agents write learnings to memory and GUARDRAILS.md after every run. 7. Cost-aware — Priority determines timeout, tool call budget, and model hint. 8. JSON-first — Structured output before prose. Downstream systems read JSON. 9. Dedup-first — Check before inserting. Design deduplication from run 1. 10. Safe-by-default — Destructive actions require preview + confirmation. Dry-run mode.
"""
Agent Builder — Create and manage focused micro-agents.
"""
import logging
from typing import List
logger = logging.getLogger(__name__)
def register(api) -> List[str]:
"""Register agent builder tools."""
registered = []
try:
from .tools import (
AgentBuildTool,
AgentTaskTool,
AgentRunTool,
AgentListTool,
AgentTeamTool,
AgentLoopTool,
AgentMessageTool,
)
api.register_tool(AgentBuildTool())
api.register_tool(AgentTaskTool())
api.register_tool(AgentRunTool())
api.register_tool(AgentListTool())
api.register_tool(AgentTeamTool())
api.register_tool(AgentLoopTool())
api.register_tool(AgentMessageTool())
registered = [
"agent_build",
"agent_task",
"agent_run",
"agent_list",
"agent_team",
"agent_loop",
"agent_message",
]
logger.info(f"Registered agent-builder tools ({len(registered)} tools)")
except Exception as e:
logger.warning(f"Failed to load agent-builder tools: {e}")
return registered
EXTENSION_INFO = {
"name": "agent-builder",
"version": "1.0.0",
"description": "Create and manage focused micro-agents with tasks, schedules, and isolated memory",
"tools": [
"agent_build",
"agent_task",
"agent_run",
"agent_list",
"agent_team",
"agent_loop",
],
}
Agent Builder
Build focused micro-agents that do 1-2 things exceptionally well. Each agent gets its own directory with a prompt, tasks, memory, and output folder. They run as background tasks via sessions_spawn.
Tools
| Tool | Purpose |
|---|---|
agent_build | Create or update a micro-agent (name, role, goal, skills, template, schedule) |
agent_task | Add, update, complete, remove, or list tasks for an agent |
agent_run | Execute an agent's next pending task in the background |
agent_list | List all agents with status, task counts, and due dates |
agent_team | Create and run leader+worker teams (cost-optimized models) |
agent_loop | Run iterative research loops with diminishing returns detection |
agent_message | Send messages to an always-on agent's inbox |
How It Works
Creating an Agent
agent_build(
name="market-scout",
display_name="Market Scout",
role="Crypto market surveillance specialist",
goal="Monitor BTC and ETH prices, flag anomalies",
skills=["coingecko", "chart"],
template="monitor",
mode="research",
schedule="0 8 * * *",
timezone="Asia/Kuala_Lumpur"
)This creates:
workspace/agents/market-scout/
├── agent.yaml # Spec: name, role, goal, model, status, schedule
├── PROMPT.md # Rendered from template with role/goal injected
├── tasks.json # Task list with priorities and dependencies
├── memory/
│ └── MEMORY.md # Agent learns across runs (patterns, quirks, mistakes)
├── references/
│ ├── general-guide.md
│ └── monitoring-guide.md
├── scripts/ # Deterministic scripts the agent can run
└── output/ # Deliverables (reports, data files)Running an Agent
agent_run(agent="market-scout")What happens: 1. Loads agent.yaml and checks status is "active" 2. Reads tasks.json and picks the highest priority pending task (respects depends_on) 3. Builds a prompt from PROMPT.md + memory/MEMORY.md + task details + resource budget 4. Calls sessions_spawn to run the task in the background 5. Agent does the work, saves results to output/, updates memory/MEMORY.md 6. Push notification when done
Task System
Tasks have priorities that control how much resources the agent gets:
| Priority | Timeout | Max Tool Calls | Strategy |
|---|---|---|---|
low | 60s | 2 | Quick check |
medium | 120s | 5 | Standard work |
high | 180s | 10 | Deep analysis |
critical | 300s | 20 | Intensive |
agent_task(agent="market-scout", action="add",
title="Check BTC price",
priority="high",
due_date="2026-04-26",
recurring=True,
depends_on=["task-id-of-prerequisite"]
)Features:
- `depends_on` — Task chaining. Task B waits until Task A completes.
- `recurring` — Resets to pending after completion. Logs history of past runs.
- `max_retries` — Dead-letters a task after N consecutive failures (default: 3).
Managing Tasks
agent_task(agent="market-scout", action="list") # See all tasks
agent_task(agent="market-scout", action="complete", task_id="task-abc123")
agent_task(agent="market-scout", action="update", task_id="task-abc123", priority="critical")
agent_task(agent="market-scout", action="remove", task_id="task-abc123")
agent_task(agent="market-scout", action="set_status", status="paused") # Pause the agent3 Prompt Templates
| Template | For | Method |
|---|---|---|
default | General focused tasks | Simple rules: stay focused, use skills, save to output/ |
monitor | Surveillance and alerting | Baseline -> Compare -> Detect -> Trend. Severity: INFO / WARNING / ALERT |
researcher | Research and analysis | Scope -> Gather -> Cross-reference -> Synthesize -> Recommend |
2 Modes
| Mode | Behavior |
|---|---|
default | Free thinking. No constraints on reasoning. |
research | Hallucination guardrails enforced: must say "I don't know" when uncertain, cite sources for every claim, extract direct quotes before analyzing. |
4 Execution Patterns
1. Singular Agent
One agent, one task at a time. The simplest and cheapest pattern.
agent_build(name="btc-watcher", ...)
agent_task(agent="btc-watcher", action="add", title="Check BTC price", recurring=True)
agent_run(agent="btc-watcher")2. Team (Leader + Workers)
Leader delegates to workers, waits for results, synthesizes a final deliverable.
agent_team(action="create",
name="market-sentiment",
leader={"name": "analyst", "display_name": "Analyst", "role": "Synthesize cross-chain sentiment", "skills": ["chart"]},
workers=[
{"name": "btc-fetcher", "display_name": "BTC Fetcher", "role": "Fetch BTC tweets", "skills": ["twitter"]},
{"name": "eth-fetcher", "display_name": "ETH Fetcher", "role": "Fetch ETH tweets", "skills": ["twitter"]},
]
)
agent_team(action="run", name="market-sentiment",
goal="Cross-chain sentiment comparison for BTC and ETH")How it works: 1. Leader creates tasks for each worker via agent_task 2. Leader runs all workers via agent_run (parallel) 3. Leader waits with a bash poll loop (checks if workers wrote to output/) 4. Leader reads worker outputs 5. Leader synthesizes final deliverable
Cost optimization:
- Leader uses Sonnet ($3/$15 per M tokens) — plans and synthesizes
- Workers use Haiku ($1/$5 per M tokens) — execute subtasks
- ~67% savings vs using Sonnet for everything
Directory structure:
workspace/agents/market-sentiment/
├── team.json # Team config (leader, workers, models)
├── analyst/ # Leader agent directory
├── btc-fetcher/ # Worker agent directory
└── eth-fetcher/ # Worker agent directory3. Loop Mode (Iterative Research)
For open-ended tasks with no fixed endpoint. The agent iterates until the goal is satisfied or budget is exhausted.
agent_loop(agent="uni-scout",
goal="Find universities with AI programs open to collaboration",
max_iterations=10,
output_file="candidates.json",
timeout=1800
)Each iteration: 1. Diagnose — Read past iteration traces to identify patterns 2. Plan — Decide strategy based on diagnosis (avoid repeated failures) 3. Execute — Search, fetch, analyze 4. Update — Add findings to output file 5. Log — Create iterations/iter-NNN/trace.md and score.json 6. Evaluate — Continue or stop?
Budget regimes adapt strategy as iterations progress:
- EXPLORE (early) — Cast a wide net, try different approaches
- CONVERGE (middle) — Focus on what's working
- FOCUS (late) — Fill remaining gaps
- FINALIZE (end) — Polish and verify
Stopping conditions:
- Agent declares goal satisfied
- Max iterations reached
- 2 consecutive dry iterations (0 new items found)
4. Always-On Daemon
An agent that runs 24/7, polling every 5 minutes for messages and tasks.
agent_build(name="ai-scout", display_name="AI Scout",
role="Research AI content creators on Twitter",
goal="Build a database of high-quality AI creators",
skills=["twitter"],
template="researcher",
mode="research",
always_on=True
)
# Activate the daemon (job_id is in the build output)
scheduled_task(action="activate", job_id="<daemon_job_id>")Every 5 minutes, the daemon checks (in priority order): 1. Inbox — New messages? Process them, respond, push to user 2. Tasks — Pending tasks? Run the highest priority one 3. Autonomous work — Nothing queued? Proactively advance its goal (30-min cooldown to prevent burning tokens)
If the agent has nothing productive to do, it outputs AUTONOMOUS_IDLE and costs nothing.
Send messages to the agent at any time:
agent_message(agent="ai-scout", message="Focus on creators with 100K+ followers who post about LLMs")Messages queue in inbox.json. The daemon processes them on its next poll (max 5 min wait). Responses appear in outbox.json and push to the user.
Scheduling
Agents can be scheduled to run automatically:
| Format | Example | Description |
|---|---|---|
| Cron | "0 8 * * *" | Daily at 8:00 UTC |
| Interval | "every 30 minutes" | Repeating interval |
| Delay | "in 2 hours" | One-shot after delay |
| At | "at 2026-05-01 14:00" | One-shot at specific time |
Set timezone with timezone="Asia/Kuala_Lumpur" (default: UTC).
Self-Improving Memory
After every run, agents update memory/MEMORY.md with:
- Approaches that worked or failed
- API quirks discovered
- Thresholds and patterns found
- Mistakes to avoid
NOT stored: raw data, timestamps, intermediate results.
The next run reads this memory, so the agent gets better over time. Memory is scoped to each agent — no cross-contamination.
Managing Agents
| Action | Command |
|---|---|
| List all agents | agent_list() |
| List by team | agent_list(team="market-sentiment") |
| Pause an agent | agent_task(agent="x", action="set_status", status="paused") |
| Resume | agent_task(agent="x", action="set_status", status="active") |
| Archive | agent_task(agent="x", action="set_status", status="archived") |
| Kill daemon | scheduled_task(action="cancel", job_id="<id>") |
| Change config | Re-run agent_build with new params (overwrites) |
| Send instructions | agent_message(agent="x", message="change focus to...") |
| Check output | read_file("agents/x/output/results.json") |
| Delete | Remove the agent's directory — agents are just files |
Design Principles
1. Focused — Each agent does 1-2 things. Resist scope creep. 2. Isolated — Each agent has its own memory. Writes only to its own space. 3. Observable — Tasks have statuses, priorities, due dates, retry counts, history. 4. Composable — Create, pause, archive, delete independently. 5. Progressive — Context loads in layers: L1 metadata -> L2 prompt -> L3 references on demand. 6. Self-improving — Agents write learnings to memory after every run. 7. Cost-aware — Priority determines timeout, tool call budget, and model hint.
File Reference
| File | Purpose |
|---|---|
SKILL.md | Skill frontmatter + routing guide for the main agent |
__init__.py | Registers 7 tools with the Star Child tool registry |
tools.py | All 7 tool implementations (~2,000 lines) |
templates/default.md | General-purpose agent prompt template |
templates/monitor.md | Monitoring/surveillance agent prompt template |
templates/researcher.md | Research/analysis agent prompt template |
templates/scheduled_run.py | Self-contained script for scheduled agent execution |
templates/daemon_run.py | Self-contained script for always-on daemon agents |
references/general-guide.md | Universal guide: output format, skills, scripts, memory |
references/monitoring-guide.md | Guide for monitor-template agents: baselines, severity, trends |
references/research-guide.md | Guide for researcher-template agents: sources, conflicts, citations |
General Agent Guide
Output
- Lead with results, not process
- Save deliverables to
output/ - Structure output with clear headers
Working With Skills
1. read_file a skill's SKILL.md for instructions before using it 2. Follow the skill's documented patterns 3. Report errors clearly with what failed and alternatives
Working With Scripts
1. Read a script before running it to understand its interface 2. Run via bash("python agents/{your-name}/scripts/script_name.py [args]") 3. Prefer scripts over generating equivalent code — they're deterministic
Memory
After each run, update memory/MEMORY.md with:
- Approaches that worked and why
- API quirks or gotchas discovered
- Thresholds and baselines for comparison
- Do NOT write raw data or timestamps — only patterns
Monitoring Agent Guide
Method
1. Baseline — First run: record current values in memory 2. Compare — Subsequent runs: compare against baseline/thresholds 3. Detect — Flag deviations beyond expected ranges 4. Trend — After 3+ observations, note directional changes
Severity
- INFO — Notable but not concerning → log to memory
- WARNING — Approaching threshold or unusual → log + report
- ALERT — Threshold breached or anomaly → log + highlight + recommend action
Output Format
### Status: OK | WARNING | ALERT
**Checked:** [targets]
**Findings:** [observations]
**Anomalies:** [unusual or "None"]
**Action Required:** [recommendation or "None"]Thresholds
- Store in memory for consistency across runs
- When breached, report: threshold, actual value, delta, direction
- If none defined, set reasonable defaults and document them
Trends (3+ observations)
Look for: consistent direction, increasing volatility, cyclical patterns, sudden shifts
Research Agent Guide
Method
1. Scope — Clarify what the task asks before gathering data 2. Gather — Use multiple tools/sources to avoid single-source bias 3. Cross-reference — Verify key claims across sources 4. Synthesize — Connect findings into insights 5. Recommend — Provide actionable next steps
Output Format
### Summary
Key findings in one paragraph.
### Data
Facts organized by source.
### Analysis
Interpretation, connections, confidence level.
### Recommendations
Numbered actionable next steps.Source Quality
- Primary data (APIs, on-chain) > secondary data (articles)
- Note data freshness — when was it last updated?
- If sources conflict, present both with reliability assessment
Memory
After each run, save to memory:
- Key findings for future reference
- Source reliability notes
- Domain terminology learned
- Do NOT save raw data — only insights
#!/usr/bin/env python3
# -*- task-system: v3 -*-
"""
Always-on daemon for: __DISPLAY_NAME__
Agent: __AGENT_NAME__
Polls inbox for messages, runs pending tasks, writes to outbox.
"""
import requests, os, json, sys, re
from datetime import datetime, timezone as tz
JOB_ID = os.environ.get("JOB_ID")
AGENT_NAME = "__AGENT_NAME__"
AGENT_TEAM = "__TEAM_NAME__" # empty string if no team
WORKSPACE = os.environ.get("WORKSPACE_DIR", os.environ.get("PWD", "."))
BASE_URL = "http://localhost:8000"
# Resolve agent directory — use exact path baked at build time
if AGENT_TEAM:
AGENT_DIR = os.path.join(WORKSPACE, "agents", AGENT_TEAM, AGENT_NAME)
else:
AGENT_DIR = os.path.join(WORKSPACE, "agents", AGENT_NAME)
# Verify agent.yaml exists, otherwise search for the real location
if not os.path.exists(os.path.join(AGENT_DIR, "agent.yaml")):
found = False
agents_root = os.path.join(WORKSPACE, "agents")
if os.path.isdir(agents_root):
# Check flat first
flat = os.path.join(agents_root, AGENT_NAME, "agent.yaml")
if os.path.exists(flat):
AGENT_DIR = os.path.join(agents_root, AGENT_NAME)
found = True
else:
# Search team directories
for d in os.listdir(agents_root):
candidate = os.path.join(agents_root, d, AGENT_NAME, "agent.yaml")
if os.path.exists(candidate):
AGENT_DIR = os.path.join(agents_root, d, AGENT_NAME)
found = True
break
if not found:
print(f"Agent directory not found for {AGENT_NAME}", file=sys.stderr)
sys.exit(0)
INBOX = os.path.join(AGENT_DIR, "inbox.json")
OUTBOX = os.path.join(AGENT_DIR, "outbox.json")
OUTPUT_FILE = os.path.join(AGENT_DIR, "output", f"{AGENT_NAME}.json")
CHAT_ROOM = os.path.join(WORKSPACE, "agents", AGENT_TEAM, "chat.json") if AGENT_TEAM else None
RESPONSE_FORMAT = '\n\nIMPORTANT: Respond with ONLY JSON: {"summary": "...", "content": "..."}'
def get_chat_timeout():
"""Read agent-specific timeout from agent.yaml. Falls back to 300s."""
spec = read_yaml_simple(os.path.join(AGENT_DIR, "agent.yaml"))
t = spec.get("timeout", 300)
try:
return (10, int(t))
except (ValueError, TypeError):
return (10, 300)
DELEGATE_FORMAT = '\n\nIf you need to delegate subtasks to your workers, respond with ONLY JSON:\n{"action": "delegate", "tasks": [{"to": "worker-name", "content": "what to do"}, ...]}\nIf you are synthesizing final results, respond with:\n{"action": "synthesize", "summary": "...", "content": "..."}'
# ---------------------------------------------------------------------------
# Team Chat Room — shared message bus for inter-agent coordination
# ---------------------------------------------------------------------------
def read_chat():
"""Read team chat room. Returns list of messages."""
if not CHAT_ROOM or not os.path.exists(CHAT_ROOM):
return []
try:
with open(CHAT_ROOM, "r") as f:
return json.load(f)
except (json.JSONDecodeError, ValueError):
return []
def post_chat(to, content, msg_type="task"):
"""Post a message to the team chat room."""
if not CHAT_ROOM:
return
msgs = read_chat()
msgs.append({
"id": f"chat-{AGENT_NAME}-{datetime.now(tz.utc).strftime('%H%M%S')}",
"from": AGENT_NAME,
"to": to,
"content": content,
"type": msg_type, # "task", "result", "info"
"status": "pending",
"timestamp": datetime.now(tz.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
})
write_json(CHAT_ROOM, msgs)
def claim_chat_message(msg_id):
"""Atomically claim a pending message. Returns True only if THIS agent got the claim.
Uses read-claim-verify pattern: write our claim, then re-read to verify nobody
else claimed it between our read and write. 5-min polling makes collision unlikely
but this handles it gracefully if it happens.
"""
if not CHAT_ROOM:
return False
msgs = read_chat()
for m in msgs:
if m["id"] == msg_id and m.get("status") == "pending":
m["status"] = "picked_up"
m["claimed_by"] = AGENT_NAME
m["claimed_at"] = datetime.now(tz.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
write_json(CHAT_ROOM, msgs)
# Verify our claim stuck (another agent might have written between our read and write)
verify = read_chat()
for v in verify:
if v["id"] == msg_id:
if v.get("claimed_by") == AGENT_NAME:
return True
else:
print(f"[chat] Claim conflict on {msg_id} — {v.get('claimed_by')} got it", file=sys.stderr)
return False
return False
TASK_TIMEOUT_MINUTES = 15 # leader stops waiting for a worker after this
def complete_chat_message(msg_id, result_content):
"""Post result back to chat room AND nudge the leader's inbox for faster pickup."""
if not CHAT_ROOM:
return
msgs = read_chat()
original = None
for m in msgs:
if m["id"] == msg_id:
m["status"] = "done"
m["completed_at"] = datetime.now(tz.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
original = m
break
# Post result as a new message back to the sender
if original:
result_id = f"result-{AGENT_NAME}-{datetime.now(tz.utc).strftime('%H%M%S')}"
msgs.append({
"id": result_id,
"from": AGENT_NAME,
"to": original["from"],
"content": result_content,
"type": "result",
"in_reply_to": msg_id,
"status": "pending",
"timestamp": datetime.now(tz.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
})
write_json(CHAT_ROOM, msgs)
# Push-notify the leader — write a nudge to their inbox so they process
# results on their NEXT poll instead of waiting for process_team_chat
leader_name = original["from"]
if leader_name != AGENT_NAME:
leader_dir = None
if AGENT_TEAM:
candidate = os.path.join(WORKSPACE, "agents", AGENT_TEAM, leader_name)
if os.path.isdir(candidate):
leader_dir = candidate
if not leader_dir:
candidate = os.path.join(WORKSPACE, "agents", leader_name)
if os.path.isdir(candidate):
leader_dir = candidate
if leader_dir:
leader_inbox = os.path.join(leader_dir, "inbox.json")
inbox = read_json(leader_inbox)
inbox.append({
"id": f"nudge-{AGENT_NAME}-{datetime.now(tz.utc).strftime('%H%M%S')}",
"from": AGENT_NAME,
"message": f"[CHAT_RESULT] Task complete. Check chat room for results.",
"type": "chat_nudge",
"timestamp": datetime.now(tz.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"processed": False,
})
write_json(leader_inbox, inbox)
print(f"[chat] Nudged leader {leader_name} via inbox", file=sys.stderr)
def get_my_pending_chat():
"""Get pending chat messages addressed to this agent."""
msgs = read_chat()
return [m for m in msgs if m.get("to") == AGENT_NAME and m.get("status") == "pending" and m.get("type") == "task"]
def get_my_results():
"""Get result messages addressed to this agent (responses from workers)."""
msgs = read_chat()
return [m for m in msgs if m.get("to") == AGENT_NAME and m.get("status") == "pending" and m.get("type") == "result"]
def get_teammates():
"""Find teammates in the same team. Returns list of (name, display_name, dir_path)."""
if not AGENT_TEAM:
return []
teammates = []
team_dir = os.path.join(WORKSPACE, "agents", AGENT_TEAM)
if not os.path.isdir(team_dir):
return []
for entry in sorted(os.listdir(team_dir)):
if entry == AGENT_NAME or entry.startswith("."):
continue
mate_yaml = os.path.join(team_dir, entry, "agent.yaml")
if os.path.exists(mate_yaml):
mate_spec = read_yaml_simple(mate_yaml)
if mate_spec.get("status") == "active":
teammates.append((entry, mate_spec.get("display_name", entry), os.path.join(team_dir, entry)))
return teammates
def build_team_context():
"""Build team context string for prompts."""
teammates = get_teammates()
if not teammates:
return ""
lines = [f"\n\n## Team: {AGENT_TEAM}", "You are part of a team. Teammates' output is available:"]
for name, display, path in teammates:
rel = f"agents/{AGENT_TEAM}/{name}"
lines.append(f"- **{display}** (`{name}`) → `{rel}/output/`")
lines.append("Use `read_file` to access their deliverables.")
lines.append(f"To message a teammate: use `agent_message(agent=\"<name>\", message=\"...\", from_agent=\"{AGENT_NAME}\")`")
return "\n".join(lines)
def notify_teammates(summary):
"""Notify all teammates that this agent has new output."""
teammates = get_teammates()
for name, display, path in teammates:
mate_inbox = os.path.join(path, "inbox.json")
inbox = read_json(mate_inbox)
inbox.append({
"id": f"auto-{AGENT_NAME}-{datetime.now(tz.utc).strftime('%H%M%S')}",
"from": AGENT_NAME,
"message": f"[AUTO] I've updated my output. Summary: {summary}",
"timestamp": datetime.now(tz.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"processed": False,
})
write_json(mate_inbox, inbox)
def push(message, channel="all", title=None):
payload = {"message": message, "channel": channel, "job_id": JOB_ID}
if title:
payload["title"] = title
try:
requests.post(f"{BASE_URL}/push", json=payload, timeout=10)
except Exception as e:
print(f"Push failed: {e}", file=sys.stderr)
def read_file(path):
try:
with open(path, "r") as f:
return f.read()
except FileNotFoundError:
return ""
def read_json(path):
try:
with open(path, "r") as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
return []
def write_json(path, data):
with open(path, "w") as f:
json.dump(data, f, indent=2, default=str)
def read_yaml_simple(path):
data = {}
content = read_file(path)
current_key = None
for line in content.splitlines():
s = line.strip()
if not s or s.startswith("#"):
continue
if s.startswith("- ") and current_key and isinstance(data.get(current_key), list):
data[current_key].append(s[2:].strip().strip("'\""))
continue
if ":" in s:
k, _, v = s.partition(":")
k, v = k.strip(), v.strip().strip("'\"")
if not v:
data[k] = []
current_key = k
elif v.lower() == "true":
data[k] = True
elif v.lower() == "false":
data[k] = False
else:
data[k] = v
if v:
current_key = k
return data
def extract_json_response(text):
if not text:
return None
s = text.strip()
fence = re.match(r"^```(?:json)?\s*\n(.*?)\n```\s*$", s, re.DOTALL)
if fence:
s = fence.group(1).strip()
try:
return json.loads(s)
except json.JSONDecodeError:
pass
start, end = s.find("{"), s.rfind("}")
if start != -1 and end > start:
try:
return json.loads(s[start:end + 1])
except json.JSONDecodeError:
pass
return None
def process_inbox(spec):
"""Check inbox for messages, process them, write responses to outbox."""
inbox = read_json(INBOX)
if not inbox:
return False # nothing to do
# Take unprocessed messages
unprocessed = [m for m in inbox if not m.get("processed")]
if not unprocessed:
return False
display = spec.get("display_name", AGENT_NAME)
prompt_base = read_file(os.path.join(AGENT_DIR, "PROMPT.md"))
memory = read_file(os.path.join(AGENT_DIR, "memory", "MEMORY.md")) or "(No memory yet)"
for msg in unprocessed:
# Chat nudges — just mark processed, let process_team_chat handle the actual results
if msg.get("type") == "chat_nudge":
msg["processed"] = True
msg["processed_at"] = datetime.now(tz.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
print(f"[inbox] Chat nudge from {msg.get('from')} — will check chat room next", file=sys.stderr)
continue
# Build prompt — leaders with teams get delegation format
teammates = get_teammates()
if CHAT_ROOM and teammates:
# Leader mode — can delegate to workers via chat room
worker_list = "\n".join(f"- `{n}`: {d}" for n, d, _ in teammates)
prompt = f"""# {display} — Message Processing (Team Leader)
{prompt_base}
## Your Memory
{memory}
## Your Workers
{worker_list}
## Incoming Message
From: {msg.get('from', 'user')}
Message: {msg.get('message', '')}
## How to Respond
If this task needs multiple workers, delegate by responding with:
{{"action": "delegate", "tasks": [{{"to": "worker-name", "content": "what to do"}}, ...]}}
If you can handle it yourself or are synthesizing results, respond with:
{{"summary": "...", "content": "..."}}
"""
else:
# Regular agent — just respond
prompt = f"""# {display} — Message Processing
{prompt_base}
## Your Memory
{memory}
## Incoming Message
From: {msg.get('from', 'user')}
Message: {msg.get('message', '')}
Respond to this message based on your role and goal.
"""
prompt += RESPONSE_FORMAT
try:
resp = requests.post(f"{BASE_URL}/chat", json={
"message": prompt,
"call_source": "task",
"internal_options": {"job_id": JOB_ID},
}, timeout=get_chat_timeout())
reply = ""
content = ""
summary = display
if resp.ok:
reply = resp.json().get("reply", "")
data = extract_json_response(reply)
# Check if leader is delegating
if data and isinstance(data, dict) and data.get("action") == "delegate" and CHAT_ROOM:
tasks_to_delegate = data.get("tasks", [])
for t in tasks_to_delegate:
post_chat(to=t["to"], content=t["content"], msg_type="task")
print(f"[inbox] Delegated to {t['to']}: {t['content'][:60]}", file=sys.stderr)
content = f"Delegated {len(tasks_to_delegate)} tasks to workers. Waiting for results."
summary = f"{display}: delegated"
elif data and isinstance(data, dict) and data.get("content"):
summary = data.get("summary", display)
content = data["content"]
else:
content = reply
else:
content = f"Error: {resp.status_code}"
# Write to outbox
outbox = read_json(OUTBOX)
outbox.append({
"from": AGENT_NAME,
"to": msg.get("from", "user"),
"in_reply_to": msg.get("id"),
"message": content,
"summary": summary,
"timestamp": datetime.now(tz.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
})
write_json(OUTBOX, outbox)
# Push to user if message was from user (but not delegation confirmations)
if msg.get("from", "user") == "user" and content.strip() and "Delegated" not in content:
push(content, title=summary)
except Exception as e:
print(f"Error processing message: {e}", file=sys.stderr)
# Mark as processed
msg["processed"] = True
msg["processed_at"] = datetime.now(tz.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
# Trim processed messages older than 50 entries to prevent unbounded growth
processed = [m for m in inbox if m.get("processed")]
if len(processed) > 50:
# Keep only the 20 most recent processed messages + all unprocessed
recent_processed = sorted(processed, key=lambda m: m.get("processed_at", ""), reverse=True)[:20]
unprocessed = [m for m in inbox if not m.get("processed")]
inbox = unprocessed + recent_processed
write_json(INBOX, inbox)
return True
def process_team_chat(spec):
"""Check team chat room for tasks addressed to this agent. Workers pick up tasks, leaders check for results."""
if not CHAT_ROOM:
return False
display = spec.get("display_name", AGENT_NAME)
role = spec.get("role", "")
# --- WORKER PATH: Pick up pending tasks from the chat room ---
pending_tasks = get_my_pending_chat()
if pending_tasks:
task_msg = pending_tasks[0] # process one per poll cycle
if not claim_chat_message(task_msg["id"]):
return False # someone else claimed it
print(f"[chat] Processing task from {task_msg['from']}: {task_msg['content'][:80]}", file=sys.stderr)
# Build worker prompt
prompt_base = read_file(os.path.join(AGENT_DIR, "PROMPT.md"))
memory = read_file(os.path.join(AGENT_DIR, "memory", "MEMORY.md")) or "(No memory yet)"
task_section = f"**Task from team chat:** {task_msg['content']}\n**Assigned by:** {task_msg['from']}"
prompt = prompt_base
prompt = prompt.replace("{task_section}", task_section)
prompt = prompt.replace("{memory_content}", memory)
prompt = prompt.replace("{memory_path}", os.path.join(AGENT_DIR, "memory", "MEMORY.md"))
prompt = prompt.replace("{output_path}", os.path.join(AGENT_DIR, "output"))
prompt = prompt.replace("{output_file}", f"{AGENT_NAME}.json")
prompt = prompt.replace("{references_section}", "(load from references/ if needed)")
prompt += build_team_context()
prompt += RESPONSE_FORMAT
try:
resp = requests.post(f"{BASE_URL}/chat", json={
"message": prompt,
"call_source": "task",
"internal_options": {"job_id": JOB_ID},
}, timeout=get_chat_timeout())
if resp.ok:
reply = resp.json().get("reply", "")
data = extract_json_response(reply)
if data and isinstance(data, dict) and data.get("content"):
content = data["content"]
else:
content = reply
complete_chat_message(task_msg["id"], content)
print(f"[chat] Completed task, posted result back to {task_msg['from']}", file=sys.stderr)
else:
complete_chat_message(task_msg["id"], f"Error: /chat returned {resp.status_code}")
except Exception as e:
print(f"[chat] Error processing task: {e}", file=sys.stderr)
complete_chat_message(task_msg["id"], f"Error: {e}")
return True
# --- LEADER PATH: Check for results from workers ---
results = get_my_results()
if results:
# Collect all pending results
result_texts = []
for r in results:
result_texts.append(f"**{r['from']}:** {r['content']}")
claim_chat_message(r["id"])
# Check if there are still outstanding tasks we sent that haven't been completed
all_msgs = read_chat()
my_outgoing = [m for m in all_msgs if m.get("from") == AGENT_NAME and m.get("type") == "task"]
still_pending = [m for m in my_outgoing if m.get("status") in ("pending", "picked_up")]
# Check for timed-out tasks — don't wait forever for silent failures
timed_out = []
truly_pending = []
for sp in still_pending:
ts = sp.get("timestamp", "")
try:
task_time = datetime.fromisoformat(ts.replace("Z", "+00:00"))
now = datetime.now(tz.utc)
elapsed = (now - task_time).total_seconds() / 60
if elapsed > TASK_TIMEOUT_MINUTES:
sp["status"] = "timed_out"
timed_out.append(sp)
else:
truly_pending.append(sp)
except (ValueError, TypeError):
truly_pending.append(sp)
if timed_out:
write_json(CHAT_ROOM, all_msgs)
for t in timed_out:
result_texts.append(f"**{t['to']}:** (TIMED OUT — no response after {TASK_TIMEOUT_MINUTES} min)")
print(f"[chat] Task to {t['to']} timed out after {TASK_TIMEOUT_MINUTES} min", file=sys.stderr)
if truly_pending:
print(f"[chat] Have {len(results)} results but {len(truly_pending)} tasks still pending, waiting...", file=sys.stderr)
return False # wait for remaining workers
# All workers done — synthesize
print(f"[chat] All workers done ({len(result_texts)} results). Synthesizing...", file=sys.stderr)
memory = read_file(os.path.join(AGENT_DIR, "memory", "MEMORY.md")) or "(No memory yet)"
synthesis_prompt = f"""# {display} — Synthesis
You are **{display}**, the team leader. Your workers have completed their tasks.
**Role:** {role}
## Worker Results
{chr(10).join(result_texts)}
## Instructions
Synthesize all worker results into a single coherent deliverable.
Save the synthesis to your output directory.
Write learnings to your memory file.
"""
synthesis_prompt += RESPONSE_FORMAT
try:
resp = requests.post(f"{BASE_URL}/chat", json={
"message": synthesis_prompt,
"call_source": "task",
"internal_options": {"job_id": JOB_ID},
}, timeout=get_chat_timeout())
if resp.ok:
reply = resp.json().get("reply", "")
data = extract_json_response(reply)
if data and isinstance(data, dict) and data.get("content"):
content = data["content"]
summary = data.get("summary", f"{display} Brief")
else:
content = reply
summary = f"{display} Brief"
if content.strip():
push(content, title=summary)
print(f"[chat] Synthesis complete, pushed to user", file=sys.stderr)
except Exception as e:
print(f"[chat] Synthesis error: {e}", file=sys.stderr)
return True
return False
def process_tasks(spec):
"""Pick and run the next pending task, same as scheduled_run.py."""
tasks_path = os.path.join(AGENT_DIR, "tasks.json")
try:
with open(tasks_path) as f:
tasks = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
return False
priority_order = {"critical": 0, "high": 1, "medium": 2, "low": 3}
pending = [t for t in tasks if t.get("status") == "pending"]
# Check depends_on
completed_ids = set()
for t in tasks:
if t.get("status") == "completed":
completed_ids.add(t["id"])
elif t.get("recurring") and t.get("history"):
completed_ids.add(t["id"])
eligible = []
for t in pending:
deps = t.get("depends_on") or []
if isinstance(deps, str):
deps = [deps]
if all(d in completed_ids for d in deps):
eligible.append(t)
if not eligible:
return False
eligible.sort(key=lambda t: (
priority_order.get(t.get("priority", "medium"), 2),
t.get("due_date") or "9999-12-31",
))
task = eligible[0]
# Build prompt
display = spec.get("display_name", AGENT_NAME)
prompt_base = read_file(os.path.join(AGENT_DIR, "PROMPT.md"))
memory = read_file(os.path.join(AGENT_DIR, "memory", "MEMORY.md")) or "(No memory yet)"
memory_path = os.path.join(AGENT_DIR, "memory", "MEMORY.md")
task_section = (
f"**Task:** {task['title']}\n"
f"**Description:** {task.get('description', 'N/A')}\n"
f"**Priority:** {task.get('priority', 'medium')}\n"
f"**Due:** {task.get('due_date') or 'No deadline'}"
)
# Mark in progress
task["status"] = "in_progress"
with open(tasks_path, "w") as f:
json.dump(tasks, f, indent=2)
prompt = prompt_base
prompt = prompt.replace("{task_section}", task_section)
prompt = prompt.replace("{memory_content}", memory)
prompt = prompt.replace("{memory_path}", memory_path)
prompt = prompt.replace("{output_path}", os.path.join(AGENT_DIR, "output"))
prompt = prompt.replace("{output_file}", f"{AGENT_NAME}.json")
prompt = prompt.replace("{references_section}", "(load from references/ if needed)")
# Team context — so agent can see and message teammates
prompt += build_team_context()
# Research mode
if spec.get("mode") == "research":
guardrails_path = os.path.join(AGENT_DIR, "references", "research-guardrails.md")
guardrails = read_file(guardrails_path)
if guardrails.strip():
prompt += "\n\n" + guardrails
prompt += f"\n\nAfter completing, write learnings to `{memory_path}`.\n"
prompt += RESPONSE_FORMAT
try:
resp = requests.post(f"{BASE_URL}/chat", json={
"message": prompt,
"call_source": "task",
"internal_options": {"job_id": JOB_ID},
}, timeout=get_chat_timeout())
content = ""
summary = display
if resp.ok:
reply = resp.json().get("reply", "")
data = extract_json_response(reply)
if data and isinstance(data, dict) and data.get("content"):
content = data["content"]
summary = data.get("summary", display)
else:
content = reply
if content.strip():
push(content, title=summary)
# Complete task
now = datetime.now(tz.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
result_text = (content[:500] + "...") if len(content) > 500 else content
if "history" not in task:
task["history"] = []
task["history"].append({"completed": now, "result": result_text})
task["result"] = result_text
if task.get("recurring"):
task["status"] = "pending"
task["completed"] = None
else:
task["status"] = "completed"
task["completed"] = now
with open(tasks_path, "w") as f:
json.dump(tasks, f, indent=2)
# Auto-notify teammates that we have new output
if AGENT_TEAM and content.strip():
try:
notify_teammates(summary)
print(f"[daemon] Notified teammates of completion: {summary}", file=sys.stderr)
except Exception as ne:
print(f"[daemon] Failed to notify teammates: {ne}", file=sys.stderr)
except Exception as e:
print(f"Error running task: {e}", file=sys.stderr)
retry_count = task.get("retry_count", 0) + 1
max_retries = task.get("max_retries", 3)
task["retry_count"] = retry_count
if retry_count >= max_retries:
task["status"] = "failed"
task["result"] = f"Dead-lettered after {retry_count} attempts: {e}"
else:
task["status"] = "pending"
with open(tasks_path, "w") as f:
json.dump(tasks, f, indent=2)
return True
AUTONOMOUS_COOLDOWN_MINUTES = 30 # don't do autonomous work more than once per 30 min
def do_autonomous_work(spec):
"""Proactively pursue the agent's goal when there are no messages or tasks."""
state_file = os.path.join(AGENT_DIR, ".daemon_state.json")
now_str = datetime.now(tz.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
# Update timestamp FIRST — even if we crash, don't retry for another 30 min
state = {}
try:
with open(state_file) as f:
state = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
pass
# Check cooldown
last_run_str = state.get("last_autonomous", "")
if last_run_str:
try:
# Parse ISO timestamp — handle both +00:00 and Z suffixes
last_ts = last_run_str.replace("Z", "+00:00")
now_ts = now_str.replace("Z", "+00:00")
from datetime import datetime as dt_cls
last_dt = dt_cls.fromisoformat(last_ts)
now_dt = dt_cls.fromisoformat(now_ts)
elapsed = (now_dt - last_dt).total_seconds() / 60
if elapsed < AUTONOMOUS_COOLDOWN_MINUTES:
return False # too soon
except Exception as e:
print(f"[autonomous] Cooldown parse error: {e}, running anyway", file=sys.stderr)
# Write timestamp NOW — prevents re-entry on crash
state["last_autonomous"] = now_str
state["last_status"] = "started"
write_json(state_file, state)
goal = spec.get("goal", "")
if not goal:
print(f"[autonomous] No goal set for {AGENT_NAME}, skipping", file=sys.stderr)
state["last_status"] = "no_goal"
write_json(state_file, state)
return False
print(f"[autonomous] Starting autonomous work for {AGENT_NAME}", file=sys.stderr)
try:
display = spec.get("display_name", AGENT_NAME)
role = spec.get("role", "")
memory = read_file(os.path.join(AGENT_DIR, "memory", "MEMORY.md")) or "(No memory yet)"
# Check existing output
output_dir = os.path.join(AGENT_DIR, "output")
existing_files = []
if os.path.isdir(output_dir):
existing_files = [f for f in os.listdir(output_dir) if os.path.isfile(os.path.join(output_dir, f))]
output_summary = ""
if existing_files:
output_summary = f"\n\nYou have existing output files: {', '.join(existing_files)}"
output_summary += "\nRead them with read_file to see what you've already collected."
else:
output_summary = "\n\nNo output files yet — this may be your first autonomous run."
# Build team context
team_context = build_team_context()
# Build output contract
output_rel = f"agents/{AGENT_TEAM}/{AGENT_NAME}" if AGENT_TEAM else f"agents/{AGENT_NAME}"
output_contract = f"""
## Output Contract
- **Primary output:** `{output_rel}/output/{AGENT_NAME}.json` (JSON array)
- Read existing file before writing. Append new items. NEVER overwrite from scratch.
- Deduplicate: check primary identifier before adding.
"""
prompt = f"""# {display} — Autonomous Work
You are **{display}**. You are always on, proactively pursuing your goal.
**Role:** {role}
**Goal:** {goal}
## Your Memory
{memory}
{output_summary}
{output_contract}
{team_context}
## Instructions
You have no pending tasks or messages. Use this time to proactively advance your goal:
1. Read your memory to see what you've done before
2. Read your existing output files to see current state
3. Decide what would be most valuable to do next
4. Do it — search, fetch, analyze, whatever advances your goal
5. Update your output files with any new findings (JSON first)
6. Update your memory with what you learned
If you have nothing productive to do (goal is fully satisfied), just output: AUTONOMOUS_IDLE
Be efficient — you get one autonomous work session every {AUTONOMOUS_COOLDOWN_MINUTES} minutes.
"""
# Research mode guardrails
if spec.get("mode") == "research":
guardrails_path = os.path.join(AGENT_DIR, "references", "research-guardrails.md")
guardrails = read_file(guardrails_path)
if guardrails.strip():
prompt += "\n\n" + guardrails
prompt += RESPONSE_FORMAT
print(f"[autonomous] Calling /chat for {AGENT_NAME} (prompt: {len(prompt)} chars)", file=sys.stderr)
resp = requests.post(f"{BASE_URL}/chat", json={
"message": prompt,
"call_source": "task",
"internal_options": {"job_id": JOB_ID},
}, timeout=get_chat_timeout())
print(f"[autonomous] /chat response: status={resp.status_code}", file=sys.stderr)
if not resp.ok:
print(f"[autonomous] /chat failed: {resp.status_code} {resp.text[:200]}", file=sys.stderr)
state["last_status"] = f"chat_error_{resp.status_code}"
write_json(state_file, state)
return False
reply = resp.json().get("reply", "")
print(f"[autonomous] Reply length: {len(reply)} chars", file=sys.stderr)
if not reply.strip():
print(f"[autonomous] Empty reply from /chat", file=sys.stderr)
state["last_status"] = "empty_reply"
write_json(state_file, state)
return False
if "AUTONOMOUS_IDLE" in reply:
print(f"[autonomous] Agent says idle", file=sys.stderr)
state["last_status"] = "idle"
write_json(state_file, state)
return True
# Parse and push response
data = extract_json_response(reply)
if data and isinstance(data, dict) and data.get("content"):
content = data["content"]
summary = data.get("summary", f"{display} update")
else:
content = reply
summary = f"{display} update"
if content.strip():
push(content, title=summary)
print(f"[autonomous] Pushed result: {summary}", file=sys.stderr)
# Auto-notify teammates
if AGENT_TEAM and content.strip() and "AUTONOMOUS_IDLE" not in reply:
try:
notify_teammates(summary)
print(f"[autonomous] Notified teammates", file=sys.stderr)
except Exception as ne:
print(f"[autonomous] Notify failed: {ne}", file=sys.stderr)
state["last_status"] = "success"
write_json(state_file, state)
return True
except Exception as e:
print(f"[autonomous] ERROR: {e}", file=sys.stderr)
import traceback
traceback.print_exc(file=sys.stderr)
state["last_status"] = f"error: {str(e)[:100]}"
write_json(state_file, state)
return False
def main():
spec = read_yaml_simple(os.path.join(AGENT_DIR, "agent.yaml"))
if spec.get("status") != "active":
return # silent exit — agent paused/archived
# Priority order:
# 1. Direct inbox messages (user → agent)
# 2. Team chat room (inter-agent coordination)
# 3. Pending tasks (explicit task queue)
# 4. Autonomous work (proactive goal pursuit)
did_inbox = process_inbox(spec)
if did_inbox:
return
did_chat = process_team_chat(spec)
if did_chat:
return
did_task = process_tasks(spec)
if did_task:
return
do_autonomous_work(spec)
if __name__ == "__main__":
main()
{display_name}
You are {display_name}, a focused micro-agent with a specific role and goal.
Role: {role} Goal: {goal}
Rules
1. Stay focused on your role and goal. Do not drift. 2. Use only the skills and tools available to you. 3. If blocked, explain what's needed. Don't guess or fabricate.
Pre-Action Reasoning (REQUIRED)
Before EVERY tool call, think through:
- WHO is affected by this action?
- WHAT exactly will you do? (tool name, parameters)
- WHY does this advance your goal?
- RISK — is this safe, moderate, or destructive?
- Destructive (delete, overwrite, clear): STOP. Generate a preview of what would change. Do NOT execute without confirmation.
Output Contract (MANDATORY)
You MUST write results to your designated output file. Markdown reports alone are NOT acceptable — downstream agents and systems read JSON.
- Primary output:
{output_path}/{output_file}(JSON array) - Format: JSON first, human summary second
- On every run: Read the existing file, append new items, write back. NEVER overwrite from scratch.
Deduplication
Before adding any item to output, check if it already exists (match on primary identifier — name, handle, URL, or ID). Skip duplicates. Log skip count.
Resource Ownership
You own your output/, memory/, and scripts/ directories. Do NOT write to other agents' directories unless explicitly instructed. If multiple agents feed the same destination, write to YOUR output and let a designated sync agent handle the merge.
Current Task
{task_section}
References
Detailed guides at agents/{agent_name}/references/ — only load if you hit an edge case. {references_section}
Your Memory
{memory_content}
{display_name}
You are {display_name}, a surveillance and monitoring micro-agent.
Role: {role} Goal: {goal}
Rules
1. Check targets systematically. Compare against thresholds or previous readings from memory. 2. Flag anomalies: INFO (notable), WARNING (approaching threshold), ALERT (breached). 3. When threshold breached: state threshold, actual value, delta, and recommended action. 4. First run with no baseline: record current values in memory for future comparison.
Pre-Action Reasoning (REQUIRED)
Before EVERY tool call, think through:
- WHO is affected by this action?
- WHAT exactly will you do? (tool name, parameters)
- WHY does this advance your goal?
- RISK — is this safe, moderate, or destructive?
- Destructive (delete, overwrite, clear): STOP. Do NOT execute without confirmation.
Output Contract (MANDATORY)
You MUST write structured results to your designated output file. Markdown alerts alone are NOT enough.
- Primary output:
{output_path}/{output_file}(JSON) - Format:
{"timestamp": "...", "status": "OK|WARNING|ALERT", "readings": [...], "anomalies": [...], "action_required": "..."} - On every run: Read existing file → append new reading → write back. Keep history.
- Human summary: Write Status/Checked/Findings/Anomalies/Action Required AFTER JSON is saved.
Deduplication
If checking the same targets across runs, compare new readings against previous ones in your output file. Only flag changes, not repeated identical states.
Resource Ownership
You own your output/, memory/, and scripts/ directories only. Do NOT write to other agents' directories.
Current Task
{task_section}
References
Detailed guides at agents/{agent_name}/references/ — only load if methodology is unfamiliar. {references_section}
Your Memory
{memory_content}
{display_name}
You are {display_name}, a research and analysis micro-agent.
Role: {role} Goal: {goal}
Rules
1. Use multiple sources. Don't conclude from a single source. 2. Separate facts (data) from interpretation (analysis). Cite which tool/API provided each data point. 3. If sources conflict, present both with your reliability assessment. 4. Primary data (APIs, on-chain) beats secondary data (articles). Note data freshness.
Pre-Action Reasoning (REQUIRED)
Before EVERY tool call, think through:
- WHO is affected by this action?
- WHAT exactly will you do? (tool name, parameters)
- WHY does this advance your goal?
- RISK — is this safe, moderate, or destructive?
- Destructive (delete, overwrite, clear): STOP. Do NOT execute without confirmation.
Output Contract (MANDATORY)
You MUST write structured results to your designated output file. Pretty markdown is NOT enough — downstream agents read JSON.
- Primary output:
{output_path}/{output_file}(JSON array) - Format: Each item must be a complete record with all fields populated
- Structure:
{"name": "...", "source": "...", "data": {...}, "fit_score": N, "notes": "..."} - On every run: Read existing file → append new items → write back. NEVER overwrite.
- Human summary: Write AFTER the JSON is saved, not instead of it.
Targeting Criteria
Apply explicit bounds to your research targets:
- Relevance: Must directly relate to your goal. Adjacent/tangential = skip.
- Reachability: Prefer targets you can actually contact or engage. Celebrity accounts with no DMs/email = low priority.
- Fit score: Rate 1-10. Only include items scoring 6+. Document your scoring rationale.
- If your task specifies bounds (follower range, geography, etc.), respect them strictly.
Deduplication
Before adding any item to output, check if it already exists (match on primary identifier). Skip duplicates. Log: "Skipped N duplicates."
Resource Ownership
You own your output/, memory/, and scripts/ directories only. Do NOT write to other agents' directories.
Current Task
{task_section}
References
Detailed guides at agents/{agent_name}/references/ — only load if methodology is unfamiliar. {references_section}
Your Memory
{memory_content}
#!/usr/bin/env python3
# -*- task-system: v3 -*-
"""
Scheduled runner for: __DISPLAY_NAME__
Agent: __AGENT_NAME__
"""
import requests, os, json, sys, re
from datetime import datetime, timezone as tz
JOB_ID = os.environ.get("JOB_ID")
AGENT_NAME = "__AGENT_NAME__"
AGENT_TEAM = "__TEAM_NAME__"
WORKSPACE = os.environ.get("WORKSPACE_DIR", os.environ.get("PWD", "."))
BASE_URL = "http://localhost:8000"
# Resolve agent directory — exact path baked at build time
if AGENT_TEAM:
AGENT_DIR = os.path.join(WORKSPACE, "agents", AGENT_TEAM, AGENT_NAME)
else:
AGENT_DIR = os.path.join(WORKSPACE, "agents", AGENT_NAME)
# Verify agent.yaml exists, otherwise search
if not os.path.exists(os.path.join(AGENT_DIR, "agent.yaml")):
agents_root = os.path.join(WORKSPACE, "agents")
found = False
if os.path.isdir(agents_root):
flat = os.path.join(agents_root, AGENT_NAME, "agent.yaml")
if os.path.exists(flat):
AGENT_DIR = os.path.join(agents_root, AGENT_NAME)
found = True
else:
for d in os.listdir(agents_root):
candidate = os.path.join(agents_root, d, AGENT_NAME, "agent.yaml")
if os.path.exists(candidate):
AGENT_DIR = os.path.join(agents_root, d, AGENT_NAME)
found = True
break
if not found:
print(f"Agent directory not found for {AGENT_NAME}", file=sys.stderr)
sys.exit(0)
RESPONSE_FORMAT = '\n\nIMPORTANT: You must respond with ONLY a JSON object, no markdown fences, no other text:\n{"summary": "<short one-line title>", "content": "<full detailed response>"}'
def push(message, channel="all", title=None):
payload = {"message": message, "channel": channel, "job_id": JOB_ID}
if title:
payload["title"] = title
try:
requests.post(f"{BASE_URL}/push", json=payload, timeout=10)
except Exception as e:
print(f"Push failed: {e}", file=sys.stderr)
def read_file(path):
try:
with open(path, "r") as f:
return f.read()
except FileNotFoundError:
return ""
def read_yaml_simple(path):
"""Read flat key:value YAML."""
data = {}
content = read_file(path)
current_key = None
for line in content.splitlines():
s = line.strip()
if not s or s.startswith("#"):
continue
if s.startswith("- ") and current_key and isinstance(data.get(current_key), list):
data[current_key].append(s[2:].strip().strip("'\""))
continue
if ":" in s:
k, _, v = s.partition(":")
k, v = k.strip(), v.strip().strip("'\"")
if not v:
data[k] = []
current_key = k
elif v.lower() == "true":
data[k] = True
elif v.lower() == "false":
data[k] = False
else:
data[k] = v
if v:
current_key = k
return data
def extract_json_response(text):
"""Parse JSON from agent response, handling markdown fences."""
if not text:
return None
s = text.strip()
fence = re.match(r"^```(?:json)?\s*\n(.*?)\n```\s*$", s, re.DOTALL)
if fence:
s = fence.group(1).strip()
try:
return json.loads(s)
except json.JSONDecodeError:
pass
start, end = s.find("{"), s.rfind("}")
if start != -1 and end > start:
try:
return json.loads(s[start:end + 1])
except json.JSONDecodeError:
pass
return None
def main():
# 1. Load agent spec
spec = read_yaml_simple(os.path.join(AGENT_DIR, "agent.yaml"))
if spec.get("status") != "active":
print(f"Agent {AGENT_NAME} is {spec.get('status', 'unknown')}, skipping", file=sys.stderr)
return
# 2. Pick next pending task
tasks_path = os.path.join(AGENT_DIR, "tasks.json")
try:
with open(tasks_path) as f:
tasks = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
tasks = []
priority_order = {"critical": 0, "high": 1, "medium": 2, "low": 3}
pending = [t for t in tasks if t.get("status") == "pending"]
pending.sort(key=lambda t: (
priority_order.get(t.get("priority", "medium"), 2),
t.get("due_date") or "9999-12-31",
))
task = pending[0] if pending else None
if task:
task_section = (
f"**Task:** {task['title']}\n"
f"**Description:** {task.get('description', 'N/A')}\n"
f"**Priority:** {task.get('priority', 'medium')}\n"
f"**Due:** {task.get('due_date') or 'No deadline'}"
)
task["status"] = "in_progress"
with open(tasks_path, "w") as f:
json.dump(tasks, f, indent=2)
else:
task_section = "No pending tasks. Review your goal and identify what needs to be done next."
# 3. Build prompt from PROMPT.md + runtime context
base_prompt = read_file(os.path.join(AGENT_DIR, "PROMPT.md"))
memory = read_file(os.path.join(AGENT_DIR, "memory", "MEMORY.md")) or "(No memory yet)"
memory_path = f"agents/{AGENT_NAME}/memory/MEMORY.md"
output_path = f"agents/{AGENT_NAME}/output"
# References
refs_dir = os.path.join(AGENT_DIR, "references")
refs = sorted(f for f in os.listdir(refs_dir) if f.endswith(".md")) if os.path.isdir(refs_dir) else []
refs_section = "\n".join(f"- `agents/{AGENT_NAME}/references/{r}`" for r in refs) if refs else "- (none)"
# Scripts
scripts_dir = os.path.join(AGENT_DIR, "scripts")
scripts = sorted(f for f in os.listdir(scripts_dir) if os.path.isfile(os.path.join(scripts_dir, f))) if os.path.isdir(scripts_dir) else []
prompt = base_prompt
prompt = prompt.replace("{task_section}", task_section)
prompt = prompt.replace("{memory_content}", memory)
prompt = prompt.replace("{memory_path}", memory_path)
prompt = prompt.replace("{output_path}", output_path)
prompt = prompt.replace("{references_section}", refs_section)
# Skills
skills = spec.get("skills", [])
if isinstance(skills, str):
skills = [skills]
if skills:
prompt += f"\n\n## Available Skills\nYou have access to: {', '.join(skills)}\nUse read_file to load a skill's SKILL.md.\n"
# Scripts in prompt
if scripts:
prompt += "\n\n## Available Scripts\n"
for s in scripts:
prompt += f"- `agents/{AGENT_NAME}/scripts/{s}`\n"
# Post-run learning
prompt += f"\n\n## Post-Run Instructions\nAfter completing your task, write learnings to `{memory_path}`.\n"
# Research mode guardrails — read from file (single source of truth with tools.py)
if spec.get("mode") == "research":
guardrails_path = os.path.join(AGENT_DIR, "references", "research-guardrails.md")
guardrails = read_file(guardrails_path)
if guardrails.strip():
prompt += "\n\n" + guardrails
else:
# Fallback if file missing
prompt += "\n\n## Research Mode Guardrails (ACTIVE)\n"
prompt += "1. Say 'I don't have enough information' when uncertain.\n"
prompt += "2. Every claim needs a citation. No quote = retract the claim.\n"
prompt += "3. Extract direct quotes before analyzing. No paraphrase drift.\n"
# 4. Call agent via /chat
display = spec.get("display_name", AGENT_NAME)
try:
resp = requests.post(f"{BASE_URL}/chat", json={
"message": prompt + RESPONSE_FORMAT,
"call_source": "task",
"internal_options": {"job_id": JOB_ID},
}, timeout=(10, 300))
if not resp.ok:
print(f"Agent call failed: {resp.status_code}", file=sys.stderr)
if task:
task["status"] = "pending"
with open(tasks_path, "w") as f:
json.dump(tasks, f, indent=2)
return
reply = resp.json().get("reply", "")
data = extract_json_response(reply)
if data and isinstance(data, dict) and data.get("content"):
summary = data.get("summary", display)
content = data["content"]
else:
summary = display
content = reply
# 5. Push result to user
if content.strip():
push(content, title=summary)
# 6. Mark task completed (recurring tasks reset to pending)
if task:
now = datetime.now(tz.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
result_text = (content[:500] + "...") if len(content) > 500 else content
# Log to history
if "history" not in task:
task["history"] = []
task["history"].append({"completed": now, "result": result_text})
task["result"] = result_text
if task.get("recurring"):
task["status"] = "pending" # reset for next scheduled run
task["completed"] = None
else:
task["status"] = "completed"
task["completed"] = now
with open(tasks_path, "w") as f:
json.dump(tasks, f, indent=2)
except Exception as e:
print(f"Error running agent: {e}", file=sys.stderr)
if task:
retry_count = task.get("retry_count", 0) + 1
max_retries = task.get("max_retries", 3)
task["retry_count"] = retry_count
if retry_count >= max_retries:
task["status"] = "failed"
task["result"] = f"Dead-lettered after {retry_count} attempts: {e}"
else:
task["status"] = "pending"
with open(tasks_path, "w") as f:
json.dump(tasks, f, indent=2)
sys.exit(1)
if __name__ == "__main__":
main()