@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 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
+ ```
@@ -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