from __future__ import annotations
import os
from typing import List, Optional
from ._http import _HttpClient
from .exceptions import RecursiveAgentError
from .models import GoalState, RunResult, SessionInfo
from .run import Run
class _AgentSession:
def __init__(
self,
session_id: str,
http: _HttpClient,
*,
_owns_session: bool = True,
) -> None:
self._session_id = session_id
self._http = http
self._owns_session = _owns_session
self._closed = False
@property
def session_id(self) -> str:
return self._session_id
def send(self, message: str) -> Run:
if self._closed:
raise RecursiveAgentError("Agent session is already closed.")
run = Run(session_id=self._session_id, http=self._http)
def _dispatch() -> None:
try:
self._http.post(
f"/sessions/{self._session_id}/messages",
{"content": message},
)
except Exception as err: run._fail(str(err))
import threading
thread = threading.Thread(target=_dispatch, daemon=True)
thread.start()
run._send_thread = thread
return run
def set_goal(
self,
condition: str,
*,
max_turns: int = 20,
) -> GoalState:
if self._closed:
raise RecursiveAgentError("Agent session is already closed.")
resp = self._http.post(
f"/sessions/{self._session_id}/goal",
{"condition": condition, "max_turns": max_turns},
)
data = resp.json()
return GoalState(
condition=condition,
status=data.get("status", "pursuing"),
turns=0,
max_turns=max_turns,
)
def clear_goal(self) -> GoalState:
if self._closed:
raise RecursiveAgentError("Agent session is already closed.")
data = self._http.delete_json(f"/sessions/{self._session_id}/goal")
return GoalState(
condition="",
status=data.get("status", "cleared"),
)
def get_goal(self) -> Optional[GoalState]:
if self._closed:
raise RecursiveAgentError("Agent session is already closed.")
data = self._http.get(f"/sessions/{self._session_id}").json()
raw = data.get("goal")
if not raw:
return None
return GoalState(
condition=raw.get("condition", ""),
status=raw.get("status", "pursuing"),
turns=raw.get("turns", 0),
max_turns=raw.get("max_turns", 20),
last_reason=raw.get("last_reason"),
)
def close(self) -> None:
if not self._closed:
self._closed = True
if self._owns_session:
try:
self._http.delete(f"/sessions/{self._session_id}")
except RecursiveAgentError:
pass self._http.close()
def __enter__(self) -> "_AgentSession":
return self
def __exit__(self, *_: object) -> None:
self.close()
class Agent:
@staticmethod
def create(
*,
base_url: Optional[str] = None,
api_key: Optional[str] = None,
system_prompt: Optional[str] = None,
timeout: float = 120.0,
) -> _AgentSession:
http = _make_client(base_url, api_key, timeout)
body: dict = {}
if system_prompt:
body["system_prompt"] = system_prompt
resp = http.post("/sessions", body)
session_id = resp.json()["id"]
return _AgentSession(session_id, http, _owns_session=True)
@staticmethod
def resume(
session_id: str,
*,
base_url: Optional[str] = None,
api_key: Optional[str] = None,
timeout: float = 120.0,
) -> _AgentSession:
http = _make_client(base_url, api_key, timeout)
http.get(f"/sessions/{session_id}")
return _AgentSession(session_id, http, _owns_session=False)
@staticmethod
def prompt(
message: str,
*,
base_url: Optional[str] = None,
api_key: Optional[str] = None,
system_prompt: Optional[str] = None,
max_steps: Optional[int] = None,
timeout: float = 120.0,
) -> RunResult:
http = _make_client(base_url, api_key, timeout)
body: dict = {"goal": message}
if system_prompt:
body["system_prompt"] = system_prompt
if max_steps is not None:
body["max_steps"] = max_steps
resp = http.post("/run", body)
data = resp.json()
usage = None
if "usage" in data:
from .run import _parse_usage
usage = _parse_usage(data["usage"])
return RunResult(
id=data.get("session_id", ""),
status=data.get("status", "finished"),
finish_reason=data.get("finish_reason"),
usage=usage,
error=data.get("error"),
)
@staticmethod
def list_sessions(
*,
limit: Optional[int] = None,
offset: Optional[int] = None,
base_url: Optional[str] = None,
api_key: Optional[str] = None,
) -> List[SessionInfo]:
http = _make_client(base_url, api_key)
params: dict = {}
if limit is not None:
params["limit"] = limit
if offset is not None:
params["offset"] = offset
url = "/sessions"
if params:
from urllib.parse import urlencode
url = f"/sessions?{urlencode(params)}"
resp = http.get(url)
return [
SessionInfo(
id=s["id"],
created_at=s.get("created_at", ""),
message_count=s.get("message_count", 0),
last_prompt=s.get("last_prompt"),
first_prompt=s.get("first_prompt"),
goal=s.get("goal"),
)
for s in resp.json()
]
@staticmethod
def rename_session(
session_id: str,
title: str,
*,
base_url: Optional[str] = None,
api_key: Optional[str] = None,
) -> None:
http = _make_client(base_url, api_key)
http.patch(f"/sessions/{session_id}", {"title": title})
http.close()
@staticmethod
def fork_session(
session_id: str,
*,
base_url: Optional[str] = None,
api_key: Optional[str] = None,
) -> "SessionInfo":
http = _make_client(base_url, api_key)
resp = http.post(f"/sessions/{session_id}/fork", {})
data = resp.json()
http.close()
return SessionInfo(
id=data["id"],
created_at=data.get("created_at", ""),
message_count=data.get("message_count", 0),
)
@staticmethod
def delete_session(
session_id: str,
*,
base_url: Optional[str] = None,
api_key: Optional[str] = None,
) -> None:
http = _make_client(base_url, api_key)
http.delete(f"/sessions/{session_id}")
http.close()
@staticmethod
def get_session_messages(
session_id: str,
*,
base_url: Optional[str] = None,
api_key: Optional[str] = None,
) -> List[dict]:
http = _make_client(base_url, api_key)
data = http.get(f"/sessions/{session_id}").json()
http.close()
return list(data.get("messages", []))
def _make_client(
base_url: Optional[str],
api_key: Optional[str],
timeout: float = 120.0,
) -> _HttpClient:
url = base_url or os.environ.get("RECURSIVE_BASE_URL", "http://127.0.0.1:3000")
return _HttpClient(base_url=url, api_key=api_key, timeout=timeout)