# Simple Agent Harness A tiny, extension-first AI harness inspired by the same design instinct as pi: keep the core small, make the seams obvious, and add capability through tools. ## What is here - `src/agent.ts` — the agent loop - `src/model/openai.ts` — the OpenAI Responses API adapter - `src/events.ts` — typed lifecycle events - `src/commands.ts` — slash-command registry - `src/tools/registry.ts` — tool registration and lookup - `src/tools/builtins.ts` — two read-only built-in tools - `src/extensions.ts` — project-local extension loader - `src/logger.ts` — ordered lifecycle logging - `extensions/example.ts` — an example extension that registers `echo` ## Quick start ```bash npm install export OPENAI_API_KEY="..." npm run dev ``` That starts a simple terminal chat loop: ```text Simple Agent Harness Type a message, "/help" for commands, or "/exit" to quit. you> ``` You can also keep using one-shot mode when scripting: ```bash npm run dev -- "List the files in this project." ``` Optional: ```bash export SYSTEM_PROMPT="You are a terse coding agent." ``` ## Design The core is deliberately narrow: 1. the model adapter asks the model what to do next 2. the agent loop executes requested tools 3. tool outputs are fed back into the next turn 4. extensions add tools, commands, event listeners, and prompt fragments without changing the loop The harness also emits ordered trace logs to stderr so you can see the control flow: ```text [01] tool.registered {"tool":"list_files"} [02] tool.registered {"tool":"read_file"} [03] extension.loading {"extension":"example.ts"} [04] tool.registered {"tool":"echo"} [05] extension.loaded {"extension":"example.ts"} [06] agent.started {"maxTurns":8} [07] agent.turn.started {"turn":1} [08] model.request {"model":"gpt-5","messages":1,"tools":["list_files","read_file","echo"]} ``` ## Commands Commands run before the model loop. They are a lightweight control plane for things that do not need inference: ```bash npm run dev -- "/help" npm run dev -- "/tools" ``` The core provides: - `/help` — show available commands - `/reset` — clear the current conversation history - `/exit` — leave the interactive session Extensions can register their own commands. ## System prompt Yes, there is now an explicit system prompt. By default it is: ```text You are a helpful agent. Use tools when they are useful, and answer clearly. ``` Override it with the `SYSTEM_PROMPT` environment variable. Extensions can also append fragments to the final system prompt with `addSystemPrompt(...)`. That gives us a clean path to later add: - more providers - write/edit/bash tools - permissions - session persistence - lifecycle hooks - streaming or a TUI ## Add a tool with an extension Create a file in `extensions/` that exports a default function: ```ts import type { Extension } from "../src/types.js"; const myExtension: Extension = async ({ registerTool, registerCommand, on, addSystemPrompt }) => { await registerTool({ name: "greet", description: "Greet a person by name.", parameters: { type: "object", properties: { name: { type: "string" } }, required: ["name"], additionalProperties: false }, execute(input: { name: string }) { return `Hello, ${input.name}!`; } }); await registerCommand({ name: "greet", description: "Greet someone without calling the model.", execute(args) { return `Hello, ${args || "friend"}!`; } }); addSystemPrompt("Prefer the greet tool when a greeting is explicitly requested."); on("tool.call.completed", ({ tool }) => { if (tool === "greet") { // Observe tool usage, persist metrics, etc. } }); }; export default myExtension; ``` Then use either path: ```bash npm run dev -- "Use the greet tool for Ada." npm run dev -- "/greet Ada" ``` ## Intentional omissions This first version does **not** include file writes, shell execution, approval gates, or persistent sessions. Those are useful, but adding them after the core shape is visible keeps the harness easier to reason about.