Metadata-Version: 2.4
Name: harnessrouter
Version: 0.1.0
Summary: Python client and CLI for any server that speaks the Unified Harness Protocol: run agent tasks, stream progress, continue sessions, cancel work, collect artifacts.
Author: HarnessRouter
License: Apache-2.0
Project-URL: Homepage, https://github.com/HarnessRouter/harnessrouter
Project-URL: Source, https://github.com/HarnessRouter/harnessrouter
Project-URL: Issues, https://github.com/HarnessRouter/harnessrouter/issues
Project-URL: Specification, https://github.com/HarnessRouter/harnessrouter/tree/main/protocol
Keywords: harnessrouter,uhp,agent,harness,ai-agents,client,sdk,cli,streaming
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: unifiedharnessprotocol>=0.1.0
Requires-Dist: harnessclientprotocol>=0.1.0
Dynamic: license-file

# harnessrouter

**A Python client and CLI for any server that speaks the Unified Harness Protocol.**

A *harness* is a configured agent: a runtime, a model, instructions and limits. A *task* is one run
of it — a real conversation against a real workspace, streamed back as it happens. This client
sends work, follows it, continues the conversation, cancels it, and collects the files it produced.

It is a UHP client, not a client for one product. It works against the open-source HarnessRouter
server, the hosted one, or any third implementation that passes the conformance suite. Standard
library only — no dependency on an HTTP stack you may not want.

```bash
pip install harnessrouter
```

## Run a task

```python
import os
from harnessrouter import HarnessRouter

client = HarnessRouter("http://localhost:3000", os.environ["UHP_API_KEY"])

harness = client.harnesses()[0]
response = client.run("Summarise README.md in three bullets.", harness_id=harness["id"])

response["status"]   # 'completed'
response["output"]   # ordered items: message, reasoning, function_call, function_call_output
```

## Follow it while it happens

Agent tasks run for minutes. Without a stream a product can only show a spinner and hope.

```python
for event in client.stream("Add tests for the parser.", harness_id=harness["id"]):
    if event["type"] == "response.output_text.delta":
        print(event["delta"], end="", flush=True)
```

Or let the client assemble it for you:

```python
task = client.collect("Add tests for the parser.", harness_id=harness["id"])
task.text          # the finished text
task.tool_calls    # what the agent ran
task.annotations   # the files it wrote
task.sequence      # (False, [12]) if the stream dropped an event
```

## Continue, cancel, collect

```python
nxt = client.continue_(response["id"], "Now add tests for the function you just wrote.")
# same session, same working directory, same files

client.cancel(nxt["id"])                      # a request to stop; the session stays continuable
files = client.session_files(nxt["metadata"]["session_id"])
data = client.download_file(files[0]["container_id"], files[0]["id"])
```

## What it does for you

- **Idempotency by default.** Every `run` carries an `Idempotency-Key`, so a retry cannot start a
  second agent against the same files.
- **Retries only what is worth retrying.** `session_busy` and `503`s back off and try again;
  `quota_exhausted` and every malformed request do not. A `POST /v1/responses` that may already
  have started is never resent on the client's own initiative.
- **Errors you can branch on.** `UhpError` carries the server's `type`, `code`, `param` and
  `detail`, plus a `retryable` property — no parsing prose to decide what to do.
- **Long timeouts.** 15 minutes by default, because a 30-second timeout cancels healthy work.

## CLI

```bash
export UHP_BASE_URL=http://localhost:3000
export UHP_API_KEY=…

harnessrouter discover                       # what the server is, and what it supports
harnessrouter harnesses                      # chrn_…  Research agent  [claude-code]
harnessrouter stream "Summarise README.md" --harness chrn_…
```

## API

| Method | What it does |
|---|---|
| `discover()` | The server's version, conformance class and capabilities (no token needed) |
| `harnesses()`, `harness(id)` | Configured harnesses in your scope |
| `models()`, `harness_models(id)` | Model catalogues, with a computed `available` |
| `run(input, ...)` | Run a task and wait for the result |
| `stream(input, ...)` | Run a task and iterate its events |
| `collect(input, ...)` | Run, stream, and return the accumulated task |
| `continue_(response_id, input)` | Continue the session that produced that response |
| `get_response(id)`, `input_items(id)`, `wait(id)` | Read a task back, or poll it to a terminal state |
| `cancel(id)`, `delete_response(id)` | Stop work; delete history. Deliberately different things |
| `sessions()`, `session(id)`, `turns(id)`, `session_files(id)` | Session listing and inspection |
| `download_file(container_id, file_id)` | Artifacts out |

## Specification

<https://github.com/HarnessRouter/harnessrouter/tree/main/protocol>

Apache-2.0.
