Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 4 additions & 46 deletions agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@
import os
import sys
import json
import time
import logging
import requests

from prompts import SYSTEM_PROMPT
from tools import TOOL_SCHEMAS, dispatch
from llm import call_openrouter


LOGGER = logging.getLogger("coding_agent")
Expand Down Expand Up @@ -83,55 +82,14 @@ def get_config():
return api_key, model


def call_openrouter(messages: list, tools: list, api_key: str, model: str) -> dict:
"""Call the OpenRouter chat completions API with retry on 5xx errors."""
payload = {
"model": model,
"messages": messages,
"tools": tools,
}
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}

for attempt in range(3):
response = requests.post(
"https://openrouter.ai/api/v1/chat/completions",
headers=headers,
json=payload,
timeout=120,
)
if response.status_code >= 500 and attempt < 2:
backoff_seconds = 2 ** attempt
LOGGER.warning(
"Server error (%s), retrying attempt %s/3 in %ss",
response.status_code,
attempt + 1,
backoff_seconds,
)
time.sleep(backoff_seconds)
continue
break

if response.status_code != 200:
raise Exception(
f"API error {response.status_code}: {response.text[:500]}"
)
data = response.json()
if "error" in data:
raise Exception(f"API error: {data['error']}")
return data


def process_tool_calls(tool_calls: list) -> list:
def process_tool_calls(tool_calls: list, api_key: str, model: str) -> list:
"""Execute tool calls and return tool result messages."""
results = []
for tc in tool_calls:
name = tc["function"]["name"]
args = tc["function"].get("arguments", "{}")
LOGGER.info("🔧 %s(%s%s)", name, args[:80], "..." if len(args) > 80 else "")
output = dispatch(name, args)
output = dispatch(name, args, context={"api_key": api_key, "model": model})
results.append({
"role": "tool",
"tool_call_id": tc["id"],
Expand Down Expand Up @@ -186,7 +144,7 @@ def agent_loop():
tool_calls = msg.get("tool_calls")
if tool_calls:
LOGGER.info("Processing %d tool call(s).", len(tool_calls))
tool_results = process_tool_calls(tool_calls)
tool_results = process_tool_calls(tool_calls, api_key, model)
messages.extend(tool_results)
continue # Loop back for the model's next response

Expand Down
49 changes: 49 additions & 0 deletions llm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""LLM calling logic for OpenRouter API."""

import time
import logging
import requests


LOGGER = logging.getLogger("coding_agent")


def call_openrouter(messages: list, tools: list, api_key: str, model: str) -> dict:
"""Call the OpenRouter chat completions API with retry on 5xx errors."""
payload = {
"model": model,
"messages": messages,
"tools": tools,
}
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}

for attempt in range(3):
response = requests.post(
"https://openrouter.ai/api/v1/chat/completions",
headers=headers,
json=payload,
timeout=120,
)
if response.status_code >= 500 and attempt < 2:
backoff_seconds = 2 ** attempt
LOGGER.warning(
"Server error (%s), retrying attempt %s/3 in %ss",
response.status_code,
attempt + 1,
backoff_seconds,
)
time.sleep(backoff_seconds)
continue
break

if response.status_code != 200:
raise Exception(
f"API error {response.status_code}: {response.text[:500]}"
)
data = response.json()
if "error" in data:
raise Exception(f"API error: {data['error']}")
return data
1 change: 1 addition & 0 deletions prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- run_command(command, timeout?): Execute a shell command in the current OS environment (default timeout: 30s)
- list_directory(path?): List files in a directory (default: current directory)
- search_files(pattern, path?): Search for text patterns across files using OS-aware behavior
- run_subagent(task, role?): Delegate a task to a specialized subagent

Guidelines:
- Use tools to explore before making changes
Expand Down
64 changes: 61 additions & 3 deletions tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import os
import json
from sandbox import run_command as sandbox_run
from llm import call_openrouter

# --- Tool Schemas (OpenAI function-calling format) ---

Expand Down Expand Up @@ -104,6 +105,27 @@
},
},
},
{
"type": "function",
"function": {
"name": "run_subagent",
"description": "Run a subagent with a specific role to perform a delegated task such as research, monitoring, or interaction.",
"parameters": {
"type": "object",
"properties": {
"task": {
"type": "string",
"description": "The task description for the subagent to execute",
},
"role": {
"type": "string",
"description": "The role of the subagent (e.g. researcher, monitor, interactors)",
},
},
"required": ["task", "role"],
},
},
},
]

# --- Tool Implementations ---
Expand Down Expand Up @@ -208,22 +230,56 @@ def search_files(pattern: str, path: str = ".") -> str:
return "\n".join(matches)


def run_subagent(task: str, role: str, context: dict = None) -> str:
"""Run a subagent with a specific role to perform a delegated task.

Uses call_openrouter from llm.py to execute the subagent task and returns
the subagent's response text.
"""
if context is None:
return "Error: run_subagent requires a context with api_key and model"

api_key = context.get("api_key")
model = context.get("model")
if not api_key or not model:
return "Error: run_subagent requires both api_key and model in context"

system_prompt = (
f"You are a subagent with the role: {role}. "
f"Complete the following task concisely and accurately."
)
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": task},
]

try:
response = call_openrouter(messages=messages, tools=None, api_key=api_key, model=model)
except Exception as e:
return f"Error running subagent: {e}"

try:
return response["choices"][0]["message"]["content"]
except (KeyError, IndexError, TypeError) as e:
return f"Error parsing subagent response: {e}"


# --- Dispatch ---

def dispatch(name: str, args_json: str) -> str:
def dispatch(name: str, args_json: str, context: dict = None) -> str:
"""Dispatch a tool call by name, parsing the JSON arguments."""
try:
args = json.loads(args_json) if args_json else {}
except json.JSONDecodeError:
return f"Error: Invalid JSON arguments: {args_json}"

try:
return _dispatch(name, args)
return _dispatch(name, args, context=context)
except KeyError as e:
return f"Error: Missing required argument {e} for tool '{name}'"


def _dispatch(name: str, args: dict) -> str:
def _dispatch(name: str, args: dict, context: dict = None) -> str:
if name == "read_file":
return read_file(args["path"])
elif name == "write_file":
Expand All @@ -234,5 +290,7 @@ def _dispatch(name: str, args: dict) -> str:
return list_directory(args.get("path", "."))
elif name == "search_files":
return search_files(args["pattern"], args.get("path", "."))
elif name == "run_subagent":
return run_subagent(args["task"], args["role"], context=context)
else:
return f"Error: Unknown tool '{name}'"