@ai-sdk/harness 1.0.38 → 1.0.40
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 +17 -0
- package/dist/agent/index.d.ts +19 -4
- package/dist/agent/index.js +189 -57
- 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/harness-stream-text-result.ts +27 -0
- package/src/agent/internal/run-prompt.ts +144 -42
- package/src/agent/internal/turn-telemetry.ts +18 -4
- package/src/bridge/index.ts +11 -1
- package/src/utils/sandbox-channel.ts +61 -3
|
@@ -9,10 +9,17 @@ import type {
|
|
|
9
9
|
HarnessAgentSkill,
|
|
10
10
|
} from './harness-agent-types';
|
|
11
11
|
import type {
|
|
12
|
+
Arrayable,
|
|
13
|
+
Context,
|
|
12
14
|
Experimental_SandboxSession as SandboxSession,
|
|
13
15
|
ToolSet,
|
|
14
16
|
} from '@ai-sdk/provider-utils';
|
|
15
|
-
import type {
|
|
17
|
+
import type {
|
|
18
|
+
ActiveTools,
|
|
19
|
+
StopCondition,
|
|
20
|
+
TelemetryOptions,
|
|
21
|
+
ToolApprovalStatus,
|
|
22
|
+
} from 'ai';
|
|
16
23
|
import type { HarnessAllTools } from './harness-agent-tool-types';
|
|
17
24
|
|
|
18
25
|
export type HarnessAgentToolApprovalConfiguration = Readonly<
|
|
@@ -91,6 +98,7 @@ type HarnessAgentToolFilteringSettings<TOOLS extends ToolSet> =
|
|
|
91
98
|
export type HarnessAgentSettings<
|
|
92
99
|
THarness extends HarnessAgentAdapter<any> = HarnessAgentAdapter,
|
|
93
100
|
TUserTools extends ToolSet = {},
|
|
101
|
+
RUNTIME_CONTEXT extends Context = Context,
|
|
94
102
|
> = {
|
|
95
103
|
/**
|
|
96
104
|
* The harness adapter driving the underlying agent runtime. Its
|
|
@@ -130,6 +138,22 @@ export type HarnessAgentSettings<
|
|
|
130
138
|
*/
|
|
131
139
|
readonly instructions?: string;
|
|
132
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Conditions that stop the current result after a completed harness tool
|
|
143
|
+
* step that can continue into another model step. The underlying turn remains
|
|
144
|
+
* unfinished and can be suspended and continued.
|
|
145
|
+
*
|
|
146
|
+
* A terminal text-only step finishes naturally and is not stopped early.
|
|
147
|
+
*
|
|
148
|
+
* When omitted, the harness runs until the turn naturally finishes or pauses.
|
|
149
|
+
*/
|
|
150
|
+
readonly stopWhen?: Arrayable<
|
|
151
|
+
StopCondition<
|
|
152
|
+
NoInfer<HarnessAllTools<THarness, TUserTools>>,
|
|
153
|
+
RUNTIME_CONTEXT
|
|
154
|
+
>
|
|
155
|
+
>;
|
|
156
|
+
|
|
133
157
|
/**
|
|
134
158
|
* Built-in tool permission mode. Defaults to `'allow-all'`, preserving the
|
|
135
159
|
* existing bypass-permissions behavior unless users opt in.
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
HarnessV1SandboxProvider,
|
|
7
7
|
} from '../v1';
|
|
8
8
|
import {
|
|
9
|
+
asArray,
|
|
9
10
|
asSchema,
|
|
10
11
|
generateId,
|
|
11
12
|
type Context,
|
|
@@ -19,6 +20,7 @@ import type {
|
|
|
19
20
|
GenerateTextResult,
|
|
20
21
|
ReasoningFileOutput,
|
|
21
22
|
ReasoningOutput,
|
|
23
|
+
StopCondition,
|
|
22
24
|
StreamTextResult,
|
|
23
25
|
} from 'ai';
|
|
24
26
|
import type {
|
|
@@ -133,7 +135,14 @@ export class HarnessAgent<
|
|
|
133
135
|
*/
|
|
134
136
|
readonly tools: HarnessAllTools<THarness, TUserTools>;
|
|
135
137
|
|
|
136
|
-
private readonly settings: HarnessAgentSettings<
|
|
138
|
+
private readonly settings: HarnessAgentSettings<
|
|
139
|
+
THarness,
|
|
140
|
+
TUserTools,
|
|
141
|
+
RUNTIME_CONTEXT
|
|
142
|
+
>;
|
|
143
|
+
private readonly stopConditions: Array<
|
|
144
|
+
StopCondition<HarnessAllTools<THarness, TUserTools>, RUNTIME_CONTEXT>
|
|
145
|
+
>;
|
|
137
146
|
private readonly sandboxConfig: HarnessAgentSandboxConfig;
|
|
138
147
|
private readonly activeUserTools: TUserTools;
|
|
139
148
|
private readonly builtinToolFiltering:
|
|
@@ -141,10 +150,14 @@ export class HarnessAgent<
|
|
|
141
150
|
| undefined;
|
|
142
151
|
private readonly permissionMode: HarnessAgentPermissionMode;
|
|
143
152
|
|
|
144
|
-
constructor(
|
|
153
|
+
constructor(
|
|
154
|
+
settings: HarnessAgentSettings<THarness, TUserTools, RUNTIME_CONTEXT>,
|
|
155
|
+
) {
|
|
145
156
|
const sandboxConfig = resolveSandboxConfig(settings);
|
|
146
157
|
validateSandboxBootstrapSettings(sandboxConfig);
|
|
147
158
|
this.settings = settings;
|
|
159
|
+
this.stopConditions =
|
|
160
|
+
settings.stopWhen == null ? [] : asArray(settings.stopWhen);
|
|
148
161
|
this.sandboxConfig = sandboxConfig;
|
|
149
162
|
this.id = settings.id;
|
|
150
163
|
const userTools = settings.tools ?? ({} as TUserTools);
|
|
@@ -528,6 +541,7 @@ export class HarnessAgent<
|
|
|
528
541
|
runtimeContext: input.runtimeContext,
|
|
529
542
|
abortSignal: input.abortSignal,
|
|
530
543
|
telemetry: this.settings.telemetry,
|
|
544
|
+
stopConditions: this.stopConditions,
|
|
531
545
|
toolApprovalContinuations: input.turnInput.toolApprovalContinuations,
|
|
532
546
|
toolResultContinuations: input.turnInput.toolResultContinuations,
|
|
533
547
|
});
|
|
@@ -546,6 +560,7 @@ export class HarnessAgent<
|
|
|
546
560
|
runtimeContext: input.runtimeContext,
|
|
547
561
|
abortSignal: input.abortSignal,
|
|
548
562
|
telemetry: this.settings.telemetry,
|
|
563
|
+
stopConditions: this.stopConditions,
|
|
549
564
|
});
|
|
550
565
|
}
|
|
551
566
|
|
|
@@ -460,6 +460,25 @@ export class HarnessStreamTextResult<
|
|
|
460
460
|
this.fullStreamController.close();
|
|
461
461
|
}
|
|
462
462
|
|
|
463
|
+
/**
|
|
464
|
+
* Settle the turn as user-aborted: emit a final `abort` part — matching
|
|
465
|
+
* `streamText`'s abort contract — and close the stream, instead of
|
|
466
|
+
* surfacing an `error` part. `toUIMessageStream` consumers then observe
|
|
467
|
+
* an `abort` chunk (and `isAborted: true`) rather than a spurious
|
|
468
|
+
* `onError`. The delayed promise accessors still reject with the
|
|
469
|
+
* underlying abort error so awaiting consumers do not hang. Idempotent.
|
|
470
|
+
*/
|
|
471
|
+
abort(input: { error: unknown; reason?: string }): void {
|
|
472
|
+
if (this.settled) return;
|
|
473
|
+
this.settled = true;
|
|
474
|
+
this.fullStreamController.enqueue({
|
|
475
|
+
type: 'abort',
|
|
476
|
+
...(input.reason !== undefined ? { reason: input.reason } : {}),
|
|
477
|
+
} as TextStreamPart<TOOLS>);
|
|
478
|
+
this.fullStreamController.close();
|
|
479
|
+
this.rejectDelayedPromises(input.error);
|
|
480
|
+
}
|
|
481
|
+
|
|
463
482
|
/**
|
|
464
483
|
* Surface a fatal error as a stream `error` part + reject every delayed
|
|
465
484
|
* promise so awaiting consumers stop hanging. Idempotent.
|
|
@@ -472,6 +491,10 @@ export class HarnessStreamTextResult<
|
|
|
472
491
|
error,
|
|
473
492
|
} as TextStreamPart<TOOLS>);
|
|
474
493
|
this.fullStreamController.close();
|
|
494
|
+
this.rejectDelayedPromises(error);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
private rejectDelayedPromises(error: unknown): void {
|
|
475
498
|
for (const dp of [
|
|
476
499
|
this._content,
|
|
477
500
|
this._text,
|
|
@@ -605,6 +628,7 @@ export class HarnessStreamTextResult<
|
|
|
605
628
|
toUIMessageStream<UI_MESSAGE extends UIMessage>({
|
|
606
629
|
originalMessages,
|
|
607
630
|
generateMessageId,
|
|
631
|
+
onEnd,
|
|
608
632
|
onFinish,
|
|
609
633
|
messageMetadata,
|
|
610
634
|
sendReasoning,
|
|
@@ -621,6 +645,7 @@ export class HarnessStreamTextResult<
|
|
|
621
645
|
tools: this.tools,
|
|
622
646
|
originalMessages,
|
|
623
647
|
generateMessageId,
|
|
648
|
+
onEnd,
|
|
624
649
|
onFinish,
|
|
625
650
|
messageMetadata,
|
|
626
651
|
sendReasoning,
|
|
@@ -643,6 +668,7 @@ export class HarnessStreamTextResult<
|
|
|
643
668
|
toUIMessageStreamResponse<UI_MESSAGE extends UIMessage>({
|
|
644
669
|
originalMessages,
|
|
645
670
|
generateMessageId,
|
|
671
|
+
onEnd,
|
|
646
672
|
onFinish,
|
|
647
673
|
messageMetadata,
|
|
648
674
|
sendReasoning,
|
|
@@ -660,6 +686,7 @@ export class HarnessStreamTextResult<
|
|
|
660
686
|
stream: this.toUIMessageStream<UI_MESSAGE>({
|
|
661
687
|
originalMessages,
|
|
662
688
|
generateMessageId,
|
|
689
|
+
onEnd,
|
|
663
690
|
onFinish,
|
|
664
691
|
messageMetadata,
|
|
665
692
|
sendReasoning,
|
|
@@ -21,16 +21,18 @@ import {
|
|
|
21
21
|
type Experimental_SandboxSession as SandboxSession,
|
|
22
22
|
type ToolSet,
|
|
23
23
|
} from '@ai-sdk/provider-utils';
|
|
24
|
-
import
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
import {
|
|
25
|
+
getErrorMessage,
|
|
26
|
+
type LanguageModelV4FinishReason,
|
|
27
|
+
type LanguageModelV4ToolCall,
|
|
28
|
+
type LanguageModelV4Usage,
|
|
28
29
|
} from '@ai-sdk/provider';
|
|
29
30
|
import { parseToolCall } from 'ai/internal';
|
|
30
31
|
import type {
|
|
31
32
|
ContentPart,
|
|
32
33
|
ProviderMetadata,
|
|
33
34
|
StepResult,
|
|
35
|
+
StopCondition,
|
|
34
36
|
TelemetryOptions,
|
|
35
37
|
TextStreamPart,
|
|
36
38
|
} from 'ai';
|
|
@@ -40,9 +42,14 @@ import type { HarnessAgentToolApprovalConfiguration } from '../harness-agent-set
|
|
|
40
42
|
import { HarnessStreamTextResult } from './harness-stream-text-result';
|
|
41
43
|
import { translateStreamPart } from './translate-stream-part';
|
|
42
44
|
import { stripWorkDir } from './strip-work-dir';
|
|
43
|
-
import {
|
|
45
|
+
import {
|
|
46
|
+
createTurnTelemetry,
|
|
47
|
+
type TurnContentPart,
|
|
48
|
+
type TurnTelemetry,
|
|
49
|
+
} from './turn-telemetry';
|
|
44
50
|
import { resolveCustomToolApproval } from './permission-mode';
|
|
45
51
|
import { logBridgeError } from '../../utils/bridge-diagnostics';
|
|
52
|
+
import { pinSandboxChannelEventCheckpoint } from '../../utils/sandbox-channel';
|
|
46
53
|
|
|
47
54
|
/**
|
|
48
55
|
* Drive one prompt turn end-to-end:
|
|
@@ -80,6 +87,7 @@ export function runPrompt<
|
|
|
80
87
|
runtimeContext: RUNTIME_CONTEXT;
|
|
81
88
|
abortSignal: AbortSignal | undefined;
|
|
82
89
|
telemetry?: TelemetryOptions | undefined;
|
|
90
|
+
stopConditions?: ReadonlyArray<StopCondition<TOOLS, RUNTIME_CONTEXT>>;
|
|
83
91
|
toolApproval?: HarnessAgentToolApprovalConfiguration | undefined;
|
|
84
92
|
pendingToolApprovals?: readonly HarnessV1PendingToolApproval[];
|
|
85
93
|
pendingToolResults?: readonly HarnessV1PendingToolResult[];
|
|
@@ -95,6 +103,7 @@ export function runPrompt<
|
|
|
95
103
|
onToolResultSettled?: (toolCallId: string) => void;
|
|
96
104
|
onTurnFinished?: () => void;
|
|
97
105
|
onTurnFailed?: () => void;
|
|
106
|
+
onStopConditionMet?: () => Promise<void>;
|
|
98
107
|
}): {
|
|
99
108
|
result: HarnessStreamTextResult<TOOLS, RUNTIME_CONTEXT>;
|
|
100
109
|
done: Promise<void>;
|
|
@@ -124,6 +133,29 @@ export function runPrompt<
|
|
|
124
133
|
runtimeContext: input.runtimeContext,
|
|
125
134
|
});
|
|
126
135
|
|
|
136
|
+
/*
|
|
137
|
+
* Settle a failed turn. When the caller's own `abortSignal` has fired, the
|
|
138
|
+
* failure is a user-initiated stop, not an error: surface it as an `abort`
|
|
139
|
+
* stream part — matching `streamText`'s abort contract — so
|
|
140
|
+
* `toUIMessageStream` consumers observe an `abort` chunk and
|
|
141
|
+
* `isAborted: true` instead of a spurious `onError`. Every other failure
|
|
142
|
+
* stays a real `error` part. Both outcomes notify `onTurnFailed` so the
|
|
143
|
+
* session's turn tracking returns to idle and the session stays usable.
|
|
144
|
+
*/
|
|
145
|
+
const settleFailure = (err: unknown) => {
|
|
146
|
+
input.onTurnFailed?.();
|
|
147
|
+
if (input.abortSignal?.aborted) {
|
|
148
|
+
result.abort({
|
|
149
|
+
error: err,
|
|
150
|
+
...(input.abortSignal.reason !== undefined
|
|
151
|
+
? { reason: getErrorMessage(input.abortSignal.reason) }
|
|
152
|
+
: {}),
|
|
153
|
+
});
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
result.fail(err);
|
|
157
|
+
};
|
|
158
|
+
|
|
127
159
|
const done = (async () => {
|
|
128
160
|
let bridge: Awaited<ReturnType<typeof toHarnessStream>>;
|
|
129
161
|
try {
|
|
@@ -159,8 +191,7 @@ export function runPrompt<
|
|
|
159
191
|
context: 'failed to start harness turn',
|
|
160
192
|
error: err,
|
|
161
193
|
});
|
|
162
|
-
|
|
163
|
-
result.fail(err);
|
|
194
|
+
settleFailure(err);
|
|
164
195
|
return;
|
|
165
196
|
}
|
|
166
197
|
|
|
@@ -197,9 +228,21 @@ export function runPrompt<
|
|
|
197
228
|
);
|
|
198
229
|
const settledHostToolCallIds = new Set<string>();
|
|
199
230
|
let closingResumedStep = false;
|
|
231
|
+
let pendingStopBoundary:
|
|
232
|
+
| {
|
|
233
|
+
finishReason: LanguageModelV4FinishReason;
|
|
234
|
+
usage: LanguageModelV4Usage;
|
|
235
|
+
releaseCheckpoint: (() => void) | undefined;
|
|
236
|
+
}
|
|
237
|
+
| undefined;
|
|
200
238
|
let finalFinish:
|
|
201
239
|
| Extract<HarnessV1StreamPart, { type: 'finish' }>
|
|
202
240
|
| undefined;
|
|
241
|
+
const completedSteps: Array<StepResult<TOOLS, RUNTIME_CONTEXT>> = [];
|
|
242
|
+
const releasePendingStopBoundary = (): void => {
|
|
243
|
+
pendingStopBoundary?.releaseCheckpoint?.();
|
|
244
|
+
pendingStopBoundary = undefined;
|
|
245
|
+
};
|
|
203
246
|
|
|
204
247
|
// Accumulate the model's output content per step so telemetry can record
|
|
205
248
|
// `gen_ai.output.messages` and reporters can log what was actually said.
|
|
@@ -247,12 +290,14 @@ export function runPrompt<
|
|
|
247
290
|
content: buildStepContent(),
|
|
248
291
|
});
|
|
249
292
|
resetStepContent();
|
|
250
|
-
|
|
293
|
+
const step = result.finishStep({
|
|
251
294
|
finishReason: input.finishReason,
|
|
252
295
|
usage: input.usage,
|
|
253
296
|
providerMetadata: input.providerMetadata,
|
|
254
297
|
warnings: [],
|
|
255
298
|
});
|
|
299
|
+
completedSteps.push(step);
|
|
300
|
+
return step;
|
|
256
301
|
};
|
|
257
302
|
const finishForHostInputPause = async (options: {
|
|
258
303
|
completeCurrentStep: boolean;
|
|
@@ -390,9 +435,16 @@ export function runPrompt<
|
|
|
390
435
|
input: approval.input,
|
|
391
436
|
} satisfies Extract<HarnessV1StreamPart, { type: 'tool-call' }>);
|
|
392
437
|
|
|
438
|
+
telemetry.start(input.session.modelId);
|
|
439
|
+
await telemetry.toolStart({
|
|
440
|
+
toolCallId: rawToolCall.toolCallId,
|
|
441
|
+
toolName: rawToolCall.toolName,
|
|
442
|
+
input: rawToolCall.input,
|
|
443
|
+
});
|
|
393
444
|
const execution = await maybeExecuteHostTool({
|
|
394
445
|
event: rawToolCall,
|
|
395
446
|
tools: activeTools,
|
|
447
|
+
wrappedExecuteTool: telemetry.executeTool,
|
|
396
448
|
sandboxSession: input.sandboxSession,
|
|
397
449
|
abortSignal: input.abortSignal,
|
|
398
450
|
control,
|
|
@@ -455,9 +507,35 @@ export function runPrompt<
|
|
|
455
507
|
|
|
456
508
|
while (true) {
|
|
457
509
|
const { value, done } = await reader.read();
|
|
458
|
-
if (done)
|
|
510
|
+
if (done) {
|
|
511
|
+
releasePendingStopBoundary();
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
459
514
|
if (value == null) continue;
|
|
460
515
|
|
|
516
|
+
if (pendingStopBoundary != null) {
|
|
517
|
+
if (value.type === 'finish') {
|
|
518
|
+
releasePendingStopBoundary();
|
|
519
|
+
} else if (
|
|
520
|
+
(
|
|
521
|
+
await Promise.all(
|
|
522
|
+
input.stopConditions!.map(condition =>
|
|
523
|
+
condition({ steps: completedSteps }),
|
|
524
|
+
),
|
|
525
|
+
)
|
|
526
|
+
).some(Boolean)
|
|
527
|
+
) {
|
|
528
|
+
await input.onStopConditionMet?.();
|
|
529
|
+
const { finishReason, usage } = pendingStopBoundary;
|
|
530
|
+
releasePendingStopBoundary();
|
|
531
|
+
telemetry.end({ finishReason, usage });
|
|
532
|
+
await result.finish();
|
|
533
|
+
return;
|
|
534
|
+
} else {
|
|
535
|
+
releasePendingStopBoundary();
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
461
539
|
// Begin the operation span on stream-start, using the runtime-resolved
|
|
462
540
|
// model the adapter reports (falling back to the session's model).
|
|
463
541
|
if (value.type === 'stream-start') {
|
|
@@ -531,6 +609,28 @@ export function runPrompt<
|
|
|
531
609
|
}
|
|
532
610
|
}
|
|
533
611
|
|
|
612
|
+
/*
|
|
613
|
+
* Settle failures before the translate-and-forward step below: the
|
|
614
|
+
* translated `error` part must not reach the consumer stream, or the
|
|
615
|
+
* turn surfaces BOTH a forwarded `error` part and the settle-owned
|
|
616
|
+
* terminal part (an `abort` part for user stops via `settleFailure`,
|
|
617
|
+
* or a second `error` part from `fail`).
|
|
618
|
+
*/
|
|
619
|
+
if (value.type === 'error' && displayValue.type === 'error') {
|
|
620
|
+
// Telemetry and stderr diagnostics keep the raw error (absolute
|
|
621
|
+
// paths help debugging); the consumer-facing settle uses the
|
|
622
|
+
// workDir-stripped one, like every other forwarded part.
|
|
623
|
+
telemetry.error(value.error);
|
|
624
|
+
logBridgeError({
|
|
625
|
+
harnessId: input.harness.harnessId,
|
|
626
|
+
sessionId: input.session.sessionId,
|
|
627
|
+
context: 'harness stream error',
|
|
628
|
+
error: value.error,
|
|
629
|
+
});
|
|
630
|
+
settleFailure(displayValue.error);
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
|
|
534
634
|
// Forward to consumer as soon as possible.
|
|
535
635
|
for (const part of translateStreamPart<TOOLS>(displayValue)) {
|
|
536
636
|
result.enqueue(part);
|
|
@@ -564,7 +664,7 @@ export function runPrompt<
|
|
|
564
664
|
toolName: value.toolName,
|
|
565
665
|
input: value.input,
|
|
566
666
|
});
|
|
567
|
-
telemetry.toolStart({
|
|
667
|
+
await telemetry.toolStart({
|
|
568
668
|
toolCallId: value.toolCallId,
|
|
569
669
|
toolName: value.toolName,
|
|
570
670
|
input: value.input,
|
|
@@ -641,6 +741,13 @@ export function runPrompt<
|
|
|
641
741
|
usage: value.usage,
|
|
642
742
|
providerMetadata: value.harnessMetadata,
|
|
643
743
|
});
|
|
744
|
+
if (input.stopConditions != null && input.stopConditions.length > 0) {
|
|
745
|
+
pendingStopBoundary = {
|
|
746
|
+
finishReason: value.finishReason,
|
|
747
|
+
usage: value.usage,
|
|
748
|
+
releaseCheckpoint: pinSandboxChannelEventCheckpoint(value),
|
|
749
|
+
};
|
|
750
|
+
}
|
|
644
751
|
}
|
|
645
752
|
|
|
646
753
|
if (value.type === 'finish') {
|
|
@@ -758,6 +865,7 @@ export function runPrompt<
|
|
|
758
865
|
const execution = await maybeExecuteHostTool({
|
|
759
866
|
event: toolCall,
|
|
760
867
|
tools: activeTools,
|
|
868
|
+
wrappedExecuteTool: telemetry.executeTool,
|
|
761
869
|
sandboxSession: input.sandboxSession,
|
|
762
870
|
abortSignal: input.abortSignal,
|
|
763
871
|
control,
|
|
@@ -799,19 +907,6 @@ export function runPrompt<
|
|
|
799
907
|
}
|
|
800
908
|
telemetry.toolEnd(toolCall.toolCallId, execution.outcome);
|
|
801
909
|
}
|
|
802
|
-
|
|
803
|
-
if (value.type === 'error') {
|
|
804
|
-
telemetry.error(value.error);
|
|
805
|
-
logBridgeError({
|
|
806
|
-
harnessId: input.harness.harnessId,
|
|
807
|
-
sessionId: input.session.sessionId,
|
|
808
|
-
context: 'harness stream error',
|
|
809
|
-
error: value.error,
|
|
810
|
-
});
|
|
811
|
-
input.onTurnFailed?.();
|
|
812
|
-
result.fail(value.error);
|
|
813
|
-
return;
|
|
814
|
-
}
|
|
815
910
|
}
|
|
816
911
|
if (finalFinish != null) {
|
|
817
912
|
input.onTurnFinished?.();
|
|
@@ -835,9 +930,9 @@ export function runPrompt<
|
|
|
835
930
|
context: 'harness turn failed',
|
|
836
931
|
error: err,
|
|
837
932
|
});
|
|
838
|
-
|
|
839
|
-
result.fail(err);
|
|
933
|
+
settleFailure(err);
|
|
840
934
|
} finally {
|
|
935
|
+
releasePendingStopBoundary();
|
|
841
936
|
reader.releaseLock();
|
|
842
937
|
}
|
|
843
938
|
})();
|
|
@@ -890,6 +985,7 @@ function hasTool(input: { tools: ToolSet; toolName: string }): boolean {
|
|
|
890
985
|
async function maybeExecuteHostTool<TOOLS extends ToolSet>(input: {
|
|
891
986
|
event: { toolCallId: string; toolName: string; input: string };
|
|
892
987
|
tools: TOOLS;
|
|
988
|
+
wrappedExecuteTool: TurnTelemetry['executeTool'];
|
|
893
989
|
sandboxSession: SandboxSession;
|
|
894
990
|
abortSignal: AbortSignal | undefined;
|
|
895
991
|
control: HarnessV1PromptControl;
|
|
@@ -918,25 +1014,31 @@ async function maybeExecuteHostTool<TOOLS extends ToolSet>(input: {
|
|
|
918
1014
|
* back to the model — preliminary values are surfaced to the consumer
|
|
919
1015
|
* stream alone, matching how the AI SDK treats `onPreliminaryToolResult`.
|
|
920
1016
|
*/
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1017
|
+
const output = await input.wrappedExecuteTool({
|
|
1018
|
+
toolCallId: input.event.toolCallId,
|
|
1019
|
+
execute: async () => {
|
|
1020
|
+
let output: unknown;
|
|
1021
|
+
const stream = executeTool({
|
|
1022
|
+
tool,
|
|
1023
|
+
input: args as never,
|
|
1024
|
+
options: {
|
|
1025
|
+
toolCallId: input.event.toolCallId,
|
|
1026
|
+
messages: [],
|
|
1027
|
+
abortSignal: input.abortSignal,
|
|
1028
|
+
context: undefined as never,
|
|
1029
|
+
experimental_sandbox: input.sandboxSession,
|
|
1030
|
+
},
|
|
1031
|
+
});
|
|
1032
|
+
for await (const part of stream) {
|
|
1033
|
+
if (part.type === 'preliminary') {
|
|
1034
|
+
input.onPreliminaryResult(part.output);
|
|
1035
|
+
} else {
|
|
1036
|
+
output = part.output;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
return output;
|
|
931
1040
|
},
|
|
932
1041
|
});
|
|
933
|
-
for await (const part of stream) {
|
|
934
|
-
if (part.type === 'preliminary') {
|
|
935
|
-
input.onPreliminaryResult(part.output);
|
|
936
|
-
} else {
|
|
937
|
-
output = part.output;
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
1042
|
|
|
941
1043
|
await input.control.submitToolResult({
|
|
942
1044
|
toolCallId: input.event.toolCallId,
|
|
@@ -58,7 +58,12 @@ export interface TurnTelemetry {
|
|
|
58
58
|
toolCallId: string;
|
|
59
59
|
toolName: string;
|
|
60
60
|
input: unknown;
|
|
61
|
-
}): void
|
|
61
|
+
}): void | 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
|
|
@@ -78,7 +83,10 @@ const NOOP: TurnTelemetry = {
|
|
|
78
83
|
start() {},
|
|
79
84
|
ensureStepOpen() {},
|
|
80
85
|
stepFinish() {},
|
|
81
|
-
toolStart() {},
|
|
86
|
+
async toolStart() {},
|
|
87
|
+
async executeTool({ execute }) {
|
|
88
|
+
return await execute();
|
|
89
|
+
},
|
|
82
90
|
toolEnd() {},
|
|
83
91
|
end() {},
|
|
84
92
|
error() {},
|
|
@@ -258,10 +266,11 @@ export function createTurnTelemetry(opts: {
|
|
|
258
266
|
stepNumber += 1;
|
|
259
267
|
},
|
|
260
268
|
|
|
261
|
-
toolStart(call) {
|
|
269
|
+
async toolStart(call) {
|
|
262
270
|
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,6 +286,11 @@ export function createTurnTelemetry(opts: {
|
|
|
277
286
|
);
|
|
278
287
|
},
|
|
279
288
|
|
|
289
|
+
async executeTool({ toolCallId, execute }) {
|
|
290
|
+
if (dispatcher.executeTool == null) return await execute();
|
|
291
|
+
return await dispatcher.executeTool({ callId, toolCallId, execute });
|
|
292
|
+
},
|
|
293
|
+
|
|
280
294
|
toolEnd(toolCallId, output) {
|
|
281
295
|
const call = openTools.get(toolCallId);
|
|
282
296
|
if (call == null) return;
|
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
|
|