Sessions
The session handle — lifecycle, the session-owned runtime (health & previews), and the typed opencode runtime.
A session is one agent run, in its own disposable sandbox, on its own git
branch. kortix.session(projectId, sessionId) binds both ids and is the single
handle for everything a session does.
const s = kortix.session(projectId, sessionId);Session-scoped, by design. A session owns its runtime, so you ask the session about health and previews — never a global "sandbox." The sandbox is plumbing the SDK resolves for you.
Lifecycle
await s.get(); // session detail
await s.update({/* … */}); // rename, settings
await s.start(); // provision + boot the runtime
await s.restart(); // restart the same runtime in place
await s.stop(); // stop the runtime without deleting the session
await s.commit(); // commit the agent's work
await s.setSharing(intent); // sharing / visibility
await s.delete();restart() preserves the session's established sandbox identity and refreshes
the handle's cached readiness state. If the original provider object is
unavailable, restart fails explicitly instead of attaching an empty replacement.
delete() removes the runtime; stop() clears readiness without deleting the
session itself.
Previews & public shares
await s.previews(); // candidate preview ports the runtime exposes
await s.publicShares.list();
await s.publicShares.create(input);
await s.publicShares.revoke(shareId);Audit & transcript
await s.audit(limit?); // per-session audit trail of executor-gated agent actions
await s.transcript(options?); // compact server-side transcript (text + tool calls, no tool inputs/outputs)transcript() is callable with project-scoped session tokens, so it's the
right read for a scoped/embedded host that only has a session token.
The runtime
The session owns its runtime, so these resolve the active sandbox for you — you pass a port or a URL, never a sandbox id.
| method | returns | what |
|---|---|---|
s.health(init?) | { status, ok, health, body } | runtime liveness + whether OpenCode is ready |
s.previewUrl(port, path?) | string | proxy/preview URL for a port the agent exposed |
s.proxyUrl(url?) | string | undefined | rewrite a localhost URL the agent printed into a reachable proxy URL |
const { ok, health } = await s.health();
// health?.runtimeReady · health?.status · health?.version …
const url = s.previewUrl(3000, '/docs'); // → the live preview URL
const fixed = s.proxyUrl('http://localhost:8080');s.health() never throws SessionNotReadyError — it's safe to poll before
the session has ever resolved a runtime. s.previewUrl() and s.proxyUrl()
do require a resolved runtime; call s.ensureReady() first (or s.send() /
s.abort(), which call it internally).
Stateless URL helpers — detecting localhost URLs in agent output, parsing preview URLs — live at
@kortix/sdk/session. The session handle wraps them with
the sandbox context already resolved.
Talking to the agent
These are the opinionated wrappers over the runtime — the right entry points for
a script, server, worker, or any non-React host. Each auto-provisions the
runtime via ensureReady() internally, resolves the OpenCode session id for
you, and always acts against this handle's own resolved runtime — never
whatever sandbox happens to be globally "active" — so two handles on two
different sessions never cross wires.
await s.ensureReady(); // provision/resume the runtime; idempotent
s.setModel({ providerID, modelID }); // sticky model for subsequent send()s
s.setAgent('build'); // sticky agent for subsequent send()s
await s.send('Refactor the auth module'); // prompt the agent
await s.send('One-off task', { model, agent }); // per-call override
await s.abort(); // abort the current runCall s.ensureReady() (or send/abort, which call it internally) before
.runtime, .previewUrl(), or .proxyUrl() — those throw
SessionNotReadyError if this handle hasn't resolved a runtime yet.
.health() is the exception: it never throws and is safe to call anytime.
Streaming events — s.stream()
For live message / part / event streaming from a non-React host, use
s.stream() — a framework-free facade over the same primitive
useSession uses
internally. It handles connect/reconnect/backoff and a 15s heartbeat watchdog.
const handle = await s.stream({
onEvent: (event) => console.log(event),
onGapRehydrate: (gapMs) => console.log('reconnected, gap:', gapMs),
});
// later
handle.close();In a React app, prefer useSession(projectId, sessionId)
instead — the hook that owns the whole runtime (start, SSE, readiness) and
returns the thread, send, and the boot phase.
The typed runtime — s.runtime
s.runtime is the typed OpenCode v2 client, scoped to this session and reached
only through the SDK — the escape hatch for anything not covered by
send/abort/stream. It requires a resolved runtime (call s.ensureReady()
first, or use it after send/abort/stream have run).
await s.ensureReady();
// send a prompt (equivalent to s.send(), shown for the raw client)
await s.runtime.session.prompt({
sessionID: (await s.ensureReady()).opencodeSessionId,
parts: [{ type: 'text', text: 'Refactor the auth module' }],
});The OpenCode sessionID the runtime expects is not the Kortix sessionId passed to
kortix.session(projectId, sessionId) — it's resolved server-side at /start and cached on the
handle. Prefer s.send() / s.abort(), which resolve it for you; only reach for raw s.runtime
calls when you need something those wrappers don't cover.
Never import @opencode-ai/sdk directly. s.runtime is the same client, owned by the SDK, with
the full opencode type surface re-exported from
@kortix/sdk/opencode-client.
Files — s.files
s.files is the session-scoped equivalent of the top-level @kortix/sdk
files export: the same 12-op workspace surface, but every call auto-provisions
via ensureReady() and always targets this handle's own resolved runtime —
never the module-global "active" sandbox. This fixes a cross-session bleed bug:
a host juggling multiple open sessions that called the global files.list()
could silently read/write the wrong sandbox.
await s.files.list(dirPath);
await s.files.read(filePath);
await s.files.readBlob(filePath);
await s.files.status();
await s.files.findFiles(query, { type: 'file', limit });
await s.files.findText(pattern);
await s.files.upload(file, targetPath?, filename?);
await s.files.create(filePath);
await s.files.copy(sourcePath, destPath);
await s.files.remove(filePath);
await s.files.mkdir(dirPath);
await s.files.rename(from, to);