> ## Documentation Index
> Fetch the complete documentation index at: https://docs.open-harness.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Middleware & Conversation

> Composable middleware and the lightweight stateful Conversation wrapper

`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()`:

```typescript theme={"dark"}
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

| Middleware                | Description                                                                                                               |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `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:

```typescript theme={"dark"}
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:

```typescript theme={"dark"}
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:

```typescript theme={"dark"}
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:

```typescript theme={"dark"}
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));
```
