@ai-sdk/harness 1.0.22 → 1.0.24
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/CHANGELOG.md +16 -0
- package/README.md +59 -49
- package/dist/agent/index.js +63 -0
- package/dist/agent/index.js.map +1 -1
- package/dist/bridge/index.d.ts +18 -0
- package/dist/bridge/index.js +69 -4
- package/dist/bridge/index.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/dist/utils/index.d.ts +40 -1
- package/dist/utils/index.js +240 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +2 -2
- package/src/agent/internal/run-prompt.ts +19 -0
- package/src/bridge/index.ts +86 -3
- package/src/utils/bridge-diagnostics.ts +213 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/sandbox-channel.ts +72 -0
- package/src/v1/harness-v1-bridge-protocol.ts +17 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @ai-sdk/harness
|
|
2
2
|
|
|
3
|
+
## 1.0.24
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [b9ac19f]
|
|
8
|
+
- Updated dependencies [a4186d6]
|
|
9
|
+
- ai@7.0.20
|
|
10
|
+
|
|
11
|
+
## 1.0.23
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- 39c8276: fix(harness): improve opaque sandbox bridge error handling
|
|
16
|
+
- 91fe6d8: fix(harness): emit `finish-step` stream parts correctly per the underlying model steps
|
|
17
|
+
- 0be5014: fix(harness): fix obsolete portions of harness package readme
|
|
18
|
+
|
|
3
19
|
## 1.0.22
|
|
4
20
|
|
|
5
21
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -7,22 +7,23 @@ _This package is **experimental**._
|
|
|
7
7
|
## Setup
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npm i @ai-sdk/harness
|
|
10
|
+
npm i ai zod @ai-sdk/harness @ai-sdk/harness-claude-code @ai-sdk/sandbox-vercel
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
## Usage
|
|
14
14
|
|
|
15
15
|
```ts
|
|
16
16
|
import { HarnessAgent } from '@ai-sdk/harness/agent';
|
|
17
|
-
import {
|
|
17
|
+
import { claudeCode } from '@ai-sdk/harness-claude-code';
|
|
18
18
|
import { createVercelSandbox } from '@ai-sdk/sandbox-vercel';
|
|
19
19
|
import { tool } from 'ai';
|
|
20
|
-
import { z } from 'zod';
|
|
20
|
+
import { z } from 'zod/v4';
|
|
21
21
|
|
|
22
22
|
const agent = new HarnessAgent({
|
|
23
|
-
harness:
|
|
23
|
+
harness: claudeCode,
|
|
24
24
|
id: 'auth-agent',
|
|
25
|
-
|
|
25
|
+
instructions:
|
|
26
|
+
'You are a careful refactoring assistant. Prefer minimal diffs.',
|
|
26
27
|
sandbox: createVercelSandbox({
|
|
27
28
|
runtime: 'node24',
|
|
28
29
|
ports: [4000],
|
|
@@ -30,7 +31,7 @@ const agent = new HarnessAgent({
|
|
|
30
31
|
sandboxConfig: {
|
|
31
32
|
bootstrapHash: 'ripgrep-v1',
|
|
32
33
|
onBootstrap: async ({ session, abortSignal }) => {
|
|
33
|
-
const
|
|
34
|
+
const streamResult = await session.run({
|
|
34
35
|
command:
|
|
35
36
|
'command -v rg >/dev/null || (apt-get update && apt-get install -y ripgrep)',
|
|
36
37
|
abortSignal,
|
|
@@ -51,29 +52,26 @@ const agent = new HarnessAgent({
|
|
|
51
52
|
deploy: tool({
|
|
52
53
|
description: 'Deploy to a target environment',
|
|
53
54
|
inputSchema: z.object({ env: z.enum(['staging', 'production']) }),
|
|
54
|
-
execute: async ({ env }) => ({ url:
|
|
55
|
+
execute: async ({ env }) => ({ url: `https://${env}.example.com` }),
|
|
55
56
|
}),
|
|
56
57
|
},
|
|
57
|
-
harnessOptions: {
|
|
58
|
-
'claude-code': { thinking: 'adaptive' },
|
|
59
|
-
},
|
|
60
58
|
});
|
|
61
59
|
|
|
62
60
|
const session = await agent.createSession();
|
|
63
61
|
|
|
64
62
|
try {
|
|
65
|
-
const
|
|
63
|
+
const generateResult = await agent.generate({
|
|
66
64
|
session,
|
|
67
65
|
prompt: 'Fix the failing test in src/auth.ts',
|
|
68
66
|
});
|
|
69
|
-
console.log(
|
|
67
|
+
console.log(generateResult.text);
|
|
70
68
|
|
|
71
69
|
// Streaming
|
|
72
|
-
const
|
|
70
|
+
const streamResult = await agent.stream({
|
|
73
71
|
session,
|
|
74
72
|
prompt: 'Now write a regression test',
|
|
75
73
|
});
|
|
76
|
-
for await (const part of stream
|
|
74
|
+
for await (const part of streamResult.stream) {
|
|
77
75
|
if (part.type === 'text-delta') {
|
|
78
76
|
process.stdout.write(part.text);
|
|
79
77
|
}
|
|
@@ -83,7 +81,7 @@ try {
|
|
|
83
81
|
}
|
|
84
82
|
```
|
|
85
83
|
|
|
86
|
-
Use `session.detach()` to park a bridge-backed session for later attach, `session.stop()` to save state and stop the sandbox, or `session.destroy()` to clean up without keeping resume state. Bridge-backed adapters
|
|
84
|
+
Use `session.detach()` to park a bridge-backed session for later attach, `session.stop()` to save state and stop the sandbox, or `session.destroy()` to clean up without keeping resume state. Bridge-backed adapters such as Claude Code, Codex, OpenCode, and DeepAgents require a sandbox provider that exposes ports — `@ai-sdk/sandbox-vercel` is the supported choice today. `@ai-sdk/sandbox-just-bash` is suitable only for host-runtime or otherwise non-bridge flows, such as Pi.
|
|
87
85
|
|
|
88
86
|
`sandbox` is a required `HarnessV1SandboxProvider` — the agent calls `provider.createSession()` when a session starts. Use `sandboxConfig` for agent specific sandbox configuration that works independently from the sandbox provider that is used:
|
|
89
87
|
|
|
@@ -101,7 +99,9 @@ bootstrap recipes and `sandboxConfig.onBootstrap`, returns the computed
|
|
|
101
99
|
preparation identity and per-harness recipe identities, and leaves snapshotting
|
|
102
100
|
or stopping the sandbox to your code. Later, create a sandbox from that snapshot
|
|
103
101
|
and pass the native sandbox object to `createVercelSandbox({ sandbox })` for the
|
|
104
|
-
`HarnessAgent`.
|
|
102
|
+
`HarnessAgent`. When you reuse a caller-provided sandbox with a bridge-backed
|
|
103
|
+
harness, declare the available port pool, for example
|
|
104
|
+
`createVercelSandbox({ sandbox, bridgePorts: [4000] })`.
|
|
105
105
|
|
|
106
106
|
### Available harnesses
|
|
107
107
|
|
|
@@ -109,7 +109,7 @@ See the [harness adapters documentation](https://ai-sdk.dev/v7/docs/ai-sdk-harne
|
|
|
109
109
|
|
|
110
110
|
## Implementing a harness
|
|
111
111
|
|
|
112
|
-
Implement the `HarnessV1` factory and a `HarnessV1Session` whose `
|
|
112
|
+
Implement the `HarnessV1` factory and a `HarnessV1Session` whose `doPromptTurn` emits events; the agent surface, streaming, tool execution, and multi-turn state are handled for you. Read `startOpts.sandboxSession` for the network sandbox session the agent created and will stop on cleanup. Call `sandboxSession.restricted()` for the tool-safe file-IO/exec/spawn surface.
|
|
113
113
|
|
|
114
114
|
```ts
|
|
115
115
|
import type { HarnessV1, HarnessV1Session } from '@ai-sdk/harness';
|
|
@@ -118,46 +118,56 @@ export function myHarness(): HarnessV1 {
|
|
|
118
118
|
return {
|
|
119
119
|
specificationVersion: 'harness-v1',
|
|
120
120
|
harnessId: 'my-harness',
|
|
121
|
-
builtinTools:
|
|
121
|
+
builtinTools: {},
|
|
122
122
|
doStart: async startOpts => {
|
|
123
|
+
const usage = {
|
|
124
|
+
inputTokens: { total: 0, noCache: 0 },
|
|
125
|
+
outputTokens: { total: 0, text: 0 },
|
|
126
|
+
};
|
|
127
|
+
const resumeState = {
|
|
128
|
+
type: 'resume-session' as const,
|
|
129
|
+
harnessId: 'my-harness',
|
|
130
|
+
specificationVersion: 'harness-v1' as const,
|
|
131
|
+
data: {},
|
|
132
|
+
};
|
|
133
|
+
const continueState = {
|
|
134
|
+
type: 'continue-turn' as const,
|
|
135
|
+
harnessId: 'my-harness',
|
|
136
|
+
specificationVersion: 'harness-v1' as const,
|
|
137
|
+
data: {},
|
|
138
|
+
};
|
|
123
139
|
const session: HarnessV1Session = {
|
|
124
140
|
sessionId: startOpts.sessionId,
|
|
125
|
-
isResume:
|
|
141
|
+
isResume:
|
|
142
|
+
startOpts.resumeFrom != null || startOpts.continueFrom != null,
|
|
126
143
|
doPromptTurn: async promptOpts => {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
144
|
+
const done = Promise.resolve().then(() => {
|
|
145
|
+
promptOpts.emit({ type: 'text-start', id: 't' });
|
|
146
|
+
promptOpts.emit({ type: 'text-delta', id: 't', delta: 'Hello.' });
|
|
147
|
+
promptOpts.emit({ type: 'text-end', id: 't' });
|
|
148
|
+
promptOpts.emit({
|
|
149
|
+
type: 'finish',
|
|
150
|
+
finishReason: { unified: 'stop', raw: 'stop' },
|
|
151
|
+
totalUsage: usage,
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
return { submitToolResult: async () => {}, done };
|
|
155
|
+
},
|
|
156
|
+
doContinueTurn: async continueOpts => {
|
|
157
|
+
const done = Promise.resolve().then(() => {
|
|
158
|
+
continueOpts.emit({
|
|
159
|
+
type: 'finish',
|
|
160
|
+
finishReason: { unified: 'stop', raw: 'stop' },
|
|
161
|
+
totalUsage: usage,
|
|
162
|
+
});
|
|
142
163
|
});
|
|
143
|
-
return { submitToolResult: async () => {}, done
|
|
164
|
+
return { submitToolResult: async () => {}, done };
|
|
144
165
|
},
|
|
145
|
-
doContinueTurn: async () => ({
|
|
146
|
-
submitToolResult: async () => {},
|
|
147
|
-
done: Promise.resolve(),
|
|
148
|
-
}),
|
|
149
166
|
doCompact: async () => {},
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
specificationVersion: 'harness-v1',
|
|
153
|
-
data: {},
|
|
154
|
-
}),
|
|
167
|
+
doDetach: async () => resumeState,
|
|
168
|
+
doStop: async () => resumeState,
|
|
155
169
|
doDestroy: async () => {},
|
|
156
|
-
doSuspendTurn: async () =>
|
|
157
|
-
harnessId: 'my-harness',
|
|
158
|
-
specificationVersion: 'harness-v1',
|
|
159
|
-
data: {},
|
|
160
|
-
}),
|
|
170
|
+
doSuspendTurn: async () => continueState,
|
|
161
171
|
};
|
|
162
172
|
return session;
|
|
163
173
|
},
|
package/dist/agent/index.js
CHANGED
|
@@ -1171,6 +1171,51 @@ function normalizeToolApprovalStatus(input) {
|
|
|
1171
1171
|
return input.status;
|
|
1172
1172
|
}
|
|
1173
1173
|
|
|
1174
|
+
// src/utils/bridge-diagnostics.ts
|
|
1175
|
+
function isSerializedError(error) {
|
|
1176
|
+
return typeof error === "object" && error != null && typeof error.message === "string";
|
|
1177
|
+
}
|
|
1178
|
+
function stringifyUnknown(value) {
|
|
1179
|
+
if (typeof value === "string") return value;
|
|
1180
|
+
try {
|
|
1181
|
+
return JSON.stringify(value);
|
|
1182
|
+
} catch (e) {
|
|
1183
|
+
return String(value);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
function formatBridgeError(error) {
|
|
1187
|
+
var _a3, _b3;
|
|
1188
|
+
if (error instanceof Error) {
|
|
1189
|
+
return (_a3 = error.stack) != null ? _a3 : `${error.name}: ${error.message}`;
|
|
1190
|
+
}
|
|
1191
|
+
if (isSerializedError(error)) {
|
|
1192
|
+
return (_b3 = error.stack) != null ? _b3 : `${error.name ? `${error.name}: ` : ""}${error.message}`;
|
|
1193
|
+
}
|
|
1194
|
+
return stringifyUnknown(error);
|
|
1195
|
+
}
|
|
1196
|
+
function writeToStderr(line) {
|
|
1197
|
+
try {
|
|
1198
|
+
process.stderr.write(line);
|
|
1199
|
+
} catch (e) {
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
function logBridgeError({
|
|
1203
|
+
harnessId,
|
|
1204
|
+
sessionId,
|
|
1205
|
+
context,
|
|
1206
|
+
error,
|
|
1207
|
+
write = writeToStderr
|
|
1208
|
+
}) {
|
|
1209
|
+
const prefix = `[harness:${harnessId}:error${sessionId ? ` session=${sessionId}` : ""}]`;
|
|
1210
|
+
const message = context ? `${context}: ${formatBridgeError(error)}` : formatBridgeError(error);
|
|
1211
|
+
for (const line of message.split("\n")) {
|
|
1212
|
+
if (line.trim().length > 0) {
|
|
1213
|
+
write(`${prefix} ${line}
|
|
1214
|
+
`);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1174
1219
|
// src/agent/internal/run-prompt.ts
|
|
1175
1220
|
function runPrompt(input) {
|
|
1176
1221
|
var _a3, _b3, _c, _d;
|
|
@@ -1222,6 +1267,12 @@ function runPrompt(input) {
|
|
|
1222
1267
|
});
|
|
1223
1268
|
} catch (err) {
|
|
1224
1269
|
telemetry.error(err);
|
|
1270
|
+
logBridgeError({
|
|
1271
|
+
harnessId: input.harness.harnessId,
|
|
1272
|
+
sessionId: input.session.sessionId,
|
|
1273
|
+
context: "failed to start harness turn",
|
|
1274
|
+
error: err
|
|
1275
|
+
});
|
|
1225
1276
|
result.fail(err);
|
|
1226
1277
|
return;
|
|
1227
1278
|
}
|
|
@@ -1665,6 +1716,12 @@ function runPrompt(input) {
|
|
|
1665
1716
|
}
|
|
1666
1717
|
if (value.type === "error") {
|
|
1667
1718
|
telemetry.error(value.error);
|
|
1719
|
+
logBridgeError({
|
|
1720
|
+
harnessId: input.harness.harnessId,
|
|
1721
|
+
sessionId: input.session.sessionId,
|
|
1722
|
+
context: "harness stream error",
|
|
1723
|
+
error: value.error
|
|
1724
|
+
});
|
|
1668
1725
|
result.fail(value.error);
|
|
1669
1726
|
return;
|
|
1670
1727
|
}
|
|
@@ -1679,6 +1736,12 @@ function runPrompt(input) {
|
|
|
1679
1736
|
);
|
|
1680
1737
|
} catch (err) {
|
|
1681
1738
|
telemetry.error(err);
|
|
1739
|
+
logBridgeError({
|
|
1740
|
+
harnessId: input.harness.harnessId,
|
|
1741
|
+
sessionId: input.session.sessionId,
|
|
1742
|
+
context: "harness turn failed",
|
|
1743
|
+
error: err
|
|
1744
|
+
});
|
|
1682
1745
|
result.fail(err);
|
|
1683
1746
|
} finally {
|
|
1684
1747
|
reader.releaseLock();
|