@ai-sdk/harness 1.0.39 → 1.0.41
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 +19 -0
- package/dist/agent/index.d.ts +19 -4
- package/dist/agent/index.js +200 -104
- package/dist/agent/index.js.map +1 -1
- package/dist/bridge/index.js +8 -1
- package/dist/bridge/index.js.map +1 -1
- package/dist/utils/index.d.ts +5 -1
- package/dist/utils/index.js +37 -5
- package/dist/utils/index.js.map +1 -1
- package/package.json +2 -2
- package/src/agent/harness-agent-session.ts +38 -8
- package/src/agent/harness-agent-settings.ts +25 -1
- package/src/agent/harness-agent.ts +17 -2
- package/src/agent/internal/run-prompt.ts +110 -38
- package/src/agent/internal/turn-telemetry.ts +60 -46
- package/src/bridge/index.ts +11 -1
- package/src/utils/sandbox-channel.ts +61 -3
|
@@ -42,9 +42,9 @@ export interface TurnTelemetry {
|
|
|
42
42
|
* model the runtime resolved to (overriding the session's configured id).
|
|
43
43
|
* Idempotent — the first call wins.
|
|
44
44
|
*/
|
|
45
|
-
start(modelId?: string): void
|
|
45
|
+
start(modelId?: string): Promise<void>;
|
|
46
46
|
/** Open a step span lazily, before the first content of a step. */
|
|
47
|
-
ensureStepOpen(): void
|
|
47
|
+
ensureStepOpen(): Promise<void>;
|
|
48
48
|
/** Close the current step (on a harness `finish-step`). */
|
|
49
49
|
stepFinish(info: {
|
|
50
50
|
finishReason: unknown;
|
|
@@ -52,13 +52,18 @@ export interface TurnTelemetry {
|
|
|
52
52
|
providerMetadata?: unknown;
|
|
53
53
|
/** The model's output content for this step (text/reasoning/tool-calls). */
|
|
54
54
|
content?: TurnContentPart[];
|
|
55
|
-
}): void
|
|
55
|
+
}): Promise<void>;
|
|
56
56
|
/** A tool execution began (on a `tool-call`). */
|
|
57
57
|
toolStart(call: {
|
|
58
58
|
toolCallId: string;
|
|
59
59
|
toolName: string;
|
|
60
60
|
input: unknown;
|
|
61
|
-
}): void
|
|
61
|
+
}): Promise<void>;
|
|
62
|
+
/** Execute a host tool through each telemetry integration's context wrapper. */
|
|
63
|
+
executeTool<T>(input: {
|
|
64
|
+
toolCallId: string;
|
|
65
|
+
execute: () => PromiseLike<T>;
|
|
66
|
+
}): Promise<T>;
|
|
62
67
|
/**
|
|
63
68
|
* A tool execution completed (on its `tool-result` or after host execution).
|
|
64
69
|
* Idempotent per `toolCallId` — the first caller wins, so provider-executed
|
|
@@ -67,21 +72,24 @@ export interface TurnTelemetry {
|
|
|
67
72
|
toolEnd(
|
|
68
73
|
toolCallId: string,
|
|
69
74
|
output: { ok: true; output: unknown } | { ok: false; error: unknown },
|
|
70
|
-
): void
|
|
75
|
+
): Promise<void>;
|
|
71
76
|
/** The turn ended (on a harness `finish`). */
|
|
72
|
-
end(info: { finishReason: unknown; usage: unknown }): void
|
|
77
|
+
end(info: { finishReason: unknown; usage: unknown }): Promise<void>;
|
|
73
78
|
/** The turn failed. */
|
|
74
|
-
error(err: unknown): void
|
|
79
|
+
error(err: unknown): Promise<void>;
|
|
75
80
|
}
|
|
76
81
|
|
|
77
82
|
const NOOP: TurnTelemetry = {
|
|
78
|
-
start() {},
|
|
79
|
-
ensureStepOpen() {},
|
|
80
|
-
stepFinish() {},
|
|
81
|
-
toolStart() {},
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
async start() {},
|
|
84
|
+
async ensureStepOpen() {},
|
|
85
|
+
async stepFinish() {},
|
|
86
|
+
async toolStart() {},
|
|
87
|
+
async executeTool({ execute }) {
|
|
88
|
+
return await execute();
|
|
89
|
+
},
|
|
90
|
+
async toolEnd() {},
|
|
91
|
+
async end() {},
|
|
92
|
+
async error() {},
|
|
85
93
|
};
|
|
86
94
|
|
|
87
95
|
export function createTurnTelemetry(opts: {
|
|
@@ -122,10 +130,10 @@ export function createTurnTelemetry(opts: {
|
|
|
122
130
|
|
|
123
131
|
// onStart — open the operation (root) span. Deferred until `start()` so the
|
|
124
132
|
// runtime-resolved model can be attached to the operation span + trace label.
|
|
125
|
-
const fireStart = (): void => {
|
|
133
|
+
const fireStart = async (): Promise<void> => {
|
|
126
134
|
if (started) return;
|
|
127
135
|
started = true;
|
|
128
|
-
dispatcher.onStart?.(
|
|
136
|
+
await dispatcher.onStart?.(
|
|
129
137
|
cast<'onStart'>({
|
|
130
138
|
callId,
|
|
131
139
|
operationId: 'ai.harness',
|
|
@@ -147,17 +155,17 @@ export function createTurnTelemetry(opts: {
|
|
|
147
155
|
);
|
|
148
156
|
};
|
|
149
157
|
|
|
150
|
-
const start = (overrideModelId?: string): void => {
|
|
158
|
+
const start = async (overrideModelId?: string): Promise<void> => {
|
|
151
159
|
if (started) return;
|
|
152
160
|
if (overrideModelId) modelId = overrideModelId;
|
|
153
|
-
fireStart();
|
|
161
|
+
await fireStart();
|
|
154
162
|
};
|
|
155
163
|
|
|
156
|
-
const ensureStepOpen = (): void => {
|
|
157
|
-
if (!started) fireStart();
|
|
164
|
+
const ensureStepOpen = async (): Promise<void> => {
|
|
165
|
+
if (!started) await fireStart();
|
|
158
166
|
if (stepOpen || ended) return;
|
|
159
167
|
stepOpen = true;
|
|
160
|
-
dispatcher.onStepStart?.(
|
|
168
|
+
await dispatcher.onStepStart?.(
|
|
161
169
|
cast<'onStepStart'>({
|
|
162
170
|
callId,
|
|
163
171
|
provider,
|
|
@@ -175,7 +183,7 @@ export function createTurnTelemetry(opts: {
|
|
|
175
183
|
);
|
|
176
184
|
// Open the inference (language-model call) span — the gen_ai home for the
|
|
177
185
|
// step's input and (on end) output messages.
|
|
178
|
-
dispatcher.onLanguageModelCallStart?.(
|
|
186
|
+
await dispatcher.onLanguageModelCallStart?.(
|
|
179
187
|
cast<'onLanguageModelCallStart'>({
|
|
180
188
|
callId,
|
|
181
189
|
provider,
|
|
@@ -187,12 +195,12 @@ export function createTurnTelemetry(opts: {
|
|
|
187
195
|
};
|
|
188
196
|
|
|
189
197
|
/** Close the inference span with the step's output content. */
|
|
190
|
-
const inferenceEnd = (info: {
|
|
198
|
+
const inferenceEnd = async (info: {
|
|
191
199
|
finishReason: unknown;
|
|
192
200
|
usage: unknown;
|
|
193
201
|
content: TurnContentPart[];
|
|
194
|
-
}): void => {
|
|
195
|
-
dispatcher.onLanguageModelCallEnd?.(
|
|
202
|
+
}): Promise<void> => {
|
|
203
|
+
await dispatcher.onLanguageModelCallEnd?.(
|
|
196
204
|
cast<'onLanguageModelCallEnd'>({
|
|
197
205
|
callId,
|
|
198
206
|
finishReason: info.finishReason,
|
|
@@ -203,9 +211,9 @@ export function createTurnTelemetry(opts: {
|
|
|
203
211
|
);
|
|
204
212
|
};
|
|
205
213
|
|
|
206
|
-
const closeOpenTools = (): void => {
|
|
214
|
+
const closeOpenTools = async (): Promise<void> => {
|
|
207
215
|
for (const call of openTools.values()) {
|
|
208
|
-
dispatcher.onToolExecutionEnd?.(
|
|
216
|
+
await dispatcher.onToolExecutionEnd?.(
|
|
209
217
|
cast<'onToolExecutionEnd'>({
|
|
210
218
|
callId,
|
|
211
219
|
toolExecutionMs: 0,
|
|
@@ -229,16 +237,16 @@ export function createTurnTelemetry(opts: {
|
|
|
229
237
|
start,
|
|
230
238
|
ensureStepOpen,
|
|
231
239
|
|
|
232
|
-
stepFinish(info) {
|
|
240
|
+
async stepFinish(info) {
|
|
233
241
|
if (!stepOpen) return;
|
|
234
242
|
const content = info.content ?? [];
|
|
235
|
-
closeOpenTools();
|
|
236
|
-
inferenceEnd({
|
|
243
|
+
await closeOpenTools();
|
|
244
|
+
await inferenceEnd({
|
|
237
245
|
finishReason: info.finishReason,
|
|
238
246
|
usage: info.usage,
|
|
239
247
|
content,
|
|
240
248
|
});
|
|
241
|
-
dispatcher.onStepEnd?.(
|
|
249
|
+
await dispatcher.onStepEnd?.(
|
|
242
250
|
cast<'onStepEnd'>({
|
|
243
251
|
callId,
|
|
244
252
|
stepNumber,
|
|
@@ -258,10 +266,11 @@ export function createTurnTelemetry(opts: {
|
|
|
258
266
|
stepNumber += 1;
|
|
259
267
|
},
|
|
260
268
|
|
|
261
|
-
toolStart(call) {
|
|
262
|
-
ensureStepOpen();
|
|
269
|
+
async toolStart(call) {
|
|
270
|
+
await ensureStepOpen();
|
|
271
|
+
if (openTools.has(call.toolCallId)) return;
|
|
263
272
|
openTools.set(call.toolCallId, call);
|
|
264
|
-
dispatcher.onToolExecutionStart?.(
|
|
273
|
+
await dispatcher.onToolExecutionStart?.(
|
|
265
274
|
cast<'onToolExecutionStart'>({
|
|
266
275
|
callId,
|
|
267
276
|
messages: [],
|
|
@@ -277,11 +286,16 @@ export function createTurnTelemetry(opts: {
|
|
|
277
286
|
);
|
|
278
287
|
},
|
|
279
288
|
|
|
280
|
-
|
|
289
|
+
async executeTool({ toolCallId, execute }) {
|
|
290
|
+
if (dispatcher.executeTool == null) return await execute();
|
|
291
|
+
return await dispatcher.executeTool({ callId, toolCallId, execute });
|
|
292
|
+
},
|
|
293
|
+
|
|
294
|
+
async toolEnd(toolCallId, output) {
|
|
281
295
|
const call = openTools.get(toolCallId);
|
|
282
296
|
if (call == null) return;
|
|
283
297
|
openTools.delete(toolCallId);
|
|
284
|
-
dispatcher.onToolExecutionEnd?.(
|
|
298
|
+
await dispatcher.onToolExecutionEnd?.(
|
|
285
299
|
cast<'onToolExecutionEnd'>({
|
|
286
300
|
callId,
|
|
287
301
|
toolExecutionMs: 0,
|
|
@@ -301,17 +315,17 @@ export function createTurnTelemetry(opts: {
|
|
|
301
315
|
);
|
|
302
316
|
},
|
|
303
317
|
|
|
304
|
-
end(info) {
|
|
318
|
+
async end(info) {
|
|
305
319
|
if (ended) return;
|
|
306
|
-
if (!started) fireStart();
|
|
320
|
+
if (!started) await fireStart();
|
|
307
321
|
if (stepOpen) {
|
|
308
|
-
closeOpenTools();
|
|
309
|
-
inferenceEnd({
|
|
322
|
+
await closeOpenTools();
|
|
323
|
+
await inferenceEnd({
|
|
310
324
|
finishReason: info.finishReason,
|
|
311
325
|
usage: info.usage,
|
|
312
326
|
content: [],
|
|
313
327
|
});
|
|
314
|
-
dispatcher.onStepEnd?.(
|
|
328
|
+
await dispatcher.onStepEnd?.(
|
|
315
329
|
cast<'onStepEnd'>({
|
|
316
330
|
callId,
|
|
317
331
|
stepNumber,
|
|
@@ -330,7 +344,7 @@ export function createTurnTelemetry(opts: {
|
|
|
330
344
|
stepOpen = false;
|
|
331
345
|
}
|
|
332
346
|
ended = true;
|
|
333
|
-
dispatcher.onEnd?.(
|
|
347
|
+
await dispatcher.onEnd?.(
|
|
334
348
|
cast<'onEnd'>({
|
|
335
349
|
callId,
|
|
336
350
|
operationId: 'ai.harness',
|
|
@@ -350,12 +364,12 @@ export function createTurnTelemetry(opts: {
|
|
|
350
364
|
);
|
|
351
365
|
},
|
|
352
366
|
|
|
353
|
-
error(err) {
|
|
367
|
+
async error(err) {
|
|
354
368
|
if (ended) return;
|
|
355
|
-
if (!started) fireStart();
|
|
356
|
-
closeOpenTools();
|
|
369
|
+
if (!started) await fireStart();
|
|
370
|
+
await closeOpenTools();
|
|
357
371
|
ended = true;
|
|
358
|
-
dispatcher.onError?.(err);
|
|
372
|
+
await dispatcher.onError?.(err);
|
|
359
373
|
},
|
|
360
374
|
};
|
|
361
375
|
}
|
package/src/bridge/index.ts
CHANGED
|
@@ -234,6 +234,7 @@ export async function runBridge<TStart extends { type: 'start' }>(
|
|
|
234
234
|
let currentBoundPort = 0;
|
|
235
235
|
let currentTurnState: BridgeState = 'init';
|
|
236
236
|
let activeSocket: WebSocket | undefined;
|
|
237
|
+
let activeSocketReadyForLiveEvents = false;
|
|
237
238
|
let isFirstTurn = true;
|
|
238
239
|
let turnAbort: AbortController | undefined;
|
|
239
240
|
let currentUserMessages: string[] | undefined;
|
|
@@ -389,7 +390,10 @@ export async function runBridge<TStart extends { type: 'start' }>(
|
|
|
389
390
|
eventLog.push({ seq, line });
|
|
390
391
|
diskBuffer += `${line}\n`;
|
|
391
392
|
scheduleEventFlush();
|
|
392
|
-
if (
|
|
393
|
+
if (
|
|
394
|
+
activeSocketReadyForLiveEvents &&
|
|
395
|
+
activeSocket?.readyState === WS_OPEN
|
|
396
|
+
) {
|
|
393
397
|
try {
|
|
394
398
|
activeSocket.send(line);
|
|
395
399
|
} catch {
|
|
@@ -522,6 +526,8 @@ export async function runBridge<TStart extends { type: 'start' }>(
|
|
|
522
526
|
): Promise<void> => {
|
|
523
527
|
switch (msg.type) {
|
|
524
528
|
case 'start': {
|
|
529
|
+
if (activeSocket !== ws) return;
|
|
530
|
+
activeSocketReadyForLiveEvents = true;
|
|
525
531
|
const firstTurn = isFirstTurn;
|
|
526
532
|
isFirstTurn = false;
|
|
527
533
|
eventLog = []; // clear previous turn; keep seqCounter monotonic
|
|
@@ -643,7 +649,9 @@ export async function runBridge<TStart extends { type: 'start' }>(
|
|
|
643
649
|
}
|
|
644
650
|
return;
|
|
645
651
|
case 'resume':
|
|
652
|
+
if (activeSocket !== ws) return;
|
|
646
653
|
replay(ws, msg.lastSeenEventId);
|
|
654
|
+
activeSocketReadyForLiveEvents = true;
|
|
647
655
|
return;
|
|
648
656
|
case 'shutdown':
|
|
649
657
|
currentTurnState = 'done';
|
|
@@ -721,6 +729,7 @@ export async function runBridge<TStart extends { type: 'start' }>(
|
|
|
721
729
|
// (the host reconnecting after a drop). The previous socket's close is a
|
|
722
730
|
// no-op below because it is no longer `activeSocket`.
|
|
723
731
|
activeSocket = ws;
|
|
732
|
+
activeSocketReadyForLiveEvents = false;
|
|
724
733
|
|
|
725
734
|
// Announce liveness the instant we accept. Some sandbox runtimes complete
|
|
726
735
|
// the host-side WS handshake before the connection is forwarded here; the
|
|
@@ -754,6 +763,7 @@ export async function runBridge<TStart extends { type: 'start' }>(
|
|
|
754
763
|
// log for replay when the host reconnects.
|
|
755
764
|
if (activeSocket === ws) {
|
|
756
765
|
activeSocket = undefined;
|
|
766
|
+
activeSocketReadyForLiveEvents = false;
|
|
757
767
|
}
|
|
758
768
|
});
|
|
759
769
|
|
|
@@ -75,6 +75,30 @@ type Listener<TOut extends { type: string }, T extends EventTypeOf<TOut>> = (
|
|
|
75
75
|
event: Extract<TOut, { type: T }>,
|
|
76
76
|
) => void;
|
|
77
77
|
|
|
78
|
+
/*
|
|
79
|
+
* The agent and utilities entrypoints bundle this module separately. A global
|
|
80
|
+
* symbol lets the agent recognize metadata attached by the channel's bundle
|
|
81
|
+
* copy, while the non-enumerable property leaves protocol payloads unchanged.
|
|
82
|
+
*/
|
|
83
|
+
const sandboxChannelEventCheckpointSymbol = Symbol.for(
|
|
84
|
+
'vercel.ai.harness.sandboxChannelEventCheckpoint',
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
type SandboxChannelEventCheckpoint = {
|
|
88
|
+
pin: () => () => void;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export function pinSandboxChannelEventCheckpoint(
|
|
92
|
+
event: unknown,
|
|
93
|
+
): (() => void) | undefined {
|
|
94
|
+
if (event == null || typeof event !== 'object') return undefined;
|
|
95
|
+
return (
|
|
96
|
+
event as {
|
|
97
|
+
[sandboxChannelEventCheckpointSymbol]?: SandboxChannelEventCheckpoint;
|
|
98
|
+
}
|
|
99
|
+
)[sandboxChannelEventCheckpointSymbol]?.pin();
|
|
100
|
+
}
|
|
101
|
+
|
|
78
102
|
const sleep = (ms: number): Promise<void> =>
|
|
79
103
|
new Promise(resolve => {
|
|
80
104
|
const t = setTimeout(resolve, ms);
|
|
@@ -136,6 +160,9 @@ export class SandboxChannel<
|
|
|
136
160
|
* replayed to the next process on `resume`.
|
|
137
161
|
*/
|
|
138
162
|
private suspended = false;
|
|
163
|
+
private pinnedSuspensionCursor:
|
|
164
|
+
| { eventId: number; token: object }
|
|
165
|
+
| undefined;
|
|
139
166
|
/** Channel is fully torn down; `send` throws and `onClose` has fired. */
|
|
140
167
|
private terminal = false;
|
|
141
168
|
private _lastSeenEventId = 0;
|
|
@@ -251,6 +278,9 @@ export class SandboxChannel<
|
|
|
251
278
|
}
|
|
252
279
|
|
|
253
280
|
interrupt(options?: { timeoutMs?: number }): Promise<void> {
|
|
281
|
+
if (this.pinnedSuspensionCursor != null) {
|
|
282
|
+
return Promise.resolve();
|
|
283
|
+
}
|
|
254
284
|
const timeoutMs = options?.timeoutMs ?? 5000;
|
|
255
285
|
return new Promise<void>((resolve, reject) => {
|
|
256
286
|
let settled = false;
|
|
@@ -313,19 +343,24 @@ export class SandboxChannel<
|
|
|
313
343
|
* aborts it) and accumulates events past the cursor for the next process to
|
|
314
344
|
* `resume`. Unlike {@link close}, the consumer's active turn is wound down
|
|
315
345
|
* cleanly — adapters distinguish a suspend from an unexpected drop via the
|
|
316
|
-
* `'suspended'` close reason and resolve `done` successfully.
|
|
346
|
+
* `'suspended'` close reason and resolve `done` successfully. When an event
|
|
347
|
+
* checkpoint is pinned, the returned cursor points to that event so any
|
|
348
|
+
* already-dispatched tail is replayed by the next process.
|
|
317
349
|
*/
|
|
318
350
|
suspend(): Promise<number> {
|
|
319
351
|
return new Promise<number>(resolve => {
|
|
352
|
+
const pinnedSuspensionCursor = this.pinnedSuspensionCursor?.eventId;
|
|
320
353
|
if (this.terminal) {
|
|
321
|
-
resolve(this._lastSeenEventId);
|
|
354
|
+
resolve(pinnedSuspensionCursor ?? this._lastSeenEventId);
|
|
322
355
|
return;
|
|
323
356
|
}
|
|
324
357
|
// Stop counting/dispatching further inbound frames immediately, and
|
|
325
358
|
// suppress reconnect so the socket close finalises.
|
|
326
359
|
this.suspended = true;
|
|
327
360
|
this.closing = true;
|
|
328
|
-
this.onClose(() =>
|
|
361
|
+
this.onClose(() =>
|
|
362
|
+
resolve(pinnedSuspensionCursor ?? this._lastSeenEventId),
|
|
363
|
+
);
|
|
329
364
|
// Queue the close behind any already-dispatched frames so everything
|
|
330
365
|
// delivered to the consumer is reflected in the final cursor.
|
|
331
366
|
this.enqueue(() => {
|
|
@@ -465,6 +500,9 @@ export class SandboxChannel<
|
|
|
465
500
|
schema: this.outboundSchema,
|
|
466
501
|
});
|
|
467
502
|
if (validated.success) {
|
|
503
|
+
if (seq !== undefined) {
|
|
504
|
+
this.attachEventCheckpoint({ event: validated.value, eventId: seq });
|
|
505
|
+
}
|
|
468
506
|
this.dispatch(validated.value);
|
|
469
507
|
} else {
|
|
470
508
|
this.dispatch({
|
|
@@ -506,6 +544,26 @@ export class SandboxChannel<
|
|
|
506
544
|
}
|
|
507
545
|
}
|
|
508
546
|
|
|
547
|
+
private attachEventCheckpoint(options: {
|
|
548
|
+
event: TOut;
|
|
549
|
+
eventId: number;
|
|
550
|
+
}): void {
|
|
551
|
+
if (!Object.isExtensible(options.event)) return;
|
|
552
|
+
Object.defineProperty(options.event, sandboxChannelEventCheckpointSymbol, {
|
|
553
|
+
value: {
|
|
554
|
+
pin: () => {
|
|
555
|
+
const token = {};
|
|
556
|
+
this.pinnedSuspensionCursor = { eventId: options.eventId, token };
|
|
557
|
+
return () => {
|
|
558
|
+
if (this.pinnedSuspensionCursor?.token === token) {
|
|
559
|
+
this.pinnedSuspensionCursor = undefined;
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
},
|
|
563
|
+
} satisfies SandboxChannelEventCheckpoint,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
|
|
509
567
|
private finalizeClose(code: number, reason: string): void {
|
|
510
568
|
if (this.terminal) return;
|
|
511
569
|
this.terminal = true;
|