@ai-sdk/harness 1.0.41 → 1.0.43
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/dist/agent/index.d.ts +4 -4
- package/dist/agent/index.js +105 -22
- package/dist/agent/index.js.map +1 -1
- package/dist/bridge/index.d.ts +0 -6
- package/dist/bridge/index.js +0 -24
- package/dist/bridge/index.js.map +1 -1
- package/dist/index.d.ts +4 -10
- package/dist/index.js +0 -10
- package/dist/index.js.map +1 -1
- package/dist/utils/index.d.ts +0 -3
- package/dist/utils/index.js +0 -69
- package/dist/utils/index.js.map +1 -1
- package/package.json +4 -2
- package/src/agent/harness-agent-session.ts +4 -0
- package/src/agent/internal/run-prompt.ts +22 -3
- package/src/agent/internal/turn-telemetry.ts +126 -17
- package/src/bridge/index.ts +1 -43
- package/src/utils/sandbox-channel.ts +0 -66
- package/src/v1/harness-v1-bridge-protocol.ts +0 -17
- package/src/v1/harness-v1-lifecycle-state.ts +1 -1
- package/src/v1/harness-v1-session.ts +2 -2
- package/src/v1/harness-v1.ts +1 -1
|
@@ -103,6 +103,11 @@ export function runPrompt<
|
|
|
103
103
|
onToolResultSettled?: (toolCallId: string) => void;
|
|
104
104
|
onTurnFinished?: () => void;
|
|
105
105
|
onTurnFailed?: () => void;
|
|
106
|
+
/**
|
|
107
|
+
* Reports that the adapter stream closed because the host intentionally
|
|
108
|
+
* suspended the still-running turn at a workflow slice boundary.
|
|
109
|
+
*/
|
|
110
|
+
isTurnSuspending?: () => boolean;
|
|
106
111
|
onStopConditionMet?: () => Promise<void>;
|
|
107
112
|
}): {
|
|
108
113
|
result: HarnessStreamTextResult<TOOLS, RUNTIME_CONTEXT>;
|
|
@@ -140,10 +145,13 @@ export function runPrompt<
|
|
|
140
145
|
* `toUIMessageStream` consumers observe an `abort` chunk and
|
|
141
146
|
* `isAborted: true` instead of a spurious `onError`. Every other failure
|
|
142
147
|
* stays a real `error` part. Both outcomes notify `onTurnFailed` so the
|
|
143
|
-
* session's turn tracking returns to idle and the session stays usable
|
|
148
|
+
* session's turn tracking returns to idle and the session stays usable,
|
|
149
|
+
* unless the turn is being suspended for a future continuation.
|
|
144
150
|
*/
|
|
145
151
|
const settleFailure = (err: unknown) => {
|
|
146
|
-
input.
|
|
152
|
+
if (!input.isTurnSuspending?.()) {
|
|
153
|
+
input.onTurnFailed?.();
|
|
154
|
+
}
|
|
147
155
|
if (input.abortSignal?.aborted) {
|
|
148
156
|
result.abort({
|
|
149
157
|
error: err,
|
|
@@ -908,7 +916,18 @@ export function runPrompt<
|
|
|
908
916
|
await telemetry.toolEnd(toolCall.toolCallId, execution.outcome);
|
|
909
917
|
}
|
|
910
918
|
}
|
|
911
|
-
|
|
919
|
+
const isTurnSuspending = input.isTurnSuspending?.() === true;
|
|
920
|
+
if (isTurnSuspending) {
|
|
921
|
+
if (finalFinish == null) {
|
|
922
|
+
/*
|
|
923
|
+
* A timed slice may stop in the middle of a model step. Its partial
|
|
924
|
+
* content remains in the bridge replay log for the next slice, but it
|
|
925
|
+
* cannot form a valid StepResult in this slice because no finish-step
|
|
926
|
+
* has arrived yet.
|
|
927
|
+
*/
|
|
928
|
+
result.discardCurrentStepContent();
|
|
929
|
+
}
|
|
930
|
+
} else if (finalFinish != null) {
|
|
912
931
|
input.onTurnFinished?.();
|
|
913
932
|
} else {
|
|
914
933
|
input.onTurnFailed?.();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { generateId, type ModelMessage } from '@ai-sdk/provider-utils';
|
|
2
2
|
import { createTelemetryDispatcher } from 'ai/internal';
|
|
3
|
-
import type { TelemetryOptions } from 'ai';
|
|
3
|
+
import type { LanguageModelUsage, TelemetryOptions } from 'ai';
|
|
4
4
|
|
|
5
5
|
/*
|
|
6
6
|
* Drives AI SDK's pluggable `Telemetry` lifecycle from a harness turn.
|
|
@@ -92,6 +92,69 @@ const NOOP: TurnTelemetry = {
|
|
|
92
92
|
async error() {},
|
|
93
93
|
};
|
|
94
94
|
|
|
95
|
+
function normalizeFinishReason(finishReason: unknown): unknown {
|
|
96
|
+
if (
|
|
97
|
+
finishReason != null &&
|
|
98
|
+
typeof finishReason === 'object' &&
|
|
99
|
+
'unified' in finishReason
|
|
100
|
+
) {
|
|
101
|
+
return (finishReason as { unified: unknown }).unified;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return finishReason;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function addTokenCounts(
|
|
108
|
+
tokenCount1: number | undefined,
|
|
109
|
+
tokenCount2: number | undefined,
|
|
110
|
+
): number | undefined {
|
|
111
|
+
return tokenCount1 == null && tokenCount2 == null
|
|
112
|
+
? undefined
|
|
113
|
+
: (tokenCount1 ?? 0) + (tokenCount2 ?? 0);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function normalizeUsage(usage: unknown): LanguageModelUsage | unknown {
|
|
117
|
+
if (
|
|
118
|
+
usage == null ||
|
|
119
|
+
typeof usage !== 'object' ||
|
|
120
|
+
!('inputTokens' in usage) ||
|
|
121
|
+
!('outputTokens' in usage)
|
|
122
|
+
) {
|
|
123
|
+
return usage;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const inputTokens = (usage as { inputTokens: unknown }).inputTokens;
|
|
127
|
+
const outputTokens = (usage as { outputTokens: unknown }).outputTokens;
|
|
128
|
+
|
|
129
|
+
if (
|
|
130
|
+
inputTokens == null ||
|
|
131
|
+
typeof inputTokens !== 'object' ||
|
|
132
|
+
outputTokens == null ||
|
|
133
|
+
typeof outputTokens !== 'object'
|
|
134
|
+
) {
|
|
135
|
+
return usage;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const input = inputTokens as Record<string, number | undefined>;
|
|
139
|
+
const output = outputTokens as Record<string, number | undefined>;
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
inputTokens: input.total,
|
|
143
|
+
inputTokenDetails: {
|
|
144
|
+
noCacheTokens: input.noCache,
|
|
145
|
+
cacheReadTokens: input.cacheRead,
|
|
146
|
+
cacheWriteTokens: input.cacheWrite,
|
|
147
|
+
},
|
|
148
|
+
outputTokens: output.total,
|
|
149
|
+
outputTokenDetails: {
|
|
150
|
+
textTokens: output.text,
|
|
151
|
+
reasoningTokens: output.reasoning,
|
|
152
|
+
},
|
|
153
|
+
totalTokens: addTokenCounts(input.total, output.total),
|
|
154
|
+
raw: (usage as { raw?: LanguageModelUsage['raw'] }).raw,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
95
158
|
export function createTurnTelemetry(opts: {
|
|
96
159
|
telemetry: TelemetryOptions | undefined;
|
|
97
160
|
harnessId: string;
|
|
@@ -119,6 +182,15 @@ export function createTurnTelemetry(opts: {
|
|
|
119
182
|
let stepOpen = false;
|
|
120
183
|
let stepNumber = 0;
|
|
121
184
|
let ended = false;
|
|
185
|
+
let finalStepText = '';
|
|
186
|
+
let finalStepReasoning: Array<{ text: string }> = [];
|
|
187
|
+
let finalStepProviderMetadata: unknown;
|
|
188
|
+
let outputToolCalls: Array<{
|
|
189
|
+
type: 'tool-call';
|
|
190
|
+
toolCallId: string;
|
|
191
|
+
toolName: string;
|
|
192
|
+
input: unknown;
|
|
193
|
+
}> = [];
|
|
122
194
|
/** Tool calls started in the current turn and not yet ended. */
|
|
123
195
|
const openTools = new Map<
|
|
124
196
|
string,
|
|
@@ -200,17 +272,40 @@ export function createTurnTelemetry(opts: {
|
|
|
200
272
|
usage: unknown;
|
|
201
273
|
content: TurnContentPart[];
|
|
202
274
|
}): Promise<void> => {
|
|
275
|
+
const finishReason = normalizeFinishReason(info.finishReason);
|
|
276
|
+
const usage = normalizeUsage(info.usage);
|
|
277
|
+
|
|
203
278
|
await dispatcher.onLanguageModelCallEnd?.(
|
|
204
279
|
cast<'onLanguageModelCallEnd'>({
|
|
205
280
|
callId,
|
|
206
|
-
finishReason
|
|
281
|
+
finishReason,
|
|
207
282
|
responseId: callId,
|
|
208
|
-
usage
|
|
283
|
+
usage,
|
|
209
284
|
content: info.content,
|
|
285
|
+
performance: {
|
|
286
|
+
responseTimeMs: undefined,
|
|
287
|
+
timeToFirstOutputMs: undefined,
|
|
288
|
+
timeBetweenOutputChunksMs: undefined,
|
|
289
|
+
},
|
|
210
290
|
}),
|
|
211
291
|
);
|
|
212
292
|
};
|
|
213
293
|
|
|
294
|
+
const recordOutputContent = (content: TurnContentPart[]): void => {
|
|
295
|
+
finalStepText = '';
|
|
296
|
+
finalStepReasoning = [];
|
|
297
|
+
|
|
298
|
+
for (const part of content) {
|
|
299
|
+
if (part.type === 'text') {
|
|
300
|
+
finalStepText += part.text;
|
|
301
|
+
} else if (part.type === 'reasoning') {
|
|
302
|
+
finalStepReasoning.push({ text: part.text });
|
|
303
|
+
} else if (part.type === 'tool-call') {
|
|
304
|
+
outputToolCalls.push(part);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
|
|
214
309
|
const closeOpenTools = async (): Promise<void> => {
|
|
215
310
|
for (const call of openTools.values()) {
|
|
216
311
|
await dispatcher.onToolExecutionEnd?.(
|
|
@@ -240,18 +335,22 @@ export function createTurnTelemetry(opts: {
|
|
|
240
335
|
async stepFinish(info) {
|
|
241
336
|
if (!stepOpen) return;
|
|
242
337
|
const content = info.content ?? [];
|
|
338
|
+
const finishReason = normalizeFinishReason(info.finishReason);
|
|
339
|
+
const usage = normalizeUsage(info.usage);
|
|
340
|
+
recordOutputContent(content);
|
|
341
|
+
finalStepProviderMetadata = info.providerMetadata;
|
|
243
342
|
await closeOpenTools();
|
|
244
343
|
await inferenceEnd({
|
|
245
|
-
finishReason
|
|
246
|
-
usage
|
|
344
|
+
finishReason,
|
|
345
|
+
usage,
|
|
247
346
|
content,
|
|
248
347
|
});
|
|
249
348
|
await dispatcher.onStepEnd?.(
|
|
250
349
|
cast<'onStepEnd'>({
|
|
251
350
|
callId,
|
|
252
351
|
stepNumber,
|
|
253
|
-
finishReason
|
|
254
|
-
usage
|
|
352
|
+
finishReason,
|
|
353
|
+
usage,
|
|
255
354
|
providerMetadata: info.providerMetadata,
|
|
256
355
|
content,
|
|
257
356
|
response: {
|
|
@@ -293,6 +392,9 @@ export function createTurnTelemetry(opts: {
|
|
|
293
392
|
|
|
294
393
|
async toolEnd(toolCallId, output) {
|
|
295
394
|
const call = openTools.get(toolCallId);
|
|
395
|
+
const normalizedOutput = output.ok
|
|
396
|
+
? { type: 'tool-result' as const, output: output.output }
|
|
397
|
+
: { type: 'error' as const, error: output.error };
|
|
296
398
|
if (call == null) return;
|
|
297
399
|
openTools.delete(toolCallId);
|
|
298
400
|
await dispatcher.onToolExecutionEnd?.(
|
|
@@ -308,29 +410,29 @@ export function createTurnTelemetry(opts: {
|
|
|
308
410
|
dynamic: true,
|
|
309
411
|
},
|
|
310
412
|
toolContext: undefined,
|
|
311
|
-
toolOutput:
|
|
312
|
-
? { type: 'tool-result', output: output.output }
|
|
313
|
-
: { type: 'error', error: output.error },
|
|
413
|
+
toolOutput: normalizedOutput,
|
|
314
414
|
}),
|
|
315
415
|
);
|
|
316
416
|
},
|
|
317
417
|
|
|
318
418
|
async end(info) {
|
|
319
419
|
if (ended) return;
|
|
420
|
+
const finishReason = normalizeFinishReason(info.finishReason);
|
|
421
|
+
const usage = normalizeUsage(info.usage);
|
|
320
422
|
if (!started) await fireStart();
|
|
321
423
|
if (stepOpen) {
|
|
322
424
|
await closeOpenTools();
|
|
323
425
|
await inferenceEnd({
|
|
324
|
-
finishReason
|
|
325
|
-
usage
|
|
426
|
+
finishReason,
|
|
427
|
+
usage,
|
|
326
428
|
content: [],
|
|
327
429
|
});
|
|
328
430
|
await dispatcher.onStepEnd?.(
|
|
329
431
|
cast<'onStepEnd'>({
|
|
330
432
|
callId,
|
|
331
433
|
stepNumber,
|
|
332
|
-
finishReason
|
|
333
|
-
usage
|
|
434
|
+
finishReason,
|
|
435
|
+
usage,
|
|
334
436
|
providerMetadata: undefined,
|
|
335
437
|
content: [],
|
|
336
438
|
response: {
|
|
@@ -348,10 +450,17 @@ export function createTurnTelemetry(opts: {
|
|
|
348
450
|
cast<'onEnd'>({
|
|
349
451
|
callId,
|
|
350
452
|
operationId: 'ai.harness',
|
|
351
|
-
finishReason
|
|
352
|
-
usage
|
|
353
|
-
totalUsage:
|
|
453
|
+
finishReason,
|
|
454
|
+
usage,
|
|
455
|
+
totalUsage: usage,
|
|
354
456
|
content: [],
|
|
457
|
+
text: finalStepText,
|
|
458
|
+
finalStep: {
|
|
459
|
+
reasoning: finalStepReasoning,
|
|
460
|
+
providerMetadata: finalStepProviderMetadata,
|
|
461
|
+
},
|
|
462
|
+
toolCalls: outputToolCalls,
|
|
463
|
+
files: [],
|
|
355
464
|
steps: new Array(stepNumber),
|
|
356
465
|
response: {
|
|
357
466
|
id: callId,
|
package/src/bridge/index.ts
CHANGED
|
@@ -120,13 +120,6 @@ export interface BridgeTurn {
|
|
|
120
120
|
/** Aborts when the host sends `abort`. */
|
|
121
121
|
readonly abortSignal: AbortSignal;
|
|
122
122
|
|
|
123
|
-
/**
|
|
124
|
-
* Register the runtime-specific interrupt hook for this active turn. The
|
|
125
|
-
* shared bridge invokes it when the host sends `interrupt`, then acknowledges
|
|
126
|
-
* only after the hook settles.
|
|
127
|
-
*/
|
|
128
|
-
onInterrupt(handler: () => void | Promise<void>): void;
|
|
129
|
-
|
|
130
123
|
/** True for the first turn since this bridge process started. */
|
|
131
124
|
readonly firstTurn: boolean;
|
|
132
125
|
|
|
@@ -191,7 +184,6 @@ type InboundControl =
|
|
|
191
184
|
}
|
|
192
185
|
| { type: 'user-message'; text: string }
|
|
193
186
|
| { type: 'abort' }
|
|
194
|
-
| { type: 'interrupt' }
|
|
195
187
|
| { type: 'shutdown' }
|
|
196
188
|
| { type: 'detach' }
|
|
197
189
|
| { type: 'resume'; lastSeenEventId: number };
|
|
@@ -238,7 +230,6 @@ export async function runBridge<TStart extends { type: 'start' }>(
|
|
|
238
230
|
let isFirstTurn = true;
|
|
239
231
|
let turnAbort: AbortController | undefined;
|
|
240
232
|
let currentUserMessages: string[] | undefined;
|
|
241
|
-
let currentInterruptHandler: (() => void | Promise<void>) | undefined;
|
|
242
233
|
|
|
243
234
|
// Diagnostics. Resolved per turn from `start.debug` with a sandbox-side
|
|
244
235
|
// env fallback; gates console capture + structured `debug-event`s.
|
|
@@ -259,7 +250,7 @@ export async function runBridge<TStart extends { type: 'start' }>(
|
|
|
259
250
|
* Disk mirror of the in-memory replay log. The in-memory log is lost when the
|
|
260
251
|
* bridge process dies; the on-disk `event-log.ndjson` survives in the sandbox
|
|
261
252
|
* filesystem so a respawned bridge (started with `BRIDGE_REPLAY_FROM_DISK=1`)
|
|
262
|
-
* can reload the
|
|
253
|
+
* can reload the in-flight turn and serve a host's resume cursor —
|
|
263
254
|
* `replay` recovery. Writes are batched on `setImmediate` (single-flight via
|
|
264
255
|
* `flushPromise`) to keep `emit` off the disk hot path.
|
|
265
256
|
*/
|
|
@@ -537,7 +528,6 @@ export async function runBridge<TStart extends { type: 'start' }>(
|
|
|
537
528
|
void writeFile(eventLogPath, '').catch(() => {});
|
|
538
529
|
turnAbort = new AbortController();
|
|
539
530
|
currentTurnState = 'running';
|
|
540
|
-
currentInterruptHandler = undefined;
|
|
541
531
|
void writeStartConfig(msg);
|
|
542
532
|
void writeBridgeMeta('running');
|
|
543
533
|
const startDebug = (msg as { debug?: BridgeDebugConfig }).debug;
|
|
@@ -565,9 +555,6 @@ export async function runBridge<TStart extends { type: 'start' }>(
|
|
|
565
555
|
}),
|
|
566
556
|
pendingUserMessages: [],
|
|
567
557
|
abortSignal: turnAbort.signal,
|
|
568
|
-
onInterrupt: handler => {
|
|
569
|
-
currentInterruptHandler = handler;
|
|
570
|
-
},
|
|
571
558
|
firstTurn,
|
|
572
559
|
bridgeLog: input => {
|
|
573
560
|
const level = input.level ?? 'debug';
|
|
@@ -592,7 +579,6 @@ export async function runBridge<TStart extends { type: 'start' }>(
|
|
|
592
579
|
} catch (err) {
|
|
593
580
|
emitError({ error: err, message: 'bridge turn failed' });
|
|
594
581
|
} finally {
|
|
595
|
-
currentInterruptHandler = undefined;
|
|
596
582
|
currentTurnState = 'waiting';
|
|
597
583
|
void writeBridgeMeta('waiting');
|
|
598
584
|
}
|
|
@@ -620,34 +606,6 @@ export async function runBridge<TStart extends { type: 'start' }>(
|
|
|
620
606
|
case 'abort':
|
|
621
607
|
turnAbort?.abort();
|
|
622
608
|
return;
|
|
623
|
-
case 'interrupt':
|
|
624
|
-
try {
|
|
625
|
-
/*
|
|
626
|
-
* A bridge waiting for a host tool result or approval is already
|
|
627
|
-
* paused at a resumable boundary. Interrupting the native runtime at
|
|
628
|
-
* that point terminates the operation that owns the pending request,
|
|
629
|
-
* so a later host process cannot satisfy it. Active turns without
|
|
630
|
-
* pending host input are interrupted before suspension as usual.
|
|
631
|
-
*/
|
|
632
|
-
if (
|
|
633
|
-
pendingToolResults.size === 0 &&
|
|
634
|
-
pendingToolApprovals.size === 0
|
|
635
|
-
) {
|
|
636
|
-
if (currentInterruptHandler) {
|
|
637
|
-
await currentInterruptHandler();
|
|
638
|
-
} else {
|
|
639
|
-
turnAbort?.abort();
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
sendControl({ type: 'bridge-interrupted', ok: true });
|
|
643
|
-
} catch (err) {
|
|
644
|
-
sendControl({
|
|
645
|
-
type: 'bridge-interrupted',
|
|
646
|
-
ok: false,
|
|
647
|
-
error: serialiseError(err),
|
|
648
|
-
});
|
|
649
|
-
}
|
|
650
|
-
return;
|
|
651
609
|
case 'resume':
|
|
652
610
|
if (activeSocket !== ws) return;
|
|
653
611
|
replay(ws, msg.lastSeenEventId);
|
|
@@ -277,62 +277,6 @@ export class SandboxChannel<
|
|
|
277
277
|
this.enqueue(() => this.finalizeClose(1000, 'closed'));
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
-
interrupt(options?: { timeoutMs?: number }): Promise<void> {
|
|
281
|
-
if (this.pinnedSuspensionCursor != null) {
|
|
282
|
-
return Promise.resolve();
|
|
283
|
-
}
|
|
284
|
-
const timeoutMs = options?.timeoutMs ?? 5000;
|
|
285
|
-
return new Promise<void>((resolve, reject) => {
|
|
286
|
-
let settled = false;
|
|
287
|
-
let unsub = (): void => {};
|
|
288
|
-
const timer = setTimeout(() => {
|
|
289
|
-
complete(
|
|
290
|
-
new Error(
|
|
291
|
-
`SandboxChannel: interrupt was not acknowledged within ${timeoutMs}ms.`,
|
|
292
|
-
),
|
|
293
|
-
);
|
|
294
|
-
}, timeoutMs);
|
|
295
|
-
timer.unref?.();
|
|
296
|
-
|
|
297
|
-
const complete = (error?: unknown): void => {
|
|
298
|
-
if (settled) return;
|
|
299
|
-
settled = true;
|
|
300
|
-
clearTimeout(timer);
|
|
301
|
-
unsub();
|
|
302
|
-
if (error) {
|
|
303
|
-
reject(error);
|
|
304
|
-
} else {
|
|
305
|
-
resolve();
|
|
306
|
-
}
|
|
307
|
-
};
|
|
308
|
-
|
|
309
|
-
unsub = this.on('bridge-interrupted' as EventTypeOf<TOut>, event => {
|
|
310
|
-
const response = event as unknown as {
|
|
311
|
-
type: 'bridge-interrupted';
|
|
312
|
-
ok: boolean;
|
|
313
|
-
error?: unknown;
|
|
314
|
-
};
|
|
315
|
-
if (response.ok) {
|
|
316
|
-
complete();
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
complete(
|
|
320
|
-
new Error(
|
|
321
|
-
`SandboxChannel: interrupt failed: ${formatControlError(
|
|
322
|
-
response.error,
|
|
323
|
-
)}`,
|
|
324
|
-
),
|
|
325
|
-
);
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
try {
|
|
329
|
-
this.send({ type: 'interrupt' } as TIn);
|
|
330
|
-
} catch (err) {
|
|
331
|
-
complete(err);
|
|
332
|
-
}
|
|
333
|
-
});
|
|
334
|
-
}
|
|
335
|
-
|
|
336
280
|
/**
|
|
337
281
|
* Gracefully suspend at a slice boundary: stop processing inbound frames
|
|
338
282
|
* (so the cursor freezes at the last delivered event), drain any frames
|
|
@@ -571,13 +515,3 @@ export class SandboxChannel<
|
|
|
571
515
|
for (const h of this.onCloseHandlers) h(code, reason);
|
|
572
516
|
}
|
|
573
517
|
}
|
|
574
|
-
|
|
575
|
-
function formatControlError(error: unknown): string {
|
|
576
|
-
if (error instanceof Error) return error.message;
|
|
577
|
-
if (error && typeof error === 'object') {
|
|
578
|
-
const message = (error as { message?: unknown }).message;
|
|
579
|
-
if (typeof message === 'string' && message.length > 0) return message;
|
|
580
|
-
}
|
|
581
|
-
if (typeof error === 'string' && error.length > 0) return error;
|
|
582
|
-
return 'unknown error';
|
|
583
|
-
}
|
|
@@ -140,17 +140,6 @@ export const harnessV1BridgeThreadSchema = z.object({
|
|
|
140
140
|
threadId: z.string(),
|
|
141
141
|
});
|
|
142
142
|
|
|
143
|
-
/**
|
|
144
|
-
* Acknowledgement for an inbound `interrupt` command. The host waits for this
|
|
145
|
-
* before freezing its replay cursor so the adapter-specific interrupt has
|
|
146
|
-
* actually reached the underlying runtime.
|
|
147
|
-
*/
|
|
148
|
-
export const harnessV1BridgeInterruptedSchema = z.object({
|
|
149
|
-
type: z.literal('bridge-interrupted'),
|
|
150
|
-
ok: z.boolean(),
|
|
151
|
-
error: z.unknown().optional(),
|
|
152
|
-
});
|
|
153
|
-
|
|
154
143
|
// --- Diagnostics frames (outbound, not consumer events) ---
|
|
155
144
|
|
|
156
145
|
/**
|
|
@@ -211,7 +200,6 @@ export const harnessV1BridgeOutboundMessageSchema = z.discriminatedUnion(
|
|
|
211
200
|
harnessV1BridgeHelloSchema,
|
|
212
201
|
harnessV1BridgeDetachSchema,
|
|
213
202
|
harnessV1BridgeThreadSchema,
|
|
214
|
-
harnessV1BridgeInterruptedSchema,
|
|
215
203
|
harnessV1BridgeSandboxLogSchema,
|
|
216
204
|
harnessV1BridgeDebugEventSchema,
|
|
217
205
|
],
|
|
@@ -289,10 +277,6 @@ export const harnessV1BridgeAbortInboundSchema = z.object({
|
|
|
289
277
|
type: z.literal('abort'),
|
|
290
278
|
});
|
|
291
279
|
|
|
292
|
-
export const harnessV1BridgeInterruptInboundSchema = z.object({
|
|
293
|
-
type: z.literal('interrupt'),
|
|
294
|
-
});
|
|
295
|
-
|
|
296
280
|
export const harnessV1BridgeShutdownInboundSchema = z.object({
|
|
297
281
|
type: z.literal('shutdown'),
|
|
298
282
|
});
|
|
@@ -324,7 +308,6 @@ export const harnessV1BridgeInboundCommandSchemas = [
|
|
|
324
308
|
harnessV1BridgeToolApprovalResponseInboundSchema,
|
|
325
309
|
harnessV1BridgeUserMessageInboundSchema,
|
|
326
310
|
harnessV1BridgeAbortInboundSchema,
|
|
327
|
-
harnessV1BridgeInterruptInboundSchema,
|
|
328
311
|
harnessV1BridgeShutdownInboundSchema,
|
|
329
312
|
harnessV1BridgeResumeInboundSchema,
|
|
330
313
|
harnessV1BridgeDetachInboundSchema,
|
|
@@ -53,7 +53,7 @@ export type HarnessV1ResumeSessionState = HarnessV1LifecycleStateBase & {
|
|
|
53
53
|
/**
|
|
54
54
|
* Opaque payload returned by `doSuspendTurn` and accepted by a future
|
|
55
55
|
* `HarnessV1.doStart({ continueFrom })` to reconnect to the same session before
|
|
56
|
-
* continuing the
|
|
56
|
+
* continuing the suspended turn.
|
|
57
57
|
*/
|
|
58
58
|
export type HarnessV1ContinueTurnState = HarnessV1LifecycleStateBase & {
|
|
59
59
|
readonly type: 'continue-turn';
|
|
@@ -216,7 +216,7 @@ export type HarnessV1Session = {
|
|
|
216
216
|
/**
|
|
217
217
|
* Continue the in-flight turn **without a new user prompt**, returning the
|
|
218
218
|
* same control surface as `doPromptTurn`. Used to keep consuming a turn that
|
|
219
|
-
* was
|
|
219
|
+
* was suspended at a process boundary (the workflow slice loop), after the
|
|
220
220
|
* session itself has been resumed via `doStart({ continueFrom })`:
|
|
221
221
|
*
|
|
222
222
|
* - When the runtime's turn is still live and reachable (bridge `attach` /
|
|
@@ -225,7 +225,7 @@ export type HarnessV1Session = {
|
|
|
225
225
|
* - When the live turn is gone (bridge respawned `rerun`, or a host-resident
|
|
226
226
|
* runtime like Pi whose turn cannot survive its process), the adapter
|
|
227
227
|
* re-drives the runtime's own thread from its persisted state. Lossy: work
|
|
228
|
-
* in flight at the
|
|
228
|
+
* in flight at the suspension is recomputed.
|
|
229
229
|
*
|
|
230
230
|
* Required on every adapter. The behaviour an adapter can guarantee follows
|
|
231
231
|
* from its architecture; the contract is uniform.
|
package/src/v1/harness-v1.ts
CHANGED
|
@@ -87,7 +87,7 @@ export type HarnessV1<TBuiltinTools extends ToolSet = ToolSet> = {
|
|
|
87
87
|
|
|
88
88
|
/**
|
|
89
89
|
* Start a fresh session, resume a parked session via `resumeFrom`, or resume
|
|
90
|
-
*
|
|
90
|
+
* a suspended turn via `continueFrom`. The host then issues prompts against
|
|
91
91
|
* the returned session, ending with `doDetach`, `doStop`, or `doDestroy`.
|
|
92
92
|
*/
|
|
93
93
|
doStart(options: HarnessV1StartOptions): PromiseLike<HarnessV1Session>;
|