@openharness/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +267 -0
- package/dist/agent.d.ts +122 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +279 -0
- package/dist/agent.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/instructions.d.ts +10 -0
- package/dist/instructions.d.ts.map +1 -0
- package/dist/instructions.js +46 -0
- package/dist/instructions.js.map +1 -0
- package/dist/mcp.d.ts +36 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +52 -0
- package/dist/mcp.js.map +1 -0
- package/dist/tools/bash.d.ts +19 -0
- package/dist/tools/bash.d.ts.map +1 -0
- package/dist/tools/bash.js +39 -0
- package/dist/tools/bash.js.map +1 -0
- package/dist/tools/fs.d.ts +143 -0
- package/dist/tools/fs.d.ts.map +1 -0
- package/dist/tools/fs.js +203 -0
- package/dist/tools/fs.js.map +1 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# OpenHarness
|
|
2
|
+
|
|
3
|
+
Claude Code, Codex, OpenCode et al. are amazing general purpose agent harnesses that go far beyond just software development.
|
|
4
|
+
|
|
5
|
+
And while Anthropic offers the Claude Agent SDK, OpenAI now offers the Codex App Server, and OpenCode has a client to connect to an OpenCode instance, these harnesses are very "heavy" to use programmatically.
|
|
6
|
+
|
|
7
|
+
OpenHarness is an open source project based on Vercel's AI SDK that aims to provide the building blocks to build very capable, general-purpose agents in code. It is inspired by all of the aforementioned coding agents.
|
|
8
|
+
|
|
9
|
+
## Agents
|
|
10
|
+
|
|
11
|
+
The `Agent` class is the core primitive. An agent wraps a language model, a set of tools, and a multi-step execution loop into a single object that you can `run()` with a prompt.
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { Agent } from "@openharness/core";
|
|
15
|
+
import { openai } from "@ai-sdk/openai";
|
|
16
|
+
import { fsTools } from "@openharness/core/tools/fs";
|
|
17
|
+
import { bash } from "@openharness/core/tools/bash";
|
|
18
|
+
|
|
19
|
+
const agent = new Agent({
|
|
20
|
+
name: "dev",
|
|
21
|
+
model: openai("gpt-5.2"),
|
|
22
|
+
systemPrompt: "You are a helpful coding assistant.",
|
|
23
|
+
tools: { ...fsTools, bash },
|
|
24
|
+
maxSteps: 20,
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Running an agent
|
|
29
|
+
|
|
30
|
+
`agent.run()` is an async generator that yields a stream of typed events as the agent works. You iterate over these events to build any UI you want — a CLI, a web app, a log file, or nothing at all.
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
for await (const event of agent.run("Refactor the auth module to use JWTs")) {
|
|
34
|
+
switch (event.type) {
|
|
35
|
+
case "text.delta":
|
|
36
|
+
process.stdout.write(event.text);
|
|
37
|
+
break;
|
|
38
|
+
case "tool.start":
|
|
39
|
+
console.log(`Calling ${event.toolName}...`);
|
|
40
|
+
break;
|
|
41
|
+
case "tool.done":
|
|
42
|
+
console.log(`${event.toolName} finished`);
|
|
43
|
+
break;
|
|
44
|
+
case "done":
|
|
45
|
+
console.log(`Result: ${event.result}, tokens: ${event.totalUsage.totalTokens}`);
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The agent maintains conversation history across `run()` calls, so you can use it in a loop for multi-turn interactions.
|
|
52
|
+
|
|
53
|
+
### Events
|
|
54
|
+
|
|
55
|
+
The full set of events emitted by `run()`:
|
|
56
|
+
|
|
57
|
+
| Event | Description |
|
|
58
|
+
| --- | --- |
|
|
59
|
+
| `text.delta` | Streamed text chunk from the model |
|
|
60
|
+
| `text.done` | Full text for the current step is complete |
|
|
61
|
+
| `reasoning.delta` | Streamed reasoning/thinking chunk (if the model supports it) |
|
|
62
|
+
| `reasoning.done` | Full reasoning text for the step is complete |
|
|
63
|
+
| `tool.start` | A tool call has been initiated |
|
|
64
|
+
| `tool.done` | A tool call completed successfully |
|
|
65
|
+
| `tool.error` | A tool call failed |
|
|
66
|
+
| `step.start` | A new agentic step is starting |
|
|
67
|
+
| `step.done` | A step completed (includes token usage and finish reason) |
|
|
68
|
+
| `error` | An error occurred during execution |
|
|
69
|
+
| `done` | The agent has finished. `result` is one of `"complete"`, `"stopped"`, `"max_steps"`, or `"error"` |
|
|
70
|
+
|
|
71
|
+
### Configuration
|
|
72
|
+
|
|
73
|
+
| Option | Default | Description |
|
|
74
|
+
| --- | --- | --- |
|
|
75
|
+
| `name` | (required) | Agent name, used in logging and subagent selection |
|
|
76
|
+
| `model` | (required) | Any Vercel AI SDK `LanguageModel` |
|
|
77
|
+
| `systemPrompt` | — | System prompt prepended to every request |
|
|
78
|
+
| `tools` | — | AI SDK `ToolSet` — the tools the agent can call |
|
|
79
|
+
| `maxSteps` | `100` | Maximum agentic steps before stopping |
|
|
80
|
+
| `temperature` | — | Sampling temperature |
|
|
81
|
+
| `maxTokens` | — | Max output tokens per step |
|
|
82
|
+
| `instructions` | `true` | Whether to load `AGENTS.md` / `CLAUDE.md` from the project directory |
|
|
83
|
+
| `approve` | — | Callback for tool call approval (see [Permissions](#permissions)) |
|
|
84
|
+
| `subagents` | — | Child agents available via the `task` tool (see [Subagents](#subagents)) |
|
|
85
|
+
| `mcpServers` | — | MCP servers to connect to (see [MCP Servers](#mcp-servers)) |
|
|
86
|
+
|
|
87
|
+
## Tools
|
|
88
|
+
|
|
89
|
+
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.
|
|
90
|
+
|
|
91
|
+
### Filesystem tools (`@openharness/core/tools/fs`)
|
|
92
|
+
|
|
93
|
+
| Tool | Description |
|
|
94
|
+
| --- | --- |
|
|
95
|
+
| `readFile` | Read file contents (supports line offset/limit) |
|
|
96
|
+
| `writeFile` | Write content to a file (creates parent dirs) |
|
|
97
|
+
| `editFile` | Find-and-replace within a file |
|
|
98
|
+
| `listFiles` | List files/directories (optionally recursive) |
|
|
99
|
+
| `grep` | Regex search across files (skips `node_modules`, `.git`) |
|
|
100
|
+
| `deleteFile` | Delete a file or directory |
|
|
101
|
+
|
|
102
|
+
All are exported individually and also grouped as `fsTools`.
|
|
103
|
+
|
|
104
|
+
### Bash tool (`@openharness/core/tools/bash`)
|
|
105
|
+
|
|
106
|
+
Runs arbitrary shell commands via `bash -c`. Configurable timeout (default 30s, max 5min) and automatic output truncation.
|
|
107
|
+
|
|
108
|
+
### Custom tools
|
|
109
|
+
|
|
110
|
+
Any AI SDK-compatible tool works. Just define it with `tool()` from the `ai` package:
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import { tool } from "ai";
|
|
114
|
+
import { z } from "zod";
|
|
115
|
+
|
|
116
|
+
const myTool = tool({
|
|
117
|
+
description: "Do something useful",
|
|
118
|
+
inputSchema: z.object({ query: z.string() }),
|
|
119
|
+
execute: async ({ query }) => {
|
|
120
|
+
return { result: `You asked: ${query}` };
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const agent = new Agent({
|
|
125
|
+
name: "my-agent",
|
|
126
|
+
model: openai("gpt-5.2"),
|
|
127
|
+
tools: { myTool },
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Permissions
|
|
132
|
+
|
|
133
|
+
By default, all tool calls are allowed. To gate tool execution — for example, prompting a user for confirmation — pass an `approve` callback:
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
const agent = new Agent({
|
|
137
|
+
name: "safe-agent",
|
|
138
|
+
model: openai("gpt-5.2"),
|
|
139
|
+
tools: { ...fsTools, bash },
|
|
140
|
+
approve: async ({ toolName, toolCallId, input }) => {
|
|
141
|
+
// Return true to allow, false to deny
|
|
142
|
+
const answer = await askUser(`Allow ${toolName}?`);
|
|
143
|
+
return answer === "yes";
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
When a tool call is denied, a `ToolDeniedError` is thrown and surfaced to the model as a tool error, so it can adjust its approach.
|
|
149
|
+
|
|
150
|
+
The callback receives a `ToolCallInfo` object:
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
interface ToolCallInfo {
|
|
154
|
+
toolName: string;
|
|
155
|
+
toolCallId: string;
|
|
156
|
+
input: unknown;
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The callback can be async — you can prompt a user in a terminal, show a modal in a web UI, or call an external approval service.
|
|
161
|
+
|
|
162
|
+
## Subagents
|
|
163
|
+
|
|
164
|
+
Agents can delegate work to other agents. When you pass a `subagents` array, a `task` tool is automatically generated that lets the parent agent spawn child agents by name.
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
const explore = new Agent({
|
|
168
|
+
name: "explore",
|
|
169
|
+
description: "Read-only codebase exploration. Use for searching and reading files.",
|
|
170
|
+
model: openai("gpt-5.2"),
|
|
171
|
+
tools: { readFile, listFiles, grep },
|
|
172
|
+
maxSteps: 30,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const agent = new Agent({
|
|
176
|
+
name: "dev",
|
|
177
|
+
model: openai("gpt-5.2"),
|
|
178
|
+
tools: { ...fsTools, bash },
|
|
179
|
+
subagents: [explore],
|
|
180
|
+
});
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
The parent model sees a `task` tool with a description listing the available subagents and their descriptions. It can call `task` with an `agent` name and a `prompt`, and the subagent runs to completion autonomously.
|
|
184
|
+
|
|
185
|
+
Key behaviors:
|
|
186
|
+
|
|
187
|
+
- **Fresh instance per task** — each `task` call creates a new agent with no shared conversation state
|
|
188
|
+
- **No approval** — subagents run autonomously without prompting for permission
|
|
189
|
+
- **No nesting** — subagents cannot themselves have subagents
|
|
190
|
+
- **Abort propagation** — the parent's abort signal is forwarded to the child
|
|
191
|
+
- **Concurrent execution** — the model can call `task` multiple times in one response to run subagents in parallel
|
|
192
|
+
|
|
193
|
+
### Live subagent events
|
|
194
|
+
|
|
195
|
+
To observe what subagents are doing in real time, pass an `onSubagentEvent` callback:
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
const agent = new Agent({
|
|
199
|
+
name: "dev",
|
|
200
|
+
model: openai("gpt-5.2"),
|
|
201
|
+
tools: { ...fsTools, bash },
|
|
202
|
+
subagents: [explore],
|
|
203
|
+
onSubagentEvent: (agentName, event) => {
|
|
204
|
+
if (event.type === "tool.done") {
|
|
205
|
+
console.log(`[${agentName}] ${event.toolName} completed`);
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
The callback receives the same `AgentEvent` types as the parent's `run()` generator.
|
|
212
|
+
|
|
213
|
+
## AGENTS.md
|
|
214
|
+
|
|
215
|
+
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.
|
|
216
|
+
|
|
217
|
+
This is enabled by default. Set `instructions: false` to disable it.
|
|
218
|
+
|
|
219
|
+
## MCP Servers
|
|
220
|
+
|
|
221
|
+
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.
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
const agent = new Agent({
|
|
225
|
+
name: "dev",
|
|
226
|
+
model: openai("gpt-5.2"),
|
|
227
|
+
tools: { ...fsTools, bash },
|
|
228
|
+
mcpServers: {
|
|
229
|
+
github: {
|
|
230
|
+
type: "stdio",
|
|
231
|
+
command: "npx",
|
|
232
|
+
args: ["-y", "@modelcontextprotocol/server-github"],
|
|
233
|
+
env: { GITHUB_TOKEN: process.env.GITHUB_TOKEN },
|
|
234
|
+
},
|
|
235
|
+
weather: {
|
|
236
|
+
type: "http",
|
|
237
|
+
url: "https://weather-mcp.example.com/mcp",
|
|
238
|
+
headers: { Authorization: "Bearer ..." },
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// MCP connections are established lazily on first run()
|
|
244
|
+
for await (const event of agent.run("What PRs are open?")) { ... }
|
|
245
|
+
|
|
246
|
+
// Clean up MCP connections when done
|
|
247
|
+
await agent.close();
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Three transport types are supported:
|
|
251
|
+
|
|
252
|
+
| Transport | Use case |
|
|
253
|
+
| --- | --- |
|
|
254
|
+
| `stdio` | Local servers — spawns a child process, communicates over stdin/stdout |
|
|
255
|
+
| `http` | Remote servers via Streamable HTTP (recommended for production) |
|
|
256
|
+
| `sse` | Remote servers via Server-Sent Events (legacy) |
|
|
257
|
+
|
|
258
|
+
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.
|
|
259
|
+
|
|
260
|
+
## Example CLI
|
|
261
|
+
|
|
262
|
+
[`example/cli.ts`](example/cli.ts) is a fully working agent CLI that ties everything together — tool approval prompts, ora spinners, streamed output, and live subagent display. It's a good reference for how to wire up all the primitives into an interactive application.
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
# requires a .env file with OPENAI_API_KEY
|
|
266
|
+
pnpm cli
|
|
267
|
+
```
|
package/dist/agent.d.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { type LanguageModel, type ToolSet, type ModelMessage } from "ai";
|
|
2
|
+
import { type MCPServerConfig } from "./mcp.js";
|
|
3
|
+
export interface TokenUsage {
|
|
4
|
+
inputTokens: number | undefined;
|
|
5
|
+
outputTokens: number | undefined;
|
|
6
|
+
totalTokens: number | undefined;
|
|
7
|
+
}
|
|
8
|
+
export type AgentEvent = {
|
|
9
|
+
type: "text.delta";
|
|
10
|
+
text: string;
|
|
11
|
+
} | {
|
|
12
|
+
type: "text.done";
|
|
13
|
+
text: string;
|
|
14
|
+
} | {
|
|
15
|
+
type: "reasoning.delta";
|
|
16
|
+
text: string;
|
|
17
|
+
} | {
|
|
18
|
+
type: "reasoning.done";
|
|
19
|
+
text: string;
|
|
20
|
+
} | {
|
|
21
|
+
type: "tool.start";
|
|
22
|
+
toolCallId: string;
|
|
23
|
+
toolName: string;
|
|
24
|
+
input: unknown;
|
|
25
|
+
} | {
|
|
26
|
+
type: "tool.done";
|
|
27
|
+
toolCallId: string;
|
|
28
|
+
toolName: string;
|
|
29
|
+
output: unknown;
|
|
30
|
+
} | {
|
|
31
|
+
type: "tool.error";
|
|
32
|
+
toolCallId: string;
|
|
33
|
+
toolName: string;
|
|
34
|
+
error: string;
|
|
35
|
+
} | {
|
|
36
|
+
type: "step.start";
|
|
37
|
+
stepNumber: number;
|
|
38
|
+
} | {
|
|
39
|
+
type: "step.done";
|
|
40
|
+
stepNumber: number;
|
|
41
|
+
usage: TokenUsage;
|
|
42
|
+
finishReason: string;
|
|
43
|
+
} | {
|
|
44
|
+
type: "error";
|
|
45
|
+
error: Error;
|
|
46
|
+
} | {
|
|
47
|
+
type: "done";
|
|
48
|
+
result: "complete" | "stopped" | "max_steps" | "error";
|
|
49
|
+
messages: ModelMessage[];
|
|
50
|
+
totalUsage: TokenUsage;
|
|
51
|
+
};
|
|
52
|
+
export interface ToolCallInfo {
|
|
53
|
+
toolName: string;
|
|
54
|
+
toolCallId: string;
|
|
55
|
+
input: unknown;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Called before each tool execution. Return `true` to allow, `false` to deny.
|
|
59
|
+
* Can be async — e.g. to prompt a user in a custom UI.
|
|
60
|
+
*/
|
|
61
|
+
export type ApproveFn = (toolCall: ToolCallInfo) => boolean | Promise<boolean>;
|
|
62
|
+
/** Called for every event emitted by a subagent during a task tool call. */
|
|
63
|
+
export type SubagentEventFn = (agentName: string, event: AgentEvent) => void;
|
|
64
|
+
export declare class Agent {
|
|
65
|
+
readonly name: string;
|
|
66
|
+
readonly description?: string;
|
|
67
|
+
readonly model: LanguageModel;
|
|
68
|
+
readonly systemPrompt?: string;
|
|
69
|
+
readonly maxSteps: number;
|
|
70
|
+
readonly temperature?: number;
|
|
71
|
+
readonly maxTokens?: number;
|
|
72
|
+
readonly instructions: boolean;
|
|
73
|
+
readonly approve?: ApproveFn;
|
|
74
|
+
readonly onSubagentEvent?: SubagentEventFn;
|
|
75
|
+
/** Static tools provided at construction time. */
|
|
76
|
+
readonly tools?: ToolSet;
|
|
77
|
+
/** MCP server configs — connected lazily on first run. */
|
|
78
|
+
private mcpServerConfigs?;
|
|
79
|
+
private mcpConnection;
|
|
80
|
+
private messages;
|
|
81
|
+
private cachedInstructions;
|
|
82
|
+
constructor(options: {
|
|
83
|
+
name: string;
|
|
84
|
+
/** Short description of this agent's purpose. Used in the task tool for subagent selection. */
|
|
85
|
+
description?: string;
|
|
86
|
+
model: LanguageModel;
|
|
87
|
+
systemPrompt?: string;
|
|
88
|
+
tools?: ToolSet;
|
|
89
|
+
maxSteps?: number;
|
|
90
|
+
temperature?: number;
|
|
91
|
+
maxTokens?: number;
|
|
92
|
+
/** Load AGENTS.md / CLAUDE.md from the project directory. Defaults to true. */
|
|
93
|
+
instructions?: boolean;
|
|
94
|
+
/**
|
|
95
|
+
* Called before each tool execution. Return `true` to allow, `false` to deny.
|
|
96
|
+
* When omitted, all tool calls are allowed.
|
|
97
|
+
*/
|
|
98
|
+
approve?: ApproveFn;
|
|
99
|
+
/** Agents available as subagents via the auto-generated `task` tool. */
|
|
100
|
+
subagents?: Agent[];
|
|
101
|
+
/** Called for every event emitted by a subagent during a task tool call. */
|
|
102
|
+
onSubagentEvent?: SubagentEventFn;
|
|
103
|
+
/**
|
|
104
|
+
* MCP servers to connect to. Tools from these servers are merged into
|
|
105
|
+
* the agent's toolset. Connections are established lazily on first run.
|
|
106
|
+
*
|
|
107
|
+
* Keys are server names (used to namespace tools when multiple servers are configured).
|
|
108
|
+
*/
|
|
109
|
+
mcpServers?: Record<string, MCPServerConfig>;
|
|
110
|
+
});
|
|
111
|
+
/**
|
|
112
|
+
* Close all MCP server connections. Call this when the agent is no longer needed.
|
|
113
|
+
*/
|
|
114
|
+
close(): Promise<void>;
|
|
115
|
+
run(input: string | ModelMessage[], options?: {
|
|
116
|
+
signal?: AbortSignal;
|
|
117
|
+
}): AsyncGenerator<AgentEvent>;
|
|
118
|
+
}
|
|
119
|
+
export declare class ToolDeniedError extends Error {
|
|
120
|
+
constructor(toolName: string);
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=agent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"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"}
|
package/dist/agent.js
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { tool, streamText, stepCountIs, } from "ai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { loadInstructions } from "./instructions.js";
|
|
4
|
+
import { connectMCPServers, closeMCPClients, } from "./mcp.js";
|
|
5
|
+
// ── Agent ────────────────────────────────────────────────────────────
|
|
6
|
+
export class Agent {
|
|
7
|
+
name;
|
|
8
|
+
description;
|
|
9
|
+
model;
|
|
10
|
+
systemPrompt;
|
|
11
|
+
maxSteps;
|
|
12
|
+
temperature;
|
|
13
|
+
maxTokens;
|
|
14
|
+
instructions;
|
|
15
|
+
approve;
|
|
16
|
+
onSubagentEvent;
|
|
17
|
+
/** Static tools provided at construction time. */
|
|
18
|
+
tools;
|
|
19
|
+
/** MCP server configs — connected lazily on first run. */
|
|
20
|
+
mcpServerConfigs;
|
|
21
|
+
mcpConnection = null;
|
|
22
|
+
messages = [];
|
|
23
|
+
cachedInstructions = null; // null = not loaded yet
|
|
24
|
+
constructor(options) {
|
|
25
|
+
this.name = options.name;
|
|
26
|
+
this.description = options.description;
|
|
27
|
+
this.model = options.model;
|
|
28
|
+
this.systemPrompt = options.systemPrompt;
|
|
29
|
+
this.maxSteps = options.maxSteps ?? 100;
|
|
30
|
+
this.temperature = options.temperature;
|
|
31
|
+
this.maxTokens = options.maxTokens;
|
|
32
|
+
this.instructions = options.instructions ?? true;
|
|
33
|
+
this.approve = options.approve;
|
|
34
|
+
this.onSubagentEvent = options.onSubagentEvent;
|
|
35
|
+
this.mcpServerConfigs = options.mcpServers;
|
|
36
|
+
// Merge the task tool into the toolset when subagents are provided
|
|
37
|
+
if (options.subagents?.length) {
|
|
38
|
+
this.tools = {
|
|
39
|
+
...(options.tools ?? {}),
|
|
40
|
+
task: createTaskTool(options.subagents, this.onSubagentEvent),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
this.tools = options.tools;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Close all MCP server connections. Call this when the agent is no longer needed.
|
|
49
|
+
*/
|
|
50
|
+
async close() {
|
|
51
|
+
if (this.mcpConnection) {
|
|
52
|
+
await closeMCPClients(this.mcpConnection.clients);
|
|
53
|
+
this.mcpConnection = null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async *run(input, options) {
|
|
57
|
+
if (typeof input === "string") {
|
|
58
|
+
this.messages.push({ role: "user", content: input });
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
this.messages.push(...input);
|
|
62
|
+
}
|
|
63
|
+
// Load AGENTS.md once per agent lifetime
|
|
64
|
+
if (this.instructions && this.cachedInstructions === null) {
|
|
65
|
+
this.cachedInstructions = await loadInstructions();
|
|
66
|
+
}
|
|
67
|
+
// Connect MCP servers once per agent lifetime
|
|
68
|
+
if (this.mcpServerConfigs && !this.mcpConnection) {
|
|
69
|
+
this.mcpConnection = await connectMCPServers(this.mcpServerConfigs);
|
|
70
|
+
}
|
|
71
|
+
const systemParts = [this.systemPrompt, this.cachedInstructions].filter(Boolean);
|
|
72
|
+
const system = systemParts.length > 0 ? systemParts.join("\n\n") : undefined;
|
|
73
|
+
// Merge static tools with MCP tools
|
|
74
|
+
const allTools = {
|
|
75
|
+
...(this.tools ?? {}),
|
|
76
|
+
...(this.mcpConnection?.tools ?? {}),
|
|
77
|
+
};
|
|
78
|
+
const tools = this.approve && Object.keys(allTools).length > 0
|
|
79
|
+
? wrapToolsWithApproval(allTools, this.approve)
|
|
80
|
+
: Object.keys(allTools).length > 0
|
|
81
|
+
? allTools
|
|
82
|
+
: undefined;
|
|
83
|
+
const stream = streamText({
|
|
84
|
+
model: this.model,
|
|
85
|
+
system,
|
|
86
|
+
messages: this.messages,
|
|
87
|
+
tools,
|
|
88
|
+
stopWhen: stepCountIs(this.maxSteps),
|
|
89
|
+
temperature: this.temperature,
|
|
90
|
+
maxOutputTokens: this.maxTokens,
|
|
91
|
+
abortSignal: options?.signal,
|
|
92
|
+
});
|
|
93
|
+
let stepNumber = 0;
|
|
94
|
+
let stepText = "";
|
|
95
|
+
let stepReasoning = "";
|
|
96
|
+
try {
|
|
97
|
+
for await (const part of stream.fullStream) {
|
|
98
|
+
switch (part.type) {
|
|
99
|
+
case "start-step":
|
|
100
|
+
stepNumber++;
|
|
101
|
+
stepText = "";
|
|
102
|
+
stepReasoning = "";
|
|
103
|
+
yield { type: "step.start", stepNumber };
|
|
104
|
+
break;
|
|
105
|
+
case "text-delta":
|
|
106
|
+
stepText += part.text;
|
|
107
|
+
yield { type: "text.delta", text: part.text };
|
|
108
|
+
break;
|
|
109
|
+
case "text-end":
|
|
110
|
+
if (stepText) {
|
|
111
|
+
yield { type: "text.done", text: stepText };
|
|
112
|
+
}
|
|
113
|
+
break;
|
|
114
|
+
case "reasoning-delta":
|
|
115
|
+
stepReasoning += part.text;
|
|
116
|
+
yield { type: "reasoning.delta", text: part.text };
|
|
117
|
+
break;
|
|
118
|
+
case "reasoning-end":
|
|
119
|
+
if (stepReasoning) {
|
|
120
|
+
yield { type: "reasoning.done", text: stepReasoning };
|
|
121
|
+
}
|
|
122
|
+
break;
|
|
123
|
+
case "tool-call":
|
|
124
|
+
yield {
|
|
125
|
+
type: "tool.start",
|
|
126
|
+
toolCallId: part.toolCallId,
|
|
127
|
+
toolName: part.toolName,
|
|
128
|
+
input: part.input,
|
|
129
|
+
};
|
|
130
|
+
break;
|
|
131
|
+
case "tool-result":
|
|
132
|
+
yield {
|
|
133
|
+
type: "tool.done",
|
|
134
|
+
toolCallId: part.toolCallId,
|
|
135
|
+
toolName: part.toolName,
|
|
136
|
+
output: part.output,
|
|
137
|
+
};
|
|
138
|
+
break;
|
|
139
|
+
case "tool-error":
|
|
140
|
+
yield {
|
|
141
|
+
type: "tool.error",
|
|
142
|
+
toolCallId: part.toolCallId,
|
|
143
|
+
toolName: part.toolName,
|
|
144
|
+
error: String(part.error),
|
|
145
|
+
};
|
|
146
|
+
break;
|
|
147
|
+
case "finish-step":
|
|
148
|
+
yield {
|
|
149
|
+
type: "step.done",
|
|
150
|
+
stepNumber,
|
|
151
|
+
usage: toTokenUsage(part.usage),
|
|
152
|
+
finishReason: part.finishReason,
|
|
153
|
+
};
|
|
154
|
+
break;
|
|
155
|
+
case "error":
|
|
156
|
+
yield {
|
|
157
|
+
type: "error",
|
|
158
|
+
error: part.error instanceof Error ? part.error : new Error(String(part.error)),
|
|
159
|
+
};
|
|
160
|
+
break;
|
|
161
|
+
case "finish": {
|
|
162
|
+
const result = part.finishReason === "stop"
|
|
163
|
+
? "complete"
|
|
164
|
+
: part.finishReason === "tool-calls"
|
|
165
|
+
? "max_steps"
|
|
166
|
+
: part.finishReason === "error"
|
|
167
|
+
? "error"
|
|
168
|
+
: "stopped";
|
|
169
|
+
const response = await stream.response;
|
|
170
|
+
this.messages.push(...response.messages);
|
|
171
|
+
yield {
|
|
172
|
+
type: "done",
|
|
173
|
+
result,
|
|
174
|
+
messages: this.messages,
|
|
175
|
+
totalUsage: toTokenUsage(part.totalUsage),
|
|
176
|
+
};
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
yield {
|
|
184
|
+
type: "error",
|
|
185
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
186
|
+
};
|
|
187
|
+
yield {
|
|
188
|
+
type: "done",
|
|
189
|
+
result: "error",
|
|
190
|
+
messages: this.messages,
|
|
191
|
+
totalUsage: { inputTokens: undefined, outputTokens: undefined, totalTokens: undefined },
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// ── Subagent task tool ───────────────────────────────────────────────
|
|
197
|
+
function createTaskTool(subagents, onSubagentEvent) {
|
|
198
|
+
const names = subagents.map((a) => a.name);
|
|
199
|
+
const byName = new Map(subagents.map((a) => [a.name, a]));
|
|
200
|
+
const listing = subagents.map((a) => `- ${a.name}: ${a.description ?? a.name}`).join("\n");
|
|
201
|
+
return tool({
|
|
202
|
+
description: [
|
|
203
|
+
"Spawn a subagent to handle a task autonomously.",
|
|
204
|
+
"The subagent runs with its own tools, completes the work, and returns the result.",
|
|
205
|
+
"Launch multiple agents concurrently when possible by calling this tool multiple times in one response.",
|
|
206
|
+
"",
|
|
207
|
+
"Available agents:",
|
|
208
|
+
listing,
|
|
209
|
+
].join("\n"),
|
|
210
|
+
inputSchema: z.object({
|
|
211
|
+
agent: z.enum(names).describe("Which agent to use"),
|
|
212
|
+
prompt: z.string().describe("Detailed task description for the subagent"),
|
|
213
|
+
}),
|
|
214
|
+
execute: async ({ agent: agentName, prompt }, { abortSignal }) => {
|
|
215
|
+
const template = byName.get(agentName);
|
|
216
|
+
// Fresh agent instance for each task — no shared state
|
|
217
|
+
const child = new Agent({
|
|
218
|
+
name: template.name,
|
|
219
|
+
model: template.model,
|
|
220
|
+
systemPrompt: template.systemPrompt,
|
|
221
|
+
tools: template.tools,
|
|
222
|
+
maxSteps: template.maxSteps,
|
|
223
|
+
temperature: template.temperature,
|
|
224
|
+
maxTokens: template.maxTokens,
|
|
225
|
+
instructions: template.instructions,
|
|
226
|
+
// No approve — subagents run autonomously
|
|
227
|
+
// No subagents — prevent recursive nesting
|
|
228
|
+
});
|
|
229
|
+
let lastText = "";
|
|
230
|
+
for await (const event of child.run(prompt, { signal: abortSignal })) {
|
|
231
|
+
onSubagentEvent?.(agentName, event);
|
|
232
|
+
if (event.type === "text.done") {
|
|
233
|
+
lastText = event.text;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return `<task_result>\n${lastText || "(no output)"}\n</task_result>`;
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
241
|
+
function toTokenUsage(usage) {
|
|
242
|
+
return {
|
|
243
|
+
inputTokens: usage.inputTokens,
|
|
244
|
+
outputTokens: usage.outputTokens,
|
|
245
|
+
totalTokens: usage.totalTokens,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function wrapToolsWithApproval(tools, approve) {
|
|
249
|
+
const wrapped = {};
|
|
250
|
+
for (const [name, t] of Object.entries(tools)) {
|
|
251
|
+
if (!t.execute) {
|
|
252
|
+
wrapped[name] = t;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
const originalExecute = t.execute;
|
|
256
|
+
wrapped[name] = {
|
|
257
|
+
...t,
|
|
258
|
+
execute: async (input, options) => {
|
|
259
|
+
const allowed = await approve({
|
|
260
|
+
toolName: name,
|
|
261
|
+
toolCallId: options.toolCallId,
|
|
262
|
+
input,
|
|
263
|
+
});
|
|
264
|
+
if (!allowed) {
|
|
265
|
+
throw new ToolDeniedError(name);
|
|
266
|
+
}
|
|
267
|
+
return originalExecute(input, options);
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
return wrapped;
|
|
272
|
+
}
|
|
273
|
+
export class ToolDeniedError extends Error {
|
|
274
|
+
constructor(toolName) {
|
|
275
|
+
super(`Tool call to "${toolName}" was denied.`);
|
|
276
|
+
this.name = "ToolDeniedError";
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
//# sourceMappingURL=agent.js.map
|