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",});Construction
Section titled “Construction”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 |
Resource groups
Section titled “Resource groups”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.
Strict SIWE
Section titled “Strict SIWE”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.
Generation-bound leases
Section titled “Generation-bound leases”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.
Durable administration jobs
Section titled “Durable administration jobs”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.
Authoritative task graph
Section titled “Authoritative task graph”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.
Related
Section titled “Related”- CLI — the
choruscommand-line client (a thinner surface than the SDK). - JSON-RPC and Transport parity matrix — what each transport exposes.