@ai-sdk/harness 1.0.72 → 1.0.73
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 +7 -0
- package/README.md +23 -0
- package/dist/agent/index.d.ts +58 -14
- package/dist/agent/index.js +180 -22
- package/dist/agent/index.js.map +1 -1
- package/dist/bridge/index.js +14 -20
- package/dist/bridge/index.js.map +1 -1
- package/dist/index.d.ts +37 -1
- package/dist/index.js +12 -1
- package/dist/index.js.map +1 -1
- package/dist/utils/index.js +11 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/agent/harness-agent-session.ts +27 -7
- package/src/agent/harness-agent-settings.ts +8 -0
- package/src/agent/harness-agent.ts +74 -23
- package/src/agent/internal/harness-stream-text-result.ts +251 -59
- package/src/agent/internal/run-prompt.ts +10 -2
- package/src/bridge/index.ts +41 -37
- package/src/v1/harness-v1-bridge-protocol.ts +13 -0
- package/src/v1/harness-v1-response-format.ts +32 -0
- package/src/v1/harness-v1-session.ts +13 -0
- package/src/v1/index.ts +8 -0
package/src/bridge/index.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// Shared in-sandbox bridge runtime. Adapter `bridge.mjs` bundles re-bundle
|
|
2
2
|
// this module (tsup inlines it; `ws` stays external and resolves from the
|
|
3
3
|
// sandbox-installed node_modules). It owns everything generic to the bridge
|
|
4
|
-
// transport — the WebSocket server, token auth,
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
4
|
+
// transport — the WebSocket server, token auth, the in-memory event log +
|
|
5
|
+
// monotonic `seq`, resume replay, and the lifecycle/meta files. Any number of
|
|
6
|
+
// hosts may be connected; exactly one of them owns the event stream, and
|
|
7
|
+
// `start`/`resume` transfer that ownership. The adapter supplies only `onStart`
|
|
8
|
+
// (drive its CLI/SDK and translate to wire events) and lifecycle cleanup hooks.
|
|
8
9
|
|
|
9
10
|
import { appendFile, mkdir, writeFile } from 'node:fs/promises';
|
|
10
11
|
import { existsSync, readFileSync } from 'node:fs';
|
|
@@ -234,8 +235,14 @@ export async function runBridge<TStart extends { type: 'start' }>(
|
|
|
234
235
|
// ─── mutable runtime state ──────────────────────────────────────────
|
|
235
236
|
let currentBoundPort = 0;
|
|
236
237
|
let currentTurnState: BridgeState = 'init';
|
|
238
|
+
/*
|
|
239
|
+
* The one connection turn events stream to. A socket claims it by asking for
|
|
240
|
+
* work — `start` (a turn) or `resume` (a catch-up) — never by connecting:
|
|
241
|
+
* every event goes here alone, so claiming on connect would silence a turn
|
|
242
|
+
* already streaming to someone else. Any number of sockets may be connected;
|
|
243
|
+
* the others still exchange control frames, they just get no events.
|
|
244
|
+
*/
|
|
237
245
|
let activeSocket: WebSocket | undefined;
|
|
238
|
-
let activeSocketReadyForLiveEvents = false;
|
|
239
246
|
let isFirstTurn = true;
|
|
240
247
|
let turnAbort: AbortController | undefined;
|
|
241
248
|
let currentUserMessages: string[] | undefined;
|
|
@@ -374,26 +381,13 @@ export async function runBridge<TStart extends { type: 'start' }>(
|
|
|
374
381
|
};
|
|
375
382
|
|
|
376
383
|
// ─── wire send + replay ─────────────────────────────────────────────
|
|
377
|
-
const sendControl = (msg: Record<string, unknown>): void => {
|
|
378
|
-
if (activeSocket?.readyState === WS_OPEN) {
|
|
379
|
-
try {
|
|
380
|
-
activeSocket.send(JSON.stringify(msg));
|
|
381
|
-
} catch {
|
|
382
|
-
// best-effort
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
};
|
|
386
|
-
|
|
387
384
|
const emit = (event: BridgeEvent): void => {
|
|
388
385
|
const seq = ++seqCounter;
|
|
389
386
|
const line = JSON.stringify({ ...event, seq });
|
|
390
387
|
eventLog.push({ seq, line });
|
|
391
388
|
diskBuffer += `${line}\n`;
|
|
392
389
|
scheduleEventFlush();
|
|
393
|
-
if (
|
|
394
|
-
activeSocketReadyForLiveEvents &&
|
|
395
|
-
activeSocket?.readyState === WS_OPEN
|
|
396
|
-
) {
|
|
390
|
+
if (activeSocket?.readyState === WS_OPEN) {
|
|
397
391
|
try {
|
|
398
392
|
activeSocket.send(line);
|
|
399
393
|
} catch {
|
|
@@ -526,8 +520,7 @@ export async function runBridge<TStart extends { type: 'start' }>(
|
|
|
526
520
|
): Promise<void> => {
|
|
527
521
|
switch (msg.type) {
|
|
528
522
|
case 'start': {
|
|
529
|
-
|
|
530
|
-
activeSocketReadyForLiveEvents = true;
|
|
523
|
+
activeSocket = ws; // asking for a turn claims the event stream
|
|
531
524
|
const firstTurn = isFirstTurn;
|
|
532
525
|
isFirstTurn = false;
|
|
533
526
|
eventLog = []; // clear previous turn; keep seqCounter monotonic
|
|
@@ -616,9 +609,9 @@ export async function runBridge<TStart extends { type: 'start' }>(
|
|
|
616
609
|
turnAbort?.abort();
|
|
617
610
|
return;
|
|
618
611
|
case 'resume':
|
|
619
|
-
|
|
612
|
+
activeSocket = ws; // asking for a catch-up claims it too
|
|
613
|
+
// Synchronous, so no event can slip out live ahead of the replayed tail.
|
|
620
614
|
replay(ws, msg.lastSeenEventId);
|
|
621
|
-
activeSocketReadyForLiveEvents = true;
|
|
622
615
|
return;
|
|
623
616
|
case 'destroy':
|
|
624
617
|
currentTurnState = 'done';
|
|
@@ -630,7 +623,7 @@ export async function runBridge<TStart extends { type: 'start' }>(
|
|
|
630
623
|
currentTurnState = 'done';
|
|
631
624
|
void writeBridgeMeta('done');
|
|
632
625
|
const data = (await onStop?.()) ?? {};
|
|
633
|
-
sendControl({ type: 'bridge-stop', data });
|
|
626
|
+
sendControl(ws, { type: 'bridge-stop', data });
|
|
634
627
|
drainThenExit(ws, 1000, 'stop');
|
|
635
628
|
return;
|
|
636
629
|
}
|
|
@@ -693,16 +686,10 @@ export async function runBridge<TStart extends { type: 'start' }>(
|
|
|
693
686
|
return;
|
|
694
687
|
}
|
|
695
688
|
|
|
696
|
-
// Single-flight: a fresh authorized connection *replaces* the active one
|
|
697
|
-
// (the host reconnecting after a drop). The previous socket's close is a
|
|
698
|
-
// no-op below because it is no longer `activeSocket`.
|
|
699
|
-
activeSocket = ws;
|
|
700
|
-
activeSocketReadyForLiveEvents = false;
|
|
701
|
-
|
|
702
689
|
// Announce liveness the instant we accept. Some sandbox runtimes complete
|
|
703
690
|
// the host-side WS handshake before the connection is forwarded here; the
|
|
704
691
|
// host waits for this frame before sending `start`/`resume`.
|
|
705
|
-
sendControl({
|
|
692
|
+
sendControl(ws, {
|
|
706
693
|
type: 'bridge-hello',
|
|
707
694
|
state: currentTurnState,
|
|
708
695
|
lastSeq: seqCounter,
|
|
@@ -715,7 +702,7 @@ export async function runBridge<TStart extends { type: 'start' }>(
|
|
|
715
702
|
typeof raw === 'string' ? raw : Buffer.from(raw).toString('utf8');
|
|
716
703
|
parsed = JSON.parse(text) as TStart | InboundControl;
|
|
717
704
|
} catch (err) {
|
|
718
|
-
sendControl({
|
|
705
|
+
sendControl(ws, {
|
|
719
706
|
type: 'error',
|
|
720
707
|
error: `protocol parse error: ${(err as Error).message}`,
|
|
721
708
|
});
|
|
@@ -725,13 +712,12 @@ export async function runBridge<TStart extends { type: 'start' }>(
|
|
|
725
712
|
});
|
|
726
713
|
|
|
727
714
|
ws.on('close', () => {
|
|
728
|
-
// Only the
|
|
729
|
-
//
|
|
730
|
-
// the in-flight turn
|
|
731
|
-
// log for replay
|
|
715
|
+
// Only the stream owner's close matters; a socket that never claimed it,
|
|
716
|
+
// or that a later `start`/`resume` displaced, closes as a no-op.
|
|
717
|
+
// Crucially we do NOT abort the in-flight turn: it keeps running and its
|
|
718
|
+
// events accumulate in the log for replay on reconnect.
|
|
732
719
|
if (activeSocket === ws) {
|
|
733
720
|
activeSocket = undefined;
|
|
734
|
-
activeSocketReadyForLiveEvents = false;
|
|
735
721
|
}
|
|
736
722
|
});
|
|
737
723
|
|
|
@@ -767,6 +753,24 @@ export async function runBridge<TStart extends { type: 'start' }>(
|
|
|
767
753
|
};
|
|
768
754
|
}
|
|
769
755
|
|
|
756
|
+
/*
|
|
757
|
+
* Control frames answer the socket that sent the frame they reply to, so the
|
|
758
|
+
* target is always explicit. Event streaming is the separate, stateful path
|
|
759
|
+
* (`emit` → `activeSocket`); this one carries no state at all.
|
|
760
|
+
*/
|
|
761
|
+
function sendControl(
|
|
762
|
+
socket: WebSocket | undefined,
|
|
763
|
+
message: Record<string, unknown>,
|
|
764
|
+
): void {
|
|
765
|
+
if (socket?.readyState === WS_OPEN) {
|
|
766
|
+
try {
|
|
767
|
+
socket.send(JSON.stringify(message));
|
|
768
|
+
} catch {
|
|
769
|
+
// best-effort
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
770
774
|
function serialiseError(err: unknown): unknown {
|
|
771
775
|
if (err instanceof Error) {
|
|
772
776
|
return { name: err.name, message: err.message, stack: err.stack };
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
harnessV1DebugLevelSchema,
|
|
5
5
|
type HarnessV1Diagnostic,
|
|
6
6
|
} from './harness-v1-diagnostic';
|
|
7
|
+
import type { HarnessV1ResponseFormat } from './harness-v1-response-format';
|
|
7
8
|
import {
|
|
8
9
|
harnessV1CompactionPartSchema,
|
|
9
10
|
harnessV1ErrorPartSchema,
|
|
@@ -87,6 +88,17 @@ export const harnessV1BridgeBuiltinToolFilteringSchema = z.discriminatedUnion(
|
|
|
87
88
|
],
|
|
88
89
|
);
|
|
89
90
|
|
|
91
|
+
export const harnessV1BridgeResponseFormatSchema: z.ZodType<HarnessV1ResponseFormat> =
|
|
92
|
+
z.discriminatedUnion('type', [
|
|
93
|
+
z.object({ type: z.literal('text') }),
|
|
94
|
+
z.object({
|
|
95
|
+
type: z.literal('json'),
|
|
96
|
+
schema: z.record(z.string(), z.json()).optional(),
|
|
97
|
+
name: z.string().optional(),
|
|
98
|
+
description: z.string().optional(),
|
|
99
|
+
}),
|
|
100
|
+
]);
|
|
101
|
+
|
|
90
102
|
/**
|
|
91
103
|
* Common fields of the inbound `start` message. Each adapter extends this with
|
|
92
104
|
* its runtime-specific configuration (e.g. `thinking`/`continue` for Claude
|
|
@@ -105,6 +117,7 @@ export const harnessV1BridgeStartBaseSchema = z.object({
|
|
|
105
117
|
debug: harnessV1DebugConfigSchema.optional(),
|
|
106
118
|
permissionMode: harnessV1BridgePermissionModeSchema.optional(),
|
|
107
119
|
builtinToolFiltering: harnessV1BridgeBuiltinToolFilteringSchema.optional(),
|
|
120
|
+
responseFormat: harnessV1BridgeResponseFormatSchema.optional(),
|
|
108
121
|
});
|
|
109
122
|
|
|
110
123
|
// --- Transport / control frames (outbound, not consumer events) ---
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type HarnessV1JSONValue =
|
|
2
|
+
| null
|
|
3
|
+
| boolean
|
|
4
|
+
| number
|
|
5
|
+
| string
|
|
6
|
+
| HarnessV1JSONArray
|
|
7
|
+
| HarnessV1JSONObject;
|
|
8
|
+
|
|
9
|
+
export interface HarnessV1JSONArray extends Array<HarnessV1JSONValue> {}
|
|
10
|
+
|
|
11
|
+
export interface HarnessV1JSONObject {
|
|
12
|
+
[key: string]: HarnessV1JSONValue | undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type HarnessV1JSONSchema = HarnessV1JSONObject;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Requested response format for one harness turn.
|
|
19
|
+
*
|
|
20
|
+
* This intentionally mirrors the AI SDK provider response-format shape
|
|
21
|
+
* without depending on `@ai-sdk/provider`. Harness implementations receive
|
|
22
|
+
* JSON Schema rather than the caller's original Zod or Standard Schema so the
|
|
23
|
+
* contract can cross process and package boundaries.
|
|
24
|
+
*/
|
|
25
|
+
export type HarnessV1ResponseFormat =
|
|
26
|
+
| { readonly type: 'text' }
|
|
27
|
+
| {
|
|
28
|
+
readonly type: 'json';
|
|
29
|
+
readonly schema?: HarnessV1JSONSchema;
|
|
30
|
+
readonly name?: string;
|
|
31
|
+
readonly description?: string;
|
|
32
|
+
};
|
|
@@ -3,6 +3,7 @@ import type { HarnessV1Observability } from './harness-v1-observability';
|
|
|
3
3
|
import type { HarnessV1PermissionMode } from './harness-v1-permission-mode';
|
|
4
4
|
import type { HarnessV1Prompt } from './harness-v1-prompt';
|
|
5
5
|
import type { HarnessV1PromptControl } from './harness-v1-prompt-control';
|
|
6
|
+
import type { HarnessV1ResponseFormat } from './harness-v1-response-format';
|
|
6
7
|
import type {
|
|
7
8
|
HarnessV1ContinueTurnState,
|
|
8
9
|
HarnessV1ResumeSessionState,
|
|
@@ -105,6 +106,12 @@ export type HarnessV1PromptTurnOptions = {
|
|
|
105
106
|
*/
|
|
106
107
|
readonly prompt: HarnessV1Prompt;
|
|
107
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Response format requested for this turn. Adapters that cannot honor a
|
|
111
|
+
* JSON response format must throw `HarnessCapabilityUnsupportedError`.
|
|
112
|
+
*/
|
|
113
|
+
readonly responseFormat?: HarnessV1ResponseFormat;
|
|
114
|
+
|
|
108
115
|
/**
|
|
109
116
|
* Host-defined tools to make available to the underlying runtime for this
|
|
110
117
|
* turn. The harness emits `tool-call` events when the runtime calls one
|
|
@@ -144,6 +151,12 @@ export type HarnessV1PromptTurnOptions = {
|
|
|
144
151
|
* that was previously suspended temporarily, e.g. by the workflow slice loop.
|
|
145
152
|
*/
|
|
146
153
|
export type HarnessV1ContinueTurnOptions = {
|
|
154
|
+
/**
|
|
155
|
+
* Response format of the in-flight turn. Rerun-based adapters use this when
|
|
156
|
+
* reconstructing the turn; attach-based adapters may ignore it.
|
|
157
|
+
*/
|
|
158
|
+
readonly responseFormat?: HarnessV1ResponseFormat;
|
|
159
|
+
|
|
147
160
|
/**
|
|
148
161
|
* Host-defined tools to make available for the continued turn. Same shape
|
|
149
162
|
* as `doPromptTurn`'s `tools`. An adapter that purely attaches to a live turn
|
package/src/v1/index.ts
CHANGED
|
@@ -25,6 +25,13 @@ export {
|
|
|
25
25
|
} from './harness-v1-builtin-tool';
|
|
26
26
|
export type { HarnessV1Metadata } from './harness-v1-metadata';
|
|
27
27
|
export type { HarnessV1Prompt } from './harness-v1-prompt';
|
|
28
|
+
export type {
|
|
29
|
+
HarnessV1JSONSchema,
|
|
30
|
+
HarnessV1JSONArray,
|
|
31
|
+
HarnessV1JSONObject,
|
|
32
|
+
HarnessV1JSONValue,
|
|
33
|
+
HarnessV1ResponseFormat,
|
|
34
|
+
} from './harness-v1-response-format';
|
|
28
35
|
export type { HarnessV1SandboxProvider } from './harness-v1-sandbox-provider';
|
|
29
36
|
export type {
|
|
30
37
|
HarnessV1ContinueTurnState,
|
|
@@ -68,6 +75,7 @@ export {
|
|
|
68
75
|
harnessV1BridgeInboundCommandSchemas,
|
|
69
76
|
harnessV1BridgeOutboundMessageSchema,
|
|
70
77
|
harnessV1BridgeReadySchema,
|
|
78
|
+
harnessV1BridgeResponseFormatSchema,
|
|
71
79
|
harnessV1BridgeResumeInboundSchema,
|
|
72
80
|
harnessV1BridgeSandboxLogSchema,
|
|
73
81
|
harnessV1BridgeStopInboundSchema,
|