@alisio/core 0.1.0-alpha.21 → 0.1.0-alpha.22

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 (89) hide show
  1. package/dist/analysis/capabilities.d.ts +48 -0
  2. package/dist/analysis/capabilities.js +90 -0
  3. package/dist/analysis/data/csv.d.ts +46 -0
  4. package/dist/analysis/data/csv.js +240 -0
  5. package/dist/analysis/data/datasets.d.ts +132 -0
  6. package/dist/analysis/data/datasets.js +441 -0
  7. package/dist/analysis/data/describe.d.ts +27 -0
  8. package/dist/analysis/data/describe.js +103 -0
  9. package/dist/analysis/data/engine-client.d.ts +58 -0
  10. package/dist/analysis/data/engine-client.js +304 -0
  11. package/dist/analysis/data/engine-entry.d.ts +1 -0
  12. package/dist/analysis/data/engine-entry.js +59 -0
  13. package/dist/analysis/data/engine-source.d.ts +4 -0
  14. package/dist/analysis/data/engine-source.js +6 -0
  15. package/dist/analysis/data/engine-types.d.ts +86 -0
  16. package/dist/analysis/data/engine-types.js +7 -0
  17. package/dist/analysis/data/infer.d.ts +42 -0
  18. package/dist/analysis/data/infer.js +118 -0
  19. package/dist/analysis/data/ingest-worker.d.ts +17 -0
  20. package/dist/analysis/data/ingest-worker.js +365 -0
  21. package/dist/analysis/data/json.d.ts +19 -0
  22. package/dist/analysis/data/json.js +44 -0
  23. package/dist/analysis/data/query-worker.d.ts +12 -0
  24. package/dist/analysis/data/query-worker.js +75 -0
  25. package/dist/analysis/data/rows.d.ts +39 -0
  26. package/dist/analysis/data/rows.js +120 -0
  27. package/dist/analysis/data/sql-guard.d.ts +20 -0
  28. package/dist/analysis/data/sql-guard.js +129 -0
  29. package/dist/analysis/data/stats.d.ts +18 -0
  30. package/dist/analysis/data/stats.js +77 -0
  31. package/dist/analysis/install-hints.d.ts +33 -0
  32. package/dist/analysis/install-hints.js +144 -0
  33. package/dist/analysis/janitor.d.ts +60 -0
  34. package/dist/analysis/janitor.js +264 -0
  35. package/dist/analysis/jobs.d.ts +94 -0
  36. package/dist/analysis/jobs.js +165 -0
  37. package/dist/analysis/oci.d.ts +129 -0
  38. package/dist/analysis/oci.js +330 -0
  39. package/dist/analysis/python/sources.d.ts +2 -0
  40. package/dist/analysis/python/sources.js +10 -0
  41. package/dist/analysis/requirements.d.ts +32 -0
  42. package/dist/analysis/requirements.js +73 -0
  43. package/dist/analysis/rerun.d.ts +40 -0
  44. package/dist/analysis/rerun.js +136 -0
  45. package/dist/analysis/runtime-manager.d.ts +146 -0
  46. package/dist/analysis/runtime-manager.js +472 -0
  47. package/dist/analysis/status.d.ts +21 -0
  48. package/dist/analysis/status.js +52 -0
  49. package/dist/application.d.ts +41 -0
  50. package/dist/application.js +195 -0
  51. package/dist/artifacts/kinds.d.ts +35 -0
  52. package/dist/artifacts/kinds.js +177 -0
  53. package/dist/artifacts/publisher.d.ts +7 -0
  54. package/dist/artifacts/publisher.js +29 -0
  55. package/dist/artifacts/store.d.ts +119 -0
  56. package/dist/artifacts/store.js +581 -0
  57. package/dist/commands/builtins.js +13 -0
  58. package/dist/config.d.ts +102 -0
  59. package/dist/config.js +167 -6
  60. package/dist/core/contracts.d.ts +69 -1
  61. package/dist/core/runner.d.ts +34 -4
  62. package/dist/core/runner.js +388 -130
  63. package/dist/index.d.ts +23 -4
  64. package/dist/index.js +22 -3
  65. package/dist/mcp/rich.js +4 -0
  66. package/dist/plugins/host.js +14 -6
  67. package/dist/runtime/blobs.d.ts +22 -0
  68. package/dist/runtime/blobs.js +84 -1
  69. package/dist/runtime/ids.d.ts +8 -0
  70. package/dist/runtime/ids.js +32 -0
  71. package/dist/runtime/paths.d.ts +5 -0
  72. package/dist/runtime/paths.js +6 -0
  73. package/dist/runtime/process.d.ts +12 -0
  74. package/dist/runtime/process.js +64 -4
  75. package/dist/runtime/sqlite.d.ts +2 -0
  76. package/dist/runtime/sqlite.js +18 -0
  77. package/dist/runtime/store.d.ts +5 -0
  78. package/dist/runtime/store.js +75 -0
  79. package/dist/runtime/zip.d.ts +24 -0
  80. package/dist/runtime/zip.js +126 -0
  81. package/dist/sessions/children.js +4 -0
  82. package/dist/tools/analysis.d.ts +37 -0
  83. package/dist/tools/analysis.js +572 -0
  84. package/dist/tools/artifacts.d.ts +14 -0
  85. package/dist/tools/artifacts.js +143 -0
  86. package/dist/tools/data.d.ts +7 -0
  87. package/dist/tools/data.js +160 -0
  88. package/dist/tools/execute.js +6 -2
  89. package/package.json +2 -2
@@ -0,0 +1,143 @@
1
+ /**
2
+ * `artifact_read` (effect `read`) and `artifact_export` (effect `write`), spec §12. Reading is
3
+ * bounded (64 KiB, truncation marked); exporting copies a published artifact into the workspace,
4
+ * confined to it, through the existing `write` gate. Both only see the caller's root session.
5
+ */
6
+ import { constants } from "node:fs";
7
+ import { copyFile, lstat, mkdir, open, realpath } from "node:fs/promises";
8
+ import { dirname, join, relative, sep } from "node:path";
9
+ import { KIND_LABELS, looksLikeText } from "../artifacts/kinds.js";
10
+ import { readHead } from "../artifacts/store.js";
11
+ import { safePath } from "../runtime/paths.js";
12
+ import { objectSchema } from "./standard.js";
13
+ export const ARTIFACT_READ_MAX = 64 * 1024;
14
+ const EXPORTED = "exported ";
15
+ /** A ready artifact of the caller's root session, or a model-readable error. */
16
+ function ownArtifact(deps, id, session) {
17
+ const record = typeof id === "string" ? deps.store.get(id) : undefined;
18
+ if (!record || record.sessionId !== deps.rootOf(session ?? ""))
19
+ throw new Error(`Artifact ${String(id)} not found in this session (see artifact_list)`);
20
+ if (record.status !== "ready")
21
+ throw new Error(`Artifact ${record.id} is ${record.status}`);
22
+ return record;
23
+ }
24
+ /**
25
+ * Workspace-relative paths an `artifact_export` result reports, in order (the Changes view of the
26
+ * web reads them; the first line of the text names the artifact and the target).
27
+ */
28
+ export function exportedPaths(result) {
29
+ if (!result || result.isError)
30
+ return [];
31
+ const text = result.content.find((part) => part.type === "text");
32
+ if (!text || text.type !== "text" || !text.text.startsWith(EXPORTED))
33
+ return [];
34
+ return text.text
35
+ .split("\n")
36
+ .slice(1)
37
+ .map((line) => line.trim())
38
+ .filter(Boolean);
39
+ }
40
+ const toPosix = (path) => path.split(sep).join("/");
41
+ export function registerArtifactTools(registry, deps) {
42
+ registry.register({
43
+ name: "artifact_read",
44
+ effect: "read",
45
+ description: "Read a published text artifact of this session (Markdown, CSV, JSON, HTML, code…), at " +
46
+ "most 64 KiB; longer files are truncated and say so. path selects one file inside a " +
47
+ "multi-file artifact (default: its entry).",
48
+ inputSchema: objectSchema({
49
+ id: { type: "string", minLength: 1, maxLength: 64 },
50
+ path: { type: "string", minLength: 1, maxLength: 512 },
51
+ maxBytes: { type: "integer", minimum: 1, maximum: ARTIFACT_READ_MAX },
52
+ }, ["id"]),
53
+ async execute(input, context) {
54
+ const record = ownArtifact(deps, input.id, context.session);
55
+ const wanted = typeof input.path === "string" ? input.path : (record.entry ?? record.fileName);
56
+ const file = await deps.store.resolveFile(record, wanted);
57
+ if (!file)
58
+ throw new Error(`${wanted} is not a file of ${record.id}; files: ${(await deps.store.files(record))
59
+ .map((f) => f.path)
60
+ .slice(0, 50)
61
+ .join(", ")}`);
62
+ if (!looksLikeText(await readHead(file.abs)))
63
+ throw new Error(`${file.path} is not a text file; download it instead`);
64
+ const max = Math.min(Number(input.maxBytes ?? ARTIFACT_READ_MAX), ARTIFACT_READ_MAX);
65
+ const handle = await open(file.abs, "r");
66
+ let text;
67
+ try {
68
+ const buffer = Buffer.alloc(Math.min(max, file.bytes));
69
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
70
+ // A multi-byte character cut at the limit decodes as U+FFFD: drop it.
71
+ text = buffer.subarray(0, bytesRead).toString("utf8").replace(/�+$/, "");
72
+ }
73
+ finally {
74
+ await handle.close();
75
+ }
76
+ const truncated = file.bytes > max;
77
+ return {
78
+ content: [
79
+ {
80
+ type: "text",
81
+ text: [
82
+ `${record.id} · ${KIND_LABELS[record.kind]} · ${file.path} · ${file.bytes} bytes`,
83
+ text,
84
+ ...(truncated ? [`[truncated: showing ${max} of ${file.bytes} bytes]`] : []),
85
+ ].join("\n"),
86
+ },
87
+ ],
88
+ };
89
+ },
90
+ });
91
+ registry.register({
92
+ name: "artifact_export",
93
+ effect: "write",
94
+ description: "Copy a published artifact of this session into the workspace (target: a workspace-" +
95
+ "relative directory; '.' is the root). A multi-file artifact becomes a folder named after " +
96
+ "it. Existing files are kept unless overwrite is true.",
97
+ inputSchema: objectSchema({
98
+ id: { type: "string", minLength: 1, maxLength: 64 },
99
+ target: { type: "string", maxLength: 1024 },
100
+ overwrite: { type: "boolean" },
101
+ }, ["id", "target"]),
102
+ async execute(input, context) {
103
+ const record = ownArtifact(deps, input.id, context.session);
104
+ const workspace = await realpath(context.workspace);
105
+ // `\` and `/` are both separators (Windows users type either).
106
+ const target = String(input.target).replace(/\\/g, "/").trim() || ".";
107
+ const directory = await safePath(workspace, target);
108
+ const files = await deps.store.files(record);
109
+ const folder = files.length > 1 ? record.fileName.replace(/\.zip$/i, "") || record.id : undefined;
110
+ const plan = [];
111
+ for (const file of files) {
112
+ const parts = files.length > 1 ? file.path.split("/") : [record.fileName];
113
+ // Every destination is confined again (no symlinked component, never outside).
114
+ const to = await safePath(workspace, join(directory, ...(folder ? [folder] : []), ...parts));
115
+ plan.push({ from: file.abs, to });
116
+ }
117
+ const overwrite = input.overwrite === true;
118
+ if (!overwrite)
119
+ for (const { to } of plan) {
120
+ const existing = await lstat(to).catch(() => undefined);
121
+ if (existing)
122
+ throw new Error(`${toPosix(relative(workspace, to))} already exists; pass overwrite: true to replace it`);
123
+ }
124
+ const written = [];
125
+ for (const { from, to } of plan) {
126
+ await mkdir(dirname(to), { recursive: true });
127
+ await copyFile(from, to, overwrite ? 0 : constants.COPYFILE_EXCL);
128
+ written.push(toPosix(relative(workspace, to)));
129
+ }
130
+ return {
131
+ content: [
132
+ {
133
+ type: "text",
134
+ text: [
135
+ `${EXPORTED}${record.fileName} (${record.id}) to ${toPosix(relative(workspace, directory)) || "."}:`,
136
+ ...written,
137
+ ].join("\n"),
138
+ },
139
+ ],
140
+ };
141
+ },
142
+ });
143
+ }
@@ -0,0 +1,7 @@
1
+ import { type DatasetService } from "../analysis/data/datasets.ts";
2
+ import type { ToolRegistry } from "../core/registry.ts";
3
+ export interface DataToolDeps {
4
+ datasets: DatasetService;
5
+ rootOf: (sessionId: string) => string;
6
+ }
7
+ export declare function registerDataTools(registry: ToolRegistry, deps: DataToolDeps): void;
@@ -0,0 +1,160 @@
1
+ import { DataError, PageParamError, } from "../analysis/data/datasets.js";
2
+ import { cellText, formatCount } from "../analysis/data/describe.js";
3
+ import { safePath } from "../runtime/paths.js";
4
+ import { objectSchema } from "./standard.js";
5
+ const INSPECT_TEXT_CHARS = 12_000;
6
+ const QUERY_TEXT_CHARS = 24_000;
7
+ /** A model-readable failure: `code: message` (the codes of §14.1 are also tool error prefixes). */
8
+ function failure(error) {
9
+ if (error instanceof DataError)
10
+ throw new Error(`${error.code}: ${error.message}`);
11
+ if (error instanceof PageParamError)
12
+ throw new Error(`invalid_request: ${error.message}`);
13
+ throw error;
14
+ }
15
+ function tableBlock(columns, rows, caption) {
16
+ return {
17
+ kind: "table",
18
+ columns,
19
+ rows: rows.map((row) => columns.map((_, i) => cellText(row[i], 120))),
20
+ ...(caption ? { caption } : {}),
21
+ };
22
+ }
23
+ export function registerDataTools(registry, deps) {
24
+ const own = (context) => deps.rootOf(context.session ?? "");
25
+ registry.register({
26
+ name: "data_inspect",
27
+ effect: "read",
28
+ description: "Describe a tabular file: sheets, columns with type hints and statistics (nulls, distinct, " +
29
+ "min/max/mean, top values) and sample rows. Pass a workspace file `path` (CSV, TSV, JSON, " +
30
+ "JSONL or XLSX; ingested once into a SQLite dataset of this session) or the `datasetId` of " +
31
+ "a dataset the user attached. The result gives the datasetId to use with data_query and " +
32
+ "python_run { inputs: [{ datasetId }] }. XLSX needs Python 3.10+; for the other formats no " +
33
+ "Python is needed.",
34
+ inputSchema: objectSchema({
35
+ path: { type: "string", minLength: 1, maxLength: 1024 },
36
+ datasetId: { type: "string", minLength: 1, maxLength: 64 },
37
+ sheet: { type: "string", maxLength: 200 },
38
+ sampleRows: { type: "integer", minimum: 0, maximum: 20 },
39
+ }),
40
+ paths: (input) => (typeof input.path === "string" ? [input.path] : []),
41
+ async execute(input, context) {
42
+ const hasPath = typeof input.path === "string" && input.path !== "";
43
+ const hasId = typeof input.datasetId === "string" && input.datasetId !== "";
44
+ if (hasPath === hasId)
45
+ throw new Error("Pass exactly one of path or datasetId");
46
+ const root = own(context);
47
+ try {
48
+ let record;
49
+ let note = "";
50
+ if (hasId)
51
+ record = deps.datasets.getFor(String(input.datasetId), root);
52
+ else {
53
+ const resolved = context.resolvePath
54
+ ? await context.resolvePath(String(input.path))
55
+ : await safePath(context.workspace, String(input.path));
56
+ let last = 0;
57
+ const result = await deps.datasets.ingest({
58
+ rootSessionId: root,
59
+ sessionId: context.session ?? "headless",
60
+ workspace: context.workspace,
61
+ path: resolved,
62
+ sourcePath: resolved,
63
+ signal: context.signal,
64
+ onProgress: (rows) => {
65
+ const now = Date.now();
66
+ if (now - last < 1000)
67
+ return;
68
+ last = now;
69
+ context.emit(`reading ${formatCount(rows)} rows…\n`);
70
+ },
71
+ });
72
+ record = result.record;
73
+ note = result.reused ? "(already ingested in this session)\n" : "";
74
+ }
75
+ const sample = Number(input.sampleRows ?? 5);
76
+ const described = await deps.datasets.describe(record, sample, INSPECT_TEXT_CHARS, context.signal);
77
+ const wantedSheet = typeof input.sheet === "string" ? input.sheet : undefined;
78
+ const shown = wantedSheet
79
+ ? described.detail.sheetDetails.filter((s) => s.name === wantedSheet || s.table === wantedSheet)
80
+ : described.detail.sheetDetails;
81
+ if (wantedSheet && !shown.length)
82
+ throw new Error(`Sheet ${wantedSheet} not found; sheets: ${described.detail.sheetDetails.map((s) => s.name).join(", ")}`);
83
+ const blocks = [];
84
+ for (const sheet of shown.slice(0, 5)) {
85
+ blocks.push(tableBlock(["column", "type", "nulls", "distinct", "min", "max", "mean"], sheet.columns.map((c) => [
86
+ c.name,
87
+ c.type,
88
+ formatCount(c.nulls),
89
+ `${formatCount(c.distinct)}${c.distinctExact ? "" : "~"}`,
90
+ c.min ?? "",
91
+ c.max ?? "",
92
+ c.mean !== undefined ? Number(c.mean.toPrecision(6)) : "",
93
+ ]), `${sheet.table} · ${formatCount(sheet.rows)} rows`));
94
+ const rows = described.samples.get(sheet.table);
95
+ if (rows?.rows.length)
96
+ blocks.push(tableBlock(rows.columns, rows.rows, `${sheet.table} · first ${rows.rows.length} rows`));
97
+ }
98
+ return {
99
+ content: [
100
+ { type: "text", text: `${note}${described.text}` },
101
+ ...blocks.map((block) => ({ type: "ui", block })),
102
+ ],
103
+ };
104
+ }
105
+ catch (error) {
106
+ return failure(error);
107
+ }
108
+ },
109
+ });
110
+ registry.register({
111
+ name: "data_query",
112
+ effect: "read",
113
+ description: "Run one read-only SQL query (SQLite dialect: a single SELECT or WITH statement) on a " +
114
+ "dataset of this session. Tables: `data` for CSV/TSV/JSON/JSONL, `s_<sheet>` for XLSX; " +
115
+ 'quote column names ("revenue"). Values are stored exactly: numbers are numbers, other ' +
116
+ "cells (007, 1,234, N/A) are their original text, so cast or filter before arithmetic. At " +
117
+ "most maxRows (default 200, max 1000) rows; statements longer than the time limit are " +
118
+ "stopped. Heavy analysis belongs in python_run.",
119
+ inputSchema: objectSchema({
120
+ datasetId: { type: "string", minLength: 1, maxLength: 64 },
121
+ sql: { type: "string", minLength: 1, maxLength: 20_000 },
122
+ maxRows: { type: "integer", minimum: 1, maximum: 1000 },
123
+ }, ["datasetId", "sql"]),
124
+ async execute(input, context) {
125
+ try {
126
+ const record = deps.datasets.getFor(String(input.datasetId), own(context));
127
+ const maxRows = Number(input.maxRows ?? 200);
128
+ const result = await deps.datasets.query(record, String(input.sql), {
129
+ maxRows,
130
+ signal: context.signal,
131
+ });
132
+ const lines = [
133
+ result.columns.join("\t"),
134
+ ...result.rows.map((row) => row.map((v) => cellText(v, 2048)).join("\t")),
135
+ ];
136
+ let text = lines.join("\n");
137
+ let cut = false;
138
+ if (text.length > QUERY_TEXT_CHARS) {
139
+ text = `${text.slice(0, QUERY_TEXT_CHARS)}…`;
140
+ cut = true;
141
+ }
142
+ const summary = `${formatCount(result.rows.length)} row${result.rows.length === 1 ? "" : "s"}${result.truncated
143
+ ? ` (truncated at maxRows ${maxRows}; add LIMIT/OFFSET or aggregate)`
144
+ : ""}${result.clipped ? "; long cells were shortened" : ""}${cut ? "; output shortened" : ""}`;
145
+ return {
146
+ content: [
147
+ { type: "text", text: `${text}\n[${summary}]` },
148
+ {
149
+ type: "ui",
150
+ block: tableBlock(result.columns, result.rows, `${record.name} · ${summary}`),
151
+ },
152
+ ],
153
+ };
154
+ }
155
+ catch (error) {
156
+ return failure(error);
157
+ }
158
+ },
159
+ });
160
+ }
@@ -22,7 +22,9 @@
22
22
  import { createContext, Script } from "node:vm";
