@ai-sdk/harness 1.0.22 → 1.0.23

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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # @ai-sdk/harness
2
2
 
3
+ ## 1.0.23
4
+
5
+ ### Patch Changes
6
+
7
+ - 39c8276: fix(harness): improve opaque sandbox bridge error handling
8
+ - 91fe6d8: fix(harness): emit `finish-step` stream parts correctly per the underlying model steps
9
+ - 0be5014: fix(harness): fix obsolete portions of harness package readme
10
+
3
11
  ## 1.0.22
4
12
 
5
13
  ### 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 { createClaudeCode } from '@ai-sdk/harness-claude-code';
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: createClaudeCode(),
23
+ harness: claudeCode,
24
24
  id: 'auth-agent',
25
- system: 'You are a careful refactoring assistant. Prefer minimal diffs.',
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 result = await session.run({
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: await deployTo(env) }),
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 result = await agent.generate({
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(result.text);
67
+ console.log(generateResult.text);
70
68
 
71
69
  // Streaming
72
- const stream = await agent.stream({
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.fullStream) {
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 (claude-code, codex) 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 non-bridge flows.
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 `doPrompt` 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.
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: false,
141
+ isResume:
142
+ startOpts.resumeFrom != null || startOpts.continueFrom != null,
126
143
  doPromptTurn: async promptOpts => {
127
- promptOpts.emit({ type: 'text-start', id: 't' });
128
- promptOpts.emit({ type: 'text-delta', id: 't', delta: 'Hello.' });
129
- promptOpts.emit({ type: 'text-end', id: 't' });
130
- promptOpts.emit({
131
- type: 'finish',
132
- finishReason: { unified: 'stop', raw: 'stop' },
133
- totalUsage: {
134
- inputTokens: {
135
- total: 0,
136
- noCache: 0,
137
- cacheRead: undefined,
138
- cacheWrite: undefined,
139
- },
140
- outputTokens: { total: 0, text: 0, reasoning: undefined },
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: Promise.resolve() };
164
+ return { submitToolResult: async () => {}, done };
144
165
  },
145
- doContinueTurn: async () => ({
146
- submitToolResult: async () => {},
147
- done: Promise.resolve(),
148
- }),
149
166
  doCompact: async () => {},
150
- doStop: async () => ({
151
- harnessId: 'my-harness',
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
  },
@@ -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();