@alisio/core 0.1.0-alpha.8 → 0.1.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.
Files changed (189) hide show
  1. package/README.md +52 -7
  2. package/dist/agents/active.d.ts +99 -0
  3. package/dist/agents/active.js +218 -0
  4. package/dist/agents/capabilities.d.ts +26 -0
  5. package/dist/agents/capabilities.js +120 -0
  6. package/dist/agents/create-agent-skill.d.ts +11 -0
  7. package/dist/agents/create-agent-skill.js +60 -0
  8. package/dist/agents/definitions.d.ts +88 -0
  9. package/dist/agents/definitions.js +404 -0
  10. package/dist/agents/draft.d.ts +41 -0
  11. package/dist/agents/draft.js +133 -0
  12. package/dist/agents/service.d.ts +57 -0
  13. package/dist/agents/service.js +86 -0
  14. package/dist/agents/templates.d.ts +8 -0
  15. package/dist/agents/templates.js +102 -0
  16. package/dist/analysis/capabilities.d.ts +48 -0
  17. package/dist/analysis/capabilities.js +90 -0
  18. package/dist/analysis/data/csv.d.ts +46 -0
  19. package/dist/analysis/data/csv.js +240 -0
  20. package/dist/analysis/data/datasets.d.ts +132 -0
  21. package/dist/analysis/data/datasets.js +441 -0
  22. package/dist/analysis/data/describe.d.ts +27 -0
  23. package/dist/analysis/data/describe.js +103 -0
  24. package/dist/analysis/data/engine-client.d.ts +58 -0
  25. package/dist/analysis/data/engine-client.js +304 -0
  26. package/dist/analysis/data/engine-entry.d.ts +1 -0
  27. package/dist/analysis/data/engine-entry.js +59 -0
  28. package/dist/analysis/data/engine-source.d.ts +4 -0
  29. package/dist/analysis/data/engine-source.js +6 -0
  30. package/dist/analysis/data/engine-types.d.ts +86 -0
  31. package/dist/analysis/data/engine-types.js +7 -0
  32. package/dist/analysis/data/infer.d.ts +42 -0
  33. package/dist/analysis/data/infer.js +118 -0
  34. package/dist/analysis/data/ingest-worker.d.ts +17 -0
  35. package/dist/analysis/data/ingest-worker.js +365 -0
  36. package/dist/analysis/data/json.d.ts +19 -0
  37. package/dist/analysis/data/json.js +44 -0
  38. package/dist/analysis/data/query-worker.d.ts +12 -0
  39. package/dist/analysis/data/query-worker.js +75 -0
  40. package/dist/analysis/data/rows.d.ts +39 -0
  41. package/dist/analysis/data/rows.js +120 -0
  42. package/dist/analysis/data/sql-guard.d.ts +20 -0
  43. package/dist/analysis/data/sql-guard.js +129 -0
  44. package/dist/analysis/data/stats.d.ts +18 -0
  45. package/dist/analysis/data/stats.js +77 -0
  46. package/dist/analysis/install-hints.d.ts +33 -0
  47. package/dist/analysis/install-hints.js +144 -0
  48. package/dist/analysis/janitor.d.ts +60 -0
  49. package/dist/analysis/janitor.js +264 -0
  50. package/dist/analysis/jobs.d.ts +94 -0
  51. package/dist/analysis/jobs.js +165 -0
  52. package/dist/analysis/oci.d.ts +129 -0
  53. package/dist/analysis/oci.js +330 -0
  54. package/dist/analysis/python/sources.d.ts +2 -0
  55. package/dist/analysis/python/sources.js +10 -0
  56. package/dist/analysis/requirements.d.ts +32 -0
  57. package/dist/analysis/requirements.js +73 -0
  58. package/dist/analysis/rerun.d.ts +40 -0
  59. package/dist/analysis/rerun.js +136 -0
  60. package/dist/analysis/runtime-manager.d.ts +146 -0
  61. package/dist/analysis/runtime-manager.js +472 -0
  62. package/dist/analysis/status.d.ts +21 -0
  63. package/dist/analysis/status.js +52 -0
  64. package/dist/application.d.ts +118 -3
  65. package/dist/application.js +510 -19
  66. package/dist/artifacts/kinds.d.ts +35 -0
  67. package/dist/artifacts/kinds.js +177 -0
  68. package/dist/artifacts/publisher.d.ts +7 -0
  69. package/dist/artifacts/publisher.js +29 -0
  70. package/dist/artifacts/store.d.ts +119 -0
  71. package/dist/artifacts/store.js +581 -0
  72. package/dist/background/janitor.d.ts +22 -0
  73. package/dist/background/janitor.js +62 -0
  74. package/dist/background/mirror.d.ts +14 -0
  75. package/dist/background/mirror.js +41 -0
  76. package/dist/background/notify.d.ts +58 -0
  77. package/dist/background/notify.js +172 -0
  78. package/dist/background/output.d.ts +46 -0
  79. package/dist/background/output.js +179 -0
  80. package/dist/background/service.d.ts +137 -0
  81. package/dist/background/service.js +410 -0
  82. package/dist/background/store.d.ts +103 -0
  83. package/dist/background/store.js +195 -0
  84. package/dist/changelog/data.d.ts +6 -0
  85. package/dist/changelog/data.js +293 -0
  86. package/dist/changelog/index.d.ts +5 -0
  87. package/dist/changelog/index.js +7 -0
  88. package/dist/changelog/parse.d.ts +53 -0
  89. package/dist/changelog/parse.js +160 -0
  90. package/dist/commands/builtins.d.ts +10 -0
  91. package/dist/commands/builtins.js +161 -0
  92. package/dist/commands/catalog.d.ts +86 -0
  93. package/dist/commands/catalog.js +304 -0
  94. package/dist/config.d.ts +252 -5
  95. package/dist/config.js +514 -13
  96. package/dist/core/compaction.js +13 -2
  97. package/dist/core/contracts.d.ts +180 -3
  98. package/dist/core/opt-in.d.ts +12 -0
  99. package/dist/core/opt-in.js +12 -0
  100. package/dist/core/output-limit.d.ts +34 -0
  101. package/dist/core/output-limit.js +39 -0
  102. package/dist/core/run-stats.d.ts +16 -0
  103. package/dist/core/run-stats.js +8 -0
  104. package/dist/core/runner.d.ts +152 -4
  105. package/dist/core/runner.js +792 -138
  106. package/dist/core/timeout.d.ts +15 -0
  107. package/dist/core/timeout.js +61 -0
  108. package/dist/core/truncation.d.ts +30 -0
  109. package/dist/core/truncation.js +83 -0
  110. package/dist/goal/blockers.d.ts +45 -0
  111. package/dist/goal/blockers.js +51 -0
  112. package/dist/goal/budget.d.ts +24 -0
  113. package/dist/goal/budget.js +53 -0
  114. package/dist/goal/command.d.ts +36 -0
  115. package/dist/goal/command.js +98 -0
  116. package/dist/goal/format.d.ts +19 -0
  117. package/dist/goal/format.js +102 -0
  118. package/dist/goal/host.d.ts +29 -0
  119. package/dist/goal/host.js +42 -0
  120. package/dist/goal/index.d.ts +9 -0
  121. package/dist/goal/index.js +9 -0
  122. package/dist/goal/machine.d.ts +186 -0
  123. package/dist/goal/machine.js +344 -0
  124. package/dist/goal/prompts.d.ts +10 -0
  125. package/dist/goal/prompts.js +75 -0
  126. package/dist/goal/service.d.ts +113 -0
  127. package/dist/goal/service.js +273 -0
  128. package/dist/goal/store.d.ts +59 -0
  129. package/dist/goal/store.js +184 -0
  130. package/dist/index.d.ts +51 -6
  131. package/dist/index.js +49 -4
  132. package/dist/mcp/connector.js +38 -6
  133. package/dist/mcp/rich.d.ts +41 -0
  134. package/dist/mcp/rich.js +362 -0
  135. package/dist/permissions/modes.d.ts +72 -0
  136. package/dist/permissions/modes.js +98 -0
  137. package/dist/plan/exit-plan.d.ts +25 -0
  138. package/dist/plan/exit-plan.js +130 -0
  139. package/dist/plan/state.d.ts +102 -0
  140. package/dist/plan/state.js +165 -0
  141. package/dist/plugins/host.d.ts +39 -1
  142. package/dist/plugins/host.js +150 -9
  143. package/dist/providers/settings.d.ts +15 -0
  144. package/dist/providers/settings.js +38 -0
  145. package/dist/reload.d.ts +75 -0
  146. package/dist/reload.js +204 -0
  147. package/dist/resources/context.d.ts +5 -0
  148. package/dist/resources/context.js +13 -1
  149. package/dist/runtime/access.d.ts +62 -0
  150. package/dist/runtime/access.js +115 -0
  151. package/dist/runtime/blobs.d.ts +51 -0
  152. package/dist/runtime/blobs.js +177 -0
  153. package/dist/runtime/diff.d.ts +23 -0
  154. package/dist/runtime/diff.js +154 -0
  155. package/dist/runtime/ids.d.ts +8 -0
  156. package/dist/runtime/ids.js +32 -0
  157. package/dist/runtime/paths.d.ts +11 -0
  158. package/dist/runtime/paths.js +17 -1
  159. package/dist/runtime/process.d.ts +22 -0
  160. package/dist/runtime/process.js +100 -15
  161. package/dist/runtime/sqlite.d.ts +2 -0
  162. package/dist/runtime/sqlite.js +18 -0
  163. package/dist/runtime/store.d.ts +91 -3
  164. package/dist/runtime/store.js +465 -11
  165. package/dist/runtime/zip.d.ts +24 -0
  166. package/dist/runtime/zip.js +126 -0
  167. package/dist/sessions/children.js +17 -1
  168. package/dist/sessions/side-questions.d.ts +57 -0
  169. package/dist/sessions/side-questions.js +161 -0
  170. package/dist/startup/screen.js +6 -5
  171. package/dist/tools/analysis.d.ts +37 -0
  172. package/dist/tools/analysis.js +573 -0
  173. package/dist/tools/artifacts.d.ts +14 -0
  174. package/dist/tools/artifacts.js +143 -0
  175. package/dist/tools/background.d.ts +7 -0
  176. package/dist/tools/background.js +165 -0
  177. package/dist/tools/data.d.ts +7 -0
  178. package/dist/tools/data.js +160 -0
  179. package/dist/tools/execute.d.ts +5 -0
  180. package/dist/tools/execute.js +15 -3
  181. package/dist/tools/goal.d.ts +19 -0
  182. package/dist/tools/goal.js +126 -0
  183. package/dist/tools/search.d.ts +17 -3
  184. package/dist/tools/search.js +153 -0
  185. package/dist/tools/standard.d.ts +4 -1
  186. package/dist/tools/standard.js +99 -18
  187. package/dist/version.d.ts +5 -3
  188. package/dist/version.js +35 -6
  189. package/package.json +2 -2
