@matatbread/matbot-plugin-api 0.3.7 → 0.4.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.
@@ -0,0 +1,69 @@
1
+ import type { PipelineEvent } from './events.js';
2
+ import type { MessageContent, Session } from './messages.js';
3
+ import type { Principal } from './principal.js';
4
+ import type { SteeringMode } from './steering.js';
5
+ import type { PromptFn } from './tools.js';
6
+
7
+ // ── Session runner ──────────────────────────────────────────────────────────────
8
+
9
+ /**
10
+ * A view onto a session returned by `SessionRunner.open`. `session` is the authoritative
11
+ * server-side state — committed messages plus an overlay of any queued-but-not-yet-run
12
+ * submissions (each carrying `metadata.pending: true`). `events` is a lazy, per-session live
13
+ * tap: accessing it subscribes to the turn event stream from now (replaying the in-flight
14
+ * turn, if any); never touching it costs nothing.
15
+ */
16
+ export interface SessionView {
17
+ session: Session;
18
+ /** Submissions waiting behind the current turn (does not count the running turn). */
19
+ queued: number;
20
+ /** Correlation id of the submission this call enqueued — present only when content was supplied. */
21
+ traceId?: string;
22
+ readonly events: AsyncIterable<PipelineEvent>;
23
+ }
24
+
25
+ /** Observe a session without submitting anything. */
26
+ export interface OpenOpts {
27
+ sessionId: string;
28
+ signal: AbortSignal;
29
+ /** Optional caller-supplied correlation id; one is generated when absent. */
30
+ traceId?: string;
31
+ }
32
+
33
+ /** Observe a session AND enqueue a submission. The compiler enforces provider/principal here;
34
+ * a remote frontend deserializing a request body must still validate the wire input itself. */
35
+ export interface SubmitOpenOpts extends OpenOpts {
36
+ content: MessageContent[];
37
+ provider: string;
38
+ principal: Principal;
39
+ /** When true, this submission may be merged with others drained in the same batch. Default false
40
+ * (queue mode: one turn per submission). */
41
+ concatQueue?: boolean;
42
+ /** Disposition for a submission arriving while a turn is running (see {@link SteeringPolicy}):
43
+ * 'queue' waits for the turn boundary (default), 'interrupt' stops the running turn — keeping its
44
+ * committed partial work — and runs this next, 'auto' defers to the registered SteeringPolicy (else
45
+ * the host default). Meaningless when nothing is running (degrades to a plain enqueue). */
46
+ mode?: SteeringMode;
47
+ /** Interactive prompt implementation for this submission's turn. The frontend owns delivery —
48
+ * it must target the frontend's per-session client connections, not a single request. */
49
+ prompt?: PromptFn;
50
+ }
51
+
52
+ /**
53
+ * Serialises turns per session. A submission never executes concurrently with another for the
54
+ * same session; the in-memory queue (lost on process restart, by design) absorbs anything that
55
+ * arrives mid-turn. The server is the source of truth: a frontend renders whatever `open()`
56
+ * returns and treats the live `events` stream purely as an optimisation.
57
+ */
58
+ export interface SessionRunner {
59
+ open(opts: OpenOpts | SubmitOpenOpts): Promise<SessionView>;
60
+ /** Abort the running turn (if any) and drop all queued submissions, emitting `cancelled` for each. */
61
+ abort(sessionId: string): void;
62
+ /** Abandon the running turn (if any) WITHOUT touching the queue — `pump` advances to the next
63
+ * queued submission, or idles. The "give up on this turn" path (a prompt cancel); contrast
64
+ * `abort`, which also clears the queue. A no-op if nothing is running. */
65
+ cancelTurn(sessionId: string): void;
66
+ /** Snapshot of a session's live state: whether a turn is running and how many submissions wait
67
+ * behind it. `busy` is `running || queued > 0`. */
68
+ status(sessionId: string): { busy: boolean; running: boolean; queued: number };
69
+ }
@@ -0,0 +1,33 @@
1
+ import type { MessageContent, Session } from './messages.js';
2
+
3
+ // ── Steering ────────────────────────────────────────────────────────────────────
4
+
5
+ /** Disposition of a submission that arrives while a turn is running. `interrupt` stops the running
6
+ * turn (its committed partial work is preserved) and runs the new message next; `queue` waits for the
7
+ * turn boundary. */
8
+ export type SteeringDecision = 'queue' | 'interrupt';
9
+ /** The wire-level request (see `SubmitOpenOpts.mode`). `auto` defers the decision to the registered
10
+ * {@link SteeringPolicy} (else the host default). */
11
+ export type SteeringMode = SteeringDecision | 'auto';
12
+
13
+ /**
14
+ * How the runner disposes of a mid-turn submission under `mode: 'auto'`, and how an interrupt's
15
+ * continuation is nudged. An optional, registerable service ({@link MatbotServices}); absent ⇒ the
16
+ * runner uses its own defaults. Both members are optional so a plugin may override one, the other, or
17
+ * both. `classify` is deliberately NOT assumed to be an LLM — a regex or a semantic classifier is a
18
+ * first-class implementation (return synchronously); an LLM `singleTurn` returns a promise.
19
+ */
20
+ export interface SteeringPolicy {
21
+ /**
22
+ * Consulted only for `mode: 'auto'` while a turn is running. `session` is the COMMITTED session
23
+ * (history up to the running turn's start — the in-flight partial is not reachable at submit time);
24
+ * `steer` is the incoming submission. Return synchronously (regex) or async (semantic / singleTurn).
25
+ * Absent ⇒ the host default disposition.
26
+ */
27
+ classify?(ctx: { session: Session; steer: MessageContent[] }): SteeringDecision | Promise<SteeringDecision>;
28
+ /**
29
+ * The ephemeral "keep going, noting the above" context folded onto an interrupt's continuation turn
30
+ * (never persisted). Absent ⇒ the host default nudge.
31
+ */
32
+ nudge?(ctx: { session: Session; steer: MessageContent[] }): MessageContent[];
33
+ }
@@ -0,0 +1,16 @@
1
+ import type { QueryResult, StoreQuery } from '../store-query.js';
2
+
3
+ // ── Storage ───────────────────────────────────────────────────────────────────
4
+ // The query grammar (Filter AST, StoreQuery, QueryResult, StoreQueryError) lives in ./store-query.
5
+
6
+ export type CASResult<T> =
7
+ | { ok: true; doc: T }
8
+ | { ok: false; current: T | null };
9
+
10
+ export interface Store<T extends { id: string; version: string }> {
11
+ get(id: string): Promise<T | null>;
12
+ set(id: string, value: T): Promise<void>;
13
+ cas(id: string, expected: string, next: T): Promise<CASResult<T>>;
14
+ delete(id: string, expectedVersion?: string): Promise<boolean>;
15
+ query(q: StoreQuery): Promise<QueryResult<T>>;
16
+ }
@@ -0,0 +1,15 @@
1
+ import type { Session } from './messages.js';
2
+
3
+ // ── System context ────────────────────────────────────────────────────────────
4
+
5
+ export type SystemContextContributor = (ctx: {
6
+ session: Session;
7
+ signal: AbortSignal;
8
+ }) => string | null | Promise<string | null>;
9
+
10
+ export interface SystemContextRegistry {
11
+ register(contributor: SystemContextContributor, pluginName?: string): void;
12
+ removeByPlugin(pluginName: string): void;
13
+ /** Calls all contributors and joins non-null, non-empty results with double newlines. */
14
+ build(ctx: { session: Session; signal: AbortSignal }): Promise<string | null>;
15
+ }
@@ -0,0 +1,262 @@
1
+ import type { FileHandle, FileStore } from './files.js';
2
+ import type { FormField, MessageContent, Session } from './messages.js';
3
+ import type { MatbotPlugin } from '../plugin.js';
4
+ import type { JSONSchema } from './primitives.js';
5
+ import type { Vault } from './vault.js';
6
+
7
+ // ── Tools ─────────────────────────────────────────────────────────────────────
8
+
9
+ /**
10
+ * Media a tool hands to the model to *look at*, as opposed to the JSON it reads as a `result`. The
11
+ * inline arms of {@link MessageContent} — bytes plus a mime type, carrying their own meaning with no
12
+ * dependency on anything that could resolve a reference. Where the tool got the bytes (a `FileStore`,
13
+ * an HTTP fetch, a chart rendered in memory) is entirely the tool's business; the harness never asks.
14
+ */
15
+ export type ModelContent = Extract<MessageContent, { type: 'image' | 'document' | 'audio' }>;
16
+
17
+ export type ToolEvent<Result = unknown> =
18
+ | { type: 'stdout'; chunk: string }
19
+ | { type: 'stderr'; chunk: string }
20
+ | { type: 'progress'; pct: number; message?: string }
21
+ | { type: 'result'; value: Result }
22
+ | { type: 'file'; handle: FileHandle }
23
+ // Media for the model's eyes, pinned to this tool call's result and carried on the wire for the
24
+ // REST OF THIS TURN only — never persisted. Independent of `result`: the durable record of what the
25
+ // tool did is its `result`, and a transcript that re-reads clean is the point. A later turn that
26
+ // needs the bytes again calls the tool again, which is the same pull the model already performed.
27
+ //
28
+ // Rest-of-turn rather than next-call-only because withdrawing content the model has already seen
29
+ // mid-history breaks the prompt cache from that point and leaves it referring to something no longer
30
+ // there. The corollary is cost: a large document is re-sent on every subsequent round of the turn,
31
+ // so a tool should hand over the smallest thing that answers the question (a page range, a thumbnail)
32
+ // and `maxRounds` is the ceiling on how often it is paid for.
33
+ | { type: 'model-content'; content: ModelContent[] }
34
+ // A durable, LLM-invisible annotation the tool emits as it runs (a link, a status, a trace of a
35
+ // side-effect). Persisted as a `marker`-role message; elided from provider submission like any
36
+ // marker. Independent of `result` — a tool may emit markers and no result (a silent side-effect,
37
+ // e.g. a trigger-fired tool), a result and no markers, or both.
38
+ | { type: 'marker'; creator: string; data: unknown }
39
+ | { type: 'error'; message: string; code?: number; stdout?: string; stderr?: string };
40
+
41
+ /**
42
+ * Per-tool call-contract registry: maps a tool's `name` to a {@link ToolContract} arm (or a union of
43
+ * them) pairing the `value` it yields in its `result` event with the params that produce it. Augment it
44
+ * exactly like {@link MarkerData} / `MatbotServices`, so a caller gets the concrete result type back
45
+ * from {@link invokeTool} + `toolResult` instead of `unknown`:
46
+ *
47
+ * declare module '@matatbread/matbot-plugin-api' {
48
+ * interface ToolContracts {
49
+ * find_fact: ToolContract<string[] | null, { question: string; terms: { term: string }[] }>;
50
+ * }
51
+ * }
52
+ *
53
+ * The tool name is the discriminator: `invokeTool(machine, 'find_fact', …)` is typed
54
+ * `AsyncIterable<ToolEvent<string[] | null>>`, so `toolResult(…)` resolves to `string[] | null`. A
55
+ * name with no registered entry resolves to `unknown` (the caller must narrow). This is a pure
56
+ * type-level construct — no runtime validation; it pins, at the call site, the contract a tool's
57
+ * executor already produces. Keep the entry in sync with what the executor actually yields.
58
+ *
59
+ * Even a single-action tool declares an **arm**, never a bare `name: Result`. The bare form still
60
+ * type-checks — `ToolResultOf`/`ToolResultFor` keep a plain-entry path so an unregistered or foreign
61
+ * tool stays loose — but it carries no params, so {@link ToolProxy} derives no call signature from it
62
+ * and `await tool.find_fact(…)` is uncallable.
63
+ *
64
+ * **Multi-action tools** are a weird form of overloaded function: the same tool returns different
65
+ * shapes depending on its params. Register such a tool as a union of {@link ToolContract} *arms*, each
66
+ * pairing a result with the discriminating params *pattern* that selects it — `invokeTool` matches the
67
+ * call's literal params against the patterns and narrows the result to the matching arm:
68
+ *
69
+ * declare module '@matatbread/matbot-plugin-api' {
70
+ * interface ToolContracts {
71
+ * session_action:
72
+ * | ToolContract<Session, { action: 'get' }>
73
+ * | ToolContract<{ id: string; title: string }, { action: 'rename' }>;
74
+ * }
75
+ * }
76
+ * // invokeTool(machine, 'session_action', { action: 'get', sessionId }) → result is Session
77
+ *
78
+ * The pattern is *any* discriminating field(s), not just `action` (a tool keying on `interval`'s
79
+ * presence registers `ToolContract<…, { interval: string }>`). It is a *pattern*, not the full input:
80
+ * key only on the discriminant so a call carrying just that field still matches. When no arm's pattern
81
+ * matches (a non-literal discriminant, or an absence-discriminant the positive patterns can't express),
82
+ * the result falls back to the union of every arm — always sound, just less narrow.
83
+ *
84
+ * One of matbot's five open-registry augmentation points — same technique at each; see
85
+ * docs/DEVELOPING.md *Open-registry augmentation* for the shared shape and the rules that follow from it.
86
+ */
87
+ export interface ToolContracts {}
88
+
89
+ // Phantom carriers for ToolContract's two parameters. `unique symbol` keys rather than `__result`/`__args`
90
+ // so the type is *uninhabitable*: a plugin author cannot name these keys, so no value of ToolContract can
91
+ // be written down by accident (or on purpose, to smuggle a shape past the checker) — where the previous
92
+ // `__`-prefixed properties were merely conventionally-private and constructible with a couple of casts.
93
+ // A cast is the one hole through which a hallucinated result shape survives to runtime, which is why the
94
+ // generated-code checker closes it deterministically; this closes the same hole in the type itself.
95
+ declare const RESULT: unique symbol;
96
+ declare const ARGS: unique symbol;
97
+
98
+ /**
99
+ * One overload arm of a multi-action tool registered in {@link ToolContracts}: the `Result` it yields
100
+ * for a call whose params match the discriminating pattern `Args`. `Args` defaults to `unknown` (an
101
+ * arm that matches any params). Purely type-level — the fields below are phantom and uninstantiable.
102
+ */
103
+ export interface ToolContract<Result, Args = unknown> {
104
+ readonly [RESULT]: Result;
105
+ readonly [ARGS]: Args;
106
+ }
107
+
108
+ type ToolResultArmed<E> = E extends ToolContract<unknown, unknown> ? true : false;
109
+ type ToolResultUnion<E> = E extends ToolContract<infer R, unknown> ? R : never;
110
+ type ToolResultMatched<E, P> = E extends ToolContract<infer R, infer A> ? (P extends A ? R : never) : never;
111
+
112
+ /** The full set of `result` values a tool named `K` may yield — the union over all its arms (for an
113
+ * arm-based entry), the entry itself (for a plain entry), or `unknown` when `K` is unregistered. This
114
+ * is what an executor binds against (`ToolExecutor<ToolResultOf<'my_tool'>>`): it must cover every arm. */
115
+ export type ToolResultOf<K extends string> =
116
+ K extends keyof ToolContracts
117
+ ? ToolResultArmed<ToolContracts[K]> extends true ? ToolResultUnion<ToolContracts[K]> : ToolContracts[K]
118
+ : unknown;
119
+
120
+ /** The `result` a call to tool `K` with params `P` yields: for an arm-based entry, the arm whose
121
+ * pattern `P` matches (or the full union when none match — always sound); otherwise {@link ToolResultOf}. */
122
+ export type ToolResultFor<K extends string, P> =
123
+ K extends keyof ToolContracts
124
+ ? ToolResultArmed<ToolContracts[K]> extends true
125
+ ? ([ToolResultMatched<ToolContracts[K], P>] extends [never]
126
+ ? ToolResultUnion<ToolContracts[K]>
127
+ : ToolResultMatched<ToolContracts[K], P>)
128
+ : ToolContracts[K]
129
+ : unknown;
130
+
131
+ // Distribute a union to its intersection: `A | B` → `A & B`. An intersection of function types is an
132
+ // overload set, so this turns a union of per-arm call signatures into an overloaded function.
133
+ type UnionToIntersection<U> =
134
+ (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
135
+
136
+ // One `ToolContracts` arm → its call signature. A fully-untyped arm (`ToolContract<unknown, unknown>`, the
137
+ // fallback for a foreign/MCP tool carrying only a JSON-Schema `inputSchema`) keeps a loose, optional-param
138
+ // signature so it stays callable; a typed arm requires its declared params.
139
+ type ArmCallSig<E> =
140
+ E extends ToolContract<infer R, infer P>
141
+ ? (unknown extends P ? (params?: unknown) => Promise<R> : (params: P) => Promise<R>)
142
+ : never;
143
+
144
+ // The union of a tool's arm params — the parameter type of the trailing catch-all signature below.
145
+ type ArmArgsUnion<E> = E extends ToolContract<unknown, infer A> ? A : never;
146
+
147
+ /**
148
+ * The type of the injected `tool` proxy that `function-tools` and compiled skills call
149
+ * (`await tool.name(params)`). Each key is derived from its {@link ToolContracts} entry: a multi-arm
150
+ * (multi-action) entry becomes an **overload set** — one `(params) => Promise<result>` signature per arm —
151
+ * so `await tool.name(params)` narrows its result by the params passed, exactly the contract a code
152
+ * generator needs to write a correct call first time. A single-arm entry is one signature. This is what the
153
+ * generated `dts()` declares `tool` as; it is *derived* — never hand-authored — from the same `ToolContracts`
154
+ * augmentation that types authors' `invokeTool`/`toolResult` calls, so the human editor and the LLM see one
155
+ * source of truth.
156
+ *
157
+ * The trailing catch-all signature (params union → result union) exists for soundness at the *meta-type*
158
+ * level, not for call sites: overload resolution is first-match, so a well-formed call still hits its
159
+ * specific arm and narrows precisely, and a malformed call matches nothing and still errors. What it
160
+ * changes: `ReturnType<typeof tool.x>` — which on a bare overload set resolves to an arbitrary (last) arm,
161
+ * a real-world codegen trap — now degrades to the sound full union; and a params value typed as a union of
162
+ * arms (dynamic multi-action dispatch) becomes callable, returning the result union.
163
+ */
164
+ export type ToolProxy = { [K in keyof ToolContracts]:
165
+ UnionToIntersection<ArmCallSig<ToolContracts[K]>>
166
+ & ((params: ArmArgsUnion<ToolContracts[K]>) => Promise<ToolResultUnion<ToolContracts[K]>>) };
167
+
168
+ /**
169
+ * Ask the user a question and resolve with their answer. The host supplies the
170
+ * implementation — readline in the CLI, an SSE round-trip in the web frontend. This is
171
+ * matbot's single mechanism for eliciting user input at runtime; it is injected both into
172
+ * tool execution (`ToolContext.prompt`) and into plugin loading (collision resolution in setup).
173
+ *
174
+ * Two call forms:
175
+ * - `(question, defaultValue?)` — free text; resolves to the typed string (or the default).
176
+ * - `(field: FormField)` — a single structured field (`select`/`confirm`/`password`/`text`),
177
+ * letting rich frontends render real controls (buttons, masked input). Frontends that can't
178
+ * render it fall back to `field.label` as plain text. Resolves to the chosen/typed value.
179
+ *
180
+ * Cancellation is the "we can't proceed — give up" path (distinct from a graceful "decline", which
181
+ * is simply one of the offered `options`). The host rejects the promise with `PromptCancelledError`
182
+ * and abandons the current turn, returning to idle; a caller's surrounding try/catch turns the
183
+ * rejection into a tool error that closes the tool call cleanly.
184
+ * This reuses `FormField` rather than a parallel type; it is a one-shot request/response and
185
+ * deliberately does NOT engage the session-bound `form`/`form-response` flow.
186
+ */
187
+ export interface PromptFn {
188
+ (question: string, defaultValue?: string): Promise<string>;
189
+ (field: FormField): Promise<string>;
190
+ }
191
+
192
+ export interface ToolContext {
193
+ callId: string;
194
+ session: Session;
195
+ signal: AbortSignal;
196
+ vault: Vault;
197
+ /** The provider key driving the current turn (`RunConfig.provider`). A tool that spawns further
198
+ * work should default to this so the child inherits the same model rather than the config default. */
199
+ provider?: string;
200
+ workdir?: string;
201
+ configPath?: string;
202
+ files?: FileStore;
203
+ /** Prompt the user for input. The host provides a readline or form implementation. */
204
+ prompt: PromptFn;
205
+ /** Hot-load a plugin by specifier without restarting the process. Returns the loaded plugin.
206
+ * `refresh` (default false) forces a remote (github/http) plugin to be re-downloaded rather than
207
+ * re-loaded from its `.plugins/` cache — pass true to pick up changed upstream source. */
208
+ loadPlugin(specifier: string, refresh?: boolean): Promise<MatbotPlugin>;
209
+ /**
210
+ * Hot-unload a plugin by specifier, removing its tools, hooks, and system context contributions.
211
+ * Resolves `true` if a plugin was actually resident and unloaded, `false` if there was nothing
212
+ * to unload. A failed `teardown()` (e.g. timeout) still throws — the plugin was resident in that case.
213
+ */
214
+ unloadPlugin(specifier: string): Promise<boolean>;
215
+ }
216
+
217
+ /**
218
+ * The read-only facts about the call a composed function is running under, injected into a
219
+ * `tool_function` body as the ambient `context` binding beside `tool`/`toolInContext`. Deliberately
220
+ * narrower than {@link ToolContext}: the identity of the call, not its capabilities — `vault`,
221
+ * `files`, `prompt` and plugin (un)loading stay off a surface authored by a model and reachable only
222
+ * through the tool contracts. A tool with real source takes `ToolContext` and needs none of this.
223
+ *
224
+ * Not `ToolCallContext` — that name belongs to the `toolcall` hook's context; this is the composed
225
+ * caller's own view, not a hook's view of a call about to run.
226
+ */
227
+ export interface ComposedCallContext {
228
+ readonly callId: string;
229
+ readonly sessionId: string;
230
+ /** The provider key driving the turn — the same one nested `tool.x(…)` calls inherit. */
231
+ readonly provider?: string;
232
+ readonly workdir?: string;
233
+ readonly signal: AbortSignal;
234
+ }
235
+
236
+ /**
237
+ * A tool's runtime. `R` is the type of the `value` carried by its `result` event — declared once,
238
+ * at the source, so the executor's yields and the tool's {@link ToolContracts} registry entry can't
239
+ * silently drift. The `unknown` default keeps untyped executors compiling untouched, and covariance
240
+ * means a narrower `ToolExecutor<X>` still satisfies `ToolExecutor` (i.e. `ToolExecutor<unknown>`),
241
+ * so the heterogeneous registry boundary (`Tool[]`) accepts any executor. Bind `R` to the registry
242
+ * entry with `ToolExecutor<ToolResultOf<'my_tool'>>` so the single augmentation is the source of truth.
243
+ */
244
+ export interface ToolExecutor<R = unknown> {
245
+ execute(input: unknown, ctx: ToolContext): AsyncIterable<ToolEvent<R>>;
246
+ }
247
+
248
+ export interface Tool<R = unknown> {
249
+ name: string;
250
+ description: string;
251
+ inputSchema: JSONSchema;
252
+ /** The call contract as TypeScript source — a {@link ToolContract}`<Result, Params>` (or a union of arms
253
+ * for a multi-action tool), *identical in shape* to what a `ToolContracts` augmentation declares. Only a
254
+ * tool with **no scannable source** carries this — a `function-tools` function (whose signature supplies
255
+ * both halves), and any similarly runtime-defined tool: the host splices it verbatim into the generated
256
+ * dts's `ToolContracts`, and derives the wire `params`/`result` text from it. A tool WITH source declares
257
+ * its contract as a `ToolContracts` augmentation instead and omits this. Foreign tools (e.g. MCP proxies)
258
+ * that have neither fall back to the loose `inputSchema`. */
259
+ toolContract?: string;
260
+ executor: ToolExecutor<R>;
261
+ pluginName?: string;
262
+ }
@@ -0,0 +1,50 @@
1
+ // ── TypeScript ──────────────────────────────────────────────────────────────
2
+
3
+ /**
4
+ * Erases TypeScript types from a source string, returning runnable JavaScript. Type-stripping is
5
+ * foundational — the harness ships raw `.ts` and any code compiled at runtime must have its types
6
+ * removed before it can execute — so every execution environment provides one, and it lives as a fixed
7
+ * runtime capability (on {@link MatbotRuntime}) the host supplies per platform, not a swappable service:
8
+ * node uses its built-in `stripTypeScriptTypes`, the browser bundle uses sucrase. A consumer that
9
+ * compiles source at runtime (e.g. `tool_function`) calls this instead of importing a platform stripper.
10
+ *
11
+ * `strip` may be async: a platform whose stripper loads lazily (the browser fetches sucrase on first use)
12
+ * returns a promise; a synchronous stripper (node) returns the string directly — callers `await` either.
13
+ * It is type-erasure, not full transpilation: node's stripper is erasable-only (enums/namespaces throw),
14
+ * sucrase is more permissive, so authors should keep to erasable TypeScript to stay portable.
15
+ */
16
+ export interface TypeScriptStripper {
17
+ strip(source: string): string | Promise<string>;
18
+ }
19
+
20
+ /**
21
+ * Optional, node-only developer-experience service: the live `.d.ts` of the types the loaded tools
22
+ * expose — what `toolResult(invokeTool(…, name, …))`, or a `function-tools` `await tool.name(…)`, resolves
23
+ * to — derived by compiling each loaded plugin's `declare module '@matatbread/matbot-plugin-api'`
24
+ * augmentations. The runtime registry can't supply this (result types are erased); only a TypeScript
25
+ * program reading the source can. Consumers that generate or compose tool-calling code use it so the model
26
+ * isn't guessing return shapes. Absent where no TypeScript program can run (the browser today) — a consumer
27
+ * must degrade (guess-and-run) when `services.ToolTypeIndex` is undefined.
28
+ *
29
+ * The result is rebuilt lazily and cached, invalidated when the tool set changes. Tools the source scan
30
+ * can't reach (a `function-tools` function) contribute their types by declaring a `toolContract` string on
31
+ * their registered {@link Tool} — identical in shape to a `ToolContracts` arm; the index splices it off the
32
+ * live registry, so no separate registration step is needed.
33
+ */
34
+ export interface ToolTypeIndex {
35
+ /** Self-contained type declarations as a `.d.ts` string: the source-derived `ToolContracts` augmentations
36
+ * merged with the arms of every other live tool, plus `declare const tool: ToolProxy` — the overloaded
37
+ * proxy a generator writes `await tool.x(params)` against. */
38
+ dts(): Promise<string>;
39
+ /** Type-check a TypeScript `snippet` against exactly the {@link dts} above — the `tool` proxy ({@link
40
+ * ToolProxy}: each multi-action tool an overload set, so `await tool.x(params)` narrows its result by the
41
+ * params) is in scope. Returns human-readable diagnostics scoped to the snippet ([] means clean). A
42
+ * composer uses it to catch bad tool-call code before running/registering it. */
43
+ check(snippet: string): Promise<string[]>;
44
+ /** Per live tool (that has a contract): the `params`/`result` wire text, flattened from the one contract —
45
+ * a source tool's `ToolContracts` arms (via the source scan) or a source-less tool's `toolContract` string.
46
+ * The single contract is thus also the source of the wire description; the host folds this into the
47
+ * outgoing tool descriptions at the turn's dispatch edge. A tool with only a loose `inputSchema` (no
48
+ * contract) is absent. */
49
+ wireContracts(): Promise<Record<string, { params: string; result: string }>>;
50
+ }
@@ -0,0 +1,37 @@
1
+ // ── Vault ─────────────────────────────────────────────────────────────────────
2
+
3
+ /**
4
+ * The primitives a vault backend must implement. Resolution, redaction, and the three
5
+ * low-level store operations the smart `createSecret` policy composes. A backend implements
6
+ * `Vault` (which is `VaultSpec` plus that policy); plugins are handed a `Vault`.
7
+ */
8
+ export interface VaultSpec {
9
+ /** Resolve ${NAME} placeholders by looking up the named value; throws MissingSecretError for any miss. */
10
+ resolve(ref: string): Promise<string>;
11
+ scrub(text: string): string;
12
+ /**
13
+ * Store `value` under exactly `name`, overwriting. The literal write; no reference/dedup logic.
14
+ * An empty `value` removes the key instead — there is no such thing as an empty secret, so the
15
+ * empty string is the removal signal (idempotent: removing an absent name is a no-op).
16
+ */
17
+ writeSecret(name: string, value: string): Promise<void>;
18
+ /** Whether a secret is stored under this exact name. */
19
+ hasKey(name: string): boolean;
20
+ /**
21
+ * The name a value is already stored under, if any. Optional: backends that can't (or won't)
22
+ * reverse-index omit it, and `createSecret`'s dedup step is skipped.
23
+ */
24
+ findByValue?(value: string): string | undefined;
25
+ }
26
+
27
+ export interface Vault extends VaultSpec {
28
+ /**
29
+ * Store a secret coming (often) from a user via the LLM, where we cannot tell a real value
30
+ * from a key name they typed by mistake. Returns the key name callers MUST reference — not
31
+ * necessarily the `name` requested:
32
+ * - `value` is already a known key name → returns `value` (it was a reference, not a secret)
33
+ * - `value` already stored under another name → returns that name (dedup)
34
+ * - otherwise → writes under `name` and returns `name`
35
+ */
36
+ createSecret(name: string, value: string): Promise<string>;
37
+ }