@lazyingart/agintiflow 0.1.0 → 0.3.0

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.
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env bash
2
+ set -Eeuo pipefail
3
+
4
+ SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
5
+ REPO_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
6
+ IMAGE="${AGINTIFLOW_TOOLCHAIN_IMAGE:-agintiflow-sandbox:latest}"
7
+ WORKSPACE="${AGINTIFLOW_TOOLCHAIN_WORKSPACE:-${REPO_ROOT}}"
8
+ DOCKERFILE="${AGINTIFLOW_TOOLCHAIN_DOCKERFILE:-${REPO_ROOT}/docker/sandbox.Dockerfile}"
9
+
10
+ if ! command -v docker >/dev/null 2>&1; then
11
+ echo "Docker is not installed or not on PATH. Run scripts/install-docker-ubuntu.sh first." >&2
12
+ exit 1
13
+ fi
14
+
15
+ mkdir -p "${WORKSPACE}"
16
+
17
+ echo "Building AgInTiFlow toolchain sandbox image:"
18
+ echo " image=${IMAGE}"
19
+ echo " dockerfile=${DOCKERFILE}"
20
+ echo " context=${REPO_ROOT}"
21
+ docker build -t "${IMAGE}" -f "${DOCKERFILE}" "${REPO_ROOT}"
22
+
23
+ echo
24
+ echo "Running toolchain preflight inside Docker:"
25
+ docker run --rm \
26
+ --network none \
27
+ --cap-drop ALL \
28
+ --security-opt no-new-privileges \
29
+ --pids-limit 256 \
30
+ --memory 2g \
31
+ --cpus 2 \
32
+ --tmpfs /tmp:rw,nosuid,nodev,size=512m \
33
+ -e HOME=/tmp \
34
+ -v "${WORKSPACE}:/workspace:rw" \
35
+ -w /workspace \
36
+ "${IMAGE}" \
37
+ bash -lc 'set -Eeuo pipefail
38
+ node -v
39
+ npm -v
40
+ python3 --version
41
+ python3 - <<PY
42
+ import matplotlib, numpy
43
+ print("matplotlib", matplotlib.__version__)
44
+ print("numpy", numpy.__version__)
45
+ PY
46
+ latexmk -version | head -1
47
+ pdflatex --version | head -1
48
+ '
49
+
50
+ echo
51
+ echo "Toolchain sandbox is ready."
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { repairModelMessageHistory, runAgent } from "../src/agent-runner.js";
7
+ import { resolveRuntimeConfig } from "../src/config.js";
8
+ import { SessionStore } from "../src/session-store.js";
9
+
10
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
11
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "agintiflow-coding-tools-"));
12
+ const runtimeDir = path.join(tempRoot, "runtime");
13
+ const workspace = path.join(tempRoot, "workspace");
14
+ await fs.mkdir(workspace, { recursive: true });
15
+
16
+ function assert(condition, message) {
17
+ if (!condition) throw new Error(message);
18
+ }
19
+
20
+ async function runMock(goal, sessionId) {
21
+ const config = resolveRuntimeConfig(
22
+ {
23
+ provider: "mock",
24
+ routingMode: "manual",
25
+ model: "mock-agent",
26
+ goal,
27
+ commandCwd: workspace,
28
+ maxSteps: 5,
29
+ },
30
+ {
31
+ baseDir: runtimeDir,
32
+ packageDir: repoRoot,
33
+ provider: "mock",
34
+ routingMode: "manual",
35
+ model: "mock-agent",
36
+ commandCwd: workspace,
37
+ allowShellTool: false,
38
+ allowFileTools: true,
39
+ sandboxMode: "host",
40
+ packageInstallPolicy: "block",
41
+ sessionId,
42
+ }
43
+ );
44
+
45
+ const result = await runAgent(config);
46
+ const store = new SessionStore(config.sessionsDir, result.sessionId);
47
+ return {
48
+ result,
49
+ events: await store.loadEvents(),
50
+ };
51
+ }
52
+
53
+ try {
54
+ const staleDeepSeekState = {
55
+ messages: [
56
+ { role: "system", content: "system" },
57
+ { role: "user", content: "draw a figure" },
58
+ { role: "assistant", content: "Execution plan:\n1. stale synthetic plan" },
59
+ {
60
+ role: "assistant",
61
+ content: "I will call a tool.",
62
+ tool_calls: [{ id: "stale-call", type: "function", function: { name: "list_files", arguments: "{}" } }],
63
+ },
64
+ { role: "tool", tool_call_id: "stale-call", content: "{\"ok\":true}" },
65
+ { role: "assistant", content: "Old final answer." },
66
+ ],
67
+ };
68
+ const repair = repairModelMessageHistory(staleDeepSeekState, { provider: "deepseek" });
69
+ assert(repair.changed, "stale DeepSeek history was not repaired");
70
+ assert(
71
+ staleDeepSeekState.messages.every(
72
+ (message) => message.role !== "assistant" || message.reasoning_content || message.reasoningContent
73
+ ),
74
+ "repaired DeepSeek history still has assistant messages without reasoning_content"
75
+ );
76
+ assert(
77
+ !staleDeepSeekState.messages.some((message) => message.role === "tool" && message.tool_call_id === "stale-call"),
78
+ "repaired DeepSeek history retained an orphan stale tool message"
79
+ );
80
+
81
+ const writeRun = await runMock("Create file: notes/mock-output.txt with a short coding smoke message.", "coding-write");
82
+ const written = await fs.readFile(path.join(workspace, "notes/mock-output.txt"), "utf8");
83
+ assert(written.includes("Created by AgInTiFlow mock mode."), "mock write did not create expected file");
84
+ assert(writeRun.events.some((event) => event.type === "file.changed"), "write run did not persist file.changed event");
85
+
86
+ await runMock("Create file: /workspace/virtual-output.txt with virtual Docker path support.", "coding-write-virtual");
87
+ const virtualWritten = await fs.readFile(path.join(workspace, "virtual-output.txt"), "utf8");
88
+ assert(virtualWritten.includes("Created by AgInTiFlow mock mode."), "virtual /workspace path was not mapped safely");
89
+
90
+ await fs.writeFile(path.join(workspace, "patch-target.txt"), "old\n", "utf8");
91
+ const patchRun = await runMock("Patch file: patch-target.txt replace old with new.", "coding-patch");
92
+ const patched = await fs.readFile(path.join(workspace, "patch-target.txt"), "utf8");
93
+ assert(patched === "new\n", "mock patch did not update expected file");
94
+ assert(patchRun.events.some((event) => event.type === "file.changed"), "patch run did not persist file.changed event");
95
+
96
+ const envRun = await runMock("Create file: .env with blocked content.", "coding-block-env");
97
+ await fs
98
+ .access(path.join(workspace, ".env"))
99
+ .then(() => {
100
+ throw new Error(".env was created despite path guardrails");
101
+ })
102
+ .catch((error) => {
103
+ if (error.code !== "ENOENT") throw error;
104
+ });
105
+ assert(envRun.events.some((event) => event.type === "tool.blocked"), ".env guardrail did not emit tool.blocked");
106
+
107
+ const outsideRun = await runMock("Create file: ../outside-workspace.txt with blocked content.", "coding-block-outside");
108
+ await fs
109
+ .access(path.join(tempRoot, "outside-workspace.txt"))
110
+ .then(() => {
111
+ throw new Error("outside-workspace.txt was created despite path guardrails");
112
+ })
113
+ .catch((error) => {
114
+ if (error.code !== "ENOENT") throw error;
115
+ });
116
+ assert(outsideRun.events.some((event) => event.type === "tool.blocked"), "outside path guardrail did not emit tool.blocked");
117
+
118
+ console.log(
119
+ JSON.stringify(
120
+ {
121
+ ok: true,
122
+ workspace,
123
+ checks: [
124
+ "deepseek_history_repair",
125
+ "write_file",
126
+ "virtual_workspace_path",
127
+ "apply_patch",
128
+ "block_env",
129
+ "block_outside",
130
+ ],
131
+ },
132
+ null,
133
+ 2
134
+ )
135
+ );
136
+ } finally {
137
+ await fs.rm(tempRoot, { recursive: true, force: true });
138
+ }
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { readArtifactContent } from "../src/artifact-tunnel.js";
7
+ import { evaluateCommandPolicy } from "../src/command-policy.js";
8
+ import { runDockerPreflight, runDockerSandboxCommand } from "../src/docker-sandbox.js";
9
+ import { resolveRuntimeConfig } from "../src/config.js";
10
+
11
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
12
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "agintiflow-toolchain-"));
13
+ const runtimeDir = path.join(tempRoot, "runtime");
14
+ const workspace = path.join(tempRoot, "workspace");
15
+ await fs.mkdir(workspace, { recursive: true });
16
+
17
+ function assert(condition, message) {
18
+ if (!condition) throw new Error(message);
19
+ }
20
+
21
+ async function runToolchainCommand(command, config) {
22
+ const policy = evaluateCommandPolicy(command, config);
23
+ assert(policy.allowed, `${command} was blocked: ${policy.reason || policy.category}`);
24
+ return runDockerSandboxCommand(command, config, policy);
25
+ }
26
+
27
+ try {
28
+ const config = resolveRuntimeConfig(
29
+ {
30
+ provider: "mock",
31
+ routingMode: "manual",
32
+ model: "mock-agent",
33
+ goal: "",
34
+ commandCwd: workspace,
35
+ maxSteps: 1,
36
+ },
37
+ {
38
+ baseDir: runtimeDir,
39
+ packageDir: repoRoot,
40
+ provider: "mock",
41
+ routingMode: "manual",
42
+ model: "mock-agent",
43
+ commandCwd: workspace,
44
+ allowShellTool: true,
45
+ allowFileTools: true,
46
+ useDockerSandbox: true,
47
+ sandboxMode: "docker-workspace",
48
+ packageInstallPolicy: "block",
49
+ dockerSandboxImage: process.env.AGINTIFLOW_TOOLCHAIN_IMAGE || "agintiflow-sandbox:latest",
50
+ }
51
+ );
52
+
53
+ const preflight = await runDockerPreflight(config, { buildImage: false });
54
+ assert(preflight.ok, "Docker toolchain preflight failed. Run scripts/setup-agent-toolchain-docker.sh first.");
55
+
56
+ await fs.writeFile(
57
+ path.join(workspace, "plot_fx.py"),
58
+ [
59
+ "import math",
60
+ "import matplotlib",
61
+ "matplotlib.use('Agg')",
62
+ "import matplotlib.pyplot as plt",
63
+ "xs = [x / 20 for x in range(-60, 61)]",
64
+ "ys = [x + math.exp(x) for x in xs]",
65
+ "fig, ax = plt.subplots(figsize=(7, 4.4), dpi=160)",
66
+ "ax.plot(xs, ys, color='#0065c9', linewidth=2.5)",
67
+ "ax.axhline(0, color='#6b7280', linewidth=0.8)",
68
+ "ax.axvline(0, color='#6b7280', linewidth=0.8)",
69
+ "ax.grid(True, alpha=0.25)",
70
+ "ax.set_title('f(x) = x + e^x')",
71
+ "ax.set_xlabel('x')",
72
+ "ax.set_ylabel('f(x)')",
73
+ "fig.tight_layout()",
74
+ "fig.savefig('plot_fx.svg')",
75
+ "",
76
+ ].join("\n"),
77
+ "utf8"
78
+ );
79
+ await runToolchainCommand("python3 plot_fx.py", config);
80
+ const svg = await fs.readFile(path.join(workspace, "plot_fx.svg"), "utf8");
81
+ assert(svg.includes("<svg"), "Python plot did not produce an SVG file");
82
+
83
+ await fs.writeFile(
84
+ path.join(workspace, "paper.tex"),
85
+ [
86
+ "\\documentclass{article}",
87
+ "\\usepackage[T1]{fontenc}",
88
+ "\\usepackage{amsmath}",
89
+ "\\title{AgInTiFlow Toolchain Smoke}",
90
+ "\\author{AgInTiFlow}",
91
+ "\\date{}",
92
+ "\\begin{document}",
93
+ "\\maketitle",
94
+ "This smoke test verifies that the companion Docker sandbox can compile a PDF.",
95
+ "\\[ f(x) = x + e^x, \\qquad f'(x) = 1 + e^x. \\]",
96
+ "\\end{document}",
97
+ "",
98
+ ].join("\n"),
99
+ "utf8"
100
+ );
101
+ await runToolchainCommand("latexmk -pdf -interaction=nonstopmode -halt-on-error paper.tex", config);
102
+ const pdfStat = await fs.stat(path.join(workspace, "paper.pdf"));
103
+ assert(pdfStat.size > 1000, "LaTeX did not produce a useful PDF file");
104
+
105
+ const pdfPreview = await readArtifactContent(
106
+ {
107
+ id: "smoke-pdf",
108
+ kind: "pdf",
109
+ title: "Toolchain PDF",
110
+ path: "paper.pdf",
111
+ ref: { type: "workspace-file", path: "paper.pdf", commandCwd: workspace },
112
+ },
113
+ { config }
114
+ );
115
+ assert(pdfPreview.ok && pdfPreview.kind === "pdf", "PDF artifact preview was not renderable");
116
+ assert(/^data:application\/pdf;base64,/.test(pdfPreview.dataUrl || ""), "PDF artifact did not return a PDF data URL");
117
+
118
+ await fs.mkdir(path.join(workspace, "figure-note"), { recursive: true });
119
+ await fs.writeFile(
120
+ path.join(workspace, "figure-note", "make_figure.py"),
121
+ [
122
+ "import math",
123
+ "import matplotlib",
124
+ "matplotlib.use('Agg')",
125
+ "import matplotlib.pyplot as plt",
126
+ "xs = [x / 20 for x in range(-40, 41)]",
127
+ "ys = [x + math.exp(x) for x in xs]",
128
+ "fig, ax = plt.subplots(figsize=(6, 3.5))",
129
+ "ax.plot(xs, ys, color='#0065c9', linewidth=2.2)",
130
+ "ax.set_title('f(x)=x+e^x')",
131
+ "ax.set_xlabel('x')",
132
+ "ax.set_ylabel('f(x)')",
133
+ "ax.grid(True, alpha=0.25)",
134
+ "fig.tight_layout()",
135
+ "fig.savefig('figure-note/fx_figure.pdf')",
136
+ "",
137
+ ].join("\n"),
138
+ "utf8"
139
+ );
140
+ await runToolchainCommand("python3 figure-note/make_figure.py", config);
141
+
142
+ await fs.writeFile(
143
+ path.join(workspace, "figure-note", "figure_note.tex"),
144
+ [
145
+ "\\documentclass{article}",
146
+ "\\usepackage[T1]{fontenc}",
147
+ "\\usepackage{amsmath}",
148
+ "\\usepackage{graphicx}",
149
+ "\\title{Figure-Included Toolchain Smoke}",
150
+ "\\author{AgInTiFlow}",
151
+ "\\date{}",
152
+ "\\begin{document}",
153
+ "\\maketitle",
154
+ "The generated figure below is included from a workspace subfolder.",
155
+ "\\[ f(x)=x+e^x \\]",
156
+ "\\begin{figure}[h]",
157
+ "\\centering",
158
+ "\\includegraphics[width=0.8\\linewidth]{fx_figure.pdf}",
159
+ "\\caption{Generated plot of $f(x)=x+e^x$.}",
160
+ "\\end{figure}",
161
+ "\\end{document}",
162
+ "",
163
+ ].join("\n"),
164
+ "utf8"
165
+ );
166
+ await runToolchainCommand("latexmk -cd -pdf -interaction=nonstopmode -halt-on-error figure-note/figure_note.tex", config);
167
+ const figureNotePdf = await fs.stat(path.join(workspace, "figure-note", "figure_note.pdf"));
168
+ assert(figureNotePdf.size > 1000, "Figure-included LaTeX note did not produce a PDF file");
169
+
170
+ console.log(
171
+ JSON.stringify(
172
+ {
173
+ ok: true,
174
+ image: config.dockerSandboxImage,
175
+ workspace,
176
+ outputs: ["plot_fx.svg", "paper.pdf", "figure-note/fx_figure.pdf", "figure-note/figure_note.pdf"],
177
+ artifactKinds: ["image/svg+xml", "application/pdf"],
178
+ },
179
+ null,
180
+ 2
181
+ )
182
+ );
183
+ } finally {
184
+ await fs.rm(tempRoot, { recursive: true, force: true });
185
+ }
@@ -0,0 +1,217 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import fs from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
9
+ const runtimeDir = await fs.mkdtemp(path.join(os.tmpdir(), "agintiflow-api-smoke-"));
10
+ const port = 43000 + Math.floor(Math.random() * 1000);
11
+ const baseUrl = `http://127.0.0.1:${port}`;
12
+ const server = spawn(process.execPath, [path.join(repoRoot, "web.js")], {
13
+ cwd: runtimeDir,
14
+ env: {
15
+ ...process.env,
16
+ PORT: String(port),
17
+ HOST: "127.0.0.1",
18
+ AGINTIFLOW_RUNTIME_DIR: runtimeDir,
19
+ },
20
+ stdio: ["ignore", "pipe", "pipe"],
21
+ });
22
+
23
+ let stdout = "";
24
+ let stderr = "";
25
+ server.stdout.on("data", (chunk) => {
26
+ stdout += chunk.toString();
27
+ });
28
+ server.stderr.on("data", (chunk) => {
29
+ stderr += chunk.toString();
30
+ });
31
+
32
+ function delay(ms) {
33
+ return new Promise((resolve) => setTimeout(resolve, ms));
34
+ }
35
+
36
+ async function fetchJson(pathname, options = {}) {
37
+ const response = await fetch(`${baseUrl}${pathname}`, options);
38
+ const body = await response.json().catch(() => ({}));
39
+ if (!response.ok) {
40
+ throw new Error(`${pathname} failed with ${response.status}: ${body.error || response.statusText}`);
41
+ }
42
+ return body;
43
+ }
44
+
45
+ async function waitForHealth() {
46
+ const deadline = Date.now() + 15000;
47
+ while (Date.now() < deadline) {
48
+ if (server.exitCode !== null) break;
49
+ try {
50
+ const health = await fetchJson("/health");
51
+ if (health.ok) return health;
52
+ } catch {
53
+ await delay(250);
54
+ }
55
+ }
56
+
57
+ throw new Error(`web server did not become healthy. stdout=${stdout.slice(-500)} stderr=${stderr.slice(-500)}`);
58
+ }
59
+
60
+ async function waitForRun(sessionId) {
61
+ const deadline = Date.now() + 20000;
62
+ while (Date.now() < deadline) {
63
+ const run = await fetchJson(`/api/runs/${encodeURIComponent(sessionId)}`);
64
+ if (run.status === "finished" || run.status === "failed") return run;
65
+ await delay(400);
66
+ }
67
+ throw new Error(`run ${sessionId} did not finish in time`);
68
+ }
69
+
70
+ try {
71
+ await waitForHealth();
72
+
73
+ const config = await fetchJson("/api/config");
74
+ if (!config.keyStatus?.mock) throw new Error("mock provider is not advertised by /api/config");
75
+ if (!config.workspace?.enabled) throw new Error("workspace file tools are not advertised by /api/config");
76
+ if (config.preferences?.preferredWrapper !== "codex") throw new Error("Codex is not the default preferred wrapper");
77
+
78
+ const status = await fetchJson("/api/sandbox/status");
79
+ if (!status.status?.workspaceReadable) throw new Error("sandbox status did not report a readable workspace");
80
+
81
+ const preflight = await fetchJson("/api/sandbox/preflight", {
82
+ method: "POST",
83
+ headers: { "Content-Type": "application/json" },
84
+ body: JSON.stringify({
85
+ sandboxMode: "host",
86
+ commandCwd: runtimeDir,
87
+ buildImage: false,
88
+ }),
89
+ });
90
+ if (!preflight.ok) throw new Error("host preflight did not pass");
91
+
92
+ const runStart = await fetchJson("/api/runs", {
93
+ method: "POST",
94
+ headers: { "Content-Type": "application/json" },
95
+ body: JSON.stringify({
96
+ provider: "mock",
97
+ routingMode: "manual",
98
+ model: "mock-agent",
99
+ goal: "Report the current working directory with a safe command.",
100
+ commandCwd: runtimeDir,
101
+ sandboxMode: "host",
102
+ packageInstallPolicy: "block",
103
+ allowShellTool: true,
104
+ preferredWrapper: "codex",
105
+ maxSteps: 4,
106
+ headless: true,
107
+ }),
108
+ });
109
+
110
+ const run = await waitForRun(runStart.sessionId);
111
+ if (run.status !== "finished") throw new Error(`mock run failed: ${run.error || "unknown error"}`);
112
+ if (!/Mock run complete/.test(run.result)) throw new Error("mock run did not return the expected result");
113
+
114
+ const chat = await fetchJson(`/api/sessions/${encodeURIComponent(runStart.sessionId)}/chat`);
115
+ if (!Array.isArray(chat.chat) || chat.chat.length < 2) throw new Error("chat history was not persisted");
116
+
117
+ const renamed = await fetchJson(`/api/sessions/${encodeURIComponent(runStart.sessionId)}`, {
118
+ method: "PATCH",
119
+ headers: { "Content-Type": "application/json" },
120
+ body: JSON.stringify({ title: "Smoke renamed conversation" }),
121
+ });
122
+ if (renamed.session?.title !== "Smoke renamed conversation") throw new Error("session rename did not persist");
123
+
124
+ const autoRenamed = await fetchJson(`/api/sessions/${encodeURIComponent(runStart.sessionId)}/auto-title`, {
125
+ method: "POST",
126
+ });
127
+ if (!/^Report the current working directory/i.test(autoRenamed.session?.title || "")) {
128
+ throw new Error("session auto rename did not derive a title from chat history");
129
+ }
130
+
131
+ const changes = await fetchJson("/api/workspace/changes");
132
+ if (!Array.isArray(changes.activity)) throw new Error("workspace changes endpoint returned an invalid payload");
133
+
134
+ const canvasRunStart = await fetchJson("/api/runs", {
135
+ method: "POST",
136
+ headers: { "Content-Type": "application/json" },
137
+ body: JSON.stringify({
138
+ provider: "mock",
139
+ routingMode: "manual",
140
+ model: "mock-agent",
141
+ goal: "Create a canvas artifact preview for this smoke test.",
142
+ commandCwd: runtimeDir,
143
+ sandboxMode: "host",
144
+ packageInstallPolicy: "block",
145
+ allowShellTool: false,
146
+ allowFileTools: true,
147
+ preferredWrapper: "codex",
148
+ maxSteps: 4,
149
+ headless: true,
150
+ }),
151
+ });
152
+ const canvasRun = await waitForRun(canvasRunStart.sessionId);
153
+ if (canvasRun.status !== "finished") throw new Error(`mock canvas run failed: ${canvasRun.error || "unknown error"}`);
154
+
155
+ const artifacts = await fetchJson(`/api/sessions/${encodeURIComponent(canvasRunStart.sessionId)}/artifacts`);
156
+ if (!Array.isArray(artifacts.items) || artifacts.items.length === 0) {
157
+ throw new Error("artifact endpoint returned no items for a finished mock run");
158
+ }
159
+ if (!artifacts.items.some((item) => item.source === "agent-canvas")) {
160
+ throw new Error("mock run did not publish an agent canvas artifact");
161
+ }
162
+ const selectedArtifactId = artifacts.selectedItemId || artifacts.items[0].id;
163
+ const selected = await fetchJson(`/api/sessions/${encodeURIComponent(canvasRunStart.sessionId)}/artifacts/select`, {
164
+ method: "POST",
165
+ headers: { "Content-Type": "application/json" },
166
+ body: JSON.stringify({ artifactId: selectedArtifactId }),
167
+ });
168
+ if (!selected.ok) throw new Error("artifact selection endpoint failed");
169
+ const artifactContent = await fetchJson(
170
+ `/api/sessions/${encodeURIComponent(canvasRunStart.sessionId)}/artifacts/${encodeURIComponent(selectedArtifactId)}`
171
+ );
172
+ if (!artifactContent.text && !artifactContent.dataUrl) {
173
+ throw new Error("artifact content endpoint did not return renderable content");
174
+ }
175
+
176
+ const deleted = await fetchJson(`/api/sessions/${encodeURIComponent(runStart.sessionId)}`, {
177
+ method: "DELETE",
178
+ });
179
+ if (!deleted.ok) throw new Error("session delete failed");
180
+
181
+ const canvasDeleted = await fetchJson(`/api/sessions/${encodeURIComponent(canvasRunStart.sessionId)}`, {
182
+ method: "DELETE",
183
+ });
184
+ if (!canvasDeleted.ok) throw new Error("canvas session delete failed");
185
+
186
+ console.log(
187
+ JSON.stringify(
188
+ {
189
+ ok: true,
190
+ endpoints: [
191
+ "/api/config",
192
+ "/api/sandbox/status",
193
+ "/api/sandbox/preflight",
194
+ "/api/runs",
195
+ "/api/sessions/:id/chat",
196
+ "PATCH /api/sessions/:id",
197
+ "POST /api/sessions/:id/auto-title",
198
+ "DELETE /api/sessions/:id",
199
+ "/api/workspace/changes",
200
+ "/api/sessions/:id/artifacts",
201
+ "/api/sessions/:id/artifacts/:artifactId",
202
+ "POST /api/sessions/:id/artifacts/select",
203
+ ],
204
+ provider: run.provider,
205
+ model: run.model,
206
+ sessionId: run.sessionId,
207
+ runtimeDir,
208
+ },
209
+ null,
210
+ 2
211
+ )
212
+ );
213
+ } finally {
214
+ server.kill("SIGTERM");
215
+ await delay(150);
216
+ await fs.rm(runtimeDir, { recursive: true, force: true });
217
+ }