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 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 a persistent workspace.
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, sandboxId } = await request.json();
31
- const result = await openpond.work.run({ prompt, sandboxId });
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. Pass the returned ID into the next turn to continue in the same filesystem. Use `onEvent` to stream sandbox, model, and command progress to a client.
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
- ## Staging
99
+ ## Lifecycle, persistence, and cleanup
70
100
 
71
- Use a server-only environment file while developing:
101
+ The generic SDK defaults to `cleanup: "keep"` for backwards compatibility. Applications can choose one of three explicit terminal policies:
72
102
 
73
- ```dotenv
74
- OPENPOND_API_KEY=opk_...
75
- OPENPOND_API_URL=https://staging-api.openpond.ai
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
- If the staging deployment has Vercel protection enabled, also set `VERCEL_AUTOMATION_BYPASS_SECRET`. The SDK only sends that bypass header to OpenPond staging hosts.
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
- ## Lifecycle and cleanup
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
- Work sandboxes remain available so conversations can continue. Delete them when a conversation is removed or expires:
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
- await emit({ type: "status", message: "Preparing sandbox" });
2341
- const sandbox = await this.#resolveSandbox(input);
2342
- await emit({
2343
- type: "sandbox",
2344
- sandboxId: sandbox.id,
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
- for (let step = 1; step <= maxSteps; step += 1) {
2355
- input.signal?.throwIfAborted();
2356
- await emit({ type: "status", message: `Thinking \xB7 step ${step}` });
2357
- const completion = await sendHostedChatTurn({
2358
- apiBaseUrl: this.#chatApiBaseUrl,
2359
- token: this.#apiKey,
2360
- model: input.model?.trim() || DEFAULT_MODEL,
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
- if (assistantText) {
2381
- finalText = assistantText;
2382
- await emit({ type: "assistant", text: assistantText });
2383
- }
2384
- if (toolCalls.length === 0) {
2385
- const result = { sandboxId: sandbox.id, text: finalText, steps: step };
2386
- await emit({ type: "done", ...result });
2387
- return result;
2388
- }
2389
- for (const [index, toolCall] of toolCalls.entries()) {
2390
- const result = await this.#executeTool(
2391
- sandbox.id,
2392
- toolCall,
2393
- index,
2394
- input.timeoutSeconds,
2395
- emit
2396
- );
2397
- messages.push(result);
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, { async: true });
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 a persistent Linux sandbox.",
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
  }