@matatbread/matbot-plugin-api 0.3.8 → 0.4.1

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/src/types.ts CHANGED
@@ -1,1089 +1,27 @@
1
- import type { MatbotPlugin } from './plugin.js';
2
- import type { StoreQuery, QueryResult } from './store-query.js';
3
-
4
- // ── Primitives ────────────────────────────────────────────────────────────────
5
-
6
- export type ISODate = string;
7
- export type MimeType = string;
8
- export type JSONSchema = Record<string, unknown>;
9
-
10
- // ── Principal ───────────────────────────────────────────────────────────────
11
-
12
- /** Identity of whatever initiated an operation — a turn, a tool call, a store read/write.
13
- * Established at each entry point and carried ambiently via the `PrincipalCarrier`
14
- * (`currentPrincipal()` / `runAs()`), so any layer can attribute or test the origin without it
15
- * being threaded through every signature. It grants nothing — policy is the service's concern. */
16
- export interface Principal {
17
- id: string;
18
- type: 'user' | 'agent' | 'system';
19
- }
20
-
21
- // ── Provider ──────────────────────────────────────────────────────────────────
22
-
23
- export interface ModelParameters {
24
- temperature?: number;
25
- maxTokens?: number;
26
- topP?: number;
27
- stopSequences?: string[];
28
- [key: string]: unknown;
29
- }
30
-
31
- export interface ProviderConfig {
32
- name: string;
33
- /** Plugin module specifier (npm name or file URL) that provides the adapter for this provider. */
34
- module: string;
35
- model: string;
36
- credentials?: Record<string, string>;
37
- endpoint?: string;
38
- parameters?: ModelParameters;
39
- fallback?: string;
40
- }
41
-
42
- /**
43
- * Token (and optional cost) counters for one provider call — pure accounting data, never conversation
44
- * content. Recorded onto the message stream for cost reckoning but elided from provider submission. The
45
- * producing provider is recorded alongside at each storage site (an assistant turn carries it on the
46
- * message's `providerName`; a tool-invoked completion pairs it explicitly — see `tool-result`), since a
47
- * tool may run a completion against any configured provider, each with its own rates.
48
- */
49
- export interface Usage {
50
- inputTokens: number;
51
- outputTokens: number;
52
- costUsd?: number;
53
- cacheReadTokens?: number;
54
- cacheCreationTokens?: number;
55
- }
56
-
57
- /** One provider call's usage, tagged with the provider billed — the unit a tool accrues (a tool may
58
- * run completions against any provider, each with its own rates) and the element persisted on a
59
- * `tool-result`'s `usage` addendum. See the ambient usage carrier (`recordUsage`/`currentUsageSink`). */
60
- export interface UsageRecord {
61
- provider: string;
62
- usage: Usage;
63
- }
64
-
65
- /**
66
- * Opaque, provider-specific round-trip metadata attached to a tool-call — captured from a completion,
67
- * persisted, and re-sent verbatim when the call is replayed in history (e.g. a Gemini 3 "thought
68
- * signature", which every historical `functionCall` must carry or the request 400s). The harness never
69
- * interprets it: it stores, renders, or elides it. Empty by default — a provider package augments its
70
- * OWN slice from its own module (namespaced by provider family, so slices from different providers in a
71
- * mixed-provider session never collide), so adding a provider that needs round-trip state touches NO
72
- * core code:
73
- *
74
- * declare module '@matatbread/matbot-plugin-api' {
75
- * interface ProviderMeta { google?: { thoughtSignature?: string } }
76
- * }
77
- *
78
- * A single session interleaves tool-calls from many providers, so this must be an open, additive union
79
- * (interface augmentation) — a generic type parameter can bind only one provider per instantiation and
80
- * so could not type a heterogeneous transcript.
81
- */
82
- export interface ProviderMeta {}
83
-
84
- export type CompletionEvent =
85
- | { type: 'text-delta'; delta: string }
86
- | { type: 'tool-call'; id: string; name: string; input: unknown;
87
- /** Provider-specific round-trip metadata for this call, opaque to the harness — see `ProviderMeta`. */
88
- meta?: ProviderMeta }
89
- | { type: 'tool-result'; id: string; result: unknown }
90
- | { type: 'thinking'; delta: string }
91
- | { type: 'thinking-block'; thinking: string; signature: string }
92
- | { type: 'redacted-thinking'; data: string }
93
- | { type: 'reasoning-block'; reasoning: string }
94
- | { type: 'refusal'; text: string }
95
- | { type: 'unknown-block'; blockType: string; raw: unknown }
96
- | ({ type: 'usage' } & Usage)
97
- | { type: 'done' };
98
-
99
- export interface ProviderAdapter {
100
- readonly name: string;
101
- complete(
102
- messages: Message[],
103
- config: ProviderConfig,
104
- tools: readonly Tool[],
105
- signal: AbortSignal
106
- ): AsyncIterable<CompletionEvent>;
107
- health(): Promise<HealthStatus>;
108
- }
109
-
110
- // ── Messages & Session ────────────────────────────────────────────────────────
111
-
112
- export type MessageRole = 'user' | 'assistant' | 'tool' | 'system' | 'marker';
113
-
114
- export type MessageContent = (
115
- | { type: 'text'; text: string }
116
- | { type: 'thinking'; thinking: string; signature: string }
117
- | { type: 'redacted-thinking'; data: string }
118
- | { type: 'reasoning'; reasoning: string }
119
- | { type: 'image'; data: string; mimeType: MimeType }
120
- | { type: 'image-url'; url: string; detail?: 'low' | 'high' | 'auto' }
121
- | { type: 'document'; data: string; mimeType: MimeType; name?: string }
122
- | { type: 'audio'; data: string; mimeType: MimeType }
123
- | { type: 'tool-call'; id: string; name: string; input: unknown;
124
- /** Provider-specific round-trip metadata, persisted and re-sent verbatim on replay — see
125
- * `ProviderMeta` and the `tool-call` `CompletionEvent`. */
126
- meta?: ProviderMeta }
127
- | { type: 'tool-result'; id: string; result: unknown; isError?: boolean;
128
- /**
129
- * Accounting addendum: completions the tool itself ran (e.g. `single_turn`, `dream_time`'s ranker/
130
- * merger), captured ambiently via the usage carrier and recorded for cost reckoning. Elided from
131
- * provider submission — adapters serialise only `id`/`result`/`isError`. One entry per provider
132
- * call, each tagged with the provider billed; never summed at write time, as rates differ.
133
- */
134
- usage?: UsageRecord[] }
135
- | { type: 'refusal'; text: string }
136
- | { type: 'file-ref'; fileId: string; name: string; mimeType: MimeType }
137
- | { type: 'form'; fields: FormField[]; submitLabel?: string }
138
- | { type: 'form-response'; values: Record<string, string> }
139
- | { type: 'marker'; creator: string; data: unknown }
140
- | { type: 'unknown-content'; blockType: string; raw: unknown }
141
- ) & {
142
- /**
143
- * Authorship provenance, for *presentation only* — orthogonal to the message's `role`, which is
144
- * the LLM-protocol identity. Absent ⇒ authored per the role (a human for `user`, the model for
145
- * `assistant`). `'robo'` ⇒ machine-authored by matbot — a `followup` resubmission, or a hook-
146
- * injected fragment inside a human turn. It is still carried to the model as ordinary
147
- * role-appropriate content (the model sees a `user` block either way); the flag is OOB metadata
148
- * that frontends use to present it agent-side (a robot indicator) rather than as the user's words.
149
- */
150
- origin?: 'robo';
151
- };
152
-
153
- /**
154
- * A marker is opaque, durable annotation carried in the message stream: links, status,
155
- * cross-references that are meaningful to a frontend but transparent to the LLM. They are
156
- * persisted unchanged, elided from provider submission, and deliberately preserved by
157
- * session compaction (removing one can break things — e.g. a pointer back to an ancestor
158
- * session). Any code with session access may emit one, normally as its own message.
159
- *
160
- * `creator` is the emitting plugin's reference; `data` is anything serialisable. For
161
- * per-creator type safety, augment `MarkerData` and read/write via `Marker<'your-creator'>`:
162
- *
163
- * declare module '@matatbread/matbot-plugin-api' {
164
- * interface MarkerData { 'split-session': { peerSessionId: string } }
165
- * }
166
- *
167
- * Unregistered creators fall back to `data: unknown`. The base `MessageContent` member stays
168
- * loose (`creator: string`) so the union and its exhaustive switches are unaffected.
169
- */
170
- export interface MarkerData {
171
- /** Emitted by the hook dispatcher when a hook handler threw: the hook was skipped (treated as a
172
- * no-op) and this records it once, so a misconfigured/throwing hook degrades visibly instead of
173
- * bricking the turn. `channel` is the hook point, `pluginName` the owning plugin if known. */
174
- 'matbot-hooks': { channel: HookPoint; pluginName?: string; message: string };
175
- }
176
-
177
- export type Marker<K extends string = string> = {
178
- type: 'marker';
179
- creator: K;
180
- data: K extends keyof MarkerData ? MarkerData[K] : unknown;
181
- };
182
-
183
- export interface FormField {
184
- name: string;
185
- label: string;
186
- type: 'text' | 'password' | 'select' | 'confirm';
187
- options?: string[];
188
- /** select-only: render an "Other…" affordance that lets the user type a free-form answer instead
189
- * of picking an option. The typed value is returned verbatim, on the same channel as a pick —
190
- * callers never learn whether the answer was a preset or free text. Ignored for other types. */
191
- allowOther?: boolean;
192
- default?: string;
193
- required?: boolean;
194
- /** Presentation hint only (default true): whether the frontend offers a cancel affordance (the
195
- * "give up" path — see `PromptFn`). Set false to render a hard-blocking prompt with no out. The
196
- * runner and server never consult this; a frontend with no cancel UI simply can't fire one. */
197
- cancelable?: boolean;
198
- }
199
-
200
- /**
201
- * Canonical values a `type: 'confirm'` prompt resolves to. The rendered label and buttons are a
202
- * cosmetic, host-specific concern (and may be localised), so consumers MUST branch on these tokens
203
- * — never on the displayed string. Compare case-insensitively to tolerate host casing differences.
204
- */
205
- export const CONFIRM_YES = 'yes';
206
- export const CONFIRM_NO = 'no';
207
-
208
- export interface Message {
209
- id: string;
210
- role: MessageRole;
211
- content: MessageContent[];
212
- createdAt: ISODate;
213
- traceId: string;
214
- providerName?: string;
215
- /** Token/cost accounting for the provider call that produced this message (assistant turns). The
216
- * billed provider is this message's `providerName`. Accounting only — elided from provider submission. */
217
- usage?: Usage;
218
- metadata?: Record<string, unknown>;
219
- }
220
-
221
- export type SessionStatus = 'active' | 'archived' | 'pinned';
222
-
223
- export interface Session {
224
- id: string;
225
- version: string;
226
- // Legacy: persisted sessions may still carry `ownerPrincipalId` / `actorPrincipalId` / `persona`.
227
- // They were never read — ownership-at-rest is structural (the storage partition), resolved via the
228
- // backend, not a field. Left undeclared; old data keeps them harmlessly as excess properties.
229
- title?: string;
230
- status: SessionStatus;
231
- contexts: string[];
232
- messages: Message[];
233
- parentSessionId?: string;
234
- branchPointMessageId?: string;
235
- createdAt: ISODate;
236
- updatedAt: ISODate;
237
- }
238
-
239
- // ── System context ────────────────────────────────────────────────────────────
240
-
241
- export type SystemContextContributor = (ctx: {
242
- session: Session;
243
- signal: AbortSignal;
244
- }) => string | null | Promise<string | null>;
245
-
246
- export interface SystemContextRegistry {
247
- register(contributor: SystemContextContributor, pluginName?: string): void;
248
- removeByPlugin(pluginName: string): void;
249
- /** Calls all contributors and joins non-null, non-empty results with double newlines. */
250
- build(ctx: { session: Session; signal: AbortSignal }): Promise<string | null>;
251
- }
252
-
253
- // ── Pipeline hooks ────────────────────────────────────────────────────────────
254
-
255
- export interface RunConfig {
256
- provider: string;
257
- sessionId?: string;
258
- traceId?: string;
259
- }
260
-
261
- /**
262
- * Hooks are sorted by the *job* they do, not by lifecycle position — the channel name is the
263
- * contract. Each channel has a fixed home, cadence, and effect-ceiling (a hook may always do
264
- * less: returning nothing makes it a pure observer). The discriminated union on `on` is what
265
- * keeps the effects honest — `contribute` hands you a read-only session, `toolcall`/`react` can't
266
- * return one at all, so a write that goes nowhere won't type-check.
267
- *
268
- * screen runner, once per turn before the first provider call. The only channel that may
269
- * durably mutate history. Returns any of: a replacement `session` (persisted),
270
- * turn-scoped `ephemeral` context (appended onto the tail of this turn's outgoing
271
- * messages — the freshest input the model reads — never persisted, and placed at the
272
- * tail rather than a system prefix so a "do X now" directive keeps its salience and
273
- * doesn't bust the cached prefix), `durable` context (the persisted, visible twin of
274
- * `ephemeral`: folded onto this turn's user message as `origin: 'robo'` blocks and
275
- * carried live as a `robo-user` event, so it survives into the next turn's history),
276
- * or `abort`. Mix freely. This is where the durable-vs-ephemeral choice for incoming
277
- * user input lives.
278
- * contribute runner, before *every* provider call. Ephemeral by construction (it re-fires, so a
279
- * durable mutation would accumulate). Returns a transformed copy of `outgoing` — the
280
- * message array about to be sent — and never touches the stored session. Mind prompt
281
- * caching: vary the *tail* (newest turn) freely, but a transform that rewrites the
282
- * cached prefix (system / early history) busts the cache on every call.
283
- * toolcall runner, before each tool execution. Read-only. Returns `rejectTool` (skip this call,
284
- * feed an error result back so the model self-corrects, without breaking the
285
- * tool_use/tool_result pairing) and/or `abort`.
286
- * toolresult runner, after each tool execution, before the result is recorded/yielded. Folds the
287
- * result through each hook: return `{ result }` to replace it (hard redaction, truncation),
288
- * or nothing to observe (auditing — the context carries args, result, isError and
289
- * `durationMs`). It owns the LLM-facing + persisted result and the `tool:end` event;
290
- * note it does NOT see the live `tool:stdout/stderr` chunks, which stream before the
291
- * result exists.
292
- * followup pump, once after a turn commits (post-persist). May `resubmit` a robo follow-up turn
293
- * (head-enqueued, so it runs next as its own real turn; `resubmitDepth` is the chain
294
- * length for the hook's own budget — the runner also hard-caps it), `retractAndRerun`
295
- * (pop the committed turn into a marker and re-run the originating user turn with
296
- * ephemeral context — supersede rather than follow), and/or append durable `markers`
297
- * to the committed session (LLM-invisible annotations — the second durable-write point
298
- * after `screen`, safe because it too fires once per turn).
299
- */
300
- export type HookPoint = 'screen' | 'contribute' | 'toolcall' | 'toolresult' | 'followup';
301
-
302
- export interface ScreenContext {
303
- session: Session;
304
- config: RunConfig;
305
- signal: AbortSignal;
306
- /** The turn's interactive prompt, when a frontend supplied one (a live user behind this turn).
307
- * A hook that drives an interactive tool (e.g. a trigger invoking `ask_user`) forwards this into
308
- * the tool's context; absent (cron/background/no frontend) the tool gets a rejecting prompt. */
309
- prompt?: PromptFn;
310
- /** Unregister the hook currently running. For one-shot hooks that should fire at most once. */
311
- removeHook(): void;
312
- }
313
- /**
314
- * A raced screen verdict the runner folds in WITHOUT gating the turn on it. A `screen` hook that starts
315
- * expensive work concurrently (e.g. a triggers classifier judging the user message) returns immediately
316
- * and hands back one of these instead of blocking on it. The runner polls `claim()` — synchronously,
317
- * never awaiting — at each turn-loop edge (before a provider call, on every stream event, and just
318
- * before committing). The first time `claim()` returns blocks (the work has settled WITH a correction),
319
- * the runner DISCARDS the uncommitted in-progress response and re-runs the loop with those blocks
320
- * tail-folded as ephemeral context — an in-situ redo: no store pop, no retraction marker, cleaner than a
321
- * post-commit retract. Because the mid-stream poll runs before each event is emitted, a verdict faster
322
- * than time-to-first-token is caught before any token reaches the frontend (the clean path); a slower
323
- * one aborts the in-flight provider request, saving the unstreamed remainder. If it never fires before
324
- * the turn commits, the turn commits normally and any correction is left to a post-commit `followup`.
325
- *
326
- * `claim()` MUST be exactly-once: return the correction on the first successful poll and `undefined`
327
- * forever after (not settled, no correction, or already claimed). The hook uses that single delivery to
328
- * coordinate with its own post-commit path — a claimed verdict is never also delivered by `followup`.
329
- */
330
- export interface DeferredScreen {
331
- claim(): DeferredCorrection | undefined;
332
- }
333
- /**
334
- * A claimed raced-verdict correction. `ephemeral` is tail-folded onto the re-run's outgoing messages
335
- * and never persisted (the transient "for this answer only" twin); `durable` is folded onto the turn's
336
- * user message — persisted, visible (the hook marks it `origin: 'robo'`), and carried live as a
337
- * `robo-user` event — so it updates the conversation rather than informing one answer (a `contextual`
338
- * trigger's semantics, preserved even though the verdict now lands mid-turn instead of before it). At
339
- * least one is non-empty when returned.
340
- */
341
- export interface DeferredCorrection {
342
- ephemeral?: MessageContent[];
343
- durable?: MessageContent[];
344
- }
345
- export interface ScreenResult {
346
- session?: Session;
347
- /**
348
- * A raced verdict the runner folds in without gating on it (see {@link DeferredScreen}). Lets a
349
- * screen hook race expensive work — a classifier — against generation: return immediately, hand this
350
- * back, and the runner restarts the turn in-situ if the verdict fires before commit. A hook that
351
- * would rather gate the turn (block until the verdict) just returns `ephemeral` as usual instead.
352
- */
353
- deferred?: DeferredScreen;
354
- /** Turn-scoped context appended onto the tail of this turn's outgoing messages (the freshest
355
- * input the model reads), never persisted. At the tail, not a system prefix, so a directive
356
- * keeps its salience and the cached system/history prefix stays stable across turns. */
357
- ephemeral?: MessageContent[];
358
- /**
359
- * The persisted, visible twin of `ephemeral`: context that should outlive this turn rather than
360
- * inform it once. The runner folds these blocks onto this turn's user message (so they ride into
361
- * the stored history and every subsequent provider call) AND carries them live as a `robo-user`
362
- * event, so a live draw and a reload render the same thing. They are LLM-visible (unlike
363
- * `markers`) and machine-authored, so a caller marks them `origin: 'robo'` for presentation.
364
- * Use when a fired hook produces context that genuinely updates the conversation, not a one-shot
365
- * corrective for the turn about to run.
366
- */
367
- durable?: MessageContent[];
368
- /**
369
- * Durable `marker` blocks to append to this turn's session (LLM-invisible). The dispatcher both
370
- * appends them to the persisted session AND carries them live on the turn's event stream, so a
371
- * live draw and a reload render the same thing. Use instead of hand-appending to `session` when
372
- * you just want to annotate (e.g. a fired trigger's silent tool recording what it did).
373
- */
374
- markers?: MessageContent[];
375
- abort?: string;
376
- }
377
-
378
- export interface ContributeContext {
379
- readonly outgoing: readonly Message[];
380
- readonly session: Session;
381
- config: RunConfig;
382
- signal: AbortSignal;
383
- /** Unregister the hook currently running. For one-shot hooks that should fire at most once. */
384
- removeHook(): void;
385
- }
386
-
387
- export interface ToolCallContext {
388
- readonly session: Session;
389
- readonly toolCall: { id: string; name: string; input: unknown };
390
- readonly tool: Tool;
391
- config: RunConfig;
392
- signal: AbortSignal;
393
- /** Unregister the hook currently running. For one-shot hooks that should fire at most once. */
394
- removeHook(): void;
395
- }
396
- export interface ToolCallResult {
397
- rejectTool?: { message: string };
398
- abort?: string;
399
- }
400
-
401
- export interface ToolResultContext {
402
- readonly session: Session;
403
- readonly toolCall: { id: string; name: string; input: unknown };
404
- readonly tool: Tool;
405
- readonly result: unknown;
406
- readonly isError: boolean;
407
- readonly durationMs: number;
408
- config: RunConfig;
409
- signal: AbortSignal;
410
- /** Unregister the hook currently running. For one-shot hooks that should fire at most once. */
411
- removeHook(): void;
412
- }
413
- // The toolresult hook returns `{ result }` to replace the tool's result, or nothing to leave it
414
- // (and just observe) — a trivial single-field return, inlined in the Hook union like `contribute`'s.
415
-
416
- export interface FollowupContext {
417
- readonly session: Session;
418
- readonly resubmitDepth: number;
419
- config: RunConfig;
420
- signal: AbortSignal;
421
- /** The turn's interactive prompt, when a frontend supplied one (a live user behind this turn).
422
- * A hook that drives an interactive tool (e.g. a trigger invoking `ask_user` as a proactive
423
- * follow-up question) forwards this into the tool's context; absent (cron/background/no frontend)
424
- * the tool gets a rejecting prompt. Note this prompt fires *post-commit*, out of band from the
425
- * turn's `done`, and blocks the pump until the human answers. */
426
- prompt?: PromptFn;
427
- /** Unregister the hook currently running. For one-shot hooks that should fire at most once. */
428
- removeHook(): void;
429
- }
430
- export interface FollowupResult {
431
- resubmit?: { content: MessageContent[] };
432
- /**
433
- * Retract-and-rerun: supersede the just-committed turn instead of following it. The pump pops the
434
- * committed turn back to (and excluding) the last user message, stashes the popped content in a
435
- * durable retraction marker (LLM-elided like every marker, so a frontend can render it
436
- * struck-through and a post-mortem can audit it), then re-runs that same user turn with `context`
437
- * injected EPHEMERALLY (tail-folded, never persisted) — agent-phase injection time-shifted onto a
438
- * committed turn. This is the inverse of `resubmit`, which leaves the response in place and appends
439
- * a new robo turn after it. Self-terminating by design: a well-formed trigger fires on a *curable*
440
- * defect that the injected context dissolves on the redo, so it won't re-fire; `resubmitDepth` (a
441
- * redo carries parent+1) caps an ill-formed one. `resubmit` and `retractAndRerun` are independent
442
- * capabilities — a single turn returning both is not expected, but both head-enqueue if it does.
443
- *
444
- * `context` is folded EPHEMERALLY onto the redo (for-this-answer-only). `durable` is instead folded
445
- * onto the re-run's user message — persisted, visible (mark it `origin: 'robo'`), carried live as a
446
- * `robo-user` event — so a `contextual`-kind correction updates the conversation durably even when it
447
- * lands post-commit. At least one of the two is present.
448
- */
449
- retractAndRerun?: { context?: MessageContent[]; durable?: MessageContent[] };
450
- /**
451
- * Durable `marker` blocks to append to the just-committed session (LLM-invisible; for tracing /
452
- * cross-references). The second durable-write capability after `screen` — safe here for the same
453
- * reason: `followup` fires once per turn, so an append can't accumulate the way a per-call channel
454
- * would. The pump persists them AND emits them live (post-commit, like a `queued` event) so a live
455
- * draw matches a reload. Use for recording what a post-commit reaction (e.g. a fired trigger's
456
- * silent tool) actually did.
457
- */
458
- markers?: MessageContent[];
459
- }
460
-
461
- export type Hook =
462
- | { on: 'screen'; priority?: number; pluginName?: string; handler(ctx: ScreenContext): ScreenResult | void | Promise<ScreenResult | void> }
463
- | { on: 'contribute'; priority?: number; pluginName?: string; handler(ctx: ContributeContext): Message[] | void | Promise<Message[] | void> }
464
- | { on: 'toolcall'; priority?: number; pluginName?: string; handler(ctx: ToolCallContext): ToolCallResult | void | Promise<ToolCallResult | void> }
465
- | { on: 'toolresult'; priority?: number; pluginName?: string; handler(ctx: ToolResultContext): { result: unknown } | void | Promise<{ result: unknown } | void> }
466
- | { on: 'followup'; priority?: number; pluginName?: string; handler(ctx: FollowupContext): FollowupResult | void | Promise<FollowupResult | void> };
467
-
468
- // ── Storage ───────────────────────────────────────────────────────────────────
469
- // The query grammar (Filter AST, StoreQuery, QueryResult, StoreQueryError) lives in ./store-query.
470
-
471
- export type CASResult<T> =
472
- | { ok: true; doc: T }
473
- | { ok: false; current: T | null };
474
-
475
- export interface Store<T extends { id: string; version: string }> {
476
- get(id: string): Promise<T | null>;
477
- set(id: string, value: T): Promise<void>;
478
- cas(id: string, expected: string, next: T): Promise<CASResult<T>>;
479
- delete(id: string, expectedVersion?: string): Promise<boolean>;
480
- query(q: StoreQuery): Promise<QueryResult<T>>;
481
- }
482
-
483
- // ── Knowledge index ───────────────────────────────────────────────────────────
484
-
485
- export interface KnowledgeEntry {
486
- id: string;
487
- version: string;
488
- entities: string[];
489
- tags: string[];
490
- summary: string;
491
- content: string;
492
- contentHash?: string;
493
- source: { type: string; uuid: string };
494
- confidence?: number;
495
- createdAt: string;
496
- updatedAt: string;
497
- }
498
-
499
- export interface KnowledgeIndex {
500
- index(entry: KnowledgeEntry): Promise<void>;
501
- /** Remove the entry with this id, if present (idempotent — removing an absent id is a no-op). The
502
- * id is the index's sole primary key (`index` replaces by it), so retraction is by id alone; the
503
- * index never inspects an entry's opaque `source`. The party that indexed an entry owns retracting
504
- * it — e.g. a skill manager retracts a hidden/deleted skill rather than the index policing tenants. */
505
- remove(id: string): Promise<void>;
506
- search(terms: Array<{ term: string; context?: string }>, signal: AbortSignal): Promise<KnowledgeEntry[]>;
507
- /** Enumerate all indexed entries. When present, register('KnowledgeIndex', …) drains these into the incoming backend. */
508
- entries?(): Iterable<KnowledgeEntry>;
509
- }
510
-
511
- // ── TypeScript ──────────────────────────────────────────────────────────────
512
-
513
- /**
514
- * Erases TypeScript types from a source string, returning runnable JavaScript. Type-stripping is
515
- * foundational — the harness ships raw `.ts` and any code compiled at runtime must have its types
516
- * removed before it can execute — so every execution environment provides one, and it lives as a fixed
517
- * runtime capability (on {@link MatbotRuntime}) the host supplies per platform, not a swappable service:
518
- * node uses its built-in `stripTypeScriptTypes`, the browser bundle uses sucrase. A consumer that
519
- * compiles source at runtime (e.g. `tool_function`) calls this instead of importing a platform stripper.
520
- *
521
- * `strip` may be async: a platform whose stripper loads lazily (the browser fetches sucrase on first use)
522
- * returns a promise; a synchronous stripper (node) returns the string directly — callers `await` either.
523
- * It is type-erasure, not full transpilation: node's stripper is erasable-only (enums/namespaces throw),
524
- * sucrase is more permissive, so authors should keep to erasable TypeScript to stay portable.
525
- */
526
- export interface TypeScriptStripper {
527
- strip(source: string): string | Promise<string>;
528
- }
529
-
530
- /**
531
- * Optional, node-only developer-experience service: the live `.d.ts` of the types the loaded tools
532
- * expose — what `toolResult(invokeTool(…, name, …))`, or a `function-tools` `await tool.name(…)`, resolves
533
- * to — derived by compiling each loaded plugin's `declare module '@matatbread/matbot-plugin-api'`
534
- * augmentations. The runtime registry can't supply this (result types are erased); only a TypeScript
535
- * program reading the source can. Consumers that generate or compose tool-calling code use it so the model
536
- * isn't guessing return shapes. Absent where no TypeScript program can run (the browser today) — a consumer
537
- * must degrade (guess-and-run) when `services.ToolTypeIndex` is undefined.
538
- *
539
- * The result is rebuilt lazily and cached, invalidated when the tool set changes. Tools the source scan
540
- * can't reach (a `function-tools` function) contribute their types by declaring a `toolContract` string on
541
- * their registered {@link Tool} — identical in shape to a `ToolContracts` arm; the index splices it off the
542
- * live registry, so no separate registration step is needed.
543
- */
544
- export interface ToolTypeIndex {
545
- /** Self-contained type declarations as a `.d.ts` string: the source-derived `ToolContracts` augmentations
546
- * merged with the arms of every other live tool, plus `declare const tool: ToolProxy` — the overloaded
547
- * proxy a generator writes `await tool.x(params)` against. */
548
- dts(): Promise<string>;
549
- /** Type-check a TypeScript `snippet` against exactly the {@link dts} above — the `tool` proxy ({@link
550
- * ToolProxy}: each multi-action tool an overload set, so `await tool.x(params)` narrows its result by the
551
- * params) is in scope. Returns human-readable diagnostics scoped to the snippet ([] means clean). A
552
- * composer uses it to catch bad tool-call code before running/registering it. */
553
- check(snippet: string): Promise<string[]>;
554
- /** Per live tool (that has a contract): the `params`/`result` wire text, flattened from the one contract —
555
- * a source tool's `ToolContracts` arms (via the source scan) or a source-less tool's `toolContract` string.
556
- * The single contract is thus also the source of the wire description; the host folds this into the
557
- * outgoing tool descriptions at the turn's dispatch edge. A tool with only a loose `inputSchema` (no
558
- * contract) is absent. */
559
- wireContracts(): Promise<Record<string, { params: string; result: string }>>;
560
- }
561
-
562
- // ── Tools ─────────────────────────────────────────────────────────────────────
563
-
564
- export type ToolEvent<Result = unknown> =
565
- | { type: 'stdout'; chunk: string }
566
- | { type: 'stderr'; chunk: string }
567
- | { type: 'progress'; pct: number; message?: string }
568
- | { type: 'result'; value: Result }
569
- | { type: 'file'; handle: FileHandle }
570
- // A durable, LLM-invisible annotation the tool emits as it runs (a link, a status, a trace of a
571
- // side-effect). Persisted as a `marker`-role message; elided from provider submission like any
572
- // marker. Independent of `result` — a tool may emit markers and no result (a silent side-effect,
573
- // e.g. a trigger-fired tool), a result and no markers, or both.
574
- | { type: 'marker'; creator: string; data: unknown }
575
- | { type: 'error'; message: string; code?: number; stdout?: string; stderr?: string };
576
-
577
- /**
578
- * Per-tool result-type registry: maps a tool's `name` to the type of the `value` it yields in its
579
- * `result` event. Augment it exactly like {@link MarkerData} / `MatbotServices`, so a caller gets the
580
- * concrete result type back from {@link invokeTool} + `toolResult` instead of `unknown`:
581
- *
582
- * declare module '@matatbread/matbot-plugin-api' {
583
- * interface ToolContracts { find_fact: string[] | null }
584
- * }
585
- *
586
- * The tool name is the discriminator: `invokeTool(machine, 'find_fact', …)` is typed
587
- * `AsyncIterable<ToolEvent<string[] | null>>`, so `toolResult(…)` resolves to `string[] | null`. A
588
- * name with no registered entry resolves to `unknown` (the caller must narrow). This is a pure
589
- * type-level construct — no runtime validation; it pins, at the call site, the contract a tool's
590
- * executor already produces. Keep the entry in sync with what the executor actually yields.
591
- *
592
- * **Multi-action tools** are a weird form of overloaded function: the same tool returns different
593
- * shapes depending on its params. Register such a tool as a union of {@link ToolContract} *arms*, each
594
- * pairing a result with the discriminating params *pattern* that selects it — `invokeTool` matches the
595
- * call's literal params against the patterns and narrows the result to the matching arm:
596
- *
597
- * declare module '@matatbread/matbot-plugin-api' {
598
- * interface ToolContracts {
599
- * session_action:
600
- * | ToolContract<Session, { action: 'get' }>
601
- * | ToolContract<{ id: string; title: string }, { action: 'rename' }>;
602
- * }
603
- * }
604
- * // invokeTool(machine, 'session_action', { action: 'get', sessionId }) → result is Session
605
- *
606
- * The pattern is *any* discriminating field(s), not just `action` (a tool keying on `interval`'s
607
- * presence registers `ToolContract<…, { interval: string }>`). It is a *pattern*, not the full input:
608
- * key only on the discriminant so a call carrying just that field still matches. When no arm's pattern
609
- * matches (a non-literal discriminant, or an absence-discriminant the positive patterns can't express),
610
- * the result falls back to the union of every arm — always sound, just less narrow.
611
- */
612
- export interface ToolContracts {}
613
-
614
- /**
615
- * One overload arm of a multi-action tool registered in {@link ToolContracts}: the `Result` it yields
616
- * for a call whose params match the discriminating pattern `Args`. `Args` defaults to `unknown` (an
617
- * arm that matches any params). Purely type-level — the `__` fields are phantom and never constructed.
618
- */
619
- export interface ToolContract<Result, Args = unknown> {
620
- readonly __result: Result;
621
- readonly __args: Args;
622
- }
623
-
624
- type ToolResultArmed<E> = E extends ToolContract<unknown, unknown> ? true : false;
625
- type ToolResultUnion<E> = E extends ToolContract<infer R, unknown> ? R : never;
626
- type ToolResultMatched<E, P> = E extends ToolContract<infer R, infer A> ? (P extends A ? R : never) : never;
627
-
628
- /** The full set of `result` values a tool named `K` may yield — the union over all its arms (for an
629
- * arm-based entry), the entry itself (for a plain entry), or `unknown` when `K` is unregistered. This
630
- * is what an executor binds against (`ToolExecutor<ToolResultOf<'my_tool'>>`): it must cover every arm. */
631
- export type ToolResultOf<K extends string> =
632
- K extends keyof ToolContracts
633
- ? ToolResultArmed<ToolContracts[K]> extends true ? ToolResultUnion<ToolContracts[K]> : ToolContracts[K]
634
- : unknown;
635
-
636
- /** The `result` a call to tool `K` with params `P` yields: for an arm-based entry, the arm whose
637
- * pattern `P` matches (or the full union when none match — always sound); otherwise {@link ToolResultOf}. */
638
- export type ToolResultFor<K extends string, P> =
639
- K extends keyof ToolContracts
640
- ? ToolResultArmed<ToolContracts[K]> extends true
641
- ? ([ToolResultMatched<ToolContracts[K], P>] extends [never]
642
- ? ToolResultUnion<ToolContracts[K]>
643
- : ToolResultMatched<ToolContracts[K], P>)
644
- : ToolContracts[K]
645
- : unknown;
646
-
647
- // Distribute a union to its intersection: `A | B` → `A & B`. An intersection of function types is an
648
- // overload set, so this turns a union of per-arm call signatures into an overloaded function.
649
- type UnionToIntersection<U> =
650
- (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
651
-
652
- // One `ToolContracts` arm → its call signature. A fully-untyped arm (`ToolContract<unknown, unknown>`, the
653
- // fallback for a foreign/MCP tool carrying only a JSON-Schema `inputSchema`) keeps a loose, optional-param
654
- // signature so it stays callable; a typed arm requires its declared params.
655
- type ArmCallSig<E> =
656
- E extends ToolContract<infer R, infer P>
657
- ? (unknown extends P ? (params?: unknown) => Promise<R> : (params: P) => Promise<R>)
658
- : never;
659
-
660
- // The union of a tool's arm params — the parameter type of the trailing catch-all signature below.
661
- type ArmArgsUnion<E> = E extends ToolContract<unknown, infer A> ? A : never;
662
-
663
- /**
664
- * The type of the injected `tool` proxy that `function-tools` and compiled skills call
665
- * (`await tool.name(params)`). Each key is derived from its {@link ToolContracts} entry: a multi-arm
666
- * (multi-action) entry becomes an **overload set** — one `(params) => Promise<result>` signature per arm —
667
- * so `await tool.name(params)` narrows its result by the params passed, exactly the contract a code
668
- * generator needs to write a correct call first time. A single-arm entry is one signature. This is what the
669
- * generated `dts()` declares `tool` as; it is *derived* — never hand-authored — from the same `ToolContracts`
670
- * augmentation that types authors' `invokeTool`/`toolResult` calls, so the human editor and the LLM see one
671
- * source of truth.
672
- *
673
- * The trailing catch-all signature (params union → result union) exists for soundness at the *meta-type*
674
- * level, not for call sites: overload resolution is first-match, so a well-formed call still hits its
675
- * specific arm and narrows precisely, and a malformed call matches nothing and still errors. What it
676
- * changes: `ReturnType<typeof tool.x>` — which on a bare overload set resolves to an arbitrary (last) arm,
677
- * a real-world codegen trap — now degrades to the sound full union; and a params value typed as a union of
678
- * arms (dynamic multi-action dispatch) becomes callable, returning the result union.
679
- */
680
- export type ToolProxy = { [K in keyof ToolContracts]:
681
- UnionToIntersection<ArmCallSig<ToolContracts[K]>>
682
- & ((params: ArmArgsUnion<ToolContracts[K]>) => Promise<ToolResultUnion<ToolContracts[K]>>) };
683
-
684
- /**
685
- * Ask the user a question and resolve with their answer. The host supplies the
686
- * implementation — readline in the CLI, an SSE round-trip in the web frontend. This is
687
- * matbot's single mechanism for eliciting user input at runtime; it is injected both into
688
- * tool execution (`ToolContext.prompt`) and into plugin loading (collision resolution in setup).
689
- *
690
- * Two call forms:
691
- * - `(question, defaultValue?)` — free text; resolves to the typed string (or the default).
692
- * - `(field: FormField)` — a single structured field (`select`/`confirm`/`password`/`text`),
693
- * letting rich frontends render real controls (buttons, masked input). Frontends that can't
694
- * render it fall back to `field.label` as plain text. Resolves to the chosen/typed value.
695
- *
696
- * Cancellation is the "we can't proceed — give up" path (distinct from a graceful "decline", which
697
- * is simply one of the offered `options`). The host rejects the promise with `PromptCancelledError`
698
- * and abandons the current turn, returning to idle; a caller's surrounding try/catch turns the
699
- * rejection into a tool error that closes the tool call cleanly.
700
- * This reuses `FormField` rather than a parallel type; it is a one-shot request/response and
701
- * deliberately does NOT engage the session-bound `form`/`form-response` flow.
702
- */
703
- export interface PromptFn {
704
- (question: string, defaultValue?: string): Promise<string>;
705
- (field: FormField): Promise<string>;
706
- }
707
-
708
- export interface ToolContext {
709
- callId: string;
710
- session: Session;
711
- signal: AbortSignal;
712
- vault: Vault;
713
- /** The provider key driving the current turn (`RunConfig.provider`). A tool that spawns further
714
- * work should default to this so the child inherits the same model rather than the config default. */
715
- provider?: string;
716
- workdir?: string;
717
- configPath?: string;
718
- files?: FileStore;
719
- /** Prompt the user for input. The host provides a readline or form implementation. */
720
- prompt: PromptFn;
721
- /** Hot-load a plugin by specifier without restarting the process. Returns the loaded plugin.
722
- * `refresh` (default false) forces a remote (github/http) plugin to be re-downloaded rather than
723
- * re-loaded from its `.plugins/` cache — pass true to pick up changed upstream source. */
724
- loadPlugin(specifier: string, refresh?: boolean): Promise<MatbotPlugin>;
725
- /**
726
- * Hot-unload a plugin by specifier, removing its tools, hooks, and system context contributions.
727
- * Resolves `true` if a plugin was actually resident and unloaded, `false` if there was nothing
728
- * to unload. A failed `teardown()` (e.g. timeout) still throws — the plugin was resident in that case.
729
- */
730
- unloadPlugin(specifier: string): Promise<boolean>;
731
- }
732
-
733
- /**
734
- * The read-only facts about the call a composed function is running under, injected into a
735
- * `tool_function` body as the ambient `context` binding beside `tool`/`toolInContext`. Deliberately
736
- * narrower than {@link ToolContext}: the identity of the call, not its capabilities — `vault`,
737
- * `files`, `prompt` and plugin (un)loading stay off a surface authored by a model and reachable only
738
- * through the tool contracts. A tool with real source takes `ToolContext` and needs none of this.
739
- *
740
- * Not `ToolCallContext` — that name belongs to the `toolcall` hook's context; this is the composed
741
- * caller's own view, not a hook's view of a call about to run.
742
- */
743
- export interface ComposedCallContext {
744
- readonly callId: string;
745
- readonly sessionId: string;
746
- /** The provider key driving the turn — the same one nested `tool.x(…)` calls inherit. */
747
- readonly provider?: string;
748
- readonly workdir?: string;
749
- readonly signal: AbortSignal;
750
- }
751
-
752
- /**
753
- * A tool's runtime. `R` is the type of the `value` carried by its `result` event — declared once,
754
- * at the source, so the executor's yields and the tool's {@link ToolContracts} registry entry can't
755
- * silently drift. The `unknown` default keeps untyped executors compiling untouched, and covariance
756
- * means a narrower `ToolExecutor<X>` still satisfies `ToolExecutor` (i.e. `ToolExecutor<unknown>`),
757
- * so the heterogeneous registry boundary (`Tool[]`) accepts any executor. Bind `R` to the registry
758
- * entry with `ToolExecutor<ToolResultOf<'my_tool'>>` so the single augmentation is the source of truth.
759
- */
760
- export interface ToolExecutor<R = unknown> {
761
- execute(input: unknown, ctx: ToolContext): AsyncIterable<ToolEvent<R>>;
762
- }
763
-
764
- export interface Tool<R = unknown> {
765
- name: string;
766
- description: string;
767
- inputSchema: JSONSchema;
768
- /** The call contract as TypeScript source — a {@link ToolContract}`<Result, Params>` (or a union of arms
769
- * for a multi-action tool), *identical in shape* to what a `ToolContracts` augmentation declares. Only a
770
- * tool with **no scannable source** carries this — a `function-tools` function (whose signature supplies
771
- * both halves), and any similarly runtime-defined tool: the host splices it verbatim into the generated
772
- * dts's `ToolContracts`, and derives the wire `params`/`result` text from it. A tool WITH source declares
773
- * its contract as a `ToolContracts` augmentation instead and omits this. Foreign tools (e.g. MCP proxies)
774
- * that have neither fall back to the loose `inputSchema`. */
775
- toolContract?: string;
776
- executor: ToolExecutor<R>;
777
- pluginName?: string;
778
- }
779
-
780
- // ── Files ─────────────────────────────────────────────────────────────────────
781
-
782
- export interface FileMetaData {
783
- id: string;
784
- version: string;
785
- name: string;
786
- mimeType: MimeType;
787
- size: number;
788
- createdAt: ISODate;
789
- sessionId?: string;
790
- messageId?: string;
791
- namespace?: string;
792
- /** Whether this file may be served at a public URL. Default-deny: absent ⇒ not servable. The
793
- * flag rides in the file's own persisted metadata (no separate allow-list), so the same read that
794
- * resolves a request also gates it. Producers opt in per put (the workspace tool sets it true). */
795
- allowed?: boolean;
796
- }
797
-
798
- export interface FileHandle extends FileMetaData {
799
- stream(signal?: AbortSignal): AsyncIterable<Uint8Array>;
800
- }
801
-
802
- export interface FileFilter {
803
- sessionId?: string;
804
- mimeType?: string;
805
- namespace?: string;
806
- createdAfter?: ISODate;
807
- createdBefore?: ISODate;
808
- }
809
-
810
- export interface FileStore {
811
- /** Store a file. When `name` is provided, upserts by (name + namespace); otherwise always creates a new entry. */
812
- put(
813
- name: string | undefined,
814
- mimeType: MimeType,
815
- data: AsyncIterable<Uint8Array>,
816
- meta?: { sessionId?: string; messageId?: string; namespace?: string; allowed?: boolean }
817
- ): Promise<FileHandle>;
818
- get(id: string): Promise<FileHandle | null>;
819
- getByName(name: string, namespace?: string): Promise<FileHandle | null>;
820
- delete(id: string): Promise<void>;
821
- list(filter?: FileFilter): AsyncIterable<FileHandle>;
822
- putTemp(name: string, mimeType: MimeType, data: AsyncIterable<Uint8Array>): Promise<FileHandle>;
823
- }
824
-
825
- // ── Frontend ──────────────────────────────────────────────────────────────────
826
-
827
- /**
828
- * Passed to `services.registerFrontend()` by a plugin declaring itself a frontend. A frontend
829
- * owns its own I/O (an HTTP server, a bot connection, a REPL); matbot only needs to know it
830
- * exists. This object is the growth point for frontend-level capability advertisement
831
- * (accepted/produced MIME types, size limits, …) as media support lands.
832
- */
833
- export interface FrontendInfo {
834
- name: string;
835
- }
836
-
837
- // ── Vault ─────────────────────────────────────────────────────────────────────
838
-
839
- /**
840
- * The primitives a vault backend must implement. Resolution, redaction, and the three
841
- * low-level store operations the smart `createSecret` policy composes. A backend implements
842
- * `Vault` (which is `VaultSpec` plus that policy); plugins are handed a `Vault`.
843
- */
844
- export interface VaultSpec {
845
- /** Resolve ${NAME} placeholders by looking up the named value; throws MissingSecretError for any miss. */
846
- resolve(ref: string): Promise<string>;
847
- scrub(text: string): string;
848
- /**
849
- * Store `value` under exactly `name`, overwriting. The literal write; no reference/dedup logic.
850
- * An empty `value` removes the key instead — there is no such thing as an empty secret, so the
851
- * empty string is the removal signal (idempotent: removing an absent name is a no-op).
852
- */
853
- writeSecret(name: string, value: string): Promise<void>;
854
- /** Whether a secret is stored under this exact name. */
855
- hasKey(name: string): boolean;
856
- /**
857
- * The name a value is already stored under, if any. Optional: backends that can't (or won't)
858
- * reverse-index omit it, and `createSecret`'s dedup step is skipped.
859
- */
860
- findByValue?(value: string): string | undefined;
861
- }
862
-
863
- export interface Vault extends VaultSpec {
864
- /**
865
- * Store a secret coming (often) from a user via the LLM, where we cannot tell a real value
866
- * from a key name they typed by mistake. Returns the key name callers MUST reference — not
867
- * necessarily the `name` requested:
868
- * - `value` is already a known key name → returns `value` (it was a reference, not a secret)
869
- * - `value` already stored under another name → returns that name (dedup)
870
- * - otherwise → writes under `name` and returns `name`
871
- */
872
- createSecret(name: string, value: string): Promise<string>;
873
- }
874
-
875
- // ── Health ────────────────────────────────────────────────────────────────────
876
-
877
- export type HealthStatus =
878
- | { status: 'ok'; latencyMs?: number }
879
- | { status: 'degraded'; reason: string; latencyMs?: number }
880
- | { status: 'down'; reason: string };
881
-
882
- // ── Registries ────────────────────────────────────────────────────────────────
883
-
884
- /** Tool CRUD is observed on the {@link Notifier}, as a `RegistryChange` with `registry: 'tools'` — this
885
- * registry had its own broadcaster over the same primitive, which is duplication, not layering. One
886
- * notification per tool (removeByPlugin announces a `removed` per matched tool); the registering
887
- * plugin's name rides in `detail`, advisory as ever — resolve the name for anything authoritative. */
888
- export interface ToolRegistry {
889
- register(tool: Tool): void;
890
- remove(name: string): void;
891
- resolve(name: string): Tool | null;
892
- list(): readonly Tool[];
893
- removeByPlugin(pluginName: string): void;
894
- }
895
-
896
- /** What the runner tells a {@link ToolPresenter} about the call it's about to make. */
897
- export interface PresentContext {
898
- /** The session as of this provider call — the persisted user turn plus any assistant/tool turns
899
- * taken so far this turn. A presenter ranks the tool set against this. */
900
- session: Session;
901
- /** The provider profile name this turn runs under, so a presenter can tailor to provider
902
- * capabilities (e.g. Anthropic's in-context tool_reference expansion). */
903
- provider: string;
904
- }
905
-
906
- /**
907
- * Chooses which tools are advertised to the model for a single provider call — a read-only *view* over
908
- * the turn's tool snapshot, NOT tool management. Presentation is orthogonal to the {@link ToolRegistry},
909
- * which stays the single source of registration, resolution, and type-contracts (its fragile, load-bearing
910
- * machinery is never a swap surface). The runner consults the presenter before *every* provider call, so
911
- * an impl may present a subset (deferring a large library behind a search tool) and grow it mid-turn as the
912
- * model discovers tools. Executor resolution is unaffected — a tool withheld here stays callable by name
913
- * through the live registry — so presentation only ever changes what the model is *told about*.
914
- *
915
- * Optional {@link MatbotServices} member (offer-loosely): absent ⇒ the runner advertises the whole
916
- * snapshot, byte-identical to today. `present` may return synchronously or async (a reranker).
917
- */
918
- export interface ToolPresenter {
919
- present(tools: readonly Tool[], ctx: PresentContext): readonly Tool[] | Promise<readonly Tool[]>;
920
- }
921
-
922
- /**
923
- * The named provider profiles the runtime resolves by name. A `ReadonlyMap` for the many read-only
924
- * consumers (`services.providers.get`/`has`/`keys`/…), plus a sanctioned write path so a plugin can
925
- * *contribute* profiles live — a storage backend replaying provider definitions it captured in its own
926
- * medium, or the built-in `provider` tool — exactly as a plugin contributes tools via {@link ToolRegistry}.
927
- * Keyed by `config.name`; `register` upserts.
928
- */
929
- export interface ProviderRegistry extends ReadonlyMap<string, ProviderConfig> {
930
- register(config: ProviderConfig): void;
931
- /** Explicit delete (the `provider` tool's `remove`) — removes the profile outright. */
932
- remove(name: string): boolean;
933
- /**
934
- * Undo a contribution: restore the boot-baseline profile for `name` if one existed (a plugin-supplied
935
- * profile that shadowed a configured one), otherwise delete it. A plugin that registers providers
936
- * (e.g. a storage backend replaying them from its medium) calls this for each in its `teardown()`, so
937
- * unloading it reverts the provider set to the host's boot condition — the multi-valued analogue of a
938
- * swap-member reverting to its captured boot default.
939
- */
940
- revert(name: string): void;
941
- }
942
-
943
- // ── Pipeline events ─────────────────────────────────────────────────────────────
944
-
945
- /**
946
- * The streaming output of a single turn. Every event carries the `traceId` of the turn that
947
- * produced it — that is the correlation key a frontend uses to route events to the right
948
- * message/bubble (see `SessionRunner`). A turn ends with exactly one terminal event:
949
- * `done` (completed), `aborted` (interrupted mid-flight), or `error`. `cancelled` is emitted
950
- * for a queued submission that was dropped before it ever ran (e.g. by `SessionRunner.abort`),
951
- * so it carries no session — there is nothing to persist.
952
- */
953
- export type PipelineEvent =
954
- | { type: 'text-delta'; delta: string; traceId: string }
955
- | { type: 'thinking'; delta: string; traceId: string }
956
- | { type: 'tool:start'; callId: string; name: string; input: unknown; traceId: string }
957
- | { type: 'tool:stdout'; callId: string; chunk: string; traceId: string }
958
- | { type: 'tool:stderr'; callId: string; chunk: string; traceId: string }
959
- | { type: 'tool:progress'; callId: string; pct: number; message?: string; traceId: string }
960
- | { type: 'tool:end'; callId: string; result: unknown; isError: boolean; traceId: string }
961
- | { type: 'file'; handle: FileHandle; traceId: string }
962
- | ({ type: 'usage'; traceId: string } & Usage)
963
- | { type: 'done'; session: Session; traceId: string }
964
- | { type: 'aborted'; reason: string; session: Session; traceId: string }
965
- | { type: 'cancelled'; sessionId: string; traceId: string }
966
- // Session-level (not turn-level, hence no traceId): the runner has fully drained its queue and is
967
- // now idle. Emitted once per busy period, *after* `running` flips false, so a consumer can map it
968
- // to an authoritative busy→idle transition without racing the internal state flip.
969
- | { type: 'idle'; sessionId: string }
970
- // A queued (not-yet-running) submission, carried on the stream so a frontend renders it as part
971
- // of the live "delta" (everything after the last committed message), never from stored state.
972
- // `queued` is the number of submissions ahead of it (0 ⇒ about to run). Emitted live on enqueue
973
- // and replayed (in queue order) to anyone subscribing mid-flight.
974
- | { type: 'queued'; content: MessageContent[]; queued: number; concatQueue: boolean; traceId: string; rootTraceId: string }
975
- // Machine-authored content folded onto the running turn's user message (a `screen` hook's
976
- // `durable` result — e.g. a fired `contextual` trigger), carried live so a frontend draws it
977
- // immediately. The blocks are already persisted in that user message (origin: 'robo'); this is
978
- // purely the live-delivery channel, so a live draw matches the reload (which splits the user
979
- // turn's robo blocks into their own agent-side bubble).
980
- | { type: 'robo-user'; content: MessageContent[]; traceId: string }
981
- // Marker blocks appended to the session this turn (e.g. the dispatcher's record of a hook that
982
- // threw), carried live so a frontend renders them without waiting for a session reload. The blocks
983
- // are already persisted in the session; this event is purely the live-delivery channel.
984
- | { type: 'marker'; content: MessageContent[]; traceId: string }
985
- // A mid-turn steer that INTERRUPTED the running turn (see `SteeringPolicy`). Emitted synchronously
986
- // when the steer is accepted, so the new user bubble lands in correct stream order and a frontend
987
- // knows the imminent `aborted` (reason 'steer') on `interruptedTraceId` is a yield, not a dead-end:
988
- // keep that turn's partial work rendered and expect a continuation. `traceId`/`rootTraceId` identify
989
- // the steer submission itself (the continuation turn). Late subscribers reconstruct the bubble from
990
- // the pump's per-turn `queued` replay seed / committed store order — this is purely live delivery.
991
- | { type: 'steer'; content: MessageContent[]; interruptedTraceId: string; traceId: string; rootTraceId: string }
992
- | { type: 'error'; error: string; traceId: string }
993
- | { type: 'system-context'; text: string; traceId: string };
994
-
995
- // ── Session runner ──────────────────────────────────────────────────────────────
996
-
997
- /**
998
- * A view onto a session returned by `SessionRunner.open`. `session` is the authoritative
999
- * server-side state — committed messages plus an overlay of any queued-but-not-yet-run
1000
- * submissions (each carrying `metadata.pending: true`). `events` is a lazy, per-session live
1001
- * tap: accessing it subscribes to the turn event stream from now (replaying the in-flight
1002
- * turn, if any); never touching it costs nothing.
1003
- */
1004
- export interface SessionView {
1005
- session: Session;
1006
- /** Submissions waiting behind the current turn (does not count the running turn). */
1007
- queued: number;
1008
- /** Correlation id of the submission this call enqueued — present only when content was supplied. */
1009
- traceId?: string;
1010
- readonly events: AsyncIterable<PipelineEvent>;
1011
- }
1012
-
1013
- /** Observe a session without submitting anything. */
1014
- export interface OpenOpts {
1015
- sessionId: string;
1016
- signal: AbortSignal;
1017
- /** Optional caller-supplied correlation id; one is generated when absent. */
1018
- traceId?: string;
1019
- }
1020
-
1021
- /** Observe a session AND enqueue a submission. The compiler enforces provider/principal here;
1022
- * a remote frontend deserializing a request body must still validate the wire input itself. */
1023
- export interface SubmitOpenOpts extends OpenOpts {
1024
- content: MessageContent[];
1025
- provider: string;
1026
- principal: Principal;
1027
- /** When true, this submission may be merged with others drained in the same batch. Default false
1028
- * (queue mode: one turn per submission). */
1029
- concatQueue?: boolean;
1030
- /** Disposition for a submission arriving while a turn is running (see {@link SteeringPolicy}):
1031
- * 'queue' waits for the turn boundary (default), 'interrupt' stops the running turn — keeping its
1032
- * committed partial work — and runs this next, 'auto' defers to the registered SteeringPolicy (else
1033
- * the host default). Meaningless when nothing is running (degrades to a plain enqueue). */
1034
- mode?: SteeringMode;
1035
- /** Interactive prompt implementation for this submission's turn. The frontend owns delivery —
1036
- * it must target the frontend's per-session client connections, not a single request. */
1037
- prompt?: PromptFn;
1038
- }
1039
-
1040
- /**
1041
- * Serialises turns per session. A submission never executes concurrently with another for the
1042
- * same session; the in-memory queue (lost on process restart, by design) absorbs anything that
1043
- * arrives mid-turn. The server is the source of truth: a frontend renders whatever `open()`
1044
- * returns and treats the live `events` stream purely as an optimisation.
1045
- */
1046
- export interface SessionRunner {
1047
- open(opts: OpenOpts | SubmitOpenOpts): Promise<SessionView>;
1048
- /** Abort the running turn (if any) and drop all queued submissions, emitting `cancelled` for each. */
1049
- abort(sessionId: string): void;
1050
- /** Abandon the running turn (if any) WITHOUT touching the queue — `pump` advances to the next
1051
- * queued submission, or idles. The "give up on this turn" path (a prompt cancel); contrast
1052
- * `abort`, which also clears the queue. A no-op if nothing is running. */
1053
- cancelTurn(sessionId: string): void;
1054
- /** Snapshot of a session's live state: whether a turn is running and how many submissions wait
1055
- * behind it. `busy` is `running || queued > 0`. */
1056
- status(sessionId: string): { busy: boolean; running: boolean; queued: number };
1057
- }
1058
-
1059
- // ── Steering ────────────────────────────────────────────────────────────────────
1060
-
1061
- /** Disposition of a submission that arrives while a turn is running. `interrupt` stops the running
1062
- * turn (its committed partial work is preserved) and runs the new message next; `queue` waits for the
1063
- * turn boundary. */
1064
- export type SteeringDecision = 'queue' | 'interrupt';
1065
- /** The wire-level request (see `SubmitOpenOpts.mode`). `auto` defers the decision to the registered
1066
- * {@link SteeringPolicy} (else the host default). */
1067
- export type SteeringMode = SteeringDecision | 'auto';
1068
-
1069
- /**
1070
- * How the runner disposes of a mid-turn submission under `mode: 'auto'`, and how an interrupt's
1071
- * continuation is nudged. An optional, registerable service ({@link MatbotServices}); absent ⇒ the
1072
- * runner uses its own defaults. Both members are optional so a plugin may override one, the other, or
1073
- * both. `classify` is deliberately NOT assumed to be an LLM — a regex or a semantic classifier is a
1074
- * first-class implementation (return synchronously); an LLM `singleTurn` returns a promise.
1075
- */
1076
- export interface SteeringPolicy {
1077
- /**
1078
- * Consulted only for `mode: 'auto'` while a turn is running. `session` is the COMMITTED session
1079
- * (history up to the running turn's start — the in-flight partial is not reachable at submit time);
1080
- * `steer` is the incoming submission. Return synchronously (regex) or async (semantic / singleTurn).
1081
- * Absent ⇒ the host default disposition.
1082
- */
1083
- classify?(ctx: { session: Session; steer: MessageContent[] }): SteeringDecision | Promise<SteeringDecision>;
1084
- /**
1085
- * The ephemeral "keep going, noting the above" context folded onto an interrupt's continuation turn
1086
- * (never persisted). Absent ⇒ the host default nudge.
1087
- */
1088
- nudge?(ctx: { session: Session; steer: MessageContent[] }): MessageContent[];
1089
- }
1
+ /*
2
+ * The plugin-facing type surface, one file per domain. It was a single 1,195-line module — the first
3
+ * thing a third-party author opens and the last thing they can hold in their head. The `── Section ──`
4
+ * banners it had already grown are the split, so each file is the unit its banner described.
5
+ *
6
+ * This barrel re-exports them in the original order, so every `from './types.js'` import across the
7
+ * repo — and `index.ts`'s `export type *` — is byte-identical in effect.
8
+ */
9
+
10
+ export * from './types/primitives.js';
11
+ export * from './types/principal.js';
12
+ export * from './types/provider.js';
13
+ export * from './types/messages.js';
14
+ export * from './types/system-context.js';
15
+ export * from './types/hooks.js';
16
+ export * from './types/storage.js';
17
+ export * from './types/knowledge.js';
18
+ export * from './types/typescript.js';
19
+ export * from './types/tools.js';
20
+ export * from './types/files.js';
21
+ export * from './types/frontend.js';
22
+ export * from './types/vault.js';
23
+ export * from './types/health.js';
24
+ export * from './types/registries.js';
25
+ export * from './types/events.js';
26
+ export * from './types/session-runner.js';
27
+ export * from './types/steering.js';