Files
openswarm/backend/apps/agents/tools/system.py
T
ciregenz ebea25f0c2 [eric] [eric] add PostHog analytics, 9Router subscription proxy, unified usage tracking
- PostHog integration: collector, analytics subapp, opt-in UI, Analytics page
  - 9Router: auto-start, OAuth subscription flow, /v1/messages Anthropic format support
  - Settings overhaul: multi-provider API keys, subscription connect UI, onboarding modal
  - Unified usage: merge 9Router cost/token data into Settings Usage tab
  - Provider system: providers/, agent_loop, tools/ (unused, for future non-Anthropic support)
  - Agent SDK: restored as primary with 9Router ANTHROPIC_BASE_URL fallback
  - Updated system prompt, credential resolution, dashboard analytics
2026-03-24 14:02:58 -07:00

126 lines
4.2 KiB
Python

"""System tools: Bash and AskUserQuestion."""
from __future__ import annotations
import asyncio
from backend.apps.agents.tools.base import BaseTool, ToolContext
_MAX_OUTPUT_BYTES = 100 * 1024 # ~100 KB cap
class BashTool(BaseTool):
name = "Bash"
description = (
"Execute a shell command and return its output. The command runs in "
"the session's working directory. Supports an optional timeout "
"(default 120 000 ms). Stdout and stderr are captured and returned."
)
def get_schema(self) -> dict:
return {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The shell command to execute.",
},
"timeout": {
"type": "integer",
"description": "Timeout in milliseconds (default 120000, max 600000).",
"default": 120000,
},
"description": {
"type": "string",
"description": "Optional human-readable description of what this command does.",
},
},
"required": ["command"],
"additionalProperties": False,
}
async def execute(self, input_data: dict, context: ToolContext) -> list[dict]:
command: str = input_data["command"]
timeout_ms: int = min(input_data.get("timeout", 120000), 600000)
timeout_s: float = timeout_ms / 1000.0
try:
proc = await asyncio.create_subprocess_shell(
command,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=context.cwd,
)
except Exception as exc:
return [{"type": "text", "text": f"Error starting command: {exc}"}]
try:
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout_s)
except asyncio.TimeoutError:
# Attempt to kill the process
try:
proc.kill()
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=5)
except Exception:
stdout, stderr = b"", b""
partial = self._decode(stdout, stderr)
msg = (
f"Command timed out after {timeout_ms}ms.\n"
f"Partial output:\n{partial}"
)
return [{"type": "text", "text": self._truncate(msg)}]
except Exception as exc:
return [{"type": "text", "text": f"Error executing command: {exc}"}]
output = self._decode(stdout, stderr)
if proc.returncode != 0:
output = f"Exit code: {proc.returncode}\n{output}"
if not output.strip():
output = f"(command completed with exit code {proc.returncode})"
return [{"type": "text", "text": self._truncate(output)}]
@staticmethod
def _decode(stdout: bytes, stderr: bytes) -> str:
parts: list[str] = []
if stdout:
parts.append(stdout.decode("utf-8", errors="replace"))
if stderr:
parts.append(stderr.decode("utf-8", errors="replace"))
return "\n".join(parts)
@staticmethod
def _truncate(text: str) -> str:
if len(text) > _MAX_OUTPUT_BYTES:
return text[:_MAX_OUTPUT_BYTES] + "\n... (output truncated)"
return text
class AskUserQuestionTool(BaseTool):
name = "AskUserQuestion"
description = (
"Ask the user a clarifying question. The actual blocking/HITL "
"interaction is handled by the agent loop's hitl_handler; this tool "
"simply surfaces the question text."
)
def get_schema(self) -> dict:
return {
"type": "object",
"properties": {
"question": {
"type": "string",
"description": "The question to ask the user.",
},
},
"required": ["question"],
"additionalProperties": False,
}
async def execute(self, input_data: dict, context: ToolContext) -> list[dict]:
question: str = input_data.get("question", "")
return [{"type": "text", "text": question}]