SDK reference
The full @kortix/sdk API surface — client methods, modules, turns, and distribution.
This page is the full @kortix/sdk API surface: every client method, the
framework-free modules, the turns helpers, and how the package ships. Use
SDK to get started and Sessions for the
session lifecycle in depth.
The client
createKortix(config) returns one client. Every method is a typed call to
the platform API. The project(id) and session(pid, sid) handles bind ids
so you never repeat them.
const kortix = createKortix({ backendUrl, getToken });
kortix.accounts; // account / team operations
kortix.accountInvites; // invite accept/decline by token alone
kortix.projects; // top-level project operations
kortix.connectors; // Connector calls scoped by an agent-minted token
kortix.project(id); // id-bound project handle
kortix.session(pid, sid); // id-bound session handle → see Sessions
kortix.github; // GitHub App install + repo linking
kortix.billing; // credits, subscription, tier, transactions
kortix.sandboxShares; // public share links for a sandbox port
kortix.transcribe; // speech-to-text
kortix.connectStatus; // easy-connect (Pipedream) status
kortix.marketplace; // public marketplace catalog
kortix.validateToken; // pasted-API-key check
kortix.config; // platform config in effect
kortix.runtime(); // OpenCode REST compatibility clientAccounts — kortix.accounts
| method | what |
|---|---|
list() · get(accountId) | accounts you belong to · one account |
create({ name }) · updateName(accountId, name) | create · rename an account |
members(accountId) · invite(accountId, input) | list members · invite one |
updateMemberRole(accountId, userId, role) · removeMember(accountId, userId) | change a role · remove a member |
invites(accountId) | pending invites |
cancelInvite(accountId, inviteId) · resendInvite(accountId, inviteId) | cancel · resend a pending invite |
leave(accountId) | leave the account |
accounts.tokens mints account-scoped API keys (kortix_pat_...). See
SDK auth for the full token model.
| method | what |
|---|---|
tokens.list(accountId?) | list your API keys |
tokens.create(input) | mint one — { accountId?, name, expiresAt?, projectId? } |
tokens.revoke(tokenId, accountId?) | revoke one |
accounts.audit is the enterprise reconstruction log. It combines authenticated
API requests with semantic session, connector, approval, and computer events.
| method | what |
|---|---|
audit.log(accountId, filters?) | list events by project, session, actor, source, outcome, request, correlation, resource, action, or time |
audit.export(accountId, filters?) | export the same filtered event stream as CSV or JSONL |
audit.webhooks.list/create/update/remove(...) | manage signed SIEM webhooks for the centralized stream |
Each event includes project_id, session_id, actor_type, source, outcome,
request_id, trace_id, and correlation_id when the action supplies them.
The API does not store request bodies, prompts, secrets, credentials, or raw
connector arguments in the centralized event. Connector events can include a
bounded argument preview that redacts credential-shaped fields and opaque data.
Account invites — kortix.accountInvites
Reached by invite token alone — the invitee may not be a member yet.
| method | what |
|---|---|
describe(inviteId) · accept(inviteId) · decline(inviteId) | preview · accept · decline an invite |
Projects — kortix.projects
| method | what |
|---|---|
list() · listForAccount(accountId) | your projects · projects in an account |
get(id) · detail(id) | summary · full detail |
create(input) · createRepo(input) | from an existing repo_url · new empty GitHub repo |
provision(input) | new project on a new Kortix-managed repo, seeded with a starter template — { name, account_id?, seed_starter?, starter_template?, marketplace_items?, source_item_id?, idempotency_key? } |
update(id, input) · archive(id) | update settings · archive |
llmCatalog(id) · modelPicker(id) | full · compact model catalog for a selector |
sandboxTemplates(id) · sandboxHealth(id) | sandbox build templates · build health |
sessions(id) · createSession(id, input?) | list visible sessions · create a session |
provision creates a new project; it does not start an existing project's
sandbox. Start a session instead — see Sessions.
Send idempotency_key when a retry is possible — a reload, a second tab, a
timeout you retried. provision mints a brand-new managed repo per call, so
without a key those all create real duplicate projects. Reuse one key for every
attempt at a single logical create and the repeats return the project the first
attempt made (201, same project_id, push_token: null). The key identifies the
attempt, not the payload — reusing one with a different name returns the first
project and ignores the new value, so mint a fresh key per distinct create.
Creating a second project with the same name and no key still works.
A repeat that arrives while the first call is still provisioning gets 409 with
code: 'provision_in_flight' rather than a project_id that call may still roll
back. Retry with the same key.
project(id).sessions.list({ scope: 'project' }) is a manager-only lifecycle
inventory. It adds accessible unavailable, warm, and soft-deleted sessions with
ownership and runtime-state metadata. Both list scopes omit every session the
caller cannot open.
GitHub — kortix.github
Account-scoped GitHub App install and repo linking, not project-scoped.
| method | what |
|---|---|
getInstallation(accountId) · listInstallations(accountId) | this account's install · installs the user can reach |
saveInstallation(input) · deleteInstallation(accountId, installationId?) | record · unlink an install |
listRepositories(accountId, installationId?) · listRepositoryBranches(...) | repos the install can see · branches and the GitHub default |
linkRepository(input) | import a repo as a project |
Billing — kortix.billing
Reads for credits, subscription, tier, and transaction history. Checkout, the customer portal, and credit purchases are Stripe flows, app-owned.
| method | what |
|---|---|
accountState(accountId?) · accountStateMinimal(accountId?) | full · minimal billing state |
transactions(params?) · transactionsSummary(params?) | history · summarized totals |
creditBreakdown(accountId?) · usageHistory(params?) | credit balance by source · usage over time |
sessionCosts.list(options?) · sessionCosts.get(sessionId, options?) | paginated session-cost records · one detailed session ledger |
tierConfigurations() | available plan tiers |
checkout.createSession(input) · checkout.confirmSession(sessionId, accountId?) | start · confirm a Stripe Checkout session |
subscription.createPortalSession(...) · subscription.cancel(...) · subscription.reactivate(...) | open the customer portal · cancel · reactivate |
subscription.scheduleDowngrade(...) · cancelScheduledChange(...) · prorationPreview(...) | schedule · cancel · preview a plan change |
credits.purchase(input) · credits.autoTopupSettings(...) · credits.configureAutoTopup(...) | one-off purchase · read · configure auto-topup |
sessionCosts.list() accepts accountId, projectId, limit, and offset.
Each row combines finalized LLM cost and billed sandbox compute cost.
sessionCosts.get() adds model usage and the discriminated LLM/compute ledger.
The list includes a reconciliation total for cost without a session.
Sandbox shares — kortix.sandboxShares
Public share links for one exposed sandbox port. Sandbox-scoped, not project-scoped.
| method | what |
|---|---|
list(sandboxId) | active share links |
create(input) | create one — { sandboxId, port, ttl?, label? } |
revoke(sandboxId, token) | revoke one |
Marketplace catalog — kortix.marketplace
Public catalog browsing, read-only — distinct from project(id).marketplace,
which installs an item onto a project.
| method | what |
|---|---|
items(options?) · item(id) · itemFile(id, path) | browse · one item · a file inside an item |
marketplaces() · featured() | all · featured marketplaces |
sources.list() · sources.add(input) · sources.remove(id) | list · add · remove a source |
The project handle — kortix.project(id)
Binds the project id; every sub-resource hangs off it.
const p = kortix.project(projectId);
await p.detail();
await p.update({ name });
await p.llmCatalog();| method | what |
|---|---|
get · detail · update · archive | read · full detail · update · archive |
llmCatalog · modelPicker · sandboxHealth | model and sandbox-build reads |
onboardingComplete | mark project onboarding done |
validateManifest(raw) | validate a kortix.yaml (or legacy kortix.toml) manifest server-side |
gitToken() | mint a fresh scoped git push token (409 for a bring-your-own repo) |
setAgentScope(agentName, scope) | bind an agent's allowed secrets and connectors |
p.tokens — project-scoped API keys
Auto-minted at session create as KORTIX_CLI_TOKEN; can also be minted by hand.
| method | what |
|---|---|
list() | project API keys |
create(input?) | mint a new one |
revoke(tokenId) | revoke one |
p.setupLinks — agent-minted setup links
A link a person opens to enter a secret or connect an app, without full project access.
| method | what |
|---|---|
requestSecret(input) · requestConnector(input) | link to collect a secret · connect an app |
p.secrets — project secrets
| method | what |
|---|---|
list() · upsert(input) | list metadata · create or update a write-only value and delivery policy |
setStrategy(identifier, strategy, options?) | change the consumer and delivery policy |
broker(identifier, request) | execute a session-authorized, policy-bound HTTPS request |
remove(identifier) | delete a secret |
setPersonal(name, value) · removePersonal(name) | set · remove a per-user override |
setGitCredential(input) | set a git auth credential |
runtime with consumer sandbox is the only policy that exposes plaintext to
the session. Managed consumers use broker. The HTTPS broker method requires a
session-scoped token and an active session handle.
p.access — members, invites, requests
| method | what |
|---|---|
list() · invite(email, role) | members with access · invite a user |
update(userId, role) · revoke(userId) | change a role · remove access |
pendingInvites() · requests() | outstanding invites · pending access requests |
resendInvite(inviteId) · revokeInvite(inviteId) · approveRequest(id) · rejectRequest(id) | resend/revoke an invite · approve/reject a request |
groupGrants() · attachGroupGrant(...) · updateGroupGrant(...) · detachGroupGrant(...) | list, grant, change, or remove an IAM group grant |
p.access.resourceGrants grants a member or group access to one resource (an
agent, a skill, a secret) instead of the whole project.
| method | what |
|---|---|
resourceGrants.list() | per-resource grants |
resourceGrants.create(input) | grant a member or group access to a resource |
resourceGrants.remove(grantId) | revoke a resource grant |
p.connectors — tool connectors
| method | what |
|---|---|
catalog() · tools() | callable Connector catalog · flattened <connector>.<action> tools |
search(query, options?) · describe(tool) | find · inspect one callable tool |
call(tool, args?) | call one <connector>.<action> tool through the server-side gateway |
uploadAttachment(content, input) | upload bytes and receive an opaque attachment handle for a later call |
list() · config(connectorId) | configured connectors · one connector's config |
create(input) | add a connector |
auth.discover(input) | preview auth from an OpenAPI spec, Postman collection, or endpoint |
remove(connectorId) · sync() | delete a connector · re-sync connectors |
setName(connectorId, name) · setSensitive(connectorId, sensitive) | rename · mark it sensitive (extra approval gating) |
setAuthorizationStrategy(slug, strategy) | select project or user connection ownership |
setCredentialMode(connectorId, mode) · setCredential(connectorId, input) | switch source · set the credential value |
policies.get(connectorId) · policies.set(connectorId, policies) | read · replace its tool policies |
connections.list() · connections.reconcile(input) | list · create/update connected accounts |
connections.updateCredential(connectionId, input) | rotate a connection credential |
connections.revoke(connectionId) · connections.activate(connectionId) | deny · restore a connection |
p.connectors.discover browses the direct-connector catalog. It is experimental and off by default — enable it per project under Customize → Feature flags → "Connectors API Discover". Easy Connect (Pipedream) remains the default connector marketplace.
| method | what |
|---|---|
discover.list(query?, cursor?) · discover.detail(id) | search OpenAPI/MCP/GraphQL/CLI entries · one entry's detail |
p.connectors.pipedream is the optional managed-OAuth path; listApps
returns OAuth apps only. Connect API-key apps directly instead.
| method | what |
|---|---|
pipedream.listApps(params?) · pipedream.connect(input) · pipedream.finalize(input) | browse the app catalog · start a connect flow · finalize it |
A connector defines the tool, provider app, authorization strategy, and
policies. connections stores its connected accounts. A session can select
one with connector_bindings: { alias: { connection_id } }. Credentials
stay encrypted and resolve per request.
connections is the only active authorization facade. The retired
authorizations and profiles names are not part of the current SDK surface.
p.policies — project policies
| method | what |
|---|---|
list() · set(policies) | the project's policies · replace the set |
p.triggers — cron and webhook automations
A trigger starts an agent action on a schedule or an inbound webhook. See Triggers for session strategy and payload templating.
| method | what |
|---|---|
list() | all triggers |
create(input) | create one |
update(triggerId, input) | edit a trigger |
remove(triggerId) | delete a trigger |
fire(triggerId) | run it now |
setActivation(paused) | pause or resume every trigger on the project |
create(input) takes { name, type: 'cron' | 'webhook' | 'monitor', prompt_template, slug?, agent?, model?, enabled?, session_mode?, session_id?, cron?, run_at?, timezone?, secret_env?, session_access? }. name and prompt_template are required.
cron/run_at are mutually exclusive (type: 'cron'); secret_env (the
webhook HMAC secret) applies to type: 'webhook'.
session_access controls who can open sessions the trigger creates. It is
{ mode: 'private' | 'members' | 'project', memberIds: string[], groupIds: string[] } and defaults to private. This policy is account-local runtime
state. It does not enter the portable kortix.yaml manifest. Updating only
session_access creates no Git commit. A pinned session keeps its own sharing
settings. Project managers can always open trigger-created sessions, including
sessions that use private or selected-member access.
p.marketplace / p.registry — installed items
Installs a catalog item's files onto the project's default branch.
registry.* is an identical alias of marketplace.*.
| method | what |
|---|---|
marketplace.list() · marketplace.install(id) | installed items · install a catalog item |
marketplace.updates() · marketplace.update(name) · marketplace.updateAll() | available updates · update one · update all |
marketplace.remove(name) | uninstall an item |
p.files — repo files (read)
Read-only access to the project's git tree. To read and write files inside a running session, use the session's file operations — see Files under Modules below.
| method | what |
|---|---|
list(options?) · read(path, ref?) | the repo tree · a file's contents at a git ref |
search(query) | search the repo |
archive(options?) · history(path) | download a tarball · a file's git history |
p.git — history
| method | what |
|---|---|
commits() | the commit log |
commit(sha) · commitDiff(sha) | one commit · its diff |
branches() · versionDiff(from, to) | branches · diff between two refs |
p.changeRequests — lifecycle and merge
A change request (CR) is how a session's work merges into the default branch.
| method | what |
|---|---|
list() · get(crId) | open CRs · one CR |
diff(crId) · mergePreview(crId) | its diff · preview the merge result |
open(input) · merge(crId, input?) | open · merge a CR |
close(crId, input?) · reopen(crId, input?) | close without merging · reopen a closed one |
requestChanges(crId, input) | record feedback, optionally delivered back to the originating session |
p.sessions — and the session handle
| method | what |
|---|---|
list() | the project's sessions |
create(input?) | create a session |
session(sid) | the session handle (same as kortix.session(id, sid)) |
The session handle is the heart of the runtime — see Sessions.
create(input) accepts connector_bindings keyed by connector-connection slug.
Each value names a connection_id. It also accepts secrets for
backend-origin secret narrowing and require_connectors for mandatory
connectors.
p.approvals — the connector approval inbox
Pending connector-gated actions awaiting a decision — backs the
permission-approval UX (APPROVE / ASK / BLOCK).
| method | what |
|---|---|
list(options?) · sessionsNeedingInput(options?) | pending approvals · sessions blocked on a decision |
resolve(executionId, decision, scope?) | approve or deny one — decision: 'approve' | 'deny', scope: 'once' | 'session' | 'session_all' |
p.gateway — LLM observability
Request logs, cost/latency rollups, budgets, and gateway API keys for this project's model traffic.
| method | what |
|---|---|
logs(opts?) · log(logId) | request log entries · one log entry |
overview(days?) · series(days?) · breakdown(days?) · sessions(days?) · errors(days?) | rollups, per-session cost, and errors over a window |
budgets() · setBudget(input) · deleteBudget(budgetId) | read · create/edit · remove a budget |
keys() · createKey(name) · revokeKey(keyId) | list · mint · revoke a gateway API key |
playground(prompt, models) | run one prompt against up to 6 models |
routing.get() · routing.set(policy) · routing.reset() | read · replace · inherit the routing policy |
routing.preview(input) | resolve a route without invoking a model |
A routing policy holds a default model, a vision model, and an ordered
fallback chain, each model attempted at most once; fallbackOn is
transient or any-error.
p.channels — Slack / email / voice
Connector surfaces that let an agent act as a Slack app, an email address, or join a realtime voice call.
| method | what |
|---|---|
slack.installation() · slack.mode() · slack.manifest() | current install · mode · app manifest |
slack.connect(input) · slack.disconnect() | connect · disconnect |
slack.getFile(url) · slack.uploadFile(input) | download · upload a file via the server proxy |
email.installation(connectorSlug?) · email.mode() | current install · mode |
email.connect(input) · email.disconnect(...) · email.updatePolicy(input) | connect · disconnect · update the send/reply policy |
voice.setBotName(name) | rename the bot in a live call |
p.modelDefaults — default model preferences
Account, agent, and project-scoped model defaults, resolved by the gateway.
| method | what |
|---|---|
get() · set(input) · clear(params) | read · set a default · clear an override |
p.setDefaultAgent — project default agent
p.setDefaultAgent(agentName) checks that the agent is declared and enabled,
then sets it as default_agent in the project's kortix.yaml. New sessions
prefer this agent unless a user picks another one.
p.updateFeatureFlag — feature flags
p.updateFeatureFlag(feature, enabled) turns one feature flag on or off for the
project. Pass enabled: null to clear the override and fall back to the
platform default. It calls the canonical PATCH /v1/projects/:id/features.
feature is one of FEATURE_FLAG_KEYS (exported from @kortix/sdk, typed as
FeatureFlagKey). The caller needs the project's project.customize.write
permission; the route answers 403 otherwise.
Every flag-gated route rejects the same way while the flag is off: HTTP 403
with { error, code: "feature_disabled", feature }. Use isFeatureDisabledError(error)
to branch on it and featureDisabledKey(error) to read the flag key — never
match on the message text.
p.updateExperimentalFeature(feature, enabled) is the deprecated alias. It
keeps calling the deprecated route alias PATCH /v1/projects/:id/experimental
so consumers pinned to an older deployed API keep working. Use
p.updateFeatureFlag in new code.
p.sandbox — templates and snapshot builds
Sandbox build config beyond sandboxHealth/sandboxTemplates on the project
handle: Dockerfile/image/warm-pool templates and their snapshot builds.
| method | what |
|---|---|
list() · snapshots() | sandboxes for this project · built snapshots |
rebuildSnapshot(slug?) · fixWithAgent() | rebuild a snapshot · ask an agent to fix a broken build |
createTemplate(input) · updateTemplate(...) · removeTemplate(...) · buildTemplate(...) | add · edit · delete · build a template |
setProvider(provider) | request a provider switch. null (or the platform default / the already-active provider) applies immediately; switching to a different enabled provider starts a durable prepare→verify→activate transition — the current provider keeps serving while the target warm image is built and verified, then activated. The return is a tagged union: kind:'project' (immediate) or kind:'preparation' (poll getProjectSandboxProviderTransition() until activated/failed) |
Escape hatch
kortix.runtime() returns the typed OpenCode REST client for the active
sandbox.
On a client created by createScopedKortix (@kortix/sdk/server), it throws
— the process-global "active" runtime is another request's sandbox in a
multi-tenant server, a cross-tenant leak. Use the session-scoped
kortix.session(pid, sid).runtime (call ensureReady() first) instead, which
resolves that session's own sandbox.
Modules
The framework-free modules behind the client facade and the React hooks. Reach for them when you need one operation without the facade, a pure helper, or a Node-only isolation layer. Each module carries a stability tier so you know what to build on.
| Tier | Meaning |
|---|---|
| Canonical | Import from the root @kortix/sdk. Use this for all new code. |
| Supported | A dedicated subpath (@kortix/sdk/react, @kortix/sdk/server). First-class, not deprecated. |
| Deprecated alias | An old subpath that still works. It re-exports code the root already exports. Import from root instead. |
| Internal | Outside semver. Do not import this in host code. |
The root entry is canonical. Every framework-free name below is importable
straight from @kortix/sdk:
import { files, getSessionHealth, getClient, authenticatedFetch, backendApi } from '@kortix/sdk';Canonical modules
| Module | What it does |
|---|---|
| Files | Workspace file operations: list, read, search, write |
| Session runtime | Health probe and preview/proxy URL builders |
| OpenCode client | The typed OpenCode REST client and its full type surface |
| Auth | authenticatedFetch and token accessors |
| Projects REST | The raw REST functions the facade wraps |
| API client | backendApi, the low-level typed HTTP client |
| Turns | Message-to-turn grouping, cost, and status math — see Turns |
| Transcripts | formatTranscript, a client-side Markdown export |
Files
import { files } from '@kortix/sdk';
const tree = await files.list('/workspace/src');
const { content } = await files.read('/workspace/README.md');
const hits = await files.findText('TODO');
await files.upload(file, '/workspace/uploads');files targets the globally active sandbox. If your host runs more than one
session at a time, call s.files on the session handle instead. It always
targets that session's own sandbox. See Sessions.
Session runtime helpers
import { getSessionHealth, isRuntimeReady } from '@kortix/sdk';
const result = await getSessionHealth();
if (result.ok && isRuntimeReady(result.health)) {
// the sandbox daemon is ready
}getSessionHealth never throws on a non-2xx status. It returns
{ status, ok, health, body } and lets you decide what a status means. The
same module exports the URL helpers that rewrite an agent's localhost output
into a reachable proxy URL: detectLocalhostUrls, rewriteLocalhostUrl,
proxyLocalhostUrl, parseLocalhostUrl, and buildWebProxyUrl.
OpenCode client
import { getClient } from '@kortix/sdk';
const client = getClient();
const { data } = await client.session.list({ limit: 100 });getClient() returns the typed OpenCode v2 compatibility client for the active sandbox,
with auth already injected. Prefer kortix.session(pid, sid).runtime, the
same client scoped to one session, over the global getClient() when your
host runs more than one session.
Auth helpers
import { authenticatedFetch, getAuthToken } from '@kortix/sdk';
const res = await authenticatedFetch(`${runtimeUrl}/kortix/health`);
const token = await getAuthToken();The token comes from the getToken function you passed to createKortix.
Most app code does not need this module — the file, session, and facade
layers already authenticate for you.
API client
import { backendApi } from '@kortix/sdk';
const data = await backendApi.get('/some/endpoint');
await backendApi.post('/some/endpoint', { name: 'x' });backendApi is the typed HTTP client every REST function builds on. Use it
only for an endpoint that has no typed wrapper yet.
Supported subpaths
| Subpath | What it does |
|---|---|
@kortix/sdk/react | React hooks — see React hooks |
@kortix/sdk/server | Request-scoped config for multi-tenant backends |
Server-side isolation
createKortix stores its config, including the token function, in one
process-wide variable. That is fine for a browser tab, a CLI, or a
single-tenant server. It is unsafe for a Node server that handles concurrent
requests for different users, because the last createKortix call wins for
every in-flight request. @kortix/sdk/server fixes this with per-request
isolation:
import { createScopedKortix } from '@kortix/sdk/server';
export async function handler(req: Request) {
const kortix = createScopedKortix({ backendUrl, getToken: () => tokenFor(req) });
return kortix.projects.list();
}createScopedKortix and runWithKortix isolate config per request with
Node's AsyncLocalStorage. Never import @kortix/sdk/server from a browser
bundle — it statically pulls in node:async_hooks.
A scoped client's top-level runtime() throws (it would resolve another
tenant's sandbox). Reach a specific session's runtime via
kortix.session(pid, sid).runtime after await s.ensureReady().
Deprecated aliases
About twenty old subpaths still work: /files, /turns, /session, /auth,
/projects-client, /api-client, /config, /event-stream,
/opencode-client, /platform-client, and more. Each one re-exports code the
root @kortix/sdk entry already exports. They stay working so no existing
import breaks, but new code should import from the root.
Internal modules
@kortix/sdk/internal/* holds the zustand stores apps/web's own runtime uses
internally — session sync state, active-runtime tracking, and reconnect
bookkeeping. This subpath is explicitly outside semver. Do not import it in
host code; the React hooks already expose the state you
need.
Turns
Plain functions that group session messages into turns and classify each
message part. Use them to build a custom chat renderer instead of the
reference one in apps/web. No React, no DOM — every export is a plain
function or type, safe to call from any host.
import { classifyPart, classifyTurn, toolInfo, toolViewModel } from '@kortix/sdk';Import every function from the root @kortix/sdk entry. The
@kortix/sdk/turns subpath still works, but it is a deprecated alias. New
code must use the root entry — see Distribution.
Classify a part
classifyPart(part) normalizes one of OpenCode's 12 wire part types into a
ClassifiedPart — a union keyed by kind. Each variant already resolves the
fields a renderer needs: tool status, parsed JSON output, image detection.
import { classifyPart, type ClassifiedPart } from '@kortix/sdk';
for (const part of message.parts) {
const classified: ClassifiedPart = classifyPart(part);
switch (classified.kind) {
case 'text':
render(classified.text);
break;
case 'tool':
render(classified.tool.title, classified.tool.status);
break;
}
}kind | shape |
|---|---|
text | { id, text, synthetic } — skip synthetic parts; they mark shell mode's synthetic prompt |
reasoning | { id, text } |
tool | { id, tool: ToolView } |
file | { id, filename?, mime, url, isImage, isPdf } |
subtask | { id, description, agent, prompt, model? } |
patch | { id, hash, files, fileCount } |
snapshot | { id, snapshot } |
agent | { id, name } |
retry | { id, attempt, message, createdAt } |
compaction | { id, auto, overflow, tailStartId? } |
step | { id, phase: 'start' | 'finish', snapshot?, reason?, cost?, tokens? } |
unknown | { raw } — a part type this SDK version does not know |
An unrecognized wire part degrades to unknown at runtime instead of
throwing. This lets an older client talk to a newer server.
A tool part classifies into ToolView:
interface ToolView {
name: string;
title: string;
status: 'pending' | 'running' | 'done' | 'error';
input?: Record<string, unknown>;
output?: string;
error?: string;
outputParsed?: unknown; // JSON.parse(output) when it parses, capped at 256KB
outputText?: string; // the raw output text, always present
}Some tools (web_search, image_search, connector calls) report
state.status: 'completed' even when their JSON body carries
success: false. classifyPart detects this and sets ToolView.status to
'error' in that case too.
classifyTurn(message) classifies every part of one assistant message and
returns a ClassifiedTurn with three fields:
parts— each part, classifiederror— frommessage.info.error, if anyisEmpty— true when the turn has no error and no part with visible content
Tool metadata
Two lookups both describe a tool. Do not confuse them.
toolInfo(name)— icon-free, returns{ label, category }.classifyPartuses this internally.ToolCategoryis'shell' | 'files' | 'search' | 'edit' | 'web' | 'task' | 'other'.getToolInfo(name, input)— icon-aware, returns{ icon, title, subtitle }for the reference tool-card UI. The subtitle comes from the tool's input, for example a file path or a search query.
toolInfo('bash'); // { label: 'Shell', category: 'shell' }
getToolInfo('write', { filePath: '/workspace/main.go' });
// { icon: 'file-pen', title: 'Write', subtitle: 'main.go /workspace' }Both functions match tool-name prefixes, so Kortix's plugin tool families
(agent_*, session_*, task_*, trigger_*, project_*, pty_*) resolve
without a registry update. An unknown tool name never throws — it falls back
to humanizeToolName(name) with category 'other'.
Tool view models
toolViewModel(classifiedTool) maps a classified tool part to a shape built
for one tool family. A UI can then render it specially instead of as a
generic JSON blob.
const vm = toolViewModel(classifiedTool);
if (vm.kind === 'shell') {
render(vm.command, vm.stdout, vm.exitCode);
}kind | shape | tools |
|---|---|---|
web-search | { query, results?, answer?, error? } | web_search, image_search |
shell | { command, stdout?, exitCode? } | bash |
file-read | { path, preview? } | read |
file-write | { path, preview? } | write |
file-edit | { path, diff?: DiffLine[] } | edit, morph_edit |
search | { pattern, matches?: SearchMatch[] } | grep, glob |
task | { description, agent? } | task |
todo | { items: TodoItem[] } | todowrite |
question | { questions: QuestionItem[], answers? } | question, ask |
generic | { label, inputPretty?, outputPretty? } | everything else, always safe to render |
DiffLine is { type: 'added' | 'removed' | 'unchanged', text }. String
fields are capped so one large tool output never breaks a render: 4000
characters for pretty-printed JSON, 256KB before a diff runs.
Group messages into turns
A turn pairs one user message with the assistant messages that answered it. It is the unit a chat UI renders as one exchange.
import { groupMessagesIntoTurns, collectTurnParts, type TurnLike } from '@kortix/sdk';
const turns: TurnLike[] = groupMessagesIntoTurns(messages);
for (const turn of turns) {
const parts = collectTurnParts(turn);
}groupMessagesIntoTurns links each assistant message to its parent user
message. It falls back to message order when a parent link is missing. It
also attaches an orphan assistant message — one with no parent that precedes
every user message — to the first turn, not the last.
Related helpers:
findLastTextPart(parts)— the turn's final response textturnHasSteps(parts)— true if atool,compaction,snapshot, orpatchpart existsisShellMode(turn)/getShellModePart(turn)— a turn that is one synthetic prompt driving onebashcall
Type guards
Narrow a part by type:
import { isTextPart, isToolPart, getPartText } from '@kortix/sdk';
if (isTextPart(part)) {
// part.type narrowed to 'text'
}
const text = getPartText(part); // works for 'text' and 'reasoning' partsAlso available: isReasoningPart, isFilePart, isAgentPart,
isCompactionPart, isSnapshotPart, isPatchPart.
Status and errors
import { getWorkingState, getTurnStatus, formatDuration } from '@kortix/sdk';
const status = getTurnStatus(parts, childMessages); // "Running commands..."
formatDuration(4300); // "4s" — durations under 1s return ''getTurnStatus scans a turn's parts for the last status line. When the last
part is a running task delegation, pass childMessages so the status shows
the sub-agent's real activity instead of a generic "Delegating..." line.
import { getTurnError, getChildSessionError, unwrapError } from '@kortix/sdk';
getTurnError(turn); // the first assistant error, unwrapped
getChildSessionError(childMessages); // newest error in a sub-agent's messages
unwrapError(rawError); // normalizes double-JSON and mixed error shapesCost and token totals
import { getTurnCost, getSessionCost, formatCost, formatTokens, COST_MARKUP } from '@kortix/sdk';
const info = getTurnCost(partsWithMessage, modelPricingLookup);
const sessionCost = getSessionCost(messages, modelPricingLookup);
formatCost(0.0032); // "$0.003"
formatTokens(12345); // "12k"Both functions read cost and token totals from step-finish parts. When a
part reports zero, they estimate cost from token counts using a
ModelPricingLookup. Every total is multiplied by COST_MARKUP (1.2) to
match what Kortix bills.
Child sessions and pending requests
A task tool call delegates to a child session. These helpers connect a
parent turn to that child's own messages.
import { getChildSessionId, getChildSessionToolParts } from '@kortix/sdk';
const childId = getChildSessionId(taskToolPart);
const steps = getChildSessionToolParts(childMessages);Match a pending permission or question request to its tool call, and find which tool parts to hide while one is active:
import { getPermissionForTool, getHiddenToolParts, isToolPartHidden } from '@kortix/sdk';
const permission = getPermissionForTool(permissions, callID);
const hidden = getHiddenToolParts(activePermission, activeQuestion);Formatting and lists
import { getFilename, getDirectory, relativizePath, stripAnsi } from '@kortix/sdk';
getFilename('/workspace/src/main.go'); // "main.go"
getDirectory('/workspace/src/main.go'); // "/workspace/src"
relativizePath('/workspace/src/main.go', '/workspace'); // "src/main.go"Session-list helpers operate on the same session data:
import { sortSessions, childMapByParent, allDescendantIds } from '@kortix/sdk';
sessions.sort(sortSessions(Date.now())); // pins sessions updated in the last 60s
const childMap = childMapByParent(sessions);
const descendants = allDescendantIds(childMap, sessionId);Retry state: getRetryInfo(sessionStatus) returns { attempt, message, next, details? } when status.type === 'retry', with message capped to 60
characters. details preserves a structured LLM-gateway envelope when present:
the final provider, gateway code, request ID, suggestion, upstream status, and
ordered candidate failures. Each candidate failure contains attempt,
provider, routeModel, resolvedModel, stage, optional status, code,
and message. Legacy plain-text retries return details: undefined.
getRetryMessage(sessionStatus) returns the full unwrapped message. When
OpenCode retains only that message, the gateway composite still includes the
request ID and each candidate's provider, resolved model, HTTP status, code,
and bounded message.
Structural types
The grouping and status functions accept minimal structural types —
PartLike, MessageInfoLike, TurnLike, ToolStateLike,
SessionStatusLike — instead of the concrete @opencode-ai/sdk wire types.
Your own message and part shapes flow through unchanged as long as they match
the required fields. classifyPart and classifyTurn are the exception.
They type against the real @opencode-ai/sdk Part union, so their
exhaustiveness check catches a new wire part type at build time.
Distribution
npm install @kortix/sdkThe package ships as compiled ESM with full TypeScript type declarations.
react and @tanstack/react-query are optional peer dependencies. If you
use @kortix/sdk/react, install both.
import { createKortix } from '@kortix/sdk'; // framework-free core
import { useSession } from '@kortix/sdk/react'; // optional React layer
import { createScopedKortix } from '@kortix/sdk/server'; // Node and Bun serversEntry points and stability
The root entry, @kortix/sdk, is canonical. It exports the full
framework-free surface and runs in browsers, Node 18+, Bun, and edge
runtimes.
| Entry | Tier | Contract |
|---|---|---|
@kortix/sdk | Canonical | Framework-free. Never imports React or node:*. |
@kortix/sdk/react | Supported | React hooks. The only entry that imports React. |
@kortix/sdk/server | Supported | Per-request config isolation for Node and Bun servers, via node:async_hooks. Never bundle it into a browser. |
Legacy subpaths (/projects-client, /turns, /files, and more) | Deprecated | Still work. Each re-exports from the root. Import from the root instead. |
@kortix/sdk/internal/* | Internal | Used by the Kortix web app only. Not a supported API. |
CDN bundles
The package also ships two browser bundles, built by tsup: an ESM bundle
(dist/kortix.esm.min.js) and an IIFE global (dist/kortix.global.js) that
defines window.Kortix.
<script src="https://unpkg.com/@kortix/sdk/dist/kortix.global.js"></script>
<script>
const kortix = Kortix.createKortix({
backendUrl: 'https://api.kortix.com/v1',
getToken: async () => KEY,
});
</script>window.Kortix exposes the same root entry as the npm import:
Kortix.createKortix, Kortix.classifyTurn, Kortix.ApiError.
See also
- SDK — install and your first session.
- Sessions — lifecycle, streaming, and error handling.
- React hooks — the reactive layer built on these modules.
