From bd7c28fe4acd4a893ca0aec393277430908805d6 Mon Sep 17 00:00:00 2001 From: Nir Singher Date: Sun, 19 Jul 2026 11:50:14 +0300 Subject: [PATCH 1/2] fix(examples): make coding_agents Docker stack spin up cleanly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The coding_agents two-agent Compose example (Claude SDK planner + Codex reviewer) crashed on `docker compose up`. Two root causes, both fixed here, plus guardrails so neither recurs. 1. Phantom config directory. Compose bind-mounts ./agent_config.yaml, and Docker auto-creates the source as an empty *directory* when the file is missing — so both runners crashed loading config. create_agents.py now writes the combined, keyed agent_config.yaml directly (single source of truth), replacing the old copy-the-example-then-paste-creds flow that left the phantom dir behind. It refuses to overwrite an existing config without FORCE=1 (re-running registers new agents and would orphan the old ones). 2. Platform mismatch / unreachable host. Agents registered against one Band platform can't be found by containers pointed at another; a misspelled or unreachable BAND_REST_URL crashes both with "httpx.ConnectError: [Errno -2] Name or service not known". create_agents.py now logs the platform it registered against and warns to match .env; .env.example and the README troubleshooting section document the symptom. Also in this change: - Mount ./prompts into the reviewer service — it read /app/prompts/reviewer.md but nothing was mounted there, so it booted without its role prompt. - Treat the repo block as optional throughout (example, README, script): with no repo block the agents boot chat-only, needing no GitHub/SSH. - test_communication.py reads the combined agent_config.yaml; drop the stale "3 agents" docstring. - gitignore coding_agents per-run artifacts (.agent_ids.txt, legacy planner/reviewer.yaml) so credentials/IDs can't be committed. --- .gitignore | 3 + examples/coding_agents/.env.example | 4 + examples/coding_agents/README.md | 63 +++++--- .../coding_agents/agent_config.yaml.example | 47 +++--- examples/coding_agents/create_agents.py | 145 +++++++++++++----- examples/coding_agents/docker-compose.yml | 5 +- examples/coding_agents/test_communication.py | 18 ++- 7 files changed, 198 insertions(+), 87 deletions(-) diff --git a/.gitignore b/.gitignore index 3f87381bc..09f72200f 100644 --- a/.gitignore +++ b/.gitignore @@ -163,6 +163,9 @@ examples/claude_sdk_docker/reviewer.yaml examples/claude_sdk_docker/implementer.yaml examples/claude_sdk_docker/.agent_ids.txt !examples/claude_sdk_docker/example_agent.yaml +examples/coding_agents/.agent_ids.txt +examples/coding_agents/planner.yaml +examples/coding_agents/reviewer.yaml # Parlant local data (sessions, cache, databases) parlant-data/ diff --git a/examples/coding_agents/.env.example b/examples/coding_agents/.env.example index 36e17ead6..08c88d8f7 100644 --- a/examples/coding_agents/.env.example +++ b/examples/coding_agents/.env.example @@ -2,6 +2,10 @@ # Copy to .env and fill in values before running docker compose up. # ── Platform URLs ───────────────────────────────────────────────────────── +# MUST match the platform you registered the agents against — i.e. the +# BAND_REST_URL used when running create_agents.py (default: app.band.ai). +# A mismatch, or an unreachable/misspelled host, crashes both containers at +# startup with: httpx.ConnectError: [Errno -2] Name or service not known. BAND_REST_URL=https://app.band.ai BAND_WS_URL=wss://app.band.ai/api/v1/socket/websocket diff --git a/examples/coding_agents/README.md b/examples/coding_agents/README.md index a4c7d69ce..6065897b3 100644 --- a/examples/coding_agents/README.md +++ b/examples/coding_agents/README.md @@ -20,10 +20,15 @@ Shared workspace volumes: ## Repo Initialization -On startup, each container reads `repo` config from `agent_config.yaml`: -1. Clone repo to `repo.path` if missing -2. Skip clone when repo already exists -3. Optionally generate context files when `repo.index: true` +The `repo` block in `agent_config.yaml` is **optional**. With no `repo` block +(the default written by `create_agents.py`), the agents boot, connect to Band, +and coordinate over chat — no repository is cloned and no GitHub/SSH access is +needed. This is the fastest way to see the two agents talk. + +When you add a `repo` block, each container on startup: +1. Clones the repo to `repo.path` if missing +2. Skips the clone when the repo already exists +3. Optionally generates context files when `repo.index: true` Generated files: - `/workspace/context/structure.md` @@ -62,28 +67,30 @@ When using HTTPS URLs, configure git credentials on host (`credential helper`, P ## Prerequisites - Docker and Docker Compose v2 -- Anthropic API key for planner -- OpenAI API key for reviewer -- Band agent credentials (`agent_id` + `api_key`) +- Anthropic API key for the planner +- OpenAI API key for the reviewer +- A Band **user** API key (`band_u_...`) to register the two agents ## Setup -1. Configure environment: +1. Add your LLM API keys: ```bash cp .env.example .env +# edit .env: set ANTHROPIC_API_KEY and OPENAI_API_KEY ``` -2. Configure agents and repo: +2. Register the agents and generate `agent_config.yaml`: ```bash -cp agent_config.yaml.example agent_config.yaml +BAND_API_KEY=band_u_... python create_agents.py ``` -Fill in: -- `planner.agent_id`, `planner.api_key` -- `reviewer.agent_id`, `reviewer.api_key` -- `planner.repo` and `reviewer.repo` (same URL/path/branch) +This registers a Planner and a Reviewer and writes the combined +`agent_config.yaml` that compose reads. No repo is wired by default (chat-only). +To point the agents at a shared repo, set `REPO_URL` (and optionally +`REPO_BRANCH`, `REPO_INDEX`) when running the script — see +`agent_config.yaml.example` for the block it produces. 3. Build and run: @@ -100,6 +107,12 @@ docker compose logs -f planner docker compose logs -f reviewer ``` +5. (Optional) Send a test message that makes the planner ping the reviewer: + +```bash +python test_communication.py +``` + ## Configuration ### `.env` @@ -118,14 +131,17 @@ docker compose logs -f reviewer ### `agent_config.yaml` -Use `repo` under both planner and reviewer: +Generated by `create_agents.py` — you don't normally edit it by hand. It's a +single combined file keyed by agent. The `repo` block is optional; omit it (the +default) for a chat-only stack. To add a repo later, drop an identical `repo` +block under **both** agents: ```yaml planner: agent_id: "..." api_key: "..." role: planner - repo: + repo: # optional url: "git@github.com:org/repo.git" path: "/workspace/repo" branch: "main" @@ -134,7 +150,8 @@ planner: reviewer: agent_id: "..." api_key: "..." - repo: + role: reviewer + repo: # keep identical to planner.repo url: "git@github.com:org/repo.git" path: "/workspace/repo" branch: "main" @@ -143,7 +160,17 @@ reviewer: ## Troubleshooting -- `Config file not found`: create `agent_config.yaml` from the example. +- **`Config file is empty` / `IsADirectoryError` on `agent_config.yaml`**: a + stale empty `agent_config.yaml` **directory** exists (Docker auto-creates the + bind-mount source when the file is missing). Run `docker compose down`, then + `rm -rf agent_config.yaml && python create_agents.py` to regenerate the file + before `docker compose up`. +- `Config file not found`: run `create_agents.py` to generate `agent_config.yaml`. +- **`httpx.ConnectError: [Errno -2] Name or service not known` (crash loop)**: + `BAND_REST_URL`/`BAND_WS_URL` in `.env` point at an unreachable/misspelled + host, or at a *different* platform than the one you registered the agents on. + They must match the `BAND_REST_URL` used with `create_agents.py` (default + `https://app.band.ai`). Fix `.env`, then `docker compose up -d --force-recreate`. - `Invalid repo configuration`: verify `repo.url` and absolute `repo.path`. - `Host not found in known_hosts`: add host key (`ssh-keyscan -H >> ~/.ssh/known_hosts`). - `Authentication failed` on clone: verify SSH keys or HTTPS token/credential helper. diff --git a/examples/coding_agents/agent_config.yaml.example b/examples/coding_agents/agent_config.yaml.example index 34837f856..11eda5ec0 100644 --- a/examples/coding_agents/agent_config.yaml.example +++ b/examples/coding_agents/agent_config.yaml.example @@ -1,30 +1,35 @@ -# Agent credentials for all agents in this example. -# Copy to agent_config.yaml and fill in values from the Band platform. +# Template for agent_config.yaml (the combined, keyed credentials file that +# docker-compose.yml consumes). +# +# You normally do NOT copy this by hand — `create_agents.py` registers both +# agents and writes agent_config.yaml for you. Use this file only as a reference +# for the format, or to add a `repo:` block after the fact. +# # NEVER commit real credentials to version control. # -# Keys must match: -# - AGENT_KEY env var for the Claude SDK planner (default: "planner") -# - REVIEWER_AGENT_KEY in .env (default: "reviewer") +# Keys must match the runner AGENT_KEY: +# planner -> AGENT_KEY=planner (compose service "planner") +# reviewer -> REVIEWER_AGENT_KEY=reviewer (compose service "reviewer") planner: - agent_id: "" # Agent UUID from Band platform + agent_id: "" # Agent UUID from Band api_key: "" # Agent-specific Band API key role: planner - repo: - # Supports either SSH or HTTPS: - # git@github.com:org/repo.git - # https://github.com/org/repo.git - url: "git@github.com:org/repo.git" - path: "/workspace/repo" - branch: "main" # Optional - index: true # Optional: generate /workspace/context/*.md + # Optional — omit entirely for a chat-only stack (no GitHub/SSH). Add an + # identical block under reviewer so both containers target the same repo. + # repo: + # # SSH (git@github.com:org/repo.git) or HTTPS (https://github.com/org/repo.git) + # url: "git@github.com:org/repo.git" + # path: "/workspace/repo" + # branch: "main" # Optional + # index: true # Optional: generate /workspace/context/*.md reviewer: - agent_id: "" # Agent UUID from Band platform + agent_id: "" # Agent UUID from Band api_key: "" # Agent-specific Band API key - repo: - # Keep this in sync with planner.repo so both containers target the same repo. - url: "git@github.com:org/repo.git" - path: "/workspace/repo" - branch: "main" # Optional - index: true # Optional: enabled by default in this example + role: reviewer + # repo: + # url: "git@github.com:org/repo.git" + # path: "/workspace/repo" + # branch: "main" + # index: true diff --git a/examples/coding_agents/create_agents.py b/examples/coding_agents/create_agents.py index f8a49ddaa..7dc957bd0 100644 --- a/examples/coding_agents/create_agents.py +++ b/examples/coding_agents/create_agents.py @@ -1,11 +1,24 @@ #!/usr/bin/env python3 -"""Create test agents for E2E testing. +"""Register the planner + reviewer agents and write agent_config.yaml. -Registers planner and reviewer agents via User API -and writes their credentials to YAML config files. +Registers both agents via the Band User API and writes a single combined +``agent_config.yaml`` (keyed ``planner:``/``reviewer:``) — exactly the file +docker-compose.yml consumes. This replaces the old two-step "copy the example, +then paste credentials" flow, which left an empty ``agent_config.yaml`` +directory behind (Docker auto-creates the bind-mount source) and crashed both +containers. Usage: BAND_API_KEY=band_u_... python create_agents.py + +Optional repo (agents clone + work on a shared git repo). Omit for a +chat-only stack that needs no GitHub/SSH: + REPO_URL=git@github.com:org/repo.git \ + REPO_BRANCH=main REPO_INDEX=true \ + BAND_API_KEY=band_u_... python create_agents.py + +Re-running registers NEW agents. To avoid orphaning the ones already in +agent_config.yaml, the script refuses to overwrite it unless FORCE=1. """ from __future__ import annotations @@ -13,6 +26,8 @@ import asyncio import logging import os +from pathlib import Path +from typing import Any import yaml @@ -20,76 +35,128 @@ logger = logging.getLogger(__name__) AGENTS = [ - {"name": "Planner", "role": "planner", "file": "planner.yaml"}, - {"name": "Reviewer", "role": "reviewer", "file": "reviewer.yaml"}, + {"name": "Planner", "role": "planner", "key": "planner"}, + {"name": "Reviewer", "role": "reviewer", "key": "reviewer"}, ] +SCRIPT_DIR = Path(__file__).resolve().parent +CONFIG_PATH = SCRIPT_DIR / "agent_config.yaml" +CLEANUP_PATH = SCRIPT_DIR / ".agent_ids.txt" + +CONFIG_HEADER = """\ +# Combined agent credentials for the coding_agents Docker Compose stack. +# Generated by create_agents.py. NEVER commit real credentials. +# +# Keys must match the runner AGENT_KEY: +# planner -> AGENT_KEY=planner (compose service "planner") +# reviewer -> REVIEWER_AGENT_KEY=reviewer (compose service "reviewer") +# +# No `repo:` block => the agents boot and coordinate over chat without cloning +# anything (no GitHub/SSH needed). Set REPO_URL when running this script, or add +# an identical `repo:` block under each agent by hand — see +# agent_config.yaml.example for the template. +""" + + +def build_repo_block() -> dict[str, Any] | None: + """Build an optional repo block from REPO_* env vars, or None if unset.""" + url = os.environ.get("REPO_URL", "").strip() + if not url: + return None + + repo: dict[str, Any] = { + "url": url, + "path": os.environ.get("REPO_PATH", "/workspace/repo").strip(), + } + branch = os.environ.get("REPO_BRANCH", "").strip() + if branch: + repo["branch"] = branch + repo["index"] = os.environ.get("REPO_INDEX", "true").strip().lower() in ( + "1", + "true", + "yes", + ) + return repo + async def main() -> None: api_key = os.environ.get("BAND_API_KEY") if not api_key: raise ValueError("BAND_API_KEY environment variable is required") + if CONFIG_PATH.is_file() and os.environ.get("FORCE", "").strip().lower() not in ( + "1", + "true", + "yes", + ): + raise ValueError( + f"{CONFIG_PATH.name} already exists. Re-running registers NEW Band " + "agents and would orphan the ones referenced there. Delete it (and " + "clean up old agents listed in .agent_ids.txt) or set FORCE=1 to " + "overwrite." + ) + base_url = os.environ.get("BAND_REST_URL", "https://app.band.ai") - from band_rest import AsyncRestClient + from band.client.rest import AsyncRestClient, DEFAULT_REQUEST_OPTIONS from band_rest.types import AgentRegisterRequest client = AsyncRestClient(api_key=api_key, base_url=base_url) - created = [] - script_dir = os.path.dirname(os.path.abspath(__file__)) + repo_block = build_repo_block() + config: dict[str, Any] = {} + created: list[dict[str, str]] = [] for agent_def in AGENTS: logger.info("Creating agent: %s ...", agent_def["name"]) response = await client.human_api_agents.register_my_agent( agent=AgentRegisterRequest( name=agent_def["name"], - description=f"E2E test agent - {agent_def['role']} role", - ) + description=f"coding_agents example - {agent_def['role']} role", + ), + request_options=DEFAULT_REQUEST_OPTIONS, ) agent = response.data.agent credentials = response.data.credentials - logger.info(" Created: %s (ID: %s)", agent.name, agent.id) - # Omit `model` so the runner falls back to None and the npm `claude` - # binary picks its own default. Override per agent by adding - # `"model": "opus"` (or any alias / pinned ID) below. - config = { + # Omit `model` so each runner falls back to its own default (the npm + # `claude` binary / codex pick their default). Add a `model:` key here + # to pin one. + section: dict[str, Any] = { "agent_id": agent.id, "api_key": credentials.api_key, "role": agent_def["role"], } + if repo_block is not None: + section["repo"] = dict(repo_block) - config_path = os.path.join(script_dir, agent_def["file"]) - with open(config_path, "w") as f: - yaml.dump(config, f, default_flow_style=False) - logger.info(" Config written to: %s", agent_def["file"]) - - created.append( - { - "name": agent.name, - "id": agent.id, - "api_key": credentials.api_key, - "role": agent_def["role"], - "file": agent_def["file"], - } - ) + config[agent_def["key"]] = section + created.append({"name": agent.name, "id": agent.id, "role": agent_def["role"]}) + + body = yaml.dump(config, default_flow_style=False, sort_keys=False) + CONFIG_PATH.write_text(f"{CONFIG_HEADER}\n{body}", encoding="utf-8") + logger.info("\nWrote combined config: %s", CONFIG_PATH.name) + + CLEANUP_PATH.write_text( + "".join(f"{a['id']}\n" for a in created), encoding="utf-8" + ) + logger.info("Agent IDs saved to %s for cleanup", CLEANUP_PATH.name) logger.info("\n=== Summary ===") for a in created: - logger.info( - "%s: id=%s, role=%s, config=%s", a["name"], a["id"], a["role"], a["file"] - ) - - # Write agent IDs to a cleanup file for later deletion - cleanup_path = os.path.join(script_dir, ".agent_ids.txt") - with open(cleanup_path, "w") as f: - for a in created: - f.write(f"{a['id']}\n") - logger.info("\nAgent IDs saved to .agent_ids.txt for cleanup") + logger.info("%s: id=%s, role=%s", a["name"], a["id"], a["role"]) + logger.info( + "\nRepo: %s", repo_block["url"] if repo_block else "none (chat-only, no clone)" + ) + logger.info("Registered against: %s", base_url) + logger.info( + "IMPORTANT: set BAND_REST_URL in .env to the SAME platform (%s), or the " + "containers won't find these agents.", + base_url, + ) + logger.info("\nNext: docker compose build && docker compose up -d") if __name__ == "__main__": diff --git a/examples/coding_agents/docker-compose.yml b/examples/coding_agents/docker-compose.yml index 4baa7a45b..cb9d526e0 100644 --- a/examples/coding_agents/docker-compose.yml +++ b/examples/coding_agents/docker-compose.yml @@ -8,8 +8,8 @@ # Use `docker compose down -v` to remove them. # # Usage: -# cp .env.example .env # Configure credentials -# cp agent_config.yaml.example agent_config.yaml # Configure all agents +# cp .env.example .env # Add your LLM API keys +# BAND_API_KEY=band_u_... python create_agents.py # Register agents + write agent_config.yaml # docker compose build # docker compose up -d # docker compose logs -f @@ -35,6 +35,7 @@ x-codex: &codex-base PHOENIX_CHANNELS_CLIENT_WHEEL: ${PHOENIX_CHANNELS_CLIENT_WHEEL:-} volumes: &codex-volumes - ./agent_config.yaml:/app/agent_config.yaml:ro + - ./prompts:/app/prompts:ro - shared_repo:/workspace/repo - shared_notes:/workspace/notes - shared_state:/workspace/state diff --git a/examples/coding_agents/test_communication.py b/examples/coding_agents/test_communication.py index 99b9a4b7a..3b30c9458 100644 --- a/examples/coding_agents/test_communication.py +++ b/examples/coding_agents/test_communication.py @@ -1,8 +1,11 @@ #!/usr/bin/env python3 """Test inter-agent communication between planner and reviewer. -Creates a chat room, adds all 3 agents, sends a test message, -and verifies delivery by checking container logs. +Creates a chat room, adds the reviewer, sends a test message mentioning it, +and lists the room messages so you can confirm delivery. + +Reads credentials from the combined agent_config.yaml written by +create_agents.py. Usage: python test_communication.py @@ -26,8 +29,8 @@ SCRIPT_DIR = Path(__file__).parent -def load_agent_config(filename: str) -> dict: - """Load agent config from YAML file.""" +def load_config_file(filename: str) -> dict: + """Load a YAML config file from the script directory.""" path = SCRIPT_DIR / filename with open(path) as f: return yaml.safe_load(f) @@ -42,9 +45,10 @@ async def main() -> None: ParticipantRequest, ) - # Load agent configs - planner = load_agent_config("planner.yaml") - reviewer = load_agent_config("reviewer.yaml") + # Load agent configs from the combined file create_agents.py writes + config = load_config_file("agent_config.yaml") + planner = config["planner"] + reviewer = config["reviewer"] base_url = os.environ.get("BAND_REST_URL", "https://app.band.ai") From 7020fc2b6d608bbc787b5ca7449e289f97921118 Mon Sep 17 00:00:00 2001 From: Nir Singher Date: Sun, 19 Jul 2026 11:58:30 +0300 Subject: [PATCH 2/2] style: ruff format create_agents.py --- examples/coding_agents/create_agents.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/examples/coding_agents/create_agents.py b/examples/coding_agents/create_agents.py index 7dc957bd0..cd76ba102 100644 --- a/examples/coding_agents/create_agents.py +++ b/examples/coding_agents/create_agents.py @@ -139,9 +139,7 @@ async def main() -> None: CONFIG_PATH.write_text(f"{CONFIG_HEADER}\n{body}", encoding="utf-8") logger.info("\nWrote combined config: %s", CONFIG_PATH.name) - CLEANUP_PATH.write_text( - "".join(f"{a['id']}\n" for a in created), encoding="utf-8" - ) + CLEANUP_PATH.write_text("".join(f"{a['id']}\n" for a in created), encoding="utf-8") logger.info("Agent IDs saved to %s for cleanup", CLEANUP_PATH.name) logger.info("\n=== Summary ===")