harnessclientprotocol
v0.1.1
Published
The client half of the Unified Harness Protocol without the HTTP: SSE decoding, dropped-event detection, output assembly, idempotency keys, and retry policy.
Maintainers
Readme
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 harnessrouterjs, which is this
plus HTTP.
npm install harnessclientprotocolDecode a stream
import { readEventStream, TaskAccumulator } from 'harnessclientprotocol';
const res = await fetch('http://localhost:3000/v1/responses', {
method: 'POST',
headers: { 'content-type': 'application/json', accept: 'text/event-stream' },
body: JSON.stringify({ input: 'Summarise README.md', stream: true }),
});
const task = new TaskAccumulator();
for await (const event of readEventStream(res)) {
task.add(event);
if (event.type === 'response.output_text.delta') process.stdout.write(event.delta);
}
task.status; // 'completed' | 'failed' | 'incomplete' | 'cancelled'
task.text; // the assistant's text, from the terminal event where available
task.toolCalls; // the tools the agent invoked
task.annotations; // artifact citations — the files it wrote
task.sequence; // { ok: false, missing: [7] } when the stream dropped an eventTaskAccumulator 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
import { idempotencyKey, isRetryable, backoffMs } from 'harnessclientprotocol';
const key = idempotencyKey(); // attach to every attempt of the same task
isRetryable({ code: 'session_busy' }); // true — wait for the in-flight task, then retry
isRetryable({ code: 'quota_exhausted' }); // false — nothing changes until the quota does
backoffMs(2, { retryAfterSeconds: 30 }); // 30000 — the server's own answer winsReuse 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 |
| readEventStream(response) | Async iterator of events over a fetch body |
| TaskAccumulator | Stream → the state a user interface actually needs |
| outputText(response) | Assistant text from a finished response object |
| artifacts(response) | The files a task produced, from the message annotations |
| idempotencyKey() | A fresh key for a task and all its retries |
| backoffMs(attempt, opts) | Exponential backoff with full jitter, deferring to Retry-After |
| isRetryable(error), checkSequence(events) | Re-exported from unifiedharnessprotocol |
Specification
https://github.com/HarnessRouter/harnessrouter/tree/main/protocol
Apache-2.0.
