@matatbread/matbot-core 0.2.7 → 0.2.9

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 CHANGED
@@ -4,7 +4,7 @@
4
4
  "node",
5
5
  "browser"
6
6
  ],
7
- "version": "0.2.7",
7
+ "version": "0.2.9",
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.2.7"
38
+ "@matatbread/matbot-plugin-api": "^0.2.9"
39
39
  },
40
40
  "devDependencies": {
41
41
  "typescript": "^6.0.3"
package/src/about.ts ADDED
@@ -0,0 +1,38 @@
1
+ import type { Tool, ToolExecutor, ToolResultOf, ToolContext } from '@matatbread/matbot-plugin-api';
2
+
3
+ declare module '@matatbread/matbot-plugin-api' {
4
+ interface ToolResults {
5
+ about_matbot: { version: string; about: string };
6
+ }
7
+ }
8
+
9
+ const ABOUT = 'Matbot composable LLM harness';
10
+
11
+ /**
12
+ * Reports what the model is running: the harness `version` and a one-line description. The harness is
13
+ * not a plugin (so it has no row in `plugin list`), and its version is a singleton fact rather than a
14
+ * per-plugin one — hence a dedicated tool. The factory lives in core (like `single_turn`) because it is
15
+ * the one cross-runtime home both the node app and the browser bundle register it from, each passing its
16
+ * own app package version.
17
+ */
18
+ export function createAboutMatbotTool(version: string): Tool<ToolResultOf<'about_matbot'>> {
19
+ const executor: ToolExecutor<ToolResultOf<'about_matbot'>> = {
20
+ async *execute(_input: unknown, _ctx: ToolContext) {
21
+ yield { type: 'result', value: { version, about: ABOUT } };
22
+ },
23
+ };
24
+
25
+ return {
26
+ name: 'about_matbot',
27
+ description:
28
+ 'Report what you are running: the matbot harness version and a one-line description. Use it when ' +
29
+ 'asked what version of matbot this is, or for an "about" of the harness itself (distinct from the ' +
30
+ 'plugin tool, which lists loaded plugins).\n\n' +
31
+ 'Parameters (TypeScript):\n' +
32
+ '```ts\n' +
33
+ '{} // -> { version, about }\n' +
34
+ '```',
35
+ inputSchema: { type: 'object', properties: {} },
36
+ executor,
37
+ };
38
+ }
package/src/index.ts CHANGED
@@ -38,8 +38,10 @@ export * from './registry.js';
38
38
  export * from './settings.js';
39
39
  export * from './loader.js';
40
40
  export * from './tool-registry.js';
41
+ export * from './provider-registry.js';
41
42
  export * from './system-context.js';
42
43
  export * from './single-turn.js';
44
+ export * from './about.js';
43
45
  export * from './config/index.js';
44
46
  export * from './security/index.js';
45
47
  export * from './knowledge/index.js';
package/src/loader.ts CHANGED
@@ -212,6 +212,7 @@ export async function loadPlugins(
212
212
  const name = toLoad[i]!.name ?? (services.resolver !== undefined ? await services.resolver.identify(spec) : defaultIdentify(spec));
213
213
  const source = sourceOf(spec);
214
214
  const runtimes = toLoad[i]!.runtimes;
215
+ const version = services.resolver?.version !== undefined ? await services.resolver.version(spec) : undefined;
215
216
  const plugin: MatbotPlugin = {
216
217
  ...spec_obj,
217
218
  name,
@@ -219,6 +220,7 @@ export async function loadPlugins(
219
220
  resolvedUrl: toLoad[i]!.resolvedUrl,
220
221
  ...(source !== undefined ? { source } : {}),
221
222
  ...(runtimes !== undefined ? { matbotRuntime: runtimes } : {}),
223
+ ...(version !== undefined ? { version } : {}),
222
224
  };
223
225
 
224
226
  // registered gates the rollback: only an attempt that *itself* registered the
@@ -0,0 +1,29 @@
1
+ import type { ProviderConfig, ProviderRegistry } from '@matatbread/matbot-plugin-api';
2
+
3
+ /**
4
+ * A `Map` of provider profiles that also satisfies the sanctioned {@link ProviderRegistry} write API.
5
+ * Extending `Map` gives every read-surface consumer (`get`/`has`/`keys`/`values`/iteration) the exact
6
+ * standard-library types, while `register`/`remove`/`revert` are the names plugins and the `provider`
7
+ * tool use to contribute profiles — mirroring `ToolRegistry.register`/`remove`.
8
+ *
9
+ * The entries present at construction are snapshotted as the *boot baseline*: `revert(name)` restores
10
+ * that value (or deletes if there was none), so a plugin that contributed — possibly shadowing — a
11
+ * profile can undo it on unload, returning the set to the host's boot condition.
12
+ */
13
+ export class ProviderRegistryImpl extends Map<string, ProviderConfig> implements ProviderRegistry {
14
+ private readonly bootBaseline: Map<string, ProviderConfig>;
15
+
16
+ constructor(initial?: Iterable<readonly [string, ProviderConfig]>) {
17
+ super(initial);
18
+ this.bootBaseline = new Map(this);
19
+ }
20
+
21
+ register(config: ProviderConfig): void { this.set(config.name, config); }
22
+ remove(name: string): boolean { return this.delete(name); }
23
+
24
+ revert(name: string): void {
25
+ const boot = this.bootBaseline.get(name);
26
+ if (boot !== undefined) this.set(name, boot);
27
+ else this.delete(name);
28
+ }
29
+ }
package/src/registry.ts CHANGED
@@ -1,7 +1,7 @@
1
- import type { Tool, ToolRegistry, Hook, PromptFn, FormField, FrontendInfo, PluginRegistryEvent } from './types.js';
1
+ import type { Tool, ToolRegistry, Hook, PromptFn, FormField, FrontendInfo, PluginRegistryEvent, ProviderAdapter, ProviderConfig } from './types.js';
2
2
  import { createBroadcaster } from '@matatbread/matbot-plugin-api';
3
3
  import type {
4
- MatbotPlugin, MatbotMachine, Mounted,
4
+ MatbotPlugin, MatbotMachine, MatbotRuntime, Mounted,
5
5
  ProviderAdapterFactory, StoreFactory,
6
6
  } from './plugin.js';
7
7
  import { PLUGIN_API_VERSION, unifyServices } from './plugin.js';
@@ -145,17 +145,47 @@ export function registerPlugin(plugin: MatbotPlugin): void {
145
145
 
146
146
  // ── Resolution ────────────────────────────────────────────────────────────────
147
147
 
148
- export function resolveProviderFactory(module: string): ProviderAdapterFactory {
149
- const factory = state.providers.get(module);
150
- if (factory === undefined) {
151
- const available = [...state.providers.keys()].join(', ') || 'none';
152
- throw new Error(
153
- `No provider registered for module "${module}". ` +
154
- `Available: ${available}. ` +
155
- `Install and load the provider plugin.`,
156
- );
148
+ export function tryResolveProviderFactory(module: string): ProviderAdapterFactory | undefined {
149
+ return state.providers.get(module);
150
+ }
151
+
152
+ /**
153
+ * Config → live adapter, loading the adapter module on demand. The fast path resolves an already
154
+ * registered factory. When the factory is absent (the common case now the boot pre-scan is disabled, or
155
+ * a provider profile a storage backend replayed from its medium ahead of its adapter module) the module
156
+ * is force-loaded and the loaded plugin's own factory is used — sidestepping the canonical-name keying the
157
+ * factory registry uses. Returns `null` (and warns, never throws) if the module can't be loaded or carries
158
+ * no adapter, so one unusable profile can't abort a turn.
159
+ */
160
+ export async function instantiateProvider(services: MatbotRuntime, config: ProviderConfig): Promise<ProviderAdapter | null> {
161
+ // Resolve the factory tolerantly. `config.module` may be the canonical plugin name or any specifier that
162
+ // identifies the plugin — the factory registry is keyed by plugin name, so when a raw specifier misses we
163
+ // map it to the loaded plugin's name (a sibling profile sharing this adapter module may have loaded it
164
+ // already). The stored profile is NEVER rewritten: it stays exactly as the source wrote it (yaml path,
165
+ // npm name, or a storage-backend manifest entry), so `provider list` reports the source truth rather than
166
+ // a mix of specifiers and package names that depends on which profiles happen to have been used.
167
+ const name = tryResolveProviderFactory(config.module) !== undefined
168
+ ? config.module
169
+ : getPluginNameForSpecifier(config.module);
170
+ if (name !== undefined) {
171
+ const factory = tryResolveProviderFactory(name);
172
+ if (factory !== undefined) return factory(config);
173
+ }
174
+
175
+ // Not loaded yet — force-load the adapter module on demand (a runtime-contributed profile, or a disabled
176
+ // pre-scan). Warn and return null rather than throw, so one unusable profile can't abort a turn. The next
177
+ // resolution hits the fast path above via specifier→name, so this loads at most once per adapter module.
178
+ try {
179
+ const plugin = await services.loadPlugin(config.module);
180
+ if (plugin.provider === undefined) {
181
+ console.warn(`[matbot] provider "${config.name}": module "${config.module}" loaded but registered no adapter.`);
182
+ return null;
183
+ }
184
+ return plugin.provider(config);
185
+ } catch (err) {
186
+ console.warn(`[matbot] provider "${config.name}": could not load adapter module "${config.module}":`, err);
187
+ return null;
157
188
  }
158
- return factory;
159
189
  }
160
190
 
161
191
  export function getRegisteredTools(): readonly Tool[] {