@alisio/core 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/application.d.ts +7 -0
- package/dist/application.js +90 -6
- package/dist/changelog/data.js +36 -0
- package/dist/commands/builtins.js +6 -0
- package/dist/commands/catalog.d.ts +8 -1
- package/dist/commands/catalog.js +62 -0
- package/dist/config.d.ts +37 -3
- package/dist/config.js +109 -7
- package/dist/core/contracts.d.ts +10 -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 +15 -1
- package/dist/core/runner.js +70 -18
- package/dist/core/timeout.js +2 -2
- package/dist/decisions/bound.d.ts +23 -0
- package/dist/decisions/bound.js +48 -0
- package/dist/decisions/breaker.d.ts +35 -0
- package/dist/decisions/breaker.js +61 -0
- package/dist/decisions/format.d.ts +15 -0
- package/dist/decisions/format.js +89 -0
- package/dist/decisions/index.d.ts +7 -0
- package/dist/decisions/index.js +7 -0
- package/dist/decisions/metrics.d.ts +45 -0
- package/dist/decisions/metrics.js +79 -0
- package/dist/decisions/registry.d.ts +22 -0
- package/dist/decisions/registry.js +64 -0
- package/dist/decisions/service.d.ts +137 -0
- package/dist/decisions/service.js +377 -0
- package/dist/decisions/validate.d.ts +26 -0
- package/dist/decisions/validate.js +182 -0
- package/dist/goal/host.d.ts +2 -0
- package/dist/goal/host.js +2 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +3 -0
- package/dist/plugins/host.d.ts +43 -2
- package/dist/plugins/host.js +111 -16
- package/package.json +2 -2
package/dist/application.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { type SettableSettingKey } from "./config.ts";
|
|
|
14
14
|
import type { ApprovalHandler } from "./core/contracts.ts";
|
|
15
15
|
import { ToolRegistry } from "./core/registry.ts";
|
|
16
16
|
import { AgentRunner } from "./core/runner.ts";
|
|
17
|
+
import { DecisionMetrics, DecisionRegistry, DecisionService } from "./decisions/index.ts";
|
|
17
18
|
import { GoalService } from "./goal/service.ts";
|
|
18
19
|
import { HerdrBridge } from "./integrations/herdr.ts";
|
|
19
20
|
import { McpConnector } from "./mcp/connector.ts";
|
|
@@ -198,6 +199,12 @@ export declare function createApplication(options?: AppOptions): Promise<{
|
|
|
198
199
|
/** Tabular datasets of every session (ingestion, read-only queries, pages). */
|
|
199
200
|
datasets: DatasetService;
|
|
200
201
|
registry: ToolRegistry;
|
|
202
|
+
/** Decision Intelligence: the provider catalog, the service and in-memory metrics. */
|
|
203
|
+
decisions: {
|
|
204
|
+
registry: DecisionRegistry;
|
|
205
|
+
service: DecisionService;
|
|
206
|
+
metrics: DecisionMetrics;
|
|
207
|
+
};
|
|
201
208
|
context: ProjectContext;
|
|
202
209
|
skills: Skills;
|
|
203
210
|
/** Path- and content-safe metadata for the interactive skills manager. */
|
package/dist/application.js
CHANGED
|
@@ -16,9 +16,11 @@ 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";
|
|
23
|
+
import { bindDecisions, DecisionMetrics, DecisionRegistry, DecisionService, } from "./decisions/index.js";
|
|
22
24
|
import { GoalService } from "./goal/service.js";
|
|
23
25
|
import { GoalStore } from "./goal/store.js";
|
|
24
26
|
import { HerdrBridge } from "./integrations/herdr.js";
|
|
@@ -86,11 +88,25 @@ export async function createApplication(options = {}) {
|
|
|
86
88
|
const addDirs = options.readOnly
|
|
87
89
|
? []
|
|
88
90
|
: await Promise.all((options.addDirs ?? []).map((dir) => realpath(resolve(cwd, dir)).catch(() => resolve(cwd, dir))));
|
|
91
|
+
// Waits for a person (approvals, questions, the plan review) do not count against the run's
|
|
92
|
+
// time limit. One registry serves the runner, the plugin host's interactive UI and the
|
|
93
|
+
// external-directory approval, so every host (TUI, web server, embedders) gets it for free.
|
|
94
|
+
const humanWaits = new HumanWaits((session) => {
|
|
95
|
+
try {
|
|
96
|
+
return store.get(session).parentId ?? undefined;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
const askExternalDirectory = options.approveExternalDirectory;
|
|
89
103
|
const pathAccess = new PathAccess({
|
|
90
104
|
workspace,
|
|
91
105
|
extraRoots: [...config.additionalDirectories, ...addDirs],
|
|
92
|
-
...(
|
|
93
|
-
? {
|
|
106
|
+
...(askExternalDirectory && !options.readOnly
|
|
107
|
+
? {
|
|
108
|
+
approve: (request) => humanWaits.track(request.session, () => askExternalDirectory(request)),
|
|
109
|
+
}
|
|
94
110
|
: {}),
|
|
95
111
|
readOnly: !!options.readOnly,
|
|
96
112
|
});
|
|
@@ -107,7 +123,19 @@ export async function createApplication(options = {}) {
|
|
|
107
123
|
cwd,
|
|
108
124
|
claudeMdFallback: config.context.claudeMdFallback,
|
|
109
125
|
maxBytes: config.context.maxBytes,
|
|
110
|
-
}), providers = new ProviderRegistry(), plugins = new PluginHost(registry, store,
|
|
126
|
+
}), providers = new ProviderRegistry(), plugins = new PluginHost(registry, store, {
|
|
127
|
+
hookTimeoutMs: config.pluginHooks.timeoutMs,
|
|
128
|
+
sessionEndTimeoutMs: config.pluginHooks.sessionEndTimeoutMs,
|
|
129
|
+
disposeTimeoutMs: config.pluginHooks.disposeTimeoutMs,
|
|
130
|
+
}, providers), decisionRegistry = new DecisionRegistry(), decisionMetrics = new DecisionMetrics(), decisionService = new DecisionService({
|
|
131
|
+
registry: decisionRegistry,
|
|
132
|
+
// Read per call: `updateSetting("decisions.*")` mutates `config.decisions`.
|
|
133
|
+
config: () => config.decisions,
|
|
134
|
+
metrics: decisionMetrics,
|
|
135
|
+
// Failures never reach the terminal (they would garble the TUI): they are kept as
|
|
136
|
+
// `lastLifecycleError` in the service status.
|
|
137
|
+
lifecycle: { timeoutMs: () => config.pluginHooks.timeoutMs },
|
|
138
|
+
}), mcp = new McpConnector(config.mcp.servers, registry, workspace, options.readOnly ? "read-only" : options.allowMcp || mcpAllow ? "allowed" : "disabled", config.mcpSources);
|
|
111
139
|
let mcpRuntimePermission = options.readOnly
|
|
112
140
|
? "read-only"
|
|
113
141
|
: options.allowMcp || mcpAllow
|
|
@@ -116,6 +144,20 @@ export async function createApplication(options = {}) {
|
|
|
116
144
|
const herdr = options.noHerdr
|
|
117
145
|
? new HerdrBridge({}, async () => "")
|
|
118
146
|
: HerdrBridge.fromEnvironment(workspace, (error) => process.stderr.write(`${error}\n`));
|
|
147
|
+
// Decision providers and per-plugin directories/options are bound before any plugin loads.
|
|
148
|
+
plugins.setDecisions({ registry: decisionRegistry, service: decisionService });
|
|
149
|
+
const stateRoot = options.db && options.db !== ":memory:" ? dirname(options.db) : stateHome();
|
|
150
|
+
plugins.setPluginEnvironment({
|
|
151
|
+
stateRoot,
|
|
152
|
+
configHome: configHome(),
|
|
153
|
+
options: (id, builtin) => {
|
|
154
|
+
if (builtin) {
|
|
155
|
+
const { enabled: _enabled, ...rest } = config.builtinPlugins[id] ?? {};
|
|
156
|
+
return rest;
|
|
157
|
+
}
|
|
158
|
+
return config.pluginOverrides[id]?.options ?? {};
|
|
159
|
+
},
|
|
160
|
+
});
|
|
119
161
|
try {
|
|
120
162
|
// First-party built-ins load through the trusted path, also under --read-only.
|
|
121
163
|
const builtinContext = {
|
|
@@ -264,6 +306,9 @@ export async function createApplication(options = {}) {
|
|
|
264
306
|
}
|
|
265
307
|
}
|
|
266
308
|
}
|
|
309
|
+
// Every plugin has registered its providers: start the configured one in the background
|
|
310
|
+
// (`activate` is bounded, never fatal and never awaited here).
|
|
311
|
+
decisionService.syncActive();
|
|
267
312
|
// Project skills follow the config trust model (--trust-project or an explicit --config).
|
|
268
313
|
await skills.discover(skillRoots({
|
|
269
314
|
workspace,
|
|
@@ -285,7 +330,6 @@ export async function createApplication(options = {}) {
|
|
|
285
330
|
resolveExtension: () => plugins.extensions.resolve("websearch"),
|
|
286
331
|
}, pathAccess);
|
|
287
332
|
// Python analysis and artifacts live next to the session database (like attachment blobs).
|
|
288
|
-
const stateRoot = options.db && options.db !== ":memory:" ? dirname(options.db) : stateHome();
|
|
289
333
|
const artifacts = new ArtifactStore({
|
|
290
334
|
root: stateRoot,
|
|
291
335
|
db: store.db,
|
|
@@ -730,7 +774,9 @@ export async function createApplication(options = {}) {
|
|
|
730
774
|
: [];
|
|
731
775
|
return declared.length ? Math.min(...declared) : undefined;
|
|
732
776
|
};
|
|
777
|
+
plugins.setHumanWaits(humanWaits);
|
|
733
778
|
const runner = new AgentRunner({
|
|
779
|
+
humanWaits,
|
|
734
780
|
provider,
|
|
735
781
|
providerFor,
|
|
736
782
|
registry,
|
|
@@ -770,6 +816,7 @@ export async function createApplication(options = {}) {
|
|
|
770
816
|
},
|
|
771
817
|
}
|
|
772
818
|
: {}),
|
|
819
|
+
decisions: (call, announce) => bindDecisions(decisionService, () => config.decisions.telemetry, call, announce),
|
|
773
820
|
artifacts: (call, announce) => createArtifactPublisher(artifacts, {
|
|
774
821
|
sessionId: call.sessionId,
|
|
775
822
|
rootSessionId: store.rootOf(call.sessionId),
|
|
@@ -899,6 +946,8 @@ export async function createApplication(options = {}) {
|
|
|
899
946
|
/** Tabular datasets of every session (ingestion, read-only queries, pages). */
|
|
900
947
|
datasets,
|
|
901
948
|
registry,
|
|
949
|
+
/** Decision Intelligence: the provider catalog, the service and in-memory metrics. */
|
|
950
|
+
decisions: { registry: decisionRegistry, service: decisionService, metrics: decisionMetrics },
|
|
902
951
|
context,
|
|
903
952
|
skills,
|
|
904
953
|
/** Path- and content-safe metadata for the interactive skills manager. */
|
|
@@ -1103,6 +1152,33 @@ export async function createApplication(options = {}) {
|
|
|
1103
1152
|
config.pluginHooks = { ...config.pluginHooks, timeoutMs: Number(value) };
|
|
1104
1153
|
plugins.applyTimeoutSettings({ hookTimeoutMs: Number(value) });
|
|
1105
1154
|
break;
|
|
1155
|
+
case "pluginHooks.disposeTimeoutMs":
|
|
1156
|
+
config.pluginHooks = { ...config.pluginHooks, disposeTimeoutMs: Number(value) };
|
|
1157
|
+
plugins.applyTimeoutSettings({ disposeTimeoutMs: Number(value) });
|
|
1158
|
+
break;
|
|
1159
|
+
case "decisions.enabled":
|
|
1160
|
+
case "decisions.telemetry": {
|
|
1161
|
+
// Read live by the service on its next call; `enabled` also drives the provider's
|
|
1162
|
+
// activate/deactivate lifecycle.
|
|
1163
|
+
const leaf = key.slice("decisions.".length);
|
|
1164
|
+
config.decisions = { ...config.decisions, [leaf]: value === true };
|
|
1165
|
+
decisionService.syncActive();
|
|
1166
|
+
break;
|
|
1167
|
+
}
|
|
1168
|
+
case "decisions.provider":
|
|
1169
|
+
// `!clear` (undefined) deactivates the feature.
|
|
1170
|
+
config.decisions = {
|
|
1171
|
+
...config.decisions,
|
|
1172
|
+
provider: value === undefined || value === null ? null : String(value),
|
|
1173
|
+
};
|
|
1174
|
+
decisionService.syncActive();
|
|
1175
|
+
break;
|
|
1176
|
+
case "decisions.timeoutMs":
|
|
1177
|
+
case "decisions.minConfidence": {
|
|
1178
|
+
const leaf = key.slice("decisions.".length);
|
|
1179
|
+
config.decisions = { ...config.decisions, [leaf]: Number(value) };
|
|
1180
|
+
break;
|
|
1181
|
+
}
|
|
1106
1182
|
case "tui.paddingX":
|
|
1107
1183
|
config.tui = { ...config.tui, paddingX: Number(value) };
|
|
1108
1184
|
break;
|
|
@@ -1357,7 +1433,11 @@ export async function createApplication(options = {}) {
|
|
|
1357
1433
|
...[...runtimes]
|
|
1358
1434
|
.filter((runtime) => runtime !== provider.currentProvider)
|
|
1359
1435
|
.map((runtime) => Promise.resolve(runtime.dispose?.())),
|
|
1360
|
-
|
|
1436
|
+
// The active decision provider stops first (its own limit), then every plugin
|
|
1437
|
+
// is disposed in parallel, each bounded by `pluginHooks.disposeTimeoutMs`.
|
|
1438
|
+
decisionService
|
|
1439
|
+
.shutdown(config.pluginHooks.disposeTimeoutMs)
|
|
1440
|
+
.then(() => plugins.close()),
|
|
1361
1441
|
]);
|
|
1362
1442
|
})());
|
|
1363
1443
|
}
|
|
@@ -1370,7 +1450,11 @@ export async function createApplication(options = {}) {
|
|
|
1370
1450
|
}
|
|
1371
1451
|
catch (e) {
|
|
1372
1452
|
await Promise.race([
|
|
1373
|
-
Promise.allSettled([
|
|
1453
|
+
Promise.allSettled([
|
|
1454
|
+
herdr.close(),
|
|
1455
|
+
mcp.close(),
|
|
1456
|
+
decisionService.shutdown(config.pluginHooks.disposeTimeoutMs).then(() => plugins.close()),
|
|
1457
|
+
]),
|
|
1374
1458
|
delay(TEARDOWN_STAGE_TIMEOUT_MS),
|
|
1375
1459
|
]);
|
|
1376
1460
|
store.close();
|
package/dist/changelog/data.js
CHANGED
|
@@ -2,6 +2,42 @@
|
|
|
2
2
|
export const CHANGELOG_DATA = {
|
|
3
3
|
schema: 1,
|
|
4
4
|
entries: [
|
|
5
|
+
{
|
|
6
|
+
version: "0.3.0",
|
|
7
|
+
date: "2026-10-03",
|
|
8
|
+
sections: [
|
|
9
|
+
{
|
|
10
|
+
title: "Added",
|
|
11
|
+
items: [
|
|
12
|
+
"Decision Intelligence: an optional provider contract that lets a plugin answer small, closed decisions (pick an option, yes/no, a level) without a full model generation. Alisio works the same without a provider; installing a provider plugin does not activate it.",
|
|
13
|
+
"`/decisions` (terminal and web) shows whether decisions are enabled, the active provider, its health and the session metrics. `/stats` gains a Decision Intelligence section once a decision has run.",
|
|
14
|
+
"New settings: the `decisions` block (`enabled`, `provider`, `timeoutMs`, `minConfidence`, `telemetry`), plugin options in `pluginOverrides.<id>.options` and `pluginHooks.disposeTimeoutMs`.",
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
title: "Improved",
|
|
19
|
+
items: [
|
|
20
|
+
"Plugins are shut down in parallel, each with its own time limit, so one slow plugin no longer delays the others.",
|
|
21
|
+
"`alisio run` now shuts plugins down cleanly on SIGTERM and SIGHUP, not only on Ctrl+C.",
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
version: "0.2.1",
|
|
28
|
+
date: "2026-10-02",
|
|
29
|
+
sections: [
|
|
30
|
+
{
|
|
31
|
+
title: "Fixed",
|
|
32
|
+
items: [
|
|
33
|
+
'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.',
|
|
34
|
+
'A run that ended or was stopped during a plan review no longer leaves the next message failing with an "Uncertain tool outcome" error.',
|
|
35
|
+
"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).",
|
|
36
|
+
'The Settings label of `limits.timeoutMs` said "Provider request timeout"; it now says it is the run time limit.',
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
},
|
|
5
41
|
{
|
|
6
42
|
version: "0.2.0",
|
|
7
43
|
date: "2026-10-02",
|
|
@@ -151,6 +151,12 @@ export const BUILTIN_COMMANDS = [
|
|
|
151
151
|
surfaces: INTERACTIVE,
|
|
152
152
|
execution: "surface",
|
|
153
153
|
},
|
|
154
|
+
{
|
|
155
|
+
name: "decisions",
|
|
156
|
+
description: "Show Decision Intelligence status, provider health and session metrics",
|
|
157
|
+
surfaces: ALL,
|
|
158
|
+
execution: "core",
|
|
159
|
+
},
|
|
154
160
|
{
|
|
155
161
|
name: "exit",
|
|
156
162
|
description: "Exit Alisio",
|
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
* The catalog reads its sources live from the host on each call, so plugin/template/skill
|
|
5
5
|
* changes show up without rebuilding it.
|
|
6
6
|
*/
|
|
7
|
-
import type
|
|
7
|
+
import { type CommandDescriptor, type ToolDefinition } from "@alisio/sdk";
|
|
8
8
|
import type { PluginCatalogEntry } from "../application.ts";
|
|
9
9
|
import type { SessionStore } from "../core/contracts.ts";
|
|
10
10
|
import type { AgentRunner } from "../core/runner.ts";
|
|
11
|
+
import { type DecisionMetrics, type DecisionService, type DecisionsConfig } from "../decisions/index.ts";
|
|
11
12
|
import type { McpServerInfo } from "../mcp/connector.ts";
|
|
12
13
|
import type { PromptTemplate } from "../resources/prompts.ts";
|
|
13
14
|
import type { SkillCatalogEntry } from "../resources/skills.ts";
|
|
@@ -54,6 +55,12 @@ export interface CommandHost {
|
|
|
54
55
|
active?: string;
|
|
55
56
|
effort?: string;
|
|
56
57
|
};
|
|
58
|
+
decisions?: Pick<DecisionsConfig, "timeoutMs" | "minConfidence" | "telemetry">;
|
|
59
|
+
};
|
|
60
|
+
/** Decision Intelligence (`/decisions`, and the `/stats` section). */
|
|
61
|
+
decisions?: {
|
|
62
|
+
service: Pick<DecisionService, "status">;
|
|
63
|
+
metrics: Pick<DecisionMetrics, "snapshot">;
|
|
57
64
|
};
|
|
58
65
|
updateSetting?(key: "agents.effort", value: unknown): Promise<string>;
|
|
59
66
|
/** `/btw` side questions (tool-less, outside the conversation). */
|
package/dist/commands/catalog.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One catalog of slash commands for every surface (TUI, web, API): built-ins, plugin commands,
|
|
3
|
+
* prompt templates and skills, with the pure core handlers of `execution: "core"` commands.
|
|
4
|
+
* The catalog reads its sources live from the host on each call, so plugin/template/skill
|
|
5
|
+
* changes show up without rebuilding it.
|
|
6
|
+
*/
|
|
7
|
+
import { summarizeDecisionEvents, } from "@alisio/sdk";
|
|
1
8
|
import { agentCatalogFromState, agentCommandDescriptors, resolveActiveAgent, } from "../agents/active.js";
|
|
9
|
+
import { formatDecisionStatsSection, formatDecisionsReport, } from "../decisions/index.js";
|
|
2
10
|
import { formatSideQuestion, SIDE_QUESTION_DESCRIPTION, SIDE_QUESTION_USAGE, } from "../sessions/side-questions.js";
|
|
3
11
|
import { BUILTIN_COMMANDS } from "./builtins.js";
|
|
4
12
|
export { BUILTIN_COMMANDS };
|
|
@@ -20,7 +28,59 @@ const firstPrompt = (host, id) => {
|
|
|
20
28
|
const first = host.store.messages(id).find((m) => m.role === "user" && !m.summary);
|
|
21
29
|
return first?.role === "user" ? first.text.replace(/\s+/g, " ").slice(0, 60) : "";
|
|
22
30
|
};
|
|
31
|
+
const EVENT_PAGE = 5000;
|
|
32
|
+
const MAX_EVENT_PAGES = 20;
|
|
33
|
+
/**
|
|
34
|
+
* The session's persisted `decision_*` events as `RunEvent`s (`createdAt` becomes the timestamp,
|
|
35
|
+
* so `lastFallback.at` is right), paged through the store. Empty without a paging store.
|
|
36
|
+
*/
|
|
37
|
+
function decisionEvents(host, sessionId) {
|
|
38
|
+
const out = [];
|
|
39
|
+
let after = 0;
|
|
40
|
+
for (let page = 0; page < MAX_EVENT_PAGES; page++) {
|
|
41
|
+
const result = host.store.eventsPage?.(sessionId, { after, limit: EVENT_PAGE });
|
|
42
|
+
if (!result?.items.length)
|
|
43
|
+
break;
|
|
44
|
+
for (const item of result.items) {
|
|
45
|
+
if (item.type !== "decision_completed" && item.type !== "decision_fallback")
|
|
46
|
+
continue;
|
|
47
|
+
out.push({
|
|
48
|
+
schemaVersion: 1,
|
|
49
|
+
runId: item.runId,
|
|
50
|
+
sessionId,
|
|
51
|
+
seq: out.length + 1,
|
|
52
|
+
type: item.type,
|
|
53
|
+
timestamp: new Date(item.createdAt ?? 0).toISOString(),
|
|
54
|
+
data: item.data,
|
|
55
|
+
eventId: item.eventId,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
if (!result.hasMore)
|
|
59
|
+
break;
|
|
60
|
+
after = Number(result.items.at(-1)?.eventId);
|
|
61
|
+
if (!Number.isFinite(after))
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
23
66
|
const HANDLERS = {
|
|
67
|
+
async decisions(_args, ctx, host) {
|
|
68
|
+
const decisions = host.decisions;
|
|
69
|
+
if (!decisions)
|
|
70
|
+
return { text: "Decision Intelligence is unavailable here", tone: "notice" };
|
|
71
|
+
const status = await decisions.service.status(ctx.signal);
|
|
72
|
+
const session = decisions.metrics.snapshot(ctx.sessionId);
|
|
73
|
+
const processView = decisions.metrics.snapshot();
|
|
74
|
+
return {
|
|
75
|
+
text: formatDecisionsReport({
|
|
76
|
+
status,
|
|
77
|
+
config: host.config?.decisions ?? { timeoutMs: 0, minConfidence: 0, telemetry: false },
|
|
78
|
+
session,
|
|
79
|
+
process: processView,
|
|
80
|
+
}),
|
|
81
|
+
data: { status, session, process: processView },
|
|
82
|
+
};
|
|
83
|
+
},
|
|
24
84
|
async tools(_args, _ctx, host) {
|
|
25
85
|
const policy = host.runner.policy;
|
|
26
86
|
const rows = host.registry
|
|
@@ -113,6 +173,7 @@ const HANDLERS = {
|
|
|
113
173
|
input: sum.input + (r.usage?.input ?? 0),
|
|
114
174
|
output: sum.output + (r.usage?.output ?? 0),
|
|
115
175
|
}), { input: 0, output: 0 });
|
|
176
|
+
const decisionSection = formatDecisionStatsSection(summarizeDecisionEvents(decisionEvents(host, ctx.sessionId)));
|
|
116
177
|
return {
|
|
117
178
|
text: [
|
|
118
179
|
"**Session statistics**",
|
|
@@ -122,6 +183,7 @@ const HANDLERS = {
|
|
|
122
183
|
`- Messages: ${host.store.messages(session.id).length}`,
|
|
123
184
|
`- Runs: ${runs.length}`,
|
|
124
185
|
`- Tokens: in ${tokens.input} · out ${tokens.output}`,
|
|
186
|
+
...(decisionSection ? ["", decisionSection] : []),
|
|
125
187
|
].join("\n"),
|
|
126
188
|
data: { session, runs: runs.length, tokens },
|
|
127
189
|
};
|
package/dist/config.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { JsonValue } from "@alisio/sdk";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
declare const serverSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
3
4
|
transport: z.ZodLiteral<"stdio">;
|
|
@@ -142,11 +143,20 @@ export declare const configSchema: z.ZodPipe<z.ZodObject<{
|
|
|
142
143
|
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
143
144
|
}, z.core.$loose>>>;
|
|
144
145
|
pluginOverrides: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
145
|
-
enabled: z.ZodBoolean
|
|
146
|
+
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
147
|
+
options: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodType<JsonValue, unknown, z.core.$ZodTypeInternals<JsonValue, unknown>>>>;
|
|
146
148
|
}, z.core.$strict>>>;
|
|
147
149
|
pluginHooks: z.ZodDefault<z.ZodObject<{
|
|
148
150
|
timeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
149
151
|
sessionEndTimeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
152
|
+
disposeTimeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
153
|
+
}, z.core.$strict>>;
|
|
154
|
+
decisions: z.ZodDefault<z.ZodObject<{
|
|
155
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
156
|
+
provider: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
157
|
+
timeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
158
|
+
minConfidence: z.ZodDefault<z.ZodNumber>;
|
|
159
|
+
telemetry: z.ZodDefault<z.ZodBoolean>;
|
|
150
160
|
}, z.core.$strict>>;
|
|
151
161
|
context: z.ZodDefault<z.ZodObject<{
|
|
152
162
|
claudeMdFallback: z.ZodDefault<z.ZodBoolean>;
|
|
@@ -267,11 +277,20 @@ export declare const configSchema: z.ZodPipe<z.ZodObject<{
|
|
|
267
277
|
enabled?: boolean | undefined;
|
|
268
278
|
}>;
|
|
269
279
|
pluginOverrides: Record<string, {
|
|
270
|
-
enabled
|
|
280
|
+
enabled?: boolean | undefined;
|
|
281
|
+
options?: Record<string, JsonValue> | undefined;
|
|
271
282
|
}>;
|
|
272
283
|
pluginHooks: {
|
|
273
284
|
timeoutMs: number;
|
|
274
285
|
sessionEndTimeoutMs: number;
|
|
286
|
+
disposeTimeoutMs: number;
|
|
287
|
+
};
|
|
288
|
+
decisions: {
|
|
289
|
+
enabled: boolean;
|
|
290
|
+
provider: string | null;
|
|
291
|
+
timeoutMs: number;
|
|
292
|
+
minConfidence: number;
|
|
293
|
+
telemetry: boolean;
|
|
275
294
|
};
|
|
276
295
|
context: {
|
|
277
296
|
claudeMdFallback: boolean;
|
|
@@ -461,11 +480,20 @@ export declare const configSchema: z.ZodPipe<z.ZodObject<{
|
|
|
461
480
|
enabled?: boolean | undefined;
|
|
462
481
|
}>;
|
|
463
482
|
pluginOverrides: Record<string, {
|
|
464
|
-
enabled
|
|
483
|
+
enabled?: boolean | undefined;
|
|
484
|
+
options?: Record<string, JsonValue> | undefined;
|
|
465
485
|
}>;
|
|
466
486
|
pluginHooks: {
|
|
467
487
|
timeoutMs: number;
|
|
468
488
|
sessionEndTimeoutMs: number;
|
|
489
|
+
disposeTimeoutMs: number;
|
|
490
|
+
};
|
|
491
|
+
decisions: {
|
|
492
|
+
enabled: boolean;
|
|
493
|
+
provider: string | null;
|
|
494
|
+
timeoutMs: number;
|
|
495
|
+
minConfidence: number;
|
|
496
|
+
telemetry: boolean;
|
|
469
497
|
};
|
|
470
498
|
context: {
|
|
471
499
|
claudeMdFallback: boolean;
|
|
@@ -625,6 +653,12 @@ declare const SETTABLE_KEYS: {
|
|
|
625
653
|
readonly "limits.firstTokenRetries": z.ZodDefault<z.ZodNumber>;
|
|
626
654
|
readonly "limits.truncationRecoveries": z.ZodDefault<z.ZodNumber>;
|
|
627
655
|
readonly "pluginHooks.timeoutMs": z.ZodDefault<z.ZodNumber>;
|
|
656
|
+
readonly "pluginHooks.disposeTimeoutMs": z.ZodDefault<z.ZodNumber>;
|
|
657
|
+
readonly "decisions.enabled": z.ZodDefault<z.ZodBoolean>;
|
|
658
|
+
readonly "decisions.provider": z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
659
|
+
readonly "decisions.timeoutMs": z.ZodDefault<z.ZodNumber>;
|
|
660
|
+
readonly "decisions.minConfidence": z.ZodDefault<z.ZodNumber>;
|
|
661
|
+
readonly "decisions.telemetry": z.ZodDefault<z.ZodBoolean>;
|
|
628
662
|
readonly "tui.paddingX": z.ZodDefault<z.ZodNumber>;
|
|
629
663
|
readonly "tui.contentPaddingX": z.ZodDefault<z.ZodNumber>;
|
|
630
664
|
readonly "tui.skillSlashCommands": z.ZodDefault<z.ZodBoolean>;
|
package/dist/config.js
CHANGED
|
@@ -2,6 +2,7 @@ import { chmod, mkdir, open, readFile, realpath, rename, unlink } from "node:fs/
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import { DEFAULT_DECISIONS_CONFIG } from "./decisions/service.js";
|
|
5
6
|
import { exists, readJson } from "./runtime/fs.js";
|
|
6
7
|
import { isPathSpec } from "./runtime/modules.js";
|
|
7
8
|
const providerSchema = z
|
|
@@ -82,6 +83,35 @@ const serversSchema = (value) => z.record(z.string(), value).superRefine((server
|
|
|
82
83
|
export const OCI_IMAGE_PATTERN = /^[a-zA-Z0-9][^\s@]*@sha256:[a-f0-9]{64}$/;
|
|
83
84
|
/** Analysis keys only the user (global) layer decides; a project layer cannot set them. */
|
|
84
85
|
const GLOBAL_ONLY_ANALYSIS = ["runtime", "oci", "retention"];
|
|
86
|
+
/** Decision Intelligence keys only the user (global) layer decides: the provider and telemetry. */
|
|
87
|
+
const GLOBAL_ONLY_DECISIONS = ["provider", "telemetry"];
|
|
88
|
+
/** JSON values a plugin may receive as options (`pluginOverrides[id].options`). */
|
|
89
|
+
const jsonValueSchema = z.lazy(() => z.union([
|
|
90
|
+
z.string(),
|
|
91
|
+
z.number().finite(),
|
|
92
|
+
z.boolean(),
|
|
93
|
+
z.null(),
|
|
94
|
+
z.array(jsonValueSchema),
|
|
95
|
+
z.record(z.string(), jsonValueSchema),
|
|
96
|
+
]));
|
|
97
|
+
const PLUGIN_OPTION_KEY = /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/;
|
|
98
|
+
const PLUGIN_OPTIONS_MAX_BYTES = 8 * 1024;
|
|
99
|
+
const pluginOptionsSchema = z
|
|
100
|
+
.record(z.string(), jsonValueSchema)
|
|
101
|
+
.superRefine((options, context) => {
|
|
102
|
+
for (const key of Object.keys(options))
|
|
103
|
+
if (!PLUGIN_OPTION_KEY.test(key))
|
|
104
|
+
context.addIssue({
|
|
105
|
+
code: "custom",
|
|
106
|
+
path: [key],
|
|
107
|
+
message: "Plugin option names start with a letter and use letters, digits, dot, underscore or dash",
|
|
108
|
+
});
|
|
109
|
+
if (Buffer.byteLength(JSON.stringify(options), "utf8") > PLUGIN_OPTIONS_MAX_BYTES)
|
|
110
|
+
context.addIssue({
|
|
111
|
+
code: "custom",
|
|
112
|
+
message: "Plugin options may not exceed 8 KB serialized",
|
|
113
|
+
});
|
|
114
|
+
});
|
|
85
115
|
const configObjectSchema = z
|
|
86
116
|
.object({
|
|
87
117
|
schemaVersion: z.literal(1).default(1),
|
|
@@ -114,15 +144,43 @@ const configObjectSchema = z
|
|
|
114
144
|
.record(z.string(), z.object({ enabled: z.boolean().optional() }).passthrough())
|
|
115
145
|
.default({}),
|
|
116
146
|
/** Project-local enable/disable overrides for configured external plugins, keyed by plugin id. */
|
|
117
|
-
pluginOverrides: z
|
|
147
|
+
pluginOverrides: z
|
|
148
|
+
.record(z.string(), z
|
|
149
|
+
.object({
|
|
150
|
+
enabled: z.boolean().optional(),
|
|
151
|
+
/** Global-only, read-only options handed to the plugin as `api.options` (8 KB). */
|
|
152
|
+
options: pluginOptionsSchema.optional(),
|
|
153
|
+
})
|
|
154
|
+
.strict())
|
|
155
|
+
.default({}),
|
|
118
156
|
/** Host-enforced timeouts for plugin hooks. */
|
|
119
157
|
pluginHooks: z
|
|
120
158
|
.object({
|
|
121
159
|
timeoutMs: z.number().int().min(100).max(120_000).default(15_000),
|
|
122
160
|
sessionEndTimeoutMs: z.number().int().min(100).max(120_000).default(10_000),
|
|
161
|
+
/** Per-plugin limit for `dispose()` (and the active decision provider's `deactivate()`). */
|
|
162
|
+
disposeTimeoutMs: z.number().int().min(100).max(10_000).default(2000),
|
|
123
163
|
})
|
|
124
164
|
.strict()
|
|
125
|
-
.default(() => ({ timeoutMs: 15_000, sessionEndTimeoutMs: 10_000 })),
|
|
165
|
+
.default(() => ({ timeoutMs: 15_000, sessionEndTimeoutMs: 10_000, disposeTimeoutMs: 2000 })),
|
|
166
|
+
/**
|
|
167
|
+
* Decision Intelligence: the active provider is chosen here and only here. `provider` and
|
|
168
|
+
* `telemetry` are global-only (a repository cannot pick the engine or the telemetry policy).
|
|
169
|
+
*/
|
|
170
|
+
decisions: z
|
|
171
|
+
.object({
|
|
172
|
+
enabled: z.boolean().default(true),
|
|
173
|
+
provider: z
|
|
174
|
+
.string()
|
|
175
|
+
.regex(/^[a-z0-9][a-z0-9.-]{0,63}$/)
|
|
176
|
+
.nullable()
|
|
177
|
+
.default(null),
|
|
178
|
+
timeoutMs: z.number().int().min(50).max(10_000).default(DEFAULT_DECISIONS_CONFIG.timeoutMs),
|
|
179
|
+
minConfidence: z.number().min(0).max(1).default(DEFAULT_DECISIONS_CONFIG.minConfidence),
|
|
180
|
+
telemetry: z.boolean().default(true),
|
|
181
|
+
})
|
|
182
|
+
.strict()
|
|
183
|
+
.default(() => ({ ...DEFAULT_DECISIONS_CONFIG })),
|
|
126
184
|
context: z
|
|
127
185
|
.object({
|
|
128
186
|
/** Use CLAUDE.md where a directory has no AGENTS.md (off by default). */
|
|
@@ -550,8 +608,20 @@ export async function loadConfigWithProvenance(workspace, options = {}) {
|
|
|
550
608
|
const tasksRaw = object.tasks && typeof object.tasks === "object" && !Array.isArray(object.tasks)
|
|
551
609
|
? object.tasks
|
|
552
610
|
: {};
|
|
611
|
+
const decisionsRaw = object.decisions && typeof object.decisions === "object" && !Array.isArray(object.decisions)
|
|
612
|
+
? object.decisions
|
|
613
|
+
: {};
|
|
614
|
+
const overridesRaw = object.pluginOverrides &&
|
|
615
|
+
typeof object.pluginOverrides === "object" &&
|
|
616
|
+
!Array.isArray(object.pluginOverrides)
|
|
617
|
+
? object.pluginOverrides
|
|
618
|
+
: {};
|
|
553
619
|
const ignored = [
|
|
554
620
|
...GLOBAL_ONLY_ANALYSIS.filter((key) => key in analysisRaw).map((key) => `analysis.${key}`),
|
|
621
|
+
...GLOBAL_ONLY_DECISIONS.filter((key) => key in decisionsRaw).map((key) => `decisions.${key}`),
|
|
622
|
+
...Object.entries(overridesRaw)
|
|
623
|
+
.filter(([, entry]) => !!entry && typeof entry === "object" && !Array.isArray(entry) && "options" in entry)
|
|
624
|
+
.map(([id]) => `pluginOverrides.${id}.options`),
|
|
555
625
|
...("retentionDays" in tasksRaw ? ["tasks.retentionDays"] : []),
|
|
556
626
|
];
|
|
557
627
|
return { config, keys: new Set(Object.keys(object)), sources, ignored, file };
|
|
@@ -592,10 +662,19 @@ export async function loadConfigWithProvenance(workspace, options = {}) {
|
|
|
592
662
|
}
|
|
593
663
|
// Additive records: merge by key, the selected layer wins per key.
|
|
594
664
|
if (key === "pluginOverrides") {
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
665
|
+
// `options` is global-only: a repository must not inject options into a plugin that has
|
|
666
|
+
// system access, so the selected entry keeps its `enabled` but never its `options`.
|
|
667
|
+
const merged = { ...overlaid.pluginOverrides };
|
|
668
|
+
for (const [id, entry] of Object.entries(selected.config.pluginOverrides)) {
|
|
669
|
+
const options = global.config.pluginOverrides[id]?.options;
|
|
670
|
+
const { options: _ignored, ...rest } = entry;
|
|
671
|
+
merged[id] = { ...global.config.pluginOverrides[id], ...rest };
|
|
672
|
+
if (options)
|
|
673
|
+
merged[id].options = options;
|
|
674
|
+
else
|
|
675
|
+
delete merged[id].options;
|
|
676
|
+
}
|
|
677
|
+
overlaid.pluginOverrides = merged;
|
|
599
678
|
continue;
|
|
600
679
|
}
|
|
601
680
|
if (key === "skillOverrides") {
|
|
@@ -622,6 +701,16 @@ export async function loadConfigWithProvenance(workspace, options = {}) {
|
|
|
622
701
|
};
|
|
623
702
|
continue;
|
|
624
703
|
}
|
|
704
|
+
if (key === "decisions") {
|
|
705
|
+
// `provider` and `telemetry` are global-only (a repository cannot pick the decision
|
|
706
|
+
// engine nor its telemetry policy); the rest follows the selected layer.
|
|
707
|
+
overlaid.decisions = {
|
|
708
|
+
...selected.config.decisions,
|
|
709
|
+
provider: global.config.decisions.provider,
|
|
710
|
+
telemetry: global.config.decisions.telemetry,
|
|
711
|
+
};
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
625
714
|
if (key === "tasks") {
|
|
626
715
|
// `retentionDays` is global-only (one sweep deletes the data of every workspace).
|
|
627
716
|
overlaid.tasks = {
|
|
@@ -773,6 +862,7 @@ const SETTABLE_SECTIONS = {
|
|
|
773
862
|
context: configObjectSchema.shape.context.removeDefault(),
|
|
774
863
|
limits: configObjectSchema.shape.limits.removeDefault(),
|
|
775
864
|
pluginHooks: configObjectSchema.shape.pluginHooks.removeDefault(),
|
|
865
|
+
decisions: configObjectSchema.shape.decisions.removeDefault(),
|
|
776
866
|
tui: configObjectSchema.shape.tui.removeDefault(),
|
|
777
867
|
websearch: configObjectSchema.shape.websearch.removeDefault(),
|
|
778
868
|
agents: configObjectSchema.shape.agents.removeDefault(),
|
|
@@ -798,6 +888,12 @@ const SETTABLE_KEYS = {
|
|
|
798
888
|
"limits.firstTokenRetries": SETTABLE_SECTIONS.limits.shape.firstTokenRetries,
|
|
799
889
|
"limits.truncationRecoveries": SETTABLE_SECTIONS.limits.shape.truncationRecoveries,
|
|
800
890
|
"pluginHooks.timeoutMs": SETTABLE_SECTIONS.pluginHooks.shape.timeoutMs,
|
|
891
|
+
"pluginHooks.disposeTimeoutMs": SETTABLE_SECTIONS.pluginHooks.shape.disposeTimeoutMs,
|
|
892
|
+
"decisions.enabled": SETTABLE_SECTIONS.decisions.shape.enabled,
|
|
893
|
+
"decisions.provider": SETTABLE_SECTIONS.decisions.shape.provider,
|
|
894
|
+
"decisions.timeoutMs": SETTABLE_SECTIONS.decisions.shape.timeoutMs,
|
|
895
|
+
"decisions.minConfidence": SETTABLE_SECTIONS.decisions.shape.minConfidence,
|
|
896
|
+
"decisions.telemetry": SETTABLE_SECTIONS.decisions.shape.telemetry,
|
|
801
897
|
"tui.paddingX": SETTABLE_SECTIONS.tui.shape.paddingX,
|
|
802
898
|
"tui.contentPaddingX": SETTABLE_SECTIONS.tui.shape.contentPaddingX,
|
|
803
899
|
"tui.skillSlashCommands": SETTABLE_SECTIONS.tui.shape.skillSlashCommands,
|
|
@@ -945,7 +1041,13 @@ export async function setProjectPluginEnabled(input) {
|
|
|
945
1041
|
!Array.isArray(raw.pluginOverrides)
|
|
946
1042
|
? raw.pluginOverrides
|
|
947
1043
|
: {};
|
|
948
|
-
|
|
1044
|
+
const entry = current[input.id] &&
|
|
1045
|
+
typeof current[input.id] === "object" &&
|
|
1046
|
+
!Array.isArray(current[input.id])
|
|
1047
|
+
? current[input.id]
|
|
1048
|
+
: {};
|
|
1049
|
+
// Keep the other fields of the entry (`options`): toggling a plugin must not erase them.
|
|
1050
|
+
raw.pluginOverrides = { ...current, [input.id]: { ...entry, enabled: input.enabled } };
|
|
949
1051
|
}
|
|
950
1052
|
raw.schemaVersion ??= 1;
|
|
951
1053
|
await mkdir(dirname(file), { recursive: true, mode: 0o700 });
|