@alisio/core 0.2.1 → 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 +71 -4
- package/dist/changelog/data.js +21 -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/runner.d.ts +3 -1
- package/dist/core/runner.js +2 -0
- 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/index.d.ts +3 -2
- package/dist/index.js +1 -0
- package/dist/plugins/host.d.ts +33 -1
- package/dist/plugins/host.js +94 -14
- 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
|
@@ -20,6 +20,7 @@ import { HumanWaits } from "./core/human-wait.js";
|
|
|
20
20
|
import { DEFAULT_MAX_OUTPUT_TOKENS } from "./core/output-limit.js";
|
|
21
21
|
import { ToolRegistry } from "./core/registry.js";
|
|
22
22
|
import { AgentRunner } from "./core/runner.js";
|
|
23
|
+
import { bindDecisions, DecisionMetrics, DecisionRegistry, DecisionService, } from "./decisions/index.js";
|
|
23
24
|
import { GoalService } from "./goal/service.js";
|
|
24
25
|
import { GoalStore } from "./goal/store.js";
|
|
25
26
|
import { HerdrBridge } from "./integrations/herdr.js";
|
|
@@ -122,7 +123,19 @@ export async function createApplication(options = {}) {
|
|
|
122
123
|
cwd,
|
|
123
124
|
claudeMdFallback: config.context.claudeMdFallback,
|
|
124
125
|
maxBytes: config.context.maxBytes,
|
|
125
|
-
}), 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);
|
|
126
139
|
let mcpRuntimePermission = options.readOnly
|
|
127
140
|
? "read-only"
|
|
128
141
|
: options.allowMcp || mcpAllow
|
|
@@ -131,6 +144,20 @@ export async function createApplication(options = {}) {
|
|
|
131
144
|
const herdr = options.noHerdr
|
|
132
145
|
? new HerdrBridge({}, async () => "")
|
|
133
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
|
+
});
|
|
134
161
|
try {
|
|
135
162
|
// First-party built-ins load through the trusted path, also under --read-only.
|
|
136
163
|
const builtinContext = {
|
|
@@ -279,6 +306,9 @@ export async function createApplication(options = {}) {
|
|
|
279
306
|
}
|
|
280
307
|
}
|
|
281
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();
|
|
282
312
|
// Project skills follow the config trust model (--trust-project or an explicit --config).
|
|
283
313
|
await skills.discover(skillRoots({
|
|
284
314
|
workspace,
|
|
@@ -300,7 +330,6 @@ export async function createApplication(options = {}) {
|
|
|
300
330
|
resolveExtension: () => plugins.extensions.resolve("websearch"),
|
|
301
331
|
}, pathAccess);
|
|
302
332
|
// Python analysis and artifacts live next to the session database (like attachment blobs).
|
|
303
|
-
const stateRoot = options.db && options.db !== ":memory:" ? dirname(options.db) : stateHome();
|
|
304
333
|
const artifacts = new ArtifactStore({
|
|
305
334
|
root: stateRoot,
|
|
306
335
|
db: store.db,
|
|
@@ -787,6 +816,7 @@ export async function createApplication(options = {}) {
|
|
|
787
816
|
},
|
|
788
817
|
}
|
|
789
818
|
: {}),
|
|
819
|
+
decisions: (call, announce) => bindDecisions(decisionService, () => config.decisions.telemetry, call, announce),
|
|
790
820
|
artifacts: (call, announce) => createArtifactPublisher(artifacts, {
|
|
791
821
|
sessionId: call.sessionId,
|
|
792
822
|
rootSessionId: store.rootOf(call.sessionId),
|
|
@@ -916,6 +946,8 @@ export async function createApplication(options = {}) {
|
|
|
916
946
|
/** Tabular datasets of every session (ingestion, read-only queries, pages). */
|
|
917
947
|
datasets,
|
|
918
948
|
registry,
|
|
949
|
+
/** Decision Intelligence: the provider catalog, the service and in-memory metrics. */
|
|
950
|
+
decisions: { registry: decisionRegistry, service: decisionService, metrics: decisionMetrics },
|
|
919
951
|
context,
|
|
920
952
|
skills,
|
|
921
953
|
/** Path- and content-safe metadata for the interactive skills manager. */
|
|
@@ -1120,6 +1152,33 @@ export async function createApplication(options = {}) {
|
|
|
1120
1152
|
config.pluginHooks = { ...config.pluginHooks, timeoutMs: Number(value) };
|
|
1121
1153
|
plugins.applyTimeoutSettings({ hookTimeoutMs: Number(value) });
|
|
1122
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
|
+
}
|
|
1123
1182
|
case "tui.paddingX":
|
|
1124
1183
|
config.tui = { ...config.tui, paddingX: Number(value) };
|
|
1125
1184
|
break;
|
|
@@ -1374,7 +1433,11 @@ export async function createApplication(options = {}) {
|
|
|
1374
1433
|
...[...runtimes]
|
|
1375
1434
|
.filter((runtime) => runtime !== provider.currentProvider)
|
|
1376
1435
|
.map((runtime) => Promise.resolve(runtime.dispose?.())),
|
|
1377
|
-
|
|
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()),
|
|
1378
1441
|
]);
|
|
1379
1442
|
})());
|
|
1380
1443
|
}
|
|
@@ -1387,7 +1450,11 @@ export async function createApplication(options = {}) {
|
|
|
1387
1450
|
}
|
|
1388
1451
|
catch (e) {
|
|
1389
1452
|
await Promise.race([
|
|
1390
|
-
Promise.allSettled([
|
|
1453
|
+
Promise.allSettled([
|
|
1454
|
+
herdr.close(),
|
|
1455
|
+
mcp.close(),
|
|
1456
|
+
decisionService.shutdown(config.pluginHooks.disposeTimeoutMs).then(() => plugins.close()),
|
|
1457
|
+
]),
|
|
1391
1458
|
delay(TEARDOWN_STAGE_TIMEOUT_MS),
|
|
1392
1459
|
]);
|
|
1393
1460
|
store.close();
|
package/dist/changelog/data.js
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
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
|
+
},
|
|
5
26
|
{
|
|
6
27
|
version: "0.2.1",
|
|
7
28
|
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 });
|
package/dist/core/contracts.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AnalysisCapability, ArtifactPublisher, ArtifactPublishInput, ArtifactRef, CompactionCheckpoint, InstallPreview, Message, ToolCall, ToolResult } from "@alisio/sdk";
|
|
1
|
+
import type { AnalysisCapability, ArtifactPublisher, ArtifactPublishInput, ArtifactRef, CompactionCheckpoint, InstallPreview, Message, RunEventDataMap, ToolCall, ToolDecisions, ToolResult } from "@alisio/sdk";
|
|
2
2
|
export interface Session {
|
|
3
3
|
id: string;
|
|
4
4
|
workspace: string;
|
|
@@ -275,6 +275,15 @@ export type ArtifactPublisherFactory = (call: {
|
|
|
275
275
|
runId: string;
|
|
276
276
|
callId: string;
|
|
277
277
|
}, announce: (published: PublishedArtifactInfo) => void) => CoreArtifactPublisher | undefined;
|
|
278
|
+
/**
|
|
279
|
+
* Creates the `ToolContext.decisions` of one tool call; `announce` emits the decision event of
|
|
280
|
+
* the run (persisted and fanned out like any other). `undefined` leaves the context without it.
|
|
281
|
+
*/
|
|
282
|
+
export type DecisionsFactory = (call: {
|
|
283
|
+
sessionId: string;
|
|
284
|
+
runId: string;
|
|
285
|
+
callId: string;
|
|
286
|
+
}, announce: <K extends "decision_completed" | "decision_fallback">(type: K, data: RunEventDataMap[K]) => void) => ToolDecisions | undefined;
|
|
278
287
|
/** Optional interactive approval for write/process/external tools not pre-allowed by the Policy. */
|
|
279
288
|
export type ApprovalHandler = (request: ApprovalRequest) => Promise<ApprovalDecision>;
|
|
280
289
|
export interface HookFailure {
|
package/dist/core/runner.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Attachment, type DatasetRef, type Message, type ModelProvider, type RunEvent, type ToolDefinition, type ToolResult } from "@alisio/sdk";
|
|
2
2
|
import type { PathAccess } from "../runtime/access.ts";
|
|
3
|
-
import type { ApprovalHandler, ArtifactPublisherFactory, CapabilityGate, ContextSource, Policy, RunnerExtensions, SessionStore } from "./contracts.ts";
|
|
3
|
+
import type { ApprovalHandler, ArtifactPublisherFactory, CapabilityGate, ContextSource, DecisionsFactory, Policy, RunnerExtensions, SessionStore } from "./contracts.ts";
|
|
4
4
|
import { HumanWaits } from "./human-wait.ts";
|
|
5
5
|
import type { ToolRegistry } from "./registry.ts";
|
|
6
6
|
export interface CompactionSettings {
|
|
@@ -81,6 +81,8 @@ export interface RunnerOptions {
|
|
|
81
81
|
capabilities?: CapabilityGate;
|
|
82
82
|
/** Artifact publisher of each tool call (`ToolContext.artifacts`). */
|
|
83
83
|
artifacts?: ArtifactPublisherFactory;
|
|
84
|
+
/** Decision Intelligence bound to each tool call (`ToolContext.decisions`). */
|
|
85
|
+
decisions?: DecisionsFactory;
|
|
84
86
|
/**
|
|
85
87
|
* Provider-native tool definitions (for example a hosted `web_search` tool) appended to every
|
|
86
88
|
* request's `tools` alongside the registry's function tools. Opt-in, set once for the whole
|
package/dist/core/runner.js
CHANGED
|
@@ -529,6 +529,7 @@ export class AgentRunner {
|
|
|
529
529
|
callId: call.id,
|
|
530
530
|
...(published.executionId ? { executionId: published.executionId } : {}),
|
|
531
531
|
}));
|
|
532
|
+
const decisions = o.decisions?.({ sessionId, runId, callId: call.id }, (type, data) => emit(type, data));
|
|
532
533
|
/**
|
|
533
534
|
* `analysis.install`: always asks, `once` only, and no flag or earlier grant covers it. Where
|
|
534
535
|
* nobody can be asked (headless, no approval handler) the answer is `deny`.
|
|
@@ -590,6 +591,7 @@ export class AgentRunner {
|
|
|
590
591
|
runId,
|
|
591
592
|
callId: call.id,
|
|
592
593
|
...(publisher ? { artifacts: publisher } : {}),
|
|
594
|
+
...(decisions ? { decisions } : {}),
|
|
593
595
|
approveInstall,
|
|
594
596
|
emit: (data) => emit("tool_progress", { id: call.id, data }),
|
|
595
597
|
// Only the built-in plan tool reports its durable events (never a plugin tool).
|