Repository navigation
Expand file tree
/
Copy pathe2b-code-interpreter.ts
More file actions
133 lines (116 loc) · 4.65 KB
/
Copy pathe2b-code-interpreter.ts
File metadata and controls
133 lines (116 loc) · 4.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
import { Sandbox, type Execution } from "@e2b/code-interpreter";
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
import { join, resolve } from "node:path";
const SANDBOX_TIMEOUT_MS = 10 * 60 * 1000;
export default function e2bCodeInterpreter(pi: ExtensionAPI) {
let sandbox: Sandbox | null = null;
let chartCount = 0;
function setStatus(ctx: ExtensionContext, text: string) {
try {
ctx.ui.setStatus("e2b", text);
} catch {
// Headless sessions do not expose TUI status controls.
}
}
async function getSandbox(ctx: ExtensionContext): Promise<Sandbox> {
if (sandbox) return sandbox;
setStatus(ctx, "creating sandbox…");
const createdSandbox = await Sandbox.create({ timeoutMs: SANDBOX_TIMEOUT_MS });
try {
const dataDir = resolve(ctx.cwd, "data");
const files = await readdir(dataDir).catch(() => [] as string[]);
if (files.length > 0) {
await createdSandbox.files.write(
await Promise.all(
files.map(async (name) => ({
path: `data/${name}`,
data: await readFile(join(dataDir, name), "utf8"),
})),
),
);
}
} catch (error) {
await createdSandbox.kill().catch(() => {});
throw error;
}
sandbox = createdSandbox;
setStatus(ctx, `sandbox ${createdSandbox.sandboxId}`);
return createdSandbox;
}
async function saveCharts(execution: Execution, cwd: string): Promise<string[]> {
const pngs = execution.results.flatMap((result) => (result.png ? [result.png] : []));
if (pngs.length === 0) return [];
const outputDir = join(cwd, "output");
await mkdir(outputDir, { recursive: true });
const saved: string[] = [];
for (const png of pngs) {
chartCount += 1;
const path = join(outputDir, `chart-${String(chartCount).padStart(2, "0")}.png`);
await writeFile(path, Buffer.from(png, "base64"));
saved.push(path);
}
return saved;
}
pi.registerTool({
name: "run_python",
label: "Run Python (E2B sandbox)",
description:
"Execute Python in a stateful Jupyter kernel running in an isolated E2B cloud sandbox. " +
"Variables, imports, and dataframes persist across calls within the session. " +
"pandas and matplotlib are preinstalled; the CSVs from ./data/ are staged at ~/data/ in the sandbox. " +
"The last expression's value is returned like in a notebook cell. " +
"matplotlib figures are captured automatically (call plt.show(), do not savefig) " +
"and saved locally under ./output/.",
promptSnippet: "Execute Python in an isolated, stateful E2B cloud sandbox",
promptGuidelines: [
"Use run_python for data analysis and plotting instead of running Python via bash — code runs in an isolated E2B cloud sandbox, not on this machine.",
"run_python keeps state between calls: load data once, then iterate in small steps.",
"For charts in run_python, end the cell with plt.show(); captured figures are saved to ./output/ automatically.",
],
parameters: Type.Object({
code: Type.String({ description: "Python code to execute in the sandbox kernel" }),
}),
async execute(_toolCallId, params, signal, onUpdate, ctx) {
if (signal?.aborted) {
return { content: [{ type: "text", text: "Cancelled" }], details: {} };
}
const sbx = await getSandbox(ctx);
let streamed = "";
const stream = (line: string) => {
streamed += line;
onUpdate?.({ content: [{ type: "text", text: streamed }], details: {} });
};
const execution = await sbx.runCode(params.code, {
onStdout: (message) => stream(message.line),
onStderr: (message) => stream(message.line),
});
if (execution.error) {
throw new Error(
`${execution.error.name}: ${execution.error.value}\n${execution.error.traceback}`,
);
}
const charts = await saveCharts(execution, ctx.cwd);
const parts: string[] = [];
if (execution.logs.stdout.length > 0) parts.push(execution.logs.stdout.join(""));
if (execution.logs.stderr.length > 0) parts.push(execution.logs.stderr.join(""));
for (const result of execution.results) {
if (result.text && !result.png) parts.push(result.text);
}
for (const path of charts) parts.push(`[chart saved to ${path}]`);
if (parts.length === 0) parts.push("(no output)");
return {
content: [{ type: "text", text: parts.join("\n") }],
details: { sandboxId: sbx.sandboxId, charts },
};
},
});
pi.on("session_shutdown", async () => {
const activeSandbox = sandbox;
sandbox = null;
// Best-effort cleanup: a flaky kill must not throw inside Pi's
// shutdown handler; the sandbox times out on its own regardless.
await activeSandbox?.kill().catch(() => {});
});
}