subharness 0.0.5 → 0.0.7
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 +59 -3
- package/dist/adapters/claude-process.js +8 -1
- package/dist/adapters/claude-process.js.map +1 -1
- package/dist/adapters/claude-tools.d.ts +6 -2
- package/dist/adapters/claude-tools.js +14 -12
- package/dist/adapters/claude-tools.js.map +1 -1
- package/dist/adapters/claude-worker-client.d.ts +49 -0
- package/dist/adapters/claude-worker-client.js +359 -0
- package/dist/adapters/claude-worker-client.js.map +1 -0
- package/dist/adapters/claude-worker-process.d.ts +20 -0
- package/dist/adapters/claude-worker-process.js +76 -0
- package/dist/adapters/claude-worker-process.js.map +1 -0
- package/dist/adapters/claude-worker-protocol.d.ts +38 -0
- package/dist/adapters/claude-worker-protocol.js +2 -0
- package/dist/adapters/claude-worker-protocol.js.map +1 -0
- package/dist/adapters/claude-worker.d.ts +1 -0
- package/dist/adapters/claude-worker.js +126 -0
- package/dist/adapters/claude-worker.js.map +1 -0
- package/dist/adapters/claude.d.ts +16 -6
- package/dist/adapters/claude.js +138 -68
- package/dist/adapters/claude.js.map +1 -1
- package/dist/cli/catalog-worker.js.map +1 -1
- package/dist/cli/dashboard-client.js +19 -30
- package/dist/cli/dashboard-client.js.map +1 -1
- package/dist/cli/main.js +53 -45
- package/dist/cli/main.js.map +1 -1
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/runtime/approval-registry.d.ts +9 -0
- package/dist/runtime/approval-registry.js +143 -4
- package/dist/runtime/approval-registry.js.map +1 -1
- package/dist/runtime/capture.d.ts +22 -0
- package/dist/runtime/capture.js +320 -0
- package/dist/runtime/capture.js.map +1 -0
- package/dist/runtime/catalog.d.ts +39 -0
- package/dist/runtime/catalog.js +84 -0
- package/dist/runtime/catalog.js.map +1 -0
- package/dist/runtime/client.d.ts +2 -1
- package/dist/runtime/client.js +52 -65
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/coordinator.d.ts +44 -7
- package/dist/runtime/coordinator.js +265 -31
- package/dist/runtime/coordinator.js.map +1 -1
- package/dist/runtime/daemon.js +12 -6
- package/dist/runtime/daemon.js.map +1 -1
- package/dist/runtime/dashboard-workspace.js +5 -1
- package/dist/runtime/dashboard-workspace.js.map +1 -1
- package/dist/runtime/definition.d.ts +6 -1
- package/dist/runtime/definition.js +31 -1
- package/dist/runtime/definition.js.map +1 -1
- package/dist/runtime/native-owner.js +7 -1
- package/dist/runtime/native-owner.js.map +1 -1
- package/dist/runtime/pagination.d.ts +9 -0
- package/dist/runtime/pagination.js +36 -0
- package/dist/runtime/pagination.js.map +1 -0
- package/dist/runtime/sdk-observation.d.ts +8 -0
- package/dist/runtime/sdk-observation.js +75 -0
- package/dist/runtime/sdk-observation.js.map +1 -0
- package/dist/runtime/sdk-projection.d.ts +8 -0
- package/dist/runtime/sdk-projection.js +19 -0
- package/dist/runtime/sdk-projection.js.map +1 -0
- package/dist/runtime/sdk-service.d.ts +11 -0
- package/dist/runtime/sdk-service.js +109 -0
- package/dist/runtime/sdk-service.js.map +1 -0
- package/dist/runtime/select-native.d.ts +4 -1
- package/dist/runtime/select-native.js +14 -17
- package/dist/runtime/select-native.js.map +1 -1
- package/dist/runtime/service.d.ts +11 -1
- package/dist/runtime/service.js +178 -3
- package/dist/runtime/service.js.map +1 -1
- package/dist/runtime/session-launcher.js +1 -1
- package/dist/runtime/session-launcher.js.map +1 -1
- package/dist/runtime/snapshots.d.ts +103 -0
- package/dist/runtime/snapshots.js +124 -0
- package/dist/runtime/snapshots.js.map +1 -0
- package/dist/runtime/state.d.ts +26 -0
- package/dist/runtime/state.js +86 -17
- package/dist/runtime/state.js.map +1 -1
- package/dist/runtime/task-data.d.ts +38 -0
- package/dist/runtime/task-data.js +2 -0
- package/dist/runtime/task-data.js.map +1 -0
- package/dist/runtime/transport.d.ts +13 -0
- package/dist/runtime/transport.js +166 -0
- package/dist/runtime/transport.js.map +1 -0
- package/dist/runtime/types.d.ts +28 -9
- package/dist/runtime/types.js.map +1 -1
- package/dist/runtime/worker-client.js +2 -0
- package/dist/runtime/worker-client.js.map +1 -1
- package/dist/runtime/worker-server.js +105 -24
- package/dist/runtime/worker-server.js.map +1 -1
- package/dist/sdk/connected-types.d.ts +42 -0
- package/dist/sdk/connected-types.js +2 -0
- package/dist/sdk/connected-types.js.map +1 -0
- package/dist/sdk/connected.d.ts +3 -0
- package/dist/sdk/connected.js +232 -0
- package/dist/sdk/connected.js.map +1 -0
- package/dist/sdk/execution-definition.d.ts +3 -0
- package/dist/sdk/execution-definition.js +59 -0
- package/dist/sdk/execution-definition.js.map +1 -0
- package/dist/sdk/execution-driver.d.ts +6 -0
- package/dist/sdk/execution-driver.js +86 -0
- package/dist/sdk/execution-driver.js.map +1 -0
- package/dist/sdk/execution-observation.d.ts +3 -0
- package/dist/sdk/execution-observation.js +35 -0
- package/dist/sdk/execution-observation.js.map +1 -0
- package/dist/sdk/execution-options.d.ts +17 -0
- package/dist/sdk/execution-options.js +115 -0
- package/dist/sdk/execution-options.js.map +1 -0
- package/dist/sdk/execution-types.d.ts +72 -0
- package/dist/sdk/execution-types.js +2 -0
- package/dist/sdk/execution-types.js.map +1 -0
- package/dist/sdk/execution.d.ts +3 -0
- package/dist/sdk/execution.js +3 -0
- package/dist/sdk/execution.js.map +1 -0
- package/dist/sdk/hosted-error.d.ts +3 -0
- package/dist/sdk/hosted-error.js +14 -0
- package/dist/sdk/hosted-error.js.map +1 -0
- package/dist/sdk/hosted.d.ts +23 -0
- package/dist/sdk/hosted.js +204 -0
- package/dist/sdk/hosted.js.map +1 -0
- package/dist/sdk/runner.d.ts +5 -0
- package/dist/sdk/runner.js +107 -0
- package/dist/sdk/runner.js.map +1 -0
- package/dist/sdk/tools.js +4 -1
- package/dist/sdk/tools.js.map +1 -1
- package/package.json +3 -3
- package/sdk/access-config.md +2 -0
- package/sdk/adapter-contract.md +8 -0
- package/sdk/agent.md +1 -1
- package/sdk/approvals.md +2 -0
- package/sdk/completion-notifications.md +2 -0
- package/sdk/distribution.md +4 -2
- package/sdk/examples/chat-tool.ts +126 -0
- package/sdk/execution.md +743 -0
- package/sdk/index.md +45 -21
- package/sdk/message-delivery.md +2 -0
- package/sdk/permissions.md +2 -0
- package/sdk/plugins/sub-agents.md +4 -2
- package/sdk/project-team.md +23 -1
- package/sdk/sessions.md +2 -0
- package/sdk/tools.md +3 -1
- package/sdk/v1-runtime.md +7 -5
- package/dist/cli/catalog.d.ts +0 -26
- package/dist/cli/catalog.js +0 -41
- package/dist/cli/catalog.js.map +0 -1
package/README.md
CHANGED
|
@@ -10,6 +10,8 @@ Stay with your favorite coding agent. Ask it to delegate tasks across Codex, Cla
|
|
|
10
10
|
|
|
11
11
|
TypeScript definitions are optional. Add them when your coding agent needs reusable specialists with shared instructions, tools, or harness alternatives. Codex and Claude Code use eligible native subscription logins or explicitly configured API access. fx, OpenCode, and GitHub Copilot accept explicitly configured native logins, direct API keys, or Vercel AI Gateway access. Cursor uses an explicitly configured native CLI login or Cursor API key.
|
|
12
12
|
|
|
13
|
+
Use the Node.js execution SDK to build an agent-running UI or expose a specialist as a tool in an existing chat. The application owns its runner and can observe task states, approval requests, and complete responses without invoking the Subharness CLI.
|
|
14
|
+
|
|
13
15
|
The library coordinates sessions, queues, and subagents. Harnesses own model execution, native tools, and context. You supply the working directory and any sandbox or worktree isolation.
|
|
14
16
|
|
|
15
17
|
## Installation
|
|
@@ -73,6 +75,60 @@ subharness run codex --prompt-file - < ./task.txt
|
|
|
73
75
|
|
|
74
76
|
Use `subharness dashboard` in a terminal to watch agents managed by the local coordinator. The live view groups agents by repository/worktree, pins the checkout containing the launch directory first, and orders other groups by their latest agent update. Each group shows current agents followed by finished runs, with harness, task title, elapsed time, and status. Finished history is retained for the coordinator’s lifetime. Use Up/Down to select a run and Enter to open its agent session’s request history. Queued requests appear first, followed by remaining requests newest first. Enter expands a request’s prompt and response; Escape returns to the overview. Completed statuses are green, and a compact Sub-agents footer summarizes direct child agents. The overview has no application banner or footer. Ctrl+C closes the view without stopping agents.
|
|
75
77
|
|
|
78
|
+
## Run agents from application code
|
|
79
|
+
|
|
80
|
+
Install `subharness` as an application dependency:
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
npm install subharness
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Create an application-owned runner and pass an in-memory definition:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { agent, codex, createRunner } from "subharness";
|
|
90
|
+
|
|
91
|
+
const reviewer = agent({
|
|
92
|
+
name: "reviewer",
|
|
93
|
+
description: "Reviews the requested changes.",
|
|
94
|
+
instructions: "Report concrete findings with file references.",
|
|
95
|
+
harness: codex({ model: "CODEX_MODEL_ID" }),
|
|
96
|
+
});
|
|
97
|
+
const runner = createRunner();
|
|
98
|
+
try {
|
|
99
|
+
const session = await runner.createSession(reviewer, { cwd: "/absolute/project/path" });
|
|
100
|
+
const { task } = await session.prompt("Review the current diff.");
|
|
101
|
+
// A separate UI handler must answer any pending approvals.
|
|
102
|
+
console.log((await task.result).text);
|
|
103
|
+
// Reuse session.prompt(...) for follow-ups in the same conversation.
|
|
104
|
+
} finally {
|
|
105
|
+
await runner.close();
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Replace the model placeholder with an available native model. A UI can consume `task.watch({ signal })` for current and changed snapshots. Aborting observation leaves execution running; a Stop action calls `task.cancel()`. Keep the runner for the chat/job lifetime and `await runner.close()` when that scope ends. Native harnesses still need installation and authorized access.
|
|
110
|
+
|
|
111
|
+
Tool functions can close over application services. Embedded sessions keep them in the host process, with its unchanged cwd and environment. Declared subagents use hosted tools instead of CLI launchers. An existing chat app can register a tool whose `execute` calls `session.prompt`, while retaining its own model loop, authorization, and UI transport.
|
|
112
|
+
|
|
113
|
+
To connect a UI backend to agents started by the CLI, use the shared coordinator:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
import { connect } from "subharness";
|
|
117
|
+
|
|
118
|
+
const client = await connect(); // Connects or starts an empty coordinator.
|
|
119
|
+
try {
|
|
120
|
+
const { items: sessions } = await client.sessions.list();
|
|
121
|
+
console.log(sessions);
|
|
122
|
+
// client.sessions.get(id) retrieves the same conversation used by the CLI.
|
|
123
|
+
} finally {
|
|
124
|
+
await client.disconnect(); // Execution continues in the shared coordinator.
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Use `connect({ start: false })` for strict attachment. Approval requests advertise actions; submit a chosen action with `client.approvals.respond(request.id, { actionId: action.id })`, adding explicitly selected grants when required. Applications can render the supplied labels without provider-specific branches.
|
|
129
|
+
|
|
130
|
+
The SDK exposes complete responses, task states, and approvals, without token deltas or native tool-progress events. History lasts for the owning coordinator's lifetime; restart recovery is not provided. See the [execution API](sdk/execution.md) and [chat-tool example](sdk/examples/chat-tool.ts) for lifecycle and integration details. Embedded access is explicit through `createRunner({ access, env })`; connected sessions use the CLI's existing project access settings.
|
|
131
|
+
|
|
76
132
|
## Define a reusable specialist
|
|
77
133
|
|
|
78
134
|
Install the SDK in the project before adding definitions:
|
|
@@ -181,7 +237,7 @@ subharness run subagent:reviewer --cwd /path/to/worktree --prompt "Review the im
|
|
|
181
237
|
|
|
182
238
|
The library supplies the parent context. Declaring `subagents` authorizes their invocation. Claude Code receives permissions scoped to its session launcher, declared children, and coordination commands; native approval rules and sandbox restrictions still apply. Children receive explicit task context rather than a copy of the parent's transcript. Cancellation propagates through delegated descendants; a parent task completes after child results and its own continuation are handled.
|
|
183
239
|
|
|
184
|
-
This checkout includes
|
|
240
|
+
This checkout includes nine [repository roles](sdk/project-team.md): the Astra `architect` and `planner`; Sol `developer` and `integrator`; Luna `api-researcher` and `systems-researcher`; Opus `reviewer`; Gemini `researcher`; and Fable 5.1 `visual-engineer` through fx. Reusable techniques live in `.agents/skills/` and are read only when the task needs them; a new technique does not require another agent definition. Discover the roles with `subharness list` and invoke them through `repo:<name>`. Skill loading, native capability limits, and the image-task execution path are documented with the team.
|
|
185
241
|
|
|
186
242
|
## Boundaries
|
|
187
243
|
|
|
@@ -214,8 +270,8 @@ pnpm run check
|
|
|
214
270
|
pnpm run check:package
|
|
215
271
|
```
|
|
216
272
|
|
|
217
|
-
`pnpm run check` builds the current SDK first, then runs TypeScript checking and behavioral tests. Building first lets repository agent definitions import the package's current exports on a clean checkout and prevents tests from exercising stale build output. Tests cover native protocol boundaries, credential selection, worktrees, task queues, cancellation races, nested CLI execution, and process lifetime.
|
|
273
|
+
`pnpm run check` builds the current SDK first, then runs TypeScript checking and behavioral tests. Building first lets repository agent definitions import the package's current exports on a clean checkout and prevents tests from exercising stale build output. Tests cover native protocol boundaries, credential selection, worktrees, task queues, cancellation races, nested CLI execution, and process lifetime. The default suite runs two test files at a time to keep subprocess-heavy protocol fixtures within their timing budgets.
|
|
218
274
|
|
|
219
|
-
`pnpm run check:package` packs the SDK and verifies installation,
|
|
275
|
+
`pnpm run check:package` packs the SDK and verifies installation, public types, embedded execution, shared CLI/SDK sessions, the chat-tool example, and CLI agent discovery in a temporary consumer project. It downloads dependencies from the public npm registry with install lifecycle scripts disabled and uses a fake native harness without calling a coding model.
|
|
220
276
|
|
|
221
277
|
The [SDK documentation](sdk/index.md) describes the final contracts. [AGENTS.md](AGENTS.md) defines the project's documentation, TDD, and review workflow. Comparative research and review evidence are kept in the ignored `.context` directory.
|
|
@@ -22,6 +22,7 @@ export class ClaudeProcessOwner {
|
|
|
22
22
|
throw new AgentError('HARNESS_FAILED', 'Claude Code attempted to start more than one native process.');
|
|
23
23
|
const child = spawn(options.command, options.args, {
|
|
24
24
|
cwd: options.cwd,
|
|
25
|
+
// The SDK accepts an exact environment replacement; framework augmentations may incorrectly require NODE_ENV.
|
|
25
26
|
env: options.env,
|
|
26
27
|
signal: options.signal,
|
|
27
28
|
stdio: ['pipe', 'pipe', 'ignore'],
|
|
@@ -43,7 +44,13 @@ export class ClaudeProcessOwner {
|
|
|
43
44
|
return processExitDescription(this.exitCode, this.exitSignal);
|
|
44
45
|
}
|
|
45
46
|
close() {
|
|
46
|
-
|
|
47
|
+
if (!this.closing) {
|
|
48
|
+
this.closing = this.stop().catch(error => {
|
|
49
|
+
this.closing = undefined;
|
|
50
|
+
throw error;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return this.closing;
|
|
47
54
|
}
|
|
48
55
|
async stop() {
|
|
49
56
|
const child = this.child;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"claude-process.js","sourceRoot":"","sources":["../../src/adapters/claude-process.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAA4B,MAAM,oBAAoB,CAAC;AAGrE,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAEnE,MAAM,cAAc,GAAG,KAAK,CAAC;AAC7B,MAAM,gBAAgB,GAAG,KAAK,CAAC;AAC/B,MAAM,YAAY,GAAG,KAAK,CAAC;AAE3B,SAAS,WAAW,CAAC,MAAqB,EAAE,OAAe;IACzD,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE;QAC3B,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,EAAC,OAAO,CAAC,CAAC;QACvD,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7D,CAAC,CAAC,CAAC;AACL,CAAC;AAED,8EAA8E;AAC9E,MAAM,OAAO,kBAAkB;IACrB,KAAK,CAA+C;IACpD,MAAM,CAAiB;IACvB,OAAO,CAAiB;IACxB,QAAQ,CAAU;IAClB,UAAU,CAAU;IAEnB,KAAK,GAAG,CAAC,OAAqB,EAAkB,EAAE;QACzD,IAAI,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,UAAU,CAAC,gBAAgB,EAAC,8DAA8D,CAAC,CAAC;QACtH,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,EAAC,OAAO,CAAC,IAAI,EAAC;YAC/C,GAAG,EAAC,OAAO,CAAC,GAAG;YACf,GAAG,EAAC,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"claude-process.js","sourceRoot":"","sources":["../../src/adapters/claude-process.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn, type ChildProcessByStdio } from 'node:child_process';\nimport type { Readable, Writable } from 'node:stream';\nimport type { SpawnedProcess, SpawnOptions } from '@anthropic-ai/claude-agent-sdk';\nimport { AgentError } from '../errors.js';\nimport { processExitDescription } from '../process-diagnostics.js';\n\nconst gracefulExitMs = 2_000;\nconst terminatedExitMs = 5_000;\nconst killedExitMs = 1_000;\n\nfunction exitsWithin(exited: Promise<void>, timeout: number): Promise<boolean> {\n return new Promise(resolve => {\n const timer = setTimeout(() => resolve(false),timeout);\n exited.then(() => { clearTimeout(timer); resolve(true); });\n });\n}\n\n/** Retains the concrete SDK subprocess until its termination is confirmed. */\nexport class ClaudeProcessOwner {\n private child?: ChildProcessByStdio<Writable,Readable,null>;\n private exited?: Promise<void>;\n private closing?: Promise<void>;\n private exitCode: unknown;\n private exitSignal: unknown;\n\n readonly spawn = (options: SpawnOptions): SpawnedProcess => {\n if (this.child) throw new AgentError('HARNESS_FAILED','Claude Code attempted to start more than one native process.');\n const child = spawn(options.command,options.args,{\n cwd:options.cwd,\n // The SDK accepts an exact environment replacement; framework augmentations may incorrectly require NODE_ENV.\n env:options.env as NodeJS.ProcessEnv,\n signal:options.signal,\n stdio:['pipe','pipe','ignore'],\n windowsHide:true,\n });\n this.child = child;\n this.exited = new Promise(resolve => {\n child.once('exit',(code,signal) => {\n this.exitCode = code;\n this.exitSignal = signal;\n resolve();\n });\n child.once('error',() => { if (child.pid === undefined) resolve(); });\n });\n return child;\n };\n\n exitDescription(): string {\n return processExitDescription(this.exitCode,this.exitSignal);\n }\n\n close(): Promise<void> {\n if (!this.closing) {\n this.closing = this.stop().catch(error => {\n this.closing = undefined;\n throw error;\n });\n }\n return this.closing;\n }\n\n private async stop(): Promise<void> {\n const child = this.child;\n const exited = this.exited;\n if (!child || !exited) return;\n try { child.stdin.end(); } catch { /* Continue to bounded termination. */ }\n if (await exitsWithin(exited,gracefulExitMs)) return;\n child.kill('SIGTERM');\n if (await exitsWithin(exited,terminatedExitMs)) return;\n child.kill('SIGKILL');\n if (await exitsWithin(exited,killedExitMs)) return;\n throw new AgentError('HARNESS_FAILED','Claude Code process termination could not be confirmed.');\n }\n}\n"]}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
2
|
import type { ExecutionDefinition } from "./types.js";
|
|
3
|
+
import type { ClaudeToolDescriptor } from "./claude-worker-protocol.js";
|
|
3
4
|
/** The native process may stop before host-side JavaScript callbacks finish. */
|
|
4
5
|
export declare function claudeTools(agent: Pick<ExecutionDefinition, "tools">): {
|
|
5
|
-
|
|
6
|
+
namespace: string;
|
|
7
|
+
descriptors: ClaudeToolDescriptor[];
|
|
8
|
+
allowedTools: string[];
|
|
9
|
+
call(name: string, input: unknown): Promise<CallToolResult>;
|
|
6
10
|
start(): void;
|
|
7
11
|
stopAdmission(): void;
|
|
8
12
|
drain(): Promise<void>;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
|
|
3
2
|
import { AgentError } from "../errors.js";
|
|
4
3
|
import { executeTool, toolJsonSchema } from "../sdk/tools.js";
|
|
5
4
|
import { mcpToolContent } from "./mcp-tool-content.js";
|
|
@@ -9,9 +8,19 @@ export function claudeTools(agent) {
|
|
|
9
8
|
let accepting = false;
|
|
10
9
|
const definitions = Object.entries(agent.tools ?? {});
|
|
11
10
|
const namespace = `subharness_${randomUUID().replaceAll("-", "")}`;
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
const descriptors = definitions.map(([name, definition]) => ({
|
|
12
|
+
name,
|
|
13
|
+
description: definition.description,
|
|
14
|
+
inputSchema: toolJsonSchema(definition),
|
|
15
|
+
}));
|
|
16
|
+
return {
|
|
17
|
+
namespace,
|
|
18
|
+
descriptors,
|
|
19
|
+
allowedTools: definitions.map(([name]) => `mcp__${namespace}__${name}`),
|
|
20
|
+
async call(name, input) {
|
|
21
|
+
const definition = agent.tools?.[name];
|
|
22
|
+
if (!definition)
|
|
23
|
+
return errorResult("The custom tool is unavailable.");
|
|
15
24
|
if (!accepting)
|
|
16
25
|
return errorResult("The task is not accepting tool calls.");
|
|
17
26
|
const execution = executeTool(definition, input);
|
|
@@ -25,14 +34,7 @@ export function claudeTools(agent) {
|
|
|
25
34
|
finally {
|
|
26
35
|
pending.delete(execution);
|
|
27
36
|
}
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
const config = definitions.length === 0 ? {} : {
|
|
31
|
-
mcpServers: { [namespace]: createSdkMcpServer({ name: namespace, version: "1.0.0", tools }) },
|
|
32
|
-
allowedTools: definitions.map(([name]) => `mcp__${namespace}__${name}`),
|
|
33
|
-
};
|
|
34
|
-
return {
|
|
35
|
-
config,
|
|
37
|
+
},
|
|
36
38
|
start() { accepting = true; },
|
|
37
39
|
stopAdmission() { accepting = false; },
|
|
38
40
|
async drain() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"claude-tools.js","sourceRoot":"","sources":["../../src/adapters/claude-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"claude-tools.js","sourceRoot":"","sources":["../../src/adapters/claude-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAE9D,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAGvD,gFAAgF;AAChF,MAAM,UAAU,WAAW,CAAC,KAAyC;IACnE,MAAM,OAAO,GAAG,IAAI,GAAG,EAAoB,CAAC;IAC5C,IAAI,SAAS,GAAG,KAAK,CAAC;IACtB,MAAM,WAAW,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;IACtD,MAAM,SAAS,GAAG,cAAc,UAAU,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,CAAC;IACnE,MAAM,WAAW,GAA2B,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,UAAU,CAAC,EAAE,EAAE,CAAC,CAAC;QACnF,IAAI;QACJ,WAAW,EAAE,UAAU,CAAC,WAAW;QACnC,WAAW,EAAE,cAAc,CAAC,UAAU,CAAC;KACxC,CAAC,CAAC,CAAC;IACJ,OAAO;QACL,SAAS;QACT,WAAW;QACX,YAAY,EAAE,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,QAAQ,SAAS,KAAK,IAAI,EAAE,CAAC;QACvE,KAAK,CAAC,IAAI,CAAC,IAAY,EAAE,KAAc;YACrC,MAAM,UAAU,GAAG,KAAK,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,CAAC;YACvC,IAAI,CAAC,UAAU;gBAAE,OAAO,WAAW,CAAC,iCAAiC,CAAC,CAAC;YACvE,IAAI,CAAC,SAAS;gBAAE,OAAO,WAAW,CAAC,uCAAuC,CAAC,CAAC;YAC5E,MAAM,SAAS,GAAG,WAAW,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;YACjD,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YACvB,IAAI,CAAC;gBACH,OAAO,EAAC,OAAO,EAAE,cAAc,CAAC,MAAM,SAAS,CAAC,EAAC,CAAC;YACpD,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,OAAO,WAAW,CAAC,KAAK,YAAY,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,yBAAyB,CAAC,CAAC;YAC9F,CAAC;oBAAS,CAAC;gBACT,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;YAC5B,CAAC;QACH,CAAC;QACD,KAAK,KAAK,SAAS,GAAG,IAAI,CAAC,CAAC,CAAC;QAC7B,aAAa,KAAK,SAAS,GAAG,KAAK,CAAC,CAAC,CAAC;QACtC,KAAK,CAAC,KAAK;YACT,SAAS,GAAG,KAAK,CAAC;YAClB,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;QACzC,CAAC;KACF,CAAC;AACJ,CAAC;AAED,SAAS,WAAW,CAAC,IAAY;IAC/B,OAAO,EAAC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,EAAC,IAAI,EAAE,MAAe,EAAE,IAAI,EAAC,CAAC,EAAC,CAAC;AACnE,CAAC","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type { CallToolResult } from \"@modelcontextprotocol/sdk/types.js\";\nimport { AgentError } from \"../errors.js\";\nimport { executeTool, toolJsonSchema } from \"../sdk/tools.js\";\nimport type { ExecutionDefinition } from \"./types.js\";\nimport { mcpToolContent } from \"./mcp-tool-content.js\";\nimport type { ClaudeToolDescriptor } from \"./claude-worker-protocol.js\";\n\n/** The native process may stop before host-side JavaScript callbacks finish. */\nexport function claudeTools(agent: Pick<ExecutionDefinition, \"tools\">) {\n const pending = new Set<Promise<unknown>>();\n let accepting = false;\n const definitions = Object.entries(agent.tools ?? {});\n const namespace = `subharness_${randomUUID().replaceAll(\"-\", \"\")}`;\n const descriptors: ClaudeToolDescriptor[] = definitions.map(([name, definition]) => ({\n name,\n description: definition.description,\n inputSchema: toolJsonSchema(definition),\n }));\n return {\n namespace,\n descriptors,\n allowedTools: definitions.map(([name]) => `mcp__${namespace}__${name}`),\n async call(name: string, input: unknown): Promise<CallToolResult> {\n const definition = agent.tools?.[name];\n if (!definition) return errorResult(\"The custom tool is unavailable.\");\n if (!accepting) return errorResult(\"The task is not accepting tool calls.\");\n const execution = executeTool(definition, input);\n pending.add(execution);\n try {\n return {content: mcpToolContent(await execution)};\n } catch (error) {\n return errorResult(error instanceof AgentError ? error.message : \"The custom tool failed.\");\n } finally {\n pending.delete(execution);\n }\n },\n start() { accepting = true; },\n stopAdmission() { accepting = false; },\n async drain() {\n accepting = false;\n await Promise.allSettled([...pending]);\n },\n };\n}\n\nfunction errorResult(text: string): CallToolResult {\n return {isError: true, content: [{type: \"text\" as const, text}]};\n}\n"]}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Writable } from "node:stream";
|
|
2
|
+
import type { ClaudeDependencies } from "./claude.js";
|
|
3
|
+
import type { ClaudeWorkerFactory } from "./claude-worker-protocol.js";
|
|
4
|
+
export declare class ClaudeWorkerMessageStream<T> implements AsyncIterable<T> {
|
|
5
|
+
private readonly values;
|
|
6
|
+
private readonly waiting;
|
|
7
|
+
private ended;
|
|
8
|
+
private failure?;
|
|
9
|
+
push(value: T): void;
|
|
10
|
+
end(): void;
|
|
11
|
+
fail(error: unknown): void;
|
|
12
|
+
[Symbol.asyncIterator](): AsyncIterator<T>;
|
|
13
|
+
}
|
|
14
|
+
export declare function writeClaudeProcessInput(stream: Writable, chunk: Buffer): Promise<void>;
|
|
15
|
+
export declare function endClaudeProcessInput(stream: Writable): Promise<void>;
|
|
16
|
+
export interface ClaudeWorkerClientOptions {
|
|
17
|
+
readonly createWorker?: ClaudeWorkerFactory;
|
|
18
|
+
}
|
|
19
|
+
export declare class ClaudeWorkerClient {
|
|
20
|
+
private readonly worker;
|
|
21
|
+
private readonly pending;
|
|
22
|
+
private readonly hostRequests;
|
|
23
|
+
private readonly messages;
|
|
24
|
+
private nextId;
|
|
25
|
+
private terminalFailure?;
|
|
26
|
+
private expectedExit;
|
|
27
|
+
private disposed?;
|
|
28
|
+
private bridge?;
|
|
29
|
+
private processOwner?;
|
|
30
|
+
private spawnAbort?;
|
|
31
|
+
private spawned?;
|
|
32
|
+
private canUseTool?;
|
|
33
|
+
private onElicitation?;
|
|
34
|
+
constructor(env: NodeJS.ProcessEnv, options?: ClaudeWorkerClientOptions);
|
|
35
|
+
readonly dependencies: ClaudeDependencies;
|
|
36
|
+
private forwardInput;
|
|
37
|
+
private queryProxy;
|
|
38
|
+
private request;
|
|
39
|
+
private postEvent;
|
|
40
|
+
private onMessage;
|
|
41
|
+
private postHostResponse;
|
|
42
|
+
private handleHostRequest;
|
|
43
|
+
private writeProcess;
|
|
44
|
+
private endProcessInput;
|
|
45
|
+
private readProcess;
|
|
46
|
+
private fail;
|
|
47
|
+
private terminate;
|
|
48
|
+
dispose(): Promise<void>;
|
|
49
|
+
}
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { delimiter, join } from "node:path";
|
|
4
|
+
import { Worker } from "node:worker_threads";
|
|
5
|
+
import { AgentError } from "../errors.js";
|
|
6
|
+
export class ClaudeWorkerMessageStream {
|
|
7
|
+
values = [];
|
|
8
|
+
waiting = [];
|
|
9
|
+
ended = false;
|
|
10
|
+
failure;
|
|
11
|
+
push(value) {
|
|
12
|
+
if (this.failure || this.ended)
|
|
13
|
+
return;
|
|
14
|
+
const next = this.waiting.shift();
|
|
15
|
+
if (next)
|
|
16
|
+
next.resolve({ value, done: false });
|
|
17
|
+
else
|
|
18
|
+
this.values.push(value);
|
|
19
|
+
}
|
|
20
|
+
end() {
|
|
21
|
+
if (this.failure || this.ended)
|
|
22
|
+
return;
|
|
23
|
+
this.ended = true;
|
|
24
|
+
for (const next of this.waiting.splice(0))
|
|
25
|
+
next.resolve({ value: undefined, done: true });
|
|
26
|
+
}
|
|
27
|
+
fail(error) {
|
|
28
|
+
if (this.failure)
|
|
29
|
+
return;
|
|
30
|
+
this.failure = error;
|
|
31
|
+
this.values.length = 0;
|
|
32
|
+
for (const next of this.waiting.splice(0))
|
|
33
|
+
next.reject(error);
|
|
34
|
+
}
|
|
35
|
+
[Symbol.asyncIterator]() {
|
|
36
|
+
return { next: () => {
|
|
37
|
+
if (this.failure)
|
|
38
|
+
return Promise.reject(this.failure);
|
|
39
|
+
if (this.values.length)
|
|
40
|
+
return Promise.resolve({ value: this.values.shift(), done: false });
|
|
41
|
+
if (this.ended)
|
|
42
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
43
|
+
return new Promise((resolve, reject) => this.waiting.push({ resolve, reject }));
|
|
44
|
+
} };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function stdinFailure() {
|
|
48
|
+
return new Error("Claude Code stdin bridge failed.");
|
|
49
|
+
}
|
|
50
|
+
export function writeClaudeProcessInput(stream, chunk) {
|
|
51
|
+
if (stream.destroyed || stream.closed || stream.writableEnded)
|
|
52
|
+
return Promise.reject(stdinFailure());
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
let settled = false;
|
|
55
|
+
const cleanup = () => {
|
|
56
|
+
stream.off("drain", drained);
|
|
57
|
+
stream.off("close", failed);
|
|
58
|
+
stream.off("error", failed);
|
|
59
|
+
};
|
|
60
|
+
const done = (operation) => {
|
|
61
|
+
if (settled)
|
|
62
|
+
return;
|
|
63
|
+
settled = true;
|
|
64
|
+
cleanup();
|
|
65
|
+
operation();
|
|
66
|
+
};
|
|
67
|
+
const drained = () => done(resolve);
|
|
68
|
+
const failed = () => done(() => reject(stdinFailure()));
|
|
69
|
+
stream.once("drain", drained);
|
|
70
|
+
stream.once("close", failed);
|
|
71
|
+
stream.once("error", failed);
|
|
72
|
+
try {
|
|
73
|
+
if (stream.write(chunk))
|
|
74
|
+
drained();
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
failed();
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
export function endClaudeProcessInput(stream) {
|
|
82
|
+
if (stream.destroyed || stream.closed)
|
|
83
|
+
return Promise.reject(stdinFailure());
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
let settled = false;
|
|
86
|
+
const cleanup = () => {
|
|
87
|
+
stream.off("close", failed);
|
|
88
|
+
stream.off("error", failed);
|
|
89
|
+
};
|
|
90
|
+
const done = (operation) => {
|
|
91
|
+
if (settled)
|
|
92
|
+
return;
|
|
93
|
+
settled = true;
|
|
94
|
+
cleanup();
|
|
95
|
+
operation();
|
|
96
|
+
};
|
|
97
|
+
const finished = () => done(resolve);
|
|
98
|
+
const failed = () => done(() => reject(stdinFailure()));
|
|
99
|
+
stream.once("close", failed);
|
|
100
|
+
stream.once("error", failed);
|
|
101
|
+
try {
|
|
102
|
+
stream.end(finished);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
failed();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
function copiedEnvironment(env) {
|
|
110
|
+
return Object.fromEntries(Object.entries(env).filter((entry) => entry[1] !== undefined));
|
|
111
|
+
}
|
|
112
|
+
async function executable(env) {
|
|
113
|
+
for (const directory of (env.PATH ?? "").split(delimiter)) {
|
|
114
|
+
const file = join(directory, "claude");
|
|
115
|
+
try {
|
|
116
|
+
await access(file, constants.X_OK);
|
|
117
|
+
return file;
|
|
118
|
+
}
|
|
119
|
+
catch { /* Try the next PATH entry. */ }
|
|
120
|
+
}
|
|
121
|
+
throw new AgentError("HARNESS_UNAVAILABLE", "Install Claude Code and make claude available on PATH.");
|
|
122
|
+
}
|
|
123
|
+
export class ClaudeWorkerClient {
|
|
124
|
+
worker;
|
|
125
|
+
pending = new Map();
|
|
126
|
+
hostRequests = new Map();
|
|
127
|
+
messages = new ClaudeWorkerMessageStream();
|
|
128
|
+
nextId = 1;
|
|
129
|
+
terminalFailure;
|
|
130
|
+
expectedExit = false;
|
|
131
|
+
disposed;
|
|
132
|
+
bridge;
|
|
133
|
+
processOwner;
|
|
134
|
+
spawnAbort;
|
|
135
|
+
spawned;
|
|
136
|
+
canUseTool;
|
|
137
|
+
onElicitation;
|
|
138
|
+
constructor(env, options = {}) {
|
|
139
|
+
const moduleUrl = new URL(`./claude-worker.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`, import.meta.url);
|
|
140
|
+
const factory = options.createWorker ?? ((url, workerOptions) => new Worker(url, workerOptions));
|
|
141
|
+
this.worker = factory(moduleUrl, { env: copiedEnvironment(env) });
|
|
142
|
+
this.worker.on("message", message => {
|
|
143
|
+
void this.onMessage(message).catch(() => this.fail("message"));
|
|
144
|
+
});
|
|
145
|
+
this.worker.on("error", () => this.fail("worker"));
|
|
146
|
+
this.worker.on("exit", code => {
|
|
147
|
+
if (!this.expectedExit)
|
|
148
|
+
this.fail("worker", code);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
dependencies = {
|
|
152
|
+
executable,
|
|
153
|
+
managedSettings: async (cwd) => await this.request("managedSettings", cwd),
|
|
154
|
+
query: async ({ prompt, options }, bridge) => {
|
|
155
|
+
this.bridge = bridge;
|
|
156
|
+
this.processOwner = bridge.processOwner;
|
|
157
|
+
this.canUseTool = options.canUseTool;
|
|
158
|
+
this.onElicitation = options.onElicitation;
|
|
159
|
+
const { spawnClaudeCodeProcess: _spawn, canUseTool: _canUseTool, onElicitation: _onElicitation, stderr: _stderr, mcpServers: _mcpServers, ...workerOptions } = options;
|
|
160
|
+
await this.request("start", {
|
|
161
|
+
options: workerOptions,
|
|
162
|
+
namespace: bridge.tools.namespace,
|
|
163
|
+
tools: bridge.tools.descriptors,
|
|
164
|
+
});
|
|
165
|
+
void this.forwardInput(prompt);
|
|
166
|
+
return this.queryProxy();
|
|
167
|
+
},
|
|
168
|
+
dispose: async () => await this.dispose(),
|
|
169
|
+
};
|
|
170
|
+
async forwardInput(prompt) {
|
|
171
|
+
try {
|
|
172
|
+
for await (const message of prompt)
|
|
173
|
+
this.postEvent("input", message);
|
|
174
|
+
this.postEvent("inputEnd");
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
this.fail("input");
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
queryProxy() {
|
|
181
|
+
return {
|
|
182
|
+
[Symbol.asyncIterator]: () => this.messages[Symbol.asyncIterator](),
|
|
183
|
+
accountInfo: async () => await this.request("accountInfo"),
|
|
184
|
+
supportedModels: async () => await this.request("supportedModels"),
|
|
185
|
+
getSettings: async () => await this.request("getSettings"),
|
|
186
|
+
setModel: async (model) => { await this.request("setModel", model); },
|
|
187
|
+
interrupt: async () => await this.request("interrupt"),
|
|
188
|
+
close: () => this.postEvent("close"),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
request(operation, value) {
|
|
192
|
+
if (this.terminalFailure)
|
|
193
|
+
return Promise.reject(this.terminalFailure);
|
|
194
|
+
const id = this.nextId++;
|
|
195
|
+
return new Promise((resolve, reject) => {
|
|
196
|
+
this.pending.set(id, { operation, resolve, reject });
|
|
197
|
+
try {
|
|
198
|
+
this.worker.postMessage({ type: "request", id, operation, value });
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
this.fail(operation);
|
|
202
|
+
reject(this.terminalFailure);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
postEvent(event, value) {
|
|
207
|
+
if (this.terminalFailure)
|
|
208
|
+
return;
|
|
209
|
+
try {
|
|
210
|
+
this.worker.postMessage({ type: "event", event, value });
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
this.fail(event);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async onMessage(message) {
|
|
217
|
+
if (message.type === "response") {
|
|
218
|
+
const current = this.pending.get(message.id);
|
|
219
|
+
if (!current)
|
|
220
|
+
return;
|
|
221
|
+
this.pending.delete(message.id);
|
|
222
|
+
if (message.ok)
|
|
223
|
+
current.resolve(message.value);
|
|
224
|
+
else if (message.failure === "settings-unavailable")
|
|
225
|
+
current.reject(new Error("Unsupported control request: get_settings."));
|
|
226
|
+
else
|
|
227
|
+
current.reject(new Error(`Claude Code ${current.operation} failed.`));
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (message.type === "host-request") {
|
|
231
|
+
const controller = new AbortController();
|
|
232
|
+
this.hostRequests.set(message.id, controller);
|
|
233
|
+
try {
|
|
234
|
+
const value = await this.handleHostRequest(message, controller.signal);
|
|
235
|
+
this.postHostResponse(message.id, true, value);
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
this.postHostResponse(message.id, false);
|
|
239
|
+
}
|
|
240
|
+
finally {
|
|
241
|
+
this.hostRequests.delete(message.id);
|
|
242
|
+
}
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (message.type !== "event")
|
|
246
|
+
return;
|
|
247
|
+
if (message.event === "message")
|
|
248
|
+
this.messages.push(message.value);
|
|
249
|
+
else if (message.event === "end")
|
|
250
|
+
this.messages.end();
|
|
251
|
+
else if (message.event === "failure")
|
|
252
|
+
this.fail(typeof message.value === "string" ? message.value : "worker");
|
|
253
|
+
else if (message.event === "hostRequestAbort")
|
|
254
|
+
this.hostRequests.get(Number(message.value))?.abort();
|
|
255
|
+
}
|
|
256
|
+
postHostResponse(id, ok, value) {
|
|
257
|
+
if (this.terminalFailure)
|
|
258
|
+
return;
|
|
259
|
+
try {
|
|
260
|
+
this.worker.postMessage({ type: "host-response", id, ok, value });
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
this.fail("host response");
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
async handleHostRequest(message, signal) {
|
|
267
|
+
const value = message.value;
|
|
268
|
+
switch (message.operation) {
|
|
269
|
+
case "toolCall": return await this.bridge.tools.call(value.name, value.input);
|
|
270
|
+
case "approval": return await this.canUseTool(value.toolName, value.toolInput, { ...value.metadata, signal });
|
|
271
|
+
case "elicitation": return await this.onElicitation(value.request, { ...value.metadata, signal });
|
|
272
|
+
case "processSpawn": {
|
|
273
|
+
this.spawnAbort = new AbortController();
|
|
274
|
+
this.spawned = this.processOwner.spawn({ ...value, signal: this.spawnAbort.signal });
|
|
275
|
+
this.spawned.once("exit", (code, exitSignal) => this.postEvent("processExit", { code, signal: exitSignal }));
|
|
276
|
+
this.spawned.once("error", () => this.postEvent("processError"));
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
case "processAbort":
|
|
280
|
+
this.spawnAbort?.abort();
|
|
281
|
+
return undefined;
|
|
282
|
+
case "processWrite":
|
|
283
|
+
await this.writeProcess(Buffer.from(value));
|
|
284
|
+
return undefined;
|
|
285
|
+
case "processEnd":
|
|
286
|
+
await this.endProcessInput();
|
|
287
|
+
return undefined;
|
|
288
|
+
case "processRead": return await this.readProcess();
|
|
289
|
+
case "processKill": return this.spawned.kill(value);
|
|
290
|
+
default: throw new Error("Unknown Claude host bridge operation.");
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
async writeProcess(chunk) {
|
|
294
|
+
await writeClaudeProcessInput(this.spawned.stdin, chunk);
|
|
295
|
+
}
|
|
296
|
+
async endProcessInput() {
|
|
297
|
+
await endClaudeProcessInput(this.spawned.stdin);
|
|
298
|
+
}
|
|
299
|
+
async readProcess() {
|
|
300
|
+
const stream = this.spawned.stdout;
|
|
301
|
+
const chunk = stream.read();
|
|
302
|
+
if (chunk)
|
|
303
|
+
return chunk;
|
|
304
|
+
if (stream.readableEnded)
|
|
305
|
+
return null;
|
|
306
|
+
return await new Promise((resolve, reject) => {
|
|
307
|
+
const cleanup = () => {
|
|
308
|
+
stream.off("readable", readable);
|
|
309
|
+
stream.off("end", ended);
|
|
310
|
+
stream.off("close", closed);
|
|
311
|
+
stream.off("error", failed);
|
|
312
|
+
};
|
|
313
|
+
const finish = (value) => { cleanup(); resolve(value); };
|
|
314
|
+
const readable = () => {
|
|
315
|
+
const available = stream.read();
|
|
316
|
+
if (available)
|
|
317
|
+
finish(available);
|
|
318
|
+
else if (stream.readableEnded)
|
|
319
|
+
finish(null);
|
|
320
|
+
};
|
|
321
|
+
const ended = () => finish(null);
|
|
322
|
+
const closed = () => stream.readableEnded ? finish(null) : failed();
|
|
323
|
+
const failed = () => { cleanup(); reject(new Error("Claude Code stdout bridge failed.")); };
|
|
324
|
+
stream.on("readable", readable);
|
|
325
|
+
stream.once("end", ended);
|
|
326
|
+
stream.once("close", closed);
|
|
327
|
+
stream.once("error", failed);
|
|
328
|
+
readable();
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
fail(operation, exitCode) {
|
|
332
|
+
const suffix = exitCode === undefined ? "" : ` (exit code ${exitCode})`;
|
|
333
|
+
this.terminate(new AgentError("HARNESS_FAILED", `Claude Code ${operation} bridge failed${suffix}.`));
|
|
334
|
+
}
|
|
335
|
+
terminate(error) {
|
|
336
|
+
if (this.terminalFailure)
|
|
337
|
+
return;
|
|
338
|
+
this.terminalFailure = error;
|
|
339
|
+
this.messages.fail(error);
|
|
340
|
+
for (const current of this.pending.values())
|
|
341
|
+
current.reject(error);
|
|
342
|
+
this.pending.clear();
|
|
343
|
+
for (const controller of this.hostRequests.values())
|
|
344
|
+
controller.abort();
|
|
345
|
+
}
|
|
346
|
+
async dispose() {
|
|
347
|
+
if (!this.disposed) {
|
|
348
|
+
this.expectedExit = true;
|
|
349
|
+
this.terminate(new AgentError("HARNESS_FAILED", "Claude Code worker closed."));
|
|
350
|
+
this.disposed = this.worker.terminate().then(() => undefined).catch(() => {
|
|
351
|
+
this.disposed = undefined;
|
|
352
|
+
this.expectedExit = false;
|
|
353
|
+
throw new AgentError("HARNESS_FAILED", "Claude Code worker cleanup failed.");
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
return await this.disposed;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
//# sourceMappingURL=claude-worker-client.js.map
|