Sessions
A Session is Skopik's public unit of Agent work. It keeps the prompt, transcript, state, workspace, limits, usage, and lineage together across turns, reconnects, deploys, and idle time.
Run once or stay open
Use run() for a job that should finish after one ask:
const result = await skopik.run({
agent: agentId,
prompt: 'Review this launch plan and return the three largest risks.',
permission: 'ask',
limits: { maxTurns: 10, maxCostUsd: 1 },
})
console.log(result.text)
console.log(result.usage)
Use sessions.open() for a durable conversation that should park after each
turn and accept follow-ups:
const session = await skopik.sessions.open({
agent: agentId,
prompt: 'Review this launch plan and identify the largest risks.',
title: 'Launch review',
permission: 'ask',
policy: {
completeWhen: 'never',
idleAfter: '7d',
},
limits: {
maxTurns: 30,
maxCostUsd: 5,
},
metadata: { launch: 'fall-2026' },
})
sessions.open() defaults to completeWhen: 'never'. The opening turn parks
the Session at idle unless it is waiting on a concrete dependency. run()
sets completeWhen: 'agent_done' and waits locally for the terminal result.
curl "https://api.skopik.com/api/v1/sessions" \
-H "Authorization: Bearer $SKOPIK_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: launch-review-001" \
-d "{
\"agent\": \"$AGENT_ID\",
\"prompt\": \"Review this launch plan and identify the largest risks.\",
\"title\": \"Launch review\",
\"permission_mode\": \"ask\",
\"policy\": { \"complete_when\": \"never\", \"idle_after\": \"7d\" },
\"limits\": { \"max_turns\": 30, \"max_cost_usd\": 5 }
}"
The SDK returns Session fields directly. The raw API returns
{ session, created, message_id?, delivery? }.
Choose the Agent
Open with exactly one of these shapes:
// Reuse a durable Agent.
await skopik.sessions.open({
agent: agentId,
prompt: 'Start the review.',
})
// Create an Agent from a Template, with optional additive Skills.
await skopik.sessions.open({
template: templateId,
skills: [skillId],
prompt: 'Start the review.',
})
// Create an Agent from the default Template.
await skopik.sessions.open({
prompt: 'Start the review.',
})
agent is mutually exclusive with template and skills.
Stream and reconnect
sessions.stream() yields persisted Messages Protocol frames, then continues
with live frames. Skopik prefers its realtime transport and falls back to SSE
when a WebSocket is unavailable.
let lastCursor: string | undefined
for await (const frame of skopik.sessions.stream(session.sessionId, {
after: lastCursor,
})) {
lastCursor = frame.cursor
render(frame.envelope)
}
Persist lastCursor. Reconnecting with after replays everything after that
cursor before tailing live activity. For finite gap recovery without a live
connection:
const page = await skopik.sessions.listChunks(session.sessionId, {
after: lastCursor,
limit: 200,
})
Raw SSE uses the same cursor:
curl -N "https://api.skopik.com/api/v1/sessions/$SESSION_ID/stream?after=$CURSOR" \
-H "Authorization: Bearer $SKOPIK_API_KEY" \
-H "Accept: text/event-stream"
Send and steer
Send a follow-up to an idle or waiting Session, or steer a Session while
it is still running:
const sent = await skopik.sessions.send(session.sessionId, {
text: 'Focus the recommendation on operational risk.',
reasoning: 'high',
idempotencyKey: 'launch-review-followup-001',
})
console.log(sent.delivery)
delivery is:
| Value | Meaning |
|---|---|
dispatched |
A parked Session accepted the message and started work. |
merged |
The opening execution accepted the message before its first step. |
queued |
A running Session persisted the message for its next safe step boundary. |
A message is never rejected merely because work is already in flight.
Terminal Sessions do not accept new turns; use continueFrom to create
explicit lineage:
const continued = await skopik.sessions.open({
continueFrom: completedSessionId,
prompt: 'Revisit the recommendation with the new constraints.',
})
Read state and transcript
const { session, todos, approvals, attachments } =
await skopik.sessions.get(sessionId)
const { data: messages } =
await skopik.sessions.listMessages(sessionId)
const { data: sessions, page } = await skopik.sessions.list({
agent: agentId,
status: 'idle',
limit: 50,
})
Session status has six values:
| Status | Meaning |
|---|---|
running |
Agent work or setup is in flight. |
idle |
The Session is open and ready for another turn. |
waiting |
Work is blocked on input, approval, a Remote, an operation, a pause, or child Sessions. |
completed |
The completion policy was satisfied. |
failed |
Work ended with a typed SessionProblem. |
canceled |
Work was canceled. |
When status is waiting, inspect session.waitingFor to decide whether to
send input, make an approval decision, resume, or wait for another dependency.
Pause, resume, cancel, and archive
Pause is cooperative: the Agent finishes its current model or tool step and parks at the next safe boundary.
await skopik.sessions.pause(sessionId)
await skopik.sessions.resume(sessionId)
await skopik.sessions.cancel(sessionId)
await skopik.sessions.archive(sessionId)
Cancellation is terminal. Archive is a retention flag; it does not create a new status or delete the durable record.
Approvals
With permission: 'ask', effectful tools park the Session for approval. Load
the pending preview from sessions.get() and decide it explicitly:
const { approvals } = await skopik.sessions.get(sessionId)
const pending = approvals.find((approval) => approval.status === 'pending')
if (pending) {
await skopik.sessions.approve(sessionId, pending.operationId, {
decision: 'approve',
reason: 'The requested write is within the approved repository.',
})
}
Session permission is fixed at open:
| Permission | Behavior |
|---|---|
readonly |
Read effects only. |
ask |
Write effects wait for approval. |
full |
Session-level write approval is removed; Agent and action policy still apply. |
Structured output
Attach a JSON Schema when the Session must produce machine-readable data:
const result = await skopik.run({
agent: agentId,
prompt: 'Assess the launch risks.',
output: {
mode: 'strict',
schema: {
type: 'object',
properties: {
risks: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string' },
severity: { enum: ['low', 'medium', 'high'] },
},
required: ['name', 'severity'],
},
},
},
required: ['risks'],
},
},
})
console.log(result.data)
strict fails completion without a valid value. best_effort preserves the
best candidate when strict completion is not required.
Stable keyed Sessions
Use key to make repeated events converge on one live Session:
const session = await skopik.sessions.open({
key: 'customer:42:quarterly-review',
ifExists: 'send',
agent: agentId,
prompt: 'Incorporate the latest review input.',
})
With ifExists: 'send', Skopik atomically creates the Session or sends the
prompt to its current live holder. Use conflict when callers must handle an
existing key explicitly.
Session files
Use the Session id as a file target. Session paths begin with input/,
notes/, work/, or output/:
await skopik.files.writeText(
{ target: sessionId, path: 'input/launch-plan.md' },
{ content: launchPlan, contentType: 'text/markdown' },
)
const { data: deliverables } = await skopik.files.list({
target: sessionId,
path: 'output',
})
When a Session uses a Remote, its work/ tree lives on that
machine. Durable inputs, notes, and outputs remain cloud-side.
Remotes and Environments
Select a Remote for hardware-local reach and an Environment for reproducible setup, teardown, variables, secrets, and governed actions:
const session = await skopik.sessions.open({
agent: agentId,
remote: remoteId,
environment: environmentId,
prompt: 'Run the test suite and fix the failing integration test.',
permission: 'ask',
})
See Remotes and Environments for their security and lifecycle models.