@@ -0,0 +1,573 @@
1
+ /**
2
+ * `python_run` (effect `process`, capability `analysis.run`), `artifact_create` (effect
3
+ * `internal`) and `artifact_list` (effect `read`). The script runs with the discovered Python in a
4
+ * job folder outside the repository; whatever it leaves in `$ALISIO_OUTPUT_DIR` is published as
5
+ * artifacts. Managed Python is NOT a sandbox.
6
+ */
7
+ import { open } from "node:fs/promises";
8
+ import { basename, join } from "node:path";
9
+ import { textResult, } from "@alisio/sdk";
10
+ import { detectHintHost, installHintsMarkdown, installHintsText, pythonInstallHints, } from "../analysis/install-hints.js";
11
+ import { ExtrasInstallError, installPreview, setupCommand, } from "../analysis/runtime-manager.js";
12
+ import { KIND_LABELS } from "../artifacts/kinds.js";
13
+ import { ArtifactRejected, toRef } from "../artifacts/store.js";
14
+ import { safePath } from "../runtime/paths.js";
15
+ import { runProcess } from "../runtime/process.js";
16
+ import { objectSchema } from "./standard.js";
17
+ export const NOT_SANDBOXED = "Managed Python is not a sandbox: the script runs with your user permissions and can read your files and use the network.";
18
+ const TERMINAL_BYTES = 256 * 1024;
19
+ const TEXT_TAIL = 4 * 1024;
20
+ const MAX_CODE = 200_000;
21
+ const MAX_TEXT = 5 * 1024 * 1024;
22
+ const KINDS = [
23
+ "dashboard",
24
+ "document",
25
+ "spreadsheet",
26
+ "image",
27
+ "data",
28
+ "code",
29
+ "archive",
30
+ "file",
31
+ ];
32
+ /** `48 KB`, `1.2 MB`. */
33
+ export function formatBytes(bytes) {
34
+ if (bytes < 1024)
35
+ return `${bytes} B`;
36
+ if (bytes < 1024 * 1024)
37
+ return `${Math.round(bytes / 1024)} KB`;
38
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
39
+ }
40
+ const tail = (text, bytes) => {
41
+ const buffer = Buffer.from(text);
42
+ return buffer.byteLength <= bytes
43
+ ? text
44
+ : buffer
45
+ .subarray(buffer.byteLength - bytes)
46
+ .toString("utf8")
47
+ .replace(/^�+/, "");
48
+ };
49
+ async function readTail(path, bytes) {
50
+ try {
51
+ const handle = await open(path, "r");
52
+ try {
53
+ const { size } = await handle.stat();
54
+ const length = Math.min(size, bytes);
55
+ const buffer = Buffer.alloc(length);
56
+ await handle.read(buffer, 0, length, size - length);
57
+ return buffer.toString("utf8").replace(/^�+/, "");
58
+ }
59
+ finally {
60
+ await handle.close();
61
+ }
62
+ }
63
+ catch {
64
+ return "";
65
+ }
66
+ }
67
+ const describeArtifact = (p) => `${p.artifact.fileName} (${p.artifact.kind}, ${formatBytes(p.artifact.bytes)}, ${p.artifact.id})`;
68
+ const artifactBlocks = (published) => published.map((p) => ({
69
+ type: "ui",
70
+ block: { kind: "artifact", artifact: p.artifact },
71
+ }));
72
+ function publisherOf(context) {
73
+ const publisher = context.artifacts;
74
+ if (!publisher || typeof publisher.publishOutputs !== "function")
75
+ throw new Error("Artifacts are not available here (no artifact store for this session)");
76
+ return publisher;
77
+ }
78
+ /** Environment of the script: the usual whitelist, HOME/TMP inside work/, and the job paths. */
79
+ function scriptEnv(job) {
80
+ const keep = [
81
+ "PATH",
82
+ "LANG",
83
+ "LC_ALL",
84
+ "SystemRoot",
85
+ "COMSPEC",
86
+ "PATHEXT",
87
+ "SYSTEMDRIVE",
88
+ "WINDIR",
89
+ ];
90
+ return {
91
+ ...Object.fromEntries(keep.flatMap((k) => (process.env[k] ? [[k, process.env[k]]] : []))),
92
+ HOME: job.work,
93
+ USERPROFILE: job.work,
94
+ TMPDIR: job.work,
95
+ TEMP: job.work,
96
+ TMP: job.work,
97
+ ALISIO_INPUT_DIR: job.input,
98
+ ALISIO_OUTPUT_DIR: job.staging,
99
+ ALISIO_WORK_DIR: job.work,
100
+ ALISIO_EXECUTION_ID: job.id,
101
+ MPLBACKEND: "Agg",
102
+ PYTHONDONTWRITEBYTECODE: "1",
103
+ PYTHONUNBUFFERED: "1",
104
+ PYTHONNOUSERSITE: "1",
105
+ PYTHONIOENCODING: "utf-8",
106
+ };
107
+ }
108
+ const CONTAINER_NOTE = "This run happens in a container (Docker or Podman) with no network access, a read-only root and only the job folders mounted; it is isolation with limits, not a hardened sandbox.";
109
+ function pythonRunDescription(deps) {
110
+ const container = deps.oci?.mode() === "oci";
111
+ const modules = container
112
+ ? "whatever the configured container image provides (and alisio_runtime)"
113
+ : deps.startup.extras.length
114
+ ? `the standard library, alisio_runtime and the "${deps.startup.extras.join(", ")}" extras (pandas, numpy, matplotlib…)`
115
+ : 'the Python standard library and alisio_runtime only (no pandas or numpy; use csv, statistics, sqlite3). Pass extras: ["analysis"] to use pandas, numpy, matplotlib, openpyxl, python-docx, reportlab, plotly and jinja2: if they are not installed the user is asked once to approve a download, and without approval the call fails';
116
+ return [
117
+ "Write and run a Python 3.10+ script in a job folder managed by Alisio, outside the repository.",
118
+ "Every file the script writes to $ALISIO_OUTPUT_DIR is published as a downloadable artifact",
119
+ "(HTML dashboard, Markdown, PDF, DOCX, XLSX, CSV, PNG/SVG, JSON, ZIP or any other file); a",
120
+ "top-level folder with index.html becomes one multi-file dashboard. Optionally write",
121
+ '$ALISIO_OUTPUT_DIR/outputs.json = {"artifacts":[{"path":"…","title":"…"}]} to publish only',
122
+ "those. The cwd is $ALISIO_WORK_DIR (scratch); inputs are copied to $ALISIO_INPUT_DIR.",
123
+ `Available modules: ${modules}. alisio_runtime offers output_dir(), html.page/table/write and`,
124
+ "svg.bar/line/scatter (dependency-free charts). inputs: [{ datasetId }] copies a dataset (see",
125
+ "data_inspect) to $ALISIO_INPUT_DIR/<name>.sqlite: alisio_runtime.datasets.open(name) returns a",
126
+ "read-only sqlite3 connection (table `data` or `s_<sheet>`). HTML is shown offline: embed data and scripts",
127
+ "inline (no fetch, no CDN). Nothing is published when the script fails unless publishOnError.",
128
+ "Keep each call's code short: the model's output is capped per response, so split large scripts or dashboards into several calls or files.",
129
+ "To run an earlier analysis again unchanged (same script, inputs checked by hash, new artifacts) pass rerunOf: an artifact or execution id of this session, instead of code.",
130
+ container ? CONTAINER_NOTE : NOT_SANDBOXED,
131
+ ...(deps.startup.candidates || container
132
+ ? []
133
+ : ["Python was not found at startup: the tool answers with install instructions."]),
134
+ ].join(" ");
135
+ }
136
+ const failure = (text) => ({
137
+ content: [{ type: "text", text }],
138
+ isError: true,
139
+ });
140
+ export function registerAnalysisTools(registry, deps) {
141
+ /**
142
+ * The optional extras (pandas…): when they are not installed, installing them needs an
143
+ * `analysis.install` approval (once, with the packages, the size and the network need). Where no
144
+ * one can be asked, or the answer is no, or the download fails, nothing is installed and the
145
+ * result names the optional command. Returns a failure result, or undefined when ready.
146
+ */
147
+ async function ensureExtras(extras, context) {
148
+ const remedy = `To install them yourself (optional): ${setupCommand(extras)}. Until then, use the standard library.`;
149
+ if (!deps.runtime.hasExtras || !deps.runtime.installExtras)
150
+ return failure(`extras_unavailable: the "${extras}" extras cannot be installed here. ${remedy}`);
151
+ if (await deps.runtime.hasExtras(extras))
152
+ return undefined;
153
+ const verdict = context.approveInstall
154
+ ? await context.approveInstall(installPreview(extras))
155
+ : "deny";
156
+ if (verdict !== "once")
157
+ return failure(`extras_not_installed: the "${extras}" extras (pandas, numpy…) are not installed, and installing them needs the user's approval and network access: it was denied, or nobody could be asked (headless runs never install; no flag allows it). ${remedy}`);
158
+ try {
159
+ await deps.runtime.installExtras(extras, {
160
+ signal: context.signal,
161
+ onOutput: context.emit,
162
+ });
163
+ }
164
+ catch (error) {
165
+ if (context.signal.aborted)
166
+ throw error;
167
+ if (error instanceof ExtrasInstallError)
168
+ return failure(`extras_install_failed: ${error.message}`);
169
+ return failure(`extras_install_failed: ${error instanceof Error ? error.message : String(error)}. ${remedy}`);
170
+ }
171
+ return undefined;
172
+ }
173
+ registry.register({
174
+ name: "python_run",
175
+ effect: "process",
176
+ capability: "analysis.run",
177
+ description: pythonRunDescription(deps),
178
+ inputSchema: objectSchema({
179
+ code: { type: "string", minLength: 1, maxLength: MAX_CODE },
180
+ title: { type: "string", maxLength: 200 },
181
+ inputs: {
182
+ type: "array",
183
+ maxItems: 20,
184
+ items: objectSchema({
185
+ path: { type: "string", minLength: 1 },
186
+ datasetId: { type: "string", minLength: 1, maxLength: 64 },
187
+ }),
188
+ },
189
+ timeoutMs: { type: "integer", minimum: 1000, maximum: deps.limits.timeoutMs },
190
+ publishOnError: { type: "boolean" },
191
+ extras: {
192
+ type: "array",
193
+ maxItems: 2,
194
+ items: { type: "string", enum: ["analysis", "science"] },
195
+ },
196
+ rerunOf: { type: "string", minLength: 5, maxLength: 64 },
197
+ }, []),
198
+ async execute(input, context) {
199
+ const publisher = publisherOf(context);
200
+ const sessionId = context.session ?? "headless";
201
+ const root = deps.rootOf(sessionId);
202
+ const rerunRef = typeof input.rerunOf === "string" ? input.rerunOf.trim() : undefined;
203
+ if (rerunRef && input.code !== undefined)
204
+ throw new Error("Pass either code or rerunOf, not both");
205
+ if (!rerunRef && typeof input.code !== "string")
206
+ throw new Error("python_run needs code (or rerunOf to run an earlier analysis again)");
207
+ // Rerun: the saved script and inputs, verified by hash; a mismatch runs nothing.
208
+ let rerun;
209
+ if (rerunRef) {
210
+ if (!deps.rerun)
211
+ throw new Error("Rerun is not available here");
212
+ const prepared = await deps.rerun.prepare(rerunRef, root);
213
+ if (!prepared.ok)
214
+ return failure(`rerun refused; nothing was executed:\n${prepared.problems.map((p) => `- ${p}`).join("\n")}`);
215
+ rerun = prepared.plan;
216
+ }
217
+ const code = rerun ? rerun.code : String(input.code);
218
+ const container = deps.oci?.mode() === "oci" ? deps.oci.runtime() : undefined;
219
+ // Container mode needs neither host Python nor extras (the image carries them).
220
+ let runtime;
221
+ if (container) {
222
+ const unavailable = await container.unavailable(context.signal);
223
+ if (unavailable)
224
+ return failure(`runtime_unavailable: ${unavailable}`);
225
+ }
226
+ else {
227
+ runtime = await deps.runtime.interpreter(context.signal);
228
+ if (!runtime.ok) {
229
+ const hints = pythonInstallHints(await detectHintHost(runtime.found));
230
+ return {
231
+ content: [
232
+ {
233
+ type: "text",
234
+ text: `runtime_unavailable: ${runtime.reason}.\n${installHintsText(hints)}`,
235
+ },
236
+ { type: "ui", block: { kind: "markdown", text: installHintsMarkdown(hints) } },
237
+ ],
238
+ isError: true,
239
+ };
240
+ }
241
+ const wanted = new Set([
242
+ ...(input.extras ?? []),
243
+ ...(rerun?.extras ?? []),
244
+ ]);
245
+ if (wanted.size) {
246
+ const need = wanted.has("science") ? "science" : "analysis";
247
+ const outcome = await ensureExtras(need, context);
248
+ if (outcome)
249
+ return outcome;
250
+ runtime = await deps.runtime.interpreter(context.signal);
251
+ if (!runtime.ok)
252
+ return failure(`runtime_unavailable: ${runtime.reason}`);
253
+ }
254
+ }
255
+ const job = await deps.jobs.create({
256
+ sessionId,
257
+ rootSessionId: root,
258
+ workspace: context.workspace,
259
+ ...(context.runId ? { runId: context.runId } : {}),
260
+ ...(context.callId ? { callId: context.callId } : {}),
261
+ code,
262
+ runtime: container ? "oci" : "managed",
263
+ ...(rerun ? { rerunOf: rerun.executionId } : {}),
264
+ });
265
+ const inputs = [];
266
+ let manifest = [];
267
+ const extrasAsked = [
268
+ ...new Set([
269
+ ...(input.extras ?? []),
270
+ ...(rerun?.extras ?? []),
271
+ ]),
272
+ ];
273
+ try {
274
+ if (rerun) {
275
+ // The verified copies of the original run, under the same names.
276
+ for (const item of rerun.inputs) {
277
+ const added = await deps.jobs.addInput(job, item.source, item.name);
278
+ if (added.sha256 !== (item.copySha256 ?? item.sha256) && !item.datasetId)
279
+ throw new Error(`Input ${item.name} changed while the rerun started`);
280
+ inputs.push({
281
+ ...added,
282
+ sha256: item.sha256,
283
+ copySha256: added.sha256,
284
+ ...(item.datasetId ? { datasetId: item.datasetId } : {}),
285
+ });
286
+ }
287
+ manifest = rerun.manifest;
288
+ }
289
+ else
290
+ for (const item of input.inputs ?? []) {
291
+ if ((item.path === undefined) === (item.datasetId === undefined))
292
+ throw new Error("Each input needs exactly one of path or datasetId");
293
+ if (item.datasetId !== undefined) {
294
+ if (!deps.datasets)
295
+ throw new Error("Datasets are not available here");
296
+ const record = deps.datasets.getFor(item.datasetId, root);
297
+ const added = await deps.jobs.addInput(job, deps.datasets.file(record), `${basename(record.name)
298
+ .replace(/\.[^.]*$/, "")
299
+ .replace(/[^\w.-]+/g, "_") || "dataset"}.sqlite`);
300
+ // Provenance names the original content, so a rerun can verify it by hash.
301
+ inputs.push({
302
+ ...added,
303
+ sha256: record.sha256,
304
+ copySha256: added.sha256,
305
+ datasetId: record.id,
306
+ });
307
+ manifest.push({
308
+ name: added.name,
309
+ kind: "dataset",
310
+ datasetId: record.id,
311
+ source: record.name,
312
+ format: record.format,
313
+ sheets: record.sheets,
314
+ });
315
+ continue;
316
+ }
317
+ const resolved = context.resolvePath
318
+ ? await context.resolvePath(item.path)
319
+ : await safePath(context.workspace, item.path);
320
+ const added = await deps.jobs.addInput(job, resolved);
321
+ inputs.push({ ...added, copySha256: added.sha256 });
322
+ manifest.push({ name: added.name, kind: "file", bytes: added.bytes });
323
+ }
324
+ if (manifest.length)
325
+ await deps.jobs.writeInputsManifest(job, manifest);
326
+ await deps.jobs.recordInputs(job, {
327
+ inputs,
328
+ manifest,
329
+ ...(extrasAsked.length ? { extras: extrasAsked } : {}),
330
+ });
331
+ }
332
+ catch (error) {
333
+ deps.jobs.finish(job.id, "failed", {
334
+ error: error instanceof Error ? error.message : String(error),
335
+ });
336
+ throw error;
337
+ }
338
+ const timeoutMs = Math.min(Number(input.timeoutMs ?? deps.limits.timeoutMs), deps.limits.timeoutMs);
339
+ const started = Date.now();
340
+ const logs = { stdout: join(job.logs, "stdout.log"), stderr: join(job.logs, "stderr.log") };
341
+ let result;
342
+ try {
343
+ if (container)
344
+ result = await container.run({
345
+ executionId: job.id,
346
+ mounts: {
347
+ script: job.script,
348
+ input: job.input,
349
+ work: job.work,
350
+ staging: job.staging,
351
+ },
352
+ signal: context.signal,
353
+ timeoutMs,
354
+ maxBytes: TERMINAL_BYTES,
355
+ logFiles: logs,
356
+ maxLogBytes: deps.limits.maxLogBytes,
357
+ onData: context.emit,
358
+ });
359
+ else {
360
+ const interpreter = runtime;
361
+ const args = [
362
+ ...(interpreter.prefixArgs ?? []),
363
+ "-E",
364
+ "-s",
365
+ "-B",
366
+ "-u",
367
+ "-X",
368
+ "utf8",
369
+ job.main,
370
+ ];
371
+ result = await runProcess(interpreter.executable, args, {
372
+ cwd: job.work,
373
+ signal: context.signal,
374
+ timeoutMs,
375
+ maxBytes: TERMINAL_BYTES,
376
+ onOverflow: "truncate",
377
+ logFiles: logs,
378
+ maxLogBytes: deps.limits.maxLogBytes,
379
+ env: scriptEnv(job),
380
+ onData: context.emit,
381
+ });
382
+ }
383
+ }
384
+ catch (error) {
385
+ if (context.signal.aborted) {
386
+ deps.jobs.finish(job.id, "cancelled");
387
+ throw error;
388
+ }
389
+ const timedOut = error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
390
+ deps.jobs.finish(job.id, timedOut ? "timed_out" : "failed", {
391
+ error: error instanceof Error ? error.message : String(error),
392
+ });
393
+ if (!timedOut)
394
+ throw error;
395
+ const output = `${await readTail(logs.stdout, TEXT_TAIL)}${await readTail(logs.stderr, TEXT_TAIL)}`;
396
+ return {
397
+ content: [
398
+ {
399
+ type: "text",
400
+ text: `timed out after ${Math.round(timeoutMs / 1000)}s; nothing was published (${job.id})${output ? `\n--- output (last 4 KB) ---\n${tail(output, TEXT_TAIL)}` : ""}`,
401
+ },
402
+ {
403
+ type: "ui",
404
+ block: {
405
+ kind: "terminal",
406
+ command: "python main.py",
407
+ output,
408
+ durationMs: Date.now() - started,
409
+ truncated: true,
410
+ },
411
+ },
412
+ ],
413
+ isError: true,
414
+ };
415
+ }
416
+ const durationMs = Date.now() - started;
417
+ const failed = result.exitCode !== 0;
418
+ let published = [];
419
+ let publishError;
420
+ if (!failed || input.publishOnError === true) {
421
+ try {
422
+ const settings = container?.settings;
423
+ const interpreter = runtime?.ok ? runtime : undefined;
424
+ published = await publisher.publishOutputs(job.staging, {
425
+ executionId: job.id,
426
+ ...(typeof input.title === "string" && input.title.trim()
427
+ ? { title: input.title.trim() }
428
+ : {}),
429
+ ...(failed ? { partial: true } : {}),
430
+ provenance: {
431
+ runtime: settings
432
+ ? {
433
+ mode: "oci",
434
+ engine: settings.engine,
435
+ ...(settings.image ? { image: settings.image } : {}),
436
+ extras: [],
437
+ }
438
+ : {
439
+ mode: "managed",
440
+ python: interpreter?.version,
441
+ extras: interpreter?.extras ?? [],
442
+ ...(interpreter?.runtimeVersion
443
+ ? { runtimeVersion: interpreter.runtimeVersion }
444
+ : {}),
445
+ },
446
+ scriptSha256: job.scriptSha256,
447
+ inputs: inputs.map((i) => ({
448
+ name: i.name,
449
+ sha256: i.sha256,
450
+ ...(i.datasetId ? { datasetId: i.datasetId } : {}),
451
+ })),
452
+ ...(rerun
453
+ ? {
454
+ rerunOf: rerun.executionId,
455
+ ...(rerun.artifactId ? { rerunOfArtifact: rerun.artifactId } : {}),
456
+ }
457
+ : {}),
458
+ },
459
+ });
460
+ }
461
+ catch (error) {
462
+ if (!(error instanceof ArtifactRejected))
463
+ throw error;
464
+ publishError = error.message;
465
+ }
466
+ }
467
+ deps.jobs.finish(job.id, failed || publishError ? "failed" : "completed", {
468
+ exitCode: result.exitCode,
469
+ ...(publishError ? { error: `publish: ${publishError}` } : {}),
470
+ });
471
+ const warnings = published.flatMap((p) => p.warnings);
472
+ const head = [
473
+ `${rerun ? `rerun of ${rerun.executionId} · ` : ""}exit ${result.exitCode} in ${(durationMs / 1000).toFixed(1)}s`,
474
+ publishError
475
+ ? `publication rejected (nothing was published): ${publishError}`
476
+ : failed && input.publishOnError !== true
477
+ ? "nothing was published (non-zero exit; pass publishOnError to keep partial outputs)"
478
+ : `published ${published.length} artifact${published.length === 1 ? "" : "s"}${published.length ? `: ${published.map(describeArtifact).join(", ")}` : ""}`,
479
+ ].join(" · ");
480
+ const sections = [
481
+ head,
482
+ ...warnings.map((w) => `warning: ${w}`),
483
+ ...(result.stdout.trim()
484
+ ? [`--- stdout (last 4 KB) ---\n${tail(result.stdout, TEXT_TAIL)}`]
485
+ : []),
486
+ ...(result.stderr.trim()
487
+ ? [`--- stderr (last 4 KB) ---\n${tail(result.stderr, TEXT_TAIL)}`]
488
+ : []),
489
+ ];
490
+ const separator = result.stdout && result.stderr && !result.stdout.endsWith("\n") ? "\n" : "";
491
+ return {
492
+ content: [
493
+ { type: "text", text: sections.join("\n") },
494
+ {
495
+ type: "ui",
496
+ block: {
497
+ kind: "terminal",
498
+ command: "python main.py",
499
+ output: tail(`${result.stdout}${separator}${result.stderr}`, TERMINAL_BYTES),
500
+ exitCode: result.exitCode,
501
+ durationMs,
502
+ ...(result.truncated ? { truncated: true } : {}),
503
+ },
504
+ },
505
+ ...artifactBlocks(published),
506
+ ],
507
+ ...(failed || publishError ? { isError: true } : {}),
508
+ };
509
+ },
510
+ });
511
+ registry.register({
512
+ name: "artifact_create",
513
+ effect: "internal",
514
+ description: "Publish text you already have (Markdown, HTML, CSV, JSON, SVG, plain text…) as a " +
515
+ "downloadable artifact of this session; works without Python. fileName decides the type " +
516
+ "(e.g. report.md, dashboard.html). HTML is shown offline: inline every script and style.",
517
+ inputSchema: objectSchema({
518
+ fileName: { type: "string", minLength: 1, maxLength: 200 },
519
+ title: { type: "string", maxLength: 200 },
520
+ text: { type: "string", maxLength: MAX_TEXT },
521
+ }, ["fileName", "text"]),
522
+ async execute(input, context) {
523
+ const publisher = publisherOf(context);
524
+ const fileName = String(input.fileName);
525
+ if (basename(fileName.replace(/\\/g, "/")) !== fileName)
526
+ throw new Error("fileName must be a plain file name without folders");
527
+ try {
528
+ const published = await publisher.publishTextDetailed({
529
+ fileName,
530
+ text: String(input.text),
531
+ ...(typeof input.title === "string" && input.title.trim()
532
+ ? { title: input.title.trim() }
533
+ : {}),
534
+ });
535
+ return {
536
+ content: [
537
+ { type: "text", text: `published ${describeArtifact(published)}` },
538
+ ...artifactBlocks([published]),
539
+ ],
540
+ };
541
+ }
542
+ catch (error) {
543
+ if (error instanceof ArtifactRejected)
544
+ throw new Error(`Publication rejected: ${error.message}`);
545
+ throw error;
546
+ }
547
+ },
548
+ });
549
+ registry.register({
550
+ name: "artifact_list",
551
+ effect: "read",
552
+ description: "List the downloadable artifacts of this session, newest first.",
553
+ inputSchema: objectSchema({
554
+ kind: { type: "string", enum: KINDS },
555
+ limit: { type: "integer", minimum: 1, maximum: 100 },
556
+ }),
557
+ async execute(input, context) {
558
+ const root = deps.rootOf(context.session ?? "");
559
+ const records = deps.store.list(root, {
560
+ ...(typeof input.kind === "string" ? { kind: input.kind } : {}),
561
+ limit: Number(input.limit ?? 50),
562
+ });
563
+ if (!records.length)
564
+ return textResult("No artifacts in this session yet.");
565
+ return textResult(records
566
+ .map((r) => {
567
+ const ref = toRef(r);
568
+ return `${ref.id} · ${KIND_LABELS[ref.kind]} · ${ref.fileName} · ${formatBytes(ref.bytes)}${ref.partial ? " · partial" : ""} · ${new Date(ref.createdAt).toISOString()}${ref.title !== ref.fileName ? ` · "${ref.title}"` : ""}`;
569
+ })
570
+ .join("\n"));
571
+ },
572
+ });
573
+ }
@@ -0,0 +1,14 @@
1
+ import type { ToolResult } from "@alisio/sdk";
2
+ import { type ArtifactStore } from "../artifacts/store.ts";
3
+ import type { ToolRegistry } from "../core/registry.ts";
4
+ export declare const ARTIFACT_READ_MAX: number;
5
+ export interface ArtifactToolDeps {
6
+ store: ArtifactStore;
7
+ rootOf: (sessionId: string) => string;
8
+ }
9
+ /**
10
+ * Workspace-relative paths an `artifact_export` result reports, in order (the Changes view of the
11
+ * web reads them; the first line of the text names the artifact and the target).
12
+ */
13
+ export declare function exportedPaths(result: ToolResult | undefined): string[];
14
+ export declare function registerArtifactTools(registry: ToolRegistry, deps: ArtifactToolDeps): void;