rigorloop 0.1.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.
- rigorloop/__init__.py +18 -0
- rigorloop/_version.py +24 -0
- rigorloop/core/__init__.py +1 -0
- rigorloop/core/config_calcs.py +302 -0
- rigorloop/core/dataset_calcs.py +193 -0
- rigorloop/core/prompt_calcs.py +329 -0
- rigorloop/core/report_calcs.py +194 -0
- rigorloop/core/scoring_calcs.py +264 -0
- rigorloop/core/strategy_calcs.py +507 -0
- rigorloop/core/types.py +649 -0
- rigorloop/py.typed +0 -0
- rigorloop/shell/__init__.py +1 -0
- rigorloop/shell/agent_calls.py +120 -0
- rigorloop/shell/cli.py +1111 -0
- rigorloop/shell/io_actions.py +497 -0
- rigorloop-0.1.0.dist-info/METADATA +224 -0
- rigorloop-0.1.0.dist-info/RECORD +20 -0
- rigorloop-0.1.0.dist-info/WHEEL +4 -0
- rigorloop-0.1.0.dist-info/entry_points.txt +2 -0
- rigorloop-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Shell wrapper around the claude CLI: subprocess calls, JSON envelope
|
|
2
|
+
parsing, transport retries, and concurrency. All CLI flags live in one
|
|
3
|
+
function so flag drift is absorbed in one place."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import subprocess
|
|
9
|
+
import threading
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
12
|
+
|
|
13
|
+
from rigorloop.core.types import (
|
|
14
|
+
AgentCallError,
|
|
15
|
+
AgentTextRequest,
|
|
16
|
+
CallFailed,
|
|
17
|
+
CallTimeout,
|
|
18
|
+
EnvelopeError,
|
|
19
|
+
Err,
|
|
20
|
+
Nothing,
|
|
21
|
+
Ok,
|
|
22
|
+
Result,
|
|
23
|
+
Some,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
AgentRunner = Callable[[AgentTextRequest], Result[str, AgentCallError]]
|
|
27
|
+
|
|
28
|
+
_STDERR_PREVIEW = 500
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def build_claude_args(request: AgentTextRequest, claude_cmd: str) -> list[str]:
|
|
32
|
+
"""The single place that knows the claude CLI's flags: headless, no tools,
|
|
33
|
+
JSON envelope output. The prompt itself travels via stdin."""
|
|
34
|
+
args = [
|
|
35
|
+
claude_cmd,
|
|
36
|
+
"-p",
|
|
37
|
+
"--tools",
|
|
38
|
+
"",
|
|
39
|
+
"--output-format",
|
|
40
|
+
"json",
|
|
41
|
+
"--model",
|
|
42
|
+
request.model,
|
|
43
|
+
]
|
|
44
|
+
match request.system_prompt:
|
|
45
|
+
case Some(system):
|
|
46
|
+
return [*args, "--append-system-prompt", system]
|
|
47
|
+
case Nothing():
|
|
48
|
+
return args
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def parse_envelope(stdout: str) -> Result[str, AgentCallError]:
|
|
52
|
+
try:
|
|
53
|
+
obj = json.loads(stdout)
|
|
54
|
+
except json.JSONDecodeError as exc:
|
|
55
|
+
return Err(EnvelopeError(f"non-JSON envelope: {exc}"))
|
|
56
|
+
if not isinstance(obj, dict):
|
|
57
|
+
return Err(EnvelopeError("envelope is not a JSON object"))
|
|
58
|
+
result = obj.get("result")
|
|
59
|
+
if obj.get("is_error"):
|
|
60
|
+
detail = result if isinstance(result, str) else "agent envelope flagged is_error"
|
|
61
|
+
return Err(CallFailed(detail))
|
|
62
|
+
if not isinstance(result, str):
|
|
63
|
+
return Err(EnvelopeError("envelope has no string 'result' field"))
|
|
64
|
+
return Ok(result)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def run_agent_once(request: AgentTextRequest, claude_cmd: str) -> Result[str, AgentCallError]:
|
|
68
|
+
try:
|
|
69
|
+
proc = subprocess.run(
|
|
70
|
+
build_claude_args(request, claude_cmd),
|
|
71
|
+
input=request.user_prompt,
|
|
72
|
+
capture_output=True,
|
|
73
|
+
text=True,
|
|
74
|
+
timeout=request.timeout_s,
|
|
75
|
+
)
|
|
76
|
+
except subprocess.TimeoutExpired:
|
|
77
|
+
return Err(CallTimeout(request.timeout_s))
|
|
78
|
+
except OSError as exc:
|
|
79
|
+
return Err(CallFailed(f"could not launch {claude_cmd!r}: {exc}"))
|
|
80
|
+
if proc.returncode != 0:
|
|
81
|
+
return Err(CallFailed(f"exit {proc.returncode}: {proc.stderr.strip()[:_STDERR_PREVIEW]}"))
|
|
82
|
+
return parse_envelope(proc.stdout)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def make_runner(claude_cmd: str) -> tuple[AgentRunner, Callable[[], int]]:
|
|
86
|
+
"""A counting runner with one transport retry. The counter is shared
|
|
87
|
+
mutable shell state, guarded by a lock (effects live at the shell)."""
|
|
88
|
+
lock = threading.Lock()
|
|
89
|
+
calls = [0]
|
|
90
|
+
|
|
91
|
+
def bump() -> None:
|
|
92
|
+
with lock:
|
|
93
|
+
calls[0] += 1
|
|
94
|
+
|
|
95
|
+
def run(request: AgentTextRequest) -> Result[str, AgentCallError]:
|
|
96
|
+
bump()
|
|
97
|
+
first = run_agent_once(request, claude_cmd)
|
|
98
|
+
match first:
|
|
99
|
+
case Ok(_):
|
|
100
|
+
return first
|
|
101
|
+
case Err(CallTimeout()):
|
|
102
|
+
return first # a timeout retried would double the stall
|
|
103
|
+
case Err(_):
|
|
104
|
+
bump()
|
|
105
|
+
return run_agent_once(request, claude_cmd)
|
|
106
|
+
|
|
107
|
+
def calls_made() -> int:
|
|
108
|
+
with lock:
|
|
109
|
+
return calls[0]
|
|
110
|
+
|
|
111
|
+
return run, calls_made
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def run_concurrently(
|
|
115
|
+
requests: tuple[AgentTextRequest, ...], runner: AgentRunner, max_workers: int
|
|
116
|
+
) -> tuple[Result[str, AgentCallError], ...]:
|
|
117
|
+
if not requests:
|
|
118
|
+
return ()
|
|
119
|
+
with ThreadPoolExecutor(max_workers=max(1, max_workers)) as pool:
|
|
120
|
+
return tuple(pool.map(runner, requests))
|