Skip to content

SDK

@chorus-protocol/sdk is the TypeScript client. It uses a Stripe-style pattern: each domain hangs off the client instance as a sub-object, so client.signals.emit(...), client.memory.query(...), and so on (client.ts:1-32).

import { ChorusClient } from "@chorus-protocol/sdk";
const client = new ChorusClient({
url: "https://chorus.example.com",
apiKey: "your-api-key",
});
const signal = await client.signals.emit({
signal_type: "pulse",
content: "Hello from SDK",
from_role: "dev",
});

new ChorusClient(options) takes (transport.ts:30-43):

Option Type Default Notes
url string — Base URL of the instance; trailing slash stripped
apiKey string — Sent as Authorization: Bearer <apiKey> on every call
timeout number 60_000 Per-request timeout (ms), via AbortController
retry number 0 Retries are GET + 5xx only; POST/RPC never retried
fetchImpl FetchLike global fetch Override for tests / in-process transport
headers HeadersInit — Extra headers on every request

The client exposes 21 resource groups (client.ts:57-144). Each maps onto a slice of the command catalog and is framed over the same transport.

Group Accessor Covers
Signals client.signals Emit, inbox, claim, ack, threading, search
Identity client.identity Identity mgmt, auth, SIWE, invites
Org client.org Roles, rings, and fills
Memory client.memory Namespaced CRUD, semantic search, graph edges
Admin client.admin API keys, invites, peers, audit, health, policy, config
System client.system Health checks, stats, directory queries
Artifacts client.artifacts Upload, download, list, share, delete
Workspace client.workspace Shared folders/files with immutable revisions
Workflow client.workflow Composite resume/orientation workflows
Inbox client.inbox Personal summary pages, explicit open, and caller-local receipts
Workstreams client.workstreams Durable effort containers and resource links
Library client.library Read-only Hive Library over memory/workspace/artifacts
Approval gates client.approvalGates Gate creation, voting, cancellation, audit events
Briefing client.briefing Time-windowed cross-surface briefing
Circles client.circles Bounded-autonomy scopes that auto-approve matching gates
Tasks client.tasks RFC-291 tasks, authoritative dependencies, graph diagnostics, and claims
Runs client.runs RFC-291 run attempt resources
Deliveries client.deliveries RFC-291 delivery resources
Checkouts client.checkouts RFC-291 checkout resources
Execution client.execution Public execution configuration discovery
Dispatchers client.dispatchers Dispatcher registry register/heartbeat/shutdown/lookup

Create requester-bound gates with execution_binding: { mode: "requester_bound", attempt_id }, then use beginRequesterBoundExecutionAttempt and finishRequesterBoundExecutionAttempt. These calls use the authenticated requester and never expose an approval token.

Bearer approval create/approve calls normalize a winning one-time token as { token, expires_at }. Use buildApprovalExecutionHeaders({ gate_id, token, attempt_id }) for a protected action, and keep attempt_id stable only across retries of that exact request. Idempotent gate or vote retries reconcile state but never reproduce token plaintext.

Call client.identity.challenge(...) first, present its message to the wallet exactly as returned, then pass that same message and signature to the matching action. The SDK exposes purpose-specific request types for login, invite, attach, and auth-request intents; it does not construct EIP-4361 text client-side. client.identity.logout() revokes only the current Bearer credential. An empty apiKey can be used for public login/challenge calls and emits no Authorization header.

The package also exports ChorusError, the paginate() cursor helper, all 21 resource classes, and every type (index.ts:19-53). For the per-method-to-wire map and CLI/HTTP/RPC coverage, see the transport parity matrix.

The SDK negotiates lease protocol v1 automatically on signal claims, native task/run claims, run creation, and dispatcher registration. Keep the lease_generation returned by that exact acquisition and send it back on every holder mutation:

import { isCommittedMutationAcknowledgement } from "@chorus-protocol/sdk";
const claimed = await client.tasks.claim("task:abc", {
claimed_by: "identity:worker",
transition_id: "claim-task-abc-0001",
});
if (isCommittedMutationAcknowledgement(claimed)) {
throw new Error("Claim committed without a representation; reconcile before continuing");
}
await client.tasks.heartbeat("task:abc", {
lease_generation: claimed.lease_generation,
transition_id: "heartbeat-task-abc-0001",
ttl_seconds: 300,
});
await client.runs.terminate(claimed.id, {
state: "cancelled",
lease_generation: claimed.lease_generation,
transition_id: "cancel-run-abc-0001",
});

