diff --git a/agent.py b/agent.py index b21092a..14cb173 100644 --- a/agent.py +++ b/agent.py @@ -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") @@ -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"], @@ -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 diff --git a/llm.py b/llm.py new file mode 100644 index 0000000..0823b73 --- /dev/null +++ b/llm.py @@ -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 \ No newline at end of file diff --git a/prompts.py b/prompts.py index 1625a45..f7bed1d 100644 --- a/prompts.py +++ b/prompts.py @@ -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 diff --git a/tools.py b/tools.py index 871a344..deaeaa1 100644 --- a/tools.py +++ b/tools.py @@ -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) --- @@ -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 --- @@ -208,9 +230,43 @@ 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 {} @@ -218,12 +274,12 @@ def dispatch(name: str, args_json: str) -> str: 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": @@ -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}'" \ No newline at end of file