Skip to content

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

js
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,
};
ts
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

js
const session = createInteractionSession({
  sessionId: "demo",
  store,
  adapters: { model: createBuiltinModel() },
  eventStream,
});
ts
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

js
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);
ts
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: