@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.
- package/CHANGELOG.md +14 -0
- package/README.md +6 -6
- package/dist/agent/index.d.ts +93 -105
- package/dist/agent/index.js +153 -180
- package/dist/agent/index.js.map +1 -1
- package/dist/index.d.ts +0 -11
- package/package.json +3 -3
- package/src/agent/harness-agent-session.ts +18 -44
- package/src/agent/harness-agent-settings.ts +4 -5
- package/src/agent/harness-agent.ts +127 -149
- package/src/agent/internal/harness-stream-text-result.ts +1 -12
- package/src/agent/internal/run-prompt.ts +17 -1
- package/src/agent/internal/translate-stream-part.ts +39 -0
- package/src/agent/prepare-harness-sandbox-template.ts +15 -7
- package/src/v1/harness-v1-sandbox-provider.ts +0 -12
- package/src/agent/internal/bridge-port-registry.ts +0 -52
|
@@ -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
|
-
|
|
35
|
-
|
|
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
|
|
46
|
-
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
|
|
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
|
-
}
|