@opengeni/runtime 0.13.0 → 0.13.4
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/dist/{chunk-65UDUFUO.js → chunk-JGUQSAG5.js} +1887 -172
- package/dist/chunk-JGUQSAG5.js.map +1 -0
- package/dist/{chunk-HAN5D27J.js → chunk-QYXOVIUK.js} +24 -4
- package/dist/chunk-QYXOVIUK.js.map +1 -0
- package/dist/{index-DGUrY_yR.d.ts → index-DU-KXVlx.d.ts} +328 -25
- package/dist/index.d.ts +25 -3
- package/dist/index.js +71 -675
- package/dist/index.js.map +1 -1
- package/dist/mcp-network.d.ts +4 -1
- package/dist/mcp-network.js +5 -1
- package/dist/sandbox/index.d.ts +1 -1
- package/dist/sandbox/index.js +39 -1
- package/package.json +3 -3
- package/src/index.ts +31 -9
- package/src/mcp-network.ts +28 -3
- package/src/sandbox/channel-a.ts +92 -46
- package/src/sandbox/exec-banner.ts +15 -0
- package/src/sandbox/index.ts +360 -145
- package/src/sandbox/provider-errors.ts +175 -0
- package/src/sandbox/routing/backend-resolver.ts +15 -0
- package/src/sandbox/routing/routing-session.ts +730 -36
- package/src/sandbox/select.ts +8 -0
- package/src/sandbox/turn-tool-cancellation.ts +366 -92
- package/src/sandbox/workspace-archive.ts +289 -0
- package/dist/chunk-65UDUFUO.js.map +0 -1
- package/dist/chunk-HAN5D27J.js.map +0 -1
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import type { Computer, Editor, Tool } from "@openai/agents";
|
|
2
|
-
|
|
3
1
|
import { runWithToolCallCorrelation, sanitizeOpIdToken } from "./op-correlation";
|
|
4
2
|
import {
|
|
5
3
|
isExecSessionLostBanner,
|
|
6
4
|
parseExecBannerExitCode,
|
|
7
5
|
parseExecBannerSessionId,
|
|
8
|
-
} from "./
|
|
6
|
+
} from "./exec-banner";
|
|
9
7
|
|
|
10
8
|
const TURN_EXEC_YIELD_MS = 250;
|
|
11
9
|
const TURN_WRITE_YIELD_MS = 250;
|
|
@@ -14,8 +12,39 @@ const SHELL_GRACEFUL_POLLS = 2;
|
|
|
14
12
|
const SHELL_POLL_MS = 100;
|
|
15
13
|
const SHELL_MARKER_DIR = "/tmp/opengeni-turn-shell";
|
|
16
14
|
|
|
17
|
-
type
|
|
18
|
-
|
|
15
|
+
type ToolCallDetails = {
|
|
16
|
+
toolCall?: {
|
|
17
|
+
callId?: string;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type FunctionToolInvoke = (
|
|
22
|
+
runContext: unknown,
|
|
23
|
+
input: string,
|
|
24
|
+
details?: ToolCallDetails,
|
|
25
|
+
) => Promise<unknown>;
|
|
26
|
+
|
|
27
|
+
type FunctionTool = Record<string, unknown> & {
|
|
28
|
+
type: "function";
|
|
29
|
+
name: string;
|
|
30
|
+
invoke: FunctionToolInvoke;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type ApplyPatchEditor = {
|
|
34
|
+
createFile(operation: unknown, context?: unknown): Promise<unknown>;
|
|
35
|
+
updateFile(operation: unknown, context?: unknown): Promise<unknown>;
|
|
36
|
+
deleteFile(operation: unknown, context?: unknown): Promise<unknown>;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type ApplyPatchTool = Record<string, unknown> & {
|
|
40
|
+
type: "apply_patch";
|
|
41
|
+
editor: ApplyPatchEditor;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type ComputerTool = Record<string, unknown> & {
|
|
45
|
+
type: "computer";
|
|
46
|
+
computer: unknown;
|
|
47
|
+
};
|
|
19
48
|
|
|
20
49
|
const delay = async (milliseconds: number): Promise<void> =>
|
|
21
50
|
await new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
@@ -32,6 +61,7 @@ type ActiveShellSession = {
|
|
|
32
61
|
runContext: Parameters<FunctionToolInvoke>[0];
|
|
33
62
|
execInvoke: FunctionToolInvoke;
|
|
34
63
|
writeInvoke: FunctionToolInvoke | null;
|
|
64
|
+
processSession: CommandCancellationSession | null;
|
|
35
65
|
identity: ShellProcessIdentity | null;
|
|
36
66
|
identityValidated: boolean;
|
|
37
67
|
cancellation: Promise<void> | null;
|
|
@@ -40,6 +70,23 @@ type ActiveShellSession = {
|
|
|
40
70
|
type CommandCancellationSession = {
|
|
41
71
|
cancelExecCommand?(opId: string): Promise<boolean>;
|
|
42
72
|
supportsPty?(): boolean;
|
|
73
|
+
hasRetainedProcess?(providerSessionId: number): boolean;
|
|
74
|
+
writeStdinForProcessMutation?(args: {
|
|
75
|
+
sessionId: number;
|
|
76
|
+
chars?: string;
|
|
77
|
+
yieldTimeMs?: number;
|
|
78
|
+
maxOutputTokens?: number;
|
|
79
|
+
}): Promise<string>;
|
|
80
|
+
writeStdinForProcessControl?(args: {
|
|
81
|
+
sessionId: number;
|
|
82
|
+
chars?: string;
|
|
83
|
+
yieldTimeMs?: number;
|
|
84
|
+
maxOutputTokens?: number;
|
|
85
|
+
}): Promise<string>;
|
|
86
|
+
execCommandForProcessControl?(
|
|
87
|
+
providerSessionId: number,
|
|
88
|
+
args: TurnSandboxCommandArgs,
|
|
89
|
+
): Promise<string>;
|
|
43
90
|
};
|
|
44
91
|
|
|
45
92
|
export type TurnSandboxCommandArgs = {
|
|
@@ -53,6 +100,13 @@ export type TurnSandboxCommandArgs = {
|
|
|
53
100
|
runAs?: string;
|
|
54
101
|
};
|
|
55
102
|
|
|
103
|
+
export type TurnSandboxCommandResult = {
|
|
104
|
+
stdout: string;
|
|
105
|
+
stderr: string;
|
|
106
|
+
exitCode: number;
|
|
107
|
+
wallTimeSeconds: number;
|
|
108
|
+
};
|
|
109
|
+
|
|
56
110
|
export type TurnSandboxCommandSession = CommandCancellationSession & {
|
|
57
111
|
exec?(args: TurnSandboxCommandArgs): Promise<unknown>;
|
|
58
112
|
execCommand?(args: TurnSandboxCommandArgs): Promise<string>;
|
|
@@ -85,10 +139,14 @@ export type TurnToolCancellationFence = {
|
|
|
85
139
|
session: TurnSandboxCommandSession,
|
|
86
140
|
args: TurnSandboxCommandArgs,
|
|
87
141
|
): Promise<unknown>;
|
|
142
|
+
runSandboxCommandStructured(
|
|
143
|
+
session: TurnSandboxCommandSession,
|
|
144
|
+
args: TurnSandboxCommandArgs,
|
|
145
|
+
): Promise<TurnSandboxCommandResult>;
|
|
88
146
|
};
|
|
89
147
|
|
|
90
148
|
export type TurnToolCancellationController = TurnToolCancellationFence & {
|
|
91
|
-
wrapTools(tools:
|
|
149
|
+
wrapTools<T>(tools: T[], session?: CommandCancellationSession): T[];
|
|
92
150
|
};
|
|
93
151
|
|
|
94
152
|
export class TurnSandboxCommandCancelledError extends Error {
|
|
@@ -173,6 +231,64 @@ function nativeCommandBanner(result: unknown): string {
|
|
|
173
231
|
throw new Error("Sandbox command did not report a session id or exit code");
|
|
174
232
|
}
|
|
175
233
|
|
|
234
|
+
type NativeCommandResult = {
|
|
235
|
+
stdout: string;
|
|
236
|
+
stderr: string;
|
|
237
|
+
exitCode: number | null;
|
|
238
|
+
sessionId: number | null;
|
|
239
|
+
wallTimeSeconds: number;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
function nativeCommandResult(result: unknown): NativeCommandResult {
|
|
243
|
+
if (typeof result === "string") {
|
|
244
|
+
return {
|
|
245
|
+
stdout: execOutput(result),
|
|
246
|
+
stderr: "",
|
|
247
|
+
exitCode: parseExecBannerExitCode(result),
|
|
248
|
+
sessionId: parseExecBannerSessionId(result),
|
|
249
|
+
wallTimeSeconds: 0,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
if (!result || typeof result !== "object") {
|
|
253
|
+
throw new Error("Sandbox command returned an invalid result");
|
|
254
|
+
}
|
|
255
|
+
const value = result as {
|
|
256
|
+
output?: unknown;
|
|
257
|
+
stdout?: unknown;
|
|
258
|
+
stderr?: unknown;
|
|
259
|
+
exitCode?: unknown;
|
|
260
|
+
exit_code?: unknown;
|
|
261
|
+
sessionId?: unknown;
|
|
262
|
+
session_id?: unknown;
|
|
263
|
+
wallTimeSeconds?: unknown;
|
|
264
|
+
};
|
|
265
|
+
return {
|
|
266
|
+
stdout:
|
|
267
|
+
typeof value.stdout === "string"
|
|
268
|
+
? value.stdout
|
|
269
|
+
: typeof value.output === "string"
|
|
270
|
+
? value.output
|
|
271
|
+
: "",
|
|
272
|
+
stderr: typeof value.stderr === "string" ? value.stderr : "",
|
|
273
|
+
exitCode:
|
|
274
|
+
typeof value.exitCode === "number"
|
|
275
|
+
? value.exitCode
|
|
276
|
+
: typeof value.exit_code === "number"
|
|
277
|
+
? value.exit_code
|
|
278
|
+
: null,
|
|
279
|
+
sessionId:
|
|
280
|
+
typeof value.sessionId === "number"
|
|
281
|
+
? value.sessionId
|
|
282
|
+
: typeof value.session_id === "number"
|
|
283
|
+
? value.session_id
|
|
284
|
+
: null,
|
|
285
|
+
wallTimeSeconds:
|
|
286
|
+
typeof value.wallTimeSeconds === "number" && value.wallTimeSeconds >= 0
|
|
287
|
+
? value.wallTimeSeconds
|
|
288
|
+
: 0,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
176
292
|
function completedCommandBanner(exitCode: number, output: string): string {
|
|
177
293
|
return `Process exited with code ${exitCode}\n\nOutput:\n${output}`;
|
|
178
294
|
}
|
|
@@ -231,13 +347,31 @@ export function cancellableShellCommand(command: string, markerPath: string): st
|
|
|
231
347
|
}
|
|
232
348
|
|
|
233
349
|
function shellHelperInput(command: string): string {
|
|
350
|
+
const args = shellHelperArgs(command);
|
|
234
351
|
return JSON.stringify({
|
|
352
|
+
cmd: args.cmd,
|
|
353
|
+
login: args.login,
|
|
354
|
+
tty: args.tty,
|
|
355
|
+
yield_time_ms: args.yieldTimeMs,
|
|
356
|
+
max_output_tokens: args.maxOutputTokens,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function shellHelperArgs(command: string): TurnSandboxCommandArgs {
|
|
361
|
+
return {
|
|
235
362
|
cmd: command,
|
|
236
363
|
login: false,
|
|
237
364
|
tty: false,
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
365
|
+
yieldTimeMs: SHELL_HELPER_YIELD_MS,
|
|
366
|
+
maxOutputTokens: 128,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function retainedProcessSession(
|
|
371
|
+
session: CommandCancellationSession | undefined,
|
|
372
|
+
providerSessionId: number,
|
|
373
|
+
): CommandCancellationSession | null {
|
|
374
|
+
return session?.hasRetainedProcess?.(providerSessionId) === true ? session : null;
|
|
241
375
|
}
|
|
242
376
|
|
|
243
377
|
function safeProcessIdentity(raw: string): ShellProcessIdentity | null {
|
|
@@ -279,9 +413,12 @@ function identityGuardScript(
|
|
|
279
413
|
].join("\n");
|
|
280
414
|
}
|
|
281
415
|
|
|
282
|
-
function wrapComputer
|
|
283
|
-
|
|
284
|
-
|
|
416
|
+
function wrapComputer<T extends object>(
|
|
417
|
+
computer: T,
|
|
418
|
+
track: <U>(operation: () => Promise<U>) => Promise<U>,
|
|
419
|
+
): T {
|
|
420
|
+
const wrapped = Object.create(Object.getPrototypeOf(computer)) as T & Record<string, unknown>;
|
|
421
|
+
const wrappedRecord = wrapped as Record<string, unknown>;
|
|
285
422
|
Object.assign(wrapped, computer);
|
|
286
423
|
for (const method of [
|
|
287
424
|
"initRun",
|
|
@@ -295,10 +432,10 @@ function wrapComputer(computer: Computer, track: <T>(operation: () => Promise<T>
|
|
|
295
432
|
"keypress",
|
|
296
433
|
"drag",
|
|
297
434
|
] as const) {
|
|
298
|
-
const original = (computer as
|
|
435
|
+
const original = (computer as Record<string, unknown>)[method];
|
|
299
436
|
if (typeof original !== "function") continue;
|
|
300
|
-
|
|
301
|
-
track(async () => await original.apply(computer, args))
|
|
437
|
+
wrappedRecord[method] = (...args: unknown[]) =>
|
|
438
|
+
track(async () => await original.apply(computer, args));
|
|
302
439
|
}
|
|
303
440
|
return wrapped;
|
|
304
441
|
}
|
|
@@ -341,14 +478,30 @@ class TurnToolCancellationControllerImpl implements TurnToolCancellationControll
|
|
|
341
478
|
runSandboxCommand(
|
|
342
479
|
session: TurnSandboxCommandSession,
|
|
343
480
|
args: TurnSandboxCommandArgs,
|
|
481
|
+
): Promise<unknown> {
|
|
482
|
+
return this.runSandboxCommandInternal(session, args, false);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async runSandboxCommandStructured(
|
|
486
|
+
session: TurnSandboxCommandSession,
|
|
487
|
+
args: TurnSandboxCommandArgs,
|
|
488
|
+
): Promise<TurnSandboxCommandResult> {
|
|
489
|
+
return (await this.runSandboxCommandInternal(session, args, true)) as TurnSandboxCommandResult;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
private runSandboxCommandInternal(
|
|
493
|
+
session: TurnSandboxCommandSession,
|
|
494
|
+
args: TurnSandboxCommandArgs,
|
|
495
|
+
structured: boolean,
|
|
344
496
|
): Promise<unknown> {
|
|
345
497
|
return this.track(async () => {
|
|
498
|
+
const startedAt = performance.now();
|
|
346
499
|
if (!session.exec && !session.execCommand) {
|
|
347
500
|
throw new Error("Sandbox session does not support command execution");
|
|
348
501
|
}
|
|
349
502
|
const lifecycleRunContext = {} as Parameters<FunctionToolInvoke>[0];
|
|
350
503
|
|
|
351
|
-
const
|
|
504
|
+
const invokeExecNative = async (input: string): Promise<unknown> => {
|
|
352
505
|
const parsed = parsedObject(input);
|
|
353
506
|
if (!parsed || typeof parsed.cmd !== "string") {
|
|
354
507
|
throw new Error("Sandbox lifecycle command input was invalid");
|
|
@@ -379,26 +532,41 @@ class TurnToolCancellationControllerImpl implements TurnToolCancellationControll
|
|
|
379
532
|
: session.exec
|
|
380
533
|
? await session.exec(commandArgs)
|
|
381
534
|
: await session.execCommand!(commandArgs);
|
|
382
|
-
return
|
|
535
|
+
return result;
|
|
383
536
|
};
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
537
|
+
const invokeExec: FunctionToolInvoke = async (_runContext, input) =>
|
|
538
|
+
nativeCommandBanner(await invokeExecNative(input));
|
|
539
|
+
const invokeWrite: FunctionToolInvoke | null =
|
|
540
|
+
session.writeStdin || session.writeStdinForProcessControl
|
|
541
|
+
? async (_runContext, input) => {
|
|
542
|
+
const parsed = parsedObject(input);
|
|
543
|
+
if (!parsed || typeof parsed.session_id !== "number") {
|
|
544
|
+
throw new Error("Sandbox lifecycle stdin input was invalid");
|
|
545
|
+
}
|
|
546
|
+
const directArgs = {
|
|
547
|
+
sessionId: parsed.session_id,
|
|
548
|
+
...(typeof parsed.chars === "string" ? { chars: parsed.chars } : {}),
|
|
549
|
+
...(typeof parsed.yield_time_ms === "number"
|
|
550
|
+
? { yieldTimeMs: parsed.yield_time_ms }
|
|
551
|
+
: {}),
|
|
552
|
+
...(typeof parsed.max_output_tokens === "number"
|
|
553
|
+
? { maxOutputTokens: parsed.max_output_tokens }
|
|
554
|
+
: {}),
|
|
555
|
+
};
|
|
556
|
+
if (
|
|
557
|
+
session.hasRetainedProcess?.(parsed.session_id) === true &&
|
|
558
|
+
session.writeStdinForProcessControl
|
|
559
|
+
) {
|
|
560
|
+
return await session.writeStdinForProcessControl(directArgs);
|
|
561
|
+
}
|
|
562
|
+
if (!session.writeStdin) {
|
|
563
|
+
throw new Error(
|
|
564
|
+
"Sandbox lifecycle command yielded an unretained session without stdin support",
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
return await session.writeStdin!(directArgs);
|
|
389
568
|
}
|
|
390
|
-
|
|
391
|
-
sessionId: parsed.session_id,
|
|
392
|
-
...(typeof parsed.chars === "string" ? { chars: parsed.chars } : {}),
|
|
393
|
-
...(typeof parsed.yield_time_ms === "number"
|
|
394
|
-
? { yieldTimeMs: parsed.yield_time_ms }
|
|
395
|
-
: {}),
|
|
396
|
-
...(typeof parsed.max_output_tokens === "number"
|
|
397
|
-
? { maxOutputTokens: parsed.max_output_tokens }
|
|
398
|
-
: {}),
|
|
399
|
-
});
|
|
400
|
-
}
|
|
401
|
-
: null;
|
|
569
|
+
: null;
|
|
402
570
|
const correlationId = `turn_lifecycle_${crypto.randomUUID()}`;
|
|
403
571
|
const useRemoteOpCancellation =
|
|
404
572
|
Boolean(session.cancelExecCommand) && session.supportsPty?.() === false;
|
|
@@ -423,19 +591,49 @@ class TurnToolCancellationControllerImpl implements TurnToolCancellationControll
|
|
|
423
591
|
...(args.maxOutputTokens !== undefined ? { max_output_tokens: args.maxOutputTokens } : {}),
|
|
424
592
|
});
|
|
425
593
|
|
|
426
|
-
let
|
|
594
|
+
let initialNative: Awaited<ReturnType<FunctionToolInvoke>>;
|
|
427
595
|
try {
|
|
428
|
-
|
|
596
|
+
initialNative = await runWithToolCallCorrelation(
|
|
429
597
|
correlationId,
|
|
430
|
-
async () => await
|
|
598
|
+
async () => await invokeExecNative(commandInput),
|
|
431
599
|
);
|
|
432
600
|
} finally {
|
|
433
601
|
remoteExec?.settle();
|
|
434
602
|
}
|
|
435
|
-
|
|
603
|
+
const initial = nativeCommandBanner(initialNative);
|
|
604
|
+
const initialResult = nativeCommandResult(initialNative);
|
|
605
|
+
if (useRemoteOpCancellation) {
|
|
606
|
+
if (!structured) return initial;
|
|
607
|
+
if (initialResult.exitCode === null) {
|
|
608
|
+
throw new Error("Sandbox command did not report a terminal exit code");
|
|
609
|
+
}
|
|
610
|
+
return {
|
|
611
|
+
stdout: initialResult.stdout,
|
|
612
|
+
stderr: initialResult.stderr,
|
|
613
|
+
exitCode: initialResult.exitCode,
|
|
614
|
+
wallTimeSeconds: Math.max(
|
|
615
|
+
initialResult.wallTimeSeconds,
|
|
616
|
+
(performance.now() - startedAt) / 1_000,
|
|
617
|
+
),
|
|
618
|
+
} satisfies TurnSandboxCommandResult;
|
|
619
|
+
}
|
|
436
620
|
|
|
437
621
|
const sessionId = parseExecBannerSessionId(initial);
|
|
438
|
-
if (sessionId === null)
|
|
622
|
+
if (sessionId === null) {
|
|
623
|
+
if (!structured) return initial;
|
|
624
|
+
if (initialResult.exitCode === null) {
|
|
625
|
+
throw new Error("Sandbox command did not report a terminal exit code");
|
|
626
|
+
}
|
|
627
|
+
return {
|
|
628
|
+
stdout: initialResult.stdout,
|
|
629
|
+
stderr: initialResult.stderr,
|
|
630
|
+
exitCode: initialResult.exitCode,
|
|
631
|
+
wallTimeSeconds: Math.max(
|
|
632
|
+
initialResult.wallTimeSeconds,
|
|
633
|
+
(performance.now() - startedAt) / 1_000,
|
|
634
|
+
),
|
|
635
|
+
} satisfies TurnSandboxCommandResult;
|
|
636
|
+
}
|
|
439
637
|
if (!invokeWrite) {
|
|
440
638
|
throw new Error("Sandbox lifecycle command yielded without stdin support");
|
|
441
639
|
}
|
|
@@ -447,6 +645,7 @@ class TurnToolCancellationControllerImpl implements TurnToolCancellationControll
|
|
|
447
645
|
runContext: lifecycleRunContext,
|
|
448
646
|
execInvoke: invokeExec,
|
|
449
647
|
writeInvoke: invokeWrite,
|
|
648
|
+
processSession: retainedProcessSession(session, sessionId),
|
|
450
649
|
identity: null,
|
|
451
650
|
identityValidated: false,
|
|
452
651
|
cancellation: null,
|
|
@@ -455,6 +654,8 @@ class TurnToolCancellationControllerImpl implements TurnToolCancellationControll
|
|
|
455
654
|
const maxOutputTokens = args.maxOutputTokens ?? 20_000;
|
|
456
655
|
const initialOutput = execOutput(initial);
|
|
457
656
|
let output = initialOutput ? appendBoundedOutput("", initialOutput, maxOutputTokens) : "";
|
|
657
|
+
let stdout = appendBoundedOutput("", initialResult.stdout, maxOutputTokens);
|
|
658
|
+
const stderr = appendBoundedOutput("", initialResult.stderr, maxOutputTokens);
|
|
458
659
|
while (true) {
|
|
459
660
|
if (this.cancelled) throw cancellationError(this.reason);
|
|
460
661
|
const next = await invokeWrite(
|
|
@@ -471,11 +672,20 @@ class TurnToolCancellationControllerImpl implements TurnToolCancellationControll
|
|
|
471
672
|
throw new Error("Sandbox lifecycle stdin returned an invalid result");
|
|
472
673
|
}
|
|
473
674
|
const nextOutput = execOutput(next);
|
|
474
|
-
if (nextOutput)
|
|
675
|
+
if (nextOutput) {
|
|
676
|
+
output = appendBoundedOutput(output, nextOutput, maxOutputTokens);
|
|
677
|
+
stdout = appendBoundedOutput(stdout, nextOutput, maxOutputTokens);
|
|
678
|
+
}
|
|
475
679
|
const exitCode = parseExecBannerExitCode(next);
|
|
476
680
|
if (exitCode !== null) {
|
|
477
681
|
this.shellSessions.delete(sessionId);
|
|
478
|
-
return completedCommandBanner(exitCode, output);
|
|
682
|
+
if (!structured) return completedCommandBanner(exitCode, output);
|
|
683
|
+
return {
|
|
684
|
+
stdout,
|
|
685
|
+
stderr,
|
|
686
|
+
exitCode,
|
|
687
|
+
wallTimeSeconds: (performance.now() - startedAt) / 1_000,
|
|
688
|
+
} satisfies TurnSandboxCommandResult;
|
|
479
689
|
}
|
|
480
690
|
// A conforming provider blocks for the requested yield. This fallback
|
|
481
691
|
// prevents a non-conforming implementation from turning a long setup
|
|
@@ -485,19 +695,21 @@ class TurnToolCancellationControllerImpl implements TurnToolCancellationControll
|
|
|
485
695
|
});
|
|
486
696
|
}
|
|
487
697
|
|
|
488
|
-
wrapTools(tools:
|
|
489
|
-
|
|
698
|
+
wrapTools<T>(tools: T[], session?: CommandCancellationSession): T[] {
|
|
699
|
+
const structuralTools = tools as unknown as Array<Record<string, unknown>>;
|
|
700
|
+
for (const tool of structuralTools) {
|
|
490
701
|
if (tool.type !== "function") continue;
|
|
491
|
-
|
|
492
|
-
if (
|
|
702
|
+
const functionTool = tool as FunctionTool;
|
|
703
|
+
if (functionTool.name === "exec_command") this.rawExecInvoke = functionTool.invoke;
|
|
704
|
+
if (functionTool.name === "write_stdin") this.rawWriteInvoke = functionTool.invoke;
|
|
493
705
|
}
|
|
494
706
|
|
|
495
|
-
return
|
|
496
|
-
if (tool.type === "function") return this.wrapFunctionTool(tool, session);
|
|
497
|
-
if (tool.type === "apply_patch") return this.wrapApplyPatchTool(tool);
|
|
498
|
-
if (tool.type === "computer") return this.wrapComputerTool(tool);
|
|
707
|
+
return structuralTools.map((tool) => {
|
|
708
|
+
if (tool.type === "function") return this.wrapFunctionTool(tool as FunctionTool, session);
|
|
709
|
+
if (tool.type === "apply_patch") return this.wrapApplyPatchTool(tool as ApplyPatchTool);
|
|
710
|
+
if (tool.type === "computer") return this.wrapComputerTool(tool as ComputerTool);
|
|
499
711
|
return tool;
|
|
500
|
-
});
|
|
712
|
+
}) as unknown as T[];
|
|
501
713
|
}
|
|
502
714
|
|
|
503
715
|
private wrapFunctionTool(
|
|
@@ -560,6 +772,7 @@ class TurnToolCancellationControllerImpl implements TurnToolCancellationControll
|
|
|
560
772
|
runContext,
|
|
561
773
|
execInvoke: tool.invoke,
|
|
562
774
|
writeInvoke: this.rawWriteInvoke,
|
|
775
|
+
processSession: retainedProcessSession(cancellationSession, sessionId),
|
|
563
776
|
identity: null,
|
|
564
777
|
identityValidated: false,
|
|
565
778
|
cancellation: null,
|
|
@@ -588,7 +801,19 @@ class TurnToolCancellationControllerImpl implements TurnToolCancellationControll
|
|
|
588
801
|
yield_time_ms: cappedYield(parsed.yield_time_ms, TURN_WRITE_YIELD_MS),
|
|
589
802
|
})
|
|
590
803
|
: input;
|
|
591
|
-
const
|
|
804
|
+
const directProcessSession =
|
|
805
|
+
sessionId === null ? null : retainedProcessSession(cancellationSession, sessionId);
|
|
806
|
+
const output =
|
|
807
|
+
sessionId !== null && directProcessSession?.writeStdinForProcessMutation
|
|
808
|
+
? await directProcessSession.writeStdinForProcessMutation({
|
|
809
|
+
sessionId,
|
|
810
|
+
...(typeof parsed?.chars === "string" ? { chars: parsed.chars } : {}),
|
|
811
|
+
yieldTimeMs: cappedYield(parsed?.yield_time_ms, TURN_WRITE_YIELD_MS),
|
|
812
|
+
...(typeof parsed?.max_output_tokens === "number"
|
|
813
|
+
? { maxOutputTokens: parsed.max_output_tokens }
|
|
814
|
+
: {}),
|
|
815
|
+
})
|
|
816
|
+
: await tool.invoke(runContext, cappedInput, details);
|
|
592
817
|
if (sessionId !== null && typeof output === "string") {
|
|
593
818
|
if (isExecSessionLostBanner(output, sessionId)) {
|
|
594
819
|
// Modal reports a vanished exec session as a non-throwing
|
|
@@ -605,6 +830,7 @@ class TurnToolCancellationControllerImpl implements TurnToolCancellationControll
|
|
|
605
830
|
runContext,
|
|
606
831
|
execInvoke: this.rawExecInvoke ?? tool.invoke,
|
|
607
832
|
writeInvoke: tool.invoke,
|
|
833
|
+
processSession: directProcessSession,
|
|
608
834
|
identity: null,
|
|
609
835
|
identityValidated: false,
|
|
610
836
|
cancellation: null,
|
|
@@ -626,9 +852,9 @@ class TurnToolCancellationControllerImpl implements TurnToolCancellationControll
|
|
|
626
852
|
};
|
|
627
853
|
}
|
|
628
854
|
|
|
629
|
-
private wrapApplyPatchTool(tool:
|
|
855
|
+
private wrapApplyPatchTool(tool: ApplyPatchTool): ApplyPatchTool {
|
|
630
856
|
const editor = tool.editor;
|
|
631
|
-
const wrappedEditor:
|
|
857
|
+
const wrappedEditor: ApplyPatchEditor = {
|
|
632
858
|
createFile: (operation, context) =>
|
|
633
859
|
this.track(async () => await editor.createFile(operation, context)),
|
|
634
860
|
updateFile: (operation, context) =>
|
|
@@ -639,33 +865,37 @@ class TurnToolCancellationControllerImpl implements TurnToolCancellationControll
|
|
|
639
865
|
return { ...tool, editor: wrappedEditor };
|
|
640
866
|
}
|
|
641
867
|
|
|
642
|
-
private wrapComputerTool(tool:
|
|
868
|
+
private wrapComputerTool(tool: ComputerTool): ComputerTool {
|
|
643
869
|
const computer = tool.computer;
|
|
644
870
|
if (typeof computer === "function") {
|
|
871
|
+
const createComputer = computer as (...args: unknown[]) => object | Promise<object>;
|
|
645
872
|
return {
|
|
646
873
|
...tool,
|
|
647
|
-
computer: async (...args:
|
|
648
|
-
wrapComputer(await this.track(async () => await
|
|
874
|
+
computer: async (...args: unknown[]) =>
|
|
875
|
+
wrapComputer(await this.track(async () => await createComputer(...args)), (operation) =>
|
|
649
876
|
this.track(operation),
|
|
650
877
|
),
|
|
651
878
|
};
|
|
652
879
|
}
|
|
653
|
-
if (
|
|
880
|
+
if (computer && typeof computer === "object" && "create" in computer) {
|
|
881
|
+
const provider = computer as Record<string, unknown>;
|
|
882
|
+
if (typeof provider.create !== "function") return tool;
|
|
883
|
+
const createComputer = provider.create as (...args: unknown[]) => object | Promise<object>;
|
|
654
884
|
return {
|
|
655
885
|
...tool,
|
|
656
886
|
computer: {
|
|
657
|
-
...
|
|
658
|
-
create: async (...args:
|
|
659
|
-
wrapComputer(
|
|
660
|
-
|
|
661
|
-
(operation) => this.track(operation),
|
|
887
|
+
...provider,
|
|
888
|
+
create: async (...args: unknown[]) =>
|
|
889
|
+
wrapComputer(await this.track(async () => await createComputer(...args)), (operation) =>
|
|
890
|
+
this.track(operation),
|
|
662
891
|
),
|
|
663
892
|
},
|
|
664
893
|
};
|
|
665
894
|
}
|
|
895
|
+
if (!computer || typeof computer !== "object") return tool;
|
|
666
896
|
return {
|
|
667
897
|
...tool,
|
|
668
|
-
computer: wrapComputer(computer
|
|
898
|
+
computer: wrapComputer(computer, (operation) => this.track(operation)),
|
|
669
899
|
};
|
|
670
900
|
}
|
|
671
901
|
|
|
@@ -776,8 +1006,11 @@ class TurnToolCancellationControllerImpl implements TurnToolCancellationControll
|
|
|
776
1006
|
// authority available for that legacy shape.
|
|
777
1007
|
if (!state.markerPath) {
|
|
778
1008
|
while (true) {
|
|
779
|
-
if (
|
|
780
|
-
|
|
1009
|
+
if (
|
|
1010
|
+
(state.writeInvoke || state.processSession?.writeStdinForProcessControl) &&
|
|
1011
|
+
(await this.rawWrite(state, "\u0003", SHELL_POLL_MS))
|
|
1012
|
+
) {
|
|
1013
|
+
await this.forgetShellSessionAfterExactSettlement(state);
|
|
781
1014
|
return;
|
|
782
1015
|
}
|
|
783
1016
|
await delay(SHELL_POLL_MS);
|
|
@@ -790,10 +1023,10 @@ class TurnToolCancellationControllerImpl implements TurnToolCancellationControll
|
|
|
790
1023
|
while (!state.identity) {
|
|
791
1024
|
state.identity = await this.readIdentity(state);
|
|
792
1025
|
if (state.identity) break;
|
|
793
|
-
if (state.writeInvoke) {
|
|
1026
|
+
if (state.writeInvoke || state.processSession?.writeStdinForProcessControl) {
|
|
794
1027
|
const completed = await this.rawWrite(state, "", SHELL_POLL_MS);
|
|
795
1028
|
if (completed) {
|
|
796
|
-
this.
|
|
1029
|
+
await this.forgetShellSessionAfterExactSettlement(state);
|
|
797
1030
|
return;
|
|
798
1031
|
}
|
|
799
1032
|
}
|
|
@@ -801,35 +1034,55 @@ class TurnToolCancellationControllerImpl implements TurnToolCancellationControll
|
|
|
801
1034
|
}
|
|
802
1035
|
|
|
803
1036
|
if (!(await this.identityAlive(state))) {
|
|
804
|
-
this.
|
|
1037
|
+
await this.forgetShellSessionAfterExactSettlement(state);
|
|
805
1038
|
return;
|
|
806
1039
|
}
|
|
807
1040
|
|
|
808
|
-
|
|
1041
|
+
// A retained provider session must remain pinned until its process group is
|
|
1042
|
+
// absent. Polling it first could settle/delete the durable route while an
|
|
1043
|
+
// ignored-signal descendant still lives, making the exact helper backend
|
|
1044
|
+
// unavailable. Generic legacy sessions retain their Ctrl-C fast path.
|
|
1045
|
+
if (!state.processSession && state.writeInvoke) {
|
|
809
1046
|
await this.rawWrite(state, "\u0003", SHELL_POLL_MS);
|
|
810
1047
|
if (!(await this.identityAlive(state))) {
|
|
811
|
-
this.
|
|
1048
|
+
await this.forgetShellSessionAfterExactSettlement(state);
|
|
812
1049
|
return;
|
|
813
1050
|
}
|
|
814
1051
|
}
|
|
815
1052
|
|
|
816
1053
|
await this.signalIdentity(state, "TERM");
|
|
817
1054
|
for (let attempt = 0; attempt < SHELL_GRACEFUL_POLLS; attempt++) {
|
|
818
|
-
if (state.
|
|
1055
|
+
if (!state.processSession && state.writeInvoke) {
|
|
1056
|
+
await this.rawWrite(state, "", SHELL_POLL_MS);
|
|
1057
|
+
}
|
|
819
1058
|
if (!(await this.identityAlive(state))) {
|
|
820
|
-
this.
|
|
1059
|
+
await this.forgetShellSessionAfterExactSettlement(state);
|
|
821
1060
|
return;
|
|
822
1061
|
}
|
|
823
1062
|
}
|
|
824
1063
|
|
|
825
1064
|
await this.signalIdentity(state, "KILL");
|
|
826
1065
|
while (await this.identityAlive(state)) {
|
|
827
|
-
if (state.
|
|
1066
|
+
if (!state.processSession && state.writeInvoke) {
|
|
1067
|
+
await this.rawWrite(state, "", SHELL_POLL_MS);
|
|
1068
|
+
}
|
|
828
1069
|
// A transient helper failure must not turn the first KILL into a single
|
|
829
1070
|
// best-effort shot. Re-issue it until the process group is provably gone.
|
|
830
1071
|
await this.signalIdentity(state, "KILL");
|
|
831
1072
|
await delay(SHELL_POLL_MS);
|
|
832
1073
|
}
|
|
1074
|
+
await this.forgetShellSessionAfterExactSettlement(state);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
private async forgetShellSessionAfterExactSettlement(state: ActiveShellSession): Promise<void> {
|
|
1078
|
+
// OS process-group absence is necessary but not sufficient for a routed
|
|
1079
|
+
// retained process. One final exact provider-session poll must publish an
|
|
1080
|
+
// exit/loss proof and durably settle the process holder before tracking is
|
|
1081
|
+
// removed. Settlement failure keeps hasRetainedProcess true and retries.
|
|
1082
|
+
while (state.processSession?.hasRetainedProcess?.(state.sessionId) === true) {
|
|
1083
|
+
if (await this.rawWrite(state, "", SHELL_POLL_MS)) break;
|
|
1084
|
+
await delay(SHELL_POLL_MS);
|
|
1085
|
+
}
|
|
833
1086
|
this.shellSessions.delete(state.sessionId);
|
|
834
1087
|
}
|
|
835
1088
|
|
|
@@ -838,18 +1091,25 @@ class TurnToolCancellationControllerImpl implements TurnToolCancellationControll
|
|
|
838
1091
|
chars: string,
|
|
839
1092
|
yieldTimeMs: number,
|
|
840
1093
|
): Promise<boolean> {
|
|
841
|
-
if (!state.writeInvoke) return false;
|
|
1094
|
+
if (!state.writeInvoke && !state.processSession?.writeStdinForProcessControl) return false;
|
|
842
1095
|
try {
|
|
843
|
-
const output =
|
|
844
|
-
state.
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
1096
|
+
const output = state.processSession?.writeStdinForProcessControl
|
|
1097
|
+
? await state.processSession.writeStdinForProcessControl({
|
|
1098
|
+
sessionId: state.sessionId,
|
|
1099
|
+
chars,
|
|
1100
|
+
yieldTimeMs,
|
|
1101
|
+
maxOutputTokens: 128,
|
|
1102
|
+
})
|
|
1103
|
+
: await state.writeInvoke!(
|
|
1104
|
+
state.runContext,
|
|
1105
|
+
JSON.stringify({
|
|
1106
|
+
session_id: state.sessionId,
|
|
1107
|
+
chars,
|
|
1108
|
+
yield_time_ms: yieldTimeMs,
|
|
1109
|
+
max_output_tokens: 128,
|
|
1110
|
+
}),
|
|
1111
|
+
undefined,
|
|
1112
|
+
);
|
|
853
1113
|
if (typeof output !== "string") return false;
|
|
854
1114
|
// Marker files can already be gone when a process exited outside the
|
|
855
1115
|
// model-facing write path. Modal's exact matching lost-session banner is
|
|
@@ -868,11 +1128,13 @@ class TurnToolCancellationControllerImpl implements TurnToolCancellationControll
|
|
|
868
1128
|
private async readIdentity(state: ActiveShellSession): Promise<ShellProcessIdentity | null> {
|
|
869
1129
|
if (!state.markerPath) return null;
|
|
870
1130
|
try {
|
|
871
|
-
const
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
1131
|
+
const command = `command cat ${singleQuote(state.markerPath)} 2>/dev/null`;
|
|
1132
|
+
const output = state.processSession?.execCommandForProcessControl
|
|
1133
|
+
? await state.processSession.execCommandForProcessControl(
|
|
1134
|
+
state.sessionId,
|
|
1135
|
+
shellHelperArgs(command),
|
|
1136
|
+
)
|
|
1137
|
+
: await state.execInvoke(state.runContext, shellHelperInput(command), undefined);
|
|
876
1138
|
return typeof output === "string" ? safeProcessIdentity(output) : null;
|
|
877
1139
|
} catch {
|
|
878
1140
|
return null;
|
|
@@ -892,7 +1154,12 @@ class TurnToolCancellationControllerImpl implements TurnToolCancellationControll
|
|
|
892
1154
|
76,
|
|
893
1155
|
);
|
|
894
1156
|
try {
|
|
895
|
-
const output =
|
|
1157
|
+
const output = state.processSession?.execCommandForProcessControl
|
|
1158
|
+
? await state.processSession.execCommandForProcessControl(
|
|
1159
|
+
state.sessionId,
|
|
1160
|
+
shellHelperArgs(script),
|
|
1161
|
+
)
|
|
1162
|
+
: await state.execInvoke(state.runContext, shellHelperInput(script), undefined);
|
|
896
1163
|
if (typeof output !== "string") return true;
|
|
897
1164
|
const exitCode = parseExecBannerExitCode(output);
|
|
898
1165
|
if (exitCode === 75) {
|
|
@@ -914,7 +1181,14 @@ class TurnToolCancellationControllerImpl implements TurnToolCancellationControll
|
|
|
914
1181
|
76,
|
|
915
1182
|
);
|
|
916
1183
|
try {
|
|
917
|
-
|
|
1184
|
+
if (state.processSession?.execCommandForProcessControl) {
|
|
1185
|
+
await state.processSession.execCommandForProcessControl(
|
|
1186
|
+
state.sessionId,
|
|
1187
|
+
shellHelperArgs(script),
|
|
1188
|
+
);
|
|
1189
|
+
} else {
|
|
1190
|
+
await state.execInvoke(state.runContext, shellHelperInput(script), undefined);
|
|
1191
|
+
}
|
|
918
1192
|
} catch {
|
|
919
1193
|
// Confirmation is performed by identityAlive(). A failed signal helper
|
|
920
1194
|
// never opens the fence; it is retried/escalated until the process is gone.
|
|
@@ -930,7 +1204,7 @@ export function createTurnToolCancellationController(
|
|
|
930
1204
|
|
|
931
1205
|
/** Wrap a capability instance without depending on SDK-private subclasses. */
|
|
932
1206
|
export function wrapCapabilityToolsForTurnCancellation(
|
|
933
|
-
capability: { tools():
|
|
1207
|
+
capability: { tools(): unknown[]; _session?: CommandCancellationSession },
|
|
934
1208
|
controller: TurnToolCancellationController,
|
|
935
1209
|
): void {
|
|
936
1210
|
const originalTools = capability.tools;
|