@alisio/core 0.1.1 → 0.2.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/dist/agents/active.d.ts +15 -1
- package/dist/agents/active.js +48 -9
- package/dist/analysis/python/sources.js +7 -4
- package/dist/application.js +35 -3
- package/dist/artifacts/chart-lint.d.ts +8 -0
- package/dist/artifacts/chart-lint.js +84 -0
- package/dist/artifacts/publisher.js +3 -0
- package/dist/artifacts/store.d.ts +3 -1
- package/dist/artifacts/store.js +11 -1
- package/dist/changelog/data.js +47 -1
- package/dist/config.d.ts +14 -0
- package/dist/config.js +15 -0
- package/dist/core/contracts.d.ts +8 -1
- package/dist/core/human-wait.d.ts +25 -0
- package/dist/core/human-wait.js +54 -0
- package/dist/core/run-clock.d.ts +47 -0
- package/dist/core/run-clock.js +111 -0
- package/dist/core/run-stats.d.ts +2 -0
- package/dist/core/runner.d.ts +12 -0
- package/dist/core/runner.js +68 -18
- package/dist/core/timeout.js +2 -2
- package/dist/goal/host.d.ts +2 -0
- package/dist/goal/host.js +2 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +5 -1
- package/dist/plan/diagrams.d.ts +76 -0
- package/dist/plan/diagrams.js +326 -0
- package/dist/plan/exit-plan.d.ts +5 -12
- package/dist/plan/exit-plan.js +199 -33
- package/dist/plan/manifest.d.ts +53 -0
- package/dist/plan/manifest.js +319 -0
- package/dist/plan/state.d.ts +21 -1
- package/dist/plan/state.js +6 -12
- package/dist/plugins/host.d.ts +10 -1
- package/dist/plugins/host.js +17 -2
- package/dist/tools/analysis.js +5 -2
- package/package.json +2 -2
package/dist/application.js
CHANGED
|
@@ -16,6 +16,7 @@ import { subagentTasks } from "./background/mirror.js";
|
|
|
16
16
|
import { BackgroundTasks } from "./background/service.js";
|
|
17
17
|
import { configFile, configHome, loadConfigWithProvenance, overridesSavedProviderProfile, setConfigValue, setGlobalMcpAllow, setMcpServerEnabled, setProjectPluginEnabled, setProjectSkillEnabled, stateHome, } from "./config.js";
|
|
18
18
|
import { completeText } from "./core/compaction.js";
|
|
19
|
+
import { HumanWaits } from "./core/human-wait.js";
|
|
19
20
|
import { DEFAULT_MAX_OUTPUT_TOKENS } from "./core/output-limit.js";
|
|
20
21
|
import { ToolRegistry } from "./core/registry.js";
|
|
21
22
|
import { AgentRunner } from "./core/runner.js";
|
|
@@ -86,11 +87,25 @@ export async function createApplication(options = {}) {
|
|
|
86
87
|
const addDirs = options.readOnly
|
|
87
88
|
? []
|
|
88
89
|
: await Promise.all((options.addDirs ?? []).map((dir) => realpath(resolve(cwd, dir)).catch(() => resolve(cwd, dir))));
|
|
90
|
+
// Waits for a person (approvals, questions, the plan review) do not count against the run's
|
|
91
|
+
// time limit. One registry serves the runner, the plugin host's interactive UI and the
|
|
92
|
+
// external-directory approval, so every host (TUI, web server, embedders) gets it for free.
|
|
93
|
+
const humanWaits = new HumanWaits((session) => {
|
|
94
|
+
try {
|
|
95
|
+
return store.get(session).parentId ?? undefined;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
const askExternalDirectory = options.approveExternalDirectory;
|
|
89
102
|
const pathAccess = new PathAccess({
|
|
90
103
|
workspace,
|
|
91
104
|
extraRoots: [...config.additionalDirectories, ...addDirs],
|
|
92
|
-
...(
|
|
93
|
-
? {
|
|
105
|
+
...(askExternalDirectory && !options.readOnly
|
|
106
|
+
? {
|
|
107
|
+
approve: (request) => humanWaits.track(request.session, () => askExternalDirectory(request)),
|
|
108
|
+
}
|
|
94
109
|
: {}),
|
|
95
110
|
readOnly: !!options.readOnly,
|
|
96
111
|
});
|
|
@@ -413,7 +428,15 @@ export async function createApplication(options = {}) {
|
|
|
413
428
|
}
|
|
414
429
|
// `exit_plan` (opt-in: only the plan agent's run sees it): the plan review hand-over.
|
|
415
430
|
const { registerExitPlan } = await import("./plan/exit-plan.js");
|
|
416
|
-
registerExitPlan(registry, {
|
|
431
|
+
registerExitPlan(registry, {
|
|
432
|
+
store,
|
|
433
|
+
ui: plugins.ui,
|
|
434
|
+
readOnly: !!options.readOnly,
|
|
435
|
+
diagrams: () => ({
|
|
436
|
+
enabled: config.plan.diagrams,
|
|
437
|
+
max: config.plan.diagrams ? config.plan.maxDiagrams : 0,
|
|
438
|
+
}),
|
|
439
|
+
});
|
|
417
440
|
// Data tools only read (effect `read`): they stay available under --read-only.
|
|
418
441
|
if (config.analysis.enabled) {
|
|
419
442
|
const { registerDataTools } = await import("./tools/data.js");
|
|
@@ -722,7 +745,9 @@ export async function createApplication(options = {}) {
|
|
|
722
745
|
: [];
|
|
723
746
|
return declared.length ? Math.min(...declared) : undefined;
|
|
724
747
|
};
|
|
748
|
+
plugins.setHumanWaits(humanWaits);
|
|
725
749
|
const runner = new AgentRunner({
|
|
750
|
+
humanWaits,
|
|
726
751
|
provider,
|
|
727
752
|
providerFor,
|
|
728
753
|
registry,
|
|
@@ -1152,6 +1177,13 @@ export async function createApplication(options = {}) {
|
|
|
1152
1177
|
};
|
|
1153
1178
|
break;
|
|
1154
1179
|
}
|
|
1180
|
+
case "plan.diagrams":
|
|
1181
|
+
// Read live: `exit_plan` offers (or hides) its `diagrams` field on the next request.
|
|
1182
|
+
config.plan = { ...config.plan, diagrams: value === true };
|
|
1183
|
+
break;
|
|
1184
|
+
case "plan.maxDiagrams":
|
|
1185
|
+
config.plan = { ...config.plan, maxDiagrams: Number(value) };
|
|
1186
|
+
break;
|
|
1155
1187
|
case "tasks.enabled":
|
|
1156
1188
|
// Tools are registered when the application starts: the change applies to the next
|
|
1157
1189
|
// application (a restart; the web recycles the workspace application).
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A cheap, deterministic quality gate for HTML dashboards: it spots the chart mistakes that models
|
|
3
|
+
* make when they hand-write charts (SVG pie arcs, fixed-size SVGs, libraries loaded from a CDN that
|
|
4
|
+
* the viewer blocks) and names the helper that avoids them. It only produces warnings for the
|
|
5
|
+
* `python_run` / `artifact_create` result; it never rejects an artifact.
|
|
6
|
+
*/
|
|
7
|
+
/** Warnings (empty when the page looks fine) for one HTML document. */
|
|
8
|
+
export declare function chartWarnings(html: string): string[];
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A cheap, deterministic quality gate for HTML dashboards: it spots the chart mistakes that models
|
|
3
|
+
* make when they hand-write charts (SVG pie arcs, fixed-size SVGs, libraries loaded from a CDN that
|
|
4
|
+
* the viewer blocks) and names the helper that avoids them. It only produces warnings for the
|
|
5
|
+
* `python_run` / `artifact_create` result; it never rejects an artifact.
|
|
6
|
+
*/
|
|
7
|
+
const HELPERS = "alisio_runtime.charts (Chart.js, interactive) or alisio_runtime.svg (static)";
|
|
8
|
+
/** Strips scripts and styles so a bundled library never triggers the markup checks. */
|
|
9
|
+
function markupOf(html) {
|
|
10
|
+
return html
|
|
11
|
+
.replace(/(<script\b[^>]*>)[\s\S]*?<\/script\s*>/gi, "$1</script>")
|
|
12
|
+
.replace(/<style\b[^>]*>[\s\S]*?<\/style\s*>/gi, "<style></style>");
|
|
13
|
+
}
|
|
14
|
+
/** `d` attributes that draw a wedge: a line to/from the centre, an arc command and a close. */
|
|
15
|
+
function wedgeCount(markup) {
|
|
16
|
+
let count = 0;
|
|
17
|
+
for (const match of markup.matchAll(/<path\b[^>]*?\sd\s*=\s*["']([^"']*)["']/gi)) {
|
|
18
|
+
const d = match[1] ?? "";
|
|
19
|
+
if (/[Aa]\s*[\d.]+[\s,]+[\d.]+[\s,]+-?[\d.]+[\s,]+[01][\s,]*[01]/.test(d) &&
|
|
20
|
+
/[Ll]/.test(d) &&
|
|
21
|
+
/[Zz]/.test(d))
|
|
22
|
+
count++;
|
|
23
|
+
}
|
|
24
|
+
return count;
|
|
25
|
+
}
|
|
26
|
+
const numeric = (value) => {
|
|
27
|
+
const match = /^\s*(\d+(?:\.\d+)?)(?:px)?\s*$/.exec(value ?? "");
|
|
28
|
+
return match ? Number(match[1]) : undefined;
|
|
29
|
+
};
|
|
30
|
+
/** Fixed-size SVG charts: numeric width and height, no `viewBox` (they do not scale with the card). */
|
|
31
|
+
function fixedSvgCount(markup) {
|
|
32
|
+
let count = 0;
|
|
33
|
+
for (const block of markup.split(/<svg\b/i).slice(1)) {
|
|
34
|
+
const tagEnd = block.indexOf(">");
|
|
35
|
+
if (tagEnd < 0)
|
|
36
|
+
continue;
|
|
37
|
+
const tag = block.slice(0, tagEnd);
|
|
38
|
+
const body = block.slice(tagEnd, Math.max(tagEnd, block.search(/<\/svg\s*>/i)));
|
|
39
|
+
const attr = (name) => new RegExp(`\\s${name}\\s*=\\s*["']([^"']*)["']`, "i").exec(tag)?.[1];
|
|
40
|
+
const width = numeric(attr("width"));
|
|
41
|
+
const height = numeric(attr("height"));
|
|
42
|
+
const drawn = (body.match(/<(?:rect|path|polyline|circle|line)\b/gi) ?? []).length;
|
|
43
|
+
if (width !== undefined &&
|
|
44
|
+
height !== undefined &&
|
|
45
|
+
width >= 150 &&
|
|
46
|
+
drawn >= 4 &&
|
|
47
|
+
!/\sviewBox\s*=/i.test(tag))
|
|
48
|
+
count++;
|
|
49
|
+
}
|
|
50
|
+
return count;
|
|
51
|
+
}
|
|
52
|
+
/** `<script src>`, `<link href>` and `@import` that point at the network. */
|
|
53
|
+
function remoteHosts(markup, html) {
|
|
54
|
+
const hosts = new Set();
|
|
55
|
+
const sources = [
|
|
56
|
+
...markup.matchAll(/<script\b[^>]*\ssrc\s*=\s*["']((?:https?:)?\/\/[^"']+)["']/gi),
|
|
57
|
+
...markup.matchAll(/<link\b[^>]*\shref\s*=\s*["']((?:https?:)?\/\/[^"']+)["']/gi),
|
|
58
|
+
...html.matchAll(/@import\s+(?:url\()?["']?((?:https?:)?\/\/[^"')\s]+)/gi),
|
|
59
|
+
];
|
|
60
|
+
for (const match of sources) {
|
|
61
|
+
try {
|
|
62
|
+
hosts.add(new URL(match[1].replace(/^\/\//, "https://")).host);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Not a URL: ignore it.
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return [...hosts];
|
|
69
|
+
}
|
|
70
|
+
/** Warnings (empty when the page looks fine) for one HTML document. */
|
|
71
|
+
export function chartWarnings(html) {
|
|
72
|
+
const markup = markupOf(html);
|
|
73
|
+
const warnings = [];
|
|
74
|
+
const hosts = remoteHosts(markup, html);
|
|
75
|
+
if (hosts.length)
|
|
76
|
+
warnings.push(`loads resources from the network (${hosts.slice(0, 3).join(", ")}); the viewer blocks the network, so they will not load. Charts: use ${HELPERS}, which inlines Chart.js`);
|
|
77
|
+
const wedges = wedgeCount(markup);
|
|
78
|
+
if (wedges >= 2)
|
|
79
|
+
warnings.push(`${wedges} hand-written SVG arc paths look like a pie or donut; hand-written arcs are often wrong (large-arc flag, 100% slice). Use charts.pie/donut or svg.pie/donut`);
|
|
80
|
+
const fixed = fixedSvgCount(markup);
|
|
81
|
+
if (fixed)
|
|
82
|
+
warnings.push(`${fixed} SVG chart${fixed === 1 ? "" : "s"} with a fixed width and height and no viewBox will not scale with its container (small text, empty space). Use ${HELPERS}, or give the SVG a viewBox and width:100%`);
|
|
83
|
+
return warnings;
|
|
84
|
+
}
|
|
@@ -8,6 +8,9 @@ export function createArtifactPublisher(store, owner, announce) {
|
|
|
8
8
|
async publish(input) {
|
|
9
9
|
return done(await store.publish(input, owner)).artifact;
|
|
10
10
|
},
|
|
11
|
+
async publishDetailed(input) {
|
|
12
|
+
return done(await store.publish(input, owner));
|
|
13
|
+
},
|
|
11
14
|
async publishText(input) {
|
|
12
15
|
return done(await store.publishText(input, owner)).artifact;
|
|
13
16
|
},
|
|
@@ -71,7 +71,9 @@ export declare class ArtifactStore {
|
|
|
71
71
|
title?: string;
|
|
72
72
|
}): Promise<PublishedArtifact[]>;
|
|
73
73
|
/** Publishes one caller-owned file or directory. */
|
|
74
|
-
publish(input: ArtifactPublishInput
|
|
74
|
+
publish(input: ArtifactPublishInput & {
|
|
75
|
+
fileName?: string;
|
|
76
|
+
}, owner: ArtifactOwner): Promise<PublishedArtifact>;
|
|
75
77
|
/** Publishes a single text file the caller holds in memory. */
|
|
76
78
|
publishText(input: {
|
|
77
79
|
fileName: string;
|
package/dist/artifacts/store.js
CHANGED
|
@@ -11,6 +11,7 @@ import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
|
11
11
|
import { newId } from "../runtime/ids.js";
|
|
12
12
|
import { inside, workspaceKey } from "../runtime/paths.js";
|
|
13
13
|
import { writeZip } from "../runtime/zip.js";
|
|
14
|
+
import { chartWarnings } from "./chart-lint.js";
|
|
14
15
|
import { classifyArtifact, isPreviewable } from "./kinds.js";
|
|
15
16
|
export const DEFAULT_ARTIFACT_LIMITS = {
|
|
16
17
|
maxFiles: 200,
|
|
@@ -21,6 +22,8 @@ export const DEFAULT_ARTIFACT_LIMITS = {
|
|
|
21
22
|
export class ArtifactRejected extends Error {
|
|
22
23
|
}
|
|
23
24
|
const MAX_DEPTH = 8;
|
|
25
|
+
/** HTML larger than this is not scanned for chart mistakes. */
|
|
26
|
+
const MAX_LINT_BYTES = 8 * 1024 * 1024;
|
|
24
27
|
const HEAD_BYTES = 8192;
|
|
25
28
|
/** `title` → `[a-z0-9-]`, at most 48 characters (`artifact` when nothing remains). */
|
|
26
29
|
export function slugify(title) {
|
|
@@ -200,6 +203,7 @@ export class ArtifactStore {
|
|
|
200
203
|
source: resolve(input.source),
|
|
201
204
|
...(input.title ? { title: input.title } : {}),
|
|
202
205
|
...(input.entry ? { entry: input.entry } : {}),
|
|
206
|
+
...(input.fileName ? { fileName: checkName(input.fileName) } : {}),
|
|
203
207
|
},
|
|
204
208
|
], owner);
|
|
205
209
|
return published;
|
|
@@ -380,19 +384,25 @@ export class ArtifactStore {
|
|
|
380
384
|
kind = type.kind;
|
|
381
385
|
mimeType = type.mimeType;
|
|
382
386
|
fileName = only.path;
|
|
387
|
+
if (kind === "dashboard" && only.bytes <= MAX_LINT_BYTES)
|
|
388
|
+
for (const note of chartWarnings(await readFile(join(filesDir, only.path), "utf8")))
|
|
389
|
+
warnings.push(`${label}: ${note}`);
|
|
383
390
|
}
|
|
384
391
|
else {
|
|
385
392
|
entry = plan.entry;
|
|
386
393
|
const type = classifyArtifact(entry, await readHead(join(filesDir, ...entry.split("/"))));
|
|
387
394
|
kind = type.kind;
|
|
388
395
|
mimeType = type.mimeType;
|
|
389
|
-
fileName =
|
|
396
|
+
fileName =
|
|
397
|
+
plan.candidate.fileName ?? (files.length > 1 ? `${baseName}.zip` : basename(entry));
|
|
390
398
|
if (type.kind === "dashboard") {
|
|
391
399
|
const html = await readFile(join(filesDir, ...entry.split("/")), "utf8");
|
|
392
400
|
const entryDir = entry.includes("/") ? entry.slice(0, entry.lastIndexOf("/")) : "";
|
|
393
401
|
const missing = missingReferences(html, entryDir, new Set(files.map((f) => f.path)));
|
|
394
402
|
if (missing.length)
|
|
395
403
|
warnings.push(`${label}: ${entry} references missing files: ${missing.join(", ")}`);
|
|
404
|
+
for (const note of chartWarnings(html))
|
|
405
|
+
warnings.push(`${label}: ${entry} ${note}`);
|
|
396
406
|
}
|
|
397
407
|
}
|
|
398
408
|
}
|
package/dist/changelog/data.js
CHANGED
|
@@ -2,9 +2,55 @@
|
|
|
2
2
|
export const CHANGELOG_DATA = {
|
|
3
3
|
schema: 1,
|
|
4
4
|
entries: [
|
|
5
|
+
{
|
|
6
|
+
version: "0.2.1",
|
|
7
|
+
date: "2026-10-02",
|
|
8
|
+
sections: [
|
|
9
|
+
{
|
|
10
|
+
title: "Fixed",
|
|
11
|
+
items: [
|
|
12
|
+
'A run no longer stops with "reached its 600 s limit" while you read a plan or answer a question. The run time limit (`limits.timeoutMs`) now counts only active time: the time the agent waits for you (the plan review, a question, a tool approval) is not counted. A model or tool that really stalls still hits the limit. The same applies to the `goal.maxMinutes` cap of a session goal.',
|
|
13
|
+
'A run that ended or was stopped during a plan review no longer leaves the next message failing with an "Uncertain tool outcome" error.',
|
|
14
|
+
"In the web, a plan review or a question can wait up to 60 minutes for your answer (tool approvals still expire after 10 minutes).",
|
|
15
|
+
'The Settings label of `limits.timeoutMs` said "Provider request timeout"; it now says it is the run time limit.',
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
version: "0.2.0",
|
|
22
|
+
date: "2026-10-02",
|
|
23
|
+
sections: [
|
|
24
|
+
{
|
|
25
|
+
title: "Added",
|
|
26
|
+
items: [
|
|
27
|
+
"Plan diagrams: the plan agent can attach small Mermaid diagrams (flows, sequences, architecture) to the plan it proposes with `exit_plan`.",
|
|
28
|
+
"A plan viewer in the web UI: the plan opens in the artifact panel with its diagrams drawn in your theme, next to the plan review. The terminal shows the diagram source.",
|
|
29
|
+
"Plan files are saved in a plan folder with a `plan.json` manifest, so the plan and its diagrams are easy to find and reuse.",
|
|
30
|
+
"Two new settings, `plan.diagrams` and `plan.maxDiagrams` (in the configuration file, **Settings** in the web and `/settings` in the terminal), to turn diagrams off or cap how many a plan keeps.",
|
|
31
|
+
"Dashboard charts for Python analysis: `alisio_runtime.charts` builds interactive pie, donut, bar, line, area and scatter charts, KPI tiles and cards with an embedded Chart.js (MIT), so dashboards work offline, in the viewer and in the downloaded file.",
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
title: "Improved",
|
|
36
|
+
items: [
|
|
37
|
+
"Charts adapt to their card and follow the light and dark theme, with value labels, tooltips and a data table for screen readers.",
|
|
38
|
+
"The pure-SVG helpers (`alisio_runtime.svg`) were rewritten: they scale with their container and carry a title and description.",
|
|
39
|
+
"The agent receives clearer guidance for `python_run` about how to draw charts and dashboards.",
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
title: "Fixed",
|
|
44
|
+
items: [
|
|
45
|
+
"Pie charts drew wrong or broken slices in generated dashboards (including a single 100% slice), and bar charts rendered at a fixed small size.",
|
|
46
|
+
"A dashboard that draws SVG pies by hand, uses fixed-size SVG charts or loads a script from the network now gets a warning in the `python_run` result that points to the chart helpers.",
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
},
|
|
5
51
|
{
|
|
6
52
|
version: "0.1.1",
|
|
7
|
-
date: "2026-10-
|
|
53
|
+
date: "2026-10-02",
|
|
8
54
|
sections: [
|
|
9
55
|
{
|
|
10
56
|
title: "Fixed",
|
package/dist/config.d.ts
CHANGED
|
@@ -231,6 +231,10 @@ export declare const configSchema: z.ZodPipe<z.ZodObject<{
|
|
|
231
231
|
noToolTurnsLimit: z.ZodDefault<z.ZodNumber>;
|
|
232
232
|
blockedRepeats: z.ZodDefault<z.ZodNumber>;
|
|
233
233
|
}, z.core.$strict>>;
|
|
234
|
+
plan: z.ZodDefault<z.ZodObject<{
|
|
235
|
+
diagrams: z.ZodDefault<z.ZodBoolean>;
|
|
236
|
+
maxDiagrams: z.ZodDefault<z.ZodNumber>;
|
|
237
|
+
}, z.core.$strict>>;
|
|
234
238
|
tui: z.ZodDefault<z.ZodObject<{
|
|
235
239
|
paddingX: z.ZodDefault<z.ZodNumber>;
|
|
236
240
|
contentPaddingX: z.ZodDefault<z.ZodNumber>;
|
|
@@ -338,6 +342,10 @@ export declare const configSchema: z.ZodPipe<z.ZodObject<{
|
|
|
338
342
|
noToolTurnsLimit: number;
|
|
339
343
|
blockedRepeats: number;
|
|
340
344
|
};
|
|
345
|
+
plan: {
|
|
346
|
+
diagrams: boolean;
|
|
347
|
+
maxDiagrams: number;
|
|
348
|
+
};
|
|
341
349
|
tui: {
|
|
342
350
|
paddingX: number;
|
|
343
351
|
contentPaddingX: number;
|
|
@@ -528,6 +536,10 @@ export declare const configSchema: z.ZodPipe<z.ZodObject<{
|
|
|
528
536
|
noToolTurnsLimit: number;
|
|
529
537
|
blockedRepeats: number;
|
|
530
538
|
};
|
|
539
|
+
plan: {
|
|
540
|
+
diagrams: boolean;
|
|
541
|
+
maxDiagrams: number;
|
|
542
|
+
};
|
|
531
543
|
tui: {
|
|
532
544
|
paddingX: number;
|
|
533
545
|
contentPaddingX: number;
|
|
@@ -643,6 +655,8 @@ declare const SETTABLE_KEYS: {
|
|
|
643
655
|
readonly "goal.repeatedReplyLimit": z.ZodDefault<z.ZodNumber>;
|
|
644
656
|
readonly "goal.noToolTurnsLimit": z.ZodDefault<z.ZodNumber>;
|
|
645
657
|
readonly "goal.blockedRepeats": z.ZodDefault<z.ZodNumber>;
|
|
658
|
+
readonly "plan.diagrams": z.ZodDefault<z.ZodBoolean>;
|
|
659
|
+
readonly "plan.maxDiagrams": z.ZodDefault<z.ZodNumber>;
|
|
646
660
|
};
|
|
647
661
|
export type SettableSettingKey = keyof typeof SETTABLE_KEYS;
|
|
648
662
|
export declare function isSettableSettingKey(key: string): key is SettableSettingKey;
|
package/dist/config.js
CHANGED
|
@@ -386,6 +386,18 @@ const configObjectSchema = z
|
|
|
386
386
|
noToolTurnsLimit: 3,
|
|
387
387
|
blockedRepeats: 2,
|
|
388
388
|
})),
|
|
389
|
+
/**
|
|
390
|
+
* Plan diagrams (`exit_plan`): the plan agent may add Mermaid diagrams to a plan. `diagrams`
|
|
391
|
+
* off removes them from the tool and from the agent's instructions; `maxDiagrams` caps how
|
|
392
|
+
* many one plan keeps (0 to 8). The size and node limits of one diagram are constants.
|
|
393
|
+
*/
|
|
394
|
+
plan: z
|
|
395
|
+
.object({
|
|
396
|
+
diagrams: z.boolean().default(true),
|
|
397
|
+
maxDiagrams: z.number().int().min(0).max(8).default(5),
|
|
398
|
+
})
|
|
399
|
+
.strict()
|
|
400
|
+
.default(() => ({ diagrams: true, maxDiagrams: 5 })),
|
|
389
401
|
tui: z
|
|
390
402
|
.object({
|
|
391
403
|
/** Horizontal padding (columns) around the editor input box. Editor-only. */
|
|
@@ -767,6 +779,7 @@ const SETTABLE_SECTIONS = {
|
|
|
767
779
|
analysis: configObjectSchema.shape.analysis.removeDefault(),
|
|
768
780
|
tasks: configObjectSchema.shape.tasks.removeDefault(),
|
|
769
781
|
goal: configObjectSchema.shape.goal.removeDefault(),
|
|
782
|
+
plan: configObjectSchema.shape.plan.removeDefault(),
|
|
770
783
|
};
|
|
771
784
|
const ANALYSIS_LIMITS = SETTABLE_SECTIONS.analysis.shape.limits.removeDefault();
|
|
772
785
|
const ANALYSIS_RETENTION = SETTABLE_SECTIONS.analysis.shape.retention.removeDefault();
|
|
@@ -807,6 +820,8 @@ const SETTABLE_KEYS = {
|
|
|
807
820
|
"goal.repeatedReplyLimit": SETTABLE_SECTIONS.goal.shape.repeatedReplyLimit,
|
|
808
821
|
"goal.noToolTurnsLimit": SETTABLE_SECTIONS.goal.shape.noToolTurnsLimit,
|
|
809
822
|
"goal.blockedRepeats": SETTABLE_SECTIONS.goal.shape.blockedRepeats,
|
|
823
|
+
"plan.diagrams": SETTABLE_SECTIONS.plan.shape.diagrams,
|
|
824
|
+
"plan.maxDiagrams": SETTABLE_SECTIONS.plan.shape.maxDiagrams,
|
|
810
825
|
};
|
|
811
826
|
export function isSettableSettingKey(key) {
|
|
812
827
|
return Object.prototype.hasOwnProperty.call(SETTABLE_KEYS, key);
|
package/dist/core/contracts.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AnalysisCapability, ArtifactPublisher, ArtifactRef, CompactionCheckpoint, InstallPreview, Message, ToolCall, ToolResult } from "@alisio/sdk";
|
|
1
|
+
import type { AnalysisCapability, ArtifactPublisher, ArtifactPublishInput, ArtifactRef, CompactionCheckpoint, InstallPreview, Message, ToolCall, ToolResult } from "@alisio/sdk";
|
|
2
2
|
export interface Session {
|
|
3
3
|
id: string;
|
|
4
4
|
workspace: string;
|
|
@@ -255,6 +255,13 @@ export interface CoreArtifactPublisher extends ArtifactPublisher {
|
|
|
255
255
|
partial?: boolean;
|
|
256
256
|
provenance?: Record<string, unknown>;
|
|
257
257
|
}): Promise<PublishedArtifactInfo[]>;
|
|
258
|
+
/**
|
|
259
|
+
* Detailed variant of `publish` (path and warnings). `fileName` names a multi-file folder with
|
|
260
|
+
* an `entry` after its main file (`plan.md`) instead of `<folder>.zip`; downloads are still ZIPs.
|
|
261
|
+
*/
|
|
262
|
+
publishDetailed(input: ArtifactPublishInput & {
|
|
263
|
+
fileName?: string;
|
|
264
|
+
}): Promise<PublishedArtifactInfo>;
|
|
258
265
|
/** Detailed variant of `publishText` (path and warnings). */
|
|
259
266
|
publishTextDetailed(input: {
|
|
260
267
|
fileName: string;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human waits: the moments a run is blocked on a PERSON (a tool approval, an external-directory
|
|
3
|
+
* approval, `ask_user_question`, the plan review) and not on work. The run's time limit does not
|
|
4
|
+
* count them (see `RunClock`).
|
|
5
|
+
*
|
|
6
|
+
* One registry per application. Each active run registers its clock under its session; anything
|
|
7
|
+
* that asks a person goes through `track`, which pauses the clocks involved until the answer
|
|
8
|
+
* arrives. A child session (a subagent) pauses its own clock AND its ancestors': a parent that
|
|
9
|
+
* waits for a child is doing real work, but when that child waits for a person nobody works.
|
|
10
|
+
*/
|
|
11
|
+
import type { RunClock } from "./run-clock.ts";
|
|
12
|
+
export declare class HumanWaits {
|
|
13
|
+
private readonly parentOf;
|
|
14
|
+
private clocks;
|
|
15
|
+
constructor(parentOf?: (session: string) => string | undefined);
|
|
16
|
+
/** A run began: its clock follows the waits of its session. Returns the unregister function. */
|
|
17
|
+
register(session: string, clock: RunClock): () => void;
|
|
18
|
+
/**
|
|
19
|
+
* Opens a wait for `session` (the session asking; its ancestors are paused too). Without a
|
|
20
|
+
* session every active run is paused: somebody is being asked and nobody can say who for.
|
|
21
|
+
*/
|
|
22
|
+
begin(session?: string): () => void;
|
|
23
|
+
/** Runs `wait` as a human wait: the clocks are paused until it settles, however it settles. */
|
|
24
|
+
track<T>(session: string | undefined, wait: () => Promise<T>): Promise<T>;
|
|
25
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export class HumanWaits {
|
|
2
|
+
parentOf;
|
|
3
|
+
clocks = new Map();
|
|
4
|
+
constructor(parentOf = () => undefined) {
|
|
5
|
+
this.parentOf = parentOf;
|
|
6
|
+
}
|
|
7
|
+
/** A run began: its clock follows the waits of its session. Returns the unregister function. */
|
|
8
|
+
register(session, clock) {
|
|
9
|
+
this.clocks.set(session, clock);
|
|
10
|
+
return () => {
|
|
11
|
+
if (this.clocks.get(session) === clock)
|
|
12
|
+
this.clocks.delete(session);
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Opens a wait for `session` (the session asking; its ancestors are paused too). Without a
|
|
17
|
+
* session every active run is paused: somebody is being asked and nobody can say who for.
|
|
18
|
+
*/
|
|
19
|
+
begin(session) {
|
|
20
|
+
const releases = [];
|
|
21
|
+
if (session === undefined)
|
|
22
|
+
for (const clock of this.clocks.values())
|
|
23
|
+
releases.push(clock.pause());
|
|
24
|
+
else {
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
for (let id = session; id !== undefined && !seen.has(id);) {
|
|
27
|
+
seen.add(id);
|
|
28
|
+
const clock = this.clocks.get(id);
|
|
29
|
+
if (clock)
|
|
30
|
+
releases.push(clock.pause());
|
|
31
|
+
try {
|
|
32
|
+
id = this.parentOf(id);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
id = undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return () => {
|
|
40
|
+
for (const release of releases)
|
|
41
|
+
release();
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** Runs `wait` as a human wait: the clocks are paused until it settles, however it settles. */
|
|
45
|
+
async track(session, wait) {
|
|
46
|
+
const end = this.begin(session);
|
|
47
|
+
try {
|
|
48
|
+
return await wait();
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
end();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The run's time limit (`limits.timeoutMs`) as a PAUSABLE clock.
|
|
3
|
+
*
|
|
4
|
+
* The limit exists to stop runaway work (a stuck model, a looping tool), not to cap how long a
|
|
5
|
+
* person takes to read a plan or answer a question. So the clock counts ACTIVE time only: while at
|
|
6
|
+
* least one human wait is open (an approval, a question, a plan review) it is paused, and it
|
|
7
|
+
* resumes with the remaining time when the last wait ends.
|
|
8
|
+
*
|
|
9
|
+
* Waits nest and overlap (several tools can wait for approvals in parallel), so pausing is
|
|
10
|
+
* counted: the clock runs again only when every pause was released. A pause is released exactly
|
|
11
|
+
* once (the function `pause()` returns is idempotent), and a released or disposed clock ignores
|
|
12
|
+
* late calls, so an aborted run can never leave a timer behind.
|
|
13
|
+
*/
|
|
14
|
+
/** Time source and timers, replaceable in tests. */
|
|
15
|
+
export interface ClockSource {
|
|
16
|
+
now(): number;
|
|
17
|
+
setTimeout(fn: () => void, ms: number): unknown;
|
|
18
|
+
clearTimeout(handle: unknown): void;
|
|
19
|
+
}
|
|
20
|
+
export declare class RunClock {
|
|
21
|
+
readonly limitMs: number;
|
|
22
|
+
private readonly controller;
|
|
23
|
+
private readonly source;
|
|
24
|
+
private consumed;
|
|
25
|
+
/** Start of the current running stretch (undefined while paused or finished). */
|
|
26
|
+
private since;
|
|
27
|
+
private timer;
|
|
28
|
+
private pauses;
|
|
29
|
+
private done;
|
|
30
|
+
constructor(limitMs: number, source?: ClockSource);
|
|
31
|
+
/** Aborts with a `TimeoutError` (the same reason `AbortSignal.timeout` uses) at the limit. */
|
|
32
|
+
get signal(): AbortSignal;
|
|
33
|
+
/** Whether at least one human wait is open. */
|
|
34
|
+
get paused(): boolean;
|
|
35
|
+
/** Time the run was really working: elapsed time minus every paused stretch. */
|
|
36
|
+
activeMs(): number;
|
|
37
|
+
remainingMs(): number;
|
|
38
|
+
/**
|
|
39
|
+
* Opens a human wait. Returns the function that closes it (idempotent). Pausing a clock that
|
|
40
|
+
* already ended (limit reached or disposed) does nothing.
|
|
41
|
+
*/
|
|
42
|
+
pause(): () => void;
|
|
43
|
+
/** Stops the clock for good and frees its timer (call when the run ends, whatever the reason). */
|
|
44
|
+
dispose(): void;
|
|
45
|
+
private arm;
|
|
46
|
+
private disarm;
|
|
47
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The run's time limit (`limits.timeoutMs`) as a PAUSABLE clock.
|
|
3
|
+
*
|
|
4
|
+
* The limit exists to stop runaway work (a stuck model, a looping tool), not to cap how long a
|
|
5
|
+
* person takes to read a plan or answer a question. So the clock counts ACTIVE time only: while at
|
|
6
|
+
* least one human wait is open (an approval, a question, a plan review) it is paused, and it
|
|
7
|
+
* resumes with the remaining time when the last wait ends.
|
|
8
|
+
*
|
|
9
|
+
* Waits nest and overlap (several tools can wait for approvals in parallel), so pausing is
|
|
10
|
+
* counted: the clock runs again only when every pause was released. A pause is released exactly
|
|
11
|
+
* once (the function `pause()` returns is idempotent), and a released or disposed clock ignores
|
|
12
|
+
* late calls, so an aborted run can never leave a timer behind.
|
|
13
|
+
*/
|
|
14
|
+
/** Longest delay a timer supports; a longer limit re-arms itself. */
|
|
15
|
+
const MAX_TIMER_MS = 2_147_483_647;
|
|
16
|
+
const realSource = {
|
|
17
|
+
now: () => Date.now(),
|
|
18
|
+
setTimeout: (fn, ms) => {
|
|
19
|
+
const timer = setTimeout(fn, ms);
|
|
20
|
+
// Like `AbortSignal.timeout`: the limit alone never keeps the process alive.
|
|
21
|
+
timer.unref?.();
|
|
22
|
+
return timer;
|
|
23
|
+
},
|
|
24
|
+
clearTimeout: (handle) => clearTimeout(handle),
|
|
25
|
+
};
|
|
26
|
+
export class RunClock {
|
|
27
|
+
limitMs;
|
|
28
|
+
controller = new AbortController();
|
|
29
|
+
source;
|
|
30
|
+
consumed = 0;
|
|
31
|
+
/** Start of the current running stretch (undefined while paused or finished). */
|
|
32
|
+
since;
|
|
33
|
+
timer;
|
|
34
|
+
pauses = 0;
|
|
35
|
+
done = false;
|
|
36
|
+
constructor(limitMs, source = realSource) {
|
|
37
|
+
this.limitMs = limitMs;
|
|
38
|
+
this.source = source;
|
|
39
|
+
this.since = source.now();
|
|
40
|
+
this.arm();
|
|
41
|
+
}
|
|
42
|
+
/** Aborts with a `TimeoutError` (the same reason `AbortSignal.timeout` uses) at the limit. */
|
|
43
|
+
get signal() {
|
|
44
|
+
return this.controller.signal;
|
|
45
|
+
}
|
|
46
|
+
/** Whether at least one human wait is open. */
|
|
47
|
+
get paused() {
|
|
48
|
+
return this.pauses > 0;
|
|
49
|
+
}
|
|
50
|
+
/** Time the run was really working: elapsed time minus every paused stretch. */
|
|
51
|
+
activeMs() {
|
|
52
|
+
return (this.consumed + (this.since === undefined ? 0 : Math.max(0, this.source.now() - this.since)));
|
|
53
|
+
}
|
|
54
|
+
remainingMs() {
|
|
55
|
+
return Math.max(0, this.limitMs - this.activeMs());
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Opens a human wait. Returns the function that closes it (idempotent). Pausing a clock that
|
|
59
|
+
* already ended (limit reached or disposed) does nothing.
|
|
60
|
+
*/
|
|
61
|
+
pause() {
|
|
62
|
+
if (this.done)
|
|
63
|
+
return () => { };
|
|
64
|
+
if (this.pauses++ === 0) {
|
|
65
|
+
if (this.since !== undefined)
|
|
66
|
+
this.consumed += Math.max(0, this.source.now() - this.since);
|
|
67
|
+
this.since = undefined;
|
|
68
|
+
this.disarm();
|
|
69
|
+
}
|
|
70
|
+
let released = false;
|
|
71
|
+
return () => {
|
|
72
|
+
if (released)
|
|
73
|
+
return;
|
|
74
|
+
released = true;
|
|
75
|
+
if (this.done)
|
|
76
|
+
return;
|
|
77
|
+
if (--this.pauses === 0) {
|
|
78
|
+
this.since = this.source.now();
|
|
79
|
+
this.arm();
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/** Stops the clock for good and frees its timer (call when the run ends, whatever the reason). */
|
|
84
|
+
dispose() {
|
|
85
|
+
if (this.done)
|
|
86
|
+
return;
|
|
87
|
+
this.consumed = this.activeMs();
|
|
88
|
+
this.done = true;
|
|
89
|
+
this.since = undefined;
|
|
90
|
+
this.disarm();
|
|
91
|
+
}
|
|
92
|
+
arm() {
|
|
93
|
+
this.disarm();
|
|
94
|
+
this.timer = this.source.setTimeout(() => {
|
|
95
|
+
this.timer = undefined;
|
|
96
|
+
if (this.done || this.pauses > 0)
|
|
97
|
+
return;
|
|
98
|
+
if (this.remainingMs() > 0)
|
|
99
|
+
return this.arm();
|
|
100
|
+
this.consumed = this.activeMs();
|
|
101
|
+
this.done = true;
|
|
102
|
+
this.since = undefined;
|
|
103
|
+
this.controller.abort(new DOMException("The operation timed out.", "TimeoutError"));
|
|
104
|
+
}, Math.min(this.remainingMs(), MAX_TIMER_MS));
|
|
105
|
+
}
|
|
106
|
+
disarm() {
|
|
107
|
+
if (this.timer !== undefined)
|
|
108
|
+
this.source.clearTimeout(this.timer);
|
|
109
|
+
this.timer = undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
package/dist/core/run-stats.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface RunStats {
|
|
|
11
11
|
output: number;
|
|
12
12
|
};
|
|
13
13
|
toolCalls: number;
|
|
14
|
+
/** Active time of the run (waits for a person excluded). */
|
|
15
|
+
activeMs?: number;
|
|
14
16
|
}
|
|
15
17
|
export declare function attachRunStats(error: unknown, stats: RunStats): void;
|
|
16
18
|
export declare function runStatsOf(error: unknown): RunStats | undefined;
|