@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/plugins/host.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import type { AskQuestionsRequest, AskQuestionsResult, CommandContext, CommandOptions, CompletionRequest, Message, PanelProvider, Plugin, PluginAPI, PluginMetadata, RunEvent, SelectRequest, ViewContext } from "@alisio/sdk";
|
|
1
|
+
import type { AskQuestionsRequest, AskQuestionsResult, CommandContext, CommandOptions, CompletionRequest, JsonValue, Message, PanelProvider, Plugin, PluginAPI, PluginMetadata, RunEvent, SelectRequest, ViewContext } from "@alisio/sdk";
|
|
2
2
|
import type { HookFailure, RunnerExtensions } from "../core/contracts.ts";
|
|
3
|
+
import type { HumanWaits } from "../core/human-wait.ts";
|
|
3
4
|
import type { ToolRegistry } from "../core/registry.ts";
|
|
5
|
+
import type { DecisionRegistry } from "../decisions/registry.ts";
|
|
6
|
+
import type { DecisionService } from "../decisions/service.ts";
|
|
4
7
|
import { ExtensionRegistry } from "../extensions/registry.ts";
|
|
5
8
|
import type { ProviderRegistry } from "../providers/registry.ts";
|
|
6
9
|
export declare const pluginPrefix: (id: string) => string;
|
|
@@ -9,6 +12,16 @@ export interface PluginHostOptions {
|
|
|
9
12
|
hookTimeoutMs?: number;
|
|
10
13
|
/** Timeout for session-end hooks (for example an automatic summary). */
|
|
11
14
|
sessionEndTimeoutMs?: number;
|
|
15
|
+
/** Per-plugin limit for `dispose()` on close (default 2000 ms). */
|
|
16
|
+
disposeTimeoutMs?: number;
|
|
17
|
+
}
|
|
18
|
+
/** What the host needs to hand each plugin its `api.paths` and `api.options`. */
|
|
19
|
+
export interface PluginEnvironment {
|
|
20
|
+
/** Root of the application state (the one analysis and artifacts use). */
|
|
21
|
+
stateRoot: string;
|
|
22
|
+
configHome: string;
|
|
23
|
+
/** `pluginOverrides[id].options` (external) or `builtinPlugins.<id>` without `enabled`. */
|
|
24
|
+
options(id: string, builtin: boolean): Record<string, JsonValue>;
|
|
12
25
|
}
|
|
13
26
|
/** A failed data view: the host maps `code` to an API error (never echoing parameter values). */
|
|
14
27
|
export declare class ViewRunError extends Error {
|
|
@@ -83,7 +96,15 @@ export declare class PluginHost implements RunnerExtensions {
|
|
|
83
96
|
private uiImpl?;
|
|
84
97
|
/** Binds the child session service once the runner exists. */
|
|
85
98
|
setSessions(sessions: PluginAPI["sessions"]): void;
|
|
86
|
-
/**
|
|
99
|
+
/** Where waits for a person are opened (see `HumanWaits`); bound once the runner exists. */
|
|
100
|
+
private humanWaits?;
|
|
101
|
+
setHumanWaits(waits: HumanWaits): void;
|
|
102
|
+
/**
|
|
103
|
+
* Binds interactive UI services (the TUI, the web server); without them select/askQuestions
|
|
104
|
+
* resolve undefined. Every question that reaches the person through here (plugins, the
|
|
105
|
+
* `ask_user_question` tool, the plan review) is a human wait: the bound UI is wrapped ONCE so no
|
|
106
|
+
* host and no caller can forget to mark it.
|
|
107
|
+
*/
|
|
87
108
|
setInteractiveUI(ui: {
|
|
88
109
|
select(request: SelectRequest): Promise<string | undefined>;
|
|
89
110
|
askQuestions(request: AskQuestionsRequest): Promise<AskQuestionsResult>;
|
|
@@ -123,6 +144,21 @@ export declare class PluginHost implements RunnerExtensions {
|
|
|
123
144
|
setCompleter(fn: (request: CompletionRequest & {
|
|
124
145
|
signal: AbortSignal;
|
|
125
146
|
}) => Promise<string>): void;
|
|
147
|
+
/**
|
|
148
|
+
* Binds Decision Intelligence. Plugins activated afterwards see `api.decisions`; without it the
|
|
149
|
+
* member is absent, exactly like on a core that predates the feature.
|
|
150
|
+
*/
|
|
151
|
+
setDecisions(decisions: {
|
|
152
|
+
registry: DecisionRegistry;
|
|
153
|
+
service: DecisionService;
|
|
154
|
+
}): void;
|
|
155
|
+
private decisionsImpl?;
|
|
156
|
+
/**
|
|
157
|
+
* Binds the directories and options plugins receive (`api.paths`, `api.options`). Plugins
|
|
158
|
+
* activated before this call, or on a host without it, see neither member.
|
|
159
|
+
*/
|
|
160
|
+
setPluginEnvironment(environment: PluginEnvironment): void;
|
|
161
|
+
private environment?;
|
|
126
162
|
/** Binds credential-free model discovery/resolution after provider startup is complete. */
|
|
127
163
|
setModels(models: PluginAPI["models"]): void;
|
|
128
164
|
/**
|
|
@@ -197,6 +233,11 @@ export declare class PluginHost implements RunnerExtensions {
|
|
|
197
233
|
/** Conservative ownership check used to protect a live session from plugin removal. */
|
|
198
234
|
ownsSessionResources(plugin: string): boolean;
|
|
199
235
|
emit(event: RunEvent): void;
|
|
236
|
+
/**
|
|
237
|
+
* Disposes every plugin in parallel, each bounded by `disposeTimeoutMs`: a slow or hung
|
|
238
|
+
* `dispose()` neither delays the others nor holds the close past its limit. Registrations are
|
|
239
|
+
* undone either way. Failures (including a timeout) are reported together afterwards.
|
|
240
|
+
*/
|
|
200
241
|
close(): Promise<void>;
|
|
201
242
|
}
|
|
202
243
|
export declare function discoverPlugins(root: string): Promise<string[]>;
|
package/dist/plugins/host.js
CHANGED
|
@@ -7,17 +7,26 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
7
7
|
return path;
|
|
8
8
|
};
|
|
9
9
|
import { createHash } from "node:crypto";
|
|
10
|
+
import { mkdirSync } from "node:fs";
|
|
10
11
|
import { readdir, stat } from "node:fs/promises";
|
|
11
12
|
import { dirname, join, resolve } from "node:path";
|
|
12
13
|
import { pathToFileURL } from "node:url";
|
|
13
14
|
import Ajv from "ajv";
|
|
14
15
|
import { z } from "zod";
|
|
15
16
|
import { ExtensionRegistry } from "../extensions/registry.js";
|
|
16
|
-
import { readJson } from "../runtime/fs.js";
|
|
17
|
+
import { exists, readJson } from "../runtime/fs.js";
|
|
17
18
|
import { resolvePluginSpec } from "../runtime/modules.js";
|
|
18
19
|
import { inside } from "../runtime/paths.js";
|
|
19
20
|
import { openDatabase } from "../runtime/sqlite.js";
|
|
20
21
|
export const pluginPrefix = (id) => `p_${createHash("sha256").update(id).digest("hex").slice(0, 10)}`;
|
|
22
|
+
const deepFreeze = (value) => {
|
|
23
|
+
if (value && typeof value === "object" && !Object.isFrozen(value)) {
|
|
24
|
+
Object.freeze(value);
|
|
25
|
+
for (const child of Object.values(value))
|
|
26
|
+
deepFreeze(child);
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
};
|
|
21
30
|
const VIEW_ID = /^[a-z][a-z0-9-]{0,39}$/;
|
|
22
31
|
const VIEW_PARAM_TYPES = new Set(["string", "integer", "number", "boolean"]);
|
|
23
32
|
/** Query strings arrive as text: coerce them to the declared primitives and apply defaults. */
|
|
@@ -85,9 +94,24 @@ export class PluginHost {
|
|
|
85
94
|
setSessions(sessions) {
|
|
86
95
|
this.sessionsImpl = sessions;
|
|
87
96
|
}
|
|
88
|
-
/**
|
|
97
|
+
/** Where waits for a person are opened (see `HumanWaits`); bound once the runner exists. */
|
|
98
|
+
humanWaits;
|
|
99
|
+
setHumanWaits(waits) {
|
|
100
|
+
this.humanWaits = waits;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Binds interactive UI services (the TUI, the web server); without them select/askQuestions
|
|
104
|
+
* resolve undefined. Every question that reaches the person through here (plugins, the
|
|
105
|
+
* `ask_user_question` tool, the plan review) is a human wait: the bound UI is wrapped ONCE so no
|
|
106
|
+
* host and no caller can forget to mark it.
|
|
107
|
+
*/
|
|
89
108
|
setInteractiveUI(ui) {
|
|
90
|
-
this.
|
|
109
|
+
const wait = (session, ask) => this.humanWaits ? this.humanWaits.track(session, ask) : ask();
|
|
110
|
+
this.uiImpl = {
|
|
111
|
+
select: (request) => wait(undefined, () => ui.select(request)),
|
|
112
|
+
askQuestions: (request) => wait(request.session, () => ui.askQuestions(request)),
|
|
113
|
+
open: (sessionId) => ui.open(sessionId),
|
|
114
|
+
};
|
|
91
115
|
}
|
|
92
116
|
/**
|
|
93
117
|
* Read-only view of one plugin's shared state (written through `api.state.set`). The host layer
|
|
@@ -141,6 +165,22 @@ export class PluginHost {
|
|
|
141
165
|
setCompleter(fn) {
|
|
142
166
|
this.completer = fn;
|
|
143
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Binds Decision Intelligence. Plugins activated afterwards see `api.decisions`; without it the
|
|
170
|
+
* member is absent, exactly like on a core that predates the feature.
|
|
171
|
+
*/
|
|
172
|
+
setDecisions(decisions) {
|
|
173
|
+
this.decisionsImpl = decisions;
|
|
174
|
+
}
|
|
175
|
+
decisionsImpl;
|
|
176
|
+
/**
|
|
177
|
+
* Binds the directories and options plugins receive (`api.paths`, `api.options`). Plugins
|
|
178
|
+
* activated before this call, or on a host without it, see neither member.
|
|
179
|
+
*/
|
|
180
|
+
setPluginEnvironment(environment) {
|
|
181
|
+
this.environment = environment;
|
|
182
|
+
}
|
|
183
|
+
environment;
|
|
144
184
|
/** Binds credential-free model discovery/resolution after provider startup is complete. */
|
|
145
185
|
setModels(models) {
|
|
146
186
|
this.modelsImpl = models;
|
|
@@ -203,6 +243,7 @@ export class PluginHost {
|
|
|
203
243
|
"mcp",
|
|
204
244
|
"storage",
|
|
205
245
|
"ui",
|
|
246
|
+
"decisions",
|
|
206
247
|
]))
|
|
207
248
|
.optional(),
|
|
208
249
|
setup: z.function(),
|
|
@@ -361,6 +402,42 @@ export class PluginHost {
|
|
|
361
402
|
});
|
|
362
403
|
},
|
|
363
404
|
},
|
|
405
|
+
...(this.environment
|
|
406
|
+
? (({ stateRoot, configHome, options }) => {
|
|
407
|
+
// The id is validated (`^[a-z0-9][a-z0-9.-]{0,63}$`): it cannot be `.`/`..` or hold a
|
|
408
|
+
// separator, so these stay inside their roots. Created 0700 on first read.
|
|
409
|
+
const state = join(stateRoot, "plugins", plugin.id);
|
|
410
|
+
const directories = {
|
|
411
|
+
state,
|
|
412
|
+
config: join(configHome, "plugins", plugin.id),
|
|
413
|
+
cache: join(state, "cache"),
|
|
414
|
+
};
|
|
415
|
+
const paths = {};
|
|
416
|
+
for (const key of ["state", "config", "cache"])
|
|
417
|
+
Object.defineProperty(paths, key, {
|
|
418
|
+
enumerable: true,
|
|
419
|
+
get: () => {
|
|
420
|
+
mkdirSync(directories[key], { recursive: true, mode: 0o700 });
|
|
421
|
+
return directories[key];
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
return {
|
|
425
|
+
paths: Object.freeze(paths),
|
|
426
|
+
options: deepFreeze(structuredClone(options(plugin.id, builtin))),
|
|
427
|
+
};
|
|
428
|
+
})(this.environment)
|
|
429
|
+
: {}),
|
|
430
|
+
...(this.decisionsImpl
|
|
431
|
+
? {
|
|
432
|
+
decisions: (({ registry, service }) => ({
|
|
433
|
+
// Registering never activates: `decisions.provider` in the configuration does.
|
|
434
|
+
registerProvider: (provider) => track(registry.register(plugin.id, provider)),
|
|
435
|
+
available: () => service.available(),
|
|
436
|
+
activeProvider: () => service.activeProvider(),
|
|
437
|
+
tryDecide: (request, options) => service.tryDecide(request, options),
|
|
438
|
+
}))(this.decisionsImpl),
|
|
439
|
+
}
|
|
440
|
+
: {}),
|
|
364
441
|
compaction: {
|
|
365
442
|
register: (hooks) => {
|
|
366
443
|
const entry = { plugin: plugin.id, hooks };
|
|
@@ -675,23 +752,34 @@ export class PluginHost {
|
|
|
675
752
|
}
|
|
676
753
|
}
|
|
677
754
|
}
|
|
755
|
+
/**
|
|
756
|
+
* Disposes every plugin in parallel, each bounded by `disposeTimeoutMs`: a slow or hung
|
|
757
|
+
* `dispose()` neither delays the others nor holds the close past its limit. Registrations are
|
|
758
|
+
* undone either way. Failures (including a timeout) are reported together afterwards.
|
|
759
|
+
*/
|
|
678
760
|
async close() {
|
|
679
|
-
const
|
|
680
|
-
|
|
761
|
+
const limit = this.options.disposeTimeoutMs ?? 2000;
|
|
762
|
+
const entries = [...this.loaded.entries()].reverse();
|
|
763
|
+
this.loaded.clear();
|
|
764
|
+
this.externalCount = 0;
|
|
765
|
+
this.builtins.clear();
|
|
766
|
+
const results = await Promise.allSettled(entries.map(async ([id, { plugin, undo }]) => {
|
|
767
|
+
let timer;
|
|
681
768
|
try {
|
|
682
|
-
await
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
769
|
+
await Promise.race([
|
|
770
|
+
Promise.resolve().then(() => plugin.dispose?.()),
|
|
771
|
+
new Promise((_, reject) => {
|
|
772
|
+
timer = setTimeout(() => reject(new Error(`Plugin ${id} dispose timed out after ${limit}ms`)), limit);
|
|
773
|
+
}),
|
|
774
|
+
]);
|
|
686
775
|
}
|
|
687
776
|
finally {
|
|
777
|
+
clearTimeout(timer);
|
|
688
778
|
for (const fn of undo.reverse())
|
|
689
779
|
fn();
|
|
690
780
|
}
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
this.externalCount = 0;
|
|
694
|
-
this.builtins.clear();
|
|
781
|
+
}));
|
|
782
|
+
const errors = results.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
|
|
695
783
|
if (errors.length)
|
|
696
784
|
throw new AggregateError(errors, "Plugin disposal failed");
|
|
697
785
|
}
|
|
@@ -706,7 +794,7 @@ export async function discoverPlugins(root) {
|
|
|
706
794
|
return [];
|
|
707
795
|
throw e;
|
|
708
796
|
}
|
|
709
|
-
|
|
797
|
+
const found = await Promise.all(entries
|
|
710
798
|
.filter(
|
|
711
799
|
// A root may hold an `npm install --prefix` layout (`node_modules`, package.json/lock);
|
|
712
800
|
// npm packages there are resolved through the configured `plugins` array instead.
|
|
@@ -714,6 +802,13 @@ export async function discoverPlugins(root) {
|
|
|
714
802
|
e.name !== "package.json" &&
|
|
715
803
|
e.name !== "package-lock.json" &&
|
|
716
804
|
((e.isFile() && /\.[cm]?[jt]s$/.test(e.name)) || e.isDirectory()))
|
|
717
|
-
.map((e) =>
|
|
718
|
-
.
|
|
805
|
+
.map(async (e) => {
|
|
806
|
+
const path = join(root, e.name);
|
|
807
|
+
// A directory is a plugin only with its manifest: `<config home>/plugins/<id>` is also
|
|
808
|
+
// where `api.paths.config` puts a plugin's own files, and those are not plugins.
|
|
809
|
+
if (e.isDirectory() && !(await exists(join(path, "alisio-plugin.json"))))
|
|
810
|
+
return undefined;
|
|
811
|
+
return path;
|
|
812
|
+
}));
|
|
813
|
+
return found.filter((path) => path !== undefined).sort();
|
|
719
814
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alisio/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Embeddable provider-agnostic Alisio agent core: tool loop, context compaction, local tools, MCP, plugin host and SQLite session store. Runs on Node >=22.16 and Bun.",
|
|
5
5
|
"author": "Gustavo Gutiérrez",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"node": ">=22.16"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@alisio/sdk": "0.
|
|
44
|
+
"@alisio/sdk": "0.3.0",
|
|
45
45
|
"@mixmark-io/domino": "2.2.0",
|
|
46
46
|
"@modelcontextprotocol/client": "2.1.0",
|
|
47
47
|
"ajv": "8.20.0",
|