23
23
  export const EXECUTE_TIMEOUT_MS = 10_000;
24
24
  export const MAX_NESTED_CALLS = 20;
25
- const allowedEffect = (policy, effect) => effect === "read" || effect === "internal" || !!policy[effect];
25
+ const allowedEffect = (policy, effect) => effect === "read" ||
26
+ effect === "internal" ||
27
+ ((effect === "write" || effect === "process" || effect === "external") && !!policy[effect]);
26
28
  export async function runExecute(code, deps) {
27
29
  let calls = 0;
28
30
  const controller = new AbortController();
@@ -42,7 +44,9 @@ export async function runExecute(code, deps) {
42
44
  throw new Error(`callTool: exceeded the limit of ${MAX_NESTED_CALLS} nested tool calls`);
43
45
  const tool = deps.registry.get(name); // throws "Unknown tool: <name>" when absent
44
46
  const effect = tool.effect ?? "external";
45
- if (!allowedEffect(deps.policy, effect))
47
+ // A capability tool is reachable only when already allowed (never through a persisted grant).
48
+ if (!allowedEffect(deps.policy, effect) &&
49
+ !(tool.capability === "analysis.run" && deps.policy.analysis))
46
50
  throw new Error(`callTool: capability denied for "${name}" (effect: ${effect}); execute cannot request ` +
47
51
  "new approvals, only use capabilities already allowed for this session");
48
52
  const rawInput = input && typeof input === "object" ? input : {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alisio/core",
3
- "version": "0.1.0-alpha.21",
3
+ "version": "0.1.0-alpha.22",
4
4
  "description": "Embeddable provider-agnostic Alisio agent core: tool loop, context compaction, local tools, MCP, plugin host and SQLite session store. Runs on Node >=22.16 and Bun.",
5
5
  "author": "Gustavo Gutiérrez",
6
6
  "license": "MIT",
@@ -41,7 +41,7 @@
41
41
  "node": ">=22.16"
42
42
  },
43
43
  "dependencies": {
44
- "@alisio/sdk": "0.1.0-alpha.17",
44
+ "@alisio/sdk": "0.1.0-alpha.18",
45
45
  "@mixmark-io/domino": "2.2.0",
46
46
  "@modelcontextprotocol/client": "2.1.0",
47
47
  "ajv": "8.20.0",