oneharness-sdk 0.4.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.
- oneharness_sdk-0.4.0/LICENSE +21 -0
- oneharness_sdk-0.4.0/PKG-INFO +67 -0
- oneharness_sdk-0.4.0/README.md +41 -0
- oneharness_sdk-0.4.0/pyproject.toml +76 -0
- oneharness_sdk-0.4.0/src/oneharness_sdk/__init__.py +35 -0
- oneharness_sdk-0.4.0/src/oneharness_sdk/_client.py +332 -0
- oneharness_sdk-0.4.0/src/oneharness_sdk/_errors.py +18 -0
- oneharness_sdk-0.4.0/src/oneharness_sdk/_generated/__init__.py +1 -0
- oneharness_sdk-0.4.0/src/oneharness_sdk/_generated/input-keys.json +41 -0
- oneharness_sdk-0.4.0/src/oneharness_sdk/_generated/schemas.json +3192 -0
- oneharness_sdk-0.4.0/src/oneharness_sdk/_generated_types.py +61 -0
- oneharness_sdk-0.4.0/src/oneharness_sdk/_version.py +3 -0
- oneharness_sdk-0.4.0/src/oneharness_sdk/py.typed +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nick DeRobertis
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: oneharness-sdk
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Typed async Python SDK for oneharness
|
|
5
|
+
Keywords: cli,agent,claude,codex,harness
|
|
6
|
+
Author: Nick DeRobertis
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Dist: jsonschema>=4.18,<5
|
|
21
|
+
Requires-Dist: oneharness-cli==0.4.0
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Project-URL: Homepage, https://github.com/nickderobertis/oneharness
|
|
24
|
+
Project-URL: Repository, https://github.com/nickderobertis/oneharness
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# oneharness-sdk
|
|
28
|
+
|
|
29
|
+
Async Python access to the `oneharness` engine, generated from and validated against the Rust-owned JSON Schema contracts. The distribution is `oneharness-sdk`, the import is `oneharness_sdk`, and every release depends on the exact same `oneharness-cli` version.
|
|
30
|
+
|
|
31
|
+
```console
|
|
32
|
+
pip install oneharness-sdk
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import asyncio
|
|
37
|
+
|
|
38
|
+
from oneharness_sdk import OneHarness
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def main() -> None:
|
|
42
|
+
oneharness = OneHarness()
|
|
43
|
+
report = await oneharness.run(
|
|
44
|
+
{"prompt": "Summarize this repository", "harnesses": ["codex"]}
|
|
45
|
+
)
|
|
46
|
+
print(report["results"][0]["text"])
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
asyncio.run(main())
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The complete client surface is `run`, `run_stream`, `list`, `detect`, `history`, `history_list`, and `history_watch`. `run_stream` and `history_watch` are async iterators. Every envelope is validated before it is yielded; closing or cancelling an iterator terminates its subprocess.
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
async for envelope in oneharness.run_stream(
|
|
56
|
+
{"prompt": "Inspect this repository", "harnesses": ["codex"]}
|
|
57
|
+
):
|
|
58
|
+
if envelope["type"] == "event" and envelope["event"]["name"] == "shell":
|
|
59
|
+
break
|
|
60
|
+
|
|
61
|
+
async for envelope in oneharness.history_watch(
|
|
62
|
+
{"labels": {"graph": "release"}, "after": last_history_id}
|
|
63
|
+
):
|
|
64
|
+
print(envelope["record"]["history_id"])
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Python input keys are snake case and strict: unknown fields and misspellings fail before the CLI starts. Output dictionaries preserve additive fields from newer compatible CLI versions. `history` and `history_watch` raise `HistoryNotFoundError` when a session, record, or cursor cannot be resolved.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# oneharness-sdk
|
|
2
|
+
|
|
3
|
+
Async Python access to the `oneharness` engine, generated from and validated against the Rust-owned JSON Schema contracts. The distribution is `oneharness-sdk`, the import is `oneharness_sdk`, and every release depends on the exact same `oneharness-cli` version.
|
|
4
|
+
|
|
5
|
+
```console
|
|
6
|
+
pip install oneharness-sdk
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
import asyncio
|
|
11
|
+
|
|
12
|
+
from oneharness_sdk import OneHarness
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def main() -> None:
|
|
16
|
+
oneharness = OneHarness()
|
|
17
|
+
report = await oneharness.run(
|
|
18
|
+
{"prompt": "Summarize this repository", "harnesses": ["codex"]}
|
|
19
|
+
)
|
|
20
|
+
print(report["results"][0]["text"])
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
asyncio.run(main())
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The complete client surface is `run`, `run_stream`, `list`, `detect`, `history`, `history_list`, and `history_watch`. `run_stream` and `history_watch` are async iterators. Every envelope is validated before it is yielded; closing or cancelling an iterator terminates its subprocess.
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
async for envelope in oneharness.run_stream(
|
|
30
|
+
{"prompt": "Inspect this repository", "harnesses": ["codex"]}
|
|
31
|
+
):
|
|
32
|
+
if envelope["type"] == "event" and envelope["event"]["name"] == "shell":
|
|
33
|
+
break
|
|
34
|
+
|
|
35
|
+
async for envelope in oneharness.history_watch(
|
|
36
|
+
{"labels": {"graph": "release"}, "after": last_history_id}
|
|
37
|
+
):
|
|
38
|
+
print(envelope["record"]["history_id"])
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Python input keys are snake case and strict: unknown fields and misspellings fail before the CLI starts. Output dictionaries preserve additive fields from newer compatible CLI versions. `history` and `history_watch` raise `HistoryNotFoundError` when a session, record, or cursor cannot be resolved.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["uv_build>=0.11,<0.12"]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "oneharness-sdk"
|
|
7
|
+
version = "0.4.0"
|
|
8
|
+
description = "Typed async Python SDK for oneharness"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Nick DeRobertis" }]
|
|
14
|
+
keywords = ["cli", "agent", "claude", "codex", "harness"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.9",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Programming Language :: Python :: 3.14",
|
|
26
|
+
"Typing :: Typed",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"jsonschema>=4.18,<5",
|
|
30
|
+
"oneharness-cli==0.4.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/nickderobertis/oneharness"
|
|
35
|
+
Repository = "https://github.com/nickderobertis/oneharness"
|
|
36
|
+
|
|
37
|
+
[tool.uv.build-backend]
|
|
38
|
+
module-name = "oneharness_sdk"
|
|
39
|
+
module-root = "src"
|
|
40
|
+
|
|
41
|
+
[tool.ruff]
|
|
42
|
+
target-version = "py39"
|
|
43
|
+
line-length = 100
|
|
44
|
+
|
|
45
|
+
[tool.ruff.lint]
|
|
46
|
+
select = ["E4", "E7", "E9", "F", "I", "UP", "B", "ASYNC", "S", "RUF"]
|
|
47
|
+
ignore = [
|
|
48
|
+
"E501", # JSON protocol fixtures are clearer as intact strings.
|
|
49
|
+
"UP045", # Optional keeps runtime annotations unambiguous on Python 3.9.
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
[tool.ruff.lint.per-file-ignores]
|
|
53
|
+
"scripts/**" = [
|
|
54
|
+
"S404", # The generator deliberately invokes Cargo across the schema boundary.
|
|
55
|
+
"S603", # Its fixed argument vector is not built from untrusted input.
|
|
56
|
+
"S607", # Cargo is a required developer tool resolved from the active toolchain.
|
|
57
|
+
]
|
|
58
|
+
"test/**" = [
|
|
59
|
+
"S101", # Assertions are the test contract.
|
|
60
|
+
"S404", # Subprocess boundaries are intentional integration coverage.
|
|
61
|
+
"S603", # Test commands use fixed arguments or controlled fixtures.
|
|
62
|
+
"S607", # Tests intentionally resolve required developer tools from PATH.
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
[tool.mypy]
|
|
66
|
+
python_version = "3.9"
|
|
67
|
+
strict = true
|
|
68
|
+
warn_unreachable = true
|
|
69
|
+
|
|
70
|
+
[tool.coverage.run]
|
|
71
|
+
branch = true
|
|
72
|
+
source = ["oneharness_sdk"]
|
|
73
|
+
|
|
74
|
+
[tool.coverage.report]
|
|
75
|
+
fail_under = 95
|
|
76
|
+
show_missing = true
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Async Python SDK for the oneharness CLI."""
|
|
2
|
+
|
|
3
|
+
from ._client import OneHarness
|
|
4
|
+
from ._errors import ContractError, HistoryNotFoundError, OneHarnessProcessError
|
|
5
|
+
from ._generated_types import (
|
|
6
|
+
Detection,
|
|
7
|
+
HarnessInfo,
|
|
8
|
+
HistoryListOptions,
|
|
9
|
+
HistoryLookup,
|
|
10
|
+
HistoryRecord,
|
|
11
|
+
HistoryStreamEnvelope,
|
|
12
|
+
HistoryWatchOptions,
|
|
13
|
+
RunOptions,
|
|
14
|
+
RunReport,
|
|
15
|
+
RunStreamEnvelope,
|
|
16
|
+
)
|
|
17
|
+
from ._version import __version__
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"ContractError",
|
|
21
|
+
"Detection",
|
|
22
|
+
"HarnessInfo",
|
|
23
|
+
"HistoryListOptions",
|
|
24
|
+
"HistoryLookup",
|
|
25
|
+
"HistoryNotFoundError",
|
|
26
|
+
"HistoryRecord",
|
|
27
|
+
"HistoryStreamEnvelope",
|
|
28
|
+
"HistoryWatchOptions",
|
|
29
|
+
"OneHarness",
|
|
30
|
+
"OneHarnessProcessError",
|
|
31
|
+
"RunOptions",
|
|
32
|
+
"RunReport",
|
|
33
|
+
"RunStreamEnvelope",
|
|
34
|
+
"__version__",
|
|
35
|
+
]
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"""Async subprocess client for the oneharness JSON and JSONL interfaces."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import builtins
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import shutil
|
|
10
|
+
from collections.abc import AsyncIterator, Mapping, Sequence
|
|
11
|
+
from functools import cache
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Optional, cast
|
|
14
|
+
|
|
15
|
+
from jsonschema import Draft202012Validator
|
|
16
|
+
from jsonschema.protocols import Validator
|
|
17
|
+
|
|
18
|
+
from ._errors import ContractError, HistoryNotFoundError, OneHarnessProcessError
|
|
19
|
+
from ._generated_types import (
|
|
20
|
+
Detection,
|
|
21
|
+
HarnessInfo,
|
|
22
|
+
HistoryListOptions,
|
|
23
|
+
HistoryLookup,
|
|
24
|
+
HistoryRecord,
|
|
25
|
+
HistoryStreamEnvelope,
|
|
26
|
+
HistoryWatchOptions,
|
|
27
|
+
RunOptions,
|
|
28
|
+
RunReport,
|
|
29
|
+
RunStreamEnvelope,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
_STREAM_LIMIT = 16 * 1024 * 1024
|
|
33
|
+
_INPUT_ROOTS = {
|
|
34
|
+
"history_list_options",
|
|
35
|
+
"history_lookup",
|
|
36
|
+
"history_watch_options",
|
|
37
|
+
"run_options",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _load_json(name: str) -> dict[str, Any]:
|
|
42
|
+
path = Path(__file__).with_name("_generated") / name
|
|
43
|
+
return cast("dict[str, Any]", json.loads(path.read_text(encoding="utf-8")))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
_SCHEMAS = _load_json("schemas.json")
|
|
47
|
+
_INPUT_KEYS = cast("dict[str, dict[str, str]]", _load_json("input-keys.json"))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@cache
|
|
51
|
+
def _validator(root: str) -> Validator:
|
|
52
|
+
schema = _SCHEMAS[root]
|
|
53
|
+
Draft202012Validator.check_schema(schema)
|
|
54
|
+
return Draft202012Validator(schema)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _validate(root: str, value: Any, label: str) -> Any:
|
|
58
|
+
errors = sorted(_validator(root).iter_errors(value), key=lambda error: list(error.path))
|
|
59
|
+
if not errors:
|
|
60
|
+
return value
|
|
61
|
+
details = []
|
|
62
|
+
for error in errors:
|
|
63
|
+
path = ".".join(str(part) for part in error.absolute_path) or "<root>"
|
|
64
|
+
details.append(f"{path}: {error.message}")
|
|
65
|
+
raise ContractError(f"{label}: {'; '.join(details)}")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _input(root: str, value: Any, label: str) -> dict[str, Any]:
|
|
69
|
+
if root not in _INPUT_ROOTS: # pragma: no cover - internal programming guard
|
|
70
|
+
raise AssertionError(f"{root} is not an input schema")
|
|
71
|
+
checked = cast("Mapping[str, Any]", _validate(root, value, label))
|
|
72
|
+
keys = _INPUT_KEYS[root]
|
|
73
|
+
return {keys.get(key, key): item for key, item in checked.items()}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _many(args: list[str], flag: str, values: Optional[Sequence[str]]) -> None:
|
|
77
|
+
for value in values or ():
|
|
78
|
+
args.extend((flag, value))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _run_arguments(options: Mapping[str, Any], *, stream: bool) -> list[str]:
|
|
82
|
+
args = ["run", "--prompt", cast("str", options["prompt"]), "--compact"]
|
|
83
|
+
_many(args, "--harness", cast("Optional[Sequence[str]]", options.get("harnesses")))
|
|
84
|
+
_many(args, "--model", cast("Optional[Sequence[str]]", options.get("models")))
|
|
85
|
+
scalar_flags = {
|
|
86
|
+
"system": "--system",
|
|
87
|
+
"reasoning": "--reasoning",
|
|
88
|
+
"resume": "--resume",
|
|
89
|
+
"session": "--session",
|
|
90
|
+
"mode": "--mode",
|
|
91
|
+
"historyName": "--history-name",
|
|
92
|
+
"historyDir": "--history-dir",
|
|
93
|
+
}
|
|
94
|
+
for key, flag in scalar_flags.items():
|
|
95
|
+
if key in options:
|
|
96
|
+
args.extend((flag, cast("str", options[key])))
|
|
97
|
+
if options.get("fork"):
|
|
98
|
+
args.append("--fork")
|
|
99
|
+
if "timeoutSeconds" in options:
|
|
100
|
+
args.extend(("--timeout", str(options["timeoutSeconds"])))
|
|
101
|
+
if options.get("events"):
|
|
102
|
+
args.append("--events")
|
|
103
|
+
if stream:
|
|
104
|
+
args.append("--stream")
|
|
105
|
+
if options.get("history"):
|
|
106
|
+
args.append("--history")
|
|
107
|
+
for key, value in cast("Mapping[str, str]", options.get("historyLabels", {})).items():
|
|
108
|
+
args.extend(("--history-label", f"{key}={value}"))
|
|
109
|
+
for key, value in cast("Mapping[str, str]", options.get("env", {})).items():
|
|
110
|
+
args.extend(("--env", f"{key}={value}"))
|
|
111
|
+
for key, value in cast("Mapping[str, str]", options.get("bins", {})).items():
|
|
112
|
+
args.extend(("--bin", f"{key}={value}"))
|
|
113
|
+
return args
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
async def _terminate(process: asyncio.subprocess.Process) -> None:
|
|
117
|
+
if process.returncode is not None:
|
|
118
|
+
return
|
|
119
|
+
try:
|
|
120
|
+
process.terminate()
|
|
121
|
+
except ProcessLookupError: # pragma: no cover - OS race after returncode check
|
|
122
|
+
return
|
|
123
|
+
try:
|
|
124
|
+
await asyncio.wait_for(process.wait(), timeout=2)
|
|
125
|
+
except asyncio.TimeoutError: # pragma: no cover - defensive hard-kill fallback
|
|
126
|
+
process.kill()
|
|
127
|
+
await process.wait()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _process_error(returncode: int, stderr: str, *, history: bool) -> OneHarnessProcessError:
|
|
131
|
+
if history and (returncode == 1 or "was not found" in stderr):
|
|
132
|
+
return HistoryNotFoundError(returncode, stderr)
|
|
133
|
+
return OneHarnessProcessError(returncode, stderr)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class OneHarness:
|
|
137
|
+
"""Validated async access to an installed oneharness CLI."""
|
|
138
|
+
|
|
139
|
+
def __init__(
|
|
140
|
+
self,
|
|
141
|
+
*,
|
|
142
|
+
executable: Optional[str] = None,
|
|
143
|
+
executable_args: Sequence[str] = (),
|
|
144
|
+
env: Optional[Mapping[str, str]] = None,
|
|
145
|
+
) -> None:
|
|
146
|
+
self._executable = executable
|
|
147
|
+
self._executable_args = tuple(executable_args)
|
|
148
|
+
self._env = dict(env or {})
|
|
149
|
+
|
|
150
|
+
def _command(self, args: Sequence[str]) -> tuple[str, ...]:
|
|
151
|
+
command = self._executable or os.environ.get("ONEHARNESS_BIN")
|
|
152
|
+
if command is None:
|
|
153
|
+
command = shutil.which("oneharness") or "oneharness"
|
|
154
|
+
return (command, *self._executable_args, *args)
|
|
155
|
+
|
|
156
|
+
async def _spawn(
|
|
157
|
+
self, args: Sequence[str], cwd: Optional[str] = None
|
|
158
|
+
) -> asyncio.subprocess.Process:
|
|
159
|
+
return await asyncio.create_subprocess_exec(
|
|
160
|
+
*self._command(args),
|
|
161
|
+
cwd=cwd,
|
|
162
|
+
env={**os.environ, **self._env},
|
|
163
|
+
stdout=asyncio.subprocess.PIPE,
|
|
164
|
+
stderr=asyncio.subprocess.PIPE,
|
|
165
|
+
limit=_STREAM_LIMIT,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
async def _invoke(
|
|
169
|
+
self,
|
|
170
|
+
args: Sequence[str],
|
|
171
|
+
*,
|
|
172
|
+
cwd: Optional[str] = None,
|
|
173
|
+
accept_json_on_nonzero: bool = False,
|
|
174
|
+
history: bool = False,
|
|
175
|
+
) -> Any:
|
|
176
|
+
process = await self._spawn(args, cwd)
|
|
177
|
+
try:
|
|
178
|
+
stdout_bytes, stderr_bytes = await process.communicate()
|
|
179
|
+
except BaseException:
|
|
180
|
+
await _terminate(process)
|
|
181
|
+
raise
|
|
182
|
+
stderr = stderr_bytes.decode("utf-8", errors="replace")
|
|
183
|
+
if process.returncode != 0 and not accept_json_on_nonzero:
|
|
184
|
+
raise _process_error(process.returncode or 0, stderr, history=history)
|
|
185
|
+
try:
|
|
186
|
+
return json.loads(stdout_bytes)
|
|
187
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as error:
|
|
188
|
+
if process.returncode != 0:
|
|
189
|
+
raise _process_error(process.returncode or 0, stderr, history=history) from error
|
|
190
|
+
raise ContractError(f"oneharness returned invalid JSON: {error}") from error
|
|
191
|
+
|
|
192
|
+
async def _stream(
|
|
193
|
+
self,
|
|
194
|
+
args: Sequence[str],
|
|
195
|
+
root: str,
|
|
196
|
+
label: str,
|
|
197
|
+
*,
|
|
198
|
+
cwd: Optional[str] = None,
|
|
199
|
+
history: bool = False,
|
|
200
|
+
) -> AsyncIterator[dict[str, Any]]:
|
|
201
|
+
process = await self._spawn(args, cwd)
|
|
202
|
+
if process.stdout is None or process.stderr is None: # pragma: no cover - PIPE invariant
|
|
203
|
+
await _terminate(process)
|
|
204
|
+
raise RuntimeError("oneharness stream pipes were not created")
|
|
205
|
+
stderr_task = asyncio.create_task(process.stderr.read())
|
|
206
|
+
try:
|
|
207
|
+
while line := await process.stdout.readline():
|
|
208
|
+
if not line.strip():
|
|
209
|
+
continue
|
|
210
|
+
try:
|
|
211
|
+
value = json.loads(line)
|
|
212
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as error:
|
|
213
|
+
raise ContractError(f"{label}: invalid JSON: {error}") from error
|
|
214
|
+
yield cast("dict[str, Any]", _validate(root, value, label))
|
|
215
|
+
returncode = await process.wait()
|
|
216
|
+
stderr = (await stderr_task).decode("utf-8", errors="replace")
|
|
217
|
+
if returncode != 0:
|
|
218
|
+
raise _process_error(returncode, stderr, history=history)
|
|
219
|
+
finally:
|
|
220
|
+
await _terminate(process)
|
|
221
|
+
if not stderr_task.done():
|
|
222
|
+
stderr_task.cancel()
|
|
223
|
+
await asyncio.gather(stderr_task, return_exceptions=True)
|
|
224
|
+
|
|
225
|
+
async def run(self, options: RunOptions) -> RunReport:
|
|
226
|
+
"""Run one prompt and return a validated report."""
|
|
227
|
+
parsed = _input("run_options", options, "invalid oneharness run options")
|
|
228
|
+
value = await self._invoke(
|
|
229
|
+
_run_arguments(parsed, stream=False),
|
|
230
|
+
cwd=cast("Optional[str]", parsed.get("cwd")),
|
|
231
|
+
accept_json_on_nonzero=True,
|
|
232
|
+
)
|
|
233
|
+
return cast("RunReport", _validate("run_report", value, "invalid oneharness run contract"))
|
|
234
|
+
|
|
235
|
+
def run_stream(self, options: RunOptions) -> AsyncIterator[RunStreamEnvelope]:
|
|
236
|
+
"""Yield validated action/result envelopes as the harness runs."""
|
|
237
|
+
parsed = _input("run_options", options, "invalid oneharness run options")
|
|
238
|
+
return self._stream(
|
|
239
|
+
_run_arguments(parsed, stream=True),
|
|
240
|
+
"run_stream_envelope",
|
|
241
|
+
"invalid oneharness run stream contract",
|
|
242
|
+
cwd=cast("Optional[str]", parsed.get("cwd")),
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
async def list(self) -> builtins.list[HarnessInfo]:
|
|
246
|
+
"""Return the validated harness registry."""
|
|
247
|
+
value = _validate(
|
|
248
|
+
"list_report",
|
|
249
|
+
await self._invoke(("list", "--compact")),
|
|
250
|
+
"invalid oneharness list contract",
|
|
251
|
+
)
|
|
252
|
+
return cast("builtins.list[HarnessInfo]", value["harnesses"])
|
|
253
|
+
|
|
254
|
+
async def detect(self, harnesses: Sequence[str] = ()) -> builtins.list[Detection]:
|
|
255
|
+
"""Probe selected harness binaries."""
|
|
256
|
+
if isinstance(harnesses, (str, bytes)) or not all(
|
|
257
|
+
isinstance(harness, str) for harness in harnesses
|
|
258
|
+
):
|
|
259
|
+
raise ContractError("invalid oneharness detect options: harnesses must be strings")
|
|
260
|
+
args = ["detect", "--compact"]
|
|
261
|
+
_many(args, "--harness", harnesses)
|
|
262
|
+
value = _validate(
|
|
263
|
+
"detect_report",
|
|
264
|
+
await self._invoke(args),
|
|
265
|
+
"invalid oneharness detect contract",
|
|
266
|
+
)
|
|
267
|
+
return cast("builtins.list[Detection]", value["detected"])
|
|
268
|
+
|
|
269
|
+
async def history(self, lookup: HistoryLookup) -> builtins.list[HistoryRecord]:
|
|
270
|
+
"""Resolve one history record or session."""
|
|
271
|
+
parsed = _input("history_lookup", lookup, "invalid oneharness history options")
|
|
272
|
+
args = ["history", "show", "--compact"]
|
|
273
|
+
if parsed.get("last") is True:
|
|
274
|
+
args.append("--last")
|
|
275
|
+
else:
|
|
276
|
+
args.append(cast("str", parsed["session"]))
|
|
277
|
+
if parsed.get("project"):
|
|
278
|
+
args.extend(("--project", cast("str", parsed["project"])))
|
|
279
|
+
if parsed.get("allProjects"):
|
|
280
|
+
args.append("--all-projects")
|
|
281
|
+
if parsed.get("historyDir"):
|
|
282
|
+
args.extend(("--history-dir", cast("str", parsed["historyDir"])))
|
|
283
|
+
value = await self._invoke(args, history=True)
|
|
284
|
+
return cast(
|
|
285
|
+
"builtins.list[HistoryRecord]",
|
|
286
|
+
_validate("history_records", value, "invalid oneharness history contract"),
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
async def history_list(
|
|
290
|
+
self, options: Optional[HistoryListOptions] = None
|
|
291
|
+
) -> builtins.list[dict[str, Any]]:
|
|
292
|
+
"""List standardized history sessions."""
|
|
293
|
+
parsed = _input(
|
|
294
|
+
"history_list_options", options or {}, "invalid oneharness history list options"
|
|
295
|
+
)
|
|
296
|
+
args = ["history", "list", "--compact"]
|
|
297
|
+
if parsed.get("project"):
|
|
298
|
+
args.extend(("--project", cast("str", parsed["project"])))
|
|
299
|
+
if parsed.get("allProjects"):
|
|
300
|
+
args.append("--all-projects")
|
|
301
|
+
if parsed.get("historyDir"):
|
|
302
|
+
args.extend(("--history-dir", cast("str", parsed["historyDir"])))
|
|
303
|
+
value = await self._invoke(args)
|
|
304
|
+
return cast(
|
|
305
|
+
"builtins.list[dict[str, Any]]",
|
|
306
|
+
_validate("history_list", value, "invalid oneharness history list contract"),
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
def history_watch(
|
|
310
|
+
self, options: Optional[HistoryWatchOptions] = None
|
|
311
|
+
) -> AsyncIterator[HistoryStreamEnvelope]:
|
|
312
|
+
"""Follow validated standardized history records."""
|
|
313
|
+
parsed = _input(
|
|
314
|
+
"history_watch_options", options or {}, "invalid oneharness history watch options"
|
|
315
|
+
)
|
|
316
|
+
args = ["history", "watch", "--format", "jsonl"]
|
|
317
|
+
if parsed.get("after"):
|
|
318
|
+
args.extend(("--after", cast("str", parsed["after"])))
|
|
319
|
+
for key, value in cast("Mapping[str, str]", parsed.get("labels", {})).items():
|
|
320
|
+
args.extend(("--label", f"{key}={value}"))
|
|
321
|
+
if parsed.get("project"):
|
|
322
|
+
args.extend(("--project", cast("str", parsed["project"])))
|
|
323
|
+
if parsed.get("allProjects"):
|
|
324
|
+
args.append("--all-projects")
|
|
325
|
+
if parsed.get("historyDir"):
|
|
326
|
+
args.extend(("--history-dir", cast("str", parsed["historyDir"])))
|
|
327
|
+
return self._stream(
|
|
328
|
+
args,
|
|
329
|
+
"history_stream_envelope",
|
|
330
|
+
"invalid oneharness history watch contract",
|
|
331
|
+
history=True,
|
|
332
|
+
)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Typed public errors raised by the Python SDK."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ContractError(ValueError):
|
|
5
|
+
"""A value did not match its Rust-owned SDK contract."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class OneHarnessProcessError(RuntimeError):
|
|
9
|
+
"""The oneharness subprocess exited unsuccessfully."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, returncode: int, stderr: str) -> None:
|
|
12
|
+
self.returncode = returncode
|
|
13
|
+
self.stderr = stderr
|
|
14
|
+
super().__init__(f"oneharness exited {returncode}: {stderr.strip()}")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class HistoryNotFoundError(OneHarnessProcessError):
|
|
18
|
+
"""A history session, record, or watch cursor could not be resolved."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Rust-generated Python SDK assets."""
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"history_list_options": {
|
|
3
|
+
"all_projects": "allProjects",
|
|
4
|
+
"history_dir": "historyDir",
|
|
5
|
+
"project": "project"
|
|
6
|
+
},
|
|
7
|
+
"history_lookup": {
|
|
8
|
+
"all_projects": "allProjects",
|
|
9
|
+
"history_dir": "historyDir",
|
|
10
|
+
"last": "last",
|
|
11
|
+
"project": "project",
|
|
12
|
+
"session": "session"
|
|
13
|
+
},
|
|
14
|
+
"history_watch_options": {
|
|
15
|
+
"after": "after",
|
|
16
|
+
"all_projects": "allProjects",
|
|
17
|
+
"history_dir": "historyDir",
|
|
18
|
+
"labels": "labels",
|
|
19
|
+
"project": "project"
|
|
20
|
+
},
|
|
21
|
+
"run_options": {
|
|
22
|
+
"bins": "bins",
|
|
23
|
+
"cwd": "cwd",
|
|
24
|
+
"env": "env",
|
|
25
|
+
"events": "events",
|
|
26
|
+
"fork": "fork",
|
|
27
|
+
"harnesses": "harnesses",
|
|
28
|
+
"history": "history",
|
|
29
|
+
"history_dir": "historyDir",
|
|
30
|
+
"history_labels": "historyLabels",
|
|
31
|
+
"history_name": "historyName",
|
|
32
|
+
"mode": "mode",
|
|
33
|
+
"models": "models",
|
|
34
|
+
"prompt": "prompt",
|
|
35
|
+
"reasoning": "reasoning",
|
|
36
|
+
"resume": "resume",
|
|
37
|
+
"session": "session",
|
|
38
|
+
"system": "system",
|
|
39
|
+
"timeout_seconds": "timeoutSeconds"
|
|
40
|
+
}
|
|
41
|
+
}
|