@matatbread/matbot-core 0.2.7 → 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/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "node",
5
5
  "browser"
6
6
  ],
7
- "version": "0.2.7",
7
+ "version": "0.3.0",
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.3.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "typescript": "^6.0.3"
package/src/about.ts ADDED
@@ -0,0 +1,34 @@
1
+ import type { Tool, ToolExecutor, ToolContract, ToolResultOf, ToolContext } from '@matatbread/matbot-plugin-api';
2
+
3
+ declare module '@matatbread/matbot-plugin-api' {
4
+ interface ToolContracts {
5
+ about_matbot: ToolContract<{ version: string; about: string }, Record<string, never>>;
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).',
31
+ inputSchema: { type: 'object', properties: {} },
32
+ executor,
33
+ };
34
+ }
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[] {
@@ -5,6 +5,7 @@ import type {
5
5
  } from './types.js';
6
6
  import type { MatbotPlugin } from './plugin.js';
7
7
  import type { HookRegistry } from './hooks.js';
8
+ import type { ToolTypeIndex } from '@matatbread/matbot-plugin-api';
8
9
  import { appendMessage, createMessage } from './session.js';
9
10
  import { contextSwitch, withUsageScope } from '@matatbread/matbot-plugin-api';
10
11
  import { runSession } from './runner.js';
@@ -13,6 +14,11 @@ export interface SessionRunnerDeps {
13
14
  store: Store<Session>;
14
15
  resolveProvider: (name: string) => Promise<{ adapter: ProviderAdapter; config: ProviderConfig } | null>;
15
16
  tools?: ToolRegistry;
17
+ // Resolved live (the service registers after boot): supplies the per-tool wire contract derived from each
18
+ // tool's single contract source (the `ToolContracts` arms, or a source-less tool's `toolContract`), folded
19
+ // into the turn-start tool snapshot's description so the wire carries params/result text. Absent (e.g.
20
+ // browser) ⇒ tools keep their plain description.
21
+ toolTypeIndex?: () => ToolTypeIndex | undefined;
16
22
  hooks?: HookRegistry;
17
23
  systemContext?: SystemContextRegistry;
18
24
  vault?: Vault;
@@ -207,8 +213,19 @@ export function createSessionRunner(deps: SessionRunnerDeps): SessionRunner {
207
213
 
208
214
  const ac = new AbortController();
209
215
  s.ac = ac;
216
+ // Fold each tool's wire contract — the `params`/`result` text flattened from its single contract
217
+ // (a source tool's ToolContracts arms, or a source-less tool's `toolContract`) — into its description
218
+ // for this turn, so the model reasons about the real TS shapes, not just the loose inputSchema.
219
+ // Derived here because `services`/ToolTypeIndex are live at dispatch. Tools with only a loose
220
+ // inputSchema (no contract) are left as-is; absent ToolTypeIndex (browser) ⇒ plain descriptions.
221
+ const wire = await deps.toolTypeIndex?.()?.wireContracts();
210
222
  const toolMap = deps.tools !== undefined
211
- ? new Map<string, Tool>(deps.tools.list().map(t => [t.name, t]))
223
+ ? new Map<string, Tool>(deps.tools.list().map(t => {
224
+ const wc = wire?.[t.name];
225
+ return [t.name, wc !== undefined
226
+ ? { ...t, description: `${t.description}\n\nTypeScript:\n params: ${wc.params}\n result: ${wc.result}` }
227
+ : t];
228
+ }))
212
229
  : undefined;
213
230
 
214
231
  try {
@@ -1,8 +1,8 @@
1
- import type { Tool, ToolExecutor, ToolResultOf, ToolContext, MatbotMachine } from '@matatbread/matbot-plugin-api';
1
+ import type { Tool, ToolExecutor, ToolContract, ToolResultOf, ToolContext, MatbotMachine } from '@matatbread/matbot-plugin-api';
2
2
 
3
3
  declare module '@matatbread/matbot-plugin-api' {
4
- interface ToolResults {
5
- single_turn: { text: string }; // the consulted provider's reply text
4
+ interface ToolContracts {
5
+ single_turn: ToolContract<{ text: string }, { provider?: string; prompt: string; system?: string }>; // the consulted provider's reply text
6
6
  }
7
7
  }
8
8
 
@@ -53,11 +53,7 @@ export function createSingleTurnTool(services: MatbotMachine): Tool<ToolResultOf
53
53
  'back its text. Use it to consult a different model — e.g. a second, ' +
54
54
  'different-lineage model critiquing your draft, or any generation that should run on a specific ' +
55
55
  'provider. `provider` is OPTIONAL: omit it to run on the current conversation\'s model, or name ' +
56
- 'a configured provider to switch models (list or add providers with the provider tool).\n\n' +
57
- 'Parameters (TypeScript):\n' +
58
- '```ts\n' +
59
- '{ provider?: string; prompt: string; system?: string } // -> { text }\n' +
60
- '```',
56
+ 'a configured provider to switch models (list or add providers with the provider tool).',
61
57
  inputSchema: {
62
58
  type: 'object',
63
59
  required: ['prompt'],