@alisio/core 0.1.0-alpha.9 → 0.1.1
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 +52 -7
- package/dist/agents/active.d.ts +99 -0
- package/dist/agents/active.js +218 -0
- package/dist/agents/capabilities.d.ts +26 -0
- package/dist/agents/capabilities.js +120 -0
- package/dist/agents/create-agent-skill.d.ts +11 -0
- package/dist/agents/create-agent-skill.js +60 -0
- package/dist/agents/definitions.d.ts +88 -0
- package/dist/agents/definitions.js +404 -0
- package/dist/agents/draft.d.ts +41 -0
- package/dist/agents/draft.js +133 -0
- package/dist/agents/service.d.ts +57 -0
- package/dist/agents/service.js +86 -0
- package/dist/agents/templates.d.ts +8 -0
- package/dist/agents/templates.js +102 -0
- package/dist/analysis/capabilities.d.ts +48 -0
- package/dist/analysis/capabilities.js +90 -0
- package/dist/analysis/data/csv.d.ts +46 -0
- package/dist/analysis/data/csv.js +240 -0
- package/dist/analysis/data/datasets.d.ts +132 -0
- package/dist/analysis/data/datasets.js +441 -0
- package/dist/analysis/data/describe.d.ts +27 -0
- package/dist/analysis/data/describe.js +103 -0
- package/dist/analysis/data/engine-client.d.ts +58 -0
- package/dist/analysis/data/engine-client.js +304 -0
- package/dist/analysis/data/engine-entry.d.ts +1 -0
- package/dist/analysis/data/engine-entry.js +59 -0
- package/dist/analysis/data/engine-source.d.ts +4 -0
- package/dist/analysis/data/engine-source.js +6 -0
- package/dist/analysis/data/engine-types.d.ts +86 -0
- package/dist/analysis/data/engine-types.js +7 -0
- package/dist/analysis/data/infer.d.ts +42 -0
- package/dist/analysis/data/infer.js +118 -0
- package/dist/analysis/data/ingest-worker.d.ts +17 -0
- package/dist/analysis/data/ingest-worker.js +365 -0
- package/dist/analysis/data/json.d.ts +19 -0
- package/dist/analysis/data/json.js +44 -0
- package/dist/analysis/data/query-worker.d.ts +12 -0
- package/dist/analysis/data/query-worker.js +75 -0
- package/dist/analysis/data/rows.d.ts +39 -0
- package/dist/analysis/data/rows.js +120 -0
- package/dist/analysis/data/sql-guard.d.ts +20 -0
- package/dist/analysis/data/sql-guard.js +129 -0
- package/dist/analysis/data/stats.d.ts +18 -0
- package/dist/analysis/data/stats.js +77 -0
- package/dist/analysis/install-hints.d.ts +33 -0
- package/dist/analysis/install-hints.js +144 -0
- package/dist/analysis/janitor.d.ts +60 -0
- package/dist/analysis/janitor.js +264 -0
- package/dist/analysis/jobs.d.ts +94 -0
- package/dist/analysis/jobs.js +165 -0
- package/dist/analysis/oci.d.ts +129 -0
- package/dist/analysis/oci.js +330 -0
- package/dist/analysis/python/sources.d.ts +2 -0
- package/dist/analysis/python/sources.js +10 -0
- package/dist/analysis/requirements.d.ts +32 -0
- package/dist/analysis/requirements.js +73 -0
- package/dist/analysis/rerun.d.ts +40 -0
- package/dist/analysis/rerun.js +136 -0
- package/dist/analysis/runtime-manager.d.ts +146 -0
- package/dist/analysis/runtime-manager.js +472 -0
- package/dist/analysis/status.d.ts +21 -0
- package/dist/analysis/status.js +52 -0
- package/dist/application.d.ts +105 -3
- package/dist/application.js +446 -18
- package/dist/artifacts/kinds.d.ts +35 -0
- package/dist/artifacts/kinds.js +177 -0
- package/dist/artifacts/publisher.d.ts +7 -0
- package/dist/artifacts/publisher.js +29 -0
- package/dist/artifacts/store.d.ts +119 -0
- package/dist/artifacts/store.js +581 -0
- package/dist/background/janitor.d.ts +22 -0
- package/dist/background/janitor.js +62 -0
- package/dist/background/mirror.d.ts +14 -0
- package/dist/background/mirror.js +41 -0
- package/dist/background/notify.d.ts +58 -0
- package/dist/background/notify.js +172 -0
- package/dist/background/output.d.ts +46 -0
- package/dist/background/output.js +179 -0
- package/dist/background/service.d.ts +137 -0
- package/dist/background/service.js +410 -0
- package/dist/background/store.d.ts +103 -0
- package/dist/background/store.js +195 -0
- package/dist/changelog/data.d.ts +6 -0
- package/dist/changelog/data.js +306 -0
- package/dist/changelog/index.d.ts +5 -0
- package/dist/changelog/index.js +7 -0
- package/dist/changelog/parse.d.ts +53 -0
- package/dist/changelog/parse.js +160 -0
- package/dist/commands/builtins.d.ts +10 -0
- package/dist/commands/builtins.js +161 -0
- package/dist/commands/catalog.d.ts +86 -0
- package/dist/commands/catalog.js +304 -0
- package/dist/config.d.ts +206 -6
- package/dist/config.js +426 -20
- package/dist/core/compaction.js +13 -2
- package/dist/core/contracts.d.ts +180 -3
- package/dist/core/opt-in.d.ts +12 -0
- package/dist/core/opt-in.js +12 -0
- package/dist/core/output-limit.d.ts +34 -0
- package/dist/core/output-limit.js +39 -0
- package/dist/core/run-stats.d.ts +16 -0
- package/dist/core/run-stats.js +8 -0
- package/dist/core/runner.d.ts +134 -4
- package/dist/core/runner.js +780 -138
- package/dist/core/timeout.d.ts +15 -0
- package/dist/core/timeout.js +61 -0
- package/dist/core/truncation.d.ts +30 -0
- package/dist/core/truncation.js +83 -0
- package/dist/goal/blockers.d.ts +45 -0
- package/dist/goal/blockers.js +51 -0
- package/dist/goal/budget.d.ts +24 -0
- package/dist/goal/budget.js +53 -0
- package/dist/goal/command.d.ts +36 -0
- package/dist/goal/command.js +98 -0
- package/dist/goal/format.d.ts +19 -0
- package/dist/goal/format.js +102 -0
- package/dist/goal/host.d.ts +29 -0
- package/dist/goal/host.js +42 -0
- package/dist/goal/index.d.ts +9 -0
- package/dist/goal/index.js +9 -0
- package/dist/goal/machine.d.ts +186 -0
- package/dist/goal/machine.js +344 -0
- package/dist/goal/prompts.d.ts +10 -0
- package/dist/goal/prompts.js +75 -0
- package/dist/goal/service.d.ts +113 -0
- package/dist/goal/service.js +273 -0
- package/dist/goal/store.d.ts +59 -0
- package/dist/goal/store.js +184 -0
- package/dist/index.d.ts +50 -5
- package/dist/index.js +49 -4
- package/dist/mcp/connector.js +38 -6
- package/dist/mcp/rich.d.ts +41 -0
- package/dist/mcp/rich.js +362 -0
- package/dist/permissions/modes.d.ts +72 -0
- package/dist/permissions/modes.js +98 -0
- package/dist/plan/exit-plan.d.ts +25 -0
- package/dist/plan/exit-plan.js +130 -0
- package/dist/plan/state.d.ts +102 -0
- package/dist/plan/state.js +165 -0
- package/dist/plugins/host.d.ts +34 -1
- package/dist/plugins/host.js +143 -9
- package/dist/providers/settings.d.ts +15 -0
- package/dist/providers/settings.js +38 -0
- package/dist/reload.d.ts +75 -0
- package/dist/reload.js +204 -0
- package/dist/resources/context.js +6 -1
- package/dist/runtime/access.d.ts +62 -0
- package/dist/runtime/access.js +115 -0
- package/dist/runtime/blobs.d.ts +51 -0
- package/dist/runtime/blobs.js +177 -0
- package/dist/runtime/diff.d.ts +23 -0
- package/dist/runtime/diff.js +154 -0
- package/dist/runtime/ids.d.ts +8 -0
- package/dist/runtime/ids.js +32 -0
- package/dist/runtime/paths.d.ts +11 -0
- package/dist/runtime/paths.js +17 -1
- package/dist/runtime/process.d.ts +22 -0
- package/dist/runtime/process.js +100 -15
- package/dist/runtime/sqlite.d.ts +2 -0
- package/dist/runtime/sqlite.js +18 -0
- package/dist/runtime/store.d.ts +91 -3
- package/dist/runtime/store.js +465 -11
- package/dist/runtime/zip.d.ts +24 -0
- package/dist/runtime/zip.js +126 -0
- package/dist/sessions/children.js +17 -1
- package/dist/sessions/side-questions.d.ts +57 -0
- package/dist/sessions/side-questions.js +161 -0
- package/dist/tools/analysis.d.ts +37 -0
- package/dist/tools/analysis.js +573 -0
- package/dist/tools/artifacts.d.ts +14 -0
- package/dist/tools/artifacts.js +143 -0
- package/dist/tools/background.d.ts +7 -0
- package/dist/tools/background.js +165 -0
- package/dist/tools/data.d.ts +7 -0
- package/dist/tools/data.js +160 -0
- package/dist/tools/execute.d.ts +5 -0
- package/dist/tools/execute.js +15 -3
- package/dist/tools/goal.d.ts +19 -0
- package/dist/tools/goal.js +126 -0
- package/dist/tools/search.d.ts +17 -3
- package/dist/tools/search.js +153 -0
- package/dist/tools/standard.d.ts +4 -1
- package/dist/tools/standard.js +99 -18
- package/dist/version.d.ts +5 -3
- package/dist/version.js +35 -6
- 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 { BackgroundTasks } from "../background/service.ts";
|
|
2
|
+
import type { ToolRegistry } from "../core/registry.ts";
|
|
3
|
+
export declare const BACKGROUND_TOOLS: readonly ["bg_run", "bg_list", "bg_output", "bg_stop"];
|
|
4
|
+
export declare function registerBackgroundTools(registry: ToolRegistry, deps: {
|
|
5
|
+
tasks: BackgroundTasks;
|
|
6
|
+
rootOf: (session: string) => string;
|
|
7
|
+
}): void;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `bg_run`, `bg_list`, `bg_output` and `bg_stop`: shell commands that keep running while the agent
|
|
3
|
+
* works. All four declare `effect: "process"`, so they pass through the SAME gates as `shell` and
|
|
4
|
+
* `run_process`: `--read-only` and the plan agent (no process effect) cannot start a task, and
|
|
5
|
+
* also cannot read the output of one or stop one; the permission modes and approvals apply as for
|
|
6
|
+
* any command. A task is not sandboxed and ends when Alisio exits.
|
|
7
|
+
*
|
|
8
|
+
* A task belongs to the root session of the session that started it; every call can only see the
|
|
9
|
+
* tasks of its own session tree ("not found" otherwise).
|
|
10
|
+
*/
|
|
11
|
+
import { stat } from "node:fs/promises";
|
|
12
|
+
import { textResult } from "@alisio/sdk";
|
|
13
|
+
import { DEFAULT_READ_BYTES, MAX_READ_BYTES } from "../background/output.js";
|
|
14
|
+
import { safePath } from "../runtime/paths.js";
|
|
15
|
+
import { objectSchema } from "./standard.js";
|
|
16
|
+
export const BACKGROUND_TOOLS = ["bg_run", "bg_list", "bg_output", "bg_stop"];
|
|
17
|
+
const STATUSES = [
|
|
18
|
+
"queued",
|
|
19
|
+
"running",
|
|
20
|
+
"stopping",
|
|
21
|
+
"succeeded",
|
|
22
|
+
"failed",
|
|
23
|
+
"cancelled",
|
|
24
|
+
"lost",
|
|
25
|
+
];
|
|
26
|
+
/** The model-facing view of a task (snake_case, no paths). */
|
|
27
|
+
function brief(task) {
|
|
28
|
+
return {
|
|
29
|
+
id: task.id,
|
|
30
|
+
label: task.label,
|
|
31
|
+
status: task.status,
|
|
32
|
+
...(task.exitCode !== undefined ? { exit_code: task.exitCode } : {}),
|
|
33
|
+
...(task.errorCode ? { error: task.errorCode } : {}),
|
|
34
|
+
...(task.abortOrigin ? { stopped_by: task.abortOrigin } : {}),
|
|
35
|
+
output_bytes: task.bytes,
|
|
36
|
+
...(task.truncated ? { output_truncated: true } : {}),
|
|
37
|
+
...(task.cwd ? { cwd: task.cwd } : {}),
|
|
38
|
+
created_at: new Date(task.createdAt).toISOString(),
|
|
39
|
+
...(task.endedAt ? { ended_at: new Date(task.endedAt).toISOString() } : {}),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function registerBackgroundTools(registry, deps) {
|
|
43
|
+
const { tasks } = deps;
|
|
44
|
+
const rootOf = (session) => {
|
|
45
|
+
if (!session)
|
|
46
|
+
throw new Error("Background tasks need a session");
|
|
47
|
+
return deps.rootOf(session);
|
|
48
|
+
};
|
|
49
|
+
registry.register({
|
|
50
|
+
name: "bg_run",
|
|
51
|
+
effect: "process",
|
|
52
|
+
description: "Start a shell command in the background and return its task id at once; the agent keeps " +
|
|
53
|
+
"working. Use it for long-running commands (servers, watchers, long builds or test suites). " +
|
|
54
|
+
"Not sandboxed: the command runs with your permissions and ends when Alisio exits. The " +
|
|
55
|
+
"output is stored in a log you read with bg_output. When the task finishes on its own you " +
|
|
56
|
+
"get ONE notification message (no polling needed). `timeoutMs` is capped by the " +
|
|
57
|
+
"tasks.maxRunMs setting (default 1 hour); a task that exceeds it is stopped and fails.",
|
|
58
|
+
inputSchema: objectSchema({
|
|
59
|
+
command: { type: "string", minLength: 1, maxLength: 8000 },
|
|
60
|
+
cwd: { type: "string", description: "Working directory, relative to the workspace." },
|
|
61
|
+
label: { type: "string", maxLength: 80, description: "Short name shown in task lists." },
|
|
62
|
+
timeoutMs: { type: "integer", minimum: 1000, maximum: 86_400_000 },
|
|
63
|
+
}, ["command"]),
|
|
64
|
+
paths: (input) => (typeof input.cwd === "string" && input.cwd ? [input.cwd] : []),
|
|
65
|
+
async execute(input, context) {
|
|
66
|
+
const session = context.session;
|
|
67
|
+
rootOf(session);
|
|
68
|
+
const cwd = typeof input.cwd === "string" && input.cwd
|
|
69
|
+
? context.resolvePath
|
|
70
|
+
? await context.resolvePath(input.cwd)
|
|
71
|
+
: await safePath(context.workspace, input.cwd)
|
|
72
|
+
: context.workspace;
|
|
73
|
+
let isDirectory = false;
|
|
74
|
+
try {
|
|
75
|
+
isDirectory = (await stat(cwd)).isDirectory();
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
/* reported below */
|
|
79
|
+
}
|
|
80
|
+
if (!isDirectory)
|
|
81
|
+
throw new Error(`The working directory does not exist: ${input.cwd}`);
|
|
82
|
+
const task = tasks.start({
|
|
83
|
+
session: session,
|
|
84
|
+
workspace: context.workspace,
|
|
85
|
+
command: String(input.command),
|
|
86
|
+
cwd,
|
|
87
|
+
...(typeof input.label === "string" ? { label: input.label } : {}),
|
|
88
|
+
...(typeof input.timeoutMs === "number" ? { timeoutMs: input.timeoutMs } : {}),
|
|
89
|
+
});
|
|
90
|
+
return textResult(JSON.stringify({
|
|
91
|
+
...brief(task),
|
|
92
|
+
timeout_ms: task.timeoutMs,
|
|
93
|
+
note: tasks.notifies
|
|
94
|
+
? "Started in the background. You will get one notification when it finishes; use bg_output to read its output and bg_stop to stop it."
|
|
95
|
+
: "Started in the background. Nothing notifies you here: use bg_output to read its output and bg_list to check its status. It ends when this Alisio process ends.",
|
|
96
|
+
}));
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
registry.register({
|
|
100
|
+
name: "bg_list",
|
|
101
|
+
effect: "process",
|
|
102
|
+
description: "List the background tasks of this session, newest first.",
|
|
103
|
+
inputSchema: objectSchema({
|
|
104
|
+
status: { enum: STATUSES },
|
|
105
|
+
limit: { type: "integer", minimum: 1, maximum: 50 },
|
|
106
|
+
}),
|
|
107
|
+
async execute(input, context) {
|
|
108
|
+
const root = rootOf(context.session);
|
|
109
|
+
const listed = tasks.list(root, {
|
|
110
|
+
...(typeof input.status === "string"
|
|
111
|
+
? { status: [input.status] }
|
|
112
|
+
: {}),
|
|
113
|
+
limit: typeof input.limit === "number" ? input.limit : 20,
|
|
114
|
+
});
|
|
115
|
+
// Listing a finished task tells the agent it ended: it will not be announced as well.
|
|
116
|
+
tasks.markSeen(listed
|
|
117
|
+
.filter((task) => task.status !== "running" && task.status !== "queued" && task.status !== "stopping")
|
|
118
|
+
.map((task) => task.id));
|
|
119
|
+
return textResult(JSON.stringify({ tasks: listed.map(brief) }));
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
registry.register({
|
|
123
|
+
name: "bg_output",
|
|
124
|
+
effect: "process",
|
|
125
|
+
description: "Read the output of a background task incrementally. Pass `offset` 0 (or omit it) the first " +
|
|
126
|
+
"time, then the `next_offset` of the previous call. `eof` is true only when the task has " +
|
|
127
|
+
"ended and everything was read. At most 64 KiB per call (16 KiB by default); the log keeps " +
|
|
128
|
+
"the first tasks.maxOutputBytes bytes and the last 32 KiB.",
|
|
129
|
+
inputSchema: objectSchema({
|
|
130
|
+
id: { type: "string", minLength: 1 },
|
|
131
|
+
offset: { type: "integer", minimum: 0 },
|
|
132
|
+
limit: { type: "integer", minimum: 1, maximum: MAX_READ_BYTES },
|
|
133
|
+
}, ["id"]),
|
|
134
|
+
async execute(input, context) {
|
|
135
|
+
const root = rootOf(context.session);
|
|
136
|
+
const out = await tasks.read(root, String(input.id), {
|
|
137
|
+
offset: typeof input.offset === "number" ? input.offset : 0,
|
|
138
|
+
limit: typeof input.limit === "number" ? input.limit : DEFAULT_READ_BYTES,
|
|
139
|
+
reader: "model",
|
|
140
|
+
});
|
|
141
|
+
return textResult(JSON.stringify({
|
|
142
|
+
id: String(input.id),
|
|
143
|
+
status: out.status,
|
|
144
|
+
...(out.exitCode !== undefined ? { exit_code: out.exitCode } : {}),
|
|
145
|
+
text: out.text,
|
|
146
|
+
next_offset: out.nextOffset,
|
|
147
|
+
eof: out.eof,
|
|
148
|
+
...(out.truncated ? { output_truncated: true } : {}),
|
|
149
|
+
}));
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
registry.register({
|
|
153
|
+
name: "bg_stop",
|
|
154
|
+
effect: "process",
|
|
155
|
+
description: "Stop a background task (SIGTERM to its process group, SIGKILL after 3 seconds; " +
|
|
156
|
+
"taskkill /T on Windows). A stopped task ends `cancelled`, never `failed`. Stopping a task " +
|
|
157
|
+
"that already ended returns its final state.",
|
|
158
|
+
inputSchema: objectSchema({ id: { type: "string", minLength: 1 } }, ["id"]),
|
|
159
|
+
async execute(input, context) {
|
|
160
|
+
const root = rootOf(context.session);
|
|
161
|
+
const task = await tasks.stop(root, String(input.id), "model");
|
|
162
|
+
return textResult(JSON.stringify(brief(task)));
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
}
|
|
@@ -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
|
+
}
|
package/dist/tools/execute.d.ts
CHANGED
|
@@ -10,6 +10,11 @@ export interface ExecuteDeps {
|
|
|
10
10
|
emit: (data: unknown) => void;
|
|
11
11
|
session?: string;
|
|
12
12
|
label?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Non-interactive path resolver for nested calls: the snippet can only use directories this
|
|
15
|
+
* session already has; it never triggers a new external-directory approval prompt.
|
|
16
|
+
*/
|
|
17
|
+
resolvePath?: (path: string) => Promise<string>;
|
|
13
18
|
/** Overrides `EXECUTE_TIMEOUT_MS`, mainly for tests. */
|
|
14
19
|
timeoutMs?: number;
|
|
15
20
|
}
|
package/dist/tools/execute.js
CHANGED
|
@@ -20,9 +20,12 @@
|
|
|
20
20
|
* otherwise need approval is denied outright here, never silently granted.
|
|
21
21
|
*/
|
|
22
22
|
import { createContext, Script } from "node:vm";
|
|
23
|
+
import { OPT_IN_TOOLS } from "../core/opt-in.js";
|
|
23
24
|
export const EXECUTE_TIMEOUT_MS = 10_000;
|
|
24
25
|
export const MAX_NESTED_CALLS = 20;
|
|
25
|
-
const allowedEffect = (policy, effect) => effect === "read" ||
|
|
26
|
+
const allowedEffect = (policy, effect) => effect === "read" ||
|
|
27
|
+
effect === "internal" ||
|
|
28
|
+
((effect === "write" || effect === "process" || effect === "external") && !!policy[effect]);
|
|
26
29
|
export async function runExecute(code, deps) {
|
|
27
30
|
let calls = 0;
|
|
28
31
|
const controller = new AbortController();
|
|
@@ -41,8 +44,13 @@ export async function runExecute(code, deps) {
|
|
|
41
44
|
if (calls > MAX_NESTED_CALLS)
|
|
42
45
|
throw new Error(`callTool: exceeded the limit of ${MAX_NESTED_CALLS} nested tool calls`);
|
|
43
46
|
const tool = deps.registry.get(name); // throws "Unknown tool: <name>" when absent
|
|
47
|
+
// Opt-in tools (`exit_plan`) belong to the run that lists them, never to a nested call.
|
|
48
|
+
if (OPT_IN_TOOLS.has(name))
|
|
49
|
+
throw new Error(`callTool: "${name}" is not available from execute`);
|
|
44
50
|
const effect = tool.effect ?? "external";
|
|
45
|
-
|
|
51
|
+
// A capability tool is reachable only when already allowed (never through a persisted grant).
|
|
52
|
+
if (!allowedEffect(deps.policy, effect) &&
|
|
53
|
+
!(tool.capability === "analysis.run" && deps.policy.analysis))
|
|
46
54
|
throw new Error(`callTool: capability denied for "${name}" (effect: ${effect}); execute cannot request ` +
|
|
47
55
|
"new approvals, only use capabilities already allowed for this session");
|
|
48
56
|
const rawInput = input && typeof input === "object" ? input : {};
|
|
@@ -53,8 +61,12 @@ export async function runExecute(code, deps) {
|
|
|
53
61
|
emit: deps.emit,
|
|
54
62
|
...(deps.session ? { session: deps.session } : {}),
|
|
55
63
|
...(deps.label ? { label: deps.label } : {}),
|
|
64
|
+
...(deps.resolvePath ? { resolvePath: deps.resolvePath } : {}),
|
|
56
65
|
});
|
|
57
|
-
const text = result.content
|
|
66
|
+
const text = result.content
|
|
67
|
+
.filter((p) => p.type === "text")
|
|
68
|
+
.map((c) => c.text)
|
|
69
|
+
.join("\n");
|
|
58
70
|
if (result.isError)
|
|
59
71
|
throw new Error(text);
|
|
60
72
|
try {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `get_goal` and `update_goal`: the model's two handles on the session goal. Both are OPT-IN
|
|
3
|
+
* tools (`core/opt-in.ts`): a run only sees them while its session has an `active` goal and the
|
|
4
|
+
* agent is not the plan agent, so their descriptions cost no tokens anywhere else. The model can
|
|
5
|
+
* only report the goal `complete` or `blocked`, with evidence; pausing, resuming, editing,
|
|
6
|
+
* clearing and the budget belong to the user and have no tool.
|
|
7
|
+
*
|
|
8
|
+
* `effect: "internal"`: reporting changes Alisio's own bookkeeping, never the workspace.
|
|
9
|
+
*/
|
|
10
|
+
import { type GoalEvidence } from "@alisio/sdk";
|
|
11
|
+
import type { ToolRegistry } from "../core/registry.ts";
|
|
12
|
+
import type { GoalService } from "../goal/service.ts";
|
|
13
|
+
export declare const GET_GOAL_TOOL = "get_goal";
|
|
14
|
+
export declare const UPDATE_GOAL_TOOL = "update_goal";
|
|
15
|
+
/** Parses the evidence list defensively (the schema is advisory for some providers). */
|
|
16
|
+
export declare function parseEvidence(raw: unknown): GoalEvidence[] | string;
|
|
17
|
+
export declare function registerGoalTools(registry: ToolRegistry, deps: {
|
|
18
|
+
goals: GoalService;
|
|
19
|
+
}): void;
|