@matatbread/matbot-plugin-api 0.3.0 → 0.3.2

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.3.0",
7
+ "version": "0.3.2",
8
8
  "engines": {
9
9
  "node": ">=24.0.0"
10
10
  },
@@ -1,5 +1,5 @@
1
1
  import type { MatbotMachine } from './plugin.js';
2
- import type { ToolContext, ToolEvent, ToolResultFor, PromptFn, FormField } from './types.js';
2
+ import type { ToolContext, ToolEvent, ToolResultFor, ToolProxy, PromptFn, FormField } from './types.js';
3
3
 
4
4
  /**
5
5
  * Inputs to {@link invokeTool} that a one-shot caller can't derive from the machine. A tool
@@ -98,3 +98,55 @@ const rejectingPrompt: PromptFn = (((p: string | FormField): Promise<string> =>
98
98
  const label = typeof p === 'string' ? p : p.label;
99
99
  return Promise.reject(new Error(`Non-interactive context: cannot prompt for "${label}"`));
100
100
  }) as PromptFn);
101
+
102
+ /** Compact a value to one trace line. */
103
+ function compactTrace(v: unknown): string {
104
+ let s: string;
105
+ if (typeof v === 'string') s = v;
106
+ else { try { s = JSON.stringify(v) ?? String(v); } catch { s = String(v); } }
107
+ return s.length > 240 ? `${s.slice(0, 240)}…` : s;
108
+ }
109
+
110
+ /**
111
+ * A factory for a context-overridden {@link ToolProxy}: `toolInContext({ provider }).some_tool(params)`
112
+ * runs `some_tool` with the given fields merged over the box's bound context (omitted fields inherited).
113
+ * Keyed on `ToolContracts`, so a hallucinated tool name is a **compile error**. This is the explicit
114
+ * escape hatch beside the default `tool` proxy — reach for it only when a call needs a different provider,
115
+ * signal, prompt or session.
116
+ */
117
+ export type ToolBox = (call?: Partial<InvokeToolOptions>) => ToolProxy;
118
+
119
+ /**
120
+ * Build the typed calling surface both `function-tools` compositions and compiled skills use:
121
+ *
122
+ * const { tool, toolInContext } = makeToolBox(machine, callContext, { onEvent });
123
+ * await tool.some_tool(params); // bound context; a bad name is a compile error
124
+ * await toolInContext({ provider }).some_tool(...); // override one field for this call
125
+ *
126
+ * `tool` is a {@link ToolProxy} bound to `callContext`; `toolInContext(override)` returns a fresh proxy
127
+ * with `override` merged over it (never mutating `tool`). `onEvent`, when supplied, receives a `stdout`
128
+ * trace per call (→ name / ← result) so a run is observable; omit it for a silent surface. Built on
129
+ * {@link invokeTool}/{@link toolResult}, which remain the low-level, dynamic-name seam.
130
+ */
131
+ export function makeToolBox(
132
+ machine: MatbotMachine,
133
+ defaultCall: InvokeToolOptions,
134
+ opts?: { onEvent?: (event: ToolEvent) => void },
135
+ ): { tool: ToolProxy; toolInContext: ToolBox } {
136
+ const build = (call: InvokeToolOptions): ToolProxy =>
137
+ new Proxy({}, {
138
+ get(_target, prop) {
139
+ // A symbol key, or `then`, must not read as a callable tool — otherwise the proxy looks like a
140
+ // thenable and awaiting/returning it would try to invoke a tool named `then`.
141
+ if (typeof prop !== 'string' || prop === 'then') return undefined;
142
+ return async (params?: unknown): Promise<unknown> => {
143
+ opts?.onEvent?.({ type: 'stdout', chunk: `→ ${prop}(${compactTrace(params)})\n` });
144
+ const value = await toolResult(invokeTool(machine, prop, params ?? {}, call));
145
+ opts?.onEvent?.({ type: 'stdout', chunk: `← ${prop}: ${compactTrace(value)}\n` });
146
+ return value;
147
+ };
148
+ },
149
+ }) as ToolProxy;
150
+ const toolInContext: ToolBox = (override) => build(override === undefined ? defaultCall : { ...defaultCall, ...override });
151
+ return { tool: build(defaultCall), toolInContext };
152
+ }
package/src/plugin.ts CHANGED
@@ -2,7 +2,7 @@ import type {
2
2
  FileStore, Vault, Message, ModelParameters,
3
3
  ProviderAdapter, ProviderConfig, ProviderRegistry, Tool, ToolRegistry, FrontendInfo,
4
4
  Store, Session, SystemContextRegistry, KnowledgeIndex, PromptFn, SessionRunner, Usage,
5
- TypeScriptStripper, ToolTypeIndex,
5
+ TypeScriptStripper, ToolTypeIndex, ToolPresenter,
6
6
  } from './types.js';
7
7
  import type { HookRegistry } from './hooks.js';
8
8
 
