skopik

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.