Sessions + Transport (Multi-Turn)
This guide shows how to wrap the interaction loop with a session boundary and host transport. You get durable state, policy hooks, and a stream of events to your client — without changing the interaction core.
> **Demo path (2/4)** — You’ve seen single‑turn interaction. This page adds sessions.
Next, wire it into a UI and then a full workflow:
How sessions layer on top of interactions
Sessions sit alongside the interaction pipeline. Each turn flows through a session, into the interaction pipeline, out to an EventStream, and back into the store as updated state.
flowchart LR U[User message] --> S[Session] S --> P[Interaction pipeline] P --> E[EventStream] S --> Store[(SessionStore)]
Step 1: Define a SessionStore and EventStream
import { createBuiltinModel } from "@geekist/llm-core/adapters";
import { createInteractionSession } from "@geekist/llm-core/interaction";
const sessionState = new Map();
/** @type {import("#adapters").EventStreamEvent[]} */
const emitted = [];
/** @type {import("#interaction").SessionStore} */
const store = {
load: loadSessionState,
save: saveSessionState,
};
/** @type {import("#adapters").EventStream} */
const eventStream = {
emit: emitEvent,
};import { createBuiltinModel } from "@geekist/llm-core/adapters";
import { createInteractionSession } from "@geekist/llm-core/interaction";
import type { EventStream, EventStreamEvent } from "@geekist/llm-core/adapters";
import type { InteractionState, SessionId, SessionStore } from "@geekist/llm-core/interaction";
const sessionState = new Map<string, InteractionState>();
const emitted: EventStreamEvent[] = [];
const store: SessionStore = {
load: loadSessionState,
save: saveSessionState,
};
const eventStream: EventStream = {
emit: emitEvent,
};You provide two adapters: a SessionStore for persistence and an EventStream for outbound events. The store can be Redis, KV, Postgres, or in‑memory — anything that can load and save the interaction state behind a simple contract.
Step 2: Create a session
const session = createInteractionSession({
sessionId: "demo",
store,
adapters: { model: createBuiltinModel() },
eventStream,
});const session = createInteractionSession({
sessionId: "demo",
store,
adapters: { model: createBuiltinModel() },
eventStream,
});A session is an orchestration wrapper around the interaction pipeline. Given a sessionId, it will load the previous state (if any), run the pipeline for the new turn, apply any policies, and persist the updated state — without leaking globals into your app.
Step 3: Send a turn and read state
const outcome = await session.send({ role: "user", content: "Hello!" });
if (isPausedOutcome(outcome)) {
throw new Error("Interaction paused.");
}
const state = session.getState();
console.log(state.messages.length);const outcome = await session.send({ role: "user", content: "Hello!" });
if ("__paused" in outcome && outcome.__paused) {
throw new Error("Interaction paused.");
}
const state = session.getState();
console.log(state.messages.length);Sending a turn is just session.send(message); you can then inspect the current state via session.getState(). If you stream to a browser, swap the EventStream implementation for SSE, WebSocket, or a worker stream — the interaction layer itself stays exactly the same.
Next step
See the UI boundary in action: