runledger 0.2.2__tar.gz → 0.2.3__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.
- {runledger-0.2.2/src/runledger.egg-info → runledger-0.2.3}/PKG-INFO +11 -20
- {runledger-0.2.2 → runledger-0.2.3}/README.md +10 -19
- {runledger-0.2.2 → runledger-0.2.3}/pyproject.toml +1 -1
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/__init__.py +1 -1
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/cli.py +1 -1
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/config/loader.py +54 -14
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/reporting/failures.py +1 -1
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/runner/subprocess.py +25 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/util/redaction.py +4 -4
- {runledger-0.2.2 → runledger-0.2.3/src/runledger.egg-info}/PKG-INFO +11 -20
- {runledger-0.2.2 → runledger-0.2.3}/LICENSE +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/setup.cfg +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/__main__.py +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/artifacts/__init__.py +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/artifacts/junit.py +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/artifacts/report.py +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/artifacts/run_log.py +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/artifacts/summary.py +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/assertions/__init__.py +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/assertions/base.py +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/assertions/engine.py +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/assertions/json_schema.py +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/assertions/required_fields.py +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/assertions/tool_contract.py +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/baseline/__init__.py +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/baseline/io.py +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/baseline/models.py +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/cassette/__init__.py +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/cassette/loader.py +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/cassette/match.py +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/cassette/models.py +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/cassette/writer.py +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/config/__init__.py +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/config/models.py +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/protocol/__init__.py +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/protocol/jsonl.py +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/protocol/messages.py +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/regression/__init__.py +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/regression/engine.py +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/reporting/__init__.py +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/runner/__init__.py +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/runner/budgets.py +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/runner/engine.py +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/runner/models.py +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/tools/__init__.py +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/tools/builtin.py +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/tools/registry.py +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/util/__init__.py +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/util/canonical_json.py +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger/util/normalize.py +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger.egg-info/SOURCES.txt +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger.egg-info/dependency_links.txt +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger.egg-info/entry_points.txt +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger.egg-info/requires.txt +0 -0
- {runledger-0.2.2 → runledger-0.2.3}/src/runledger.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: runledger
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: CI for tool-using agents: deterministic eval suites with record/replay, hard assertions, budgets, and PR regression gates.
|
|
5
5
|
Author: RunLedger
|
|
6
6
|
License-Expression: MIT
|
|
@@ -69,13 +69,13 @@ The same check as a GitHub Action. `suite.yaml` already points at `baselines/dem
|
|
|
69
69
|
|
|
70
70
|
```yaml
|
|
71
71
|
- id: evals
|
|
72
|
-
uses: runledger/Runledger@v0.2
|
|
72
|
+
uses: runledger/Runledger@v0.2.3
|
|
73
73
|
with:
|
|
74
74
|
path: ./evals/demo
|
|
75
75
|
mode: replay
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
`runledger/Runledger@v0.2.3` accepts a `baseline` input and sets `result` (`pass` or `fail`), `exit-code`, and `artifacts-path`. The in-repo self-test calls the action with `uses: ./`.
|
|
79
79
|
|
|
80
80
|
Page copy for runledger.io lives in `docs/site-quickstart.md`.
|
|
81
81
|
|
|
@@ -229,7 +229,7 @@ Optional:
|
|
|
229
229
|
|
|
230
230
|
```yaml
|
|
231
231
|
suite_name: support-triage
|
|
232
|
-
agent_command: ["
|
|
232
|
+
agent_command: ["python3", "agent.py"]
|
|
233
233
|
mode: replay # replay | record | live
|
|
234
234
|
cases_path: cases
|
|
235
235
|
tool_registry:
|
|
@@ -306,13 +306,10 @@ Each line is one tool invocation:
|
|
|
306
306
|
MVP assertions:
|
|
307
307
|
|
|
308
308
|
* `json_schema` (validate final output with JSON Schema)
|
|
309
|
-
* `required_fields` (keys exist
|
|
310
|
-
* `
|
|
311
|
-
* `
|
|
312
|
-
|
|
313
|
-
* must call tool X
|
|
314
|
-
* must not call tool Y
|
|
315
|
-
* X before Y (ordering)
|
|
309
|
+
* `required_fields` (keys exist)
|
|
310
|
+
* `must_call` (must call tool X)
|
|
311
|
+
* `must_not_call` (must not call tool Y)
|
|
312
|
+
* `call_order` (X before Y; tools do not have to be adjacent)
|
|
316
313
|
|
|
317
314
|
Not default-gating (optional later):
|
|
318
315
|
|
|
@@ -323,18 +320,12 @@ Not default-gating (optional later):
|
|
|
323
320
|
|
|
324
321
|
## Budgets (merge gates)
|
|
325
322
|
|
|
326
|
-
|
|
323
|
+
Budgets that fail a run:
|
|
327
324
|
|
|
328
325
|
* `max_wall_ms`
|
|
329
326
|
* `max_tool_calls`
|
|
330
327
|
* `max_tool_errors`
|
|
331
328
|
|
|
332
|
-
Not yet enforced (accepted in config, reported in artifacts when the agent sends metrics, but
|
|
333
|
-
they do not fail a run today):
|
|
334
|
-
|
|
335
|
-
* `max_tokens_out`
|
|
336
|
-
* `max_cost_usd`
|
|
337
|
-
|
|
338
329
|
---
|
|
339
330
|
|
|
340
331
|
## Baselines + regression checks
|
|
@@ -389,7 +380,7 @@ jobs:
|
|
|
389
380
|
|
|
390
381
|
- name: Run deterministic evals (replay)
|
|
391
382
|
id: evals
|
|
392
|
-
uses: runledger/Runledger@v0.2
|
|
383
|
+
uses: runledger/Runledger@v0.2.3
|
|
393
384
|
with:
|
|
394
385
|
path: ./evals/demo
|
|
395
386
|
mode: replay
|
|
@@ -398,7 +389,7 @@ jobs:
|
|
|
398
389
|
run: echo "result=${{ steps.evals.outputs.result }} exit=${{ steps.evals.outputs.exit-code }}"
|
|
399
390
|
```
|
|
400
391
|
|
|
401
|
-
`runledger init` writes `.github/workflows/runledger.yml`, which installs the CLI and runs the suite with `--baseline`. The action above is the Marketplace entry in `action.yml`. It uploads `runledger_out/**`
|
|
392
|
+
`runledger init` writes `.github/workflows/runledger.yml`, which installs the CLI and runs the suite with `--baseline`. The action above is the Marketplace entry in `action.yml` at `runledger/Runledger@v0.2.3`. It uploads `runledger_out/**` on success and on failure. The step sets `result`, `exit-code`, and `artifacts-path`, and you can pass `baseline:` to override `suite.yaml`. A failing run fails the job.
|
|
402
393
|
|
|
403
394
|
---
|
|
404
395
|
|
|
@@ -41,13 +41,13 @@ The same check as a GitHub Action. `suite.yaml` already points at `baselines/dem
|
|
|
41
41
|
|
|
42
42
|
```yaml
|
|
43
43
|
- id: evals
|
|
44
|
-
uses: runledger/Runledger@v0.2
|
|
44
|
+
uses: runledger/Runledger@v0.2.3
|
|
45
45
|
with:
|
|
46
46
|
path: ./evals/demo
|
|
47
47
|
mode: replay
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
`runledger/Runledger@v0.2.3` accepts a `baseline` input and sets `result` (`pass` or `fail`), `exit-code`, and `artifacts-path`. The in-repo self-test calls the action with `uses: ./`.
|
|
51
51
|
|
|
52
52
|
Page copy for runledger.io lives in `docs/site-quickstart.md`.
|
|
53
53
|
|
|
@@ -201,7 +201,7 @@ Optional:
|
|
|
201
201
|
|
|
202
202
|
```yaml
|
|
203
203
|
suite_name: support-triage
|
|
204
|
-
agent_command: ["
|
|
204
|
+
agent_command: ["python3", "agent.py"]
|
|
205
205
|
mode: replay # replay | record | live
|
|
206
206
|
cases_path: cases
|
|
207
207
|
tool_registry:
|
|
@@ -278,13 +278,10 @@ Each line is one tool invocation:
|
|
|
278
278
|
MVP assertions:
|
|
279
279
|
|
|
280
280
|
* `json_schema` (validate final output with JSON Schema)
|
|
281
|
-
* `required_fields` (keys exist
|
|
282
|
-
* `
|
|
283
|
-
* `
|
|
284
|
-
|
|
285
|
-
* must call tool X
|
|
286
|
-
* must not call tool Y
|
|
287
|
-
* X before Y (ordering)
|
|
281
|
+
* `required_fields` (keys exist)
|
|
282
|
+
* `must_call` (must call tool X)
|
|
283
|
+
* `must_not_call` (must not call tool Y)
|
|
284
|
+
* `call_order` (X before Y; tools do not have to be adjacent)
|
|
288
285
|
|
|
289
286
|
Not default-gating (optional later):
|
|
290
287
|
|
|
@@ -295,18 +292,12 @@ Not default-gating (optional later):
|
|
|
295
292
|
|
|
296
293
|
## Budgets (merge gates)
|
|
297
294
|
|
|
298
|
-
|
|
295
|
+
Budgets that fail a run:
|
|
299
296
|
|
|
300
297
|
* `max_wall_ms`
|
|
301
298
|
* `max_tool_calls`
|
|
302
299
|
* `max_tool_errors`
|
|
303
300
|
|
|
304
|
-
Not yet enforced (accepted in config, reported in artifacts when the agent sends metrics, but
|
|
305
|
-
they do not fail a run today):
|
|
306
|
-
|
|
307
|
-
* `max_tokens_out`
|
|
308
|
-
* `max_cost_usd`
|
|
309
|
-
|
|
310
301
|
---
|
|
311
302
|
|
|
312
303
|
## Baselines + regression checks
|
|
@@ -361,7 +352,7 @@ jobs:
|
|
|
361
352
|
|
|
362
353
|
- name: Run deterministic evals (replay)
|
|
363
354
|
id: evals
|
|
364
|
-
uses: runledger/Runledger@v0.2
|
|
355
|
+
uses: runledger/Runledger@v0.2.3
|
|
365
356
|
with:
|
|
366
357
|
path: ./evals/demo
|
|
367
358
|
mode: replay
|
|
@@ -370,7 +361,7 @@ jobs:
|
|
|
370
361
|
run: echo "result=${{ steps.evals.outputs.result }} exit=${{ steps.evals.outputs.exit-code }}"
|
|
371
362
|
```
|
|
372
363
|
|
|
373
|
-
`runledger init` writes `.github/workflows/runledger.yml`, which installs the CLI and runs the suite with `--baseline`. The action above is the Marketplace entry in `action.yml`. It uploads `runledger_out/**`
|
|
364
|
+
`runledger init` writes `.github/workflows/runledger.yml`, which installs the CLI and runs the suite with `--baseline`. The action above is the Marketplace entry in `action.yml` at `runledger/Runledger@v0.2.3`. It uploads `runledger_out/**` on success and on failure. The step sets `result`, `exit-code`, and `artifacts-path`, and you can pass `baseline:` to override `suite.yaml`. A failing run fails the job.
|
|
374
365
|
|
|
375
366
|
---
|
|
376
367
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "runledger"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.3"
|
|
8
8
|
description = "CI for tool-using agents: deterministic eval suites with record/replay, hard assertions, budgets, and PR regression gates."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -498,7 +498,7 @@ def init(
|
|
|
498
498
|
" runledger run "
|
|
499
499
|
f'"{_posix_rel(evals_dir, cwd)}" --mode replay --baseline "{_posix_rel(baseline_file, cwd)}"'
|
|
500
500
|
)
|
|
501
|
-
console.print(f"
|
|
501
|
+
console.print(f" report: runledger_out/{suite}/<run_id>/report.html")
|
|
502
502
|
console.print(f" commit {_display_path(workflow_path, base_dir=cwd)}")
|
|
503
503
|
|
|
504
504
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import re
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
from typing import Any
|
|
5
6
|
|
|
@@ -7,12 +8,33 @@ import yaml
|
|
|
7
8
|
|
|
8
9
|
from .models import CaseConfig, SuiteConfig
|
|
9
10
|
|
|
11
|
+
_WINDOWS_DRIVE = re.compile(r"^[A-Za-z]:[\\/]")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def normalize_config_path(value: str) -> str:
|
|
15
|
+
"""Turn Windows separators into POSIX separators.
|
|
16
|
+
|
|
17
|
+
Suites authored on Windows often store ``agent\\agent.py`` and
|
|
18
|
+
``cassettes\\t1.jsonl``. Those strings do not resolve on the Linux runners
|
|
19
|
+
used by GitHub Actions. Forward slashes work on both.
|
|
20
|
+
"""
|
|
21
|
+
if "\\" not in value:
|
|
22
|
+
return value
|
|
23
|
+
return value.replace("\\", "/")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _is_absolute_config_path(value: str) -> bool:
|
|
27
|
+
if Path(value).is_absolute():
|
|
28
|
+
return True
|
|
29
|
+
return bool(_WINDOWS_DRIVE.match(value)) or value.startswith("//")
|
|
30
|
+
|
|
10
31
|
|
|
11
32
|
def _load_yaml(path: Path) -> dict[str, Any]:
|
|
12
33
|
try:
|
|
13
34
|
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
14
35
|
except yaml.YAMLError as exc:
|
|
15
|
-
|
|
36
|
+
detail = str(exc).strip() or "could not parse the file"
|
|
37
|
+
raise ValueError(f"Invalid YAML in {path}: {detail}") from exc
|
|
16
38
|
except OSError as exc:
|
|
17
39
|
raise FileNotFoundError(f"Unable to read {path}") from exc
|
|
18
40
|
|
|
@@ -28,8 +50,11 @@ def _resolve_schema_paths(assertions: list[object], base_dir: Path) -> None:
|
|
|
28
50
|
if entry.get("type") != "json_schema":
|
|
29
51
|
continue
|
|
30
52
|
schema_path = entry.get("schema_path")
|
|
31
|
-
if isinstance(schema_path, str)
|
|
32
|
-
|
|
53
|
+
if isinstance(schema_path, str):
|
|
54
|
+
schema_path = normalize_config_path(schema_path)
|
|
55
|
+
if not _is_absolute_config_path(schema_path):
|
|
56
|
+
schema_path = str((base_dir / schema_path).resolve())
|
|
57
|
+
entry["schema_path"] = schema_path
|
|
33
58
|
|
|
34
59
|
|
|
35
60
|
def load_suite(path: Path) -> SuiteConfig:
|
|
@@ -39,31 +64,43 @@ def load_suite(path: Path) -> SuiteConfig:
|
|
|
39
64
|
if not suite_path.is_file():
|
|
40
65
|
raise FileNotFoundError(f"Suite file not found: {suite_path}")
|
|
41
66
|
data = _load_yaml(suite_path)
|
|
67
|
+
cases_path = data.get("cases_path")
|
|
68
|
+
if isinstance(cases_path, str):
|
|
69
|
+
data["cases_path"] = normalize_config_path(cases_path)
|
|
42
70
|
agent_command = data.get("agent_command")
|
|
43
71
|
if isinstance(agent_command, list):
|
|
44
72
|
resolved_command: list[object] = []
|
|
45
73
|
for part in agent_command:
|
|
46
|
-
if isinstance(part, str)
|
|
47
|
-
|
|
48
|
-
if
|
|
49
|
-
|
|
50
|
-
|
|
74
|
+
if isinstance(part, str):
|
|
75
|
+
part = normalize_config_path(part)
|
|
76
|
+
if not _is_absolute_config_path(part):
|
|
77
|
+
candidate = (suite_path.parent / part).resolve()
|
|
78
|
+
if candidate.exists():
|
|
79
|
+
resolved_command.append(str(candidate))
|
|
80
|
+
continue
|
|
51
81
|
resolved_command.append(part)
|
|
52
82
|
data["agent_command"] = resolved_command
|
|
53
83
|
assertions = data.get("assertions", [])
|
|
54
84
|
if isinstance(assertions, list):
|
|
55
85
|
_resolve_schema_paths(assertions, suite_path.parent)
|
|
56
86
|
baseline_path = data.get("baseline_path")
|
|
57
|
-
if isinstance(baseline_path, str)
|
|
58
|
-
|
|
87
|
+
if isinstance(baseline_path, str):
|
|
88
|
+
baseline_path = normalize_config_path(baseline_path)
|
|
89
|
+
if not _is_absolute_config_path(baseline_path):
|
|
90
|
+
baseline_path = str((suite_path.parent / baseline_path).resolve())
|
|
91
|
+
data["baseline_path"] = baseline_path
|
|
59
92
|
output_dir = data.get("output_dir")
|
|
60
|
-
if isinstance(output_dir, str)
|
|
61
|
-
|
|
93
|
+
if isinstance(output_dir, str):
|
|
94
|
+
output_dir = normalize_config_path(output_dir)
|
|
95
|
+
if not _is_absolute_config_path(output_dir):
|
|
96
|
+
output_dir = str((suite_path.parent / output_dir).resolve())
|
|
97
|
+
data["output_dir"] = output_dir
|
|
62
98
|
return SuiteConfig.model_validate(data)
|
|
63
99
|
|
|
64
100
|
|
|
65
101
|
def load_cases(suite_dir: Path, cases_path: str) -> list[CaseConfig]:
|
|
66
102
|
"""Load cases with cassette paths resolved relative to the suite directory."""
|
|
103
|
+
cases_path = normalize_config_path(cases_path)
|
|
67
104
|
cases_dir = (suite_dir / cases_path).resolve()
|
|
68
105
|
if not cases_dir.is_dir():
|
|
69
106
|
raise FileNotFoundError(f"Cases directory not found: {cases_dir}")
|
|
@@ -76,8 +113,11 @@ def load_cases(suite_dir: Path, cases_path: str) -> list[CaseConfig]:
|
|
|
76
113
|
for case_file in case_files:
|
|
77
114
|
data = _load_yaml(case_file)
|
|
78
115
|
cassette_value = data.get("cassette")
|
|
79
|
-
if isinstance(cassette_value, str)
|
|
80
|
-
|
|
116
|
+
if isinstance(cassette_value, str):
|
|
117
|
+
cassette_value = normalize_config_path(cassette_value)
|
|
118
|
+
if not _is_absolute_config_path(cassette_value):
|
|
119
|
+
cassette_value = str((suite_dir / cassette_value).resolve())
|
|
120
|
+
data["cassette"] = cassette_value
|
|
81
121
|
assertions = data.get("assertions")
|
|
82
122
|
if isinstance(assertions, list):
|
|
83
123
|
_resolve_schema_paths(assertions, suite_dir)
|
|
@@ -63,7 +63,7 @@ def _expected_observed(assertion_type: str, details: Mapping[str, Any] | None) -
|
|
|
63
63
|
if assertion_type == "required_fields":
|
|
64
64
|
missing = details.get("missing")
|
|
65
65
|
return (
|
|
66
|
-
f"fields
|
|
66
|
+
f"required fields: {_join_names(missing)}",
|
|
67
67
|
f"missing: {_join_names(missing)}",
|
|
68
68
|
)
|
|
69
69
|
if assertion_type == "json_schema":
|
|
@@ -4,6 +4,7 @@ from collections import deque
|
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
import json
|
|
6
6
|
import queue
|
|
7
|
+
import shutil
|
|
7
8
|
import subprocess
|
|
8
9
|
import threading
|
|
9
10
|
import time
|
|
@@ -29,6 +30,29 @@ def _command_basename(program: str) -> str:
|
|
|
29
30
|
return program.replace("\\", "/").rsplit("/", 1)[-1].lower()
|
|
30
31
|
|
|
31
32
|
|
|
33
|
+
def _prefer_available_python(command: Sequence[str]) -> list[str]:
|
|
34
|
+
"""Run ``python3`` when a suite asks for bare ``python`` and only python3 exists.
|
|
35
|
+
|
|
36
|
+
Ubuntu, macOS, and pipx installs often have no ``python`` executable. Suites
|
|
37
|
+
generated before that was the default, and the checked-in examples, still say
|
|
38
|
+
``python``. Use ``python3`` instead of failing those runs. An explicit path
|
|
39
|
+
such as ``/usr/bin/python`` is left unchanged.
|
|
40
|
+
"""
|
|
41
|
+
if not command:
|
|
42
|
+
return list(command)
|
|
43
|
+
program = command[0]
|
|
44
|
+
normalized = program.replace("\\", "/")
|
|
45
|
+
if "/" in normalized:
|
|
46
|
+
return list(command)
|
|
47
|
+
if _command_basename(program) not in {"python", "python.exe"}:
|
|
48
|
+
return list(command)
|
|
49
|
+
if shutil.which(program):
|
|
50
|
+
return list(command)
|
|
51
|
+
if shutil.which("python3"):
|
|
52
|
+
return ["python3", *list(command[1:])]
|
|
53
|
+
return list(command)
|
|
54
|
+
|
|
55
|
+
|
|
32
56
|
def _agent_start_error(command: Sequence[str], exc: BaseException) -> AgentProcessError:
|
|
33
57
|
"""Turn a failed exec into an actionable error for suite.yaml authors."""
|
|
34
58
|
program = command[0] if command else ""
|
|
@@ -100,6 +124,7 @@ class AgentProcess:
|
|
|
100
124
|
return
|
|
101
125
|
if not self._command or not self._command[0].strip():
|
|
102
126
|
raise _agent_start_error(self._command, ValueError("empty command"))
|
|
127
|
+
self._command = _prefer_available_python(self._command)
|
|
103
128
|
try:
|
|
104
129
|
self._process = subprocess.Popen(
|
|
105
130
|
self._command,
|
|
@@ -21,11 +21,11 @@ _SENSITIVE_PARTS = {
|
|
|
21
21
|
|
|
22
22
|
_PATTERNS: list[tuple[re.Pattern[str], str]] = [
|
|
23
23
|
(re.compile(r"(?i)bearer\s+[a-z0-9._~+/=-]+"), "Bearer [REDACTED]"),
|
|
24
|
-
(re.compile(r"
|
|
25
|
-
(re.compile(r"
|
|
26
|
-
(re.compile(r"
|
|
24
|
+
(re.compile(r"\bsk-[A-Za-z0-9]{20,}\b"), "[REDACTED]"),
|
|
25
|
+
(re.compile(r"\bghp_[A-Za-z0-9]{36}\b"), "[REDACTED]"),
|
|
26
|
+
(re.compile(r"\bAKIA[0-9A-Z]{16}\b"), "[REDACTED]"),
|
|
27
27
|
(
|
|
28
|
-
re.compile(r"
|
|
28
|
+
re.compile(r"\beyJ[a-zA-Z0-9_-]{8,}\.[a-zA-Z0-9_-]{8,}\.[a-zA-Z0-9_-]{8,}\b"),
|
|
29
29
|
"[REDACTED]",
|
|
30
30
|
),
|
|
31
31
|
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: runledger
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: CI for tool-using agents: deterministic eval suites with record/replay, hard assertions, budgets, and PR regression gates.
|
|
5
5
|
Author: RunLedger
|
|
6
6
|
License-Expression: MIT
|
|
@@ -69,13 +69,13 @@ The same check as a GitHub Action. `suite.yaml` already points at `baselines/dem
|
|
|
69
69
|
|
|
70
70
|
```yaml
|
|
71
71
|
- id: evals
|
|
72
|
-
uses: runledger/Runledger@v0.2
|
|
72
|
+
uses: runledger/Runledger@v0.2.3
|
|
73
73
|
with:
|
|
74
74
|
path: ./evals/demo
|
|
75
75
|
mode: replay
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
`runledger/Runledger@v0.2.3` accepts a `baseline` input and sets `result` (`pass` or `fail`), `exit-code`, and `artifacts-path`. The in-repo self-test calls the action with `uses: ./`.
|
|
79
79
|
|
|
80
80
|
Page copy for runledger.io lives in `docs/site-quickstart.md`.
|
|
81
81
|
|
|
@@ -229,7 +229,7 @@ Optional:
|
|
|
229
229
|
|
|
230
230
|
```yaml
|
|
231
231
|
suite_name: support-triage
|
|
232
|
-
agent_command: ["
|
|
232
|
+
agent_command: ["python3", "agent.py"]
|
|
233
233
|
mode: replay # replay | record | live
|
|
234
234
|
cases_path: cases
|
|
235
235
|
tool_registry:
|
|
@@ -306,13 +306,10 @@ Each line is one tool invocation:
|
|
|
306
306
|
MVP assertions:
|
|
307
307
|
|
|
308
308
|
* `json_schema` (validate final output with JSON Schema)
|
|
309
|
-
* `required_fields` (keys exist
|
|
310
|
-
* `
|
|
311
|
-
* `
|
|
312
|
-
|
|
313
|
-
* must call tool X
|
|
314
|
-
* must not call tool Y
|
|
315
|
-
* X before Y (ordering)
|
|
309
|
+
* `required_fields` (keys exist)
|
|
310
|
+
* `must_call` (must call tool X)
|
|
311
|
+
* `must_not_call` (must not call tool Y)
|
|
312
|
+
* `call_order` (X before Y; tools do not have to be adjacent)
|
|
316
313
|
|
|
317
314
|
Not default-gating (optional later):
|
|
318
315
|
|
|
@@ -323,18 +320,12 @@ Not default-gating (optional later):
|
|
|
323
320
|
|
|
324
321
|
## Budgets (merge gates)
|
|
325
322
|
|
|
326
|
-
|
|
323
|
+
Budgets that fail a run:
|
|
327
324
|
|
|
328
325
|
* `max_wall_ms`
|
|
329
326
|
* `max_tool_calls`
|
|
330
327
|
* `max_tool_errors`
|
|
331
328
|
|
|
332
|
-
Not yet enforced (accepted in config, reported in artifacts when the agent sends metrics, but
|
|
333
|
-
they do not fail a run today):
|
|
334
|
-
|
|
335
|
-
* `max_tokens_out`
|
|
336
|
-
* `max_cost_usd`
|
|
337
|
-
|
|
338
329
|
---
|
|
339
330
|
|
|
340
331
|
## Baselines + regression checks
|
|
@@ -389,7 +380,7 @@ jobs:
|
|
|
389
380
|
|
|
390
381
|
- name: Run deterministic evals (replay)
|
|
391
382
|
id: evals
|
|
392
|
-
uses: runledger/Runledger@v0.2
|
|
383
|
+
uses: runledger/Runledger@v0.2.3
|
|
393
384
|
with:
|
|
394
385
|
path: ./evals/demo
|
|
395
386
|
mode: replay
|
|
@@ -398,7 +389,7 @@ jobs:
|
|
|
398
389
|
run: echo "result=${{ steps.evals.outputs.result }} exit=${{ steps.evals.outputs.exit-code }}"
|
|
399
390
|
```
|
|
400
391
|
|
|
401
|
-
`runledger init` writes `.github/workflows/runledger.yml`, which installs the CLI and runs the suite with `--baseline`. The action above is the Marketplace entry in `action.yml`. It uploads `runledger_out/**`
|
|
392
|
+
`runledger init` writes `.github/workflows/runledger.yml`, which installs the CLI and runs the suite with `--baseline`. The action above is the Marketplace entry in `action.yml` at `runledger/Runledger@v0.2.3`. It uploads `runledger_out/**` on success and on failure. The step sets `result`, `exit-code`, and `artifacts-path`, and you can pass `baseline:` to override `suite.yaml`. A failing run fails the job.
|
|
402
393
|
|
|
403
394
|
---
|
|
404
395
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|