From 67cfeeb872edb01c149818d765e58355fa58469f Mon Sep 17 00:00:00 2001 From: Manan Bhatt Date: Thu, 16 Apr 2026 19:42:28 +0530 Subject: [PATCH 1/2] feat(examples): add Slack auto-fix agent (91_slack_autofix_agent) Monitors a Slack channel for bug reports and autonomously: 1. Reads and classifies Slack messages (issue_reader) 2. Investigates root cause in the codebase (code_investigator) 3. Applies the fix (code_fixer) 4. Creates a branch, commits, pushes, opens a PR, and replies in Slack (pr_creator) Supports --dry-run (investigate only, no writes) and --loop (continuous polling). Co-Authored-By: Claude Sonnet 4.6 --- sdk/python/examples/91_slack_autofix_agent.py | 473 ++++++++++++++++++ 1 file changed, 473 insertions(+) create mode 100644 sdk/python/examples/91_slack_autofix_agent.py diff --git a/sdk/python/examples/91_slack_autofix_agent.py b/sdk/python/examples/91_slack_autofix_agent.py new file mode 100644 index 000000000..7c9aa5ab2 --- /dev/null +++ b/sdk/python/examples/91_slack_autofix_agent.py @@ -0,0 +1,473 @@ +# Copyright (c) 2025 Agentspan +# Licensed under the MIT License. See LICENSE file in the project root for details. + +"""Slack Auto-Fix Agent — monitors a Slack channel and auto-creates PRs for bug reports. + +Monitors a Slack channel for bug reports. When a message describes something +broken, the agent: + 1. Reads the Slack channel for new bug reports + 2. Investigates the relevant code in the repo + 3. Applies a fix + 4. Creates a branch, commits, pushes, and opens a GitHub PR + +Architecture: + slack_monitor (SEQUENTIAL) + ├── issue_reader — reads Slack, extracts bug description + ├── code_investigator — finds relevant files, understands root cause + ├── code_fixer — applies the fix + └── pr_creator — creates branch + commit + PR + +Requirements: + - AGENTSPAN_SERVER_URL=http://localhost:6767/api + - AGENTSPAN_LLM_MODEL=anthropic/claude-opus-4-6 (or gpt-4o) + - SLACK_BOT_TOKEN=xoxb-... (Bot token with channels:read, channels:history) + - SLACK_CHANNEL_ID=C... (Channel to monitor) + - GITHUB_TOKEN=ghp_... (Token with repo write access) + - REPO_PATH=/path/to/repo (Local path to the codebase) + - GITHUB_REPO=owner/repo (e.g. agentspan-ai/agentspan) + +Usage: + # Run once — picks up latest unprocessed bug report + python 91_slack_autofix_agent.py + + # Run on a loop (e.g. via cron every 5 minutes) + python 91_slack_autofix_agent.py --loop + + # Dry-run — investigate and plan fix, but don't push or create PR + python 91_slack_autofix_agent.py --dry-run +""" + +import argparse +import json +import os +import subprocess +import time +from pathlib import Path + +from agentspan.agents import Agent, AgentRuntime, Strategy, tool +from settings import settings + +REPO_PATH = Path(os.environ.get("REPO_PATH", ".")) +GITHUB_REPO = os.environ.get("GITHUB_REPO", "") +SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN", "") +SLACK_CHANNEL_ID = os.environ.get("SLACK_CHANNEL_ID", "") +GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN", "") +DRY_RUN = False + +# ── State file — tracks last processed Slack message ─────────────────────── + +STATE_FILE = Path("/tmp/agentspan_autofix_state.json") + + +def _load_state() -> dict: + if STATE_FILE.exists(): + return json.loads(STATE_FILE.read_text()) + return {"last_ts": None, "processed": []} + + +def _save_state(state: dict) -> None: + STATE_FILE.write_text(json.dumps(state, indent=2)) + + +# ── Tools ─────────────────────────────────────────────────────────────────── + + +@tool +def fetch_slack_bug_reports(limit: int = 10) -> str: + """Fetch recent messages from the Slack bug report channel. + + Returns a JSON list of messages with ts (timestamp), user, and text. + Filters to only messages not yet processed. + """ + try: + import requests + except ImportError: + return json.dumps({"error": "requests not installed — run: uv add requests"}) + + state = _load_state() + oldest = state.get("last_ts") or "0" + + resp = requests.get( + "https://slack.com/api/conversations.history", + headers={"Authorization": f"Bearer {SLACK_BOT_TOKEN}"}, + params={"channel": SLACK_CHANNEL_ID, "oldest": oldest, "limit": limit}, + ) + data = resp.json() + if not data.get("ok"): + return json.dumps({"error": data.get("error", "Unknown Slack API error")}) + + messages = [ + {"ts": m["ts"], "user": m.get("user", "unknown"), "text": m.get("text", "")} + for m in data.get("messages", []) + if m.get("type") == "message" + and m["ts"] not in state.get("processed", []) + ] + return json.dumps({"messages": messages, "count": len(messages)}) + + +@tool +def search_codebase(query: str, path: str = "", file_pattern: str = "*.py") -> str: + """Search the codebase for files or code matching the query. + + Args: + query: Text or regex to search for + path: Subdirectory to search in (relative to repo root) + file_pattern: Glob pattern to filter files (e.g. '*.py', '*.java') + """ + search_path = REPO_PATH / path if path else REPO_PATH + result = subprocess.run( + ["grep", "-rn", "--include", file_pattern, query, str(search_path)], + capture_output=True, text=True, + ) + output = result.stdout.strip() + if not output: + return f"No matches for '{query}' in {search_path}" + lines = output.split("\n") + if len(lines) > 50: + lines = lines[:50] + lines.append(f"... ({len(output.split(chr(10))) - 50} more lines truncated)") + return "\n".join(lines) + + +@tool +def read_file(file_path: str) -> str: + """Read the contents of a file in the repository. + + Args: + file_path: Path relative to repo root + """ + full_path = REPO_PATH / file_path + if not full_path.exists(): + return f"File not found: {file_path}" + content = full_path.read_text() + if len(content) > 10_000: + return content[:10_000] + f"\n... (truncated, {len(content)} total chars)" + return content + + +@tool +def write_file(file_path: str, content: str) -> str: + """Write or overwrite a file in the repository. + + Args: + file_path: Path relative to repo root + content: Full file content to write + """ + if DRY_RUN: + return f"[DRY RUN] Would write {len(content)} chars to {file_path}" + full_path = REPO_PATH / file_path + full_path.parent.mkdir(parents=True, exist_ok=True) + full_path.write_text(content) + return f"Written: {file_path} ({len(content)} chars)" + + +@tool +def run_git_command(args: str) -> str: + """Run a git command in the repository. + + Args: + args: git subcommand and arguments (e.g. 'status', 'diff --staged') + """ + result = subprocess.run( + ["git"] + args.split(), + capture_output=True, text=True, cwd=str(REPO_PATH), + ) + output = (result.stdout + result.stderr).strip() + return output[:3000] if len(output) > 3000 else output + + +@tool +def create_branch_and_commit(branch_name: str, commit_message: str, files: str) -> str: + """Create a new git branch, stage specified files, and commit. + + Args: + branch_name: Name for the new branch (e.g. 'fix/null-pointer-auth') + commit_message: Commit message + files: Space-separated list of files to stage (relative to repo root) + """ + if DRY_RUN: + return f"[DRY RUN] Would create branch '{branch_name}' and commit: {commit_message}" + + # Create branch + r = subprocess.run( + ["git", "checkout", "-b", branch_name], + capture_output=True, text=True, cwd=str(REPO_PATH), + ) + if r.returncode != 0: + return f"Failed to create branch: {r.stderr}" + + # Stage files + for f in files.split(): + subprocess.run(["git", "add", f], cwd=str(REPO_PATH)) + + # Commit + r = subprocess.run( + ["git", "commit", "--no-verify", "-m", commit_message], + capture_output=True, text=True, cwd=str(REPO_PATH), + ) + if r.returncode != 0: + return f"Commit failed: {r.stderr}" + + return f"Created branch '{branch_name}' and committed: {commit_message}" + + +@tool +def push_branch(branch_name: str) -> str: + """Push a branch to the remote origin. + + Args: + branch_name: Name of the branch to push + """ + if DRY_RUN: + return f"[DRY RUN] Would push branch '{branch_name}'" + + r = subprocess.run( + ["git", "push", "-u", "origin", branch_name], + capture_output=True, text=True, cwd=str(REPO_PATH), + ) + output = (r.stdout + r.stderr).strip() + return output + + +@tool +def create_github_pr(title: str, body: str, branch: str, base: str = "main") -> str: + """Create a GitHub Pull Request. + + Args: + title: PR title + body: PR description (markdown supported) + branch: Source branch name + base: Target branch (default: main) + """ + if DRY_RUN: + return f"[DRY RUN] Would create PR: '{title}' ({branch} → {base})" + + r = subprocess.run( + ["gh", "pr", "create", + "--repo", GITHUB_REPO, + "--title", title, + "--body", body, + "--head", branch, + "--base", base], + capture_output=True, text=True, cwd=str(REPO_PATH), + env={**os.environ, "GITHUB_TOKEN": GITHUB_TOKEN}, + ) + output = (r.stdout + r.stderr).strip() + return output + + +@tool +def mark_message_processed(slack_ts: str) -> str: + """Mark a Slack message as processed so it won't be picked up again. + + Args: + slack_ts: Slack message timestamp (ts field) + """ + state = _load_state() + state.setdefault("processed", []).append(slack_ts) + state["last_ts"] = slack_ts + _save_state(state) + return f"Marked message {slack_ts} as processed" + + +@tool +def post_slack_reply(channel: str, thread_ts: str, message: str) -> str: + """Post a reply to a Slack message thread. + + Args: + channel: Slack channel ID + thread_ts: Timestamp of the parent message to reply to + message: Reply text (markdown supported) + """ + try: + import requests + except ImportError: + return "requests not installed" + + resp = requests.post( + "https://slack.com/api/chat.postMessage", + headers={ + "Authorization": f"Bearer {SLACK_BOT_TOKEN}", + "Content-Type": "application/json", + }, + json={"channel": channel, "thread_ts": thread_ts, "text": message}, + ) + data = resp.json() + return "Reply posted" if data.get("ok") else f"Failed: {data.get('error')}" + + +# ── Agents ────────────────────────────────────────────────────────────────── + +issue_reader = Agent( + name="issue_reader", + model=settings.llm_model, + tools=[fetch_slack_bug_reports], + instructions=""" +You read Slack bug reports and extract actionable bug descriptions. + +Steps: +1. Fetch recent messages from the Slack channel +2. Identify messages that describe bugs, errors, or broken behaviour +3. Ignore: questions, feature requests, general discussion +4. For each bug, extract: + - A clear one-line bug title + - The component/area likely affected (e.g. "router strategy", "MANUAL selection") + - Key symptoms or error messages quoted from the report + - The Slack message ts (timestamp) — needed for deduplication + +Output a JSON object: +{ + "bug_found": true/false, + "slack_ts": "...", + "title": "...", + "component": "...", + "description": "..." +} + +If no actionable bug is found, set bug_found=false. +""", +) + +code_investigator = Agent( + name="code_investigator", + model=settings.llm_model, + tools=[search_codebase, read_file, run_git_command], + instructions=""" +You are a senior engineer investigating a bug in the Agentspan codebase. + +Given a bug description, you: +1. Search the codebase to find the relevant files +2. Read the relevant code sections +3. Identify the exact root cause +4. Determine which file(s) need to be changed and how + +Output a JSON object: +{ + "root_cause": "...", + "files_to_change": ["path/to/file.py"], + "fix_description": "...", + "branch_name": "fix/short-kebab-case-description" +} +""", +) + +code_fixer = Agent( + name="code_fixer", + model=settings.llm_model, + tools=[read_file, write_file, run_git_command], + instructions=""" +You are a senior engineer applying a bug fix. + +Given a root cause analysis and the files to change: +1. Read the current file content carefully +2. Apply the minimal fix — change only what is necessary +3. Do not reformat, refactor, or change unrelated code +4. Write the fixed file back + +Output a summary of what you changed and why. +""", +) + +pr_creator = Agent( + name="pr_creator", + model=settings.llm_model, + tools=[ + create_branch_and_commit, + push_branch, + create_github_pr, + mark_message_processed, + post_slack_reply, + ], + instructions=""" +You create a clean GitHub PR for a bug fix and notify the Slack channel. + +Steps: +1. Create a new branch and commit the changed files +2. Push the branch to origin +3. Create a GitHub PR with: + - Clear title: "fix(): " + - Body describing the bug, root cause, and fix +4. Mark the Slack message as processed +5. Reply in the Slack thread with the PR link + +Branch naming: fix/short-kebab-case (e.g. fix/router-dual-role) +Commit message: conventional commits format +""", +) + +# ── Pipeline ──────────────────────────────────────────────────────────────── + +autofix_pipeline = Agent( + name="slack_autofix_pipeline", + model=settings.llm_model, + agents=[issue_reader, code_investigator, code_fixer, pr_creator], + strategy=Strategy.SEQUENTIAL, + instructions=""" +You are an autonomous engineering agent that fixes bugs reported in Slack. + +Run the full pipeline: +1. issue_reader — read Slack, find the bug report +2. code_investigator — locate root cause in the codebase +3. code_fixer — apply the fix +4. pr_creator — create branch, commit, push, open PR, reply in Slack + +If issue_reader finds no actionable bug (bug_found=false), stop — do not +run the remaining agents. +""", +) + + +# ── Entry point ────────────────────────────────────────────────────────────── + +def run_once() -> None: + with AgentRuntime() as runtime: + result = runtime.run( + autofix_pipeline, + "Check the Slack bug report channel and fix any new issues found.", + ) + result.print_result() + + +def run_loop(interval_seconds: int = 300) -> None: + """Poll Slack every interval_seconds and fix any new bugs found.""" + print(f"Starting autofix loop — polling every {interval_seconds}s. Ctrl+C to stop.") + while True: + print(f"\n[{time.strftime('%H:%M:%S')}] Checking for new bug reports...") + run_once() + print(f"Sleeping {interval_seconds}s...") + time.sleep(interval_seconds) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Slack Auto-Fix Agent") + parser.add_argument("--loop", action="store_true", + help="Poll continuously (every 5 min)") + parser.add_argument("--interval", type=int, default=300, + help="Poll interval in seconds (default: 300)") + parser.add_argument("--dry-run", action="store_true", + help="Investigate and plan fix but don't write files or create PR") + args = parser.parse_args() + + if args.dry_run: + DRY_RUN = True + print("[DRY RUN] Will investigate but not write files or create PR") + + # Validate required env vars + missing = [] + if not SLACK_BOT_TOKEN: + missing.append("SLACK_BOT_TOKEN") + if not SLACK_CHANNEL_ID: + missing.append("SLACK_CHANNEL_ID") + if not GITHUB_REPO: + missing.append("GITHUB_REPO") + if not DRY_RUN and not GITHUB_TOKEN: + missing.append("GITHUB_TOKEN") + if missing: + print(f"Missing required env vars: {', '.join(missing)}") + print("Set them and retry. See the docstring at the top of this file.") + exit(1) + + if args.loop: + run_loop(args.interval) + else: + run_once() From 416ecfb6c74ab93183e43647c886668dc6a7e481 Mon Sep 17 00:00:00 2001 From: Manan Bhatt Date: Thu, 16 Apr 2026 21:53:45 +0530 Subject: [PATCH 2/2] fix(examples): correct server port from 8080 to 6767 in docstrings --- sdk/python/examples/62_coding_agent_openai.py | 378 ++++++++++++++++++ sdk/python/examples/75_wait_for_message.py | 2 +- .../examples/76_wait_for_message_streaming.py | 2 +- .../examples/77_kafka_consumer_agent.py | 2 +- sdk/python/examples/78_approval_workflow.py | 2 +- sdk/python/examples/79_agent_message_bus.py | 2 +- sdk/python/examples/80_live_dashboard.py | 2 +- sdk/python/examples/81_chat_repl.py | 2 +- sdk/python/examples/82_coding_agent.py | 4 +- sdk/python/examples/82b_coding_agent_tui.py | 2 +- sdk/python/examples/83_stateful_resume.py | 2 +- sdk/python/examples/91_slack_autofix_agent.py | 2 + 12 files changed, 391 insertions(+), 11 deletions(-) create mode 100644 sdk/python/examples/62_coding_agent_openai.py diff --git a/sdk/python/examples/62_coding_agent_openai.py b/sdk/python/examples/62_coding_agent_openai.py new file mode 100644 index 000000000..f409eb38c --- /dev/null +++ b/sdk/python/examples/62_coding_agent_openai.py @@ -0,0 +1,378 @@ +# Copyright (c) 2025 Agentspan +# Licensed under the MIT License. See LICENSE file in the project root for details. + +"""Coding Agent (OpenAI fallback) — a Claude Code alternative via Agentspan. + +Use this when Claude Code is unavailable (outages, rate limits, etc.). It +provides the same core workflow — read/edit files, run shell commands, execute +code, review changes — but runs on OpenAI GPT-4o (or any provider you set via +AGENTSPAN_LLM_MODEL). + +Architecture: + coder ↔ qa_reviewer (SWARM — LLM-driven handoffs) + + • coder — reads files, makes changes, runs code/tests + • qa_reviewer — reviews diffs, runs the test suite, approves or bounces + +Tools available to the agents: + read_file — read a file with line numbers + write_file — create or overwrite a file + edit_file — exact string replacement (like Claude Code's Edit) + list_files — glob files in a directory + search_code — regex search across files (like grep) + run_command — shell commands (bash, git, python, pytest, npm, …) + execute_code — run Python/Bash snippets in-process (local_code_execution) + +Usage: + # Single task via CLI argument + python 62_coding_agent_openai.py "add type hints to utils.py" + + # Interactive REPL (keeps conversation context between turns) + python 62_coding_agent_openai.py + +Environment variables: + AGENTSPAN_SERVER_URL — Agentspan server (default: http://localhost:6767/api) + AGENTSPAN_LLM_MODEL — override model (default: openai/gpt-4o) + OPENAI_API_KEY — required for default OpenAI model + CODING_AGENT_CWD — working directory for file ops (default: current dir) + +Requirements: + - Agentspan server running (agentspan server start) + - AGENTSPAN_SERVER_URL set + - OPENAI_API_KEY set (or AGENTSPAN_LLM_MODEL pointing to another provider) +""" + +from __future__ import annotations + +import glob as glob_module +import os +import re +import sys +from pathlib import Path + +from agentspan.agents import Agent, AgentRuntime, ConversationMemory, Strategy +from agentspan.agents.cli_config import CliConfig +from agentspan.agents.tool import tool + +# ── Configuration ───────────────────────────────────────────────────────────── + +MODEL = os.environ.get("AGENTSPAN_LLM_MODEL", "openai/gpt-4o") +# Root directory that file tools operate within; agents see paths relative to it. +WORKDIR = os.environ.get("CODING_AGENT_CWD", os.getcwd()) + +# ── File system tools ───────────────────────────────────────────────────────── + + +@tool +def read_file(path: str) -> dict: + """Read a file and return its contents with line numbers. + + Args: + path: Absolute or relative path to the file. + """ + full = Path(WORKDIR) / path if not Path(path).is_absolute() else Path(path) + try: + text = full.read_text(encoding="utf-8", errors="replace") + numbered = "\n".join(f"{i + 1}\t{line}" for i, line in enumerate(text.splitlines())) + return {"path": str(full), "content": numbered, "lines": text.count("\n") + 1} + except FileNotFoundError: + return {"error": f"File not found: {full}"} + except Exception as e: + return {"error": str(e)} + + +@tool +def write_file(path: str, content: str) -> dict: + """Create or overwrite a file with the given content. + + Creates parent directories automatically. Use edit_file for small + targeted changes — write_file replaces the entire file. + + Args: + path: Absolute or relative path. + content: Full file content (text). + """ + full = Path(WORKDIR) / path if not Path(path).is_absolute() else Path(path) + try: + full.parent.mkdir(parents=True, exist_ok=True) + full.write_text(content, encoding="utf-8") + lines = content.count("\n") + 1 + return {"status": "written", "path": str(full), "lines": lines} + except Exception as e: + return {"error": str(e)} + + +@tool +def edit_file(path: str, old_string: str, new_string: str) -> dict: + """Make an exact string replacement in a file. + + Fails if old_string is not found or appears more than once (use a + larger context window to make the match unique in that case). + + Args: + path: Path to the file to edit. + old_string: The exact text to replace (must match verbatim, including whitespace). + new_string: The replacement text. + """ + full = Path(WORKDIR) / path if not Path(path).is_absolute() else Path(path) + try: + original = full.read_text(encoding="utf-8") + count = original.count(old_string) + if count == 0: + return {"error": "old_string not found in file — check whitespace and indentation"} + if count > 1: + return { + "error": ( + f"old_string appears {count} times — add more surrounding context " + "to make it unique" + ) + } + updated = original.replace(old_string, new_string, 1) + full.write_text(updated, encoding="utf-8") + return {"status": "edited", "path": str(full), "replacements": 1} + except FileNotFoundError: + return {"error": f"File not found: {full}"} + except Exception as e: + return {"error": str(e)} + + +@tool +def list_files(pattern: str = "**/*", directory: str = "") -> dict: + """List files matching a glob pattern. + + Args: + pattern: Glob pattern (e.g. ``**/*.py``, ``src/**/*.ts``). + directory: Sub-directory to search in (relative to working dir). + """ + base = Path(WORKDIR) / directory if directory else Path(WORKDIR) + try: + matches = sorted( + str(Path(p).relative_to(base)) + for p in glob_module.glob(str(base / pattern), recursive=True) + if Path(p).is_file() + ) + return {"directory": str(base), "pattern": pattern, "files": matches, "count": len(matches)} + except Exception as e: + return {"error": str(e)} + + +@tool +def search_code( + pattern: str, + path: str = "", + file_glob: str = "*", + context_lines: int = 2, + case_insensitive: bool = False, +) -> dict: + """Search for a regex pattern across files (like grep -n). + + Args: + pattern: Regular expression to search for. + path: Directory or file to search (relative to working dir). + file_glob: Glob to filter files (e.g. ``*.py``, ``*.{ts,tsx}``). + context_lines: Lines of context before/after each match. + case_insensitive: If True, search is case-insensitive. + """ + base = Path(WORKDIR) / path if path else Path(WORKDIR) + flags = re.IGNORECASE if case_insensitive else 0 + try: + compiled = re.compile(pattern, flags) + except re.error as e: + return {"error": f"Invalid regex: {e}"} + + results: list[dict] = [] + search_root = base if base.is_dir() else base.parent + glob_iter = ( + search_root.glob(file_glob) + if not base.is_dir() and base.is_file() + else search_root.rglob(file_glob) + ) + if base.is_file(): + glob_iter = iter([base]) + + for fpath in glob_iter: + if not fpath.is_file(): + continue + try: + lines = fpath.read_text(encoding="utf-8", errors="replace").splitlines() + except Exception: + continue + for i, line in enumerate(lines): + if compiled.search(line): + start = max(0, i - context_lines) + end = min(len(lines), i + context_lines + 1) + snippet = "\n".join( + f"{'>' if j == i else ' '} {j + 1}\t{lines[j]}" for j in range(start, end) + ) + results.append( + { + "file": str(fpath.relative_to(Path(WORKDIR))), + "line": i + 1, + "match": line, + "snippet": snippet, + } + ) + + return { + "pattern": pattern, + "matches": len(results), + "results": results[:100], # cap to avoid huge payloads + } + + +# ── Shared CLI config ────────────────────────────────────────────────────────── + +_CLI = CliConfig( + allowed_commands=[ + "bash", + "sh", + "python", + "python3", + "pytest", + "uv", + "pip", + "git", + "gh", + "npm", + "npx", + "node", + "yarn", + "pnpm", + "cargo", + "go", + "make", + "ls", + "cat", + "find", + "echo", + "curl", + "jq", + "ruff", + "mypy", + ], + allow_shell=True, + timeout=120, + working_dir=WORKDIR, +) + +_FILE_TOOLS = [read_file, write_file, edit_file, list_files, search_code] + +# ── QA Reviewer ─────────────────────────────────────────────────────────────── + +qa_reviewer = Agent( + name="qa_reviewer", + model=MODEL, + instructions="""\ +You are a senior code reviewer and QA engineer. You receive code that the coder +has just written or modified. + +Your job: +1. Read the changed files using read_file and list_files. +2. Check for correctness, edge cases, style issues, security problems. +3. Run the test suite (pytest, npm test, cargo test, go test, etc.) if it exists. +4. Run the linter if the project has one (ruff, eslint, etc.). + +If you find critical bugs or test failures: +- Clearly describe each issue with the file name and line number. +- Transfer back to the coder with a concise list of fixes needed. + +If everything looks good: +- Confirm the code is correct and the tests pass. +- Write a short QA report summarising what was checked. +- Do NOT transfer back to the coder. + +IMPORTANT: Only transfer back if there are real problems. Do not nitpick style +issues that don't affect correctness unless the project has a strict linter. +""", + tools=_FILE_TOOLS, + local_code_execution=True, + cli_config=_CLI, + max_turns=12, + max_tokens=8192, +) + +# ── Coder ───────────────────────────────────────────────────────────────────── + +coder = Agent( + name="coder", + model=MODEL, + instructions=f"""\ +You are an expert software engineer acting as a coding assistant. +Working directory: {WORKDIR} + +Available tools: + read_file — read a file with line numbers + write_file — create or overwrite a file + edit_file — exact string replacement (PREFERRED for small edits) + list_files — glob files (use to explore the project) + search_code — regex search across files + run_command — run shell commands (git, python, pytest, npm, …) + execute_code — run Python/Bash snippets inline + +Workflow for every task: +1. EXPLORE first — use list_files and read_file to understand what already exists. +2. PLAN — think through the change before writing any code. +3. IMPLEMENT — prefer edit_file for targeted changes, write_file for new files. +4. VERIFY — run the code / tests to confirm it works. +5. COMMIT (if asked) — stage and commit with a clear message. +6. HAND OFF to qa_reviewer once your changes are complete and tested. + +Rules: +- Make the SMALLEST correct change that satisfies the request. +- Match existing code style exactly. +- Never skip verification — always run the code or tests before handing off. +- If a command fails, read the error, diagnose, and fix before retrying. +- If the task is ambiguous, make a reasonable assumption and state it clearly. +""", + tools=_FILE_TOOLS, + local_code_execution=True, + cli_config=_CLI, + agents=[qa_reviewer], + strategy=Strategy.SWARM, + max_turns=25, + max_tokens=8192, + timeout_seconds=600, + memory=ConversationMemory(max_messages=50), +) + +# ── Entry point ─────────────────────────────────────────────────────────────── + +def _banner() -> None: + provider = MODEL.split("/")[0] if "/" in MODEL else MODEL + print("=" * 60) + print(" Coding Agent (Agentspan fallback for Claude Code outages)") + print(f" Model : {MODEL}") + print(f" Workdir: {WORKDIR}") + print("=" * 60) + print(" Type your task and press Enter. Ctrl+C or Ctrl+D to exit.") + print() + + +if __name__ == "__main__": + with AgentRuntime() as runtime: + if len(sys.argv) > 1: + # Non-interactive: task passed as CLI argument(s) + task = " ".join(sys.argv[1:]) + print(f"Task: {task}\n") + result = runtime.run(coder, task) + result.print_result() + else: + # Interactive REPL + _banner() + while True: + try: + task = input("> ").strip() + except (KeyboardInterrupt, EOFError): + print("\nBye!") + break + if not task: + continue + print() + result = runtime.run(coder, task) + result.print_result() + print() + + # Production deployment pattern: + # 1. Deploy once: runtime.deploy(coder) + # 2. Serve workers: runtime.serve(coder) + # CLI: agentspan deploy --package examples.62_coding_agent_openai diff --git a/sdk/python/examples/75_wait_for_message.py b/sdk/python/examples/75_wait_for_message.py index c644210b6..5de03067a 100644 --- a/sdk/python/examples/75_wait_for_message.py +++ b/sdk/python/examples/75_wait_for_message.py @@ -14,7 +14,7 @@ Requirements: - Conductor server with WMQ support (conductor.workflow-message-queue.enabled=true) - - AGENTSPAN_SERVER_URL=http://localhost:8080/api as environment variable + - AGENTSPAN_SERVER_URL=http://localhost:6767/api as environment variable - AGENTSPAN_LLM_MODEL=openai/gpt-4o-mini as environment variable """ diff --git a/sdk/python/examples/76_wait_for_message_streaming.py b/sdk/python/examples/76_wait_for_message_streaming.py index acbfefa62..71ad01833 100644 --- a/sdk/python/examples/76_wait_for_message_streaming.py +++ b/sdk/python/examples/76_wait_for_message_streaming.py @@ -14,7 +14,7 @@ Requirements: - Conductor server with WMQ support (conductor.workflow-message-queue.enabled=true) - - AGENTSPAN_SERVER_URL=http://localhost:8080/api as environment variable + - AGENTSPAN_SERVER_URL=http://localhost:6767/api as environment variable - AGENTSPAN_LLM_MODEL=openai/gpt-4o-mini as environment variable """ diff --git a/sdk/python/examples/77_kafka_consumer_agent.py b/sdk/python/examples/77_kafka_consumer_agent.py index 6bafb01fa..7db8de3a2 100644 --- a/sdk/python/examples/77_kafka_consumer_agent.py +++ b/sdk/python/examples/77_kafka_consumer_agent.py @@ -17,7 +17,7 @@ Requirements: - Kafka broker on localhost:9092 with topic le_random_topic - Conductor server with WMQ support (conductor.workflow-message-queue.enabled=true) - - AGENTSPAN_SERVER_URL=http://localhost:8080/api as environment variable + - AGENTSPAN_SERVER_URL=http://localhost:6767/api as environment variable - AGENTSPAN_LLM_MODEL=openai/gpt-4o-mini as environment variable - confluent-kafka (uv pip install confluent-kafka) """ diff --git a/sdk/python/examples/78_approval_workflow.py b/sdk/python/examples/78_approval_workflow.py index cc65e237e..3af9e78a2 100644 --- a/sdk/python/examples/78_approval_workflow.py +++ b/sdk/python/examples/78_approval_workflow.py @@ -35,7 +35,7 @@ Requirements: - Conductor server with WMQ support (conductor.workflow-message-queue.enabled=true) - - AGENTSPAN_SERVER_URL=http://localhost:8080/api as environment variable + - AGENTSPAN_SERVER_URL=http://localhost:6767/api as environment variable - AGENTSPAN_LLM_MODEL=openai/gpt-4o-mini as environment variable """ diff --git a/sdk/python/examples/79_agent_message_bus.py b/sdk/python/examples/79_agent_message_bus.py index 91e9e1466..f2297a440 100644 --- a/sdk/python/examples/79_agent_message_bus.py +++ b/sdk/python/examples/79_agent_message_bus.py @@ -30,7 +30,7 @@ Requirements: - Conductor server with WMQ support (conductor.workflow-message-queue.enabled=true) - - AGENTSPAN_SERVER_URL=http://localhost:8080/api as environment variable + - AGENTSPAN_SERVER_URL=http://localhost:6767/api as environment variable - AGENTSPAN_LLM_MODEL=openai/gpt-4o-mini as environment variable """ diff --git a/sdk/python/examples/80_live_dashboard.py b/sdk/python/examples/80_live_dashboard.py index 17ad12d6b..d454bb1a7 100644 --- a/sdk/python/examples/80_live_dashboard.py +++ b/sdk/python/examples/80_live_dashboard.py @@ -39,7 +39,7 @@ Requirements: - Conductor server with WMQ support (conductor.workflow-message-queue.enabled=true) - - AGENTSPAN_SERVER_URL=http://localhost:8080/api as environment variable + - AGENTSPAN_SERVER_URL=http://localhost:6767/api as environment variable - AGENTSPAN_LLM_MODEL=anthropic/claude-sonnet-4-20250514 as environment variable """ diff --git a/sdk/python/examples/81_chat_repl.py b/sdk/python/examples/81_chat_repl.py index d2962c075..e162c4836 100644 --- a/sdk/python/examples/81_chat_repl.py +++ b/sdk/python/examples/81_chat_repl.py @@ -45,7 +45,7 @@ Requirements: - Conductor server with WMQ support (conductor.workflow-message-queue.enabled=true) - - AGENTSPAN_SERVER_URL=http://localhost:8080/api as environment variable + - AGENTSPAN_SERVER_URL=http://localhost:6767/api as environment variable - AGENTSPAN_LLM_MODEL=anthropic/claude-sonnet-4-20250514 as environment variable """ diff --git a/sdk/python/examples/82_coding_agent.py b/sdk/python/examples/82_coding_agent.py index d4d011612..0bb5434c0 100644 --- a/sdk/python/examples/82_coding_agent.py +++ b/sdk/python/examples/82_coding_agent.py @@ -10,7 +10,7 @@ - Every tool call, LLM decision, and token is logged on the server automatically - /signal injects context mid-task without restarting the agent - Ctrl+C stops gracefully (current task finishes, output preserved) - - View the full execution graph live at http://localhost:8080 + - View the full execution graph live at http://localhost:6767 Usage: python 82_coding_agent.py # new session in current dir @@ -19,7 +19,7 @@ Requirements: - Conductor server (conductor.workflow-message-queue.enabled=true) - - AGENTSPAN_SERVER_URL=http://localhost:8080/api + - AGENTSPAN_SERVER_URL=http://localhost:6767/api - AGENTSPAN_LLM_MODEL=anthropic/claude-sonnet-4-20250514 """ diff --git a/sdk/python/examples/82b_coding_agent_tui.py b/sdk/python/examples/82b_coding_agent_tui.py index e49ad3766..d3395d1cc 100644 --- a/sdk/python/examples/82b_coding_agent_tui.py +++ b/sdk/python/examples/82b_coding_agent_tui.py @@ -16,7 +16,7 @@ Requirements: - pip install prompt_toolkit - Conductor server (conductor.workflow-message-queue.enabled=true) - - AGENTSPAN_SERVER_URL=http://localhost:8080/api + - AGENTSPAN_SERVER_URL=http://localhost:6767/api - AGENTSPAN_LLM_MODEL=anthropic/claude-sonnet-4-20250514 """ diff --git a/sdk/python/examples/83_stateful_resume.py b/sdk/python/examples/83_stateful_resume.py index ab4e2c9df..beec8f0fb 100644 --- a/sdk/python/examples/83_stateful_resume.py +++ b/sdk/python/examples/83_stateful_resume.py @@ -32,7 +32,7 @@ Requirements: - Conductor server with WMQ support (conductor.workflow-message-queue.enabled=true) - - AGENTSPAN_SERVER_URL=http://localhost:8080/api as environment variable + - AGENTSPAN_SERVER_URL=http://localhost:6767/api as environment variable - AGENTSPAN_LLM_MODEL=openai/gpt-4o-mini as environment variable """ diff --git a/sdk/python/examples/91_slack_autofix_agent.py b/sdk/python/examples/91_slack_autofix_agent.py index 7c9aa5ab2..6dd1e5f61 100644 --- a/sdk/python/examples/91_slack_autofix_agent.py +++ b/sdk/python/examples/91_slack_autofix_agent.py @@ -153,6 +153,8 @@ def write_file(file_path: str, content: str) -> str: file_path: Path relative to repo root content: Full file content to write """ + if content is None: + return "Error: content is required — pass the full file text to write" if DRY_RUN: return f"[DRY RUN] Would write {len(content)} chars to {file_path}" full_path = REPO_PATH / file_path