@ai-sdk/harness 1.0.74 → 1.0.76

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.
@@ -14,6 +14,8 @@ import { generateId, type ToolSet } from '@ai-sdk/provider-utils';
14
14
  * - tool-call events are not translated here — validation against the
15
15
  * merged tool set is async and handled by `validateToolCall` in
16
16
  * `run-prompt.ts`
17
+ * - a failed `tool-result` from a provider-executed tool becomes a
18
+ * `tool-error` (see `isProviderExecuted`)
17
19
  * - the harness `raw` part is forwarded as the AI SDK `raw` part
18
20
  *
19
21
  * Returns an array of zero or more AI SDK parts. Most harness events project
@@ -26,6 +28,17 @@ import { generateId, type ToolSet } from '@ai-sdk/provider-utils';
26
28
  */
27
29
  export function translateStreamPart<TOOLS extends ToolSet>(
28
30
  event: HarnessV1StreamPart,
31
+ options: {
32
+ /**
33
+ * Whether the tool call that produced this event ran inside the harness
34
+ * runtime. Host tools travel the same `tool-result` events and their
35
+ * failures are echoed back with `isError`, so the event alone cannot say
36
+ * who ran the tool — only the originating `tool-call` can, and correlating
37
+ * the two is the caller's job. Omit the callback to treat every failure as
38
+ * provider-executed.
39
+ */
40
+ isProviderExecuted?: (toolCallId: string) => boolean;
41
+ } = {},
29
42
  ): ReadonlyArray<TextStreamPart<TOOLS>> {
30
43
  switch (event.type) {
31
44
  case 'stream-start':
@@ -101,6 +114,32 @@ export function translateStreamPart<TOOLS extends ToolSet>(
101
114
  return [];
102
115
 
103
116
  case 'tool-result':
117
+ if (
118
+ event.isError === true &&
119
+ (options.isProviderExecuted?.(event.toolCallId) ?? true)
120
+ ) {
121
+ /*
122
+ * `providerExecuted` decides whether the message survives:
123
+ * `toUIMessageChunk` forwards the real error text only for
124
+ * provider-executed errors, and replaces every other one with the
125
+ * generic `onError` string.
126
+ */
127
+ return [
128
+ {
129
+ type: 'tool-error',
130
+ toolCallId: event.toolCallId,
131
+ toolName: event.toolName,
132
+ input: undefined,
133
+ error: event.result,
134
+ providerExecuted: true,
135
+ ...(event.dynamic !== undefined ? { dynamic: event.dynamic } : {}),
136
+ ...(event.providerMetadata !== undefined
137
+ ? { providerMetadata: event.providerMetadata }
138
+ : {}),
139
+ } as TextStreamPart<TOOLS>,
140
+ ];
141
+ }
142
+
104
143
  return [
105
144
  {
106
145
  type: 'tool-result',
@@ -1,4 +1,4 @@
1
- import type { HarnessV1SandboxProvider } from '../v1';
1
+ import type { HarnessV1Bootstrap, HarnessV1SandboxProvider } from '../v1';
2
2
  import type { HarnessAgentAdapter } from './harness-agent-types';
3
3
  import type { HarnessAgentSandboxConfig } from './harness-agent-settings';
4
4
  import { applyBootstrapRecipe } from './internal/bootstrap-recipe';
@@ -31,9 +31,14 @@ export async function prepareHarnessSandboxTemplate(options: {
31
31
  }): Promise<void> {
32
32
  const sandboxConfig = options.sandboxConfig ?? {};
33
33
  validateSandboxBootstrapSettings(sandboxConfig);
34
- const recipe = await options.harness.getBootstrap?.({
35
- abortSignal: options.abortSignal,
36
- });
34
+
35
+ const { harness, sandboxProvider, abortSignal } = options;
36
+
37
+ let recipe: HarnessV1Bootstrap | undefined;
38
+ if (harness.getBootstrap != null) {
39
+ recipe = await harness.getBootstrap({ abortSignal });
40
+ }
41
+
37
42
  const bootstrapPlan = await createSandboxBootstrapPlan({
38
43
  recipe,
39
44
  settings: sandboxConfig,
@@ -42,12 +47,15 @@ export async function prepareHarnessSandboxTemplate(options: {
42
47
  return;
43
48
  }
44
49
 
45
- const sandboxSession = await options.sandboxProvider.createSession({
46
- abortSignal: options.abortSignal,
50
+ const sandboxSession = await sandboxProvider.createSession({
51
+ abortSignal,
47
52
  identity: bootstrapPlan.identity,
48
53
  onFirstCreate: bootstrapPlan.onFirstCreate,
49
54
  });
50
55
 
56
+ // Unlike `prepareSandboxForHarness()` and `HarnessAgent.createSession()`, this function
57
+ // does not apply the agent-specific sandbox config, since the function is meant as a
58
+ // general harness utility, not for a concrete agent.
51
59
  try {
52
60
  if (bootstrapPlan.recipe != null && bootstrapPlan.recipeIdentity != null) {
53
61
  await applyBootstrapRecipe({
@@ -55,7 +63,7 @@ export async function prepareHarnessSandboxTemplate(options: {
55
63
  recipe: bootstrapPlan.recipe,
56
64
  identity: bootstrapPlan.recipeIdentity,
57
65
  defaultWorkingDirectory: sandboxSession.defaultWorkingDirectory,
58
- abortSignal: options.abortSignal,
66
+ abortSignal,
59
67
  });
60
68
  }
61
69
  } finally {
@@ -11,18 +11,6 @@ export interface HarnessV1SandboxProvider {
11
11
  readonly specificationVersion: 'harness-sandbox-v1';
12
12
  readonly providerId: string;
13
13
 
14
- /**
15
- * Pool of ports the consumer reserved on a caller-provided sandbox for
16
- * concurrent harness sessions. The session manager leases one port per
17
- * session and releases on stop or destroy.
18
- *
19
- * Only meaningful when the provider wraps a caller-provided sandbox
20
- * (the caller pre-declared the ports). In create-new modes the provider
21
- * mints a fresh sandbox per session, so no leasing is needed; providers
22
- * leave this undefined.
23
- */
24
- readonly bridgePorts?: ReadonlyArray<number>;
25
-
26
14
  /**
27
15
  * Providers should throw `HarnessSandboxAuthenticationError` when sandbox
28
16
  * acquisition fails because credentials are missing, invalid, or not
@@ -1,52 +0,0 @@
1
- /**
2
- * Process-wide registry for bridge-port leases. Used when a sandbox provider
3
- * wraps a caller-provided sandbox with a pre-declared port pool — each
4
- * concurrent harness session leases one port from the pool, releases on
5
- * session stop or destroy. Multiple sessions on the same provider instance
6
- * share the same pool; different provider instances (even wrapping the same
7
- * underlying sandbox) get independent registries.
8
- *
9
- * Sized to the typical case: one provider object passed to N HarnessAgents.
10
- * Callers that need cross-process coordination must layer that on top.
11
- */
12
-
13
- type RegistryEntry = {
14
- readonly pool: ReadonlyArray<number>;
15
- readonly leases: Map<string, number>;
16
- };
17
-
18
- const registries = new WeakMap<object, RegistryEntry>();
19
-
20
- export function acquireBridgePort(options: {
21
- poolKey: object;
22
- pool: ReadonlyArray<number>;
23
- sessionId: string;
24
- }): number {
25
- let entry = registries.get(options.poolKey);
26
- if (entry == null) {
27
- entry = { pool: options.pool, leases: new Map() };
28
- registries.set(options.poolKey, entry);
29
- }
30
- const existing = entry.leases.get(options.sessionId);
31
- if (existing != null) return existing;
32
-
33
- const leased = new Set(entry.leases.values());
34
- for (const port of entry.pool) {
35
- if (!leased.has(port)) {
36
- entry.leases.set(options.sessionId, port);
37
- return port;
38
- }
39
- }
40
- throw new Error(
41
- `No available bridge port — pool of ${entry.pool.length} ports is fully leased.`,
42
- );
43
- }
44
-
45
- export function releaseBridgePort(options: {
46
- poolKey: object;
47
- sessionId: string;
48
- }): void {
49
- const entry = registries.get(options.poolKey);
50
- if (entry == null) return;
51
- entry.leases.delete(options.sessionId);
52
- }