steerplane 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,63 @@
1
+ Metadata-Version: 2.4
2
+ Name: steerplane
3
+ Version: 0.1.0
4
+ Summary: Agent Control Plane for Autonomous Systems
5
+ Home-page: https://github.com/vijaym2k6/SteerPlane
6
+ Author: SteerPlane
7
+ Author-email: SteerPlane <hello@steerplane.ai>
8
+ License: MIT
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: requests>=2.28.0
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest>=7.0; extra == "dev"
14
+ Requires-Dist: pytest-cov; extra == "dev"
15
+ Dynamic: author
16
+ Dynamic: home-page
17
+ Dynamic: requires-python
18
+
19
+ # SteerPlane SDK
20
+
21
+ **Agent Control Plane for Autonomous Systems**
22
+
23
+ > "Agents don't fail in the dark anymore."
24
+
25
+ ## Quick Start
26
+
27
+ ```bash
28
+ pip install steerplane
29
+ ```
30
+
31
+ ### Decorator API (Minimal Integration)
32
+
33
+ ```python
34
+ from steerplane import guard
35
+
36
+ @guard(max_cost_usd=10, max_steps=50)
37
+ def run_agent():
38
+ agent.run()
39
+ ```
40
+
41
+ ### Context Manager API (Full Control)
42
+
43
+ ```python
44
+ from steerplane import SteerPlane
45
+
46
+ sp = SteerPlane(agent_id="my_bot")
47
+
48
+ with sp.run(max_cost_usd=10) as run:
49
+ run.log_step("query_db", tokens=380, cost=0.002)
50
+ run.log_step("generate_response", tokens=1240, cost=0.008)
51
+ ```
52
+
53
+ ## Features
54
+
55
+ - 🔄 **Loop Detection** — Automatically detects repeating agent behavior
56
+ - 💰 **Cost Limits** — Stop expensive agent runs before they blow budgets
57
+ - 🚫 **Step Limits** — Prevent runaway execution
58
+ - 📊 **Telemetry** — Full step-by-step execution tracking
59
+ - 🖥️ **Dashboard** — Visual execution monitoring
60
+
61
+ ## Documentation
62
+
63
+ See [docs.steerplane.ai](https://docs.steerplane.ai) for full documentation.
@@ -0,0 +1,45 @@
1
+ # SteerPlane SDK
2
+
3
+ **Agent Control Plane for Autonomous Systems**
4
+
5
+ > "Agents don't fail in the dark anymore."
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
10
+ pip install steerplane
11
+ ```
12
+
13
+ ### Decorator API (Minimal Integration)
14
+
15
+ ```python
16
+ from steerplane import guard
17
+
18
+ @guard(max_cost_usd=10, max_steps=50)
19
+ def run_agent():
20
+ agent.run()
21
+ ```
22
+
23
+ ### Context Manager API (Full Control)
24
+
25
+ ```python
26
+ from steerplane import SteerPlane
27
+
28
+ sp = SteerPlane(agent_id="my_bot")
29
+
30
+ with sp.run(max_cost_usd=10) as run:
31
+ run.log_step("query_db", tokens=380, cost=0.002)
32
+ run.log_step("generate_response", tokens=1240, cost=0.008)
33
+ ```
34
+
35
+ ## Features
36
+
37
+ - 🔄 **Loop Detection** — Automatically detects repeating agent behavior
38
+ - 💰 **Cost Limits** — Stop expensive agent runs before they blow budgets
39
+ - 🚫 **Step Limits** — Prevent runaway execution
40
+ - 📊 **Telemetry** — Full step-by-step execution tracking
41
+ - 🖥️ **Dashboard** — Visual execution monitoring
42
+
43
+ ## Documentation
44
+
45
+ See [docs.steerplane.ai](https://docs.steerplane.ai) for full documentation.
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["setuptools>=65.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "steerplane"
7
+ version = "0.1.0"
8
+ description = "Agent Control Plane for Autonomous Systems"
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ {name = "SteerPlane", email = "hello@steerplane.ai"}
14
+ ]
15
+ dependencies = [
16
+ "requests>=2.28.0",
17
+ ]
18
+
19
+ [project.optional-dependencies]
20
+ dev = [
21
+ "pytest>=7.0",
22
+ "pytest-cov",
23
+ ]
24
+
25
+ [tool.pytest.ini_options]
26
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,41 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="steerplane",
5
+ version="0.1.0",
6
+ description="Agent Control Plane for Autonomous Systems — Runtime guards, loop detection, cost limits, and telemetry for AI agents.",
7
+ long_description=open("README.md", encoding="utf-8").read(),
8
+ long_description_content_type="text/markdown",
9
+ author="SteerPlane",
10
+ author_email="hello@steerplane.ai",
11
+ url="https://github.com/vijaym2k6/SteerPlane",
12
+ project_urls={
13
+ "Homepage": "https://steerplane.ai",
14
+ "Documentation": "https://docs.steerplane.ai",
15
+ "Repository": "https://github.com/vijaym2k6/SteerPlane",
16
+ "Issues": "https://github.com/vijaym2k6/SteerPlane/issues",
17
+ },
18
+ packages=find_packages(),
19
+ python_requires=">=3.10",
20
+ install_requires=[
21
+ "requests>=2.28.0",
22
+ ],
23
+ extras_require={
24
+ "dev": [
25
+ "pytest>=7.0",
26
+ "pytest-cov",
27
+ ],
28
+ },
29
+ classifiers=[
30
+ "Development Status :: 3 - Alpha",
31
+ "Intended Audience :: Developers",
32
+ "License :: OSI Approved :: MIT License",
33
+ "Programming Language :: Python :: 3",
34
+ "Programming Language :: Python :: 3.10",
35
+ "Programming Language :: Python :: 3.11",
36
+ "Programming Language :: Python :: 3.12",
37
+ "Topic :: Software Development :: Libraries",
38
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
39
+ ],
40
+ keywords="ai agents guard safety monitoring telemetry llm",
41
+ )
@@ -0,0 +1,69 @@
1
+ """
2
+ SteerPlane SDK
3
+
4
+ Agent Control Plane for Autonomous Systems.
5
+ "Agents don't fail in the dark anymore."
6
+
7
+ Usage:
8
+ from steerplane import guard, SteerPlane
9
+
10
+ # Decorator API (minimal integration)
11
+ @guard(max_cost_usd=10, max_steps=50)
12
+ def run_agent():
13
+ agent.run()
14
+
15
+ # Context Manager API (full control)
16
+ sp = SteerPlane(agent_id="my_bot")
17
+ with sp.run(max_cost_usd=10) as run:
18
+ run.log_step("search", tokens=100)
19
+
20
+ # Programmatic API
21
+ sp = SteerPlane(agent_id="my_bot")
22
+ run = sp.create_run(max_cost_usd=10)
23
+ run.start()
24
+ run.log_step("search", tokens=100)
25
+ run.end()
26
+ """
27
+
28
+ __version__ = "0.1.0"
29
+
30
+ from .guard import guard, SteerPlane, get_active_run
31
+ from .run_manager import RunManager
32
+ from .loop_detector import LoopDetector, detect_loop
33
+ from .cost_tracker import CostTracker
34
+ from .telemetry import TelemetryCollector, StepEvent
35
+ from .config import configure, get_config
36
+ from .exceptions import (
37
+ SteerPlaneError,
38
+ LoopDetectedError,
39
+ CostLimitExceeded,
40
+ StepLimitExceeded,
41
+ RunTerminatedError,
42
+ APIConnectionError,
43
+ )
44
+
45
+ __all__ = [
46
+ # Main APIs
47
+ "guard",
48
+ "SteerPlane",
49
+ "get_active_run",
50
+ # Core classes
51
+ "RunManager",
52
+ "LoopDetector",
53
+ "CostTracker",
54
+ "TelemetryCollector",
55
+ "StepEvent",
56
+ # Utilities
57
+ "detect_loop",
58
+ "configure",
59
+ "get_config",
60
+ # Exceptions
61
+ "SteerPlaneError",
62
+ "LoopDetectedError",
63
+ "CostLimitExceeded",
64
+ "StepLimitExceeded",
65
+ "RunTerminatedError",
66
+ "APIConnectionError",
67
+ # Metadata
68
+ "__version__",
69
+ ]
@@ -0,0 +1,129 @@
1
+ """
2
+ SteerPlane SDK — API Client
3
+
4
+ HTTP client that communicates with the SteerPlane API server.
5
+ Handles run lifecycle: start → log steps → end.
6
+ """
7
+
8
+ import requests
9
+ import logging
10
+ from typing import Any
11
+
12
+ from .config import get_config
13
+ from .exceptions import APIConnectionError
14
+
15
+ logger = logging.getLogger("steerplane")
16
+
17
+
18
+ class SteerPlaneClient:
19
+ """
20
+ HTTP client for the SteerPlane API.
21
+
22
+ Sends run events, step telemetry, and receives commands.
23
+ Gracefully degrades if API is unavailable (SDK still works locally).
24
+ """
25
+
26
+ def __init__(self, api_url: str | None = None, api_key: str | None = None):
27
+ config = get_config()
28
+ self.api_url = (api_url or config.api_url).rstrip("/")
29
+ self.api_key = api_key or config.api_key
30
+ self.session = requests.Session()
31
+ self.session.headers.update({
32
+ "Content-Type": "application/json",
33
+ "User-Agent": "SteerPlane-SDK/0.1.0",
34
+ })
35
+ if self.api_key:
36
+ self.session.headers["Authorization"] = f"Bearer {self.api_key}"
37
+
38
+ self._api_available = True
39
+
40
+ def _request(self, method: str, path: str, **kwargs) -> dict | None:
41
+ """Make an HTTP request to the API. Returns None if API unavailable."""
42
+ if not self._api_available:
43
+ return None
44
+
45
+ url = f"{self.api_url}{path}"
46
+ try:
47
+ response = self.session.request(method, url, timeout=5, **kwargs)
48
+ response.raise_for_status()
49
+ return response.json()
50
+ except requests.ConnectionError:
51
+ self._api_available = False
52
+ logger.warning(
53
+ f"⚠️ SteerPlane API not reachable at {self.api_url}. "
54
+ f"Running in offline mode (guards still active, no dashboard data)."
55
+ )
56
+ return None
57
+ except requests.RequestException as e:
58
+ logger.warning(f"⚠️ API request failed: {e}")
59
+ return None
60
+
61
+ def start_run(
62
+ self,
63
+ run_id: str,
64
+ agent_name: str,
65
+ max_cost_usd: float = 0,
66
+ max_steps: int = 0,
67
+ ) -> dict | None:
68
+ """Register a new run with the API."""
69
+ return self._request("POST", "/runs/start", json={
70
+ "run_id": run_id,
71
+ "agent_name": agent_name,
72
+ "max_cost_usd": max_cost_usd,
73
+ "max_steps": max_steps,
74
+ })
75
+
76
+ def log_step(
77
+ self,
78
+ run_id: str,
79
+ step_number: int,
80
+ action: str,
81
+ tokens: int = 0,
82
+ cost_usd: float = 0.0,
83
+ latency_ms: float = 0.0,
84
+ status: str = "completed",
85
+ error: str | None = None,
86
+ metadata: dict | None = None,
87
+ ) -> dict | None:
88
+ """Log a step event to the API."""
89
+ return self._request("POST", "/runs/step", json={
90
+ "run_id": run_id,
91
+ "step_number": step_number,
92
+ "action": action,
93
+ "tokens": tokens,
94
+ "cost_usd": cost_usd,
95
+ "latency_ms": latency_ms,
96
+ "status": status,
97
+ "error": error,
98
+ "metadata": metadata or {},
99
+ })
100
+
101
+ def end_run(
102
+ self,
103
+ run_id: str,
104
+ status: str = "completed",
105
+ total_cost: float = 0.0,
106
+ total_steps: int = 0,
107
+ error: str | None = None,
108
+ ) -> dict | None:
109
+ """Finalize a run."""
110
+ return self._request("POST", "/runs/end", json={
111
+ "run_id": run_id,
112
+ "status": status,
113
+ "total_cost": total_cost,
114
+ "total_steps": total_steps,
115
+ "error": error,
116
+ })
117
+
118
+ def get_run(self, run_id: str) -> dict | None:
119
+ """Fetch run details."""
120
+ return self._request("GET", f"/runs/{run_id}")
121
+
122
+ def list_runs(self, limit: int = 50, offset: int = 0) -> dict | None:
123
+ """List recent runs."""
124
+ return self._request("GET", f"/runs?limit={limit}&offset={offset}")
125
+
126
+ @property
127
+ def is_connected(self) -> bool:
128
+ """Check if API is reachable."""
129
+ return self._api_available
@@ -0,0 +1,49 @@
1
+ """
2
+ SteerPlane SDK — Configuration
3
+
4
+ Default settings and environment variable overrides.
5
+ """
6
+
7
+ import os
8
+
9
+
10
+ class SteerPlaneConfig:
11
+ """SDK configuration with environment variable overrides."""
12
+
13
+ def __init__(
14
+ self,
15
+ api_url: str | None = None,
16
+ api_key: str | None = None,
17
+ agent_id: str | None = None,
18
+ default_max_cost_usd: float = 50.0,
19
+ default_max_steps: int = 200,
20
+ default_max_runtime_sec: int = 3600,
21
+ loop_window_size: int = 8,
22
+ enable_telemetry: bool = True,
23
+ log_to_console: bool = True,
24
+ ):
25
+ self.api_url = api_url or os.getenv("STEERPLANE_API_URL", "http://localhost:8000")
26
+ self.api_key = api_key or os.getenv("STEERPLANE_API_KEY", "")
27
+ self.agent_id = agent_id or os.getenv("STEERPLANE_AGENT_ID", "default_agent")
28
+ self.default_max_cost_usd = default_max_cost_usd
29
+ self.default_max_steps = default_max_steps
30
+ self.default_max_runtime_sec = default_max_runtime_sec
31
+ self.loop_window_size = loop_window_size
32
+ self.enable_telemetry = enable_telemetry
33
+ self.log_to_console = log_to_console
34
+
35
+
36
+ # Global default config (can be overridden)
37
+ _config = SteerPlaneConfig()
38
+
39
+
40
+ def get_config() -> SteerPlaneConfig:
41
+ """Get the current global configuration."""
42
+ return _config
43
+
44
+
45
+ def configure(**kwargs) -> SteerPlaneConfig:
46
+ """Update global SDK configuration."""
47
+ global _config
48
+ _config = SteerPlaneConfig(**kwargs)
49
+ return _config
@@ -0,0 +1,171 @@
1
+ """
2
+ SteerPlane SDK — Cost Tracker
3
+
4
+ Tracks cumulative token usage and cost per run.
5
+ Enforces cost limits and projects final cost.
6
+ """
7
+
8
+ from dataclasses import dataclass, field
9
+ from .exceptions import CostLimitExceeded
10
+
11
+
12
+ # Default pricing per token (can be overridden per model)
13
+ DEFAULT_PRICING = {
14
+ "gpt-4o": {"input": 0.0000025, "output": 0.000010},
15
+ "gpt-4o-mini": {"input": 0.00000015, "output": 0.0000006},
16
+ "gpt-4": {"input": 0.00003, "output": 0.00006},
17
+ "gpt-3.5-turbo": {"input": 0.0000005, "output": 0.0000015},
18
+ "claude-3-opus": {"input": 0.000015, "output": 0.000075},
19
+ "claude-3-sonnet": {"input": 0.000003, "output": 0.000015},
20
+ "claude-3-haiku": {"input": 0.00000025, "output": 0.00000125},
21
+ "gemini-pro": {"input": 0.00000025, "output": 0.0000005},
22
+ "default": {"input": 0.000002, "output": 0.000002},
23
+ }
24
+
25
+
26
+ @dataclass
27
+ class StepCost:
28
+ """Cost breakdown for a single step."""
29
+ input_tokens: int = 0
30
+ output_tokens: int = 0
31
+ total_tokens: int = 0
32
+ cost_usd: float = 0.0
33
+ model: str = "default"
34
+
35
+
36
+ @dataclass
37
+ class CostSummary:
38
+ """Cumulative cost summary for an entire run."""
39
+ total_input_tokens: int = 0
40
+ total_output_tokens: int = 0
41
+ total_tokens: int = 0
42
+ total_cost_usd: float = 0.0
43
+ step_costs: list[StepCost] = field(default_factory=list)
44
+ projected_final_cost: float = 0.0
45
+
46
+
47
+ class CostTracker:
48
+ """
49
+ Tracks cumulative cost across an agent run.
50
+
51
+ Enforces cost limits and provides real-time cost projection.
52
+ """
53
+
54
+ def __init__(self, max_cost_usd: float = 50.0, model: str = "default"):
55
+ """
56
+ Args:
57
+ max_cost_usd: Maximum allowed cost before termination.
58
+ model: Default model name for pricing lookup.
59
+ """
60
+ self.max_cost_usd = max_cost_usd
61
+ self.model = model
62
+ self.total_cost: float = 0.0
63
+ self.total_tokens: int = 0
64
+ self.total_input_tokens: int = 0
65
+ self.total_output_tokens: int = 0
66
+ self.step_costs: list[StepCost] = []
67
+
68
+ def calculate_step_cost(
69
+ self,
70
+ input_tokens: int = 0,
71
+ output_tokens: int = 0,
72
+ total_tokens: int = 0,
73
+ model: str | None = None,
74
+ cost_override: float | None = None,
75
+ ) -> StepCost:
76
+ """
77
+ Calculate cost for a single step.
78
+
79
+ Args:
80
+ input_tokens: Number of input/prompt tokens.
81
+ output_tokens: Number of output/completion tokens.
82
+ total_tokens: Total tokens (used if input/output not split).
83
+ model: Model name for pricing (overrides default).
84
+ cost_override: Directly specify cost (skips calculation).
85
+
86
+ Returns:
87
+ StepCost with the calculated cost.
88
+ """
89
+ use_model = model or self.model
90
+ pricing = DEFAULT_PRICING.get(use_model, DEFAULT_PRICING["default"])
91
+
92
+ if cost_override is not None:
93
+ cost = cost_override
94
+ elif input_tokens > 0 or output_tokens > 0:
95
+ cost = (input_tokens * pricing["input"]) + (output_tokens * pricing["output"])
96
+ total_tokens = input_tokens + output_tokens
97
+ elif total_tokens > 0:
98
+ # If no split, assume 50/50 input/output
99
+ avg_price = (pricing["input"] + pricing["output"]) / 2
100
+ cost = total_tokens * avg_price
101
+ else:
102
+ cost = 0.0
103
+
104
+ step_cost = StepCost(
105
+ input_tokens=input_tokens,
106
+ output_tokens=output_tokens,
107
+ total_tokens=total_tokens,
108
+ cost_usd=cost,
109
+ model=use_model,
110
+ )
111
+ return step_cost
112
+
113
+ def add_step(self, step_cost: StepCost) -> float:
114
+ """
115
+ Add a step's cost to the running total and enforce limits.
116
+
117
+ Args:
118
+ step_cost: The StepCost to add.
119
+
120
+ Returns:
121
+ The new cumulative cost.
122
+
123
+ Raises:
124
+ CostLimitExceeded: If cost exceeds the configured limit.
125
+ """
126
+ self.total_cost += step_cost.cost_usd
127
+ self.total_tokens += step_cost.total_tokens
128
+ self.total_input_tokens += step_cost.input_tokens
129
+ self.total_output_tokens += step_cost.output_tokens
130
+ self.step_costs.append(step_cost)
131
+
132
+ if self.total_cost > self.max_cost_usd:
133
+ raise CostLimitExceeded(self.total_cost, self.max_cost_usd)
134
+
135
+ return self.total_cost
136
+
137
+ def project_final_cost(
138
+ self, steps_completed: int, expected_total_steps: int
139
+ ) -> float:
140
+ """
141
+ Project the final cost based on current spending rate.
142
+
143
+ Args:
144
+ steps_completed: Number of steps completed so far.
145
+ expected_total_steps: Expected total number of steps.
146
+
147
+ Returns:
148
+ Projected final cost in USD.
149
+ """
150
+ if steps_completed <= 0:
151
+ return 0.0
152
+ projected = self.total_cost * (expected_total_steps / steps_completed)
153
+ return projected
154
+
155
+ def get_summary(self) -> CostSummary:
156
+ """Get the current cost summary."""
157
+ return CostSummary(
158
+ total_input_tokens=self.total_input_tokens,
159
+ total_output_tokens=self.total_output_tokens,
160
+ total_tokens=self.total_tokens,
161
+ total_cost_usd=self.total_cost,
162
+ step_costs=list(self.step_costs),
163
+ )
164
+
165
+ def reset(self):
166
+ """Reset all tracking."""
167
+ self.total_cost = 0.0
168
+ self.total_tokens = 0
169
+ self.total_input_tokens = 0
170
+ self.total_output_tokens = 0
171
+ self.step_costs.clear()
@@ -0,0 +1,68 @@
1
+ """
2
+ SteerPlane SDK — Custom Exceptions
3
+
4
+ All errors raised by the SteerPlane guard system.
5
+ """
6
+
7
+
8
+ class SteerPlaneError(Exception):
9
+ """Base exception for all SteerPlane errors."""
10
+ pass
11
+
12
+
13
+ class LoopDetectedError(SteerPlaneError):
14
+ """Raised when a repeating action pattern is detected."""
15
+
16
+ def __init__(self, pattern: list, window_size: int):
17
+ self.pattern = pattern
18
+ self.window_size = window_size
19
+ super().__init__(
20
+ f"🔄 Loop detected! Repeating pattern {pattern} "
21
+ f"found in last {window_size} actions. Run terminated."
22
+ )
23
+
24
+
25
+ class CostLimitExceeded(SteerPlaneError):
26
+ """Raised when cumulative run cost exceeds the configured limit."""
27
+
28
+ def __init__(self, current_cost: float, max_cost: float):
29
+ self.current_cost = current_cost
30
+ self.max_cost = max_cost
31
+ super().__init__(
32
+ f"💰 Cost limit exceeded! Current: ${current_cost:.4f}, "
33
+ f"Limit: ${max_cost:.2f}. Run terminated."
34
+ )
35
+
36
+
37
+ class StepLimitExceeded(SteerPlaneError):
38
+ """Raised when the number of steps exceeds the configured limit."""
39
+
40
+ def __init__(self, current_steps: int, max_steps: int):
41
+ self.current_steps = current_steps
42
+ self.max_steps = max_steps
43
+ super().__init__(
44
+ f"🚫 Step limit exceeded! Steps: {current_steps}, "
45
+ f"Limit: {max_steps}. Run terminated."
46
+ )
47
+
48
+
49
+ class RunTerminatedError(SteerPlaneError):
50
+ """Raised when a run is forcefully terminated."""
51
+
52
+ def __init__(self, run_id: str, reason: str = "Manual termination"):
53
+ self.run_id = run_id
54
+ self.reason = reason
55
+ super().__init__(
56
+ f"⛔ Run {run_id} terminated. Reason: {reason}"
57
+ )
58
+
59
+
60
+ class APIConnectionError(SteerPlaneError):
61
+ """Raised when the SDK cannot connect to the SteerPlane API."""
62
+
63
+ def __init__(self, url: str, detail: str = ""):
64
+ self.url = url
65
+ self.detail = detail
66
+ super().__init__(
67
+ f"🔌 Cannot connect to SteerPlane API at {url}. {detail}"
68
+ )