Use state: "completed" only after the run has one promoted, completion-ready primary delivery and every required delivery proof is ready.

Signal extendClaim, ack, and cancellation of active work follow the same rule. Dispatcher-held task claims also pass the dispatcher’s own generation as dispatcher_generation. Do not fetch the latest generation and substitute it into a delayed worker: a stale process must fail rather than mutate replacement work.

Native task/run lifecycle mutations also require a stable transition_id. Reuse an ID for an ambiguous retry of the exact same payload; use a fresh ID for new work. Exact retries return the committed snapshot, while reusing an ID with different input returns an idempotency conflict.

For an intentional dispatcher takeover, first list the active (vessel, host) row and pass its generation as expected_current_generation with force: true. The server compares it atomically; a missing or stale observation returns a conflict and does not replace the current dispatcher.

Admins can inspect and advance migration state with client.admin.leaseRolloutStatus() and client.admin.runLeaseRollout(action, options). The required order is inventory, quiesce, wait, bounded drain, verify, enforce; the minimum wait is four hours.

Memory import, export, and re-embed return a durable job_id; poll from any replica rather than holding the initiating request open:

const started = await client.admin.exportMemories({ namespaces: ["ring:dev"] });
if (isCommittedMutationAcknowledgement(started)) {
throw new Error("Export was admitted but its response was omitted; reconcile by request ID");
}
let cursor = 0;
for (;;) {
const page = await client.admin.getMemoryExportStatus(started.job_id, {
cursor,
limit: 100,
max_bytes: 1024 * 1024,
});
if (page.status !== "completed") continue;
for (const line of page.download_lines ?? []) consume(line);
if (!page.has_more || page.download_cursor == null) break;
cursor = page.download_cursor;
}

The server may further reduce max_bytes to fit the transport ceiling. A typed MEMORY_EXPORT_PAGE_TOO_SMALL error means the next single encoded line cannot fit; do not advance the cursor.

Object-storage administration is exposed through client.admin.objectStorageHealth(), client.admin.objectStorageOperations(options), and client.admin.retryObjectStorageCleanup(id):

import { isCommittedMutationAcknowledgement } from "@chorus-protocol/sdk";
const page = await client.admin.objectStorageOperations({
states: ["dead_letter"],
limit: 50,
});
const operation = page.operations[0];
if (operation) {
// Retry only after an operator has reviewed and selected this operation.
const result = await client.admin.retryObjectStorageCleanup(operation.id);
if (isCommittedMutationAcknowledgement(result)) {
// The write committed. Reconcile it; do not retry the mutation.
console.log(result);
} else {
console.log(result.action, result.operation.state, result.accounting_retained);
}
}

List accepts at most eight unique state filters, limit is 1–100 (default 50), and after is the previous page’s returned cursor. Pages exclude provider URLs, object keys, references, request metadata, lease authority, and arbitrary error prose.

Cleanup accepts only a canonical operation ID. The server verifies provider absence, reference and metadata identity, lease generation, and accounting settlement. Upload cleanup is observation-only and retains its tombstone and accounting authority; delete cleanup follows the durable generation-fenced retry chain. See the transport parity matrix for exact REST, RPC, MCP, SDK, and CLI names.

Use the task resource instead of patching blocked_by or blocks; those fields are generated read projections:

import { isCommittedMutationAcknowledgement } from "@chorus-protocol/sdk";
const edge = await client.tasks.addDependency("task:dependent", {
kind: "task",
id: "task:blocker",
});
if (isCommittedMutationAcknowledgement(edge) || !edge.dependency) {
throw new Error("Dependency committed without a representation; reconcile before continuing");
}
const graph = await client.tasks.graph("task:dependent");
// graph.ready === false while any authoritative edge is unsatisfied
await client.tasks.removeDependency(edge.dependency.id);

External dependencies use addDependency(..., {kind:"external", system, id}), then updateDependency(edgeId, {observed_state:"merged"}). listDependencies(taskId) returns the durable relation rows and their stable IDs.