Files
AI-Harness-Basic/README.md
2026-05-15 15:09:48 -04:00

233 lines
6.2 KiB
Markdown

# 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/memory.ts` — file-backed durable memory
- `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/tools/memory.ts` — memory tools backed by markdown files
- `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>
```
Interactive responses stream into the terminal as they are generated, and the
session keeps conversation history until you reset it. The current session is
saved to `.harness/session.json` and reloaded the next time the app starts.
The interactive mode is a small full-screen TUI: the transcript scrolls above,
while the input composer stays pinned to the bottom of the terminal. Press
`Ctrl+J` to insert a newline while composing.
```text
┌ transcript
│ system> Loaded 3 conversation items...
│ you> ...
│ assistant> ...
└────────────────────────────────
you> input stays here
```
For multiline input:
```text
you> summarize this:
line two
line three
```
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
- `/history` — show current session details
- `/new` — start a fresh empty conversation
- `/reset` — clear the current conversation history
- `/save` — persist the current conversation immediately
- `/logs [on|off]` — toggle lifecycle traces
- `/memory` — inspect durable memory files
- `/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(...)`.
`USER.md` is also loaded into the system prompt on every run so stable user
preferences are available immediately. `MEMORY.md` and daily logs remain
tool-driven rather than being injected automatically.
## Durable memory
The harness keeps long-lived memory in simple markdown files:
```text
.harness/memory/
MEMORY.md
USER.md
daily/
YYYY-MM-DD.md
```
The memory model is intentionally split:
- `USER.md` is always injected into the system prompt
- `MEMORY.md` is available through tools when broader durable context is useful
- `daily/` stays tool-driven for episodic notes
The model can choose to use:
- `memory_read` — read `MEMORY.md` or `USER.md`
- `memory_write` — append durable facts or user preferences
- `memory_log` — append chronological notes to today's daily file
The split is intentional:
- `MEMORY.md` — durable project or world facts
- `USER.md` — user preferences and profile
- `daily/` — episodic notes that are useful as chronology rather than permanent truth
These files are plain markdown so they remain inspectable and easy to edit by hand.
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 version still does **not** include file writes, shell execution, or approval
gates. Those are useful, but adding them after the core shape is visible keeps
the harness easier to reason about.