Metadata-Version: 2.4
Name: harnessclientprotocol
Version: 0.1.0
Summary: The client half of the Unified Harness Protocol without the HTTP: SSE decoding, dropped-event detection, output assembly, idempotency keys, and retry policy.
Author: HarnessRouter
License: Apache-2.0
Project-URL: Homepage, https://github.com/HarnessRouter/harnessrouter/tree/main/protocol
Project-URL: Source, https://github.com/HarnessRouter/harnessrouter
Project-URL: Issues, https://github.com/HarnessRouter/harnessrouter/issues
Keywords: uhp,hcp,harness,agent,sse,streaming,idempotency,retry,ai-agents
Classifier: Development Status :: 4 - Beta
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
Dynamic: license-file

# harnessclientprotocol

**The client half of the Unified Harness Protocol, without the HTTP.**

Agent tasks take minutes, arrive as a stream of fragments, and fail in ways that are sometimes
worth retrying and sometimes not. This package is the mechanics of getting that right — decoding
the event stream, noticing a dropped event, assembling text out of deltas, generating idempotency
keys, and classifying failures.

There is no network code here. Give it bytes or events; it gives you facts. Bring your own
transport, or use [`harnessrouter`](https://pypi.org/project/harnessrouter/), which is this plus
HTTP.

```bash
pip install harnessclientprotocol
```

## Decode a stream

```python
from harnessclientprotocol import SseDecoder, TaskAccumulator

task = TaskAccumulator()
decoder = SseDecoder()

for chunk in response:                     # any iterable of body chunks
    for event in decoder.push(chunk):
        task.add(event)
        if event["type"] == "response.output_text.delta":
            print(event["delta"], end="", flush=True)

task.status        # 'completed' | 'failed' | 'incomplete' | 'cancelled'
task.text          # the assistant's text, from the terminal event where available
task.tool_calls    # the tools the agent invoked
task.annotations   # artifact citations — the files it wrote
task.sequence      # (False, [7]) when the stream dropped an event
```

`TaskAccumulator` treats the terminal event as authoritative and the deltas as an optimisation,
which is the right way round: a dropped connection mid-stream should cost latency, not correctness.

## Retry without running the agent twice

```python
from harnessclientprotocol import idempotency_key, is_retryable, backoff_seconds

key = idempotency_key()                    # attach to every attempt of the same task
is_retryable(code="session_busy")          # True  — wait for the in-flight task, then retry
is_retryable(code="quota_exhausted")       # False — nothing changes until the quota does
backoff_seconds(2, retry_after=30)         # 30.0 — the server's own answer wins
```

**Reuse the key across retries.** A retry after a timeout without one runs the task a second time,
and the first may still be running, editing the same files. It is the most damaging mistake a UHP
client can make, and one header prevents it.

## Everything exported

| Export | What it does |
|---|---|
| `SseDecoder` | Incremental SSE decoding; holds partial frames until the rest arrives |
| `iter_events(chunks)` | Decode an iterable of body chunks into events |
| `TaskAccumulator` | Stream → the state a user interface actually needs |
| `output_text(response)` | Assistant text from a finished response object |
| `artifacts(response)` | The files a task produced, from the message annotations |
| `idempotency_key()` | A fresh key for a task and all its retries |
| `backoff_seconds(attempt, ...)` | Exponential backoff with full jitter, deferring to `Retry-After` |
| `is_retryable(...)`, `check_sequence(events)` | Re-exported from `unifiedharnessprotocol` |

## Specification

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

Apache-2.0.
