@ai-sdk/harness 0.0.0 → 1.0.0-canary.3
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 +24 -0
- package/LICENSE +13 -0
- package/README.md +142 -0
- package/agent/index.ts +17 -0
- package/bridge/index.ts +10 -0
- package/dist/agent/index.d.ts +1480 -0
- package/dist/agent/index.js +2554 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/bridge/index.d.ts +111 -0
- package/dist/bridge/index.js +414 -0
- package/dist/bridge/index.js.map +1 -0
- package/dist/index.d.ts +1510 -0
- package/dist/index.js +15834 -0
- package/dist/index.js.map +1 -0
- package/dist/observability/index.d.ts +97 -0
- package/dist/observability/index.js +225 -0
- package/dist/observability/index.js.map +1 -0
- package/dist/utils/index.d.ts +196 -0
- package/dist/utils/index.js +327 -0
- package/dist/utils/index.js.map +1 -0
- package/package.json +104 -1
- package/src/agent/harness-agent-session.ts +352 -0
- package/src/agent/harness-agent-settings.ts +131 -0
- package/src/agent/harness-agent-tool-approval-continuation.ts +94 -0
- package/src/agent/harness-agent.ts +750 -0
- package/src/agent/harness-diagnostics.ts +88 -0
- package/src/agent/internal/bootstrap-recipe.ts +124 -0
- package/src/agent/internal/bridge-port-registry.ts +52 -0
- package/src/agent/internal/harness-stream-text-result.ts +720 -0
- package/src/agent/internal/permission-mode.ts +50 -0
- package/src/agent/internal/resolve-observability.ts +128 -0
- package/src/agent/internal/resume-state-validation.ts +51 -0
- package/src/agent/internal/run-prompt.ts +811 -0
- package/src/agent/internal/strip-work-dir.ts +68 -0
- package/src/agent/internal/to-harness-stream.ts +75 -0
- package/src/agent/internal/translate-stream-part.ts +221 -0
- package/src/agent/internal/turn-telemetry.ts +359 -0
- package/src/agent/prewarm.ts +46 -0
- package/src/bridge/index.ts +700 -0
- package/src/errors/harness-capability-unsupported-error.ts +41 -0
- package/src/errors/harness-error.ts +22 -0
- package/src/index.ts +3 -0
- package/src/observability/file-reporter.ts +209 -0
- package/src/observability/index.ts +13 -0
- package/src/observability/trace-tree-reporter.ts +122 -0
- package/src/utils/classify-disk-log.ts +43 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/sandbox-channel.ts +453 -0
- package/src/v1/harness-v1-bootstrap.ts +46 -0
- package/src/v1/harness-v1-bridge-protocol.ts +310 -0
- package/src/v1/harness-v1-builtin-tool.ts +138 -0
- package/src/v1/harness-v1-call-warning.ts +22 -0
- package/src/v1/harness-v1-diagnostic.ts +66 -0
- package/src/v1/harness-v1-metadata.ts +13 -0
- package/src/v1/harness-v1-network-sandbox-session.ts +123 -0
- package/src/v1/harness-v1-observability.ts +20 -0
- package/src/v1/harness-v1-permission-mode.ts +11 -0
- package/src/v1/harness-v1-prompt-control.ts +41 -0
- package/src/v1/harness-v1-prompt.ts +11 -0
- package/src/v1/harness-v1-resume-state.ts +46 -0
- package/src/v1/harness-v1-sandbox-provider.ts +76 -0
- package/src/v1/harness-v1-session.ts +268 -0
- package/src/v1/harness-v1-skill.ts +22 -0
- package/src/v1/harness-v1-stream-part.ts +363 -0
- package/src/v1/harness-v1-tool-spec.ts +31 -0
- package/src/v1/harness-v1.ts +83 -0
- package/src/v1/index.ts +93 -0
- package/utils/index.ts +1 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
import {
|
|
3
|
+
harnessV1DebugConfigSchema,
|
|
4
|
+
harnessV1DebugLevelSchema,
|
|
5
|
+
type HarnessV1Diagnostic,
|
|
6
|
+
} from './harness-v1-diagnostic';
|
|
7
|
+
import {
|
|
8
|
+
harnessV1CompactionPartSchema,
|
|
9
|
+
harnessV1ErrorPartSchema,
|
|
10
|
+
harnessV1FileChangePartSchema,
|
|
11
|
+
harnessV1FinishPartSchema,
|
|
12
|
+
harnessV1FinishStepPartSchema,
|
|
13
|
+
harnessV1RawPartSchema,
|
|
14
|
+
harnessV1ReasoningDeltaPartSchema,
|
|
15
|
+
harnessV1ReasoningEndPartSchema,
|
|
16
|
+
harnessV1ReasoningStartPartSchema,
|
|
17
|
+
harnessV1StreamStartPartSchema,
|
|
18
|
+
harnessV1TextDeltaPartSchema,
|
|
19
|
+
harnessV1TextEndPartSchema,
|
|
20
|
+
harnessV1TextStartPartSchema,
|
|
21
|
+
harnessV1ToolApprovalRequestPartSchema,
|
|
22
|
+
harnessV1ToolCallPartSchema,
|
|
23
|
+
harnessV1ToolResultPartSchema,
|
|
24
|
+
} from './harness-v1-stream-part';
|
|
25
|
+
|
|
26
|
+
/*
|
|
27
|
+
* The bridge wire protocol shared by every bridge-backed harness adapter.
|
|
28
|
+
*
|
|
29
|
+
* This is the serialization of the host<->runtime contract for adapters that
|
|
30
|
+
* run the agent runtime inside the sandbox and talk to the host over a
|
|
31
|
+
* WebSocket. It exists ONLY because of that transport: untrusted JSON frames
|
|
32
|
+
* crossing the sandbox boundary need runtime validation, the connection needs
|
|
33
|
+
* a handshake, and the host drives turns with serialized commands. Every export
|
|
34
|
+
* here is therefore prefixed `harnessV1Bridge…`.
|
|
35
|
+
*
|
|
36
|
+
* It has three tiers:
|
|
37
|
+
*
|
|
38
|
+
* 1. The OUTBOUND events — `HarnessV1StreamPart` re-expressed as Zod (imported
|
|
39
|
+
* member schemas from `harness-v1-stream-part.ts`), because the part type is
|
|
40
|
+
* compile-time only and the frames need runtime validation at the boundary.
|
|
41
|
+
* 2. The transport/control frames that are NOT consumer events — `bridge-hello`
|
|
42
|
+
* (handshake), `bridge-detach` (resume payload), `bridge-thread` (a resume
|
|
43
|
+
* coordinate some runtimes announce). These ride the same socket.
|
|
44
|
+
* 3. The INBOUND command vocabulary the host sends back: the shared commands
|
|
45
|
+
* live here; the per-adapter `start` payload extends
|
|
46
|
+
* `harnessV1BridgeStartBaseSchema` and assembles the final inbound union in
|
|
47
|
+
* the adapter package.
|
|
48
|
+
*
|
|
49
|
+
* Non-bridge adapters (e.g. Pi) do not use this layer at all — they have no
|
|
50
|
+
* serialization boundary and target the universal `HarnessV1StreamPart` type
|
|
51
|
+
* directly. That is the deliberate split: `harness-v1-stream-part.ts` is the
|
|
52
|
+
* transport-agnostic event vocabulary; this file is the bridge transport.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The subset of a host-defined tool that travels on the `start` message. The
|
|
57
|
+
* runtime only needs the name, description, and JSON-Schema input to surface
|
|
58
|
+
* the tool; `execute` stays on the host.
|
|
59
|
+
*/
|
|
60
|
+
export const harnessV1BridgeToolWireSchema = z.object({
|
|
61
|
+
name: z.string(),
|
|
62
|
+
description: z.string().optional(),
|
|
63
|
+
inputSchema: z.unknown().optional(),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export type HarnessV1BridgeToolWire = z.infer<
|
|
67
|
+
typeof harnessV1BridgeToolWireSchema
|
|
68
|
+
>;
|
|
69
|
+
|
|
70
|
+
export const harnessV1BridgePermissionModeSchema = z.enum([
|
|
71
|
+
'allow-reads',
|
|
72
|
+
'allow-edits',
|
|
73
|
+
'allow-all',
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Common fields of the inbound `start` message. Each adapter extends this with
|
|
78
|
+
* its runtime-specific configuration (e.g. `thinking`/`continue` for Claude
|
|
79
|
+
* Code, `reasoningEffort`/`webSearch`/`skills`/`resumeThreadId` for Codex) and
|
|
80
|
+
* assembles the final inbound union from the shared command members below.
|
|
81
|
+
*
|
|
82
|
+
* `debug` carries the general `HarnessV1DebugConfig` — diagnostics config is not
|
|
83
|
+
* a bridge concept, it just happens to ride the `start` frame for bridge-backed
|
|
84
|
+
* adapters.
|
|
85
|
+
*/
|
|
86
|
+
export const harnessV1BridgeStartBaseSchema = z.object({
|
|
87
|
+
type: z.literal('start'),
|
|
88
|
+
prompt: z.string(),
|
|
89
|
+
tools: z.array(harnessV1BridgeToolWireSchema).optional(),
|
|
90
|
+
model: z.string().optional(),
|
|
91
|
+
debug: harnessV1DebugConfigSchema.optional(),
|
|
92
|
+
permissionMode: harnessV1BridgePermissionModeSchema.optional(),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// --- Transport / control frames (outbound, not consumer events) ---
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Sent the instant the bridge accepts an authenticated WS connection. The host
|
|
99
|
+
* waits for it before sending `start`/`resume`, because some sandbox runtimes
|
|
100
|
+
* complete the upstream WS handshake before the connection is wired through to
|
|
101
|
+
* the bridge process — anything sent in that gap is dropped. Carries the
|
|
102
|
+
* bridge's lifecycle `state` and highest emitted `seq` for reconnect.
|
|
103
|
+
*/
|
|
104
|
+
export const harnessV1BridgeHelloSchema = z.object({
|
|
105
|
+
type: z.literal('bridge-hello'),
|
|
106
|
+
state: z.string().optional(),
|
|
107
|
+
lastSeq: z.number().optional(),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The bridge's reply to an inbound `detach`. Carries the adapter-specific
|
|
112
|
+
* payload the host serializes into `HarnessV1ResumeState`.
|
|
113
|
+
*/
|
|
114
|
+
export const harnessV1BridgeDetachSchema = z.object({
|
|
115
|
+
type: z.literal('bridge-detach'),
|
|
116
|
+
data: z.unknown(),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* A resume coordinate the bridge proactively announces (e.g. Codex's thread id)
|
|
121
|
+
* so the host can cache it for a later resume without waiting for `detach`.
|
|
122
|
+
*/
|
|
123
|
+
export const harnessV1BridgeThreadSchema = z.object({
|
|
124
|
+
type: z.literal('bridge-thread'),
|
|
125
|
+
threadId: z.string(),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// --- Diagnostics frames (outbound, not consumer events) ---
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* One captured console line from inside the sandbox. The bridge line-buffers
|
|
132
|
+
* `process.stdout`/`process.stderr` and emits one of these per complete line.
|
|
133
|
+
* Routed host-side to the diagnostics sink, never to the consumer stream.
|
|
134
|
+
*/
|
|
135
|
+
export const harnessV1BridgeSandboxLogSchema = z.object({
|
|
136
|
+
type: z.literal('sandbox-log'),
|
|
137
|
+
source: z.string(),
|
|
138
|
+
stream: z.enum(['stdout', 'stderr']),
|
|
139
|
+
line: z.string(),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* A structured diagnostic an adapter emits from inside the bridge via
|
|
144
|
+
* `turn.bridgeLog(...)`. Gated by the session's debug level + subsystem filter.
|
|
145
|
+
*/
|
|
146
|
+
export const harnessV1BridgeDebugEventSchema = z.object({
|
|
147
|
+
type: z.literal('debug-event'),
|
|
148
|
+
level: harnessV1DebugLevelSchema,
|
|
149
|
+
subsystem: z.string(),
|
|
150
|
+
message: z.string(),
|
|
151
|
+
attrs: z.record(z.string(), z.unknown()).optional(),
|
|
152
|
+
error: z
|
|
153
|
+
.object({
|
|
154
|
+
name: z.string().optional(),
|
|
155
|
+
message: z.string(),
|
|
156
|
+
stack: z.string().optional(),
|
|
157
|
+
})
|
|
158
|
+
.optional(),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Every frame a bridge can send to the host: the stream-part events plus the
|
|
163
|
+
* transport/control frames. This is the schema the host `SandboxChannel`
|
|
164
|
+
* validates inbound frames against.
|
|
165
|
+
*/
|
|
166
|
+
export const harnessV1BridgeOutboundMessageSchema = z.discriminatedUnion(
|
|
167
|
+
'type',
|
|
168
|
+
[
|
|
169
|
+
harnessV1StreamStartPartSchema,
|
|
170
|
+
harnessV1TextStartPartSchema,
|
|
171
|
+
harnessV1TextDeltaPartSchema,
|
|
172
|
+
harnessV1TextEndPartSchema,
|
|
173
|
+
harnessV1ReasoningStartPartSchema,
|
|
174
|
+
harnessV1ReasoningDeltaPartSchema,
|
|
175
|
+
harnessV1ReasoningEndPartSchema,
|
|
176
|
+
harnessV1ToolCallPartSchema,
|
|
177
|
+
harnessV1ToolApprovalRequestPartSchema,
|
|
178
|
+
harnessV1ToolResultPartSchema,
|
|
179
|
+
harnessV1FinishStepPartSchema,
|
|
180
|
+
harnessV1FinishPartSchema,
|
|
181
|
+
harnessV1FileChangePartSchema,
|
|
182
|
+
harnessV1CompactionPartSchema,
|
|
183
|
+
harnessV1ErrorPartSchema,
|
|
184
|
+
harnessV1RawPartSchema,
|
|
185
|
+
harnessV1BridgeHelloSchema,
|
|
186
|
+
harnessV1BridgeDetachSchema,
|
|
187
|
+
harnessV1BridgeThreadSchema,
|
|
188
|
+
harnessV1BridgeSandboxLogSchema,
|
|
189
|
+
harnessV1BridgeDebugEventSchema,
|
|
190
|
+
],
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
export type HarnessV1BridgeOutboundMessage = z.infer<
|
|
194
|
+
typeof harnessV1BridgeOutboundMessageSchema
|
|
195
|
+
>;
|
|
196
|
+
|
|
197
|
+
export type HarnessV1BridgeSandboxLog = z.infer<
|
|
198
|
+
typeof harnessV1BridgeSandboxLogSchema
|
|
199
|
+
>;
|
|
200
|
+
|
|
201
|
+
export type HarnessV1BridgeDebugEvent = z.infer<
|
|
202
|
+
typeof harnessV1BridgeDebugEventSchema
|
|
203
|
+
>;
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Normalize a bridge diagnostics wire frame into the transport-agnostic
|
|
207
|
+
* `HarnessV1Diagnostic` an adapter reports to the framework. A captured console
|
|
208
|
+
* line maps `stderr` → `warn` and `stdout` → `info`; a structured event passes
|
|
209
|
+
* its fields through. This is the seam where the bridge's serialization is
|
|
210
|
+
* lifted into the general emission shape every harness shares.
|
|
211
|
+
*/
|
|
212
|
+
export function harnessV1DiagnosticFromBridgeFrame(
|
|
213
|
+
frame: HarnessV1BridgeSandboxLog | HarnessV1BridgeDebugEvent,
|
|
214
|
+
context: { sessionId?: string; timestamp: number },
|
|
215
|
+
): HarnessV1Diagnostic {
|
|
216
|
+
if (frame.type === 'sandbox-log') {
|
|
217
|
+
return {
|
|
218
|
+
level: frame.stream === 'stderr' ? 'warn' : 'info',
|
|
219
|
+
message: frame.line,
|
|
220
|
+
subsystem: `sandbox.log.${frame.source}`,
|
|
221
|
+
kind: 'log',
|
|
222
|
+
source: frame.source,
|
|
223
|
+
stream: frame.stream,
|
|
224
|
+
sessionId: context.sessionId,
|
|
225
|
+
timestamp: context.timestamp,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
level: frame.level,
|
|
230
|
+
message: frame.message,
|
|
231
|
+
subsystem: frame.subsystem,
|
|
232
|
+
kind: 'event',
|
|
233
|
+
attrs: frame.attrs,
|
|
234
|
+
error: frame.error,
|
|
235
|
+
sessionId: context.sessionId,
|
|
236
|
+
timestamp: context.timestamp,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// --- Shared inbound command members (host -> bridge) ---
|
|
241
|
+
|
|
242
|
+
export const harnessV1BridgeToolResultInboundSchema = z.object({
|
|
243
|
+
type: z.literal('tool-result'),
|
|
244
|
+
toolCallId: z.string(),
|
|
245
|
+
output: z.unknown(),
|
|
246
|
+
isError: z.boolean().optional(),
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
export const harnessV1BridgeToolApprovalResponseInboundSchema = z.object({
|
|
250
|
+
type: z.literal('tool-approval-response'),
|
|
251
|
+
approvalId: z.string(),
|
|
252
|
+
approved: z.boolean(),
|
|
253
|
+
reason: z.string().optional(),
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
export const harnessV1BridgeUserMessageInboundSchema = z.object({
|
|
257
|
+
type: z.literal('user-message'),
|
|
258
|
+
text: z.string(),
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
export const harnessV1BridgeAbortInboundSchema = z.object({
|
|
262
|
+
type: z.literal('abort'),
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
export const harnessV1BridgeShutdownInboundSchema = z.object({
|
|
266
|
+
type: z.literal('shutdown'),
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Reconnect: after re-establishing the socket the host asks the bridge to
|
|
271
|
+
* replay every buffered event with `seq > lastSeenEventId`.
|
|
272
|
+
*/
|
|
273
|
+
export const harnessV1BridgeResumeInboundSchema = z.object({
|
|
274
|
+
type: z.literal('resume'),
|
|
275
|
+
lastSeenEventId: z.number(),
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* The bridge replies with `bridge-detach` carrying any cached resume payload,
|
|
280
|
+
* then exits.
|
|
281
|
+
*/
|
|
282
|
+
export const harnessV1BridgeDetachInboundSchema = z.object({
|
|
283
|
+
type: z.literal('detach'),
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* The inbound command members shared by every bridge adapter. Spread these
|
|
288
|
+
* alongside the adapter's own `start` schema to build the final inbound union:
|
|
289
|
+
* `z.discriminatedUnion('type', [adapterStartSchema, ...harnessV1BridgeInboundCommandSchemas])`.
|
|
290
|
+
*/
|
|
291
|
+
export const harnessV1BridgeInboundCommandSchemas = [
|
|
292
|
+
harnessV1BridgeToolResultInboundSchema,
|
|
293
|
+
harnessV1BridgeToolApprovalResponseInboundSchema,
|
|
294
|
+
harnessV1BridgeUserMessageInboundSchema,
|
|
295
|
+
harnessV1BridgeAbortInboundSchema,
|
|
296
|
+
harnessV1BridgeShutdownInboundSchema,
|
|
297
|
+
harnessV1BridgeResumeInboundSchema,
|
|
298
|
+
harnessV1BridgeDetachInboundSchema,
|
|
299
|
+
] as const;
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* The JSON line the bridge writes to stdout once its WebSocket server is bound,
|
|
303
|
+
* announcing the port the host should connect to.
|
|
304
|
+
*/
|
|
305
|
+
export const harnessV1BridgeReadySchema = z.object({
|
|
306
|
+
type: z.literal('bridge-ready'),
|
|
307
|
+
port: z.number(),
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
export type HarnessV1BridgeReady = z.infer<typeof harnessV1BridgeReadySchema>;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { tool, type FlexibleSchema, type Tool } from '@ai-sdk/provider-utils';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Cross-harness vocabulary of common built-in tool names with their baseline
|
|
6
|
+
* input schemas. Adapters that declare a built-in with one of these
|
|
7
|
+
* `commonName`s must accept (at least) every input the baseline schema
|
|
8
|
+
* accepts. Extra optional fields are encouraged.
|
|
9
|
+
*
|
|
10
|
+
* Used both as runtime values (spread into `ToolSet`s for inspection) and as
|
|
11
|
+
* a vocabulary source — `HarnessV1BuiltinToolName` is derived from its keys.
|
|
12
|
+
*/
|
|
13
|
+
export const HARNESS_V1_BUILTIN_TOOLS = {
|
|
14
|
+
read: tool({
|
|
15
|
+
description: 'Read file contents',
|
|
16
|
+
inputSchema: z.object({ file_path: z.string() }),
|
|
17
|
+
outputSchema: z.unknown(),
|
|
18
|
+
}),
|
|
19
|
+
write: tool({
|
|
20
|
+
description: 'Write content to a file',
|
|
21
|
+
inputSchema: z.object({ file_path: z.string(), content: z.string() }),
|
|
22
|
+
outputSchema: z.unknown(),
|
|
23
|
+
}),
|
|
24
|
+
edit: tool({
|
|
25
|
+
description: 'Edit a file by replacing text',
|
|
26
|
+
inputSchema: z.object({
|
|
27
|
+
file_path: z.string(),
|
|
28
|
+
old_string: z.string(),
|
|
29
|
+
new_string: z.string(),
|
|
30
|
+
}),
|
|
31
|
+
outputSchema: z.unknown(),
|
|
32
|
+
}),
|
|
33
|
+
bash: tool({
|
|
34
|
+
description: 'Execute a shell command',
|
|
35
|
+
inputSchema: z.object({ command: z.string() }),
|
|
36
|
+
outputSchema: z.unknown(),
|
|
37
|
+
}),
|
|
38
|
+
grep: tool({
|
|
39
|
+
description: 'Search file contents with regex',
|
|
40
|
+
inputSchema: z.object({ pattern: z.string() }),
|
|
41
|
+
outputSchema: z.unknown(),
|
|
42
|
+
}),
|
|
43
|
+
glob: tool({
|
|
44
|
+
description: 'Find files matching a glob pattern',
|
|
45
|
+
inputSchema: z.object({ pattern: z.string() }),
|
|
46
|
+
outputSchema: z.unknown(),
|
|
47
|
+
}),
|
|
48
|
+
webSearch: tool({
|
|
49
|
+
description: 'Search the web',
|
|
50
|
+
inputSchema: z.object({ query: z.string() }),
|
|
51
|
+
outputSchema: z.unknown(),
|
|
52
|
+
}),
|
|
53
|
+
} as const;
|
|
54
|
+
|
|
55
|
+
export type HarnessV1BuiltinToolName = keyof typeof HARNESS_V1_BUILTIN_TOOLS;
|
|
56
|
+
|
|
57
|
+
export const HARNESS_V1_BUILTIN_TOOL_NAMES = Object.keys(
|
|
58
|
+
HARNESS_V1_BUILTIN_TOOLS,
|
|
59
|
+
) as ReadonlyArray<HarnessV1BuiltinToolName>;
|
|
60
|
+
|
|
61
|
+
export type HarnessV1BuiltinToolUseKind = 'readonly' | 'edit' | 'bash';
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* A tool that the adapter's underlying runtime exposes natively. Extends the
|
|
65
|
+
* AI SDK `Tool` shape with two optional harness-specific fields:
|
|
66
|
+
*
|
|
67
|
+
* - `nativeName`: the name as the underlying runtime knows it. Required
|
|
68
|
+
* only when the tool's key in the harness's `builtinTools` is not the
|
|
69
|
+
* native name — i.e. when the tool maps to a `commonName` (e.g. key
|
|
70
|
+
* `'bash'` for Claude Code's native `'Bash'`). Tools without a common
|
|
71
|
+
* equivalent are keyed by their native name directly, so `nativeName`
|
|
72
|
+
* is redundant and omitted.
|
|
73
|
+
* - `commonName`: cross-harness label drawn from
|
|
74
|
+
* `HARNESS_V1_BUILTIN_TOOL_NAMES`. Set when the tool maps to a familiar
|
|
75
|
+
* capability; consumers use it to recognize, e.g., that Claude Code's
|
|
76
|
+
* `Bash` and Codex's `shell` are the same kind of tool.
|
|
77
|
+
*
|
|
78
|
+
* Always set both fields together via the `commonTool` helper, or neither
|
|
79
|
+
* (declare the tool with the AI SDK's `tool()` directly).
|
|
80
|
+
*/
|
|
81
|
+
export type HarnessV1BuiltinTool<INPUT = unknown, OUTPUT = unknown> = Tool<
|
|
82
|
+
INPUT,
|
|
83
|
+
OUTPUT,
|
|
84
|
+
any
|
|
85
|
+
> & {
|
|
86
|
+
readonly nativeName?: string;
|
|
87
|
+
readonly commonName?: HarnessV1BuiltinToolName;
|
|
88
|
+
readonly toolUseKind?: HarnessV1BuiltinToolUseKind;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
type InputOf<T> = T extends Tool<infer I, any, any> ? I : never;
|
|
92
|
+
|
|
93
|
+
type StandardInputOf<N extends HarnessV1BuiltinToolName> = InputOf<
|
|
94
|
+
(typeof HARNESS_V1_BUILTIN_TOOLS)[N]
|
|
95
|
+
>;
|
|
96
|
+
|
|
97
|
+
/*
|
|
98
|
+
* Type-level superset check. If `TStandard` is assignable to `TAdapter`
|
|
99
|
+
* (i.e. the adapter accepts every input the standard accepts), the return
|
|
100
|
+
* type is `TOk`. Otherwise it's a tagged error tuple that surfaces a clear
|
|
101
|
+
* TypeScript error at the call site.
|
|
102
|
+
*/
|
|
103
|
+
type SupersetCheck<TStandard, TAdapter, TOk> = TStandard extends TAdapter
|
|
104
|
+
? TOk
|
|
105
|
+
: [
|
|
106
|
+
'ERROR: adapter input schema must be a superset of the standard schema',
|
|
107
|
+
{ expected: TStandard; got: TAdapter },
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Declare a built-in tool that maps to a cross-harness common name. The
|
|
112
|
+
* adapter's input schema must accept every input the standard schema for
|
|
113
|
+
* `commonName` accepts. Extra optional fields are encouraged.
|
|
114
|
+
*
|
|
115
|
+
* If the schema is missing a field the standard requires (or has an
|
|
116
|
+
* incompatible type), the return type collapses to a tagged error tuple,
|
|
117
|
+
* which fails the surrounding `as const satisfies ToolSet` assignment and
|
|
118
|
+
* surfaces a readable TypeScript error at the offending entry.
|
|
119
|
+
*/
|
|
120
|
+
export function commonTool<TName extends HarnessV1BuiltinToolName, TInput>(
|
|
121
|
+
commonName: TName,
|
|
122
|
+
opts: {
|
|
123
|
+
readonly nativeName: string;
|
|
124
|
+
readonly toolUseKind?: HarnessV1BuiltinToolUseKind;
|
|
125
|
+
readonly description?: string;
|
|
126
|
+
readonly inputSchema: FlexibleSchema<TInput>;
|
|
127
|
+
},
|
|
128
|
+
): SupersetCheck<StandardInputOf<TName>, TInput, HarnessV1BuiltinTool<TInput>> {
|
|
129
|
+
return {
|
|
130
|
+
...tool({
|
|
131
|
+
description: opts.description,
|
|
132
|
+
inputSchema: opts.inputSchema as FlexibleSchema<TInput>,
|
|
133
|
+
}),
|
|
134
|
+
nativeName: opts.nativeName,
|
|
135
|
+
commonName,
|
|
136
|
+
toolUseKind: opts.toolUseKind,
|
|
137
|
+
} as never;
|
|
138
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Warning emitted by a harness adapter during a call.
|
|
3
|
+
*
|
|
4
|
+
* Surfaces non-fatal issues such as unsupported options or quirks of the
|
|
5
|
+
* underlying agent runtime. Mirrors the shape of `SharedV4Warning` but lives
|
|
6
|
+
* in the harness namespace.
|
|
7
|
+
*/
|
|
8
|
+
export type HarnessV1CallWarning =
|
|
9
|
+
| {
|
|
10
|
+
type: 'unsupported-setting';
|
|
11
|
+
setting: string;
|
|
12
|
+
details?: string;
|
|
13
|
+
}
|
|
14
|
+
| {
|
|
15
|
+
type: 'unsupported-tool';
|
|
16
|
+
tool: string;
|
|
17
|
+
details?: string;
|
|
18
|
+
}
|
|
19
|
+
| {
|
|
20
|
+
type: 'other';
|
|
21
|
+
message: string;
|
|
22
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Diagnostics EMISSION contract — part of the `HarnessV1` spec.
|
|
5
|
+
*
|
|
6
|
+
* These are the types a harness adapter produces and receives: an adapter
|
|
7
|
+
* reports a `HarnessV1Diagnostic` to the framework (a bridge adapter normalizes
|
|
8
|
+
* its wire frames into one; a non-bridge adapter constructs one directly), and
|
|
9
|
+
* receives a `HarnessV1DebugConfig` to gate what it emits. They are distinct
|
|
10
|
+
* from the unaffixed host-facing `HarnessDiagnostic` / `HarnessDebugConfig`
|
|
11
|
+
* (the external/telemetry surface) — the framework maps between the two at the
|
|
12
|
+
* boundary, so the emission and consumption surfaces can evolve independently.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Severity of a diagnostic, ordered most → least severe. */
|
|
16
|
+
export const harnessV1DebugLevelSchema = z.enum([
|
|
17
|
+
'error',
|
|
18
|
+
'warn',
|
|
19
|
+
'info',
|
|
20
|
+
'debug',
|
|
21
|
+
'trace',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
export type HarnessV1DebugLevel = z.infer<typeof harnessV1DebugLevelSchema>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Per-session diagnostics configuration the framework hands an adapter (and the
|
|
28
|
+
* host sends on `start.debug`). When absent or `enabled` is false the adapter
|
|
29
|
+
* captures and emits nothing. `subsystems` filters structured events by dotted
|
|
30
|
+
* prefix; console capture is independent of the subsystem filter.
|
|
31
|
+
*/
|
|
32
|
+
export const harnessV1DebugConfigSchema = z.object({
|
|
33
|
+
enabled: z.boolean().optional(),
|
|
34
|
+
level: harnessV1DebugLevelSchema.optional(),
|
|
35
|
+
subsystems: z.array(z.string()).optional(),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export type HarnessV1DebugConfig = z.infer<typeof harnessV1DebugConfigSchema>;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A diagnostic as emitted by a harness adapter. Structurally identical to the
|
|
42
|
+
* host-facing `HarnessDiagnostic` today, but kept separate: this is the spec's
|
|
43
|
+
* emission shape, that is the external consumption shape.
|
|
44
|
+
*/
|
|
45
|
+
export type HarnessV1Diagnostic = {
|
|
46
|
+
/** Severity. */
|
|
47
|
+
readonly level: HarnessV1DebugLevel;
|
|
48
|
+
/** Human-readable line (console capture) or message (structured event). */
|
|
49
|
+
readonly message: string;
|
|
50
|
+
/** Dotted subsystem (`sandbox.log.<source>` for console capture). */
|
|
51
|
+
readonly subsystem: string;
|
|
52
|
+
/** `'log'` = captured console line; `'event'` = structured emission. */
|
|
53
|
+
readonly kind: 'log' | 'event';
|
|
54
|
+
/** Originating source label (console capture). */
|
|
55
|
+
readonly source?: string;
|
|
56
|
+
/** Which standard stream the line came from (console capture). */
|
|
57
|
+
readonly stream?: 'stdout' | 'stderr';
|
|
58
|
+
/** Structured attributes (structured events only). */
|
|
59
|
+
readonly attrs?: Record<string, unknown>;
|
|
60
|
+
/** Error payload (structured events only). */
|
|
61
|
+
readonly error?: { name?: string; message: string; stack?: string };
|
|
62
|
+
/** The harness session this diagnostic originated from. */
|
|
63
|
+
readonly sessionId?: string;
|
|
64
|
+
/** Emission time (epoch ms). */
|
|
65
|
+
readonly timestamp: number;
|
|
66
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { JSONValue } from '@ai-sdk/provider';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Adapter-namespaced opaque data attached to harness events.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors the `providerMetadata` pattern from `@ai-sdk/provider`, but lives
|
|
7
|
+
* in the harness namespace because a harness is a peer concept to a
|
|
8
|
+
* provider, not a kind of provider.
|
|
9
|
+
*
|
|
10
|
+
* Keys are harness ids (e.g. `'claude-code'`). Inner values are arbitrary
|
|
11
|
+
* JSON-serializable data the adapter chooses to surface to callers.
|
|
12
|
+
*/
|
|
13
|
+
export type HarnessV1Metadata = Record<string, Record<string, JSONValue>>;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { Experimental_SandboxSession as SandboxSession } from '@ai-sdk/provider-utils';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Network sandbox session returned by `HarnessV1SandboxProvider.createSession()`. The
|
|
5
|
+
* harness keeps this for the lifetime of a session. It is itself a
|
|
6
|
+
* {@link SandboxSession} (file I/O, exec, spawn) and adds the infra surface on
|
|
7
|
+
* top: port resolution, lifecycle, and network-policy mutation.
|
|
8
|
+
*
|
|
9
|
+
* Code that should only touch the filesystem and spawn processes receives the
|
|
10
|
+
* reduced view from {@link HarnessV1NetworkSandboxSession.restricted}, never the
|
|
11
|
+
* network sandbox session itself — so it cannot stop the sandbox or change its
|
|
12
|
+
* network policy.
|
|
13
|
+
*/
|
|
14
|
+
export interface HarnessV1NetworkSandboxSession extends SandboxSession {
|
|
15
|
+
/**
|
|
16
|
+
* Stable identifier for the underlying sandbox resource. Used by the
|
|
17
|
+
* harness session manager as the durable lookup key for cross-process
|
|
18
|
+
* resume — the framework persists this on the resume payload so a future
|
|
19
|
+
* process can call `HarnessV1SandboxProvider.resume?({ sessionId })` and
|
|
20
|
+
* reach the same resource. Providers populate it from their native
|
|
21
|
+
* identifier (Vercel: the sandbox name; just-bash: a UUID minted at
|
|
22
|
+
* create time).
|
|
23
|
+
*/
|
|
24
|
+
readonly id: string;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The sandbox's default working directory — the absolute path that
|
|
28
|
+
* `run`/`spawn` resolve relative commands against when no `workingDirectory`
|
|
29
|
+
* is given. Read from the live sandbox (it is provider-specific and
|
|
30
|
+
* configurable at create time: Vercel defaults to `/vercel/sandbox`,
|
|
31
|
+
* just-bash to `/home/user`), never hardcoded.
|
|
32
|
+
*
|
|
33
|
+
* The framework composes each session's working directory underneath this
|
|
34
|
+
* path (`<defaultWorkingDirectory>/<harnessId>-<sessionId>`) so adapters do
|
|
35
|
+
* not bake a provider-specific base into their own paths.
|
|
36
|
+
*/
|
|
37
|
+
readonly defaultWorkingDirectory: string;
|
|
38
|
+
|
|
39
|
+
/** Ports the sandbox exposes; resolvable to public URLs via `getPortUrl`. */
|
|
40
|
+
readonly ports: ReadonlyArray<number>;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolve a publicly-reachable URL for a sandbox-exposed port. Bridge-backed
|
|
44
|
+
* adapters call this to open their WebSocket to the in-sandbox bridge.
|
|
45
|
+
*/
|
|
46
|
+
readonly getPortUrl: (options: {
|
|
47
|
+
port: number;
|
|
48
|
+
protocol?: 'http' | 'https' | 'ws';
|
|
49
|
+
}) => PromiseLike<string>;
|
|
50
|
+
|
|
51
|
+
/** Stop the sandbox. Idempotent. */
|
|
52
|
+
readonly stop: () => PromiseLike<void>;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Destroy/delete the sandbox resource when supported. Optional because some
|
|
56
|
+
* providers only have a stop/dispose concept. Implementations must handle
|
|
57
|
+
* both a still-running sandbox and a previously stopped sandbox.
|
|
58
|
+
*/
|
|
59
|
+
readonly destroy?: () => PromiseLike<void>;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Update the sandbox's outbound network policy. Optional — implementations
|
|
63
|
+
* without a local enforcement primitive (e.g. just-bash) omit this. Callers
|
|
64
|
+
* use optional-call (`sandboxSession.setNetworkPolicy?.(policy)`); a
|
|
65
|
+
* missing implementation is a no-op.
|
|
66
|
+
*/
|
|
67
|
+
readonly setNetworkPolicy?: (
|
|
68
|
+
policy: HarnessV1NetworkPolicy,
|
|
69
|
+
) => PromiseLike<void>;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Replace the set of ports exposed by the sandbox. Full-replacement
|
|
73
|
+
* semantics: ports omitted from the array are deregistered. Optional —
|
|
74
|
+
* implementations that cannot expose ports (e.g. just-bash) omit this.
|
|
75
|
+
*/
|
|
76
|
+
readonly setPorts?: (
|
|
77
|
+
ports: ReadonlyArray<number>,
|
|
78
|
+
options?: { abortSignal?: AbortSignal },
|
|
79
|
+
) => PromiseLike<void>;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Reduced view of this session, typed as the bare {@link SandboxSession}
|
|
83
|
+
* (file I/O, exec, spawn) — nothing that could stop the sandbox or change
|
|
84
|
+
* its network policy. Pass this to user-tool `execute()` calls and other
|
|
85
|
+
* code that must not reach the infra surface.
|
|
86
|
+
*
|
|
87
|
+
* The returned object points at exactly the same underlying sandbox
|
|
88
|
+
* resource as the network sandbox session it was produced from; it is only a
|
|
89
|
+
* narrower surface over the same resource, not a separate sandbox.
|
|
90
|
+
*/
|
|
91
|
+
readonly restricted: () => SandboxSession;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Outbound network policy applied by the sandbox runtime.
|
|
96
|
+
*
|
|
97
|
+
* `'allow-all'` and `'deny-all'` are convenience presets. `'custom'` is an
|
|
98
|
+
* allow-list with an optional CIDR deny-list that takes precedence:
|
|
99
|
+
*
|
|
100
|
+
* - Reachable hosts are the union of `allowedHosts` and `allowedCIDRs`.
|
|
101
|
+
* - `deniedCIDRs` wins over both, useful for blocking cloud-metadata IPs while
|
|
102
|
+
* otherwise allowing broad access.
|
|
103
|
+
*
|
|
104
|
+
* The two `'custom'` branches share the same discriminator but each requires
|
|
105
|
+
* a different allow field. Specifying `'custom'` with only `deniedCIDRs`
|
|
106
|
+
* (deny-only) is rejected at compile time — functionally it would be
|
|
107
|
+
* equivalent to `'deny-all'`.
|
|
108
|
+
*/
|
|
109
|
+
export type HarnessV1NetworkPolicy =
|
|
110
|
+
| { mode: 'allow-all' }
|
|
111
|
+
| { mode: 'deny-all' }
|
|
112
|
+
| {
|
|
113
|
+
mode: 'custom';
|
|
114
|
+
allowedHosts: ReadonlyArray<string>;
|
|
115
|
+
allowedCIDRs?: ReadonlyArray<string>;
|
|
116
|
+
deniedCIDRs?: ReadonlyArray<string>;
|
|
117
|
+
}
|
|
118
|
+
| {
|
|
119
|
+
mode: 'custom';
|
|
120
|
+
allowedHosts?: ReadonlyArray<string>;
|
|
121
|
+
allowedCIDRs: ReadonlyArray<string>;
|
|
122
|
+
deniedCIDRs?: ReadonlyArray<string>;
|
|
123
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
HarnessV1DebugConfig,
|
|
3
|
+
HarnessV1Diagnostic,
|
|
4
|
+
} from './harness-v1-diagnostic';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Diagnostics wiring the framework hands to an adapter's `doStart`. `report` is
|
|
8
|
+
* the general emission sink: a bridge adapter normalizes each wire frame into a
|
|
9
|
+
* `HarnessV1Diagnostic` (via `harnessV1DiagnosticFromBridgeFrame`) and calls it;
|
|
10
|
+
* a non-bridge adapter constructs a `HarnessV1Diagnostic` from its host-side
|
|
11
|
+
* logs/errors and calls it directly. `debug` gates what the adapter emits.
|
|
12
|
+
* Absent when the consumer has not enabled diagnostics.
|
|
13
|
+
*/
|
|
14
|
+
export type HarnessV1Observability = {
|
|
15
|
+
/** Per-session debug config gating what the adapter captures/emits. */
|
|
16
|
+
readonly debug?: HarnessV1DebugConfig;
|
|
17
|
+
|
|
18
|
+
/** General emission sink — any adapter reports a `HarnessV1Diagnostic` here. */
|
|
19
|
+
readonly report?: (diagnostic: HarnessV1Diagnostic) => void;
|
|
20
|
+
};
|