opensaddle 1.0.0__py3-none-any.whl

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.
Files changed (58) hide show
  1. opensaddle/__init__.py +5 -0
  2. opensaddle/__main__.py +7 -0
  3. opensaddle/adapters/__init__.py +1 -0
  4. opensaddle/adapters/base.py +85 -0
  5. opensaddle/adapters/cli.py +192 -0
  6. opensaddle/agent_presets.py +194 -0
  7. opensaddle/audit/__init__.py +1 -0
  8. opensaddle/audit/auditor.py +496 -0
  9. opensaddle/audit/stream.py +43 -0
  10. opensaddle/audit/types.py +68 -0
  11. opensaddle/benchmark/__init__.py +27 -0
  12. opensaddle/benchmark/metrics.py +92 -0
  13. opensaddle/benchmark/reports.py +56 -0
  14. opensaddle/benchmark/runner.py +683 -0
  15. opensaddle/benchmark/suite.py +217 -0
  16. opensaddle/cli/__init__.py +2 -0
  17. opensaddle/cli/main.py +1119 -0
  18. opensaddle/config.py +653 -0
  19. opensaddle/core/__init__.py +44 -0
  20. opensaddle/core/agent.py +16 -0
  21. opensaddle/core/episode.py +116 -0
  22. opensaddle/core/failure_attribution.py +211 -0
  23. opensaddle/core/model.py +28 -0
  24. opensaddle/core/route.py +126 -0
  25. opensaddle/core/stored_episode.py +197 -0
  26. opensaddle/core/task.py +49 -0
  27. opensaddle/dashboard.py +1797 -0
  28. opensaddle/dashboard_reviews.py +682 -0
  29. opensaddle/execution/__init__.py +1 -0
  30. opensaddle/execution/broker.py +242 -0
  31. opensaddle/execution/configured_routes.py +319 -0
  32. opensaddle/execution/policy_observer.py +71 -0
  33. opensaddle/execution/pty.py +140 -0
  34. opensaddle/execution/runner.py +333 -0
  35. opensaddle/execution/stored_episodes.py +166 -0
  36. opensaddle/execution/worktree.py +80 -0
  37. opensaddle/governance.py +67 -0
  38. opensaddle/history.py +737 -0
  39. opensaddle/model_registry.py +168 -0
  40. opensaddle/openrouter.py +425 -0
  41. opensaddle/optimize/__init__.py +6 -0
  42. opensaddle/optimize/estimator.py +128 -0
  43. opensaddle/optimize/route_materialization.py +328 -0
  44. opensaddle/optimize/route_scoring.py +286 -0
  45. opensaddle/optimize/task_profile.py +65 -0
  46. opensaddle/privacy.py +125 -0
  47. opensaddle/storage/__init__.py +3 -0
  48. opensaddle/storage/migrations.py +137 -0
  49. opensaddle/storage/sqlite.py +923 -0
  50. opensaddle/templates.py +170 -0
  51. opensaddle/verification/__init__.py +1 -0
  52. opensaddle/verification/checks.py +115 -0
  53. opensaddle/verification/diff_risk.py +50 -0
  54. opensaddle-1.0.0.dist-info/METADATA +318 -0
  55. opensaddle-1.0.0.dist-info/RECORD +58 -0
  56. opensaddle-1.0.0.dist-info/WHEEL +4 -0
  57. opensaddle-1.0.0.dist-info/entry_points.txt +2 -0
  58. opensaddle-1.0.0.dist-info/licenses/LICENSE +21 -0
