SDKs and the control-plane API
This page is for people building on top of a Kheish daemon: application backends, automation, internal tools, sidecars. If you run the daemon itself, read Running Kheish in production — this page assumes someone else owns the process and you have a base URL and (usually) a token. The mental model is short and worth stating up front:
The Kheish daemon is the durable orchestration engine. The SDKs are a thin
integration layer over its HTTP control plane. Nothing an SDK does is magic —
every call maps to a documented /v1/... endpoint, and everything an SDK
“remembers” is actually durable state inside the daemon.
That has a practical consequence you will lean on constantly: an SDK client is
disposable, a session is durable. You can create a client in one process,
submit a run, lose the process, reconnect from a different process or a different
language entirely, and pick the same session and run back up by id. The daemon is
the source of truth; the SDK is a typed window onto it.
Environment conventions
Every SDK reads the same two environment variables, and every SDK has afrom_env constructor that is the intended entrypoint:
KHEISH_BASE_URL— the daemon control-plane URL. Defaults tohttp://127.0.0.1:4000.KHEISH_TOKEN— the bearer token. Optional: only needed when the daemon has control-plane auth enabled (which it should for anything non-loopback).
status and inspect resources; it cannot
mutate. Sensitive read paths — raw asset bytes, run debug artifacts, brokered
runtime-auth subject/lease details — require the admin token when auth is
enabled. Give your integration the narrowest token that does its job.
There is no separate “API key” concept for the SDK: authentication to the daemon
is the bearer token; authentication from the daemon to model providers is
handled inside the daemon via auth_ref slots you never see over the wire.
A note on wire shape that saves debugging time: the daemon speaks snake_case
JSON, and the SDKs preserve it. The TypeScript SDK explicitly targets the
daemon’s snake_case fields (output_text, run_id, session_id), so do not
expect camelCase in payloads even in the TS client.
The four SDKs
All four wrap the same control plane. They differ in ergonomics, not in capability, and each keeps araw/Raw()/RawClient escape hatch for daemon
fields the typed surface has not wrapped yet. The quickstarts below are copied
faithfully from the official kheish-sdks READMEs.
Python — kheish-sdk
The Python SDK exposes a resource-based client with both synchronous and
asynchronous variants. The high-level path is client.responses.create(...).
raw:
client.responses— high-level prompt/files/assets/boards facadeclient.sessions— durable session lifecycle, persona defaults, route policy, reply targetsclient.runs— run inspection, debug bundles, cancellationclient.approvalsandclient.questions— the explicit human-loop control planeclient.tasks— daemon task inspection and stop controlsclient.assets— upload, download, saveclient.personas— create, update, inspectclient.schedules— create, trigger, pause, resume, cancelclient.agentsandclient.mailboxes— sidechains, mailbox posting, nickname managementclient.runtime,client.secrets,client.auth_accounts,client.connectors— runtime adminclient.playbooksandclient.flows— playbook lifecycle and flow executionclient.learningandclient.capture— daemon learning state and capture provisioningclient.channels,client.projects,client.boards— collaborative control-plane resourcesclient.observationsandclient.derivations— durable input pipelinesclient.eventsandclient.raw— global event stream and escape hatchExternalDaemonClient— for Python sidecars using the external connector ingress contract
kheish.models and kheish.params are no longer shipped.
TypeScript — @kheish/sdk
A thin SDK for both browser and server runtimes, targeting Node 20+ and ESM.
responses.create(...) returns a run handle,
run.wait({ raiseOnFailure: true }) blocks until terminal and returns a result
whose output_text is the assistant’s text. Because it runs in browsers too, it
respects the daemon’s loopback-only CORS policy — if you call the daemon directly
from a browser, the browser origin must be an exact loopback origin the daemon
accepts (for example http://localhost:5173), otherwise serve the UI
same-origin behind your gateway.
Go
Idiomatic Go: every method takes acontext.Context, common requests use small
fluent builders, and Raw() remains the escape hatch.
KHEISH_BASE_URL (default http://127.0.0.1:4000) and
KHEISH_TOKEN (optional bearer token). Note the Idempotency(...) builder — Go
and Rust favour the explicit session-first submit path with idempotency keys,
which is exactly what you want in a service that might retry.
Rust
Async by default (tokio), with an optional blocking feature for synchronous
code.
blocking:
RawClient and
serde_json::Value remain available for new daemon fields.
Choosing an SDK
| If you are… | Reach for | Because |
|---|---|---|
| Writing a Python service, notebook, or automation | Python | sync + async clients, the widest resource surface, responses.create facade |
| Building a browser UI or a Node backend | TypeScript | ESM, runs in browsers (respecting loopback CORS), same snake_case wire |
| Writing a Go service that retries | Go | context-first, fluent builders, explicit idempotency keys |
| Embedding in a Rust app or another daemon-adjacent tool | Rust | async by default, optional blocking, typed builders |
| Bridging an external connector sidecar | Python ExternalDaemonClient | implements the external connector ingress contract directly |
The fire-and-wait pattern
The most common integration is “send one prompt, wait for one answer.” That is whatresponses.create(...) plus run.wait(...) is for. Under the hood,
responses.create submits input to a session and hands you a run handle; wait
polls (or streams) the run until it reaches a terminal status and then returns
the result.
Python:
raise_on_failure=True (or { raiseOnFailure: true } in TS) turns a failed
run into an exception instead of a result you have to inspect. Use it when a
failed run should abort your code path; omit it when you want to branch on the
outcome yourself.
Two things every fire-and-wait integration should set:
- A route —
Route.openai("gpt-5.4")etc. If you omit it you get the daemon’s default route, which is fine for demos but surprising in production when an operator changes the daemon default. - An idempotency key, especially in a service that retries. The Go/Rust
submit path takes one directly (
Idempotency("…")/.idempotency_key("…")); the daemon uses it to collapse duplicate submissions instead of running twice.
The session-first pattern
Fire-and-wait implicitly uses a session (thesession="demo" argument). When you
want durable identity, a persona, a route policy, and reply-target defaults that
persist across many runs, create the session explicitly first, then submit runs
against it.
- Personas are snapshotted at bind time. The session captures the persona as
it was when bound. Updating the persona record later does not retroactively
change an already-bound session. This is deliberate — it keeps a long-running
session stable — but it means “I updated the persona and nothing changed” is
expected, not a bug. Rebinding a persona is a session mutation and only allowed
while the session is idle (a rebind on a busy session returns
409). - Route policy persists. The session’s route becomes the default for its runs, so you set it once instead of on every call.
- Reply targets persist. Set the session’s reply-target defaults once and every run’s output routes there (see webhooks). Reply-target edits are prospective — a run snapshots its reply targets at submission, so changing the session default does not re-route an in-flight run.
- Capability and credential scopes live on the session. The Go/Rust
quickstarts show
AllowSkill(...)/allow_route(...)— these narrow what the session’s runs can reach, fail-closed.
Human in the loop
Agent runs can pause to ask the operator a structured question or to request approval before a risky action. An integration that ignores these will see runs that “hang” — they are not hung, they are waiting for you. The SDKs expose an explicit human-loop control plane so you can detect the wait, resolve it, and let the run continue.- Questions can be multiple-choice or free-text. The loop above answers option-bearing questions with an option id list and free-text questions with a string. Real integrations surface these to a human UI instead of auto-answering.
- Approvals and questions are durable. They survive a daemon restart — the
run stays in
waiting_for_approval/waiting_for_user_questionand resumes when you resolve it. Your integration can crash and reconnect and the pending work is still there. - A shell-heavy run may need more than one approval wave. Do not assume a
single
approve_allunblocks everything; loop until the run reaches a terminal status. - Timeouts are yours to own.
wait_until_blocked(timeout=…)bounds how long you wait for the run to reach a blocked-or-terminal state; it does not put a deadline on the human.
GET /v1/sessions/{id}/approvals, GET /v1/sessions/{id}/questions, and the
matching resolve endpoints (see
Questions and approvals API).
The run wait/poll lifecycle
Because integrators live and die by run states, here is the full state machine the daemon enforces.run.wait(...) and wait_until_blocked(...) are convenience
wrappers over this; if you poll GET /v1/runs/{run_id} yourself, this is what you
are watching.
The statuses, verbatim from the run model:
queued— submitted, waiting for a slotrunning— executingwaiting_for_approval— paused on a pending approvalwaiting_for_user_question— paused on a pending structured questioncompleted— finished successfully;outputspopulatedfailed— finished with an error stringinterrupted— interrupted (for example, a normal input run displaced during restart repair, or an explicitsessions interrupt)cancelled— cancelled viaruns cancel
- The lifecycle only moves forward. Normal execution goes
queued → running, thenrunning →one of the waiting or terminal states, and waiting runs resume torunningor settle terminally.running → queuedis reserved for daemon restart recovery of replayable daemon-owned work. An ordinary API mutation that tries to rewind lifecycle state returns409application/problem+jsonwith domainrunsand coderun_state_conflict. Do not try to force a run backwards. GET /v1/runs/{run_id}is the source of truth for the route and model actually used.RunView.request(a compactRunRequestSummary) exposes the resolvedprovider(route selector) andmodel. Do not infer the model from what you requested; read what the run used.- One session runs one thing at a time. Submitting
/inputwhile a run is active or queued returns409with domainsessions, codesession_busy. Use the detached/runssubmission (what Go/RustSubmitRundoes) when you want work to queue behind existing work instead of being rejected.
Poll vs. stream
You have two ways to watch a run. Poll when you want simplicity; stream when you want a live operator view without a polling loop.- Data events carry monotone decimal-string ids. Typed
heartbeatkeepalives are id-less and do not advance the reconnect cursor — do not treat a heartbeat as progress. - Reconnect with
Last-Event-IDor?cursor=<event_id>; if both are present the larger numeric cursor wins. - The daemon emits typed
stream_gapnotices when your cursor is older than the bounded replay window, predates the current event-id epoch after a restart, or you fell behind as a slow consumer. Gap payloads carryskipped,reason,scope,skipped_is_estimate, and an optionalresume_after_id. Handle gaps by reconciling with aGETlist/get rather than assuming you saw every event. - The CLI streaming commands abort if one SSE frame exceeds 1 MiB; use list/get endpoints for bulk output reconciliation.
GET /v1/runs supports session_id,
limit (clamped to 100), and priority_active (sorts active/pending work
first). See
Sessions and runs API.
Structured input and output contracts
Free-text prompts are fine for conversational work. For machine-to-machine integration you often want a guaranteed shape in both directions: a strict input schema the daemon validates before running, and a strict output schema the daemon repairs the model output toward. Kheish supports both as per-session contracts. The contract is set once per session as a JSON Schema (a strict daemon-supported subset), and then every run through that session is validated (input) or coerced (output) against it. Set the contract once per session with a schema PUT:Setting a contract
The schema goes in viaPOST/PUT to the contract endpoint. The daemon parses
your JSON Schema into its strict internal StructuredFieldSchema. If your schema
uses a feature outside the supported subset, you get 400 with domain
sessions and code output_contract_schema_unsupported (or the input analogue)
— fix the schema, do not retry blindly.
GET reads the current contract,
POST/PUT sets or replaces it, DELETE clears it.
Running against a contract
Once an input contract is set, the run payload must match the schema or the daemon rejects the run — so you send canonical JSON instead of free prose. Once an output contract is set, the daemon returns canonical JSON matching the output schema, repairing the model’s output up tomax_repair_attempts times
before failing. This gives you a genuine typed boundary: you send structured
input, you get structured output, and the daemon enforces both.
This is exactly what the Kheish Air “Use via SDK” snippet does when a session has
an input contract — it fetches the schema and generates a payload skeleton so the
snippet passes validation instead of sending free text. See
the Use via SDK button.
For provider-neutral input beyond a single prompt string, use input_items —
the preferred input surface — with text, asset_reference, board_reference,
and inline_asset variants. See
Sessions and runs API.
Webhooks as reply targets
An integration often wants the daemon to push results to it rather than polling. Reply targets are how: attach an HTTP reply target to a session (or a run, or a connector), and the daemonPOSTs the assistant output there as real
JSON when the run produces output.
The JSON body the daemon delivers to an HTTP reply target contains real,
structured output — this is not an opaque blob:
session_idandthread_idcontent— the plain-text fallbackparts— the structured output parts (text, attachment, …)resolved_parts— parts with attachments resolvedartifactsandartifact_attachments— daemon-owned artifact references emitted with the outputmetadata
- Delivery is durable and retried. A reply target that returns
429is retried with backoff (the daemon honoursRetry-After). Repeated failure dead-letters the delivery, which you can inspect and replay (GET /v1/deliveries/dead-letter,POST /v1/deliveries/{id}/replay). - Reply targets snapshot at run/task creation. Editing the session default is prospective — an already-submitted run keeps the targets it captured. So a webhook change does not retroactively re-route in-flight work.
- Operator delivery views are redacted.
GET /v1/deliveriesexposes the plugin, a target digest, attempts, safe error codes, timestamps, counts, and therun_id— but not raw reply addresses or payload metadata. Your webhook URL does not leak through the operator delivery API.
Assets and multimodal input
Many integrations need to send the agent a file — a document to review, a screenshot to describe, a spreadsheet to parse — and get file output back. Kheish models these as daemon-owned assets, and the SDKs expose an assets resource plus multimodal input items so you rarely hand-roll the wire format. There are two ways to get a file into a run:- Inline — embed the bytes in the run payload as an
inline_assetitem (or a legacyattachmentsentry). The daemon materializes an asset from it. Inline assets require both a non-emptyfile_nameand non-emptycontent_base64. - Reference — upload the asset once via
POST /v1/assets, get anasset_id, and reference it from many runs with anasset_referenceitem. Upload once, reuse many times.
input_items is the preferred input surface precisely because it is
provider-neutral and ordered — you control exactly how text and files interleave.
The four item variants are text, asset_reference, board_reference, and
inline_asset.
On the output side, assistant output is stored as daemon output records whose
parts can include attachment parts and whose artifacts list references
daemon-owned assets by id. The built-in emit_output tool is how a run emits
explicit structured output with artifacts; a normal assistant message becomes a
daemon output automatically. To pull an artifact’s bytes back down, fetch
GET /v1/assets/{asset_id}/raw (a raw-bytes endpoint that needs the admin token
when auth is enabled). See Assets API for the upload
payload, reference counting, and garbage-collection endpoints.
Scheduling recurring work
A large class of integrations is “run this agent on a schedule” — a nightly triage, an hourly digest, a periodic sync. You do not build a cron loop in your service; you create a durable schedule and the daemon fires it.request— a full session input request fired at each occurrence.observation_materialization— materialize captured observations on a cadence.flow_start— start a referenced Playbook Flow at each fire; the daemon records the root run as scheduled work.
- Cron timezone defaults to
UTC. Set it explicitly if you mean local time. - Recurring
flow_startschedules derive a uniqueflow_idandidempotency_keyper fire. Explicit identifiers are only accepted on one-shot schedules; a one-shot with neither derives both, and supplying one preserves it without synthesizing the other. - When a nested schedule request uses only
input_items,contentcan be omitted.
GET /v1/schedules/{id}, and the pause, resume,
cancel, and trigger sub-routes (the Python SDK mirrors these as
client.schedules.create / trigger / pause / resume / cancel). trigger fires a
schedule now without waiting for its next occurrence — handy for testing an
automation without waiting a day. See
Tasks, schedules, and agents API.
Idempotency, retries, and duplicate submissions
A service that submits runs will, eventually, submit the same run twice — a retry after a timeout, a redelivered message, a double-click. The daemon gives you a clean at-most-once story through idempotency keys, and every SDK’s submit path takes one.- Python/TypeScript
responses.createaccepts an idempotency key; Go’sNewRunInput(...).Idempotency("order-8842")and Rust’sSubmitRunRequest::new(...).idempotency_key("order-8842")set it explicitly. - Derive the key from something stable in your domain — an order id, a message id, a webhook delivery id — not from a random value, or retries will not collapse.
- Idempotency composes with the run state machine: a retry that lands after the
original completed returns the completed run, so your
waitsees the real result rather than starting a duplicate.
| Symptom | Retry? | How |
|---|---|---|
| Network error / 5xx before a response | yes | same idempotency key; the daemon collapses duplicates |
409 session_busy | yes, later | the session is running; use detached /runs to queue, or wait for idle |
409 run_state_conflict / revision conflict | re-read then retry | your view is stale; fetch current state first |
400 bad payload / unsupported contract schema | no | fix the request; a retry fails identically |
404 unknown id | no | wrong id or wrong state root; do not spin |
Run reached failed | maybe | it is a real failure with an error string; decide in domain logic, then resubmit with a new key if appropriate |
409 conflicts with the same key;
never retry a 400/404; treat a failed run as a domain decision.
Sidechains, agents, and mailboxes
Beyond one session answering one prompt, the daemon supports multi-agent patterns. Integrators reach for these when a run needs a specialist to do a bounded sub-task, or when several sessions collaborate.- Agents / sidechains (
client.agents) let a run spawn a bounded child agent for a sub-task and get a structured result back — a reviewer, a researcher, a checker — without the parent losing its own context. Spawns are bounded by a daemon-owned spawn-policy quota (recorded inspawn-policy-ledger.json), so a runaway fan-out cannot exhaust the daemon. - Mailboxes (
client.mailboxes) are how sessions post messages to each other and manage nicknames. A mailbox post is accepted (202) and delivered as amailbox_deliveryrun in the target session. - Channels and projects (
client.channels,client.projects) are the collaborative surfaces: public conversations with turn leases and thread-work, and daemon-owned projects with members, linked channels, and project tasks.
Bridging an external connector sidecar
Sometimes your integration is not a client of the daemon but a transport into it — a bridge that turns platform events (a webhook from some SaaS, a chat message, a domain event) into daemon sessions and runs. That is the external connector ingress contract, and the Python SDK shipsExternalDaemonClient for
exactly this.
Key facts for building one:
- The ingress routes (
POST /v1/connectors/external/{name}/eventsand/events/batch) sit outside the control-plane auth middleware — they are authenticated by the connector’s ownshared_token, not the admin token. If no shared token is configured,allow_unauthenticated_ingress=truemust be explicit. - A connector that creates sessions dynamically must opt in with
session_policy={"create_if_missing": true}in its runtime connector config; otherwise it can only route to existing sessions. - The bundled Python sidecar helpers submit ingress
protocol_version = 2by default and fall back to1when talking to an older daemon, so you do not hand-manage the protocol version. remote_httpexternal connectors reject loopback, private, link-local, and metadata-network targets by default (SSRF hardening); setallow_private_network=trueonly for trusted local sidecars, and note the daemon never follows redirects and re-validates resolved addresses on every fetch.
A guided tour of the control-plane API
The SDKs cover the common paths; the full per-endpoint reference lives under Control plane API and its per-domain pages. This section is a map, not a spec: it tells you which domain owns which job so you know where to look (or whichraw path to call). Everything under /v1/ is
control plane and sits behind the auth middleware; connector ingress and
observation upload ingress are mounted separately, on purpose.
Control plane /v1 (behind auth middleware):
sessions— durable containers: create, input, route policy, capability/credential scope, reply targets, persona binding, input/output contracts, memory context/searchruns— execution attempts: list, get (source of truth for route+model used), cancel, debug bundles, external actionsquestions & approvals— the human-loop control planetasks— background daemon tasks: list, output, stopschedules— durable schedules + wakeups: create, trigger, pause, resume, cancellearnings & memory— learning-candidates, learnings, revoke-matching, learning-skills (promoted procedural skills)runtime—/v1/status,/v1/capabilities,/v1/runtime, learning-policy, secrets, auth subjects/leases, hooks, debug-level, connectors/external/metrics, routes overlayconnectors— runtime connectors + ingress definitions (http/slack/telegram/external)assets— daemon-owned assets and documents (upload/download/raw)boards— immutable visual revisionspersonas— persona records + session persona bindingchannels— public conversations, members, messages, leases, stimuli, thread-workprojects— projects, members, linked channels, project tasksplaybooks & flows— playbook lifecycle + Flow execution/streamingobservations & derivations— observation sources, observations, materializations and durable derivationsdocs—GET /v1/docs— the daemon’s own documentation manifestlogs—GET /v1/logs— bounded ring of daemon tracing eventsstreams— SSE: events, sessions/, runs/, flows/
- connector ingress —
/v1/connectors/{http,slack,telegram}/{name},/v1/connectors/external/{name}/events[/batch],/v1/connectors/external/{name}/credentials/{key} - observation upload —
/v1/observation-sources/{id}/observations(source-scoped bearer token, not the admin token)
- Sessions and runs — the core. Create sessions, submit input or detached runs, inspect run status/route/model, fetch debug bundles, read the derived memory-context view. Reference: Sessions and runs API.
- Schedules — recurring or one-shot work:
request,observation_materialization, orflow_startpayloads. Recurring schedules derive a per-fire id and idempotency key. Reference: Tasks, schedules, and agents API. - Learnings and memory — the daemon’s durable learning store and promoted procedural skills, plus the automatic learning policy. Reference: Learnings API.
- Runtime — the operator surface: status, capabilities, route/model, secrets, brokered auth subjects/leases, hooks, debug level. Reference: Runtime API.
- Connectors — transports the daemon owns and the ingress the daemon accepts. Reference: Connectors API.
- Assets — the daemon-owned asset model behind attachments and outputs. Reference: Assets API.
- Docs and logs —
GET /v1/docsreturns the daemon’s documentation manifest;GET /v1/logs?limit=Nreturns the bounded ring buffer of daemon tracing events (default 1,000, ring capacity 5,000) that powers the Kheish Air logs explorer.
Status codes you will actually handle
The API returns concrete views, not a generic envelope. The status codes worth building around:| Code | Meaning | What to do |
|---|---|---|
200 | read or in-place mutation succeeded | proceed |
201 | resource created (persona, session, schedule) | keep the returned id |
202 | detached run submitted / mailbox post accepted | poll or stream the run |
400 | invalid payload, malformed multimodal input, bad route combo, unsupported contract schema | fix the request; do not retry unchanged |
404 | unknown session/run/persona/asset/observation/connector | check the id and the state root you are talking to |
409 | state conflict: rebinding while a session is not idle, session_busy, run_state_conflict, runtime revision conflict, duplicate persona id | back off, re-read state, retry when idle |
409 is almost always “you are racing the daemon’s state machine” — read the
application/problem+json body’s domain and code, wait for the right state, and
retry. It is not a transient network error to blindly retry.
Use via SDK in Kheish Air
Kheish Air (the web console) has a “Use via SDK” action on every session page and on the playground bench. It answers the question every integrator asks — “how do I talk to this agent from my code?” — by generating a ready-to-paste snippet. What the generated snippet gives you:- The right install line and env vars for the chosen language
(
pip install kheish-sdk,npm install @kheish/sdk,go get github.com/kheish/kheish-sdks/go,cargo add kheish), withKHEISH_BASE_URLprefilled from your console connection settings and a reminder thatKHEISH_TOKENis only needed when the daemon requires auth. - The idiomatic call for that language — Python/TypeScript use
responses.create+wait, Go and Rust use the session-firstSubmitRunpath with an idempotency key — all targeting the exact session you clicked from. - Contract-aware payloads. If the session enforces a structured input
contract, the snippet includes a payload skeleton derived from the schema
(numbers become
1, booleanstrue, strings"<field>", arrays[], nested objects recurse) plus a comment warning that the payload must match the schema or the daemon rejects the run. If there is no contract, it sends a plain example prompt.
kheish-sdks READMEs, so the code
the console hands you is the same code documented on this page — copy it, replace
the placeholders, and it runs against the session you were looking at.
FAQ
Do I need a token? Only when the daemon has control-plane auth enabled — which it should for anything reachable beyond loopback. SetKHEISH_TOKEN. On a purely loopback
daemon it can be omitted. Use the read-only token for read-only integrations;
sensitive reads (raw assets, debug artifacts, auth subject/lease details) need
the admin token.
Why is my run “hanging”?
It is probably waiting_for_approval or waiting_for_user_question, not hung.
Use wait_until_blocked (or read the run status) and resolve the approval or
question. Approvals and questions are durable across restarts, so the wait
persists until you answer.
I submitted input and got a 409 session_busy.
A session runs one thing at a time; /input is rejected while a run is active or
queued. Use the detached /runs submission (Go/Rust SubmitRun) to queue work
behind the active run instead.
I updated a persona and the session did not change.
Expected. The session snapshotted the persona at bind time. Rebind the persona
(only while the session is idle) if you want the new version.
The model I requested is not the model that ran.
Read GET /v1/runs/{run_id} — RunView.request reports the provider (route)
and model actually resolved for that execution. The daemon default route can
differ from what you passed if you did not pin a route.
Which input surface should I use — prompt, content, or input_items?
input_items is the preferred provider-neutral surface (text, asset/board
references, inline assets). prompt/content are the simple text path.
reply_plugin/reply_address are compatibility fields — prefer reply_targets.
How do I get results pushed to me instead of polling?
Attach an HTTP reply target to the session; the daemon POSTs structured JSON
(session_id, content, parts, artifacts, …) to your webhook when output is
produced, with durable retries and a replayable dead-letter queue.
How do I stream instead of poll?
GET /v1/runs/{run_id}/stream (SSE). Reconnect with Last-Event-ID or
?cursor=, ignore id-less heartbeats for progress, and handle stream_gap
frames by reconciling with a GET.
Is there an escape hatch for endpoints the SDK hasn’t wrapped?
Yes — client.raw (Python), Raw() (Go), RawClient (Rust), and the daemon’s
snake_case JSON. The typed builders cover the common paths; raw covers the
rest.
Related reading
- Running Kheish in production — the operator side of the same daemon: state root, backups, upgrades, observability.
- Sessions and runs — the execution model your calls drive.
- Architecture — how the daemon, SDKs, and connectors fit together.
- Security — trust zones, tokens, and CORS boundaries.
- Quickstart — from zero to a first run.
- Reference: Control plane API, Sessions and runs API, Runtime API, Questions and approvals API, Connectors API, Learnings API, Tasks, schedules, and agents API, Assets API.

