@matatbread/matbot-core 0.3.7 → 0.3.8
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/package.json +2 -2
- package/src/index.ts +1 -0
- package/src/loader.ts +4 -1
- package/src/registry.ts +14 -14
- package/src/storage-base/query/validate.ts +2 -2
- package/src/tool-registry.ts +18 -12
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"node",
|
|
5
5
|
"browser"
|
|
6
6
|
],
|
|
7
|
-
"version": "0.3.
|
|
7
|
+
"version": "0.3.8",
|
|
8
8
|
"engines": {
|
|
9
9
|
"node": ">=24.0.0"
|
|
10
10
|
},
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"access": "public"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@matatbread/matbot-plugin-api": "^0.3.
|
|
38
|
+
"@matatbread/matbot-plugin-api": "^0.3.8"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"typescript": "^6.0.3"
|
package/src/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ export { unifyServices } from '@matatbread/matbot-plugin-api';
|
|
|
19
19
|
export { forwardingProxy, makeSwappable, singleTurnRequest, createMountTable } from '@matatbread/matbot-plugin-api';
|
|
20
20
|
// Observation-stream primitives (broadcaster + the bare-subscribe wrapper); the host wires `mounted`.
|
|
21
21
|
export { createBroadcaster, subscribable } from '@matatbread/matbot-plugin-api';
|
|
22
|
+
export { createNotifier, scopedNotifier, notifyingStore, ItemChangeKind, RegistryChangeKind } from '@matatbread/matbot-plugin-api';
|
|
22
23
|
export type { Subscribable, Broadcaster } from '@matatbread/matbot-plugin-api';
|
|
23
24
|
// Ambient security principal: interface is a type (carried by `export type *`); these are the values.
|
|
24
25
|
export {
|
package/src/loader.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { MatbotPlugin, MatbotPluginSpec, MatbotMachine, PluginSource, Runtime } from './plugin.js';
|
|
2
2
|
import type { PromptFn } from './types.js';
|
|
3
3
|
import { incompatibleRuntimeError, notAPluginError } from '@matatbread/matbot-plugin-api';
|
|
4
|
-
import { registerPlugin, setupPlugin, unloadPlugin, recordFailedPlugin, clearFailedPlugin } from './registry.js';
|
|
4
|
+
import { registerPlugin, setupPlugin, unloadPlugin, recordFailedPlugin, clearFailedPlugin, announcePluginLoaded } from './registry.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* The runtime this process is executing in. Detected the same way the rejected-import branch below
|
|
@@ -235,6 +235,9 @@ export async function loadPlugins(
|
|
|
235
235
|
try {
|
|
236
236
|
registerPlugin(plugin);
|
|
237
237
|
registered = true;
|
|
238
|
+
// Announced at registration, not after setup: a setup throw rolls back through unloadPlugin, which
|
|
239
|
+
// announces the removal — so an observer sees a matched add/remove pair rather than a bare removal.
|
|
240
|
+
announcePluginLoaded(services, plugin.name);
|
|
238
241
|
await setupPlugin(plugin, services, prompt);
|
|
239
242
|
loaded.push(plugin);
|
|
240
243
|
// Successful (re)load: a fixed plugin drops off the failed list (reload runs this same path).
|
package/src/registry.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Tool, ToolRegistry, Hook, PromptFn, FormField, FrontendInfo,
|
|
2
|
-
import {
|
|
1
|
+
import type { Tool, ToolRegistry, Hook, PromptFn, FormField, FrontendInfo, ProviderAdapter, ProviderConfig } from './types.js';
|
|
2
|
+
import { scopedNotifier, RegistryChangeKind } from '@matatbread/matbot-plugin-api';
|
|
3
3
|
import type {
|
|
4
4
|
MatbotPlugin, MatbotMachine, MatbotRuntime, Mounted,
|
|
5
5
|
ProviderAdapterFactory, StoreFactory,
|
|
@@ -34,14 +34,13 @@ const state = {
|
|
|
34
34
|
overwriteAllTools: undefined as boolean | undefined, // persisted "overwrite on collision, this install" choice, loaded lazily
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
-
//
|
|
38
|
-
// web plugins panel refreshing live when a backend
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
export
|
|
43
|
-
|
|
44
|
-
for await (const { value } of pluginEvents.subscribe(signal)) yield value;
|
|
37
|
+
// Plugin load/unload is announced on the Notifier as a `RegistryChange` with `registry: 'plugins'`, for
|
|
38
|
+
// consumers that key off plugin presence (e.g. the web plugins panel refreshing live when a backend
|
|
39
|
+
// restores a plugin set out of band). It was a module-level broadcaster of its own, which is the same
|
|
40
|
+
// primitive the bus already is. `loaded` is announced by the loader — the only caller of
|
|
41
|
+
// registerPlugin, and the one place holding the machine this registry deliberately doesn't.
|
|
42
|
+
export function announcePluginLoaded(services: MatbotMachine, name: string): void {
|
|
43
|
+
services.Notifier.notify({ kind: RegistryChangeKind, source: 'plugins', registry: 'plugins', name, operation: 'added' });
|
|
45
44
|
}
|
|
46
45
|
|
|
47
46
|
// Settings namespace + key under which the user's "overwrite all colliding tools" choice
|
|
@@ -150,8 +149,6 @@ export function registerPlugin(plugin: MatbotPlugin): void {
|
|
|
150
149
|
for (const [type, factory] of Object.entries(plugin.storage ?? {})) {
|
|
151
150
|
state.storage.set(type, factory);
|
|
152
151
|
}
|
|
153
|
-
|
|
154
|
-
pluginEvents.emit({ type: 'loaded', name: plugin.name });
|
|
155
152
|
}
|
|
156
153
|
|
|
157
154
|
// ── Resolution ────────────────────────────────────────────────────────────────
|
|
@@ -331,13 +328,16 @@ export async function setupPlugin(plugin: MatbotPlugin, services: MatbotMachine,
|
|
|
331
328
|
specifier: plugin.specifier,
|
|
332
329
|
...(plugin.source !== undefined ? { source: plugin.source } : {}),
|
|
333
330
|
},
|
|
331
|
+
// Everything this plugin publishes is attributed to it by default — the notification analogue of
|
|
332
|
+
// stamping `pluginName` on its tools. Reads through the host's swap proxy, so a registered
|
|
333
|
+
// distributed Notifier takes effect for a plugin that captured this in setup().
|
|
334
|
+
get Notifier() { return scopedNotifier(services.Notifier, plugin.name); },
|
|
334
335
|
tools: {
|
|
335
336
|
register: registerTool,
|
|
336
337
|
remove: (name: string) => services.tools.remove(name),
|
|
337
338
|
resolve: (name: string) => services.tools.resolve(name),
|
|
338
339
|
list: () => services.tools.list(),
|
|
339
340
|
removeByPlugin:(name: string) => services.tools.removeByPlugin(name),
|
|
340
|
-
watch: (signal?: AbortSignal) => services.tools.watch(signal),
|
|
341
341
|
},
|
|
342
342
|
hooks: {
|
|
343
343
|
register(hook: Hook) {
|
|
@@ -396,7 +396,7 @@ export async function unloadPlugin(pluginName: string, services: MatbotMachine):
|
|
|
396
396
|
state.frontendPlugins.delete(pluginName);
|
|
397
397
|
|
|
398
398
|
state.plugins.splice(idx, 1);
|
|
399
|
-
|
|
399
|
+
services.Notifier.notify({ kind: RegistryChangeKind, source: 'plugins', registry: 'plugins', name: pluginName, operation: 'removed' });
|
|
400
400
|
await Promise.race([
|
|
401
401
|
plugin.teardown?.(),
|
|
402
402
|
new Promise<void>((_, reject) => setTimeout(() => reject(new Error(`Teardown timeout for plugin ${pluginName}`)), 10000))
|
|
@@ -93,11 +93,11 @@ function validateFilter(f: Filter, ptr: string): void {
|
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
const QUERY_KEYS = new Set(['where', 'sort', 'limit', 'cursor']);
|
|
96
|
+
const QUERY_KEYS = new Set(['where', 'sort', 'limit', 'cursor', 'immutable']);
|
|
97
97
|
|
|
98
98
|
export function validateQuery(q: StoreQuery): void {
|
|
99
99
|
if (q === null || typeof q !== 'object' || Array.isArray(q))
|
|
100
|
-
throw new StoreQueryError('query must be an object with optional keys: where, sort, limit, cursor', '/', 'MALFORMED');
|
|
100
|
+
throw new StoreQueryError('query must be an object with optional keys: where, sort, limit, cursor, immutable', '/', 'MALFORMED');
|
|
101
101
|
|
|
102
102
|
// Reject unknown top-level keys. Without this the envelope is silently permissive: a clause placed
|
|
103
103
|
// under a wrong key (e.g. `filter` instead of `where`, an SQL-ism LLMs reach for) is dropped, the
|
package/src/tool-registry.ts
CHANGED
|
@@ -1,26 +1,37 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import { RegistryChangeKind } from '@matatbread/matbot-plugin-api';
|
|
2
|
+
import type { Tool, ToolRegistry, Notifier } from './types.js';
|
|
3
3
|
|
|
4
4
|
export class ToolRegistryImpl implements ToolRegistry {
|
|
5
5
|
private readonly tools = new Map<string, Tool>();
|
|
6
|
-
|
|
6
|
+
// The host's notifier proxy, injected at boot (both hosts build it before this registry). Held as the
|
|
7
|
+
// proxy, not a resolved impl, so a later register('Notifier', …) takes effect here too. Optional: a
|
|
8
|
+
// registry built without one (tests) simply announces nothing.
|
|
9
|
+
private readonly notifier: Notifier | undefined;
|
|
7
10
|
|
|
8
|
-
constructor(initial?: Iterable<Tool
|
|
11
|
+
constructor(initial?: Iterable<Tool>, notifier?: Notifier) {
|
|
9
12
|
if (initial !== undefined) for (const tool of initial) this.tools.set(tool.name, tool);
|
|
13
|
+
this.notifier = notifier;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
private announce(name: string, operation: 'added' | 'removed', pluginName?: string): void {
|
|
17
|
+
this.notifier?.notify({
|
|
18
|
+
kind: RegistryChangeKind, source: 'tools', registry: 'tools', name, operation,
|
|
19
|
+
...(pluginName !== undefined ? { detail: { pluginName } } : {}),
|
|
20
|
+
});
|
|
10
21
|
}
|
|
11
22
|
|
|
12
23
|
register(tool: Tool): void {
|
|
13
24
|
this.tools.set(tool.name, tool);
|
|
14
|
-
this.
|
|
25
|
+
this.announce(tool.name, 'added', tool.pluginName);
|
|
15
26
|
}
|
|
16
27
|
|
|
17
28
|
remove(name: string): void {
|
|
18
|
-
if (this.tools.delete(name)) this.
|
|
29
|
+
if (this.tools.delete(name)) this.announce(name, 'removed');
|
|
19
30
|
}
|
|
20
31
|
|
|
21
32
|
removeByPlugin(pluginName: string): void {
|
|
22
33
|
for (const [name, tool] of this.tools) {
|
|
23
|
-
if (tool.pluginName === pluginName && this.tools.delete(name)) this.
|
|
34
|
+
if (tool.pluginName === pluginName && this.tools.delete(name)) this.announce(name, 'removed');
|
|
24
35
|
}
|
|
25
36
|
}
|
|
26
37
|
|
|
@@ -31,9 +42,4 @@ export class ToolRegistryImpl implements ToolRegistry {
|
|
|
31
42
|
list(): Tool[] {
|
|
32
43
|
return [...this.tools.values()];
|
|
33
44
|
}
|
|
34
|
-
|
|
35
|
-
async *watch(signal?: AbortSignal): AsyncIterable<ToolRegistryEvent> {
|
|
36
|
-
// Tool CRUD is global — no origin — so unwrap the envelope to the bare event.
|
|
37
|
-
for await (const { value } of this.events.subscribe(signal)) yield value;
|
|
38
|
-
}
|
|
39
45
|
}
|