mechaharness 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.
- mechaharness-0.1.0/LICENSE +21 -0
- mechaharness-0.1.0/PKG-INFO +171 -0
- mechaharness-0.1.0/README.md +123 -0
- mechaharness-0.1.0/pyproject.toml +94 -0
- mechaharness-0.1.0/setup.cfg +4 -0
- mechaharness-0.1.0/src/mechaharness/__init__.py +54 -0
- mechaharness-0.1.0/src/mechaharness/api/__init__.py +5 -0
- mechaharness-0.1.0/src/mechaharness/api/app.py +42 -0
- mechaharness-0.1.0/src/mechaharness/api_connection.py +160 -0
- mechaharness-0.1.0/src/mechaharness/cli/__init__.py +1 -0
- mechaharness-0.1.0/src/mechaharness/cli/main.py +131 -0
- mechaharness-0.1.0/src/mechaharness/config.py +37 -0
- mechaharness-0.1.0/src/mechaharness/connection.py +9 -0
- mechaharness-0.1.0/src/mechaharness/context_experiments.py +108 -0
- mechaharness-0.1.0/src/mechaharness/core/__init__.py +117 -0
- mechaharness-0.1.0/src/mechaharness/core/access.py +464 -0
- mechaharness-0.1.0/src/mechaharness/core/completer.py +28 -0
- mechaharness-0.1.0/src/mechaharness/core/contract.py +38 -0
- mechaharness-0.1.0/src/mechaharness/core/environment.py +115 -0
- mechaharness-0.1.0/src/mechaharness/core/events.py +328 -0
- mechaharness-0.1.0/src/mechaharness/core/exceptions.py +19 -0
- mechaharness-0.1.0/src/mechaharness/core/outcomes.py +48 -0
- mechaharness-0.1.0/src/mechaharness/core/types.py +86 -0
- mechaharness-0.1.0/src/mechaharness/decision_log.py +107 -0
- mechaharness-0.1.0/src/mechaharness/decisions.py +10 -0
- mechaharness-0.1.0/src/mechaharness/di.py +324 -0
- mechaharness-0.1.0/src/mechaharness/factory.py +65 -0
- mechaharness-0.1.0/src/mechaharness/graph.py +148 -0
- mechaharness-0.1.0/src/mechaharness/harness/__init__.py +18 -0
- mechaharness-0.1.0/src/mechaharness/harness/base.py +407 -0
- mechaharness-0.1.0/src/mechaharness/harness/families.py +17 -0
- mechaharness-0.1.0/src/mechaharness/harness/pass_through.py +28 -0
- mechaharness-0.1.0/src/mechaharness/harness/react.py +90 -0
- mechaharness-0.1.0/src/mechaharness/harness/tool_loop.py +23 -0
- mechaharness-0.1.0/src/mechaharness/inference/__init__.py +29 -0
- mechaharness-0.1.0/src/mechaharness/inference/anthropic.py +176 -0
- mechaharness-0.1.0/src/mechaharness/inference/base.py +43 -0
- mechaharness-0.1.0/src/mechaharness/inference/judge.py +378 -0
- mechaharness-0.1.0/src/mechaharness/inference/mock.py +46 -0
- mechaharness-0.1.0/src/mechaharness/inference/openai_compat.py +253 -0
- mechaharness-0.1.0/src/mechaharness/inference/openai_wire.py +142 -0
- mechaharness-0.1.0/src/mechaharness/inference/systemone.py +232 -0
- mechaharness-0.1.0/src/mechaharness/judgement_policy.py +217 -0
- mechaharness-0.1.0/src/mechaharness/operation_registry.py +49 -0
- mechaharness-0.1.0/src/mechaharness/operations.py +8 -0
- mechaharness-0.1.0/src/mechaharness/policy.py +15 -0
- mechaharness-0.1.0/src/mechaharness/py.typed +0 -0
- mechaharness-0.1.0/src/mechaharness/research.py +129 -0
- mechaharness-0.1.0/src/mechaharness/routing.py +121 -0
- mechaharness-0.1.0/src/mechaharness/tools/__init__.py +7 -0
- mechaharness-0.1.0/src/mechaharness/tools/base.py +117 -0
- mechaharness-0.1.0/src/mechaharness/tools/subagents.py +103 -0
- mechaharness-0.1.0/src/mechaharness.egg-info/PKG-INFO +171 -0
- mechaharness-0.1.0/src/mechaharness.egg-info/SOURCES.txt +77 -0
- mechaharness-0.1.0/src/mechaharness.egg-info/dependency_links.txt +1 -0
- mechaharness-0.1.0/src/mechaharness.egg-info/entry_points.txt +2 -0
- mechaharness-0.1.0/src/mechaharness.egg-info/requires.txt +24 -0
- mechaharness-0.1.0/src/mechaharness.egg-info/top_level.txt +1 -0
- mechaharness-0.1.0/tests/test_access.py +365 -0
- mechaharness-0.1.0/tests/test_api.py +29 -0
- mechaharness-0.1.0/tests/test_api_connection.py +67 -0
- mechaharness-0.1.0/tests/test_completer.py +58 -0
- mechaharness-0.1.0/tests/test_context_experiments.py +39 -0
- mechaharness-0.1.0/tests/test_di.py +109 -0
- mechaharness-0.1.0/tests/test_environment.py +75 -0
- mechaharness-0.1.0/tests/test_events.py +178 -0
- mechaharness-0.1.0/tests/test_graph.py +41 -0
- mechaharness-0.1.0/tests/test_harness.py +108 -0
- mechaharness-0.1.0/tests/test_inference_registry.py +55 -0
- mechaharness-0.1.0/tests/test_judge.py +165 -0
- mechaharness-0.1.0/tests/test_judgement_policy.py +107 -0
- mechaharness-0.1.0/tests/test_openai_wire.py +158 -0
- mechaharness-0.1.0/tests/test_operation_registry.py +15 -0
- mechaharness-0.1.0/tests/test_outcomes.py +43 -0
- mechaharness-0.1.0/tests/test_pass_through.py +100 -0
- mechaharness-0.1.0/tests/test_research.py +23 -0
- mechaharness-0.1.0/tests/test_routing.py +66 -0
- mechaharness-0.1.0/tests/test_subagent_tools.py +59 -0
- mechaharness-0.1.0/tests/test_usage_reasoning.py +40 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Richard Lee
|
|
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,171 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mechaharness
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Agentic harness that abstracts inference backends and model-family harness architectures
|
|
5
|
+
Author-email: Richard Lee <rlee@tokyo3.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://rl337.org/mechaharness/
|
|
8
|
+
Project-URL: Documentation, https://rl337.org/mechaharness/
|
|
9
|
+
Project-URL: Repository, https://github.com/rl337/mechaharness
|
|
10
|
+
Project-URL: Issues, https://github.com/rl337/mechaharness/issues
|
|
11
|
+
Project-URL: Changelog, https://rl337.org/mechaharness/changelog.html
|
|
12
|
+
Keywords: agent,llm,harness,inference,cli
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: pyiv>=0.3.0
|
|
28
|
+
Requires-Dist: httpx>=0.27
|
|
29
|
+
Requires-Dist: pydantic>=2.7
|
|
30
|
+
Requires-Dist: pydantic-settings>=2.3
|
|
31
|
+
Requires-Dist: typer>=0.12
|
|
32
|
+
Requires-Dist: rich>=13.7
|
|
33
|
+
Requires-Dist: fastapi>=0.111
|
|
34
|
+
Requires-Dist: uvicorn[standard]>=0.30
|
|
35
|
+
Requires-Dist: eval_type_backport>=0.2; python_version < "3.10"
|
|
36
|
+
Provides-Extra: dev
|
|
37
|
+
Requires-Dist: pytest>=8.2; extra == "dev"
|
|
38
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
39
|
+
Requires-Dist: ruff>=0.5; extra == "dev"
|
|
40
|
+
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
41
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
42
|
+
Requires-Dist: twine>=5.0; extra == "dev"
|
|
43
|
+
Provides-Extra: docs
|
|
44
|
+
Requires-Dist: sphinx>=5.0.0; extra == "docs"
|
|
45
|
+
Requires-Dist: sphinx-rtd-theme>=1.0.0; extra == "docs"
|
|
46
|
+
Requires-Dist: myst-parser>=2.0.0; extra == "docs"
|
|
47
|
+
Dynamic: license-file
|
|
48
|
+
|
|
49
|
+
# MechaHarness
|
|
50
|
+
|
|
51
|
+

