@ai-sdk/harness 1.0.76 → 1.0.78
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 +20 -0
- package/bridge/index.ts +2 -0
- package/dist/agent/index.d.ts +42 -23
- package/dist/agent/index.js +168 -61
- package/dist/agent/index.js.map +1 -1
- package/dist/bridge/index.d.ts +12 -7
- package/dist/bridge/index.js +140 -5
- package/dist/bridge/index.js.map +1 -1
- package/dist/index.d.ts +38 -13
- package/dist/index.js +17 -1
- package/dist/index.js.map +1 -1
- package/dist/utils/index.d.ts +177 -1
- package/dist/utils/index.js +121 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +4 -4
- package/src/agent/harness-agent-session.ts +136 -12
- package/src/agent/harness-agent.ts +54 -22
- package/src/agent/internal/run-prompt.ts +2 -0
- package/src/agent/internal/sandbox-bootstrap.ts +3 -28
- package/src/agent/prepare-sandbox-for-harness.ts +3 -3
- package/src/bridge/index.ts +184 -12
- package/src/utils/bridge-user-message-submitter.ts +96 -0
- package/src/utils/get-restricted-sandbox-session.ts +10 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/resolve-sandbox-default-working-directory.ts +33 -0
- package/src/utils/sandbox-channel.ts +9 -0
- package/src/v1/harness-v1-bridge-protocol.ts +23 -0
- package/src/v1/harness-v1-session.ts +9 -12
- package/src/v1/index.ts +4 -1
package/src/bridge/index.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { appendFile, mkdir, writeFile } from 'node:fs/promises';
|
|
11
11
|
import { existsSync, readFileSync } from 'node:fs';
|
|
12
|
+
import { randomUUID } from 'node:crypto';
|
|
12
13
|
import { env as procEnv, pid, stdout } from 'node:process';
|
|
13
14
|
import { WebSocketServer, type WebSocket } from 'ws';
|
|
14
15
|
|
|
@@ -21,6 +22,29 @@ export type BridgeEvent = Record<string, unknown> & { type: string };
|
|
|
21
22
|
|
|
22
23
|
export type BridgeDebugLevel = 'error' | 'warn' | 'info' | 'debug' | 'trace';
|
|
23
24
|
|
|
25
|
+
export interface Experimental_BridgeUserMessage {
|
|
26
|
+
readonly messageId: string;
|
|
27
|
+
readonly text: string;
|
|
28
|
+
accept(): void;
|
|
29
|
+
reject(error: unknown): void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface Experimental_BridgeUserMessageQueue extends AsyncIterable<Experimental_BridgeUserMessage> {
|
|
33
|
+
readonly pendingCount: number;
|
|
34
|
+
close(error?: unknown): void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type InternalBridgeUserMessageQueue = Experimental_BridgeUserMessageQueue & {
|
|
38
|
+
enqueue(input: { messageId: string; text: string }): void;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type BridgeUserMessageResponse = {
|
|
42
|
+
type: 'user-message-response';
|
|
43
|
+
messageId: string;
|
|
44
|
+
accepted: boolean;
|
|
45
|
+
error?: { message: string };
|
|
46
|
+
};
|
|
47
|
+
|
|
24
48
|
/**
|
|
25
49
|
* Per-session diagnostics config. The host resolves it from settings +
|
|
26
50
|
* env and sends it on `start.debug`; the bridge gates console capture and
|
|
@@ -71,6 +95,125 @@ function formatBridgeError(err: unknown): {
|
|
|
71
95
|
return { message: String(err) };
|
|
72
96
|
}
|
|
73
97
|
|
|
98
|
+
function createBridgeUserMessageQueue(options: {
|
|
99
|
+
respond(response: BridgeUserMessageResponse): void;
|
|
100
|
+
}): InternalBridgeUserMessageQueue {
|
|
101
|
+
const messages: Experimental_BridgeUserMessage[] = [];
|
|
102
|
+
const waiters: Array<
|
|
103
|
+
(result: IteratorResult<Experimental_BridgeUserMessage>) => void
|
|
104
|
+
> = [];
|
|
105
|
+
const entries = new Map<
|
|
106
|
+
string,
|
|
107
|
+
{
|
|
108
|
+
response?: BridgeUserMessageResponse;
|
|
109
|
+
reject(error: unknown): void;
|
|
110
|
+
}
|
|
111
|
+
>();
|
|
112
|
+
let closed = false;
|
|
113
|
+
let pendingCount = 0;
|
|
114
|
+
|
|
115
|
+
const enqueue = (input: { messageId: string; text: string }): void => {
|
|
116
|
+
const existing = entries.get(input.messageId);
|
|
117
|
+
if (existing != null) {
|
|
118
|
+
if (existing.response != null) {
|
|
119
|
+
options.respond(existing.response);
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let settled = false;
|
|
125
|
+
const settle = (response: BridgeUserMessageResponse): void => {
|
|
126
|
+
if (settled) return;
|
|
127
|
+
settled = true;
|
|
128
|
+
pendingCount--;
|
|
129
|
+
const entry = entries.get(input.messageId);
|
|
130
|
+
if (entry != null) entry.response = response;
|
|
131
|
+
options.respond(response);
|
|
132
|
+
};
|
|
133
|
+
const message: Experimental_BridgeUserMessage = {
|
|
134
|
+
messageId: input.messageId,
|
|
135
|
+
text: input.text,
|
|
136
|
+
accept: () => {
|
|
137
|
+
settle({
|
|
138
|
+
type: 'user-message-response',
|
|
139
|
+
messageId: input.messageId,
|
|
140
|
+
accepted: true,
|
|
141
|
+
});
|
|
142
|
+
},
|
|
143
|
+
reject: error => {
|
|
144
|
+
settle({
|
|
145
|
+
type: 'user-message-response',
|
|
146
|
+
messageId: input.messageId,
|
|
147
|
+
accepted: false,
|
|
148
|
+
error: { message: formatBridgeError(error).message },
|
|
149
|
+
});
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
entries.set(input.messageId, {
|
|
153
|
+
reject: message.reject,
|
|
154
|
+
});
|
|
155
|
+
pendingCount++;
|
|
156
|
+
|
|
157
|
+
if (closed) {
|
|
158
|
+
message.reject(
|
|
159
|
+
new Error('The bridge turn is no longer accepting user messages.'),
|
|
160
|
+
);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const waiter = waiters.shift();
|
|
165
|
+
if (waiter != null) {
|
|
166
|
+
waiter({ done: false, value: message });
|
|
167
|
+
} else {
|
|
168
|
+
messages.push(message);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const close = (error?: unknown): void => {
|
|
173
|
+
if (closed) return;
|
|
174
|
+
closed = true;
|
|
175
|
+
const reason =
|
|
176
|
+
error ??
|
|
177
|
+
new Error('The bridge turn ended before accepting the user message.');
|
|
178
|
+
for (const entry of entries.values()) {
|
|
179
|
+
if (entry.response == null) entry.reject(reason);
|
|
180
|
+
}
|
|
181
|
+
messages.length = 0;
|
|
182
|
+
while (waiters.length > 0) {
|
|
183
|
+
waiters.shift()!({ done: true, value: undefined });
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
get pendingCount() {
|
|
189
|
+
return pendingCount;
|
|
190
|
+
},
|
|
191
|
+
enqueue,
|
|
192
|
+
close,
|
|
193
|
+
[Symbol.asyncIterator]() {
|
|
194
|
+
return {
|
|
195
|
+
next: () => {
|
|
196
|
+
const message = messages.shift();
|
|
197
|
+
if (message != null) {
|
|
198
|
+
return Promise.resolve({ done: false as const, value: message });
|
|
199
|
+
}
|
|
200
|
+
if (closed) {
|
|
201
|
+
return Promise.resolve({
|
|
202
|
+
done: true as const,
|
|
203
|
+
value: undefined,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return new Promise<IteratorResult<Experimental_BridgeUserMessage>>(
|
|
207
|
+
resolve => {
|
|
208
|
+
waiters.push(resolve);
|
|
209
|
+
},
|
|
210
|
+
);
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
74
217
|
function parseEnvList(value: string | undefined): string[] | undefined {
|
|
75
218
|
if (!value) return undefined;
|
|
76
219
|
const items = value
|
|
@@ -113,12 +256,7 @@ export interface BridgeTurn {
|
|
|
113
256
|
approvalId: string,
|
|
114
257
|
): Promise<{ approved: boolean; reason?: string }>;
|
|
115
258
|
|
|
116
|
-
|
|
117
|
-
* Live queue of mid-turn user messages. The runtime pushes inbound
|
|
118
|
-
* `user-message` text here; the adapter drains it as its runtime accepts
|
|
119
|
-
* interactive input.
|
|
120
|
-
*/
|
|
121
|
-
readonly pendingUserMessages: string[];
|
|
259
|
+
readonly experimental_userMessages: Experimental_BridgeUserMessageQueue;
|
|
122
260
|
|
|
123
261
|
/** Aborts when the host sends `abort`. */
|
|
124
262
|
readonly abortSignal: AbortSignal;
|
|
@@ -192,7 +330,7 @@ type InboundControl =
|
|
|
192
330
|
approved: boolean;
|
|
193
331
|
reason?: string;
|
|
194
332
|
}
|
|
195
|
-
| { type: 'user-message'; text: string }
|
|
333
|
+
| { type: 'user-message'; messageId?: string; text: string }
|
|
196
334
|
| { type: 'abort' }
|
|
197
335
|
| { type: 'stop' }
|
|
198
336
|
| { type: 'destroy' }
|
|
@@ -245,7 +383,7 @@ export async function runBridge<TStart extends { type: 'start' }>(
|
|
|
245
383
|
let activeSocket: WebSocket | undefined;
|
|
246
384
|
let isFirstTurn = true;
|
|
247
385
|
let turnAbort: AbortController | undefined;
|
|
248
|
-
let currentUserMessages:
|
|
386
|
+
let currentUserMessages: InternalBridgeUserMessageQueue | undefined;
|
|
249
387
|
|
|
250
388
|
// Diagnostics. Resolved per turn from `start.debug` with a sandbox-side
|
|
251
389
|
// env fallback; gates console capture + structured `debug-event`s.
|
|
@@ -521,6 +659,9 @@ export async function runBridge<TStart extends { type: 'start' }>(
|
|
|
521
659
|
switch (msg.type) {
|
|
522
660
|
case 'start': {
|
|
523
661
|
activeSocket = ws; // asking for a turn claims the event stream
|
|
662
|
+
currentUserMessages?.close(
|
|
663
|
+
new Error('A new bridge turn replaced the active turn.'),
|
|
664
|
+
);
|
|
524
665
|
const firstTurn = isFirstTurn;
|
|
525
666
|
isFirstTurn = false;
|
|
526
667
|
eventLog = []; // clear previous turn; keep seqCounter monotonic
|
|
@@ -545,6 +686,7 @@ export async function runBridge<TStart extends { type: 'start' }>(
|
|
|
545
686
|
if (debugConfig.enabled) {
|
|
546
687
|
installConsoleCapture();
|
|
547
688
|
}
|
|
689
|
+
const userMessages = createBridgeUserMessageQueue({ respond: emit });
|
|
548
690
|
const turn: BridgeTurn = {
|
|
549
691
|
emit,
|
|
550
692
|
requestToolResult: toolCallId =>
|
|
@@ -555,7 +697,7 @@ export async function runBridge<TStart extends { type: 'start' }>(
|
|
|
555
697
|
new Promise(resolve => {
|
|
556
698
|
pendingToolApprovals.set(approvalId, resolve);
|
|
557
699
|
}),
|
|
558
|
-
|
|
700
|
+
experimental_userMessages: userMessages,
|
|
559
701
|
abortSignal: turnAbort.signal,
|
|
560
702
|
firstTurn,
|
|
561
703
|
bridgeLog: input => {
|
|
@@ -575,12 +717,16 @@ export async function runBridge<TStart extends { type: 'start' }>(
|
|
|
575
717
|
emitWarning,
|
|
576
718
|
emitError,
|
|
577
719
|
};
|
|
578
|
-
currentUserMessages =
|
|
720
|
+
currentUserMessages = userMessages;
|
|
579
721
|
try {
|
|
580
722
|
await onStart(msg as TStart, turn);
|
|
581
723
|
} catch (err) {
|
|
582
724
|
emitError({ error: err, message: 'bridge turn failed' });
|
|
583
725
|
} finally {
|
|
726
|
+
userMessages.close();
|
|
727
|
+
if (currentUserMessages === userMessages) {
|
|
728
|
+
currentUserMessages = undefined;
|
|
729
|
+
}
|
|
584
730
|
currentTurnState = 'waiting';
|
|
585
731
|
void writeBridgeMeta('waiting');
|
|
586
732
|
}
|
|
@@ -602,9 +748,34 @@ export async function runBridge<TStart extends { type: 'start' }>(
|
|
|
602
748
|
}
|
|
603
749
|
return;
|
|
604
750
|
}
|
|
605
|
-
case 'user-message':
|
|
606
|
-
|
|
751
|
+
case 'user-message': {
|
|
752
|
+
const messageId = msg.messageId ?? randomUUID();
|
|
753
|
+
if (currentUserMessages == null) {
|
|
754
|
+
sendControl(ws, {
|
|
755
|
+
type: 'user-message-response',
|
|
756
|
+
messageId,
|
|
757
|
+
accepted: false,
|
|
758
|
+
error: { message: 'The bridge has no active turn to steer.' },
|
|
759
|
+
});
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
if (ws !== activeSocket) {
|
|
763
|
+
sendControl(ws, {
|
|
764
|
+
type: 'user-message-response',
|
|
765
|
+
messageId,
|
|
766
|
+
accepted: false,
|
|
767
|
+
error: {
|
|
768
|
+
message: 'The connection does not own the active bridge turn.',
|
|
769
|
+
},
|
|
770
|
+
});
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
currentUserMessages.enqueue({
|
|
774
|
+
messageId,
|
|
775
|
+
text: msg.text,
|
|
776
|
+
});
|
|
607
777
|
return;
|
|
778
|
+
}
|
|
608
779
|
case 'abort':
|
|
609
780
|
turnAbort?.abort();
|
|
610
781
|
return;
|
|
@@ -693,6 +864,7 @@ export async function runBridge<TStart extends { type: 'start' }>(
|
|
|
693
864
|
type: 'bridge-hello',
|
|
694
865
|
state: currentTurnState,
|
|
695
866
|
lastSeq: seqCounter,
|
|
867
|
+
capabilities: { experimental_userMessageResponses: true },
|
|
696
868
|
});
|
|
697
869
|
|
|
698
870
|
ws.on('message', (raw: ArrayBufferLike | string) => {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export type Experimental_BridgeUserMessageRequest = {
|
|
2
|
+
type: 'user-message';
|
|
3
|
+
messageId: string;
|
|
4
|
+
text: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type Experimental_BridgeUserMessageResponse = {
|
|
8
|
+
type: 'user-message-response';
|
|
9
|
+
messageId: string;
|
|
10
|
+
accepted: boolean;
|
|
11
|
+
error?: { message: string };
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type Experimental_BridgeUserMessageSubmitter = {
|
|
15
|
+
submit(text: string): Promise<void>;
|
|
16
|
+
close(error?: unknown): void;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function experimental_createBridgeUserMessageSubmitter(options: {
|
|
20
|
+
send(message: Experimental_BridgeUserMessageRequest): void;
|
|
21
|
+
onResponse(
|
|
22
|
+
listener: (response: Experimental_BridgeUserMessageResponse) => void,
|
|
23
|
+
): () => void;
|
|
24
|
+
onReconnect(listener: () => void): () => void;
|
|
25
|
+
}): Experimental_BridgeUserMessageSubmitter {
|
|
26
|
+
const pending = new Map<
|
|
27
|
+
string,
|
|
28
|
+
{
|
|
29
|
+
request: Experimental_BridgeUserMessageRequest;
|
|
30
|
+
resolve(): void;
|
|
31
|
+
reject(error: unknown): void;
|
|
32
|
+
}
|
|
33
|
+
>();
|
|
34
|
+
let closed = false;
|
|
35
|
+
|
|
36
|
+
const send = (request: Experimental_BridgeUserMessageRequest): void => {
|
|
37
|
+
try {
|
|
38
|
+
options.send(request);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
const entry = pending.get(request.messageId);
|
|
41
|
+
if (entry == null) return;
|
|
42
|
+
pending.delete(request.messageId);
|
|
43
|
+
entry.reject(error);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const unsubscribeResponse = options.onResponse(response => {
|
|
48
|
+
const entry = pending.get(response.messageId);
|
|
49
|
+
if (entry == null) return;
|
|
50
|
+
pending.delete(response.messageId);
|
|
51
|
+
if (response.accepted) {
|
|
52
|
+
entry.resolve();
|
|
53
|
+
} else {
|
|
54
|
+
entry.reject(
|
|
55
|
+
new Error(
|
|
56
|
+
response.error?.message ?? 'The runtime rejected the user message.',
|
|
57
|
+
),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
const unsubscribeReconnect = options.onReconnect(() => {
|
|
62
|
+
for (const entry of pending.values()) send(entry.request);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
submit: text => {
|
|
67
|
+
if (closed) {
|
|
68
|
+
return Promise.reject(
|
|
69
|
+
new Error('The bridge turn is no longer accepting user messages.'),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
const messageId = crypto.randomUUID();
|
|
73
|
+
const request: Experimental_BridgeUserMessageRequest = {
|
|
74
|
+
type: 'user-message',
|
|
75
|
+
messageId,
|
|
76
|
+
text,
|
|
77
|
+
};
|
|
78
|
+
const promise = new Promise<void>((resolve, reject) => {
|
|
79
|
+
pending.set(messageId, { request, resolve, reject });
|
|
80
|
+
});
|
|
81
|
+
send(request);
|
|
82
|
+
return promise;
|
|
83
|
+
},
|
|
84
|
+
close: error => {
|
|
85
|
+
if (closed) return;
|
|
86
|
+
closed = true;
|
|
87
|
+
unsubscribeResponse();
|
|
88
|
+
unsubscribeReconnect();
|
|
89
|
+
const reason =
|
|
90
|
+
error ??
|
|
91
|
+
new Error('The bridge turn ended before accepting the user message.');
|
|
92
|
+
for (const entry of pending.values()) entry.reject(reason);
|
|
93
|
+
pending.clear();
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Experimental_SandboxSession as SandboxSession } from '@ai-sdk/provider-utils';
|
|
2
|
+
import type { HarnessV1NetworkSandboxSession } from '../v1';
|
|
3
|
+
|
|
4
|
+
export function getRestrictedSandboxSession(
|
|
5
|
+
sandboxSession: HarnessV1NetworkSandboxSession | SandboxSession,
|
|
6
|
+
): SandboxSession {
|
|
7
|
+
return 'restricted' in sandboxSession
|
|
8
|
+
? sandboxSession.restricted()
|
|
9
|
+
: sandboxSession;
|
|
10
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -4,6 +4,12 @@ export {
|
|
|
4
4
|
type SandboxChannelOptions,
|
|
5
5
|
type SandboxChannelReconnectOptions,
|
|
6
6
|
} from './sandbox-channel';
|
|
7
|
+
export {
|
|
8
|
+
experimental_createBridgeUserMessageSubmitter,
|
|
9
|
+
type Experimental_BridgeUserMessageRequest,
|
|
10
|
+
type Experimental_BridgeUserMessageResponse,
|
|
11
|
+
type Experimental_BridgeUserMessageSubmitter,
|
|
12
|
+
} from './bridge-user-message-submitter';
|
|
7
13
|
export { classifyDiskLog, type DiskLogRecoveryMode } from './classify-disk-log';
|
|
8
14
|
export { getAiGatewayAuthFromEnv } from './ai-gateway-auth';
|
|
9
15
|
export {
|
|
@@ -34,3 +40,5 @@ export {
|
|
|
34
40
|
forwardBridgeProcessStream,
|
|
35
41
|
logBridgeError,
|
|
36
42
|
} from './bridge-diagnostics';
|
|
43
|
+
export { resolveSandboxDefaultWorkingDirectory } from './resolve-sandbox-default-working-directory';
|
|
44
|
+
export { getRestrictedSandboxSession } from './get-restricted-sandbox-session';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { posix } from 'node:path';
|
|
2
|
+
import type { Experimental_SandboxSession as SandboxSession } from '@ai-sdk/provider-utils';
|
|
3
|
+
import type { HarnessV1NetworkSandboxSession } from '../v1';
|
|
4
|
+
|
|
5
|
+
export async function resolveSandboxDefaultWorkingDirectory({
|
|
6
|
+
sandboxSession,
|
|
7
|
+
abortSignal,
|
|
8
|
+
}: {
|
|
9
|
+
readonly sandboxSession: HarnessV1NetworkSandboxSession | SandboxSession;
|
|
10
|
+
readonly abortSignal?: AbortSignal;
|
|
11
|
+
}): Promise<string> {
|
|
12
|
+
if ('defaultWorkingDirectory' in sandboxSession) {
|
|
13
|
+
return sandboxSession.defaultWorkingDirectory;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const result = await sandboxSession.run({
|
|
17
|
+
command: 'pwd',
|
|
18
|
+
abortSignal,
|
|
19
|
+
});
|
|
20
|
+
if (result.exitCode !== 0) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
`Failed to resolve sandbox default working directory (exit ${result.exitCode}): ${result.stderr || result.stdout}`,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const cwd = result.stdout.trim();
|
|
27
|
+
if (!posix.isAbsolute(cwd)) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`Failed to resolve sandbox default working directory: expected an absolute path, got ${JSON.stringify(cwd)}.`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
return cwd === '/' ? cwd : cwd.replace(/\/+$/, '');
|
|
33
|
+
}
|
|
@@ -133,6 +133,7 @@ export class SandboxChannel<
|
|
|
133
133
|
private readonly onCloseHandlers = new Set<
|
|
134
134
|
(code: number, reason: string) => void
|
|
135
135
|
>();
|
|
136
|
+
private readonly onReconnectHandlers = new Set<() => void>();
|
|
136
137
|
|
|
137
138
|
private readonly connectThunk: () => Promise<WebSocket>;
|
|
138
139
|
private readonly outboundSchema: FlexibleSchema<TOut>;
|
|
@@ -247,6 +248,13 @@ export class SandboxChannel<
|
|
|
247
248
|
this.onCloseHandlers.add(handler);
|
|
248
249
|
}
|
|
249
250
|
|
|
251
|
+
onReconnect(handler: () => void): () => void {
|
|
252
|
+
this.onReconnectHandlers.add(handler);
|
|
253
|
+
return () => {
|
|
254
|
+
this.onReconnectHandlers.delete(handler);
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
250
258
|
send(message: TIn): void {
|
|
251
259
|
if (this.terminal) {
|
|
252
260
|
throw new Error(
|
|
@@ -386,6 +394,7 @@ export class SandboxChannel<
|
|
|
386
394
|
}),
|
|
387
395
|
);
|
|
388
396
|
this.flushPending();
|
|
397
|
+
for (const handler of this.onReconnectHandlers) handler();
|
|
389
398
|
this.onDebug?.({
|
|
390
399
|
event: 'reconnected',
|
|
391
400
|
attempt,
|
|
@@ -133,6 +133,18 @@ export const harnessV1BridgeHelloSchema = z.object({
|
|
|
133
133
|
type: z.literal('bridge-hello'),
|
|
134
134
|
state: z.string().optional(),
|
|
135
135
|
lastSeq: z.number().optional(),
|
|
136
|
+
capabilities: z
|
|
137
|
+
.object({
|
|
138
|
+
experimental_userMessageResponses: z.boolean().optional(),
|
|
139
|
+
})
|
|
140
|
+
.optional(),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
export const experimental_harnessV1BridgeUserMessageResponseSchema = z.object({
|
|
144
|
+
type: z.literal('user-message-response'),
|
|
145
|
+
messageId: z.string(),
|
|
146
|
+
accepted: z.boolean(),
|
|
147
|
+
error: z.object({ message: z.string() }).optional(),
|
|
136
148
|
});
|
|
137
149
|
|
|
138
150
|
/**
|
|
@@ -211,6 +223,7 @@ export const harnessV1BridgeOutboundMessageSchema = z.discriminatedUnion(
|
|
|
211
223
|
harnessV1ErrorPartSchema,
|
|
212
224
|
harnessV1RawPartSchema,
|
|
213
225
|
harnessV1BridgeHelloSchema,
|
|
226
|
+
experimental_harnessV1BridgeUserMessageResponseSchema,
|
|
214
227
|
harnessV1BridgeStopSchema,
|
|
215
228
|
harnessV1BridgeThreadSchema,
|
|
216
229
|
harnessV1BridgeSandboxLogSchema,
|
|
@@ -222,6 +235,10 @@ export type HarnessV1BridgeOutboundMessage = z.infer<
|
|
|
222
235
|
typeof harnessV1BridgeOutboundMessageSchema
|
|
223
236
|
>;
|
|
224
237
|
|
|
238
|
+
export type Experimental_HarnessV1BridgeUserMessageResponse = z.infer<
|
|
239
|
+
typeof experimental_harnessV1BridgeUserMessageResponseSchema
|
|
240
|
+
>;
|
|
241
|
+
|
|
225
242
|
export type HarnessV1BridgeSandboxLog = z.infer<
|
|
226
243
|
typeof harnessV1BridgeSandboxLogSchema
|
|
227
244
|
>;
|
|
@@ -283,9 +300,15 @@ export const harnessV1BridgeToolApprovalResponseInboundSchema = z.object({
|
|
|
283
300
|
|
|
284
301
|
export const harnessV1BridgeUserMessageInboundSchema = z.object({
|
|
285
302
|
type: z.literal('user-message'),
|
|
303
|
+
messageId: z.string().optional(),
|
|
286
304
|
text: z.string(),
|
|
287
305
|
});
|
|
288
306
|
|
|
307
|
+
export const experimental_harnessV1BridgeUserMessageInboundSchema =
|
|
308
|
+
harnessV1BridgeUserMessageInboundSchema.extend({
|
|
309
|
+
messageId: z.string(),
|
|
310
|
+
});
|
|
311
|
+
|
|
289
312
|
export const harnessV1BridgeAbortInboundSchema = z.object({
|
|
290
313
|
type: z.literal('abort'),
|
|
291
314
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { HarnessV1NetworkSandboxSession } from './harness-v1-network-sandbox-session';
|
|
2
|
+
import type { Experimental_SandboxSession as SandboxSession } from '@ai-sdk/provider-utils';
|
|
2
3
|
import type { HarnessV1Observability } from './harness-v1-observability';
|
|
3
4
|
import type { HarnessV1PermissionMode } from './harness-v1-permission-mode';
|
|
4
5
|
import type { HarnessV1Prompt } from './harness-v1-prompt';
|
|
@@ -76,21 +77,17 @@ export type HarnessV1StartOptions = {
|
|
|
76
77
|
*/
|
|
77
78
|
readonly observability?: HarnessV1Observability;
|
|
78
79
|
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* `setRequestTransformations`, `addRequestTransformations`) for bridge
|
|
84
|
-
* wiring. Adapters must not call `stop()` themselves; the agent does that
|
|
85
|
-
* during cleanup.
|
|
80
|
+
* Sandbox session the adapter operates against. Network sandbox sessions
|
|
81
|
+
* expose optional infrastructure capabilities for bridge wiring; caller-
|
|
82
|
+
* provided basic sandbox sessions expose only filesystem and process APIs.
|
|
83
|
+
* Adapters must not stop or destroy the sandbox themselves.
|
|
86
84
|
*/
|
|
87
|
-
readonly sandboxSession: HarnessV1NetworkSandboxSession;
|
|
85
|
+
readonly sandboxSession: HarnessV1NetworkSandboxSession | SandboxSession;
|
|
88
86
|
|
|
89
87
|
/**
|
|
90
|
-
* Absolute path the adapter runs the agent in for this session. Composed
|
|
91
|
-
* the
|
|
92
|
-
*
|
|
93
|
-
* deriving its own provider-specific path.
|
|
88
|
+
* Absolute path the adapter runs the agent in for this session. Composed
|
|
89
|
+
* underneath the sandbox's resolved default working directory and created
|
|
90
|
+
* before `doStart`.
|
|
94
91
|
*/
|
|
95
92
|
readonly sessionWorkDir: string;
|
|
96
93
|
};
|
package/src/v1/index.ts
CHANGED
|
@@ -73,6 +73,7 @@ export {
|
|
|
73
73
|
harnessV1BridgeDestroyInboundSchema,
|
|
74
74
|
harnessV1BridgeHelloSchema,
|
|
75
75
|
harnessV1BridgeInboundCommandSchemas,
|
|
76
|
+
harnessV1BridgeUserMessageInboundSchema,
|
|
76
77
|
harnessV1BridgeOutboundMessageSchema,
|
|
77
78
|
harnessV1BridgeReadySchema,
|
|
78
79
|
harnessV1BridgeResponseFormatSchema,
|
|
@@ -86,13 +87,15 @@ export {
|
|
|
86
87
|
harnessV1BridgeToolResultInboundSchema,
|
|
87
88
|
harnessV1BridgePermissionModeSchema,
|
|
88
89
|
harnessV1BridgeToolWireSchema,
|
|
89
|
-
|
|
90
|
+
experimental_harnessV1BridgeUserMessageInboundSchema,
|
|
91
|
+
experimental_harnessV1BridgeUserMessageResponseSchema,
|
|
90
92
|
harnessV1DiagnosticFromBridgeFrame,
|
|
91
93
|
type HarnessV1BridgeDebugEvent,
|
|
92
94
|
type HarnessV1BridgeOutboundMessage,
|
|
93
95
|
type HarnessV1BridgeReady,
|
|
94
96
|
type HarnessV1BridgeSandboxLog,
|
|
95
97
|
type HarnessV1BridgeToolWire,
|
|
98
|
+
type Experimental_HarnessV1BridgeUserMessageResponse,
|
|
96
99
|
} from './harness-v1-bridge-protocol';
|
|
97
100
|
export {
|
|
98
101
|
harnessV1DebugConfigSchema,
|