from typing import List, Optional
import requests
from .models import (
GoalState,
MessageResponse,
PlanProposedMessage,
RunResponse,
SessionDetail,
SessionInfo,
SlashCommandInfo,
ToolInfo,
)
class RecursiveClient:
def __init__(self, base_url: str = "http://127.0.0.1:3000"):
self.base_url = base_url.rstrip("/")
self.session = requests.Session()
def health(self) -> str:
resp = self.session.get(f"{self.base_url}/health")
resp.raise_for_status()
return resp.text
def list_tools(self) -> List[ToolInfo]:
resp = self.session.get(f"{self.base_url}/tools")
resp.raise_for_status()
return [ToolInfo(**t) for t in resp.json()]
def run(
self,
goal: str,
max_steps: Optional[int] = None,
system_prompt: Optional[str] = None,
) -> RunResponse:
body: dict = {"goal": goal}
if max_steps is not None:
body["max_steps"] = max_steps
if system_prompt is not None:
body["system_prompt"] = system_prompt
resp = self.session.post(f"{self.base_url}/run", json=body)
resp.raise_for_status()
return RunResponse(**resp.json())
def create_session(self, system_prompt: Optional[str] = None) -> str:
body: dict = {}
if system_prompt is not None:
body["system_prompt"] = system_prompt
resp = self.session.post(f"{self.base_url}/sessions", json=body)
resp.raise_for_status()
return resp.json()["id"]
def list_sessions(self) -> List[SessionInfo]:
resp = self.session.get(f"{self.base_url}/sessions")
resp.raise_for_status()
return [SessionInfo(**s) for s in resp.json()]
def send_message(self, session_id: str, content: str) -> MessageResponse:
resp = self.session.post(
f"{self.base_url}/sessions/{session_id}/messages",
json={"content": content},
)
resp.raise_for_status()
return MessageResponse(**resp.json())
def get_session(self, session_id: str) -> SessionDetail:
resp = self.session.get(f"{self.base_url}/sessions/{session_id}")
resp.raise_for_status()
data = resp.json()
known = {k: v for k, v in data.items() if k in ("id", "created_at", "messages", "status", "pending_plan")}
detail = SessionDetail(**known)
raw_goal = data.get("goal")
if raw_goal is not None:
detail.__dict__["goal"] = raw_goal
return detail
def delete_session(self, session_id: str) -> None:
resp = self.session.delete(f"{self.base_url}/sessions/{session_id}")
resp.raise_for_status()
def approve_plan(self, session_id: str, edits: Optional[str] = None) -> dict:
body: dict = {}
if edits is not None:
body["edits"] = edits
resp = self.session.post(
f"{self.base_url}/sessions/{session_id}/plan/confirm",
json=body,
)
resp.raise_for_status()
return resp.json()
def reject_plan(self, session_id: str, reason: str = "") -> dict:
resp = self.session.post(
f"{self.base_url}/sessions/{session_id}/plan/reject",
json={"reason": reason},
)
resp.raise_for_status()
return resp.json()
def set_goal(
self,
session_id: str,
condition: str,
max_turns: int = 20,
) -> dict:
resp = self.session.post(
f"{self.base_url}/sessions/{session_id}/goal",
json={"condition": condition, "max_turns": max_turns},
)
resp.raise_for_status()
return resp.json()
def clear_goal(self, session_id: str) -> dict:
resp = self.session.delete(f"{self.base_url}/sessions/{session_id}/goal")
resp.raise_for_status()
return resp.json()
def get_goal(self, session_id: str) -> Optional[GoalState]:
detail = self.get_session(session_id)
raw = detail.__dict__.get("goal")
if raw is None:
return None
if isinstance(raw, dict):
return GoalState(**raw)
return raw
def list_slash_commands(self) -> List[SlashCommandInfo]:
resp = self.session.get(f"{self.base_url}/slash-commands")
resp.raise_for_status()
return [
SlashCommandInfo(
name=c["name"],
description=c["description"],
source=c["source"],
aliases=c.get("aliases", []),
argument_hint=c.get("argument_hint", ""),
)
for c in resp.json()
]