Skip to main content
Session bundles compaction, retry, persistence, turn tracking, and hooks into a single class. If you want more control — composing only the behaviors you need, or writing custom middleware — use the functional Runner / Middleware / Conversation API instead.

Runners and Middleware

A Runner is an async generator function with the same shape as agent.run(). A Middleware transforms one Runner into another. You compose them with apply():
import {
  Agent, Conversation, toRunner, apply,
  withTurnTracking, withCompaction, withRetry, withPersistence, withHooks,
} from "@openharness/core";

const agent = new Agent({
  name: "dev",
  model: openai("gpt-5.4"),
  tools: { ...fsTools, bash },
});

// Compose only the middleware you need
const runner = apply(
  toRunner(agent),
  withTurnTracking(),
  withCompaction({ contextWindow: 200_000, model: agent.model }),
  withRetry({ maxRetries: 5 }),
  withPersistence({ store: myStore, sessionId: "abc" }),
);

const chat = new Conversation({ runner });

for await (const event of chat.send("Fix the bug in auth.ts")) {
  if (event.type === "text.delta") process.stdout.write(event.text);
}
// chat.messages is automatically updated from the done event
Middleware listed first in apply() wraps outermost. The ordering above means: turn tracking brackets everything, compaction runs once before retries, retry wraps only the agent call, and persistence saves after a successful response.

Available Middleware

MiddlewareDescription
withTurnTracking()Emits turn.start/turn.done events. Maintains a turn counter across calls.
withCompaction(config)Auto-compacts history when approaching the context window limit. Tracks lastInputTokens from step.done events.
withRetry(config?)Retries on transient API errors (429, 500, etc.) with exponential backoff. Only retries before content has been streamed.
withPersistence(config)Auto-saves messages to a SessionStore on every done event.
withHooks(hooks)Applies SessionHooks (onBeforeSend, onAfterResponse, onError) around the inner runner.

Conversation

Conversation is a thin stateful wrapper over a composed Runner. It manages messages (updating from done events) and provides the same toUIMessageStream() and toResponse() methods as Session for AI SDK 5 integration:
const chat = new Conversation({ runner, sessionId: "abc", store: myStore });

// Optionally load previous messages
await chat.load();

// Send messages — chat.messages is updated automatically
for await (const event of chat.send("hello")) { /* ... */ }

// Manual save (separate from withPersistence auto-save)
await chat.save();

// Next.js route handler
return chat.toResponse(input, { signal: req.signal });

Stream Combinators

For lightweight event stream transforms, four curried combinators are available:
import { tap, filter, map, takeUntil } from "@openharness/core";

// Log every event
const logged = tap(e => console.log(e.type));
for await (const event of logged(agent.run([], "hello"))) { /* ... */ }

// Drop reasoning events (done events are never filtered)
const noReasoning = filter(e => e.type !== "reasoning.delta");

// Transform text events
const uppercased = map(e =>
  e.type === "text.delta" ? { ...e, text: e.text.toUpperCase() } : e
);

// Stop after first text completion
const firstText = takeUntil(e => e.type === "text.done");

Writing Custom Middleware

A middleware is a function that takes a Runner and returns a Runner:
import type { Middleware } from "@openharness/core";

const withLogging: Middleware = (runner) =>
  async function* (history, input, options) {
    console.log(`Sending: ${typeof input === "string" ? input : "[messages]"}`);
    for await (const event of runner(history, input, options)) {
      if (event.type === "done") console.log(`Done: ${event.result}`);
      yield event;
    }
  };

const runner = apply(toRunner(agent), withLogging, withRetry());

Composing with pipe

For reusable middleware stacks, use pipe() to create a combined middleware:
import { pipe } from "@openharness/core";

const production = pipe(
  withTurnTracking(),
  withCompaction({ contextWindow: 200_000, model }),
  withRetry({ maxRetries: 5 }),
);

// Apply the same stack to multiple runners
const runner1 = production(toRunner(agent1));
const runner2 = production(toRunner(agent2));