* Normalize session_id into ws_id as sole persistent identity Eliminate the separate session_id concept. The workstream ID (ws_id) is now the single identity used for both real-time routing and conversation persistence, removing a layer of indirection that was 1:1 in practice and buggy on resume (stale pointers, orphaned rows). Schema changes (migration 006): - Drop sessions table; add alias/title columns to workstreams - Rename conversations.session_id → ws_id - Rename session_config table → workstream_config (ws_id column) - Data migration remaps existing conversations to ws_id Storage/API renames: - register_session → register_workstream (already existed, merged) - save_message/load_messages now keyed by ws_id - resolve_session → resolve_workstream - ChatSession.session_id property → ws_id - ChatSession.resume_session() → resume() - resume_session field → resume_ws - SessionResumedEvent → WorkstreamResumedEvent - /api/sessions → /api/workstreams/saved - /sessions slash command → /workstreams - --session-retention-days → --retention-days Channel eviction recovery simplified: reuses old ws_id directly instead of get_session_id_by_ws() reverse lookup. * Fix Copilot review feedback: stale session wording in docs, regenerate OpenAPI spec - docs/channels.md: "resumes the session" → "resumes the workstream", "Session resumed:" → "Resumed:", "old session was pruned" → "old workstream was pruned" - docs/api-reference.md: "Each session object" → "Each saved workstream object", field descriptions updated, removed stale node_id field - sdk/typescript/openapi-server.json: fully regenerated from Python models — removes all stale session_id properties from WorkstreamInfo, DashboardWorkstream, CreateWorkstreamResponse schemas
11 KiB
Turnstone Client SDK
See also: API Reference | Architecture | SDK Class Diagram
Typed HTTP client libraries for programmatic access to the turnstone server and console APIs. Available in Python (sync + async) and TypeScript.
Python SDK
The Python SDK is included in the turnstone package — no extra install required. It wraps the REST and SSE endpoints with typed methods that return Pydantic models directly.
Quick Start
from turnstone.sdk import TurnstoneServer
# Synchronous client — login with username/password
with TurnstoneServer("http://localhost:8080") as client:
client.login(username="alice", password="s3cret")
# Create a workstream
ws = client.create_workstream(name="Analysis")
# Send a message and wait for the full response
result = client.send_and_wait("Summarize this codebase.", ws.ws_id)
print(result.content)
# Stream events in real time
for event in client.stream_events(ws.ws_id):
if event.type == "content":
print(event.text, end="", flush=True)
# Close when done
client.close_workstream(ws.ws_id)
Alternatively, authenticate with an API token:
with TurnstoneServer("http://localhost:8080") as client:
client.login(token="ts_abc123...")
ws = client.create_workstream(name="CI run")
result = client.send_and_wait("Run the test suite.", ws.ws_id)
Async Client
import asyncio
from turnstone.sdk import AsyncTurnstoneServer
async def main():
async with AsyncTurnstoneServer("http://localhost:8080") as client:
await client.login(username="alice", password="s3cret")
ws = await client.create_workstream(name="demo")
async for event in client.stream_events(ws.ws_id):
if event.type == "content":
print(event.text, end="", flush=True)
asyncio.run(main())
Server Client API
Both TurnstoneServer (sync) and AsyncTurnstoneServer (async) expose:
| Category | Method | Returns |
|---|---|---|
| Workstreams | list_workstreams() |
ListWorkstreamsResponse |
dashboard() |
DashboardResponse |
|
create_workstream(*, name, model, auto_approve) |
CreateWorkstreamResponse |
|
close_workstream(ws_id) |
StatusResponse |
|
| Chat | send(message, ws_id) |
SendResponse |
approve(*, ws_id, approved, feedback, always) |
StatusResponse |
|
plan_feedback(*, ws_id, feedback) |
StatusResponse |
|
command(*, ws_id, command) |
StatusResponse |
|
| Streaming | stream_events(ws_id) |
Iterator[ServerEvent] |
stream_global_events() |
Iterator[ServerEvent] |
|
| High-level | send_and_wait(message, ws_id, *, timeout, on_event) |
TurnResult |
| Saved | list_saved_workstreams() |
ListSavedWorkstreamsResponse |
| Auth | login(username=..., password=...) |
AuthLoginResponse |
login(token="ts_xxx") |
AuthLoginResponse |
|
logout() |
StatusResponse |
|
auth_status() |
AuthStatusResponse |
|
| Health | health() |
HealthResponse |
Console Client API
Both TurnstoneConsole (sync) and AsyncTurnstoneConsole (async) expose:
| Category | Method | Returns |
|---|---|---|
| Cluster | overview() |
ClusterOverviewResponse |
nodes(*, sort, limit, offset) |
ClusterNodesResponse |
|
workstreams(*, state, node, search, sort, page, per_page) |
ClusterWorkstreamsResponse |
|
node_detail(node_id) |
NodeDetailResponse |
|
create_workstream(*, node_id, name, model, initial_message) |
ConsoleCreateWsResponse |
|
| Schedules | list_schedules() |
ListSchedulesResponse |
create_schedule(*, name, schedule_type, initial_message, ...) |
ScheduleInfo |
|
get_schedule(task_id) |
ScheduleInfo |
|
update_schedule(task_id, *, name=..., enabled=..., ...) |
ScheduleInfo |
|
delete_schedule(task_id) |
StatusResponse |
|
list_schedule_runs(task_id, *, limit=50) |
ListScheduleRunsResponse |
|
| Streaming | stream_cluster_events() |
Iterator[ClusterEvent] |
| Auth | login(username=..., password=...) / login(token="ts_xxx") |
AuthLoginResponse |
logout() |
StatusResponse |
|
| Health | health() |
ConsoleHealthResponse |
Event Types
SSE events are deserialized into typed dataclasses. Use event.type to discriminate.
Per-workstream events (from stream_events(ws_id)):
| Type | Class | Key Fields |
|---|---|---|
connected |
ConnectedEvent |
model, model_alias, skip_permissions |
history |
HistoryEvent |
messages |
content |
ContentEvent |
text |
reasoning |
ReasoningEvent |
text |
tool_info |
ToolInfoEvent |
items |
approve_request |
ApproveRequestEvent |
items |
tool_result |
ToolResultEvent |
call_id, name, output |
tool_output_chunk |
ToolOutputChunkEvent |
call_id, chunk |
status |
StatusEvent |
prompt_tokens, total_tokens, pct, effort |
plan_review |
PlanReviewEvent |
content |
error |
ErrorEvent |
message |
info |
InfoEvent |
message |
stream_end |
StreamEndEvent |
— |
Global events (from stream_global_events()):
| Type | Class | Key Fields |
|---|---|---|
ws_state |
WsStateEvent |
ws_id, state, tokens, activity |
ws_activity |
WsActivityEvent |
ws_id, activity, activity_state |
ws_rename |
WsRenameEvent |
ws_id, name |
ws_closed |
WsClosedEvent |
ws_id |
Cluster events (from stream_cluster_events()):
| Type | Class | Key Fields |
|---|---|---|
node_joined |
NodeJoinedEvent |
node_id |
node_lost |
NodeLostEvent |
node_id |
cluster_state |
ClusterStateEvent |
ws_id, node_id, state, tokens |
ws_created |
ClusterWsCreatedEvent |
ws_id, node_id, name |
TurnResult
The send_and_wait() method returns a TurnResult that aggregates the full response:
result = client.send_and_wait("Hello", ws_id, timeout=60)
result.content # Full text response
result.reasoning # Chain-of-thought (if shown)
result.tool_results # List of (tool_name, output) tuples
result.errors # Any error messages
result.ok # True if no errors and not timed out
result.timed_out # True if timeout expired
Error Handling
Non-2xx responses raise TurnstoneAPIError:
from turnstone.sdk import TurnstoneServer, TurnstoneAPIError
try:
client.send("hi", "bad_ws_id")
except TurnstoneAPIError as e:
print(e.status_code) # 404
print(e.message) # "Unknown workstream"
TypeScript SDK
Located at sdk/typescript/. Zero runtime dependencies for browsers; uses native fetch and ReadableStream for SSE parsing.
Quick Start
import { TurnstoneServer } from "@turnstone/sdk";
const client = new TurnstoneServer({ baseUrl: "http://localhost:8080" });
// Login with username/password or API token
await client.login({ username: "alice", password: "s3cret" });
// or: await client.login({ token: "ts_abc123..." });
// Create workstream and send message
const ws = await client.createWorkstream({ name: "demo" });
const result = await client.sendAndWait("Hello!", ws.ws_id);
console.log(result.content);
// Stream events
for await (const event of client.streamEvents(ws.ws_id)) {
if (event.type === "content") {
process.stdout.write(event.text);
}
}
Console Client
import { TurnstoneConsole } from "@turnstone/sdk";
const client = new TurnstoneConsole({ baseUrl: "http://localhost:8090" });
await client.login({ username: "alice", password: "s3cret" });
const overview = await client.overview();
console.log(`Nodes: ${overview.nodes}, Workstreams: ${overview.workstreams}`);
// Stream cluster events
for await (const event of client.clusterEvents()) {
console.log(event.type, event);
}
Type Safety
All event types are modeled as a discriminated union:
import { isContentEvent, isErrorEvent } from "@turnstone/sdk";
import type { ServerEvent } from "@turnstone/sdk";
function handleEvent(event: ServerEvent) {
if (isContentEvent(event)) {
// event is narrowed to ContentEvent
console.log(event.text);
} else if (isErrorEvent(event)) {
console.error(event.message);
}
}
Custom Fetch
The client accepts a custom fetch implementation for testing or Node.js environments:
const client = new TurnstoneServer({
baseUrl: "http://localhost:8080",
fetch: myCustomFetch,
});
Architecture
turnstone/sdk/ Python SDK (sub-package)
_base.py Shared httpx async client, auth, error handling
_sync.py Background event loop for sync wrappers
_types.py TurnResult + TurnstoneAPIError
events.py 27 SSE event dataclasses with type registry
server.py AsyncTurnstoneServer + TurnstoneServer
console.py AsyncTurnstoneConsole + TurnstoneConsole
sdk/typescript/ TypeScript SDK (npm package)
src/base.ts fetch wrapper, auth, SSE streaming
src/server.ts TurnstoneServer class
src/console.ts TurnstoneConsole class
src/events.ts Discriminated union events + type guards
src/sse.ts ReadableStream SSE parser
src/types.ts Request/response interfaces
The Python SDK reuses Pydantic models from turnstone/api/ directly — no schema duplication. The TypeScript SDK has hand-written interfaces matching those models.
Both SDKs follow the same design: typed methods for REST endpoints, async iterators for SSE streams, and a high-level send_and_wait method for simple request-response patterns.
Authentication
When auth is enabled on the server, the SDK handles JWT-based authentication automatically.
Login Flow
There are two ways to authenticate:
-
Username + password — calls
POST /v1/api/auth/loginwith credentials. The server validates against the user database and returns a JWT. -
API token — calls
POST /v1/api/auth/loginwith ats_-prefixed token string. The server looks up the token, resolves the associated user, and returns a JWT.
In both cases the server returns the JWT in the response body and as a Set-Cookie header. The SDK extracts the JWT and includes it as a Bearer token in the Authorization header on all subsequent requests.
# Username + password
client.login(username="alice", password="s3cret")
# API token (created via admin API or turnstone-admin CLI)
client.login(token="ts_abc123...")
Token Lifecycle
- JWTs have a configurable expiry (default: 24 hours).
client.auth_status()returns the current user identity and scopes without refreshing the token.client.logout()clears the stored JWT from the client.- If a request returns 401, the SDK raises
TurnstoneAPIError— the caller is responsible for re-authenticating.
Backward Compatibility
The config-file token (TURNSTONE_AUTH_TOKEN) still works as a simple Bearer token for environments that do not use the user/JWT system. When the server receives a non-JWT Bearer token, it falls back to the legacy token check.