@zephyrcloudio/harness-client
Typed Effect Client for the Ze Harness v1 protocol.
Use network Fetch with baseUrl, or inject Fetch for an in-process handler:
import { createClient } from '@zephyrcloudio/harness-client';
const client = createClient({ baseUrl: 'http://127.0.0.1:4317' });
Every session call addresses one harness server. Local hosts serve only the default server, so the option can be omitted there; remote callers pass the target server explicitly:
import { ServerName, createClient } from '@zephyrcloudio/harness-client';
const remote = createClient({
baseUrl: 'https://harness.example',
server: ServerName.make('my-server'),
});
Pass onServerVersion: ({ protocolVersion, harnessVersion }) => { ... } to observe the
Ze-Harness-Version response header, such as 1/0.6.0. The callback runs for the first valid
value and each subsequent change, on ordinary responses and newly opened SSE streams. No
handshake request is added and no response body is consumed to obtain the version. Missing or
malformed headers are ignored. Callback exceptions and protocol/version differences do not
reject, retry, or cancel API calls; the observer is for application notices, not enforcement.
An existing SSE response keeps its original headers until the next connection.
Client methods return Effects. watchTurn and watchSession validate SSE progress, reconnect where
safe, apply text deltas and tool-activity replacements, and reconcile gaps from authoritative
snapshots. Tool activity checkpoints must advance monotonically and match the requested tool part.
The Client does not import Core or Server.
watchSession resumes transport failures from its last safe activity cursor. Before each retry it
emits session.reconnecting with the error and delay, and it emits session.resumed once the
resumed stream catches up. Servers repeat the cursor while idle, so the watch also treats three
missed heartbeats (SESSION_EVENT_HEARTBEAT_MILLIS × 3) without any event as a lost connection.
When a reconciliation read overtakes queued stream snapshots or deltas, the Client keeps the newer
state and consumes the covered revisions without moving the Turn backwards.
watchTurn reconnects after transport and server (5xx) failures and resumes after its last known
revision. While a Turn is quiet, servers send turn.heartbeat and never leave a gap longer than
TURN_EVENT_HEARTBEAT_MILLIS, so the watch treats three such intervals
(TURN_EVENT_HEARTBEAT_MILLIS × 3) without any event as a lost connection. Heartbeats are not
forwarded to onEvent.
On both watches, a response body that fails mid-stream, such as a closed or timed-out connection, is
a transport failure and resumes. Malformed events are still protocol failures.
submitPrompt requires either queue delivery with a turnId, or steer delivery with a steerId and
expectedTurnId.
client.watchServer({ afterServerCursor?, onEvent? }) watches every Session on the server from one
stream. It pages no history. Without afterServerCursor it starts live: subscribe first, read the
current cursor from server.ready, then bootstrap with listSessions() and, per Session,
listDescendantPendingApprovals and listDescendantPendingQuestions; events that arrive during the
bootstrap are upserts over it. Pass afterServerCursor instead to replay every stamped row after a
cursor observed earlier (zero replays everything). It emits server.ready on the first event, carrying
the cursor the server announced, session.created (a Session snapshot, repeatable after a resume,
so upsert by session.id), session.title, session.deleted, session.turn.updated (the
authoritative Turn, reconciled through getTurn when a durable row names state the watch does not
hold, carrying the serverCursor of the event that produced it; a 404 or 409 on that read means
the Session or Turn is gone or closing for deletion, so the Turn is dropped and the stream
continues until its session.deleted arrives), server.activity when the cursor advances past rows that produced no
event, and the same
server.reconnecting and server.resumed lifecycle as watchSession, with the same idle timeout
(SERVER_EVENT_HEARTBEAT_MILLIS × 3) and backoff. Live text deltas are not on this stream; watch a
Session for those.
Use appendConversation(sessionId, { content }) to add durable user context without creating a
Turn or calling a model. The Client generates an entry ID unless entryId is supplied. Work already
admitted before the append does not see the entry. Pass authorId to attribute the entry to a
stable human author; receipts and reads echo it back.
Use getSessionConfiguration and revision-checked replaceSessionConfiguration for ordered Session
rules. Use replyPermission with once, always, or reject. listSavedPermissions and
removeSavedPermission manage the durable allow grants created by always.
Use forkSession(sourceId, { id?, turnId? }) to copy settled history before an optional Turn into
a new Session; an absent turnId forks the whole history. Pass through instead to copy
through a named message inclusively — a Turn message selects its whole Turn, a conversation
entry selects itself — and visibility to restrict the copy to what the forking actor may
observe. The fork inherits the parent workdir,
model, system prompt, and permission rules, and records forkedFrom lineage on the new Session.
Use listSessions() for the server's Sessions oldest-first, and deleteServer() to cancel and
delete them all (workspace files stay). Pass workdir to createSession for a per-Session
working directory; the server root is the default. Use listSessionChildren(sessionId) for a
Session's spawned children, promoteSessionBackground(sessionId) to detach its blocking
foreground sub-agent chains, and capabilities() to report host feature flags.
Model discovery and provider management
The client provides typed discovery and connection management:
client.getRuntimeModels()andclient.watchRuntimeModels(): fetch the runtime model snapshot, or take a single-shot current-revision check.client.getSessionModels(sessionId)andclient.watchSessionModels(sessionId): fetch the snapshot a Session adopted, or take a single-shot current-revision check.client.listProviders()andclient.getProvider(providerId): list and inspect safe provider summaries and connection status.client.connectProviderKey(providerId, { apiKey, label? }): connect a provider API key atomically.client.updateProviderConnection(providerId, connectionId, { label }): update a connection's label.client.deleteProviderConnection(providerId, connectionId): delete a stored connection.
The watch*Models streams are single-shot, not long-lived subscriptions: the server emits the
current revision once and closes. Re-call to re-check, and use the get* snapshot calls for fresh
catalog data.
Public TypeScript exports
AddSessionPluginRequestAgentIdAppendConversationInputAuthorIdCatalogSnapshotClientErrorClientIdClientWorkdirClientWorkdirValueCompleteMcpCredentialCallbackInputContextCompactionIdContextCompactionOutcomeContextCompactionReceiptContextCompactionSnapshotConversationContentConversationEntryIdConversationEntryReceiptcreateClientCreateClientOptionsCreateSessionInputForkSessionInputHARNESS_PROTOCOL_VERSIONHarnessClientHarnessFetchimageBase64PromptImageMimeTypeImageReferenceimageUrlPromptListPluginJournalOptionsListSessionCompactionsOptionsListSessionTurnsOptionsMCP_CREDENTIAL_AUTHORIZATION_REQUIRED_REASONSMCP_CREDENTIAL_CALLBACK_DENIED_REASONSMCP_CREDENTIAL_REVOCATION_STATUSESMCP_PROTOCOL_VERSIONModelRefPendingDescendantApprovalPendingDescendantQuestionPermissionReplyPermissionRequestPermissionRequestIdPluginJournalCursorInputProjectIdPromptContentPromptContentPartQuestionAnswerQuestionInfoQuestionOptionQuestionRequestQuestionRequestIdReasoningEffortSavedPermissionSavedPermissionIdSERVER_VERSION_HEADERServerActivityCursorServerNameServerVersionServerWatchEventSessionSessionActivityCursorSessionAgentEntryValueSessionAgentsResponseValueSessionCompactionHistoryCursorSessionCompactionStateSessionConfigurationSessionConfigurationValueSessionHistoryCursorSessionIdSessionWatchEventSessionWorkdirSkillIdSkillListEntryValueSkillSelectionsniffImageMimeStartMcpCredentialInputSteerIdSteerReceiptSubmitContextCompactionInputSubmitMcpPromptInputSubmitPromptReceiptSubmitPromptRequesttextPromptToolPermissionRequestTurnIdTurnSnapshotTurnWatchEventWatchServerOptionsWatchSessionOptionsWatchTurnOptions