openpond-sdk 0.0.2 → 0.0.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/README.md +61 -14
- package/dist/index.js +340 -59
- package/dist/index.js.map +2 -2
- package/dist/types/packages/sdk/src/index.d.ts +1 -1
- package/dist/types/packages/sdk/src/index.d.ts.map +1 -1
- package/dist/types/packages/sdk/src/work.d.ts +66 -1
- package/dist/types/packages/sdk/src/work.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `openpond-sdk`
|
|
2
2
|
|
|
3
|
-
The OpenPond SDK is the server-side TypeScript client for running agentic work in OpenPond sandboxes. It gives Node.js applications, Next.js route handlers, workers, and backend services a small API for creating sandboxes, executing commands, managing files and runtimes, and running a model/tool loop in
|
|
3
|
+
The OpenPond SDK is the server-side TypeScript client for running agentic work in OpenPond sandboxes. It gives Node.js applications, Next.js route handlers, workers, and backend services a small API for creating sandboxes, executing commands, managing files and runtimes, and running a model/tool loop in an isolated workspace.
|
|
4
4
|
|
|
5
5
|
OpenPond is an open-source agent orchestration system for doing durable work with any model, provider, or subscription. The desktop app, CLI/TUI, and this SDK live in the same repository and share the sandbox client implementation. Desktop builds use the workspace source directly; installing this package from npm is only for external applications.
|
|
6
6
|
|
|
@@ -27,13 +27,43 @@ const openpond = createOpenPondClient({
|
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
export async function POST(request: Request) {
|
|
30
|
-
const { prompt
|
|
31
|
-
const result = await openpond.work.run({
|
|
30
|
+
const { prompt } = await request.json();
|
|
31
|
+
const result = await openpond.work.run({
|
|
32
|
+
prompt,
|
|
33
|
+
cleanup: "delete",
|
|
34
|
+
persistOutput: async ({ output, download }) => {
|
|
35
|
+
const response = await download();
|
|
36
|
+
const bytes = Buffer.from(response.file.contentsBase64, "base64");
|
|
37
|
+
await durableOutputStore.put({ output, bytes });
|
|
38
|
+
},
|
|
39
|
+
});
|
|
32
40
|
return Response.json(result);
|
|
33
41
|
}
|
|
34
42
|
```
|
|
35
43
|
|
|
36
|
-
`work.run` creates a sandbox when `sandboxId` is omitted.
|
|
44
|
+
`work.run` creates a sandbox when `sandboxId` is omitted. Use `onEvent` to stream sandbox, model, command, persistence, and cleanup progress to a client. Keep API keys and the persistence callback in server code.
|
|
45
|
+
|
|
46
|
+
Completed files written under `/workspace/outputs` are collected automatically. The model does not need to publish or register them. Each detected file is emitted as an `output` event and returned in `result.outputs`:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
const result = await openpond.work.run({
|
|
50
|
+
prompt: "Create a DOCX summary",
|
|
51
|
+
onEvent(event) {
|
|
52
|
+
if (event.type === "output") console.log(event.output.name);
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
for (const output of result.outputs) {
|
|
57
|
+
const downloaded = await openpond.work.downloadOutput(
|
|
58
|
+
result.sandboxId,
|
|
59
|
+
output,
|
|
60
|
+
);
|
|
61
|
+
const bytes = Buffer.from(downloaded.file.contentsBase64, "base64");
|
|
62
|
+
// Stream bytes from your authenticated server route.
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Output descriptors include the sandbox path, filename, MIME type, size, modification time, and preview hints. `downloadOutput` remains available when the sandbox is kept. For ephemeral Work, use the lazy `download` function inside `persistOutput`; it verifies that the complete file arrived before deletion can begin.
|
|
37
67
|
|
|
38
68
|
If sandbox execution is unavailable, the API fails with the stable `sandbox_runner_unavailable` error instead of returning a successful command result.
|
|
39
69
|
|
|
@@ -66,26 +96,43 @@ console.log(result.command.output);
|
|
|
66
96
|
|
|
67
97
|
The package also exports `createOpenPondSandboxClient`, all public sandbox input and response types, and the OpChat helpers used by the Work loop.
|
|
68
98
|
|
|
69
|
-
##
|
|
99
|
+
## Lifecycle, persistence, and cleanup
|
|
70
100
|
|
|
71
|
-
|
|
101
|
+
The generic SDK defaults to `cleanup: "keep"` for backwards compatibility. Applications can choose one of three explicit terminal policies:
|
|
72
102
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
103
|
+
- `keep` leaves the sandbox running and makes the caller responsible for cleanup.
|
|
104
|
+
- `stop` releases active compute while retaining the sandbox for deliberate resume.
|
|
105
|
+
- `delete` removes ephemeral compute after output persistence succeeds.
|
|
106
|
+
|
|
107
|
+
Deleting a turn that produced outputs requires an awaited `persistOutput` callback. If an application intentionally does not need the files, it must say so with `discardOutputs: true`. A persistence failure stops the sandbox instead of deleting recoverable output state. `result.lifecycle` and `persistence`/`cleanup` events expose the ordering and final observed state.
|
|
108
|
+
|
|
109
|
+
For a follow-up turn on fresh compute, stage selected durable outputs as structured inputs:
|
|
77
110
|
|
|
78
|
-
|
|
111
|
+
```ts
|
|
112
|
+
await openpond.work.run({
|
|
113
|
+
prompt: "Revise the report",
|
|
114
|
+
cleanup: "delete",
|
|
115
|
+
inputs: [{
|
|
116
|
+
id: savedOutput.id,
|
|
117
|
+
name: savedOutput.name,
|
|
118
|
+
contentsBase64: savedOutput.contentsBase64,
|
|
119
|
+
mimeType: savedOutput.mimeType,
|
|
120
|
+
checksumSha256: savedOutput.sha256,
|
|
121
|
+
revision: savedOutput.revision,
|
|
122
|
+
}],
|
|
123
|
+
persistOutput: saveOutput,
|
|
124
|
+
});
|
|
125
|
+
```
|
|
79
126
|
|
|
80
|
-
|
|
127
|
+
Inputs are placed under `/workspace/inputs/previous-outputs/` with a structured manifest at `/workspace/inputs/.openpond-context.json`. Arbitrary scratch files are not retained by ephemeral Work.
|
|
81
128
|
|
|
82
|
-
|
|
129
|
+
You can still delete a caller-managed sandbox directly:
|
|
83
130
|
|
|
84
131
|
```ts
|
|
85
132
|
await openpond.work.deleteSandbox(sandboxId);
|
|
86
133
|
```
|
|
87
134
|
|
|
88
|
-
Use conservative budgets and application-level retention. API keys, provider credentials, and bypass secrets must remain in server-side configuration.
|
|
135
|
+
The sandbox's 15-minute idle timeout is crash protection, not the normal successful-turn cleanup path. Use conservative budgets and application-level retention. API keys, provider credentials, and bypass secrets must remain in server-side configuration.
|
|
89
136
|
|
|
90
137
|
## Development
|
|
91
138
|
|
package/dist/index.js
CHANGED
|
@@ -2321,6 +2321,12 @@ async function* streamHostedChatTurn(options) {
|
|
|
2321
2321
|
var DEFAULT_MODEL = "openpond-chat";
|
|
2322
2322
|
var DEFAULT_MAX_STEPS = 24;
|
|
2323
2323
|
var MAX_TOOL_OUTPUT_CHARS = 4e4;
|
|
2324
|
+
var WORK_OUTPUT_DIRECTORY = "/workspace/outputs";
|
|
2325
|
+
var WORK_INPUT_DIRECTORY = "/workspace/inputs/previous-outputs";
|
|
2326
|
+
var WORK_INPUT_MANIFEST = "/workspace/inputs/.openpond-context.json";
|
|
2327
|
+
var MAX_WORK_OUTPUTS = 100;
|
|
2328
|
+
var MAX_WORK_INPUTS = 100;
|
|
2329
|
+
var MAX_WORK_INPUT_BYTES = 100 * 1024 * 1024;
|
|
2324
2330
|
var OpenPondWorkClient = class {
|
|
2325
2331
|
#apiKey;
|
|
2326
2332
|
#apiBaseUrl;
|
|
@@ -2337,72 +2343,242 @@ var OpenPondWorkClient = class {
|
|
|
2337
2343
|
if (!prompt) throw new Error("Work prompt is required");
|
|
2338
2344
|
input.signal?.throwIfAborted();
|
|
2339
2345
|
const emit = async (event) => input.onEvent?.(event);
|
|
2340
|
-
|
|
2341
|
-
const
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
state: sandbox.state
|
|
2346
|
-
});
|
|
2347
|
-
const messages = [
|
|
2348
|
-
{ role: "system", content: systemPrompt(sandbox.id) },
|
|
2349
|
-
...(input.history ?? []).map((message) => ({ ...message })),
|
|
2350
|
-
{ role: "user", content: prompt }
|
|
2351
|
-
];
|
|
2346
|
+
const cleanupPolicy = input.cleanup ?? "keep";
|
|
2347
|
+
const lifecycle = initialLifecycle(cleanupPolicy);
|
|
2348
|
+
let sandbox = null;
|
|
2349
|
+
let outputBaseline = null;
|
|
2350
|
+
let finalizationAttempted = false;
|
|
2352
2351
|
const maxSteps = boundedInteger(input.maxSteps, 1, 100, DEFAULT_MAX_STEPS);
|
|
2353
2352
|
let finalText = "";
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
await
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
messages,
|
|
2362
|
-
tools: WORK_TOOLS,
|
|
2363
|
-
toolChoice: "auto",
|
|
2364
|
-
signal: input.signal,
|
|
2365
|
-
metadata: {
|
|
2366
|
-
source: "openpond-sdk-work",
|
|
2367
|
-
sandboxId: sandbox.id,
|
|
2368
|
-
apiBaseUrl: this.#apiBaseUrl,
|
|
2369
|
-
...input.metadata
|
|
2370
|
-
}
|
|
2371
|
-
});
|
|
2372
|
-
const choice = completion.choices?.[0];
|
|
2373
|
-
const assistantText = choice?.message?.content?.trim() ?? "";
|
|
2374
|
-
const toolCalls = choice?.message?.tool_calls ?? [];
|
|
2375
|
-
messages.push({
|
|
2376
|
-
role: "assistant",
|
|
2377
|
-
content: assistantText || null,
|
|
2378
|
-
...toolCalls.length > 0 ? { tool_calls: toolCalls } : {}
|
|
2353
|
+
try {
|
|
2354
|
+
await emit({ type: "status", message: "Preparing sandbox" });
|
|
2355
|
+
sandbox = await this.#resolveSandbox(input);
|
|
2356
|
+
await emit({
|
|
2357
|
+
type: "sandbox",
|
|
2358
|
+
sandboxId: sandbox.id,
|
|
2359
|
+
state: sandbox.state
|
|
2379
2360
|
});
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2361
|
+
await this.#stageInputs(sandbox.id, input.inputs ?? []);
|
|
2362
|
+
outputBaseline = await this.#prepareOutputDirectory(sandbox.id);
|
|
2363
|
+
const messages = [
|
|
2364
|
+
{
|
|
2365
|
+
role: "system",
|
|
2366
|
+
content: systemPrompt(sandbox.id, (input.inputs?.length ?? 0) > 0)
|
|
2367
|
+
},
|
|
2368
|
+
...(input.history ?? []).map((message) => ({ ...message })),
|
|
2369
|
+
{ role: "user", content: prompt }
|
|
2370
|
+
];
|
|
2371
|
+
for (let step = 1; step <= maxSteps; step += 1) {
|
|
2372
|
+
input.signal?.throwIfAborted();
|
|
2373
|
+
await emit({ type: "status", message: `Thinking \xB7 step ${step}` });
|
|
2374
|
+
const completion = await sendHostedChatTurn({
|
|
2375
|
+
apiBaseUrl: this.#chatApiBaseUrl,
|
|
2376
|
+
token: this.#apiKey,
|
|
2377
|
+
model: input.model?.trim() || DEFAULT_MODEL,
|
|
2378
|
+
messages,
|
|
2379
|
+
tools: WORK_TOOLS,
|
|
2380
|
+
toolChoice: "auto",
|
|
2381
|
+
signal: input.signal,
|
|
2382
|
+
metadata: {
|
|
2383
|
+
source: "openpond-sdk-work",
|
|
2384
|
+
sandboxId: sandbox.id,
|
|
2385
|
+
apiBaseUrl: this.#apiBaseUrl,
|
|
2386
|
+
...input.metadata
|
|
2387
|
+
}
|
|
2388
|
+
});
|
|
2389
|
+
const choice = completion.choices?.[0];
|
|
2390
|
+
const assistantText = choice?.message?.content?.trim() ?? "";
|
|
2391
|
+
const toolCalls = choice?.message?.tool_calls ?? [];
|
|
2392
|
+
messages.push({
|
|
2393
|
+
role: "assistant",
|
|
2394
|
+
content: assistantText || null,
|
|
2395
|
+
...toolCalls.length > 0 ? { tool_calls: toolCalls } : {}
|
|
2396
|
+
});
|
|
2397
|
+
if (assistantText) {
|
|
2398
|
+
finalText = assistantText;
|
|
2399
|
+
await emit({ type: "assistant", text: assistantText });
|
|
2400
|
+
}
|
|
2401
|
+
if (toolCalls.length === 0) {
|
|
2402
|
+
const outputs = await this.#collectOutputs(sandbox.id, outputBaseline);
|
|
2403
|
+
for (const output of outputs) await emit({ type: "output", output });
|
|
2404
|
+
finalizationAttempted = true;
|
|
2405
|
+
await this.#finalizeSandbox(sandbox.id, outputs, input, lifecycle, emit);
|
|
2406
|
+
const result = {
|
|
2407
|
+
sandboxId: sandbox.id,
|
|
2408
|
+
text: finalText,
|
|
2409
|
+
steps: step,
|
|
2410
|
+
outputs,
|
|
2411
|
+
lifecycle
|
|
2412
|
+
};
|
|
2413
|
+
await emit({ type: "done", ...result });
|
|
2414
|
+
return result;
|
|
2415
|
+
}
|
|
2416
|
+
for (const [index, toolCall] of toolCalls.entries()) {
|
|
2417
|
+
const result = await this.#executeTool(
|
|
2418
|
+
sandbox.id,
|
|
2419
|
+
toolCall,
|
|
2420
|
+
index,
|
|
2421
|
+
input.timeoutSeconds,
|
|
2422
|
+
emit
|
|
2423
|
+
);
|
|
2424
|
+
messages.push(result);
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
throw new Error(`Work did not finish within ${maxSteps} steps`);
|
|
2428
|
+
} catch (error) {
|
|
2429
|
+
if (sandbox && outputBaseline && !finalizationAttempted) {
|
|
2430
|
+
try {
|
|
2431
|
+
const outputs = await this.#collectOutputs(sandbox.id, outputBaseline);
|
|
2432
|
+
for (const output of outputs) await emit({ type: "output", output });
|
|
2433
|
+
await this.#finalizeSandbox(sandbox.id, outputs, input, lifecycle, emit);
|
|
2434
|
+
} catch (finalizationError) {
|
|
2435
|
+
attachWorkFailure(error, lifecycle, finalizationError);
|
|
2436
|
+
}
|
|
2398
2437
|
}
|
|
2438
|
+
attachWorkFailure(error, lifecycle);
|
|
2439
|
+
throw error;
|
|
2399
2440
|
}
|
|
2400
|
-
throw new Error(`Work did not finish within ${maxSteps} steps`);
|
|
2401
2441
|
}
|
|
2402
2442
|
async deleteSandbox(sandboxId) {
|
|
2403
2443
|
const sandbox = await this.#sandboxes.get(sandboxId);
|
|
2404
2444
|
if (sandbox.state === "deleted") return;
|
|
2405
|
-
await this.#sandboxes.delete(sandboxId
|
|
2445
|
+
await this.#sandboxes.delete(sandboxId);
|
|
2446
|
+
}
|
|
2447
|
+
async #stageInputs(sandboxId, inputs) {
|
|
2448
|
+
if (inputs.length === 0) return;
|
|
2449
|
+
if (inputs.length > MAX_WORK_INPUTS) {
|
|
2450
|
+
throw new Error(`Work inputs exceed the ${MAX_WORK_INPUTS} file limit`);
|
|
2451
|
+
}
|
|
2452
|
+
const decodedBytes = inputs.reduce(
|
|
2453
|
+
(total, input) => total + Buffer.byteLength(input.contentsBase64, "base64"),
|
|
2454
|
+
0
|
|
2455
|
+
);
|
|
2456
|
+
if (decodedBytes > MAX_WORK_INPUT_BYTES) {
|
|
2457
|
+
throw new Error(`Work inputs exceed the ${MAX_WORK_INPUT_BYTES} byte limit`);
|
|
2458
|
+
}
|
|
2459
|
+
await this.#sandboxes.mkdir(sandboxId, {
|
|
2460
|
+
path: WORK_INPUT_DIRECTORY,
|
|
2461
|
+
recursive: true
|
|
2462
|
+
});
|
|
2463
|
+
const usedNames = /* @__PURE__ */ new Set();
|
|
2464
|
+
const manifest = [];
|
|
2465
|
+
for (const [index, input] of inputs.entries()) {
|
|
2466
|
+
const name = uniqueInputName(input, index, usedNames);
|
|
2467
|
+
const stagedPath = `${WORK_INPUT_DIRECTORY}/${name}`;
|
|
2468
|
+
await this.#sandboxes.uploadFileBase64(
|
|
2469
|
+
sandboxId,
|
|
2470
|
+
stagedPath,
|
|
2471
|
+
input.contentsBase64
|
|
2472
|
+
);
|
|
2473
|
+
manifest.push({
|
|
2474
|
+
id: input.id,
|
|
2475
|
+
path: stagedPath,
|
|
2476
|
+
name: input.name,
|
|
2477
|
+
mimeType: input.mimeType ?? null,
|
|
2478
|
+
checksumSha256: input.checksumSha256 ?? null,
|
|
2479
|
+
revision: input.revision ?? null,
|
|
2480
|
+
metadata: input.metadata ?? {}
|
|
2481
|
+
});
|
|
2482
|
+
}
|
|
2483
|
+
await this.#sandboxes.uploadFile(
|
|
2484
|
+
sandboxId,
|
|
2485
|
+
WORK_INPUT_MANIFEST,
|
|
2486
|
+
JSON.stringify({ version: 1, files: manifest }, null, 2)
|
|
2487
|
+
);
|
|
2488
|
+
}
|
|
2489
|
+
async #finalizeSandbox(sandboxId, outputs, input, lifecycle, emit) {
|
|
2490
|
+
lifecycle.persistence.outputCount = outputs.length;
|
|
2491
|
+
if (outputs.length === 0) {
|
|
2492
|
+
lifecycle.persistence.status = "not_needed";
|
|
2493
|
+
} else if (input.persistOutput) {
|
|
2494
|
+
lifecycle.persistence.status = "running";
|
|
2495
|
+
for (const output of outputs) {
|
|
2496
|
+
await emit({ type: "persistence", output, status: "started" });
|
|
2497
|
+
try {
|
|
2498
|
+
const download = lazyOutputDownload(this.#sandboxes, sandboxId, output);
|
|
2499
|
+
await input.persistOutput({ output, download });
|
|
2500
|
+
lifecycle.persistence.persistedCount += 1;
|
|
2501
|
+
await emit({ type: "persistence", output, status: "succeeded" });
|
|
2502
|
+
} catch (error) {
|
|
2503
|
+
const message = errorMessage(error);
|
|
2504
|
+
lifecycle.persistence.status = "failed";
|
|
2505
|
+
lifecycle.persistence.error = message;
|
|
2506
|
+
await emit({
|
|
2507
|
+
type: "persistence",
|
|
2508
|
+
output,
|
|
2509
|
+
status: "failed",
|
|
2510
|
+
error: message
|
|
2511
|
+
});
|
|
2512
|
+
if ((input.cleanup ?? "keep") !== "keep") {
|
|
2513
|
+
await this.#cleanupSandbox(sandboxId, "stop", lifecycle, emit);
|
|
2514
|
+
}
|
|
2515
|
+
throw error;
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
lifecycle.persistence.status = "complete";
|
|
2519
|
+
} else if ((input.cleanup ?? "keep") === "delete" && !input.discardOutputs) {
|
|
2520
|
+
lifecycle.persistence.status = "failed";
|
|
2521
|
+
lifecycle.persistence.error = "Deleting a Work sandbox with outputs requires persistOutput or discardOutputs: true";
|
|
2522
|
+
await this.#cleanupSandbox(sandboxId, "stop", lifecycle, emit);
|
|
2523
|
+
throw new Error(lifecycle.persistence.error);
|
|
2524
|
+
} else {
|
|
2525
|
+
lifecycle.persistence.status = "not_requested";
|
|
2526
|
+
}
|
|
2527
|
+
await this.#cleanupSandbox(
|
|
2528
|
+
sandboxId,
|
|
2529
|
+
input.cleanup ?? "keep",
|
|
2530
|
+
lifecycle,
|
|
2531
|
+
emit
|
|
2532
|
+
);
|
|
2533
|
+
}
|
|
2534
|
+
async #cleanupSandbox(sandboxId, policy, lifecycle, emit) {
|
|
2535
|
+
if (policy === "keep") return;
|
|
2536
|
+
lifecycle.cleanup.status = "running";
|
|
2537
|
+
await emit({ type: "cleanup", policy, status: "started" });
|
|
2538
|
+
try {
|
|
2539
|
+
const sandbox = policy === "delete" ? await this.#sandboxes.delete(sandboxId) : (await this.#sandboxes.stop(sandboxId)).sandbox;
|
|
2540
|
+
lifecycle.cleanup.finalSandboxState = sandbox.state;
|
|
2541
|
+
lifecycle.cleanup.status = policy === "delete" && sandbox.state === "deleted" || policy === "stop" && sandbox.state === "stopped" ? "complete" : "pending";
|
|
2542
|
+
await emit({
|
|
2543
|
+
type: "cleanup",
|
|
2544
|
+
policy,
|
|
2545
|
+
status: lifecycle.cleanup.status,
|
|
2546
|
+
sandboxState: sandbox.state
|
|
2547
|
+
});
|
|
2548
|
+
} catch (error) {
|
|
2549
|
+
const message = errorMessage(error);
|
|
2550
|
+
lifecycle.cleanup.status = "failed";
|
|
2551
|
+
lifecycle.cleanup.error = message;
|
|
2552
|
+
await emit({ type: "cleanup", policy, status: "failed", error: message });
|
|
2553
|
+
throw error;
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
downloadOutput(sandboxId, output) {
|
|
2557
|
+
return this.#sandboxes.downloadFileResponse(
|
|
2558
|
+
sandboxId,
|
|
2559
|
+
typeof output === "string" ? output : output.path
|
|
2560
|
+
);
|
|
2561
|
+
}
|
|
2562
|
+
async #prepareOutputDirectory(sandboxId) {
|
|
2563
|
+
await this.#sandboxes.mkdir(sandboxId, {
|
|
2564
|
+
path: WORK_OUTPUT_DIRECTORY,
|
|
2565
|
+
recursive: true
|
|
2566
|
+
});
|
|
2567
|
+
return outputSignatures(await this.#listOutputFiles(sandboxId));
|
|
2568
|
+
}
|
|
2569
|
+
async #collectOutputs(sandboxId, baseline) {
|
|
2570
|
+
const files = await this.#listOutputFiles(sandboxId);
|
|
2571
|
+
return files.filter((file) => baseline.get(file.path) !== outputSignature(file)).sort((left, right) => left.path.localeCompare(right.path)).slice(0, MAX_WORK_OUTPUTS).map(workOutputFromFile);
|
|
2572
|
+
}
|
|
2573
|
+
async #listOutputFiles(sandboxId) {
|
|
2574
|
+
const listed = await this.#sandboxes.listFiles(sandboxId, {
|
|
2575
|
+
path: WORK_OUTPUT_DIRECTORY,
|
|
2576
|
+
recursive: true,
|
|
2577
|
+
maxEntries: 500
|
|
2578
|
+
});
|
|
2579
|
+
return listed.files.filter(
|
|
2580
|
+
(file) => file.type === "file" && !normalizedOutputPath(file.path).includes("/.openpond-")
|
|
2581
|
+
);
|
|
2406
2582
|
}
|
|
2407
2583
|
async #resolveSandbox(input) {
|
|
2408
2584
|
if (input.sandboxId) {
|
|
@@ -2511,16 +2687,121 @@ var WORK_TOOLS = [
|
|
|
2511
2687
|
}
|
|
2512
2688
|
}
|
|
2513
2689
|
];
|
|
2514
|
-
function systemPrompt(sandboxId) {
|
|
2690
|
+
function systemPrompt(sandboxId, hasInputs) {
|
|
2515
2691
|
return [
|
|
2516
|
-
"You are OpenPond Work, a careful coding agent operating in
|
|
2692
|
+
"You are OpenPond Work, a careful coding agent operating in an isolated Linux sandbox.",
|
|
2517
2693
|
`The active sandbox is ${sandboxId}.`,
|
|
2694
|
+
...hasInputs ? [
|
|
2695
|
+
`Caller-provided durable files are staged under ${WORK_INPUT_DIRECTORY}; structured metadata is in ${WORK_INPUT_MANIFEST}.`
|
|
2696
|
+
] : [],
|
|
2518
2697
|
"Use run_command to inspect the workspace, edit files, and validate your work.",
|
|
2698
|
+
`Place completed user-facing files in ${WORK_OUTPUT_DIRECTORY}. The runtime collects new and revised files there automatically when the turn completes.`,
|
|
2519
2699
|
"Do the requested work completely. Preserve existing user changes and avoid destructive commands.",
|
|
2520
2700
|
"Keep the user informed with concise prose, but call tools whenever verification or file changes are needed.",
|
|
2521
2701
|
"Before finishing, run relevant tests and summarize the concrete result."
|
|
2522
2702
|
].join("\n");
|
|
2523
2703
|
}
|
|
2704
|
+
function initialLifecycle(cleanupPolicy) {
|
|
2705
|
+
return {
|
|
2706
|
+
cleanupPolicy,
|
|
2707
|
+
persistence: {
|
|
2708
|
+
status: "not_requested",
|
|
2709
|
+
outputCount: 0,
|
|
2710
|
+
persistedCount: 0
|
|
2711
|
+
},
|
|
2712
|
+
cleanup: { status: "not_requested" }
|
|
2713
|
+
};
|
|
2714
|
+
}
|
|
2715
|
+
function lazyOutputDownload(sandboxes, sandboxId, output) {
|
|
2716
|
+
let pending = null;
|
|
2717
|
+
return () => {
|
|
2718
|
+
pending ??= sandboxes.downloadFileResponse(sandboxId, {
|
|
2719
|
+
path: output.path,
|
|
2720
|
+
maxBytes: Math.max(1, output.sizeBytes)
|
|
2721
|
+
}).then((response) => {
|
|
2722
|
+
const decodedBytes = Buffer.byteLength(
|
|
2723
|
+
response.file.contentsBase64,
|
|
2724
|
+
"base64"
|
|
2725
|
+
);
|
|
2726
|
+
if (response.file.truncated || response.file.returnedBytes !== response.file.totalSizeBytes || decodedBytes !== output.sizeBytes) {
|
|
2727
|
+
throw new Error(`Output download was incomplete for ${output.name}`);
|
|
2728
|
+
}
|
|
2729
|
+
return response;
|
|
2730
|
+
});
|
|
2731
|
+
return pending;
|
|
2732
|
+
};
|
|
2733
|
+
}
|
|
2734
|
+
function uniqueInputName(input, index, usedNames) {
|
|
2735
|
+
const safeId = input.id.replace(/[^a-zA-Z0-9._-]+/g, "-").slice(0, 80) || `input-${index + 1}`;
|
|
2736
|
+
const safeName = input.name.replace(/[^a-zA-Z0-9._-]+/g, "-").slice(-120) || "file";
|
|
2737
|
+
const base = `${safeId}-${safeName}`;
|
|
2738
|
+
let candidate = base;
|
|
2739
|
+
let collision = 1;
|
|
2740
|
+
while (usedNames.has(candidate)) {
|
|
2741
|
+
collision += 1;
|
|
2742
|
+
candidate = `${base}-${collision}`;
|
|
2743
|
+
}
|
|
2744
|
+
usedNames.add(candidate);
|
|
2745
|
+
return candidate;
|
|
2746
|
+
}
|
|
2747
|
+
function attachWorkFailure(error, lifecycle, finalizationError) {
|
|
2748
|
+
if (!error || typeof error !== "object") return;
|
|
2749
|
+
Object.assign(error, {
|
|
2750
|
+
workLifecycle: lifecycle,
|
|
2751
|
+
...finalizationError ? { workFinalizationError: errorMessage(finalizationError) } : {}
|
|
2752
|
+
});
|
|
2753
|
+
}
|
|
2754
|
+
function outputSignatures(files) {
|
|
2755
|
+
return new Map(files.map((file) => [file.path, outputSignature(file)]));
|
|
2756
|
+
}
|
|
2757
|
+
function outputSignature(file) {
|
|
2758
|
+
return `${file.sizeBytes}:${file.updatedAt}`;
|
|
2759
|
+
}
|
|
2760
|
+
function workOutputFromFile(file) {
|
|
2761
|
+
const name = normalizedOutputPath(file.path).split("/").at(-1) || file.path;
|
|
2762
|
+
const mimeType = workOutputMimeType(name);
|
|
2763
|
+
return {
|
|
2764
|
+
path: file.path,
|
|
2765
|
+
name,
|
|
2766
|
+
mimeType,
|
|
2767
|
+
sizeBytes: file.sizeBytes,
|
|
2768
|
+
updatedAt: file.updatedAt,
|
|
2769
|
+
isBinary: file.isBinary ?? null,
|
|
2770
|
+
previewable: file.previewable ?? (mimeType.startsWith("image/") || mimeType === "application/pdf" || mimeType.startsWith("text/"))
|
|
2771
|
+
};
|
|
2772
|
+
}
|
|
2773
|
+
function normalizedOutputPath(value) {
|
|
2774
|
+
return value.replaceAll("\\", "/");
|
|
2775
|
+
}
|
|
2776
|
+
function workOutputMimeType(name) {
|
|
2777
|
+
const extension = name.toLowerCase().match(/\.[^.]+$/)?.[0] ?? "";
|
|
2778
|
+
return WORK_OUTPUT_MIME_TYPES[extension] ?? "application/octet-stream";
|
|
2779
|
+
}
|
|
2780
|
+
var WORK_OUTPUT_MIME_TYPES = Object.freeze({
|
|
2781
|
+
".avif": "image/avif",
|
|
2782
|
+
".csv": "text/csv",
|
|
2783
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
2784
|
+
".gif": "image/gif",
|
|
2785
|
+
".html": "text/html",
|
|
2786
|
+
".jpeg": "image/jpeg",
|
|
2787
|
+
".jpg": "image/jpeg",
|
|
2788
|
+
".json": "application/json",
|
|
2789
|
+
".m4a": "audio/mp4",
|
|
2790
|
+
".md": "text/markdown",
|
|
2791
|
+
".mov": "video/quicktime",
|
|
2792
|
+
".mp3": "audio/mpeg",
|
|
2793
|
+
".mp4": "video/mp4",
|
|
2794
|
+
".pdf": "application/pdf",
|
|
2795
|
+
".png": "image/png",
|
|
2796
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
2797
|
+
".svg": "image/svg+xml",
|
|
2798
|
+
".tsv": "text/tab-separated-values",
|
|
2799
|
+
".txt": "text/plain",
|
|
2800
|
+
".wav": "audio/wav",
|
|
2801
|
+
".webm": "video/webm",
|
|
2802
|
+
".webp": "image/webp",
|
|
2803
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
2804
|
+
});
|
|
2524
2805
|
function toolMessage(toolCallId, payload) {
|
|
2525
2806
|
return { role: "tool", tool_call_id: toolCallId, content: JSON.stringify(payload) };
|
|
2526
2807
|
}
|