@@ -101,6 +101,11 @@ export interface MatbotServices {
101
101
  * reference held across a swap keeps resolving to the live backend. Always present (boot default). */
102
102
  readonly Vault: Vault;
103
103
  readonly KnowledgeIndex: KnowledgeIndex;
104
+ /** Optional tool-presentation policy: chooses which tools are advertised to the model per provider
105
+ * call (a large-library search/deferral plugin registers one). Absent ⇒ the runner advertises the
106
+ * whole turn snapshot. A plain registered service (not a swap-member); consumed as a member. See
107
+ * {@link ToolPresenter}. */
108
+ readonly ToolPresenter?: ToolPresenter | undefined;
104
109
  }
105
110
 
106
111
  /** The assembled machine: registry services wired to the fixed runtime — what `setup()` receives. */
package/src/types.ts CHANGED
@@ -590,6 +590,9 @@ type ArmCallSig<E> =
590
590
  ? (unknown extends P ? (params?: unknown) => Promise<R> : (params: P) => Promise<R>)
591
591
  : never;
592
592
 
593
+ // The union of a tool's arm params — the parameter type of the trailing catch-all signature below.
594
+ type ArmArgsUnion<E> = E extends ToolContract<unknown, infer A> ? A : never;
595
+
593
596
  /**
594
597
  * The type of the injected `tool` proxy that `function-tools` and compiled skills call
595
598
  * (`await tool.name(params)`). Each key is derived from its {@link ToolContracts} entry: a multi-arm
@@ -599,8 +602,17 @@ type ArmCallSig<E> =
599
602
  * generated `dts()` declares `tool` as; it is *derived* — never hand-authored — from the same `ToolContracts`
600
603
  * augmentation that types authors' `invokeTool`/`toolResult` calls, so the human editor and the LLM see one
601
604
  * source of truth.
605
+ *
606
+ * The trailing catch-all signature (params union → result union) exists for soundness at the *meta-type*
607
+ * level, not for call sites: overload resolution is first-match, so a well-formed call still hits its
608
+ * specific arm and narrows precisely, and a malformed call matches nothing and still errors. What it
609
+ * changes: `ReturnType<typeof tool.x>` — which on a bare overload set resolves to an arbitrary (last) arm,
610
+ * a real-world codegen trap — now degrades to the sound full union; and a params value typed as a union of
611
+ * arms (dynamic multi-action dispatch) becomes callable, returning the result union.
602
612
  */
603
- export type ToolProxy = { [K in keyof ToolContracts]: UnionToIntersection<ArmCallSig<ToolContracts[K]>> };
613
+ export type ToolProxy = { [K in keyof ToolContracts]:
614
+ UnionToIntersection<ArmCallSig<ToolContracts[K]>>
615
+ & ((params: ArmArgsUnion<ToolContracts[K]>) => Promise<ToolResultUnion<ToolContracts[K]>>) };
604
616
 
605
617
  /**
606
618
  * Ask the user a question and resolve with their answer. The host supplies the
@@ -802,6 +814,32 @@ export interface ToolRegistry {
802
814
  watch(signal?: AbortSignal): AsyncIterable<ToolRegistryEvent>;
803
815
  }
804
816
 
817
+ /** What the runner tells a {@link ToolPresenter} about the call it's about to make. */
818
+ export interface PresentContext {
819
+ /** The session as of this provider call — the persisted user turn plus any assistant/tool turns
820
+ * taken so far this turn. A presenter ranks the tool set against this. */
821
+ session: Session;
822
+ /** The provider profile name this turn runs under, so a presenter can tailor to provider
823
+ * capabilities (e.g. Anthropic's in-context tool_reference expansion). */
824
+ provider: string;
825
+ }
826
+
827
+ /**
828
+ * Chooses which tools are advertised to the model for a single provider call — a read-only *view* over
829
+ * the turn's tool snapshot, NOT tool management. Presentation is orthogonal to the {@link ToolRegistry},
830
+ * which stays the single source of registration, resolution, and type-contracts (its fragile, load-bearing
831
+ * machinery is never a swap surface). The runner consults the presenter before *every* provider call, so
832
+ * an impl may present a subset (deferring a large library behind a search tool) and grow it mid-turn as the
833
+ * model discovers tools. Executor resolution is unaffected — a tool withheld here stays callable by name
834
+ * through the live registry — so presentation only ever changes what the model is *told about*.
835
+ *
836
+ * Optional {@link MatbotServices} member (offer-loosely): absent ⇒ the runner advertises the whole
837
+ * snapshot, byte-identical to today. `present` may return synchronously or async (a reranker).
838
+ */
839
+ export interface ToolPresenter {
840
+ present(tools: readonly Tool[], ctx: PresentContext): readonly Tool[] | Promise<readonly Tool[]>;
841
+ }
842
+
805
843
  /**
806
844
  * The named provider profiles the runtime resolves by name. A `ReadonlyMap` for the many read-only
807
845
  * consumers (`services.providers.get`/`has`/`keys`/…), plus a sanctioned write path so a plugin can