opensaddle/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """opensaddle package."""
2
+
3
+ __all__ = ["__version__"]
4
+
5
+ __version__ = "1.0.0"
opensaddle/__main__.py ADDED
@@ -0,0 +1,7 @@
1
+ """Run the opensaddle CLI with ``python -m opensaddle``."""
2
+
3
+ from opensaddle.cli.main import app
4
+
5
+
6
+ if __name__ == "__main__":
7
+ app()
@@ -0,0 +1 @@
1
+ """Agent adapter implementations."""
@@ -0,0 +1,85 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from enum import Enum
5
+ from pathlib import Path
6
+ from typing import Any, Protocol
7
+
8
+
9
+ class CliExecutionMode(str, Enum):
10
+ EXEC = "exec"
11
+ PTY = "pty"
12
+
13
+
14
+ class TemplateRenderError(ValueError):
15
+ """Raised when a command template cannot be rendered."""
16
+
17
+
18
+ class _StrictTemplateValues(dict[str, str]):
19
+ def __missing__(self, key: str) -> str:
20
+ raise TemplateRenderError(f"missing template value: {key}")
21
+
22
+
23
+ @dataclass(slots=True)
24
+ class UsageReport:
25
+ confidence: str
26
+ prompt_tokens: int | None = None
27
+ completion_tokens: int | None = None
28
+ total_tokens: int | None = None
29
+ raw: dict[str, Any] = field(default_factory=dict)
30
+
31
+
32
+ @dataclass(slots=True)
33
+ class CommandCapture:
34
+ argv: list[str]
35
+ cwd: str
36
+ exit_code: int | None
37
+ stdout: str
38
+ stderr: str
39
+ timed_out: bool
40
+ duration_seconds: float
41
+ execution_mode: str = CliExecutionMode.EXEC.value
42
+ env: dict[str, str] = field(default_factory=dict)
43
+
44
+
45
+ @dataclass(slots=True)
46
+ class CliExecutionResult:
47
+ capture: CommandCapture
48
+ usage: UsageReport | None = None
49
+
50
+
51
+ class UsageParser(Protocol):
52
+ def parse(self, *, capture: CommandCapture) -> UsageReport | None:
53
+ """Extract usage information from a finished command."""
54
+
55
+
56
+ @dataclass(slots=True)
57
+ class NullUsageParser:
58
+ def parse(self, *, capture: CommandCapture) -> UsageReport | None:
59
+ return None
60
+
61
+
62
+ @dataclass(slots=True)
63
+ class CliCommandTemplate:
64
+ argv: list[str]
65
+ env: dict[str, str] = field(default_factory=dict)
66
+
67
+ def render(self, context: dict[str, Any]) -> tuple[list[str], dict[str, str]]:
68
+ rendered_context = _StrictTemplateValues(
69
+ {key: "" if value is None else str(value) for key, value in context.items()}
70
+ )
71
+ argv = [part.format_map(rendered_context) for part in self.argv]
72
+ env = {
73
+ key: value.format_map(rendered_context)
74
+ for key, value in self.env.items()
75
+ }
76
+ return argv, env
77
+
78
+
79
+ @dataclass(slots=True)
80
+ class AgentInvocation:
81
+ instruction: str
82
+ workdir: Path
83
+ model: str | None = None
84
+ timeout_seconds: float | None = None
85
+ extra_context: dict[str, Any] = field(default_factory=dict)
@@ -0,0 +1,192 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import re
5
+ import subprocess
6
+ import time
7
+ from dataclasses import dataclass, field
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from opensaddle.adapters.base import (
12
+ AgentInvocation,
13
+ CliExecutionMode,
14
+ CliCommandTemplate,
15
+ CliExecutionResult,
16
+ CommandCapture,
17
+ NullUsageParser,
18
+ UsageParser,
19
+ UsageReport,
20
+ )
21
+ from opensaddle.audit.stream import AuditStream
22
+ from opensaddle.execution.pty import PtyCommandRunner
23
+
24
+
25
+ def _normalize_output(value: str | bytes | None) -> str:
26
+ if value is None:
27
+ return ""
28
+ if isinstance(value, bytes):
29
+ return value.decode("utf-8", errors="replace")
30
+ return value
31
+
32
+
33
+ @dataclass(slots=True)
34
+ class RegexUsageParser:
35
+ pattern: str
36
+ confidence: str = "parsed"
37
+ flags: int = re.MULTILINE
38
+
39
+ def parse(self, *, capture: CommandCapture) -> UsageReport | None:
40
+ match = re.search(self.pattern, capture.stdout, self.flags)
41
+ if not match:
42
+ return None
43
+ raw = match.groupdict()
44
+ return UsageReport(
45
+ confidence=self.confidence,
46
+ prompt_tokens=int(raw["prompt"]) if raw.get("prompt") else None,
47
+ completion_tokens=int(raw["completion"]) if raw.get("completion") else None,
48
+ total_tokens=int(raw["total"]) if raw.get("total") else None,
49
+ raw=raw,
50
+ )
51
+
52
+
53
+ @dataclass(slots=True)
54
+ class CliAgentAdapter:
55
+ name: str
56
+ command_template: CliCommandTemplate
57
+ usage_parser: UsageParser = field(default_factory=NullUsageParser)
58
+ timeout_seconds: float | None = None
59
+ execution_mode: CliExecutionMode = CliExecutionMode.EXEC
60
+ pty_rows: int = 40
61
+ pty_columns: int = 120
62
+ pty_term: str = "xterm-256color"
63
+
64
+ def run(
65
+ self,
66
+ invocation: AgentInvocation,
67
+ *,
68
+ audit_stream: AuditStream | None = None,
69
+ base_env: dict[str, str] | None = None,
70
+ clean_env: bool = False,
71
+ command_prefix: list[str] | None = None,
72
+ ) -> CliExecutionResult:
73
+ context: dict[str, Any] = {
74
+ "instruction": invocation.instruction,
75
+ "model": invocation.model or "",
76
+ "workdir": str(invocation.workdir),
77
+ }
78
+ context.update(invocation.extra_context)
79
+ argv, rendered_env = self.command_template.render(context)
80
+ if command_prefix:
81
+ argv = [*command_prefix, *argv]
82
+ timeout_seconds = invocation.timeout_seconds or self.timeout_seconds
83
+ env = {} if clean_env else dict(os.environ)
84
+ env.update(base_env or {})
85
+ env.update(rendered_env)
86
+
87
+ if audit_stream is not None:
88
+ audit_stream.record(
89
+ "command_started",
90
+ actor="agent",
91
+ payload={
92
+ "argv": argv,
93
+ "cwd": str(invocation.workdir),
94
+ "execution_mode": self.execution_mode.value,
95
+ },
96
+ )
97
+
98
+ started_at = time.perf_counter()
99
+ capture = self._run_command(
100
+ argv=argv,
101
+ workdir=invocation.workdir,
102
+ env=env,
103
+ rendered_env=rendered_env,
104
+ timeout_seconds=timeout_seconds,
105
+ started_at=started_at,
106
+ )
107
+
108
+ if audit_stream is not None:
109
+ audit_stream.record(
110
+ "command_finished",
111
+ actor="agent",
112
+ payload={
113
+ "argv": argv,
114
+ "cwd": str(invocation.workdir),
115
+ "exit_code": capture.exit_code,
116
+ "timed_out": capture.timed_out,
117
+ "duration_seconds": capture.duration_seconds,
118
+ "execution_mode": capture.execution_mode,
119
+ },
120
+ )
121
+ if capture.stdout:
122
+ audit_stream.record(
123
+ "agent_stdout",
124
+ actor="agent",
125
+ payload={"text": capture.stdout},
126
+ )
127
+ if capture.stderr:
128
+ audit_stream.record(
129
+ "agent_stderr",
130
+ actor="agent",
131
+ payload={"text": capture.stderr},
132
+ )
133
+
134
+ usage = self.usage_parser.parse(capture=capture)
135
+ return CliExecutionResult(capture=capture, usage=usage)
136
+
137
+ def _run_command(
138
+ self,
139
+ *,
140
+ argv: list[str],
141
+ workdir: Path,
142
+ env: dict[str, str],
143
+ rendered_env: dict[str, str],
144
+ timeout_seconds: float | None,
145
+ started_at: float,
146
+ ) -> CommandCapture:
147
+ if self.execution_mode is CliExecutionMode.PTY:
148
+ return PtyCommandRunner(
149
+ rows=self.pty_rows,
150
+ columns=self.pty_columns,
151
+ term=self.pty_term,
152
+ ).run(
153
+ argv=argv,
154
+ workdir=workdir,
155
+ env=env,
156
+ timeout_seconds=timeout_seconds,
157
+ rendered_env=rendered_env,
158
+ )
159
+
160
+ try:
161
+ completed = subprocess.run(
162
+ argv,
163
+ cwd=workdir,
164
+ env=env or None,
165
+ capture_output=True,
166
+ text=True,
167
+ timeout=timeout_seconds,
168
+ check=False,
169
+ )
170
+ return CommandCapture(
171
+ argv=argv,
172
+ cwd=str(workdir),
173
+ exit_code=completed.returncode,
174
+ stdout=completed.stdout,
175
+ stderr=completed.stderr,
176
+ timed_out=False,
177
+ duration_seconds=time.perf_counter() - started_at,
178
+ execution_mode=CliExecutionMode.EXEC.value,
179
+ env=rendered_env,
180
+ )
181
+ except subprocess.TimeoutExpired as exc:
182
+ return CommandCapture(
183
+ argv=argv,
184
+ cwd=str(workdir),
185
+ exit_code=None,
186
+ stdout=_normalize_output(exc.stdout),
187
+ stderr=_normalize_output(exc.stderr),
188
+ timed_out=True,
189
+ duration_seconds=time.perf_counter() - started_at,
190
+ execution_mode=CliExecutionMode.EXEC.value,
191
+ env=rendered_env,
192
+ )
@@ -0,0 +1,194 @@
1
+ """Built-in YAML presets for common coding-agent CLIs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+
8
+ import yaml
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class AgentPreset:
13
+ """A built-in agent config snippet that still uses the generic CLI adapter."""
14
+
15
+ agent_id: str
16
+ command: str
17
+ exec_argv: tuple[str, ...]
18
+ models: tuple[dict[str, str], ...] = ()
19
+ usage_parser_kind: str | None = None
20
+ usage_parser_path_glob: str | None = None
21
+
22
+ def to_config(self) -> dict[str, Any]:
23
+ config: dict[str, Any] = {
24
+ "adapter": "cli",
25
+ "command": self.command,
26
+ "modes": {
27
+ "exec": {
28
+ "argv": list(self.exec_argv),
29
+ },
30
+ "pty": {
31
+ "enabled": False,
32
+ },
33
+ },
34
+ }
35
+ if self.models:
36
+ config["models"] = [dict(binding) for binding in self.models]
37
+ if self.usage_parser_kind is not None:
38
+ usage_parser = {"kind": self.usage_parser_kind}
39
+ if self.usage_parser_path_glob is not None:
40
+ usage_parser["path_glob"] = self.usage_parser_path_glob
41
+ config["usage_parser"] = usage_parser
42
+ return config
43
+
44
+
45
+ BUILTIN_AGENT_PRESETS: dict[str, AgentPreset] = {
46
+ "codex": AgentPreset(
47
+ agent_id="codex",
48
+ command="codex",
49
+ exec_argv=("codex", "exec", "--model", "{model}", "{instruction}"),
50
+ models=(
51
+ {
52
+ "id": "gpt-5",
53
+ "cost_profile": "openai_gpt5",
54
+ "success_prior": "strong_coding",
55
+ },
56
+ {
57
+ "id": "gpt-5-mini",
58
+ "cost_profile": "openai_gpt5_mini",
59
+ "success_prior": "cheap_coding",
60
+ },
61
+ ),
62
+ usage_parser_kind="estimated",
63
+ ),
64
+ "claude_code": AgentPreset(
65
+ agent_id="claude_code",
66
+ command="claude",
67
+ exec_argv=("claude", "-p", "--model", "{model}", "{instruction}"),
68
+ usage_parser_kind="transcript_json",
69
+ usage_parser_path_glob=".claude/**/*.json",
70
+ ),
71
+ "github_copilot": AgentPreset(
72
+ agent_id="github_copilot",
73
+ command="copilot",
74
+ exec_argv=("copilot", "-p", "--model", "{model}", "{instruction}"),
75
+ usage_parser_kind="estimated",
76
+ ),
77
+ "aider": AgentPreset(
78
+ agent_id="aider",
79
+ command="aider",
80
+ exec_argv=(
81
+ "aider",
82
+ "--model",
83
+ "{model}",
84
+ "--yes",
85
+ "--no-auto-commits",
86
+ "--message",
87
+ "{instruction}",
88
+ ),
89
+ usage_parser_kind="estimated",
90
+ ),
91
+ "cursor": AgentPreset(
92
+ agent_id="cursor",
93
+ command="agent",
94
+ exec_argv=(
95
+ "agent",
96
+ "-p",
97
+ "--model",
98
+ "{model}",
99
+ "--output-format",
100
+ "text",
101
+ "{instruction}",
102
+ ),
103
+ usage_parser_kind="estimated",
104
+ ),
105
+ "amp": AgentPreset(
106
+ agent_id="amp",
107
+ command="amp",
108
+ exec_argv=(
109
+ "amp",
110
+ "--execute",
111
+ "{instruction}",
112
+ "--stream-json",
113
+ ),
114
+ usage_parser_kind="stream_json",
115
+ ),
116
+ "antigravity": AgentPreset(
117
+ agent_id="antigravity",
118
+ command="agy",
119
+ exec_argv=(
120
+ "agy",
121
+ "--model",
122
+ "{model}",
123
+ "--prompt",
124
+ "{instruction}",
125
+ ),
126
+ usage_parser_kind="estimated",
127
+ ),
128
+ "claw": AgentPreset(
129
+ agent_id="claw",
130
+ command="openclaw",
131
+ exec_argv=(
132
+ "openclaw",
133
+ "infer",
134
+ "model",
135
+ "run",
136
+ "--local",
137
+ "--model",
138
+ "{model}",
139
+ "--prompt",
140
+ "{instruction}",
141
+ "--json",
142
+ ),
143
+ usage_parser_kind="json",
144
+ ),
145
+ "hermes": AgentPreset(
146
+ agent_id="hermes",
147
+ command="hermes",
148
+ exec_argv=(
149
+ "hermes",
150
+ "chat",
151
+ "--model",
152
+ "{model}",
153
+ "-q",
154
+ "{instruction}",
155
+ ),
156
+ usage_parser_kind="estimated",
157
+ ),
158
+ "pi": AgentPreset(
159
+ agent_id="pi",
160
+ command="pi",
161
+ exec_argv=(
162
+ "pi",
163
+ "--approve",
164
+ "--model",
165
+ "{model}",
166
+ "-p",
167
+ "{instruction}",
168
+ ),
169
+ usage_parser_kind="estimated",
170
+ ),
171
+ }
172
+
173
+
174
+ def builtin_agent_preset_names() -> tuple[str, ...]:
175
+ """Return built-in preset ids in stable display order."""
176
+ return tuple(BUILTIN_AGENT_PRESETS)
177
+
178
+
179
+ def builtin_agent_configs() -> dict[str, dict[str, Any]]:
180
+ """Return the `agents:` mapping used by generated configs."""
181
+ return {
182
+ preset.agent_id: preset.to_config()
183
+ for preset in BUILTIN_AGENT_PRESETS.values()
184
+ }
185
+
186
+
187
+ def render_agent_preset_fragment(preset_name: str) -> str:
188
+ """Render one built-in preset as a YAML fragment for docs and examples."""
189
+ try:
190
+ preset = BUILTIN_AGENT_PRESETS[preset_name]
191
+ except KeyError as exc:
192
+ available = ", ".join(builtin_agent_preset_names())
193
+ raise ValueError(f"unknown built-in preset '{preset_name}' (expected one of: {available})") from exc
194
+ return yaml.safe_dump({preset.agent_id: preset.to_config()}, sort_keys=False).strip() + "\n"
@@ -0,0 +1 @@
1
+ """Audit stream helpers."""