|
|
52
|
+
|
|
53
|
+
Python **agent harness** for hosts that compose inference safely — not a thin
|
|
54
|
+
chat-client wrapper. Founding principles:
|
|
55
|
+
|
|
56
|
+
1. **Modular via dependency injection** — pyiv `MechaHarnessConfig`; hosts
|
|
57
|
+
subclass and compose (no forked enums / string registries)
|
|
58
|
+
2. **Multi-model / lanes native** — reason, judge, and media lanes;
|
|
59
|
+
`Completion` / `Judgement` / `Generation` outcomes
|
|
60
|
+
3. **Cost in the object model** — `CostAccountant` on the harness path so runs
|
|
61
|
+
cannot quietly go AWOL on spend
|
|
62
|
+
4. **EventLog telemetry built in** — queryable lifecycle, inference, tools,
|
|
63
|
+
cost, and access events on every run
|
|
64
|
+
|
|
65
|
+
Also: deny-by-default grants / `CompoundPolicy`, and host-extendable open
|
|
66
|
+
identity. Docs: [https://rl337.org/mechaharness/](https://rl337.org/mechaharness/)
|
|
67
|
+
|
|
68
|
+
## Install
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pip install mechaharness
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Requires Python 3.9+. Unreleased `main`:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install git+https://github.com/rl337/mechaharness.git
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Quick start
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# List backends / harness families
|
|
84
|
+
mechaharness backends
|
|
85
|
+
mechaharness families
|
|
86
|
+
|
|
87
|
+
# Local LM Studio (OpenAI-compatible)
|
|
88
|
+
mechaharness run "What is 2+2?" \
|
|
89
|
+
--backend lmstudio \
|
|
90
|
+
--family tool_loop \
|
|
91
|
+
--model local-model
|
|
92
|
+
|
|
93
|
+
# OpenAI
|
|
94
|
+
export MECHA_API_KEY=sk-...
|
|
95
|
+
mechaharness run "Hello" --backend openai --model gpt-4o-mini
|
|
96
|
+
|
|
97
|
+
# HTTP API
|
|
98
|
+
mechaharness serve --port 8080
|
|
99
|
+
curl -s http://127.0.0.1:8080/health
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Environment variables use the `MECHA_` prefix (`MECHA_API_KEY`, `MECHA_INFERENCE_BACKEND`, `MECHA_MODEL`, `MECHA_BASE_URL`, …).
|
|
103
|
+
|
|
104
|
+
## Library usage
|
|
105
|
+
|
|
106
|
+
Non-DI (OpenAPI-shaped):
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
import asyncio
|
|
110
|
+
from mechaharness import RunRequest, run
|
|
111
|
+
|
|
112
|
+
async def main() -> None:
|
|
113
|
+
result = await run(RunRequest(prompt="What is 19 + 23?", backend="lmstudio"))
|
|
114
|
+
print(result.final_text)
|
|
115
|
+
|
|
116
|
+
asyncio.run(main())
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
DI (pyiv Config hooks):
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from pyiv import get_injector
|
|
123
|
+
|
|
124
|
+
from mechaharness.di import MechaHarnessConfig
|
|
125
|
+
from mechaharness.harness.base import AbstractHarness
|
|
126
|
+
from mechaharness.harness.tool_loop import ToolLoopHarness
|
|
127
|
+
from mechaharness.inference.openai_compat import OpenAICompatStrategy
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class MyConfig(MechaHarnessConfig):
|
|
131
|
+
def get_inference_class(self):
|
|
132
|
+
return OpenAICompatStrategy
|
|
133
|
+
|
|
134
|
+
def get_harness_class(self):
|
|
135
|
+
return ToolLoopHarness
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
injector = get_injector(MyConfig)
|
|
139
|
+
harness = injector.inject(AbstractHarness)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
See [Dependency injection](https://rl337.org/mechaharness/guides/dependency-injection.html).
|
|
143
|
+
|
|
144
|
+
## Extending
|
|
145
|
+
|
|
146
|
+
Subclass `InferenceStrategy` (constructor takes `Settings`) and return it from `get_inference_class()`, or merge it into `SettingsConfig.inference_classes()`. Subclass `AbstractHarness` and return it from `get_harness_class()`.
|
|
147
|
+
|
|
148
|
+
## Development
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
python -m venv .venv
|
|
152
|
+
source .venv/bin/activate
|
|
153
|
+
pip install -e ".[dev,docs]"
|
|
154
|
+
./run_checks.sh
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`run_checks.sh` is what CI runs: ruff, mypy, pytest, Sphinx, and CLI smoke. EventLog + cost without a GPU:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
mechaharness run "What is 2+2?" --backend mock --family pass_through --model mock --json
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Design notes
|
|
164
|
+
|
|
165
|
+
| Concern | Pattern | Why |
|
|
166
|
+
|--------|---------|-----|
|
|
167
|
+
| Wiring | pyiv Config hooks | DI-first; hosts inject MechaHarness types |
|
|
168
|
+
| Provider I/O | Strategy | Swap cloud/local without touching agent logic |
|
|
169
|
+
| Agent loop | Template method hierarchy | Share turn accounting; specialize stop/tool rules per model family |
|
|
170
|
+
| Frontends | OpenAPI `RunRequest` / `run()` | Same contract for CLI, HTTP, and non-DI Python |
|
|
171
|
+
| Bindings later | Pydantic + OpenAPI | Types stay serializable; API is the first non-Python client surface |
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# MechaHarness
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
Python **agent harness** for hosts that compose inference safely — not a thin
|
|
6
|
+
chat-client wrapper. Founding principles:
|
|
7
|
+
|
|
8
|
+
1. **Modular via dependency injection** — pyiv `MechaHarnessConfig`; hosts
|
|
9
|
+
subclass and compose (no forked enums / string registries)
|
|
10
|
+
2. **Multi-model / lanes native** — reason, judge, and media lanes;
|
|
11
|
+
`Completion` / `Judgement` / `Generation` outcomes
|
|
12
|
+
3. **Cost in the object model** — `CostAccountant` on the harness path so runs
|
|
13
|
+
cannot quietly go AWOL on spend
|
|
14
|
+
4. **EventLog telemetry built in** — queryable lifecycle, inference, tools,
|
|
15
|
+
cost, and access events on every run
|
|
16
|
+
|
|
17
|
+
Also: deny-by-default grants / `CompoundPolicy`, and host-extendable open
|
|
18
|
+
identity. Docs: [https://rl337.org/mechaharness/](https://rl337.org/mechaharness/)
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install mechaharness
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Requires Python 3.9+. Unreleased `main`:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install git+https://github.com/rl337/mechaharness.git
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Quick start
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# List backends / harness families
|
|
36
|
+
mechaharness backends
|
|
37
|
+
mechaharness families
|
|
38
|
+
|
|
39
|
+
# Local LM Studio (OpenAI-compatible)
|
|
40
|
+
mechaharness run "What is 2+2?" \
|
|
41
|
+
--backend lmstudio \
|
|
42
|
+
--family tool_loop \
|
|
43
|
+
--model local-model
|
|
44
|
+
|
|
45
|
+
# OpenAI
|
|
46
|
+
export MECHA_API_KEY=sk-...
|
|
47
|
+
mechaharness run "Hello" --backend openai --model gpt-4o-mini
|
|
48
|
+
|
|
49
|
+
# HTTP API
|
|
50
|
+
mechaharness serve --port 8080
|
|
51
|
+
curl -s http://127.0.0.1:8080/health
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Environment variables use the `MECHA_` prefix (`MECHA_API_KEY`, `MECHA_INFERENCE_BACKEND`, `MECHA_MODEL`, `MECHA_BASE_URL`, …).
|
|
55
|
+
|
|
56
|
+
## Library usage
|
|
57
|
+
|
|
58
|
+
Non-DI (OpenAPI-shaped):
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import asyncio
|
|
62
|
+
from mechaharness import RunRequest, run
|
|
63
|
+
|
|
64
|
+
async def main() -> None:
|
|
65
|
+
result = await run(RunRequest(prompt="What is 19 + 23?", backend="lmstudio"))
|
|
66
|
+
print(result.final_text)
|
|
67
|
+
|
|
68
|
+
asyncio.run(main())
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
DI (pyiv Config hooks):
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from pyiv import get_injector
|
|
75
|
+
|
|
76
|
+
from mechaharness.di import MechaHarnessConfig
|
|
77
|
+
from mechaharness.harness.base import AbstractHarness
|
|
78
|
+
from mechaharness.harness.tool_loop import ToolLoopHarness
|
|
79
|
+
from mechaharness.inference.openai_compat import OpenAICompatStrategy
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class MyConfig(MechaHarnessConfig):
|
|
83
|
+
def get_inference_class(self):
|
|
84
|
+
return OpenAICompatStrategy
|
|
85
|
+
|
|
86
|
+
def get_harness_class(self):
|
|
87
|
+
return ToolLoopHarness
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
injector = get_injector(MyConfig)
|
|
91
|
+
harness = injector.inject(AbstractHarness)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
See [Dependency injection](https://rl337.org/mechaharness/guides/dependency-injection.html).
|
|
95
|
+
|
|
96
|
+
## Extending
|
|
97
|
+
|
|
98
|
+
Subclass `InferenceStrategy` (constructor takes `Settings`) and return it from `get_inference_class()`, or merge it into `SettingsConfig.inference_classes()`. Subclass `AbstractHarness` and return it from `get_harness_class()`.
|
|
99
|
+
|
|
100
|
+
## Development
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
python -m venv .venv
|
|
104
|
+
source .venv/bin/activate
|
|
105
|
+
pip install -e ".[dev,docs]"
|
|
106
|
+
./run_checks.sh
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`run_checks.sh` is what CI runs: ruff, mypy, pytest, Sphinx, and CLI smoke. EventLog + cost without a GPU:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
mechaharness run "What is 2+2?" --backend mock --family pass_through --model mock --json
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Design notes
|
|
116
|
+
|
|
117
|
+
| Concern | Pattern | Why |
|
|
118
|
+
|--------|---------|-----|
|
|
119
|
+
| Wiring | pyiv Config hooks | DI-first; hosts inject MechaHarness types |
|
|
120
|
+
| Provider I/O | Strategy | Swap cloud/local without touching agent logic |
|
|
121
|
+
| Agent loop | Template method hierarchy | Share turn accounting; specialize stop/tool rules per model family |
|
|
122
|
+
| Frontends | OpenAPI `RunRequest` / `run()` | Same contract for CLI, HTTP, and non-DI Python |
|
|
123
|
+
| Bindings later | Pydantic + OpenAPI | Types stay serializable; API is the first non-Python client surface |
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mechaharness"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Agentic harness that abstracts inference backends and model-family harness architectures"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Richard Lee", email = "rlee@tokyo3.com" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["agent", "llm", "harness", "inference", "cli"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.9",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
27
|
+
"Typing :: Typed",
|
|
28
|
+
]
|
|
29
|
+
dependencies = [
|
|
30
|
+
"pyiv>=0.3.0",
|
|
31
|
+
"httpx>=0.27",
|
|
32
|
+
"pydantic>=2.7",
|
|
33
|
+
"pydantic-settings>=2.3",
|
|
34
|
+
"typer>=0.12",
|
|
35
|
+
"rich>=13.7",
|
|
36
|
+
"fastapi>=0.111",
|
|
37
|
+
"uvicorn[standard]>=0.30",
|
|
38
|
+
"eval_type_backport>=0.2; python_version < '3.10'",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[project.optional-dependencies]
|
|
42
|
+
dev = [
|
|
43
|
+
"pytest>=8.2",
|
|
44
|
+
"pytest-asyncio>=0.23",
|
|
45
|
+
"ruff>=0.5",
|
|
46
|
+
"mypy>=1.10",
|
|
47
|
+
"build>=1.2",
|
|
48
|
+
"twine>=5.0",
|
|
49
|
+
]
|
|
50
|
+
docs = [
|
|
51
|
+
"sphinx>=5.0.0",
|
|
52
|
+
"sphinx-rtd-theme>=1.0.0",
|
|
53
|
+
"myst-parser>=2.0.0",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[project.scripts]
|
|
57
|
+
mechaharness = "mechaharness.cli.main:app"
|
|
58
|
+
|
|
59
|
+
[project.urls]
|
|
60
|
+
Homepage = "https://rl337.org/mechaharness/"
|
|
61
|
+
Documentation = "https://rl337.org/mechaharness/"
|
|
62
|
+
Repository = "https://github.com/rl337/mechaharness"
|
|
63
|
+
Issues = "https://github.com/rl337/mechaharness/issues"
|
|
64
|
+
Changelog = "https://rl337.org/mechaharness/changelog.html"
|
|
65
|
+
|
|
66
|
+
[tool.setuptools.packages.find]
|
|
67
|
+
where = ["src"]
|
|
68
|
+
|
|
69
|
+
[tool.setuptools.package-data]
|
|
70
|
+
mechaharness = ["py.typed"]
|
|
71
|
+
|
|
72
|
+
[tool.pytest.ini_options]
|
|
73
|
+
asyncio_mode = "auto"
|
|
74
|
+
testpaths = ["tests"]
|
|
75
|
+
pythonpath = ["."]
|
|
76
|
+
markers = [
|
|
77
|
+
"story_live: dual-mode user stories against live backends (MECHA_STORY_BACKEND=live)",
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
[tool.ruff]
|
|
81
|
+
line-length = 100
|
|
82
|
+
target-version = "py39"
|
|
83
|
+
|
|
84
|
+
[tool.ruff.lint]
|
|
85
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
86
|
+
|
|
87
|
+
[tool.mypy]
|
|
88
|
+
python_version = "3.9"
|
|
89
|
+
strict = true
|
|
90
|
+
packages = ["mechaharness"]
|
|
91
|
+
|
|
92
|
+
[[tool.mypy.overrides]]
|
|
93
|
+
module = "pyiv.*"
|
|
94
|
+
ignore_missing_imports = true
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""MechaHarness: agentic harness with pluggable inference and harness families."""
|
|
2
|
+
|
|
3
|
+
from mechaharness.config import Settings
|
|
4
|
+
from mechaharness.core.access import AccessControl, CostAccountant
|
|
5
|
+
from mechaharness.core.completer import Completer
|
|
6
|
+
from mechaharness.core.contract import RunRequest, RunResponse
|
|
7
|
+
from mechaharness.core.environment import InferenceEnvironment
|
|
8
|
+
from mechaharness.core.events import Event, EventLog, EventType
|
|
9
|
+
from mechaharness.core.types import (
|
|
10
|
+
ChatMessage,
|
|
11
|
+
CompletionRequest,
|
|
12
|
+
CompletionResponse,
|
|
13
|
+
Role,
|
|
14
|
+
ToolCall,
|
|
15
|
+
ToolDefinition,
|
|
16
|
+
ToolResult,
|
|
17
|
+
)
|
|
18
|
+
from mechaharness.di import MechaHarnessConfig, SettingsConfig, list_inference_backends
|
|
19
|
+
from mechaharness.factory import run
|
|
20
|
+
from mechaharness.harness.base import AbstractHarness, HarnessConfig, HarnessResult
|
|
21
|
+
from mechaharness.inference.base import InferenceStrategy
|
|
22
|
+
from mechaharness.tools.base import Tool, ToolRegistry
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"AbstractHarness",
|
|
26
|
+
"AccessControl",
|
|
27
|
+
"ChatMessage",
|
|
28
|
+
"Completer",
|
|
29
|
+
"CompletionRequest",
|
|
30
|
+
"CompletionResponse",
|
|
31
|
+
"CostAccountant",
|
|
32
|
+
"Event",
|
|
33
|
+
"EventLog",
|
|
34
|
+
"EventType",
|
|
35
|
+
"HarnessConfig",
|
|
36
|
+
"HarnessResult",
|
|
37
|
+
"InferenceEnvironment",
|
|
38
|
+
"InferenceStrategy",
|
|
39
|
+
"MechaHarnessConfig",
|
|
40
|
+
"Role",
|
|
41
|
+
"RunRequest",
|
|
42
|
+
"RunResponse",
|
|
43
|
+
"Settings",
|
|
44
|
+
"SettingsConfig",
|
|
45
|
+
"Tool",
|
|
46
|
+
"ToolCall",
|
|
47
|
+
"ToolDefinition",
|
|
48
|
+
"ToolRegistry",
|
|
49
|
+
"ToolResult",
|
|
50
|
+
"list_inference_backends",
|
|
51
|
+
"run",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""HTTP API surface (stable contract for non-Python clients)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from fastapi import FastAPI, HTTPException
|
|
6
|
+
|
|
7
|
+
from mechaharness import __version__
|
|
8
|
+
from mechaharness.core.contract import RunRequest, RunResponse
|
|
9
|
+
from mechaharness.core.exceptions import MechaHarnessError
|
|
10
|
+
from mechaharness.di import list_harness_families, list_inference_backends
|
|
11
|
+
from mechaharness.factory import run as run_harness
|
|
12
|
+
|
|
13
|
+
app = FastAPI(
|
|
14
|
+
title="MechaHarness",
|
|
15
|
+
version=__version__,
|
|
16
|
+
description="Agentic harness API — pluggable inference backends and harness families.",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.get("/health")
|
|
21
|
+
async def health() -> dict[str, str]:
|
|
22
|
+
return {"status": "ok", "version": __version__}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@app.get("/backends")
|
|
26
|
+
async def backends() -> dict[str, list[str]]:
|
|
27
|
+
return {"backends": list_inference_backends()}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@app.get("/families")
|
|
31
|
+
async def families() -> dict[str, list[str]]:
|
|
32
|
+
return {"families": list_harness_families()}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@app.post("/v1/run", response_model=RunResponse)
|
|
36
|
+
async def run(req: RunRequest) -> RunResponse:
|
|
37
|
+
try:
|
|
38
|
+
return await run_harness(req)
|
|
39
|
+
except KeyError as exc:
|
|
40
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
41
|
+
except MechaHarnessError as exc:
|
|
42
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""HTTP connection configuration for inference adapters.
|
|
2
|
+
|
|
3
|
+
``APIConnectionConfig`` is the injectable “how to reach this service” bundle
|
|
4
|
+
(endpoint + credentials + timeout). Wire shape stays on ``JudgeProvider`` /
|
|
5
|
+
``InferenceStrategy``. Hosts swap implementations (simple HTTP, later OAuth)
|
|
6
|
+
via Config hooks without proliferating URL-only and auth-only types.
|
|
7
|
+
|
|
8
|
+
Judge-lane env knobs (``MECHA_JUDGE_*``) bind here via ``from_env`` — not on
|
|
9
|
+
``Settings``.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
from abc import ABC, abstractmethod
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
DEFAULT_JUDGE_PATH = "/v1/systemone"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class APIConnectionConfig(ABC):
|
|
22
|
+
"""How to open an HTTP connection to an inference / judge endpoint."""
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def endpoint_url(self) -> str:
|
|
26
|
+
"""Absolute URL used for the request (no further path joining)."""
|
|
27
|
+
|
|
28
|
+
def headers(self) -> dict[str, str]:
|
|
29
|
+
"""Extra HTTP headers (Authorization, etc.)."""
|
|
30
|
+
return {}
|
|
31
|
+
|
|
32
|
+
def timeout_seconds(self) -> float:
|
|
33
|
+
return 60.0
|
|
34
|
+
|
|
35
|
+
def model_id(self) -> str | None:
|
|
36
|
+
"""Optional model id for JSON bodies that accept ``model``."""
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SimpleHttpConnectionConfig(APIConnectionConfig):
|
|
41
|
+
"""Base URL + path (or full URL) with optional API key header.
|
|
42
|
+
|
|
43
|
+
Default judge path is ``/v1/systemone``. Set ``url`` to override the full
|
|
44
|
+
endpoint (e.g. a hosted provider with a different scheme).
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
*,
|
|
50
|
+
base_url: str | None = None,
|
|
51
|
+
path: str = DEFAULT_JUDGE_PATH,
|
|
52
|
+
url: str | None = None,
|
|
53
|
+
api_key: str | None = None,
|
|
54
|
+
model: str | None = None,
|
|
55
|
+
timeout: float = 60.0,
|
|
56
|
+
header_name: str = "Authorization",
|
|
57
|
+
header_value_template: str = "Bearer {api_key}",
|
|
58
|
+
) -> None:
|
|
59
|
+
self._base_url = (base_url or "").rstrip("/")
|
|
60
|
+
self._path = path if path.startswith("/") else f"/{path}"
|
|
61
|
+
self._url = url.rstrip("/") if url else None
|
|
62
|
+
self._api_key = api_key
|
|
63
|
+
self._model = model
|
|
64
|
+
self._timeout = timeout
|
|
65
|
+
self._header_name = header_name
|
|
66
|
+
self._header_value_template = header_value_template
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def from_env(
|
|
70
|
+
cls,
|
|
71
|
+
*,
|
|
72
|
+
prefix: str = "MECHA_JUDGE",
|
|
73
|
+
legacy_prefix: str | None = "MECHA_DECIDE",
|
|
74
|
+
default_path: str = DEFAULT_JUDGE_PATH,
|
|
75
|
+
fallback_api_key_env: str = "MECHA_API_KEY",
|
|
76
|
+
) -> SimpleHttpConnectionConfig:
|
|
77
|
+
"""Bind connection knobs from the process environment.
|
|
78
|
+
|
|
79
|
+
Reads ``{prefix}_BASE_URL``, ``_PATH``, ``_URL``, ``_MODEL``,
|
|
80
|
+
``_API_KEY``, ``_TIMEOUT_SECONDS``. When ``legacy_prefix`` is set,
|
|
81
|
+
falls back for base URL / model (judge migration window).
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def _get(*keys: str) -> str | None:
|
|
85
|
+
for key in keys:
|
|
86
|
+
value = os.environ.get(key)
|
|
87
|
+
if value is not None and value != "":
|
|
88
|
+
return value
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
base = _get(f"{prefix}_BASE_URL")
|
|
92
|
+
model = _get(f"{prefix}_MODEL")
|
|
93
|
+
if legacy_prefix:
|
|
94
|
+
if base is None:
|
|
95
|
+
base = _get(f"{legacy_prefix}_BASE_URL")
|
|
96
|
+
if model is None:
|
|
97
|
+
model = _get(f"{legacy_prefix}_MODEL")
|
|
98
|
+
path = _get(f"{prefix}_PATH") or default_path
|
|
99
|
+
url = _get(f"{prefix}_URL")
|
|
100
|
+
api_key = _get(f"{prefix}_API_KEY", fallback_api_key_env)
|
|
101
|
+
timeout_raw = _get(f"{prefix}_TIMEOUT_SECONDS")
|
|
102
|
+
timeout = float(timeout_raw) if timeout_raw else 60.0
|
|
103
|
+
return cls(
|
|
104
|
+
base_url=base,
|
|
105
|
+
path=path,
|
|
106
|
+
url=url,
|
|
107
|
+
api_key=api_key,
|
|
108
|
+
model=model,
|
|
109
|
+
timeout=timeout,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def for_judge(cls, **overrides: Any) -> SimpleHttpConnectionConfig:
|
|
114
|
+
"""Judge-lane connection from env, with optional constructor overrides."""
|
|
115
|
+
conn = cls.from_env()
|
|
116
|
+
if not overrides:
|
|
117
|
+
return conn
|
|
118
|
+
return cls(
|
|
119
|
+
base_url=overrides.get("base_url", conn._base_url or None),
|
|
120
|
+
path=overrides.get("path", conn._path),
|
|
121
|
+
url=overrides.get("url", conn._url),
|
|
122
|
+
api_key=overrides.get("api_key", conn._api_key),
|
|
123
|
+
model=overrides.get("model", conn._model),
|
|
124
|
+
timeout=float(overrides.get("timeout", conn._timeout)),
|
|
125
|
+
header_name=overrides.get("header_name", conn._header_name),
|
|
126
|
+
header_value_template=overrides.get(
|
|
127
|
+
"header_value_template", conn._header_value_template
|
|
128
|
+
),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def endpoint_url(self) -> str:
|
|
132
|
+
if self._url:
|
|
133
|
+
return self._url
|
|
134
|
+
if not self._base_url:
|
|
135
|
+
raise ValueError(
|
|
136
|
+
"MECHA_JUDGE_BASE_URL is required when MECHA_JUDGE_URL is unset"
|
|
137
|
+
)
|
|
138
|
+
return f"{self._base_url}{self._path}"
|
|
139
|
+
|
|
140
|
+
def headers(self) -> dict[str, str]:
|
|
141
|
+
if not self._api_key:
|
|
142
|
+
return {}
|
|
143
|
+
value = self._header_value_template.format(api_key=self._api_key)
|
|
144
|
+
return {self._header_name: value}
|
|
145
|
+
|
|
146
|
+
def timeout_seconds(self) -> float:
|
|
147
|
+
return self._timeout
|
|
148
|
+
|
|
149
|
+
def model_id(self) -> str | None:
|
|
150
|
+
return self._model
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def connection_as_dict(conn: APIConnectionConfig) -> dict[str, Any]:
|
|
154
|
+
"""Debug helper — never log secrets from headers."""
|
|
155
|
+
return {
|
|
156
|
+
"endpoint_url": conn.endpoint_url(),
|
|
157
|
+
"timeout_seconds": conn.timeout_seconds(),
|
|
158
|
+
"model_id": conn.model_id(),
|
|
159
|
+
"header_keys": sorted(conn.headers()),
|
|
160
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI package."""
|