@alisio/core 0.1.0-alpha.3
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/LICENSE +21 -0
- package/README.md +25 -0
- package/dist/application.d.ts +192 -0
- package/dist/application.js +764 -0
- package/dist/config.d.ts +420 -0
- package/dist/config.js +416 -0
- package/dist/core/compaction.d.ts +48 -0
- package/dist/core/compaction.js +206 -0
- package/dist/core/contracts.d.ts +141 -0
- package/dist/core/contracts.js +1 -0
- package/dist/core/registry.d.ts +8 -0
- package/dist/core/registry.js +54 -0
- package/dist/core/runner.d.ts +121 -0
- package/dist/core/runner.js +543 -0
- package/dist/extensions/registry.d.ts +28 -0
- package/dist/extensions/registry.js +58 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +27 -0
- package/dist/integrations/herdr.d.ts +34 -0
- package/dist/integrations/herdr.js +174 -0
- package/dist/mcp/connector.d.ts +63 -0
- package/dist/mcp/connector.js +384 -0
- package/dist/plugins/host.d.ts +164 -0
- package/dist/plugins/host.js +572 -0
- package/dist/providers/openai-compatible.d.ts +14 -0
- package/dist/providers/openai-compatible.js +228 -0
- package/dist/providers/registry.d.ts +35 -0
- package/dist/providers/registry.js +74 -0
- package/dist/providers/routing.d.ts +11 -0
- package/dist/providers/routing.js +41 -0
- package/dist/providers/settings.d.ts +30 -0
- package/dist/providers/settings.js +80 -0
- package/dist/resources/context.d.ts +39 -0
- package/dist/resources/context.js +164 -0
- package/dist/resources/prompts.d.ts +77 -0
- package/dist/resources/prompts.js +182 -0
- package/dist/resources/skills.d.ts +82 -0
- package/dist/resources/skills.js +214 -0
- package/dist/runtime/fs.d.ts +9 -0
- package/dist/runtime/fs.js +73 -0
- package/dist/runtime/modules.d.ts +14 -0
- package/dist/runtime/modules.js +76 -0
- package/dist/runtime/paths.d.ts +4 -0
- package/dist/runtime/paths.js +42 -0
- package/dist/runtime/process.d.ts +16 -0
- package/dist/runtime/process.js +104 -0
- package/dist/runtime/sqlite.d.ts +3 -0
- package/dist/runtime/sqlite.js +85 -0
- package/dist/runtime/store.d.ts +37 -0
- package/dist/runtime/store.js +250 -0
- package/dist/sessions/children.d.ts +55 -0
- package/dist/sessions/children.js +231 -0
- package/dist/startup/index.d.ts +41 -0
- package/dist/startup/index.js +105 -0
- package/dist/startup/mascot.d.ts +6 -0
- package/dist/startup/mascot.js +30 -0
- package/dist/startup/screen.d.ts +18 -0
- package/dist/startup/screen.js +116 -0
- package/dist/startup/text.d.ts +19 -0
- package/dist/startup/text.js +68 -0
- package/dist/tools/execute.d.ts +16 -0
- package/dist/tools/execute.js +96 -0
- package/dist/tools/search.d.ts +69 -0
- package/dist/tools/search.js +232 -0
- package/dist/tools/standard.d.ts +22 -0
- package/dist/tools/standard.js +408 -0
- package/dist/tools/webfetch.d.ts +26 -0
- package/dist/tools/webfetch.js +119 -0
- package/dist/trust.d.ts +34 -0
- package/dist/trust.js +113 -0
- package/package.json +62 -0
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import { readdir, stat } from "node:fs/promises";
|
|
11
|
+
import { dirname, join, resolve } from "node:path";
|
|
12
|
+
import { pathToFileURL } from "node:url";
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
import { ExtensionRegistry } from "../extensions/registry.js";
|
|
15
|
+
import { readJson } from "../runtime/fs.js";
|
|
16
|
+
import { resolvePluginSpec } from "../runtime/modules.js";
|
|
17
|
+
import { inside } from "../runtime/paths.js";
|
|
18
|
+
import { openDatabase } from "../runtime/sqlite.js";
|
|
19
|
+
export const pluginPrefix = (id) => `p_${createHash("sha256").update(id).digest("hex").slice(0, 10)}`;
|
|
20
|
+
const message = (error) => (error instanceof Error ? error.message : String(error));
|
|
21
|
+
export class PluginHost {
|
|
22
|
+
registry;
|
|
23
|
+
state;
|
|
24
|
+
options;
|
|
25
|
+
providers;
|
|
26
|
+
commands = new Map();
|
|
27
|
+
commandInfo = new Map();
|
|
28
|
+
/** Ids of active built-in plugins. */
|
|
29
|
+
builtins = new Set();
|
|
30
|
+
/** Status entries contributed through `ui.status`, keyed `plugin:key`. */
|
|
31
|
+
status = new Map();
|
|
32
|
+
onStatusChange;
|
|
33
|
+
/** Providers for typed extension points (mascot, startup-screen, ...). */
|
|
34
|
+
extensions = new ExtensionRegistry();
|
|
35
|
+
/** Loaded plugins that did not come from the built-in registry. */
|
|
36
|
+
externalCount = 0;
|
|
37
|
+
compactionHooks = [];
|
|
38
|
+
startHandlers = [];
|
|
39
|
+
endHandlers = [];
|
|
40
|
+
fieldOwners = new Map();
|
|
41
|
+
completer;
|
|
42
|
+
modelsImpl;
|
|
43
|
+
observers = new Set();
|
|
44
|
+
contexts = [];
|
|
45
|
+
/** Skill directories registered by plugins, retaining trusted registration ownership. */
|
|
46
|
+
skillSources = [];
|
|
47
|
+
/** Prompt template directories registered by plugins, with their plugin id. */
|
|
48
|
+
promptSources = [];
|
|
49
|
+
/** Agent definition directories registered by plugins, with their plugin id. */
|
|
50
|
+
agentSources = [];
|
|
51
|
+
/** Tree panels contributed by plugins, keyed `plugin:id`. */
|
|
52
|
+
panels = new Map();
|
|
53
|
+
sessionsImpl;
|
|
54
|
+
uiImpl;
|
|
55
|
+
/** Binds the child session service once the runner exists. */
|
|
56
|
+
setSessions(sessions) {
|
|
57
|
+
this.sessionsImpl = sessions;
|
|
58
|
+
}
|
|
59
|
+
/** Binds interactive UI services (the TUI); without them select/askQuestions resolve undefined. */
|
|
60
|
+
setInteractiveUI(ui) {
|
|
61
|
+
this.uiImpl = ui;
|
|
62
|
+
}
|
|
63
|
+
get sessions() {
|
|
64
|
+
if (!this.sessionsImpl)
|
|
65
|
+
throw new Error("Child sessions are not available yet");
|
|
66
|
+
return this.sessionsImpl;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Live accessor for core standard tools (e.g. `ask_user_question`), which are not "a plugin"
|
|
70
|
+
* with an id and so cannot use the per-plugin `ui` wrapper built in `activate()`. Bound lazily
|
|
71
|
+
* (reads `this.uiImpl` at call time) since `registerStandard` runs before the TUI calls
|
|
72
|
+
* `setInteractiveUI`. `interactive()` reflects whether ANY interactive UI is bound at all,
|
|
73
|
+
* never which session is asking, so child sessions under an interactive root TUI pass too.
|
|
74
|
+
*/
|
|
75
|
+
get ui() {
|
|
76
|
+
return {
|
|
77
|
+
interactive: () => !!this.uiImpl,
|
|
78
|
+
askQuestions: (request) => this.uiImpl
|
|
79
|
+
? this.uiImpl.askQuestions(request)
|
|
80
|
+
: Promise.resolve(Object.fromEntries(request.questions.map((q) => [q.id, undefined]))),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
get promptRoots() {
|
|
84
|
+
return this.promptSources.map((s) => s.dir);
|
|
85
|
+
}
|
|
86
|
+
get skillRoots() {
|
|
87
|
+
return this.skillSources.map((source) => source.dir);
|
|
88
|
+
}
|
|
89
|
+
loaded = new Map();
|
|
90
|
+
constructor(registry, state, options = {}, providers) {
|
|
91
|
+
this.registry = registry;
|
|
92
|
+
this.state = state;
|
|
93
|
+
this.options = options;
|
|
94
|
+
this.providers = providers;
|
|
95
|
+
}
|
|
96
|
+
/** Binds the provider-agnostic completion service once a provider exists. */
|
|
97
|
+
setCompleter(fn) {
|
|
98
|
+
this.completer = fn;
|
|
99
|
+
}
|
|
100
|
+
/** Binds credential-free model discovery/resolution after provider startup is complete. */
|
|
101
|
+
setModels(models) {
|
|
102
|
+
this.modelsImpl = models;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Loads an explicitly trusted plugin: a file/directory path, or an npm package name resolved
|
|
106
|
+
* from `from` (project) upwards and then global roots. Never called for untrusted input.
|
|
107
|
+
*/
|
|
108
|
+
async load(spec, options = {}) {
|
|
109
|
+
const candidate = await this.inspect(spec, options);
|
|
110
|
+
await this.activate(candidate.plugin, candidate.base);
|
|
111
|
+
}
|
|
112
|
+
/** Imports and validates a trusted plugin without running setup, for cataloging disabled entries. */
|
|
113
|
+
async inspect(spec, options = {}) {
|
|
114
|
+
let path = await resolvePluginSpec(spec, {
|
|
115
|
+
from: options.from ?? process.cwd(),
|
|
116
|
+
...(options.globalRoots ? { globalRoots: options.globalRoots } : {}),
|
|
117
|
+
});
|
|
118
|
+
if ((await stat(path)).isDirectory()) {
|
|
119
|
+
const manifest = z
|
|
120
|
+
.object({ entry: z.string(), apiVersion: z.literal(1) })
|
|
121
|
+
.passthrough()
|
|
122
|
+
.parse(await readJson(join(path, "alisio-plugin.json")));
|
|
123
|
+
const entry = resolve(path, manifest.entry);
|
|
124
|
+
if (!inside(path, entry))
|
|
125
|
+
throw new Error("Plugin entry escapes directory");
|
|
126
|
+
path = entry;
|
|
127
|
+
}
|
|
128
|
+
// Caller must explicitly trust before import: module evaluation executes arbitrary code.
|
|
129
|
+
let module;
|
|
130
|
+
try {
|
|
131
|
+
module = await import(__rewriteRelativeImportExtension(pathToFileURL(path).href));
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (error.code === "ERR_UNKNOWN_FILE_EXTENSION")
|
|
135
|
+
throw new Error(`Cannot load ${path}: TypeScript plugins need Bun or Node >=22.18 (type stripping); publish plugins as JavaScript.`);
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
const plugin = module.default;
|
|
139
|
+
this.validate(plugin);
|
|
140
|
+
return { plugin, base: dirname(path) };
|
|
141
|
+
}
|
|
142
|
+
validate(plugin) {
|
|
143
|
+
z.object({
|
|
144
|
+
id: z.string().regex(/^[a-z0-9][a-z0-9.-]{0,63}$/),
|
|
145
|
+
version: z.string().regex(/^\d+\.\d+\.\d+(?:-[\w.-]+)?$/),
|
|
146
|
+
apiVersion: z.literal(1),
|
|
147
|
+
name: z.string().min(1).optional(),
|
|
148
|
+
description: z.string().min(1).optional(),
|
|
149
|
+
categories: z.array(z.literal("model-provider")).optional(),
|
|
150
|
+
setup: z.function(),
|
|
151
|
+
})
|
|
152
|
+
.passthrough()
|
|
153
|
+
.parse(plugin);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* `builtin` is reserved for first-party plugins from the built-in registry: their tools and
|
|
157
|
+
* commands keep unprefixed names and may declare the `internal` effect.
|
|
158
|
+
*/
|
|
159
|
+
async activate(plugin, base, options = {}) {
|
|
160
|
+
const builtin = !!options.builtin;
|
|
161
|
+
this.validate(plugin);
|
|
162
|
+
if (this.loaded.has(plugin.id))
|
|
163
|
+
throw new Error(`Duplicate plugin: ${plugin.id}`);
|
|
164
|
+
const undo = [];
|
|
165
|
+
const statusKeys = new Set();
|
|
166
|
+
const track = (fn) => {
|
|
167
|
+
undo.push(fn);
|
|
168
|
+
return fn;
|
|
169
|
+
};
|
|
170
|
+
const api = {
|
|
171
|
+
tools: {
|
|
172
|
+
register: (tool) => track(this.registry.register(builtin
|
|
173
|
+
? tool
|
|
174
|
+
: {
|
|
175
|
+
...tool,
|
|
176
|
+
name: `${pluginPrefix(plugin.id)}_${tool.name}`,
|
|
177
|
+
// Only built-ins may claim the always-allowed internal effect.
|
|
178
|
+
...(tool.effect === "internal" ? { effect: "external" } : {}),
|
|
179
|
+
})),
|
|
180
|
+
},
|
|
181
|
+
commands: {
|
|
182
|
+
register: (name, handler, info) => {
|
|
183
|
+
const key = builtin ? name : `${plugin.id}:${name}`;
|
|
184
|
+
if (this.commands.has(key))
|
|
185
|
+
throw new Error(`Duplicate command ${key}`);
|
|
186
|
+
this.commands.set(key, handler);
|
|
187
|
+
this.commandInfo.set(key, { ...info, plugin: plugin.id, builtin });
|
|
188
|
+
return track(() => {
|
|
189
|
+
this.commands.delete(key);
|
|
190
|
+
this.commandInfo.delete(key);
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
events: {
|
|
195
|
+
on: (handler) => {
|
|
196
|
+
this.observers.add(handler);
|
|
197
|
+
return track(() => {
|
|
198
|
+
this.observers.delete(handler);
|
|
199
|
+
});
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
context: {
|
|
203
|
+
register: (provider) => {
|
|
204
|
+
this.contexts.push(provider);
|
|
205
|
+
return track(() => {
|
|
206
|
+
const i = this.contexts.indexOf(provider);
|
|
207
|
+
if (i >= 0)
|
|
208
|
+
this.contexts.splice(i, 1);
|
|
209
|
+
});
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
resources: {
|
|
213
|
+
skills: (path) => {
|
|
214
|
+
const entry = {
|
|
215
|
+
plugin: plugin.id,
|
|
216
|
+
name: plugin.name ?? plugin.id,
|
|
217
|
+
dir: resolve(base, path),
|
|
218
|
+
};
|
|
219
|
+
this.skillSources.push(entry);
|
|
220
|
+
track(() => {
|
|
221
|
+
this.skillSources = this.skillSources.filter((x) => x !== entry);
|
|
222
|
+
});
|
|
223
|
+
},
|
|
224
|
+
prompts: (path) => {
|
|
225
|
+
const entry = { plugin: plugin.id, dir: resolve(base, path) };
|
|
226
|
+
this.promptSources.push(entry);
|
|
227
|
+
track(() => {
|
|
228
|
+
this.promptSources = this.promptSources.filter((x) => x !== entry);
|
|
229
|
+
});
|
|
230
|
+
},
|
|
231
|
+
agents: (path) => {
|
|
232
|
+
const entry = { plugin: plugin.id, dir: resolve(base, path) };
|
|
233
|
+
this.agentSources.push(entry);
|
|
234
|
+
track(() => {
|
|
235
|
+
this.agentSources = this.agentSources.filter((x) => x !== entry);
|
|
236
|
+
});
|
|
237
|
+
},
|
|
238
|
+
list: (kind) => (kind === "skills"
|
|
239
|
+
? this.skillSources
|
|
240
|
+
: kind === "prompts"
|
|
241
|
+
? this.promptSources
|
|
242
|
+
: this.agentSources).map((x) => ({ ...x })),
|
|
243
|
+
},
|
|
244
|
+
state: {
|
|
245
|
+
get: (key) => this.state.getState(plugin.id, key),
|
|
246
|
+
set: (key, value) => this.state.setState(plugin.id, key, value),
|
|
247
|
+
},
|
|
248
|
+
storage: {
|
|
249
|
+
sqlite: (file) => {
|
|
250
|
+
const db = openDatabase(file);
|
|
251
|
+
track(() => {
|
|
252
|
+
try {
|
|
253
|
+
db.close();
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
/* Already closed by the plugin. */
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
return db;
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
compaction: {
|
|
263
|
+
register: (hooks) => {
|
|
264
|
+
const entry = { plugin: plugin.id, hooks };
|
|
265
|
+
this.compactionHooks.push(entry);
|
|
266
|
+
return track(() => {
|
|
267
|
+
this.compactionHooks = this.compactionHooks.filter((x) => x !== entry);
|
|
268
|
+
});
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
session: {
|
|
272
|
+
onStart: (handler) => {
|
|
273
|
+
const entry = { plugin: plugin.id, handler };
|
|
274
|
+
this.startHandlers.push(entry);
|
|
275
|
+
return track(() => {
|
|
276
|
+
this.startHandlers = this.startHandlers.filter((x) => x !== entry);
|
|
277
|
+
});
|
|
278
|
+
},
|
|
279
|
+
onEnd: (handler) => {
|
|
280
|
+
const entry = { plugin: plugin.id, handler };
|
|
281
|
+
this.endHandlers.push(entry);
|
|
282
|
+
return track(() => {
|
|
283
|
+
this.endHandlers = this.endHandlers.filter((x) => x !== entry);
|
|
284
|
+
});
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
model: {
|
|
288
|
+
complete: async (request) => {
|
|
289
|
+
if (!this.completer)
|
|
290
|
+
throw new Error("Model completion is not available yet");
|
|
291
|
+
const timeout = AbortSignal.timeout(120_000);
|
|
292
|
+
return this.completer({
|
|
293
|
+
...request,
|
|
294
|
+
signal: request.signal ? AbortSignal.any([request.signal, timeout]) : timeout,
|
|
295
|
+
});
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
models: {
|
|
299
|
+
list: (signal) => {
|
|
300
|
+
if (!this.modelsImpl)
|
|
301
|
+
throw new Error("Model resolution is not available yet");
|
|
302
|
+
return this.modelsImpl.list(signal);
|
|
303
|
+
},
|
|
304
|
+
resolve: (reference, signal) => {
|
|
305
|
+
if (!this.modelsImpl)
|
|
306
|
+
throw new Error("Model resolution is not available yet");
|
|
307
|
+
return this.modelsImpl.resolve(reference, signal);
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
providers: {
|
|
311
|
+
register: (provider) => {
|
|
312
|
+
if (!this.providers)
|
|
313
|
+
throw new Error("Provider registry is not available");
|
|
314
|
+
return track(this.providers.register(provider, { plugin: plugin.id, builtin }));
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
extensions: {
|
|
318
|
+
register: (point, provider, options) => track(this.extensions.register(point, provider, {
|
|
319
|
+
plugin: plugin.id,
|
|
320
|
+
...(options?.priority !== undefined ? { priority: options.priority } : {}),
|
|
321
|
+
})),
|
|
322
|
+
},
|
|
323
|
+
sessions: {
|
|
324
|
+
spawn: (spec) => this.sessions.spawn(spec),
|
|
325
|
+
create: (spec) => this.sessions.create(spec),
|
|
326
|
+
run: (id, prompt, options) => this.sessions.run(id, prompt, options),
|
|
327
|
+
get: (id) => this.sessions.get(id),
|
|
328
|
+
children: (parentId) => this.sessions.children(parentId),
|
|
329
|
+
ancestors: (id) => this.sessions.ancestors(id),
|
|
330
|
+
cancel: (id) => this.sessions.cancel(id),
|
|
331
|
+
enqueue: (id, text) => this.sessions.enqueue(id, text),
|
|
332
|
+
isRunning: (id) => this.sessions.isRunning(id),
|
|
333
|
+
capabilities: (id) => this.sessions.capabilities(id),
|
|
334
|
+
model: (id) => this.sessions.model(id),
|
|
335
|
+
workspace: (id) => this.sessions.workspace(id),
|
|
336
|
+
setStatus: (id, status) => this.sessions.setStatus(id, status),
|
|
337
|
+
},
|
|
338
|
+
ui: {
|
|
339
|
+
panel: (id, provider) => {
|
|
340
|
+
const key = `${plugin.id}:${id}`;
|
|
341
|
+
this.panels.set(key, { plugin: plugin.id, provider });
|
|
342
|
+
return track(() => {
|
|
343
|
+
this.panels.delete(key);
|
|
344
|
+
});
|
|
345
|
+
},
|
|
346
|
+
select: async (request) => (this.uiImpl ? this.uiImpl.select(request) : undefined),
|
|
347
|
+
askQuestions: async (request) => this.uiImpl
|
|
348
|
+
? this.uiImpl.askQuestions(request)
|
|
349
|
+
: Object.fromEntries(request.questions.map((q) => [q.id, undefined])),
|
|
350
|
+
open: (sessionId) => this.uiImpl?.open(sessionId) ?? false,
|
|
351
|
+
interactive: () => !!this.uiImpl,
|
|
352
|
+
status: (key, text, detail) => {
|
|
353
|
+
const id = `${plugin.id}:${key}`;
|
|
354
|
+
if (text === undefined)
|
|
355
|
+
this.status.delete(id);
|
|
356
|
+
else
|
|
357
|
+
this.status.set(id, { plugin: plugin.id, text, ...(detail ? { detail } : {}) });
|
|
358
|
+
if (!statusKeys.has(id)) {
|
|
359
|
+
statusKeys.add(id);
|
|
360
|
+
track(() => {
|
|
361
|
+
this.status.delete(id);
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
try {
|
|
365
|
+
this.onStatusChange?.();
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
/* UI observers cannot break plugins. */
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
try {
|
|
374
|
+
// Declarative sugar: `extensions: { mascot, "startup-screen" }` registers at priority 0.
|
|
375
|
+
for (const [point, provider] of Object.entries(plugin.extensions ?? {}))
|
|
376
|
+
if (provider)
|
|
377
|
+
api.extensions.register(point, provider);
|
|
378
|
+
await plugin.setup(api);
|
|
379
|
+
this.loaded.set(plugin.id, { plugin, undo, builtin });
|
|
380
|
+
if (builtin)
|
|
381
|
+
this.builtins.add(plugin.id);
|
|
382
|
+
else
|
|
383
|
+
this.externalCount++;
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
for (const fn of undo.reverse())
|
|
387
|
+
fn();
|
|
388
|
+
await plugin.dispose?.();
|
|
389
|
+
throw error;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
/** Runs one hook with a host-enforced timeout; the hook receives an abort signal. */
|
|
393
|
+
async guarded(ms, run) {
|
|
394
|
+
const controller = new AbortController();
|
|
395
|
+
let timer;
|
|
396
|
+
const timeout = new Promise((_, reject) => {
|
|
397
|
+
timer = setTimeout(() => {
|
|
398
|
+
const error = new Error(`Timed out after ${ms}ms`);
|
|
399
|
+
controller.abort(error);
|
|
400
|
+
reject(error);
|
|
401
|
+
}, ms);
|
|
402
|
+
});
|
|
403
|
+
try {
|
|
404
|
+
return await Promise.race([run(controller.signal), timeout]);
|
|
405
|
+
}
|
|
406
|
+
finally {
|
|
407
|
+
clearTimeout(timer);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
async beforeCompact(input) {
|
|
411
|
+
const instructions = [];
|
|
412
|
+
const fields = {};
|
|
413
|
+
const failures = [];
|
|
414
|
+
const owners = new Map();
|
|
415
|
+
for (const { plugin, hooks } of this.compactionHooks) {
|
|
416
|
+
if (!hooks.beforeCompact)
|
|
417
|
+
continue;
|
|
418
|
+
try {
|
|
419
|
+
const result = await this.guarded(this.options.hookTimeoutMs ?? 15_000, (signal) => Promise.resolve(hooks.beforeCompact?.({ ...input, signal })));
|
|
420
|
+
if (result?.instructions)
|
|
421
|
+
instructions.push(result.instructions);
|
|
422
|
+
for (const [name, description] of Object.entries(result?.outputFields ?? {})) {
|
|
423
|
+
if (name === "checkpoint" || fields[name] || !/^[a-zA-Z][\w]{0,40}$/.test(name)) {
|
|
424
|
+
failures.push({
|
|
425
|
+
source: plugin,
|
|
426
|
+
hook: "beforeCompact",
|
|
427
|
+
error: `Output field rejected: ${name}`,
|
|
428
|
+
});
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
fields[name] = { source: plugin, description };
|
|
432
|
+
owners.set(name, plugin);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
catch (error) {
|
|
436
|
+
failures.push({ source: plugin, hook: "beforeCompact", error: message(error) });
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
this.fieldOwners.set(input.sessionId, owners);
|
|
440
|
+
return { instructions, fields, failures };
|
|
441
|
+
}
|
|
442
|
+
async afterCompact(input) {
|
|
443
|
+
const inject = [];
|
|
444
|
+
const reports = {};
|
|
445
|
+
const failures = [];
|
|
446
|
+
const owners = this.fieldOwners.get(input.sessionId) ?? new Map();
|
|
447
|
+
this.fieldOwners.delete(input.sessionId);
|
|
448
|
+
for (const { plugin, hooks } of this.compactionHooks) {
|
|
449
|
+
if (!hooks.afterCompact)
|
|
450
|
+
continue;
|
|
451
|
+
const extracted = Object.fromEntries(Object.entries(input.extracted).filter(([name]) => owners.get(name) === plugin));
|
|
452
|
+
try {
|
|
453
|
+
const result = await this.guarded(this.options.hookTimeoutMs ?? 15_000, (signal) => Promise.resolve(hooks.afterCompact?.({ ...input, extracted, signal })));
|
|
454
|
+
if (result?.injectContext?.trim())
|
|
455
|
+
inject.push(result.injectContext);
|
|
456
|
+
if (result?.report)
|
|
457
|
+
reports[plugin] = result.report;
|
|
458
|
+
}
|
|
459
|
+
catch (error) {
|
|
460
|
+
failures.push({ source: plugin, hook: "afterCompact", error: message(error) });
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return { inject, reports, failures };
|
|
464
|
+
}
|
|
465
|
+
async sessionStart(input) {
|
|
466
|
+
const inject = [];
|
|
467
|
+
const failures = [];
|
|
468
|
+
for (const { plugin, handler } of this.startHandlers) {
|
|
469
|
+
try {
|
|
470
|
+
const text = await this.guarded(this.options.hookTimeoutMs ?? 15_000, (signal) => handler({ ...input, reason: "start", messages: [], signal }));
|
|
471
|
+
if (typeof text === "string" && text.trim())
|
|
472
|
+
inject.push({ source: plugin, text });
|
|
473
|
+
}
|
|
474
|
+
catch (error) {
|
|
475
|
+
failures.push({ source: plugin, hook: "sessionStart", error: message(error) });
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return { inject, failures };
|
|
479
|
+
}
|
|
480
|
+
/** Session end hooks run concurrently and are bounded by one host timeout. */
|
|
481
|
+
async sessionEnd(input) {
|
|
482
|
+
const failures = [];
|
|
483
|
+
await Promise.all(this.endHandlers.map(async ({ plugin, handler }) => {
|
|
484
|
+
try {
|
|
485
|
+
await this.guarded(this.options.sessionEndTimeoutMs ?? 10_000, (signal) => handler({ ...input, signal }));
|
|
486
|
+
}
|
|
487
|
+
catch (error) {
|
|
488
|
+
failures.push({ source: plugin, hook: "sessionEnd", error: message(error) });
|
|
489
|
+
}
|
|
490
|
+
}));
|
|
491
|
+
return { failures };
|
|
492
|
+
}
|
|
493
|
+
/** Loaded plugins (id, version, built-in flag), ordered by id. */
|
|
494
|
+
metadata() {
|
|
495
|
+
return [...this.loaded.values()]
|
|
496
|
+
.map(({ plugin, builtin }) => ({
|
|
497
|
+
id: plugin.id,
|
|
498
|
+
version: plugin.version,
|
|
499
|
+
builtin,
|
|
500
|
+
...(plugin.name ? { name: plugin.name } : {}),
|
|
501
|
+
...(plugin.description ? { description: plugin.description } : {}),
|
|
502
|
+
...((plugin.categories?.length ?? 0) > 0 ||
|
|
503
|
+
this.providers?.list().some((provider) => provider.plugin === plugin.id)
|
|
504
|
+
? {
|
|
505
|
+
categories: [
|
|
506
|
+
...new Set([
|
|
507
|
+
...(plugin.categories ?? []),
|
|
508
|
+
...(this.providers?.list().some((provider) => provider.plugin === plugin.id)
|
|
509
|
+
? ["model-provider"]
|
|
510
|
+
: []),
|
|
511
|
+
]),
|
|
512
|
+
],
|
|
513
|
+
}
|
|
514
|
+
: {}),
|
|
515
|
+
}))
|
|
516
|
+
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
517
|
+
}
|
|
518
|
+
get hasSessionEndHooks() {
|
|
519
|
+
return this.endHandlers.length > 0;
|
|
520
|
+
}
|
|
521
|
+
/** Conservative ownership check used to protect a live session from plugin removal. */
|
|
522
|
+
ownsSessionResources(plugin) {
|
|
523
|
+
return (this.startHandlers.some((entry) => entry.plugin === plugin) ||
|
|
524
|
+
this.endHandlers.some((entry) => entry.plugin === plugin) ||
|
|
525
|
+
[...this.panels.values()].some((entry) => entry.plugin === plugin));
|
|
526
|
+
}
|
|
527
|
+
emit(event) {
|
|
528
|
+
for (const observer of this.observers) {
|
|
529
|
+
try {
|
|
530
|
+
observer(Object.freeze(structuredClone(event)));
|
|
531
|
+
}
|
|
532
|
+
catch {
|
|
533
|
+
/* Observers cannot break runs. */
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
async close() {
|
|
538
|
+
const errors = [];
|
|
539
|
+
for (const { plugin, undo } of [...this.loaded.values()].reverse()) {
|
|
540
|
+
try {
|
|
541
|
+
await plugin.dispose?.();
|
|
542
|
+
}
|
|
543
|
+
catch (e) {
|
|
544
|
+
errors.push(e);
|
|
545
|
+
}
|
|
546
|
+
finally {
|
|
547
|
+
for (const fn of undo.reverse())
|
|
548
|
+
fn();
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
this.loaded.clear();
|
|
552
|
+
this.externalCount = 0;
|
|
553
|
+
this.builtins.clear();
|
|
554
|
+
if (errors.length)
|
|
555
|
+
throw new AggregateError(errors, "Plugin disposal failed");
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
export async function discoverPlugins(root) {
|
|
559
|
+
let entries;
|
|
560
|
+
try {
|
|
561
|
+
entries = await readdir(root, { withFileTypes: true });
|
|
562
|
+
}
|
|
563
|
+
catch (e) {
|
|
564
|
+
if (e.code === "ENOENT")
|
|
565
|
+
return [];
|
|
566
|
+
throw e;
|
|
567
|
+
}
|
|
568
|
+
return entries
|
|
569
|
+
.filter((e) => (e.isFile() && /\.[cm]?[jt]s$/.test(e.name)) || e.isDirectory())
|
|
570
|
+
.map((e) => join(root, e.name))
|
|
571
|
+
.sort();
|
|
572
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ModelInfo, ModelProvider, ProviderEvent } from "@alisio/sdk";
|
|
2
|
+
import OpenAI from "openai";
|
|
3
|
+
import type { Config } from "../config.ts";
|
|
4
|
+
export declare class OpenAICompatibleProvider implements ModelProvider {
|
|
5
|
+
private config;
|
|
6
|
+
readonly id: string;
|
|
7
|
+
readonly model: string;
|
|
8
|
+
private client;
|
|
9
|
+
constructor(config: Config["provider"], client?: OpenAI);
|
|
10
|
+
/** Lists models from GET /models. Context window fields are provider extensions. */
|
|
11
|
+
listModels(signal: AbortSignal): Promise<ModelInfo[]>;
|
|
12
|
+
stream(request: Parameters<ModelProvider["stream"]>[0]): AsyncIterable<ProviderEvent>;
|
|
13
|
+
private responses;
|
|
14
|
+
}
|