# AGENTS.md Source: https://docs.open-harness.dev/advanced/agents-md Automatic instruction loading from project files OpenHarness supports the [AGENTS.md](https://agents.md) spec. On first run, the agent walks up from the current directory to the filesystem root looking for `AGENTS.md` or `CLAUDE.md`. The first file found is loaded and prepended to the system prompt. ## How It Works When an agent runs for the first time, it: 1. Starts from the current working directory 2. Walks up the directory tree toward the filesystem root 3. Looks for `AGENTS.md` or `CLAUDE.md` at each level 4. Loads the first file found and prepends its content to the system prompt This allows you to define project-specific instructions that any agent running in that directory will automatically pick up. ## Disabling Instructions This behavior is enabled by default. Set `instructions: false` to disable it: ```typescript theme={"dark"} const agent = new Agent({ name: "dev", model: openai("gpt-5.4"), tools: { ...fsTools, bash }, instructions: false, // don't load AGENTS.md or CLAUDE.md }); ``` ## Use Cases * **Project conventions** — coding style, test requirements, deployment rules * **Safety guardrails** — directories the agent should never modify, commands to avoid * **Context** — project architecture notes, key dependencies, known issues # MCP Servers Source: https://docs.open-harness.dev/advanced/mcp-servers Connect to Model Context Protocol servers for external tools Agents can connect to [Model Context Protocol](https://modelcontextprotocol.io) servers. Tools from MCP servers are merged into the agent's toolset alongside any static tools. ## Configuration ```typescript theme={"dark"} const agent = new Agent({ name: "dev", model: openai("gpt-5.4"), tools: { ...fsTools, bash }, mcpServers: { github: { type: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-github"], env: { GITHUB_TOKEN: process.env.GITHUB_TOKEN }, }, weather: { type: "http", url: "https://weather-mcp.example.com/mcp", headers: { Authorization: "Bearer ..." }, }, }, }); // MCP connections are established lazily on first run() for await (const event of agent.run([], "What PRs are open?")) { // ... } // Clean up MCP connections when done await agent.close(); ``` ## Transport Types Three transport types are supported: | Transport | Use case | | --------- | ---------------------------------------------------------------------- | | `stdio` | Local servers — spawns a child process, communicates over stdin/stdout | | `http` | Remote servers via Streamable HTTP (recommended for production) | | `sse` | Remote servers via Server-Sent Events (legacy) | ## Tool Namespacing When multiple MCP servers are configured, tools are namespaced as `serverName_toolName` to avoid collisions. With a single server, tool names are used as-is. ## Lazy Connection MCP connections are established lazily on the first `run()` call and cached for the agent's lifetime. This follows the same pattern as skills discovery. Always call `agent.close()` when done to clean up connections. # Skills Source: https://docs.open-harness.dev/advanced/skills On-demand instruction packages loaded into conversations Skills are reusable instruction packages — markdown documents that get loaded into the LLM conversation on demand. They provide domain-specific knowledge, workflows, and reference material without executing arbitrary code. Think of them as context-injecting tools: the model calls a `skill` tool, and the skill's markdown content is returned as structured output. ## Skill File Format Each skill is a directory containing a `SKILL.md` file with YAML frontmatter: ``` my-skill/ SKILL.md # Required — skill definition references/ # Optional — auxiliary files the model can read api.md scripts/ setup.sh ``` ```yaml SKILL.md theme={"dark"} --- name: my-skill description: A short description of what this skill does --- # My Skill Full markdown content here. This is the prompt that gets injected into the conversation when the skill is loaded. ``` Skill names must be lowercase alphanumeric with single hyphens (`^[a-z0-9]+(-[a-z0-9]+)*$`). ## Configuration Point the agent at one or more directories containing skill folders: ```typescript theme={"dark"} const agent = new Agent({ name: "dev", model: openai("gpt-5.4"), tools: { ...fsTools, bash }, skills: { paths: ["./skills", "~/.my-app/skills"], }, }); ``` Paths can be absolute, relative (resolved from cwd), or use `~/` for the home directory. When multiple paths contain a skill with the same name, later paths take precedence. ## How It Works When `skills` is configured, a `skill` tool is automatically added to the agent's toolset (similar to how the `task` tool is auto-generated for subagents). The tool's description includes an XML listing of all available skills, so the model knows what it can invoke. Skills are discovered lazily on the first `run()` call and cached for the agent's lifetime. When the model calls the `skill` tool: 1. The skill's markdown body is returned as structured XML output 2. Auxiliary files in the skill directory (up to 10) are listed so the model can read them with other tools 3. The base directory path is included so relative references resolve correctly Since the skill tool is a regular tool, it emits the standard `tool.start` and `tool.done` events. It also goes through the `approve` callback if one is configured. ## Standalone Usage The discovery and tool creation functions are exported separately for full control: ```typescript theme={"dark"} import { discoverSkills, createSkillTool } from "@openharness/core"; const skills = await discoverSkills({ paths: ["./skills"] }); console.log(skills.map(s => s.name)); // inspect discovered skills // Create the tool manually and pass it in const agent = new Agent({ name: "dev", model: openai("gpt-5.4"), tools: { ...fsTools, bash, skill: createSkillTool(skills) }, }); ``` # Subagents Source: https://docs.open-harness.dev/advanced/subagents Delegate work to specialized child agents Agents can delegate work to other agents. When you pass a `subagents` source, OpenHarness auto-registers a `task` tool that lets the parent spawn child agents by name. ## Basic Setup ```typescript theme={"dark"} const explore = new Agent({ name: "explore", description: "Read-only codebase exploration. Use for searching and reading files.", model: openai("gpt-5.4"), tools: { readFile, listFiles, grep }, maxSteps: 30, }); const agent = new Agent({ name: "dev", model: openai("gpt-5.4"), tools: { ...fsTools, bash }, subagents: [explore], }); ``` The parent model sees a `task` tool listing the available subagents. It can call `task` with an `agent` name and a `prompt`, and the child runs to completion autonomously. ## Key Behaviors * **Stateless by default** — each `task` call creates a fresh subagent instance with no shared conversation state * **No approval** — subagents run autonomously without prompting for permission * **Configurable nesting** — by default subagents cannot themselves have subagents (`maxSubagentDepth: 1`). Set a higher depth to enable nested delegation. * **Abort propagation** — the parent's abort signal is forwarded to the child * **Concurrent execution** — the model can call `task` multiple times in one response to run subagents in parallel Existing behavior stays the same unless you explicitly opt into `subagentSessions`. ## Dynamic Catalogs `subagents` can be either a static `Agent[]` or a dynamic catalog: ```typescript theme={"dark"} import type { SubagentCatalog } from "@openharness/core"; const catalog: SubagentCatalog = { async list() { return [ { name: "explore", description: "Read-only repo exploration" }, { name: "researcher", description: "Long-form investigation" }, ]; }, async resolve(name) { if (name === "explore") return explore; if (name === "researcher") return researcher; return undefined; }, }; const agent = new Agent({ name: "dev", model: openai("gpt-5.4"), tools: { ...fsTools, bash }, subagents: catalog, }); ``` The catalog is listed at run time, so the `task` tool schema and description stay in sync with whatever agents are currently available. ## Nested Subagents By default, subagents cannot delegate further. Set `maxSubagentDepth` to allow nesting: ```typescript theme={"dark"} const search = new Agent({ name: "search", description: "Focused file search", model: openai("gpt-5.4"), tools: { grep, listFiles }, }); const explore = new Agent({ name: "explore", description: "Read-only codebase exploration", model: openai("gpt-5.4"), tools: { readFile, listFiles, grep }, subagents: [search], // explore can delegate to search }); const agent = new Agent({ name: "dev", model: openai("gpt-5.4"), tools: { ...fsTools, bash }, subagents: [explore], maxSubagentDepth: 2, // allow explore -> search nesting }); ``` The depth decrements at each level: the root agent has depth 2, its child `explore` gets depth 1, and `search` gets depth 0. ## Resumable Subagent Sessions To let subagents keep their own message history across multiple `task` calls, enable `subagentSessions`. This layers `Session` persistence on top of the built-in `task` tool while keeping `Agent` itself stateless. ```typescript theme={"dark"} const messageStore = new Map(); const metadataStore = new Map(); const agent = new Agent({ name: "dev", model: openai("gpt-5.4"), tools: { ...fsTools, bash }, subagents: [researcher], subagentSessions: { messages: { async load(id) { return messageStore.get(id); }, async save(id, messages) { messageStore.set(id, messages); }, }, metadata: { async load(id) { return metadataStore.get(id); }, async save(meta) { metadataStore.set(meta.sessionId, meta); }, }, defaultMode: "stateless", }, }); ``` With `subagentSessions` enabled, `task` accepts an optional `session` object: ```typescript theme={"dark"} task({ agent: "researcher", prompt: "Find auth entrypoints", session: { mode: "new" }, }); task({ agent: "researcher", prompt: "What did you find earlier?", session: { mode: "resume", id: "sub-123" }, }); task({ agent: "researcher", prompt: "Explore the alternative fix", session: { mode: "fork", id: "sub-123" }, }); ``` ### Session Modes | Mode | Behavior | | ----------- | ---------------------------------------------------------------------------- | | `stateless` | Current behavior. Fresh child agent, no saved history. | | `new` | Create a new persistent subagent session and return its `session_id`. | | `resume` | Load an existing session by ID and continue from its saved history. | | `fork` | Clone an existing session into a new `session_id`, then continue from there. | ### Configuration | Option | Default | Description | | ---------------- | ----------- | ------------------------------------------------------------------------------------------------- | | `messages` | (required) | `SessionStore` used to save and load subagent messages | | `metadata` | in-memory | Tracks `sessionId -> agentName` and timestamps | | `defaultMode` | `stateless` | Default mode when `task(..., session)` is omitted. Only `stateless` and `new` are valid defaults. | | `sessionOptions` | — | Extra `Session` options to apply to child sessions (compaction, retry, hooks, etc.) | When a task runs in `new`, `resume`, or `fork`, the tool result includes `session_id="..."`: ```xml theme={"dark"} ... ``` Subagent sessions are single-writer. If the same `sessionId` is already running, the next `resume` or `fork` attempt fails instead of interleaving histories. ## Live Subagent Events To observe what subagents are doing in real time, pass an `onSubagentEvent` callback: ```typescript theme={"dark"} const agent = new Agent({ name: "dev", model: openai("gpt-5.4"), tools: { ...fsTools, bash }, subagents: [explore], onSubagentEvent: (path, event) => { if (event.type === "tool.done") { console.log(`[${path.join(" > ")}] ${event.toolName} completed`); } }, }); ``` The `path` parameter is a `string[]` representing the full ancestry from outermost to innermost agent. Events from nested subagents automatically bubble up through the chain. ## Background Subagents By default, all subagent calls are synchronous. Enable `subagentBackground` to let the parent spawn subagents in the background, do other work, and collect results later. ```typescript theme={"dark"} const agent = new Agent({ name: "dev", model: openai("gpt-5.4"), tools: { ...fsTools, bash }, subagents: [explore, researcher, coder], subagentBackground: true, }); ``` When enabled: 1. `task` gains an optional `background` parameter. When `true`, the subagent is spawned in the background and the tool returns immediately with a run ID. 2. `agent_await` is registered for waiting on background runs using different strategies. 3. `agent_status` and `agent_cancel` are registered for checking and cancelling runs. Example: ```text theme={"dark"} task({ agent: "researcher", prompt: "Find deprecated APIs", background: true }) task({ agent: "coder", prompt: "Refactor config parser" }) agent_await({ ids: ["bg-1"], mode: "all" }) ``` If resumable sessions are enabled, background spawns keep the same split: * **run ID** — returned as `agent_id="bg-1"` and used with `agent_status`, `agent_await`, and `agent_cancel` * **session ID** — returned as `session_id="sub-123"` and used with later `task(..., session: { mode: "resume", id: "sub-123" })` ### Await Modes | Mode | Behavior | | ------------ | --------------------------------------------------------------- | | `all` | Wait for all runs to succeed. Fails fast if any run fails. | | `allSettled` | Wait for all runs to finish. Returns results and errors. | | `any` | Wait for the first run to succeed. Only fails if all runs fail. | | `race` | Wait for the first run to settle (succeed or fail). | ### Configuration Pass `true` for sensible defaults, or an object for fine-grained control: ```typescript theme={"dark"} const agent = new Agent({ name: "dev", model: openai("gpt-5.4"), tools: { ...fsTools, bash }, subagents: [explore, researcher], subagentBackground: { maxConcurrent: 3, timeout: 120_000, autoCancel: true, tools: { status: true, cancel: true, await: ["all", "race"], }, }, }); ``` | Option | Default | Description | | --------------- | -------------- | -------------------------------------------------------------------------- | | `maxConcurrent` | `Infinity` | Maximum number of background runs running simultaneously | | `timeout` | — | Auto-cancel background runs after this many milliseconds | | `autoCancel` | `true` | Cancel all running background runs when `agent.close()` is called | | `tools.status` | `true` | Register the `agent_status` tool | | `tools.cancel` | `true` | Register the `agent_cancel` tool | | `tools.await` | all four modes | Which await modes to expose (`true` = all, `false` = disable, or an array) | ### Cleanup Background subagents respect the parent's abort signal. When `autoCancel` is `true`, calling `agent.close()` cancels any still-running background runs. # Agents Source: https://docs.open-harness.dev/core/agents The core primitive for building AI agents The `Agent` class is the core primitive of OpenHarness. An agent wraps a language model, a set of tools, and a multi-step execution loop into a **stateless executor** that you can `run()` with a message history and new input. ## Creating an Agent ```typescript theme={"dark"} import { Agent, createFsTools, createBashTool, NodeFsProvider, NodeShellProvider, } from "@openharness/core"; import { openai } from "@ai-sdk/openai"; const fsTools = createFsTools(new NodeFsProvider()); const { bash } = createBashTool(new NodeShellProvider()); const agent = new Agent({ name: "dev", model: openai("gpt-5.4"), systemPrompt: "You are a helpful coding assistant.", tools: { ...fsTools, bash }, maxSteps: 20, }); ``` ## Running an Agent `agent.run()` is an async generator that takes a message history and new input, and yields a stream of typed events as the agent works. The agent is **stateless** — it doesn't accumulate messages internally. You pass the conversation history in and get the updated history back in the `done` event. ```typescript theme={"dark"} import type { ModelMessage } from "ai"; let messages: ModelMessage[] = []; for await (const event of agent.run(messages, "Refactor the auth module to use JWTs")) { switch (event.type) { case "text.delta": process.stdout.write(event.text); break; case "tool.start": console.log(`Calling ${event.toolName}...`); break; case "tool.done": console.log(`${event.toolName} finished`); break; case "done": messages = event.messages; // capture updated history for next turn console.log(`Result: ${event.result}, tokens: ${event.totalUsage.totalTokens}`); break; } } ``` This makes it easy to build multi-turn interactions — just pass the messages from the previous `done` event into the next `run()` call. It also means you have full control over the conversation history: you can inspect it, modify it, or share it between agents. ## Events The full set of events emitted by `run()`: | Event | Description | | ----------------- | -------------------------------------------------------------------------------------------------- | | `text.delta` | Streamed text chunk from the model | | `text.done` | Full text for the current step is complete | | `reasoning.delta` | Streamed reasoning/thinking chunk (if the model supports it) | | `reasoning.done` | Full reasoning text for the step is complete | | `tool.start` | A tool call has been initiated | | `tool.done` | A tool call completed successfully | | `tool.error` | A tool call failed | | `step.start` | A new agentic step is starting | | `step.done` | A step completed (includes token usage and finish reason) | | `error` | An error occurred during execution | | `done` | The agent has finished — `result` is one of `"complete"`, `"stopped"`, `"max_steps"`, or `"error"` | ## Configuration | Option | Default | Description | | -------------------- | ---------- | -------------------------------------------------------------------------------------------------------- | | `name` | (required) | Agent name, used in logging and subagent selection | | `model` | (required) | Any Vercel AI SDK `LanguageModel` | | `systemPrompt` | — | System prompt prepended to every request | | `tools` | — | AI SDK `ToolSet` — the tools the agent can call | | `maxSteps` | `100` | Maximum agentic steps before stopping | | `temperature` | — | Sampling temperature | | `maxTokens` | — | Max output tokens per step | | `instructions` | `true` | Whether to load `AGENTS.md` / `CLAUDE.md` from the project directory | | `approve` | — | Callback for [tool call approval](/tools/permissions) | | `subagents` | — | Child agents or a `SubagentCatalog` available via the `task` tool (see [Subagents](/advanced/subagents)) | | `subagentSessions` | — | Optional persistent `Session` layer for resumable subagent memory | | `maxSubagentDepth` | `1` | Maximum nesting depth for subagents | | `subagentBackground` | — | Enable [background subagent execution](/advanced/subagents#background-subagents) | | `mcpServers` | — | [MCP servers](/advanced/mcp-servers) to connect to | | `skills` | — | [Skills](/advanced/skills) configuration | ## Multi-Turn Conversations Since the agent is stateless, multi-turn is achieved by passing the updated messages back in: ```typescript theme={"dark"} let messages: ModelMessage[] = []; // Turn 1 for await (const event of agent.run(messages, "List all files")) { if (event.type === "done") messages = event.messages; } // Turn 2 — agent sees the full history for await (const event of agent.run(messages, "Now read the largest file")) { if (event.type === "done") messages = event.messages; } ``` For automatic history management, compaction, and retry, use a [Session](/core/sessions) or [Conversation](/core/middleware#conversation). ## Cleanup If the agent uses MCP servers or background subagents, call `close()` when done: ```typescript theme={"dark"} await agent.close(); ``` # Middleware & Conversation Source: https://docs.open-harness.dev/core/middleware 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)); ``` # Providers Source: https://docs.open-harness.dev/core/providers Swappable filesystem and shell access for any environment Providers abstract how agents interact with the filesystem and shell. Instead of coupling tool logic to Node.js APIs, OpenHarness defines two interfaces — `FsProvider` and `ShellProvider` — that you can implement for any environment. This lets you run the same agent code locally, in a sandbox, or in the cloud, just by swapping the provider. ## Interfaces ### FsProvider ```typescript theme={"dark"} interface FsProvider { readFile(path: string): Promise; writeFile(path: string, content: string): Promise; exists(path: string): Promise; stat(path: string): Promise; readdir(path: string): Promise; mkdir(path: string, options?: { recursive?: boolean }): Promise; remove(path: string, options?: { recursive?: boolean }): Promise; rename(oldPath: string, newPath: string): Promise; resolvePath(path: string): string; } ``` ### ShellProvider ```typescript theme={"dark"} interface ShellProvider { exec( command: string, options?: { timeout?: number; cwd?: string; env?: Record; } ): Promise; } ``` ### Environment The `Environment` type combines both providers: ```typescript theme={"dark"} interface Environment { fs: FsProvider; shell: ShellProvider; } ``` ## Built-in Providers ### NodeFsProvider The default filesystem provider for Node.js environments. Uses `node:fs` under the hood. ```typescript theme={"dark"} import { createFsTools, NodeFsProvider } from "@openharness/core"; const fsTools = createFsTools(new NodeFsProvider()); ``` **Options:** | Option | Default | Description | | ------------- | --------------- | ---------------------------------------------- | | `cwd` | `process.cwd()` | Working directory for resolving relative paths | | `maxFileSize` | `10 MB` | Maximum file size `readFile` will load | ```typescript theme={"dark"} const fs = new NodeFsProvider({ cwd: "/home/user/project", maxFileSize: 20 * 1024 * 1024, // 20 MB }); ``` Safety features: * **File size guard** — throws `FileTooLargeError` if a file exceeds `maxFileSize` * **Auto-mkdir** — `writeFile` creates parent directories automatically * **Path resolution** — relative paths are resolved from `cwd` ### NodeShellProvider The default shell provider for Node.js. Runs commands via `bash -c`. ```typescript theme={"dark"} import { createBashTool, NodeShellProvider } from "@openharness/core"; const { bash } = createBashTool(new NodeShellProvider()); ``` **Options:** | Option | Default | Description | | ----------- | --------------- | -------------------------------------- | | `cwd` | `process.cwd()` | Default working directory for commands | | `maxStdout` | `50,000` chars | Truncate stdout beyond this length | | `maxStderr` | `10,000` chars | Truncate stderr beyond this length | ```typescript theme={"dark"} const shell = new NodeShellProvider({ cwd: "/home/user/project", maxStdout: 100_000, }); ``` The `exec` method supports per-call `timeout` (default 30s), `cwd`, and `env` overrides. ### VfsFsProvider A virtual filesystem provider from the `@openharness/provider-vfs` package. Provides sandboxed, in-memory, or SQLite-backed file access — ideal for testing, isolated execution, or environments without a real filesystem. ```bash theme={"dark"} npm install @openharness/provider-vfs ``` ```typescript theme={"dark"} import { createFsTools } from "@openharness/core"; import { VfsFsProvider } from "@openharness/provider-vfs"; const fsTools = createFsTools(new VfsFsProvider()); ``` By default, VfsFsProvider uses an in-memory backend mounted at `/workspace`. It initializes lazily on first use. **Options:** | Option | Default | Description | | ------------- | ------------------------------------------ | -------------------------------------------------------------- | | `vfs` | — | Pre-created `VirtualFileSystem` instance (skips auto-creation) | | `provider` | `MemoryProvider` | VFS storage backend | | `vfsOptions` | `{ moduleHooks: false, virtualCwd: true }` | Options passed to `vfs.create()` | | `mountPoint` | `"/workspace"` | Mount point for the virtual filesystem | | `maxFileSize` | `10 MB` | Maximum file size `readFile` will load | #### Storage Backends The VFS provider supports three backends via `@platformatic/vfs` (or the future `node:vfs`): In-memory filesystem — fast, ephemeral, no persistence: ```typescript theme={"dark"} const fs = new VfsFsProvider(); // MemoryProvider is the default ``` SQLite-backed filesystem — persistent, supports larger datasets: ```typescript theme={"dark"} import { getVfsModule } from "@openharness/provider-vfs"; const vfsModule = await getVfsModule(); const fs = new VfsFsProvider({ provider: new vfsModule.SqliteProvider("./agent-fs.db"), }); ``` Real filesystem scoped to a root directory — sandboxed access to a real path: ```typescript theme={"dark"} import { getVfsModule } from "@openharness/provider-vfs"; const vfsModule = await getVfsModule(); const fs = new VfsFsProvider({ provider: new vfsModule.RealFSProvider("/path/to/sandbox"), }); ``` #### Accessing the VFS Instance For advanced use cases, you can access the underlying `VirtualFileSystem`: ```typescript theme={"dark"} const provider = new VfsFsProvider(); const vfs = await provider.getVfs(); ``` ## Custom Providers Implement the `FsProvider` and/or `ShellProvider` interfaces to support any environment: ```typescript theme={"dark"} import type { FsProvider, ShellProvider } from "@openharness/core"; class MyCloudFsProvider implements FsProvider { async readFile(path: string) { /* ... */ } async writeFile(path: string, content: string) { /* ... */ } // ... implement all methods } ``` Possible targets: * **E2B sandboxes** for isolated code execution * **Cloudflare Workers** for edge deployment * **Daytona** for managed dev environments * **Docker containers** for reproducible builds * **Custom APIs** wrapping remote filesystems # Sessions Source: https://docs.open-harness.dev/core/sessions Stateful multi-turn conversations with compaction, retry, and persistence While `Agent` is a stateless executor, `Session` adds the statefulness and resilience you need for interactive, multi-turn conversations. It owns the message history and handles compaction, retry, persistence, and lifecycle hooks automatically. ## Creating a Session ```typescript theme={"dark"} import { Session } from "@openharness/core"; const session = new Session({ agent, contextWindow: 200_000, }); for await (const event of session.send("Refactor the auth module")) { switch (event.type) { case "text.delta": process.stdout.write(event.text); break; case "compaction.done": console.log(`Compacted: ${event.tokensBefore} -> ${event.tokensAfter} tokens`); break; case "retry": console.log(`Retrying in ${event.delayMs}ms...`); break; case "turn.done": console.log(`Turn ${event.turnNumber} complete`); break; } } ``` `session.send()` yields all the same `AgentEvent` types as `agent.run()`, plus additional session lifecycle events. ## Configuration | Option | Default | Description | | -------------------- | ---------------------------------------- | ----------------------------------------------------------------- | | `agent` | (required) | The `Agent` to use for execution | | `contextWindow` | — | Model context window size in tokens. Required for auto-compaction | | `reservedTokens` | `min(20_000, agent.maxTokens ?? 20_000)` | Tokens reserved for output | | `autoCompact` | `true` when `contextWindow` is set | Enable auto-compaction | | `shouldCompact` | — | Custom overflow detection function | | `compactionStrategy` | `DefaultCompactionStrategy()` | Custom compaction strategy | | `retry` | — | Retry config for transient API errors | | `hooks` | — | Lifecycle hooks | | `sessionStore` | — | Pluggable persistence backend | | `sessionId` | auto-generated UUID | Session identifier | ## Session Events In addition to all `AgentEvent` types, `session.send()` yields: | Event | Description | | -------------------- | ------------------------------------------------------------------- | | `turn.start` | A new turn is starting | | `turn.done` | Turn completed (includes token usage) | | `compaction.start` | Compaction triggered (includes reason and token count) | | `compaction.pruned` | Tool results pruned (phase 1) | | `compaction.summary` | Conversation summarized (phase 2) | | `compaction.done` | Compaction finished (includes before/after token counts) | | `retry` | Retrying after a transient error (includes attempt count and delay) | ## Compaction When a conversation approaches the context window limit, the session automatically compacts the message history. The default strategy works in two phases: 1. **Pruning** — replaces tool result content in older messages with `"[pruned]"`, preserving the most recent \~40K tokens of context. No LLM call needed. 2. **Summarization** — when pruning isn't enough, calls the model to generate a structured summary and replaces the entire history with it. You can customize compaction at multiple levels: ```typescript theme={"dark"} import { DefaultCompactionStrategy } from "@openharness/core"; // Tune the default strategy const session = new Session({ agent, contextWindow: 128_000, compactionStrategy: new DefaultCompactionStrategy({ protectedTokens: 60_000, // protect more recent context summaryModel: cheapModel, // use a cheaper model for summarization }), }); // Or replace the strategy entirely const session = new Session({ agent, contextWindow: 128_000, compactionStrategy: { async compact(context) { // your own compaction logic return { messages: [...], messagesRemoved: 0, tokensPruned: 0 }; }, }, }); // Or go fully manual const session = new Session({ agent, autoCompact: false }); // ...later: for await (const event of session.compact()) { /* ... */ } ``` ## Retry Transient API errors (429, 500, 502, 503, 504, 529, rate limits, timeouts) are retried automatically with exponential backoff and jitter. Retries only happen **before** any content has been streamed — once the model starts producing output, the session commits to that attempt. ```typescript theme={"dark"} const session = new Session({ agent, retry: { maxRetries: 5, initialDelayMs: 2000, maxDelayMs: 60_000, isRetryable: (error) => error.message.includes("overloaded"), }, }); ``` ## Hooks Hooks let you intercept and customize the session lifecycle: ```typescript theme={"dark"} const session = new Session({ agent, hooks: { // Modify messages before each LLM call onBeforeSend: (messages) => { return messages.filter(m => !isStale(m)); }, // Post-processing after each turn onAfterResponse: ({ turnNumber, messages, usage }) => { console.log(`Turn ${turnNumber}: ${usage.totalTokens} tokens`); }, // Custom compaction prompt onCompaction: (context) => { return "Summarize with emphasis on code changes and file paths."; }, // Custom error handling (return true to suppress) onError: (error, attempt) => { logger.warn(`Attempt ${attempt} failed: ${error.message}`); }, }, }); ``` ## Persistence Plug in any storage backend by implementing the `SessionStore` interface: ```typescript theme={"dark"} const session = new Session({ agent, sessionId: "user-123-conversation-1", sessionStore: { async load(id) { return db.get(id); }, async save(id, messages) { await db.set(id, messages); }, async delete(id) { await db.del(id); }, }, }); // Restore a previous session await session.load(); // Messages are auto-saved after each turn, or save manually: await session.save(); ``` ## Direct State Access The session's message history is directly readable and writable: ```typescript theme={"dark"} // Read current state console.log(session.messages.length, session.turns, session.totalUsage); // Inject or modify messages session.messages.push({ role: "user", content: "Remember: always use TypeScript.", }); ``` # Examples Source: https://docs.open-harness.dev/examples Example applications built with OpenHarness OpenHarness ships with three example applications that demonstrate different integration patterns. ## CLI Agent An interactive terminal agent with tool approval prompts, subagent display, and composed middleware. ```bash theme={"dark"} pnpm --filter cli-demo start ``` To use ChatGPT/Codex OAuth instead of `OPENAI_API_KEY`: ```bash theme={"dark"} pnpm --filter cli-demo start -- --chatgpt ``` **Features:** * Tool approval prompts with queued display * Subagent status with spinners * Message compaction * Optional ChatGPT/Codex OAuth provider via `--chatgpt` * Composed middleware stack **Source:** [`examples/cli`](https://github.com/MaxGfeller/open-harness/tree/main/examples/cli) ## Next.js Chat App A full chat UI built with Next.js and `@openharness/react`. ```bash theme={"dark"} cp examples/nextjs-demo/.env.example examples/nextjs-demo/.env # Edit .env and add your OPENAI_API_KEY pnpm --filter nextjs-demo dev ``` Then open [http://localhost:3000](http://localhost:3000). **Features:** * Streaming text and tool visualization * Subagent status tracking * `announce` tool for agent narration * Session persistence (in-memory) * Composed middleware (turn tracking, compaction, retry, persistence) **Source:** [`examples/nextjs-demo`](https://github.com/MaxGfeller/open-harness/tree/main/examples/nextjs-demo) ## Nuxt Chat App The same chat experience built with Vue 3, Nuxt 4, and `@openharness/vue`. ```bash theme={"dark"} pnpm --filter nuxt-demo dev ``` Then open [http://localhost:3000](http://localhost:3000). **Features:** * Same capabilities as the Next.js example * Vue 3 composables and reactive state * Nuxt 4 server routes **Source:** [`examples/nuxt-demo`](https://github.com/MaxGfeller/open-harness/tree/main/examples/nuxt-demo) ## Prerequisites By default the examples use `OPENAI_API_KEY`. Create a `.env` file in the repo root: ```bash theme={"dark"} echo "OPENAI_API_KEY=sk-..." > .env ``` The CLI example can instead use ChatGPT/Codex device login with `--chatgpt`. Clone and build the repository first: ```bash theme={"dark"} git clone https://github.com/MaxGfeller/open-harness.git cd open-harness pnpm install pnpm build ``` # Installation Source: https://docs.open-harness.dev/getting-started/installation Install OpenHarness packages in your project ## Install the Core Package ```bash theme={"dark"} npm install @openharness/core ``` ```bash theme={"dark"} pnpm add @openharness/core ``` ```bash theme={"dark"} yarn add @openharness/core ``` ## UI Packages (Optional) If you're building a chat UI, install the framework-specific package: ```bash theme={"dark"} npm install @openharness/react ``` ```bash theme={"dark"} npm install @openharness/vue ``` ## AI SDK Provider OpenHarness uses Vercel's AI SDK for model access. Install the provider for your preferred model: ```bash theme={"dark"} npm install @ai-sdk/openai ``` ```bash theme={"dark"} npm install @ai-sdk/anthropic ``` ```bash theme={"dark"} npm install @ai-sdk/google ``` Any [AI SDK-compatible provider](https://sdk.vercel.ai/providers) works with OpenHarness. ## Environment Variables Set the API key for your model provider: ```bash theme={"dark"} # OpenAI export OPENAI_API_KEY=sk-... # Anthropic export ANTHROPIC_API_KEY=sk-ant-... # Google export GOOGLE_GENERATIVE_AI_API_KEY=... ``` ## Clone the Repository To run the examples or contribute: ```bash theme={"dark"} git clone https://github.com/MaxGfeller/open-harness.git cd open-harness pnpm install pnpm build ``` # Quickstart Source: https://docs.open-harness.dev/getting-started/quickstart Build your first AI agent in minutes This guide walks you through creating a simple agent that can read files and run shell commands. ## Create an Agent Create a new project and install dependencies: ```bash theme={"dark"} mkdir my-agent && cd my-agent npm init -y npm install @openharness/core @ai-sdk/openai ``` Set your API key: ```bash theme={"dark"} export OPENAI_API_KEY=sk-... ``` Create an `agent.ts` file: ```typescript agent.ts theme={"dark"} import { Agent, createFsTools, createBashTool, NodeFsProvider, NodeShellProvider, } from "@openharness/core"; import { openai } from "@ai-sdk/openai"; const fsTools = createFsTools(new NodeFsProvider()); const { bash } = createBashTool(new NodeShellProvider()); const agent = new Agent({ name: "dev", model: openai("gpt-5.4"), systemPrompt: "You are a helpful coding assistant.", tools: { ...fsTools, bash }, maxSteps: 20, }); ``` Add a simple loop that streams the agent's output: ```typescript agent.ts theme={"dark"} import type { ModelMessage } from "ai"; let messages: ModelMessage[] = []; for await (const event of agent.run(messages, "What files are in this directory?")) { switch (event.type) { case "text.delta": process.stdout.write(event.text); break; case "tool.start": console.log(`\nCalling ${event.toolName}...`); break; case "done": messages = event.messages; break; } } ``` ## Add a Session for Multi-Turn Chat For interactive conversations with automatic compaction and retry, wrap the agent in a `Session`: ```typescript theme={"dark"} import { Session } from "@openharness/core"; const session = new Session({ agent, contextWindow: 128_000, }); // First turn for await (const event of session.send("List all TypeScript files")) { if (event.type === "text.delta") process.stdout.write(event.text); } // Second turn — session remembers the conversation for await (const event of session.send("Now refactor the largest one")) { if (event.type === "text.delta") process.stdout.write(event.text); } ``` ## Run the Examples To run the examples, first clone and build the repository: ```bash theme={"dark"} git clone https://github.com/MaxGfeller/open-harness.git cd open-harness pnpm install && pnpm build ``` Then pick an example: An interactive terminal agent with tool approval prompts and subagent display: ```bash theme={"dark"} pnpm --filter cli-demo start ``` To use ChatGPT/Codex OAuth instead of `OPENAI_API_KEY`: ```bash theme={"dark"} pnpm --filter cli-demo start -- --chatgpt ``` A chat app with streaming, tool visualization, and subagent status: ```bash theme={"dark"} cp examples/nextjs-demo/.env.example examples/nextjs-demo/.env # Edit .env and add your OPENAI_API_KEY pnpm --filter nextjs-demo dev ``` Then open [http://localhost:3000](http://localhost:3000). The same chat experience built with Vue 3 and Nuxt 4: ```bash theme={"dark"} pnpm --filter nuxt-demo dev ``` Then open [http://localhost:3000](http://localhost:3000). By default the examples use `OPENAI_API_KEY`. Create a `.env` file in the repo root: ```bash theme={"dark"} echo "OPENAI_API_KEY=sk-..." > .env ``` The CLI example can instead use ChatGPT/Codex device login with `--chatgpt`. ## Next Steps Learn about the Agent API, events, and configuration options. Add compaction, retry, and persistence for multi-turn conversations. Explore built-in tools and learn to create custom ones. # Introduction Source: https://docs.open-harness.dev/index Build capable, general-purpose AI agents in code OpenHarness is an open-source framework for building general-purpose AI agents programmatically. Built on [Vercel's AI SDK](https://sdk.vercel.ai), it provides the building blocks inspired by Claude Code, Codex, and similar advanced agent harnesses — but in a lightweight, composable form you can use in your own applications. ## Why OpenHarness? Agent harnesses like Claude Code, Codex, and OpenCode are powerful general-purpose agents. While their creators offer SDKs (Claude Agent SDK, Codex App Server, etc.), these are heavy to use programmatically. OpenHarness gives you the same building blocks — multi-step tool loops, subagent delegation, context compaction, streaming UI integration — as composable primitives you control entirely in code. ## Key Features Agents are pure executors: pass in history, get back events. Full control over conversation state. Add compaction, retry, persistence, and hooks as independent, composable layers. Agents can spawn child agents for subtasks, with configurable nesting depth and background execution. Filesystem and shell access through swappable providers — run locally, in sandboxes, or in the cloud. Connect to Model Context Protocol servers for external tools via stdio, HTTP, or SSE transports. Stream agent sessions directly to React or Vue chat UIs with typed data parts. ## Quick Example ```typescript theme={"dark"} import { Agent, createFsTools, createBashTool, NodeFsProvider, NodeShellProvider } from "@openharness/core"; import { openai } from "@ai-sdk/openai"; const agent = new Agent({ name: "dev", model: openai("gpt-5.4"), tools: { ...createFsTools(new NodeFsProvider()), ...createBashTool(new NodeShellProvider()), }, maxSteps: 20, }); for await (const event of agent.run([], "Refactor the auth module")) { if (event.type === "text.delta") process.stdout.write(event.text); } ``` # Built-in Tools Source: https://docs.open-harness.dev/tools/built-in-tools Filesystem, shell, and other tools that ship with OpenHarness Tools use the Vercel AI SDK `tool()` helper with Zod schemas. OpenHarness ships a set of built-in tools that you can use as-is, compose, or replace entirely. ## Filesystem Tools Create filesystem tools by passing a provider to the factory function: ```typescript theme={"dark"} import { createFsTools, NodeFsProvider } from "@openharness/core"; const fsTools = createFsTools(new NodeFsProvider()); ``` This returns the following tools: | Tool | Description | | ------------ | ------------------------------------------------------------------------------------------------------ | | `readFile` | Read file contents (supports line offset/limit for large files) | | `writeFile` | Write content to a file (creates parent directories automatically) | | `editFile` | Find-and-replace within a file | | `listFiles` | List files and directories (optionally recursive, paginated for large listings) | | `grep` | Regex search across files (automatically skips `node_modules`, `.git`, paginated for large match sets) | | `deleteFile` | Delete a file or directory | ## Bash Tool The bash tool runs arbitrary shell commands via `bash -c`: ```typescript theme={"dark"} import { createBashTool, NodeShellProvider } from "@openharness/core"; const { bash } = createBashTool(new NodeShellProvider()); ``` Features: * Configurable timeout (default 30s, max 5min) * Automatic output truncation for large results * Captures both stdout and stderr with exit code ## Using Tools with an Agent Pass tools directly to the agent constructor: ```typescript theme={"dark"} const agent = new Agent({ name: "dev", model: openai("gpt-5.4"), tools: { ...fsTools, bash }, }); ``` You can selectively include tools — for example, a read-only agent: ```typescript theme={"dark"} const readOnlyAgent = new Agent({ name: "explore", model: openai("gpt-5.4"), tools: { readFile: fsTools.readFile, listFiles: fsTools.listFiles, grep: fsTools.grep, }, }); ``` ## Providers The provider interface decouples tool logic from the runtime environment. The built-in `NodeFsProvider` and `NodeShellProvider` work in Node.js, but you can implement the interfaces for other environments: * **Virtual filesystems** for testing * **E2B sandboxes** for isolated execution * **Cloudflare Workers** for edge deployment * **Daytona** for managed dev environments ### FsProvider Interface ```typescript theme={"dark"} interface FsProvider { readFile(path: string): Promise; writeFile(path: string, content: string): Promise; exists(path: string): Promise; stat(path: string): Promise; readdir(path: string): Promise; mkdir(path: string, options?: { recursive?: boolean }): Promise; remove(path: string, options?: { recursive?: boolean }): Promise; rename(oldPath: string, newPath: string): Promise; resolvePath(path: string): string; } ``` ### ShellProvider Interface ```typescript theme={"dark"} interface ShellProvider { exec( command: string, options?: { timeout?: number; cwd?: string; env?: Record; } ): Promise; } ``` The built-in `NodeFsProvider` includes safety guards like file size limits and binary file detection. `NodeShellProvider` captures exit code, stdout, and stderr with output size limits. ## Todo Tools Create session-scoped todo tools with a pluggable store: ```typescript theme={"dark"} import { createTodoTools, InMemoryTodoStore } from "@openharness/core"; const todoStore = new InMemoryTodoStore(); const todoTools = createTodoTools({ sessionId: "chat-123", store: todoStore, }); ``` This returns: | Tool | Description | | ----------- | ---------------------------------------------------------- | | `todowrite` | Replace the current todo list with an updated ordered list | | `todoread` | Read the current todo list for the session | Todo items use typed statuses (`pending`, `in_progress`, `completed`, `cancelled`) and priorities (`high`, `medium`, `low`). If priority is omitted, it defaults to `medium`. When used with `sessionEventsToUIStream()`, successful `todowrite` calls emit a `data-oh:todo.updated` part so UI integrations can update without parsing raw tool output. # Custom Tools Source: https://docs.open-harness.dev/tools/custom-tools Create your own tools using the AI SDK tool() helper Any AI SDK-compatible tool works with OpenHarness. Define tools using the `tool()` function from the `ai` package with Zod schemas for input validation. ## Creating a Custom Tool ```typescript theme={"dark"} import { tool } from "ai"; import { z } from "zod"; const myTool = tool({ description: "Do something useful", inputSchema: z.object({ query: z.string() }), execute: async ({ query }) => { return { result: `You asked: ${query}` }; }, }); const agent = new Agent({ name: "my-agent", model: openai("gpt-5.4"), tools: { myTool }, }); ``` ## Combining Built-in and Custom Tools Mix and match built-in tools with your own: ```typescript theme={"dark"} import { createFsTools, createBashTool, NodeFsProvider, NodeShellProvider } from "@openharness/core"; import { tool } from "ai"; import { z } from "zod"; const fsTools = createFsTools(new NodeFsProvider()); const { bash } = createBashTool(new NodeShellProvider()); const deployTool = tool({ description: "Deploy the application to production", inputSchema: z.object({ environment: z.enum(["staging", "production"]), version: z.string(), }), execute: async ({ environment, version }) => { // your deployment logic return { status: "deployed", environment, version }; }, }); const agent = new Agent({ name: "dev", model: openai("gpt-5.4"), tools: { ...fsTools, bash, deploy: deployTool }, }); ``` ## Tool Best Practices * **Clear descriptions** help the model understand when and how to use each tool * **Zod schemas** provide both validation and type safety * **Return structured data** so the model can reason about results * **Handle errors gracefully** — thrown errors are surfaced to the model as tool errors, allowing it to adjust its approach # Permissions Source: https://docs.open-harness.dev/tools/permissions Gate tool execution with an approval callback By default, all tool calls are allowed. To gate tool execution — for example, prompting a user for confirmation — pass an `approve` callback to the agent. ## Basic Approval ```typescript theme={"dark"} const agent = new Agent({ name: "safe-agent", model: openai("gpt-5.4"), tools: { ...fsTools, bash }, approve: async ({ toolName, toolCallId, input }) => { // Return true to allow, false to deny const answer = await askUser(`Allow ${toolName}?`); return answer === "yes"; }, }); ``` ## ToolCallInfo The approval callback receives a `ToolCallInfo` object: ```typescript theme={"dark"} interface ToolCallInfo { toolName: string; toolCallId: string; input: unknown; } ``` ## Denied Tool Calls When a tool call is denied, a `ToolDeniedError` is thrown and surfaced to the model as a tool error. This lets the model adjust its approach — for example, by trying a different tool or asking the user for guidance. ## Async Approval The callback can be async, enabling a variety of approval patterns: * **Terminal prompts** — ask the user in a CLI * **Web UI modals** — show a confirmation dialog * **External services** — call an approval API or workflow system ```typescript theme={"dark"} // Example: approve reads automatically, require confirmation for writes const agent = new Agent({ name: "careful-agent", model: openai("gpt-5.4"), tools: { ...fsTools, bash }, approve: async ({ toolName }) => { const readOnly = ["readFile", "listFiles", "grep"]; if (readOnly.includes(toolName)) return true; return await promptUser(`Allow ${toolName}?`); }, }); ``` Subagents run autonomously without prompting for permission by design. If you need to control subagent tool access, limit the tools you pass to the subagent. # React Source: https://docs.open-harness.dev/ui-integration/react React hooks and provider for AI SDK 5 chat UIs The `@openharness/react` package provides hooks that wire into AI SDK 5's `useChat` and track OpenHarness-specific state like subagent activity, compaction, and turn lifecycle. ## Setup ```bash theme={"dark"} npm install @openharness/react ``` Wrap your app (or chat component) with `OpenHarnessProvider`: ```tsx theme={"dark"} import { OpenHarnessProvider } from "@openharness/react"; function App() { return ( ); } ``` ## `useOpenHarness` Creates a chat session connected to your API endpoint. Returns the same interface as AI SDK 5's `useChat` (`messages`, `sendMessage`, `status`, `stop`, etc.), typed with `OHUIMessage`: ```tsx theme={"dark"} import { useOpenHarness } from "@openharness/react"; function Chat() { const { messages, sendMessage, status, stop } = useOpenHarness({ endpoint: "/api/chat", }); return (
{messages.map((msg) => (
{msg.parts.map((part, i) => part.type === "text" ? {part.text} : null )}
))}
); } ``` ### Resuming Streams Pass a stable `id` and `resume: true` to reconnect to an active server-side stream when the component mounts: ```tsx theme={"dark"} const chat = useOpenHarness({ endpoint: "/api/tasks/task-1/chat", id: "task-1", resume: true, prepareReconnectToStreamRequest: ({ id }) => ({ api: `/api/tasks/${id}/chat/stream`, credentials: "include", }), }); ``` By default, reconnect attempts use `GET ${endpoint}/${id}/stream`. The server must own the active run and return the active stream or `204` when no stream is running. AI SDK stream resumption is not compatible with treating browser request abort as cancellation. If a user needs to stop a background run, expose a separate server-side cancel action. ## `useSubagentStatus` Derives reactive state from `data-oh:subagent.*` events: ```tsx theme={"dark"} import { useSubagentStatus } from "@openharness/react"; function SubagentDisplay() { const { activeSubagents, recentSubagents, hasActiveSubagents } = useSubagentStatus(); if (!hasActiveSubagents) return null; return (
{activeSubagents.map((agent) => (
{agent.name}: {agent.task}
))}
); } ``` * **`activeSubagents`** — currently running subagents * **`recentSubagents`** — all subagents seen in this session * **`hasActiveSubagents`** — boolean shorthand ## `useSessionStatus` Tracks turn index, compaction state, and retry info from `data-oh:*` events: ```tsx theme={"dark"} import { useSessionStatus } from "@openharness/react"; function SessionInfo() { const session = useSessionStatus(); return (
Turn: {session.turnIndex} | Messages: {session.messageCount}
); } ``` ## `useTodos` Tracks the latest todo list from `data-oh:todo.updated` events emitted by the core todo tools: ```tsx theme={"dark"} import { useTodos } from "@openharness/react"; function TodoDock() { const { todos, activeTodo, completed, total, hasTodos } = useTodos(); if (!hasTodos) return null; return ( ); } ``` ## Full Example ```tsx theme={"dark"} import { OpenHarnessProvider, useOpenHarness, useSubagentStatus, useSessionStatus, useTodos, } from "@openharness/react"; function App() { return ( ); } function Chat() { const { messages, sendMessage, status, stop } = useOpenHarness({ endpoint: "/api/chat", }); const { activeSubagents, hasActiveSubagents } = useSubagentStatus(); const session = useSessionStatus(); return (
{messages.map((msg) => (
{msg.parts.map((part, i) => part.type === "text" ? {part.text} : null )}
))}
); } ``` # Server Streaming Source: https://docs.open-harness.dev/ui-integration/server-streaming Stream agent sessions to AI SDK 5 chat UIs OpenHarness integrates with AI SDK 5's data stream protocol, so you can stream agent sessions directly to `useChat`-based React and Vue UIs. ## `toResponse()` Both `Session` and `Conversation` provide two methods for streaming to the client: * **`toUIMessageStream(input)`** — returns a `ReadableStream` that maps session events to AI SDK 5 typed chunks * **`toResponse(input, options)`** — wraps the stream in an HTTP `Response` with SSE headers, ready to return from any route handler ## Next.js Example ```typescript app/api/chat/route.ts theme={"dark"} import { Agent, Conversation, toRunner, apply, withTurnTracking, withCompaction, withRetry, withPersistence, extractUserInput, type SessionStore, } from "@openharness/core"; const store: SessionStore = { async load(id) { return db.get(id); }, async save(id, messages) { await db.set(id, messages); }, }; const conversations = new Map(); function getOrCreateConversation(id?: string): Conversation { const convId = id ?? crypto.randomUUID(); let conv = conversations.get(convId); if (!conv) { const runner = apply( toRunner(agent), withTurnTracking(), withCompaction({ contextWindow: 128_000, model: agent.model }), withRetry(), withPersistence({ store, sessionId: convId }), ); conv = new Conversation({ runner, sessionId: convId, store }); conversations.set(convId, conv); } return conv; } export async function POST(req: Request) { const { id, messages } = await req.json(); const conv = getOrCreateConversation(id); const input = await extractUserInput(messages); return conv.toResponse(input, { signal: req.signal }); } ``` ## Resumable Streams The direct `toResponse(input, { signal: req.signal })` pattern is enough for simple request-bound chat. For resumable chat, the server must decouple the active run from the HTTP response: * `POST` starts or attaches to a server-owned run for the chat id. * Client disconnect removes only that subscriber; it should not abort the run. * `GET ${endpoint}/${id}/stream` returns the active stream, or `204` when no stream is running. * Cancellation should be a separate server-side action. React and Vue clients can enable reconnects with `resume: true` and customize the GET endpoint with `prepareReconnectToStreamRequest`. ## Custom Data Parts The stream emits all standard AI SDK 5 chunk types (`text-delta`, `reasoning-delta`, `tool-input-available`, `tool-output-available`, `start`, `finish`, etc.) plus custom OpenHarness data parts for subagent activity, compaction, retry, and turn lifecycle: | Data part | Description | | ---------------------------- | ------------------------------ | | `data-oh:subagent.start` | A subagent task was spawned | | `data-oh:subagent.done` | A subagent task completed | | `data-oh:subagent.error` | A subagent task failed | | `data-oh:compaction.start` | Compaction started | | `data-oh:compaction.done` | Compaction finished | | `data-oh:retry` | Retrying after transient error | | `data-oh:turn.start` | Turn started | | `data-oh:turn.done` | Turn finished | | `data-oh:session.compacting` | Session is compacting | ## Using Data Part Types If you're building custom UI components that consume the stream directly, the core package exports typed data part types and guards: ```typescript theme={"dark"} import { type OHDataPart, isSubagentEvent, isCompactionEvent, } from "@openharness/core"; ``` # Vue Source: https://docs.open-harness.dev/ui-integration/vue Vue 3 composables and provider for AI SDK 5 chat UIs The `@openharness/vue` package provides the same functionality as the React package, using Vue 3 composables and Nuxt-compatible components. ## Setup ```bash theme={"dark"} npm install @openharness/vue ``` Wrap your app with the `OpenHarnessProvider` component: ```vue theme={"dark"} ``` ## `useOpenHarness` Creates a chat session connected to your API endpoint. Returns an AI SDK 5 `Chat` instance with reactive properties (`messages`, `status`, `sendMessage`, `stop`, etc.), typed with `OHUIMessage`: ```vue theme={"dark"} ``` ### Resuming Streams Pass a stable `id` and `resume: true` to reconnect to an active server-side stream when the component mounts: ```vue theme={"dark"} ``` By default, reconnect attempts use `GET ${endpoint}/${id}/stream`. The server must own the active run and return the active stream or `204` when no stream is running. AI SDK stream resumption is not compatible with treating browser request abort as cancellation. If a user needs to stop a background run, expose a separate server-side cancel action. ## `useSubagentStatus` Returns a computed ref deriving reactive state from `data-oh:subagent.*` events: ```vue theme={"dark"} ``` ## `useSessionStatus` Returns a computed ref tracking turn index, compaction state, and retry info: ```vue theme={"dark"} ``` ## Full Example ```vue theme={"dark"} ``` The `OpenHarnessProvider` is a renderless wrapper component that provides shared subagent, session, and sandbox state via Vue's `provide`/`inject`.