@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/package.json +3 -2
- package/src/hooks.ts +6 -6
- package/src/host-machine.ts +192 -0
- package/src/host.ts +43 -0
- package/src/index.ts +27 -9
- package/src/invoke-tool.ts +25 -3
- package/src/notify.ts +3 -0
- package/src/plugin.ts +40 -190
- package/src/session.ts +30 -1
- package/src/types/events.ts +81 -0
- package/src/types/files.ts +46 -0
- package/src/types/frontend.ts +11 -0
- package/src/types/health.ts +6 -0
- package/src/types/hooks.ts +217 -0
- package/src/types/knowledge.ts +27 -0
- package/src/types/messages.ts +154 -0
- package/src/types/primitives.ts +5 -0
- package/src/types/principal.ts +10 -0
- package/src/types/provider.ts +157 -0
- package/src/types/registries.ts +84 -0
- package/src/types/session-runner.ts +69 -0
- package/src/types/steering.ts +33 -0
- package/src/types/storage.ts +16 -0
- package/src/types/system-context.ts +15 -0
- package/src/types/tools.ts +262 -0
- package/src/types/typescript.ts +50 -0
- package/src/types/vault.ts +37 -0
- package/src/types.ts +27 -1089
package/src/plugin.ts
CHANGED
|
@@ -1,14 +1,26 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
FileStore, Principal, Vault, Message, ModelParameters,
|
|
3
3
|
ProviderAdapter, ProviderConfig, ProviderRegistry, Tool, ToolRegistry, FrontendInfo,
|
|
4
|
-
Store, Session, SystemContextRegistry, KnowledgeIndex, PromptFn, SessionRunner, Usage,
|
|
4
|
+
Store, Session, SystemContextRegistry, KnowledgeIndex, PromptFn, SessionRunner, Usage, HookRegistrar,
|
|
5
5
|
TypeScriptStripper, ToolTypeIndex, ToolPresenter, SteeringPolicy,
|
|
6
6
|
} from './types.js';
|
|
7
|
-
import type { Routed } from './broadcast.js';
|
|
8
7
|
import type { Notifier } from './notify.js';
|
|
9
|
-
import type { HookRegistry } from './hooks.js';
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
/**
|
|
10
|
+
* The plugin API contract version: this package's `major.minor`, and nothing else. It sat at `'0.1'`
|
|
11
|
+
* while the package shipped 0.3.x, so it conveyed no information and could not be reasoned about at a
|
|
12
|
+
* version boundary — the point at which it is the only thing every third-party plugin hardcodes.
|
|
13
|
+
*
|
|
14
|
+
* The gate (`checkApiVersion`, in core's registry) reads it as: **major must match exactly** — a
|
|
15
|
+
* mismatch is a hard load failure — and a plugin declaring a *newer minor* than the runtime warns and
|
|
16
|
+
* loads, since it may reach for something absent. Declare it as `apiVersion: PLUGIN_API_VERSION` and it
|
|
17
|
+
* stays right for free; that is what every plugin in this repo does.
|
|
18
|
+
*
|
|
19
|
+
* Bumping 0.1 → 0.4 breaks nothing: at 0.x the major is `'0'` either way, and an older declared minor
|
|
20
|
+
* does not warn. It matters at 1.0.0, where a plugin still declaring `'0.x'` will fail the major check
|
|
21
|
+
* loudly instead of loading against a contract it was never built for.
|
|
22
|
+
*/
|
|
23
|
+
export const PLUGIN_API_VERSION = '0.4';
|
|
12
24
|
|
|
13
25
|
// ── Sub-runner types ──────────────────────────────────────────────────────────
|
|
14
26
|
|
|
@@ -17,13 +29,18 @@ export interface CompletionRequest {
|
|
|
17
29
|
messages: Message[];
|
|
18
30
|
system?: string;
|
|
19
31
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
32
|
+
* Per-call parameter overrides, shallow-merged over the named provider's own `parameters` (request
|
|
33
|
+
* wins). For *transient* model behaviour — a lower `temperature` to classify, `thinking` off for a
|
|
34
|
+
* cheap sub-call, a tighter `maxTokens` on a call whose output is parsed rather than shown.
|
|
35
|
+
*
|
|
36
|
+
* The alternative is a near-duplicate provider profile per behaviour, identical to its sibling but
|
|
37
|
+
* for one field, which pushes a caller's transient concern into global, user-editable config and
|
|
38
|
+
* multiplies the profiles a user has to understand. So: **durable properties of a model belong in
|
|
39
|
+
* the provider profile; a single call's behaviour belongs here.** If the same override appears at
|
|
40
|
+
* every call site of a provider, that is the signal it was a profile all along.
|
|
41
|
+
*
|
|
42
|
+
* Honoured by both hosts (CLI + web-bundle), which resolve the merge before handing the config to
|
|
43
|
+
* the adapter; an unrecognised key is the adapter's business, exactly as in a profile.
|
|
27
44
|
*/
|
|
28
45
|
parameters?: Partial<ModelParameters>;
|
|
29
46
|
signal?: AbortSignal;
|
|
@@ -38,8 +55,7 @@ export interface SingleTurnRequest {
|
|
|
38
55
|
provider: string;
|
|
39
56
|
prompt: string;
|
|
40
57
|
system?: string;
|
|
41
|
-
/**
|
|
42
|
-
* anti-pattern caveat — prefer a dedicated provider profile over per-call overrides. */
|
|
58
|
+
/** Forwarded verbatim to {@link CompletionRequest.parameters} — same merge, same guidance. */
|
|
43
59
|
parameters?: Partial<ModelParameters>;
|
|
44
60
|
signal?: AbortSignal;
|
|
45
61
|
}
|
|
@@ -99,11 +115,15 @@ export interface PluginSelf {
|
|
|
99
115
|
// ── Services container ────────────────────────────────────────────────────────
|
|
100
116
|
|
|
101
117
|
/**
|
|
118
|
+
* One of matbot's five open-registry augmentation points — same technique at each; see
|
|
119
|
+
* docs/DEVELOPING.md *Open-registry augmentation* for the shared shape and the rules that follow from it.
|
|
120
|
+
*
|
|
102
121
|
* The registry bucket: the swappable, registerable services, keyed by interface name. This is the
|
|
103
122
|
* `keyof` domain of {@link MatbotRuntime.register}/`get`, and the surface third-party plugins augment
|
|
104
123
|
* (`declare module '@matatbread/matbot-plugin-api' { interface MatbotServices { Foo?: Foo } }`). The
|
|
105
|
-
*
|
|
106
|
-
* optional and simply drops. Read each as
|
|
124
|
+
* four swap-members (`StorageBackend`, `KnowledgeIndex`, `Vault`, `Notifier`) carry a host boot default
|
|
125
|
+
* and revert to it when unregistered; an augmented service is optional and simply drops. Read each as
|
|
126
|
+
* a member (`services.KnowledgeIndex`); swap with register().
|
|
107
127
|
*/
|
|
108
128
|
export interface MatbotServices {
|
|
109
129
|
readonly StorageBackend?: StorageBackend | undefined;
|
|
@@ -255,7 +275,7 @@ export interface MatbotRuntime {
|
|
|
255
275
|
* runSession directly, so concurrent submits queue instead of clobbering the session. */
|
|
256
276
|
readonly run?: SessionRunner | undefined;
|
|
257
277
|
readonly files?: FileStore;
|
|
258
|
-
readonly hooks:
|
|
278
|
+
readonly hooks: HookRegistrar;
|
|
259
279
|
readonly tools: ToolRegistry;
|
|
260
280
|
readonly systemContext: SystemContextRegistry;
|
|
261
281
|
/** Default working directory for tool execution. Plugins that create servers should forward this to tool contexts. */
|
|
@@ -274,74 +294,6 @@ export interface MatbotRuntime {
|
|
|
274
294
|
isSubAgent(): boolean;
|
|
275
295
|
}
|
|
276
296
|
|
|
277
|
-
/**
|
|
278
|
-
* Wrap a services object so every registered service is reachable as a member (`services.InterfaceName`),
|
|
279
|
-
* not only via `services.get('InterfaceName')` — one access surface for base members and plugin-registered
|
|
280
|
-
* services alike. A member read of a key the object doesn't carry falls back to its own `get()` (the
|
|
281
|
-
* registry), so the augmentation that declares `InterfaceName?: InterfaceName` is also the access path;
|
|
282
|
-
* the optional `?` is the single call-site signal of "may be absent, null-check it". Assignment throws,
|
|
283
|
-
* directing callers to `register()` (the swap-aware write path). Applied to both the host services object
|
|
284
|
-
* and the per-plugin scoped object, so plugins see the same surface the host does.
|
|
285
|
-
*/
|
|
286
|
-
export function unifyServices(services: MatbotMachine): MatbotMachine {
|
|
287
|
-
return new Proxy(services, {
|
|
288
|
-
get(target, key, receiver) {
|
|
289
|
-
if (Reflect.has(target, key)) return Reflect.get(target, key, receiver);
|
|
290
|
-
return typeof key === 'string' ? target.get(key as keyof MatbotServices) : undefined;
|
|
291
|
-
},
|
|
292
|
-
has(target, key) {
|
|
293
|
-
if (Reflect.has(target, key)) return true;
|
|
294
|
-
return typeof key === 'string' && target.get(key as keyof MatbotServices) !== undefined;
|
|
295
|
-
},
|
|
296
|
-
set(_target, key) {
|
|
297
|
-
throw new Error(
|
|
298
|
-
`Cannot assign services.${String(key)} directly — use services.register('${String(key)}', impl) to register or replace a service.`,
|
|
299
|
-
);
|
|
300
|
-
},
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// ── Capture-safe swap proxies ───────────────────────────────────────────────────
|
|
305
|
-
|
|
306
|
-
export type SwapFn<T extends object> = (next: T) => void;
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* A capture-safe forwarding proxy: every trap routes to whatever `getCurrent()` returns *now*, so a
|
|
310
|
-
* reference captured before a register()-driven swap keeps resolving to the live impl. getPrototypeOf
|
|
311
|
-
* is forwarded so `instanceof` sees the real impl (the StorageBackend identity checks depend on it);
|
|
312
|
-
* ownKeys + getOwnPropertyDescriptor keep object spread faithful. Methods bind to the current impl,
|
|
313
|
-
* not the proxy. A nullish current (an optional service with nothing registered yet) reads as empty.
|
|
314
|
-
*/
|
|
315
|
-
export function forwardingProxy<T extends object>(getCurrent: () => T | undefined): T {
|
|
316
|
-
return new Proxy({} as T, {
|
|
317
|
-
get(_t, prop) {
|
|
318
|
-
const cur = getCurrent();
|
|
319
|
-
if (cur === undefined) return undefined;
|
|
320
|
-
const val = Reflect.get(cur, prop, cur);
|
|
321
|
-
return typeof val === 'function' ? (val as (...a: unknown[]) => unknown).bind(cur) : val;
|
|
322
|
-
},
|
|
323
|
-
has(_t, prop) { const cur = getCurrent(); return cur !== undefined && Reflect.has(cur, prop); },
|
|
324
|
-
getPrototypeOf() { const cur = getCurrent(); return cur === undefined ? null : Reflect.getPrototypeOf(cur); },
|
|
325
|
-
ownKeys() { const cur = getCurrent(); return cur === undefined ? [] : Reflect.ownKeys(cur); },
|
|
326
|
-
getOwnPropertyDescriptor(_t, prop) {
|
|
327
|
-
const cur = getCurrent();
|
|
328
|
-
if (cur === undefined) return undefined;
|
|
329
|
-
const d = Reflect.getOwnPropertyDescriptor(cur, prop);
|
|
330
|
-
if (d !== undefined) d.configurable = true; // Proxy invariant: props absent from the {} target must be configurable.
|
|
331
|
-
return d;
|
|
332
|
-
},
|
|
333
|
-
});
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
/**
|
|
337
|
-
* Returns [proxy, swap]: the Store/FileStore handle plugins capture, plus the fn register() calls to
|
|
338
|
-
* repoint it at a new backend's store. Built on forwardingProxy so capture-safety is uniform.
|
|
339
|
-
*/
|
|
340
|
-
export function makeSwappable<T extends object>(initial: T): [T, SwapFn<T>] {
|
|
341
|
-
let current = initial;
|
|
342
|
-
return [forwardingProxy<T>(() => current), (next: T) => { current = next; }];
|
|
343
|
-
}
|
|
344
|
-
|
|
345
297
|
// ── Mount table ─────────────────────────────────────────────────────────────────
|
|
346
298
|
|
|
347
299
|
/** The machine with one registry key narrowed to present — what a keyed mount handler receives. */
|
|
@@ -355,7 +307,10 @@ export interface MountConsumeOptions<K extends keyof MatbotServices> {
|
|
|
355
307
|
* (then on each later remount). Off by default — a cacher that did its initial load in setup() wants
|
|
356
308
|
* only future transitions; a deferred dependency wants the latch. */
|
|
357
309
|
readonly replay?: boolean;
|
|
358
|
-
/** Ends the subscription
|
|
310
|
+
/** Ends the subscription *earlier* than the observing plugin's unload. Optional because it is a
|
|
311
|
+
* narrowing convenience, not the cleanup path: the host binds every plugin-scoped observe() to that
|
|
312
|
+
* plugin's load extent, so an interest can never outlive its owner. Pass one only when the
|
|
313
|
+
* subscription's life is shorter than the plugin's (a per-session cache, a one-shot latch). */
|
|
359
314
|
readonly signal?: AbortSignal;
|
|
360
315
|
/** The *dependency's* teardown: fired when `key` is committed-unloaded (removed and not replaced by
|
|
361
316
|
* the quiescent edge) while this consumer lives on. The stream continues — a later remount re-fires
|
|
@@ -373,112 +328,7 @@ export interface Mounted {
|
|
|
373
328
|
): void;
|
|
374
329
|
}
|
|
375
330
|
|
|
376
|
-
/** The host-side mount table: {@link Mounted} for plugins, plus the producer half the host drives from
|
|
377
|
-
* its register/unregister and quiescent-edge flush. */
|
|
378
|
-
export interface MountTable {
|
|
379
|
-
readonly mounted: Mounted;
|
|
380
|
-
/** Record that a key's presence may have changed since the last edge (called by register/unregister). */
|
|
381
|
-
markDirty(key: keyof MatbotServices): void;
|
|
382
|
-
/** At a quiescent edge, compute each dirty key's net presence transition and multicast it. */
|
|
383
|
-
flush(): void;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
interface MountInterest {
|
|
387
|
-
readonly handler: (machine: MatbotMachine) => void | Promise<void>;
|
|
388
|
-
readonly onUnmount: ((machine: MatbotMachine) => void | Promise<void>) | undefined;
|
|
389
|
-
readonly signal: AbortSignal | undefined;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
function reportMountHandlerError(e: unknown): void {
|
|
393
|
-
console.error('[matbot] mounted handler threw:', e instanceof Error ? e.message : e);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
/**
|
|
397
|
-
* Build a {@link MountTable} over a lazily-read machine. Notifications batch to the quiescent edge
|
|
398
|
-
* ({@link flush}), where each dirty key's net presence (absent→present = mount, present→present =
|
|
399
|
-
* remount, present→absent = committed unload) is multicast to that key's subscribers. The clock holds
|
|
400
|
-
* the last-committed presence per key, so a reload collapses to one remount and a committed unload is
|
|
401
|
-
* well-defined. Presence is read by member access on the unified machine, which resolves both the core
|
|
402
|
-
* getters (StorageBackend/Vault/KnowledgeIndex) and the registry-backed augmented keys.
|
|
403
|
-
*/
|
|
404
|
-
export function createMountTable(getMachine: () => MatbotMachine): MountTable {
|
|
405
|
-
const interests = new Map<string, Set<MountInterest>>();
|
|
406
|
-
const committed = new Map<string, boolean>(); // last-committed presence per key (the clock)
|
|
407
|
-
const dirty = new Set<string>();
|
|
408
|
-
|
|
409
|
-
const present = (key: string): boolean => (getMachine() as unknown as Record<string, unknown>)[key] !== undefined;
|
|
410
|
-
|
|
411
|
-
const run = (fn: (machine: MatbotMachine) => void | Promise<void>, machine: MatbotMachine): void => {
|
|
412
|
-
try {
|
|
413
|
-
const r = fn(machine);
|
|
414
|
-
if (r instanceof Promise) r.catch(reportMountHandlerError);
|
|
415
|
-
} catch (e) { reportMountHandlerError(e); }
|
|
416
|
-
};
|
|
417
|
-
|
|
418
|
-
const mounted: Mounted = {
|
|
419
|
-
observe(options, handler) {
|
|
420
|
-
const { key, replay, signal, onUnmount } = options;
|
|
421
|
-
if (signal?.aborted === true) return;
|
|
422
|
-
const interest: MountInterest = {
|
|
423
|
-
handler: handler as MountInterest['handler'],
|
|
424
|
-
onUnmount: onUnmount as MountInterest['onUnmount'],
|
|
425
|
-
signal,
|
|
426
|
-
};
|
|
427
|
-
let set = interests.get(key);
|
|
428
|
-
if (set === undefined) { set = new Set(); interests.set(key, set); }
|
|
429
|
-
const subs = set;
|
|
430
|
-
subs.add(interest);
|
|
431
|
-
signal?.addEventListener('abort', () => { subs.delete(interest); }, { once: true });
|
|
432
|
-
// Replay on the next microtask (async-iterator parity — never inline in the consume() frame).
|
|
433
|
-
// Reads the live machine, not `committed`: replay is "current state", separate from the clock.
|
|
434
|
-
if (replay === true) queueMicrotask(() => {
|
|
435
|
-
if (signal?.aborted === true) return;
|
|
436
|
-
if (present(key as string)) run(interest.handler, getMachine());
|
|
437
|
-
});
|
|
438
|
-
},
|
|
439
|
-
};
|
|
440
|
-
|
|
441
|
-
return {
|
|
442
|
-
mounted,
|
|
443
|
-
markDirty(key) { dirty.add(key as string); },
|
|
444
|
-
flush() {
|
|
445
|
-
if (dirty.size === 0) return;
|
|
446
|
-
const keys = [...dirty];
|
|
447
|
-
dirty.clear();
|
|
448
|
-
const machine = getMachine();
|
|
449
|
-
for (const key of keys) {
|
|
450
|
-
const before = committed.get(key) ?? false;
|
|
451
|
-
const after = present(key);
|
|
452
|
-
committed.set(key, after);
|
|
453
|
-
const subs = interests.get(key);
|
|
454
|
-
if (subs === undefined) continue;
|
|
455
|
-
if (after) {
|
|
456
|
-
for (const i of subs) run(i.handler, machine); // mount / remount
|
|
457
|
-
} else if (before) {
|
|
458
|
-
for (const i of subs) if (i.onUnmount !== undefined) run(i.onUnmount, machine); // committed unload
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
},
|
|
462
|
-
};
|
|
463
|
-
}
|
|
464
331
|
|
|
465
|
-
/**
|
|
466
|
-
* Build the one-message CompletionRequest for a {@link MatbotRuntime.singleTurn} call, hiding the
|
|
467
|
-
* otherwise-mandatory and meaningless Message fields (id/traceId/createdAt) an out-of-band one-shot
|
|
468
|
-
* has no use for. Pure; the host invokes its own complete() with the result.
|
|
469
|
-
*/
|
|
470
|
-
export function singleTurnRequest(req: SingleTurnRequest): CompletionRequest {
|
|
471
|
-
return {
|
|
472
|
-
provider: req.provider,
|
|
473
|
-
messages: [{
|
|
474
|
-
id: '', traceId: '', createdAt: new Date().toISOString(), role: 'user',
|
|
475
|
-
content: [{ type: 'text', text: req.prompt }],
|
|
476
|
-
}],
|
|
477
|
-
...(req.system !== undefined ? { system: req.system } : {}),
|
|
478
|
-
...(req.parameters !== undefined ? { parameters: req.parameters } : {}),
|
|
479
|
-
...(req.signal !== undefined ? { signal: req.signal } : {}),
|
|
480
|
-
};
|
|
481
|
-
}
|
|
482
332
|
|
|
483
333
|
// ── Factory types ─────────────────────────────────────────────────────────────
|
|
484
334
|
|
package/src/session.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Session } from './types.js';
|
|
1
|
+
import type { MessageContent, Session } from './types.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* A session's `updatedAt` invariant: the timestamp of its last message, or its own `createdAt` when it
|
|
@@ -15,3 +15,32 @@ export function lastActivityAt(session: Session): string {
|
|
|
15
15
|
const last = session.messages[session.messages.length - 1];
|
|
16
16
|
return last ? last.createdAt : session.createdAt;
|
|
17
17
|
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Index of the turn's user message — the message a durable fold attaches to, and the point a retraction
|
|
21
|
+
* pops back to. `-1` when the session has none.
|
|
22
|
+
*/
|
|
23
|
+
export function lastUserIndex(session: Session): number {
|
|
24
|
+
return session.messages.findLastIndex(m => m.role === 'user');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Append `blocks` to the turn's user message, returning a new session. This is what "durable" means
|
|
29
|
+
* everywhere it appears: unlike `ephemeral` (tail-folded into the outgoing copy and discarded), the blocks
|
|
30
|
+
* become part of the user turn — persisted, visible to a reader, and carried by every subsequent provider
|
|
31
|
+
* call in the turn and after it.
|
|
32
|
+
*
|
|
33
|
+
* With no user message the blocks are **dropped rather than orphaned**: there is nothing they could belong
|
|
34
|
+
* to, and appending a message of their own would change the role sequence the adapters depend on.
|
|
35
|
+
*
|
|
36
|
+
* Shared because the three callers must agree: the `screen` hook fold, the runner's raced-verdict
|
|
37
|
+
* correction, and the pump's retract-and-rerun rebase. They were three independent copies of one
|
|
38
|
+
* `findLastIndex` + splice, in two different packages.
|
|
39
|
+
*/
|
|
40
|
+
export function foldOntoUserTurn(session: Session, blocks: readonly MessageContent[]): Session {
|
|
41
|
+
if (blocks.length === 0) return session;
|
|
42
|
+
const idx = lastUserIndex(session);
|
|
43
|
+
if (idx < 0) return session;
|
|
44
|
+
return { ...session, messages: session.messages.map((m, i) =>
|
|
45
|
+
i === idx ? { ...m, content: [...m.content, ...blocks] } : m) };
|
|
46
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { FileHandle } from './files.js';
|
|
2
|
+
import type { MessageContent, Session } from './messages.js';
|
|
3
|
+
import type { Usage } from './provider.js';
|
|
4
|
+
|
|
5
|
+
// ── Pipeline events ─────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A turn's own events. `traceId` — the turn that produced it — is the correlation key a frontend routes
|
|
9
|
+
* on (see `SessionRunner`), and every arm here carries it. Grouped by what a consumer does with them:
|
|
10
|
+
* streamed model output, tool progress, live delivery of something already persisted, and the terminals.
|
|
11
|
+
*
|
|
12
|
+
* A turn ends with **exactly one** terminal: `done`, `aborted` (interrupted mid-flight), or `error`.
|
|
13
|
+
*/
|
|
14
|
+
export type TurnEvent =
|
|
15
|
+
// ── streamed model output ──
|
|
16
|
+
| { type: 'text-delta'; delta: string; traceId: string }
|
|
17
|
+
| { type: 'thinking'; delta: string; traceId: string }
|
|
18
|
+
| { type: 'system-context'; text: string; traceId: string }
|
|
19
|
+
| ({ type: 'usage'; traceId: string } & Usage)
|
|
20
|
+
|
|
21
|
+
// ── tool execution ──
|
|
22
|
+
| { type: 'tool:start'; callId: string; name: string; input: unknown; traceId: string }
|
|
23
|
+
| { type: 'tool:stdout'; callId: string; chunk: string; traceId: string }
|
|
24
|
+
| { type: 'tool:stderr'; callId: string; chunk: string; traceId: string }
|
|
25
|
+
| { type: 'tool:progress'; callId: string; pct: number; message?: string; traceId: string }
|
|
26
|
+
| { type: 'tool:end'; callId: string; result: unknown; isError: boolean; traceId: string }
|
|
27
|
+
|
|
28
|
+
// ── live delivery of state that is ALREADY persisted; never the source of truth ──
|
|
29
|
+
| { type: 'file'; handle: FileHandle; traceId: string }
|
|
30
|
+
// Machine-authored content folded onto the running turn's user message (a `screen` hook's
|
|
31
|
+
// `durable` result — e.g. a fired `contextual` trigger), carried live so a frontend draws it
|
|
32
|
+
// immediately. The blocks are already persisted in that user message (origin: 'robo'); this is
|
|
33
|
+
// purely the live-delivery channel, so a live draw matches the reload (which splits the user
|
|
34
|
+
// turn's robo blocks into their own agent-side bubble).
|
|
35
|
+
| { type: 'robo-user'; content: MessageContent[]; traceId: string }
|
|
36
|
+
// Marker blocks appended to the session this turn (e.g. the dispatcher's record of a hook that
|
|
37
|
+
// threw), carried live so a frontend renders them without waiting for a session reload. The blocks
|
|
38
|
+
// are already persisted in the session; this event is purely the live-delivery channel.
|
|
39
|
+
| { type: 'marker'; content: MessageContent[]; traceId: string }
|
|
40
|
+
|
|
41
|
+
// ── queue disposition ──
|
|
42
|
+
// A queued (not-yet-running) submission, carried on the stream so a frontend renders it as part
|
|
43
|
+
// of the live "delta" (everything after the last committed message), never from stored state.
|
|
44
|
+
// `queued` is the number of submissions ahead of it (0 ⇒ about to run). Emitted live on enqueue
|
|
45
|
+
// and replayed (in queue order) to anyone subscribing mid-flight.
|
|
46
|
+
| { type: 'queued'; content: MessageContent[]; queued: number; concatQueue: boolean; traceId: string; rootTraceId: string }
|
|
47
|
+
// A mid-turn steer that INTERRUPTED the running turn (see `SteeringPolicy`). Emitted synchronously
|
|
48
|
+
// when the steer is accepted, so the new user bubble lands in correct stream order and a frontend
|
|
49
|
+
// knows the imminent `aborted` (reason 'steer') on `interruptedTraceId` is a yield, not a dead-end:
|
|
50
|
+
// keep that turn's partial work rendered and expect a continuation. `traceId`/`rootTraceId` identify
|
|
51
|
+
// the steer submission itself (the continuation turn). Late subscribers reconstruct the bubble from
|
|
52
|
+
// the pump's per-turn `queued` replay seed / committed store order — this is purely live delivery.
|
|
53
|
+
| { type: 'steer'; content: MessageContent[]; interruptedTraceId: string; traceId: string; rootTraceId: string }
|
|
54
|
+
|
|
55
|
+
// ── terminals: exactly one per turn ──
|
|
56
|
+
| { type: 'done'; session: Session; traceId: string }
|
|
57
|
+
| { type: 'aborted'; reason: string; session: Session; traceId: string }
|
|
58
|
+
| { type: 'error'; error: string; traceId: string }
|
|
59
|
+
// A queued submission dropped before it ever ran (e.g. by `SessionRunner.abort`). It carries no
|
|
60
|
+
// session because nothing ran and so nothing was persisted — but it does have a traceId: the
|
|
61
|
+
// submission it cancels was assigned one at enqueue, and a frontend needs it to retire that bubble.
|
|
62
|
+
| { type: 'cancelled'; sessionId: string; traceId: string };
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Events about the *session*, not any one turn — hence keyed on `sessionId` with no `traceId`. Split from
|
|
66
|
+
* {@link TurnEvent} so that absence is type-checkable rather than a comment: `idle` was the single arm of
|
|
67
|
+
* a 20-arm union without a `traceId`, which every consumer had to know by having read the note saying so.
|
|
68
|
+
*/
|
|
69
|
+
export type SessionEvent =
|
|
70
|
+
// The runner has fully drained its queue. Emitted once per busy period, *after* `running` flips false,
|
|
71
|
+
// so a consumer can map it to an authoritative busy→idle transition without racing the internal flip.
|
|
72
|
+
| { type: 'idle'; sessionId: string };
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The streaming output of a session: its turns' events, plus the session-level ones.
|
|
76
|
+
*
|
|
77
|
+
* Consumers that only ever see one turn (a tool reporting progress, a per-turn frontend) should narrow to
|
|
78
|
+
* {@link TurnEvent} and get `traceId` unconditionally; a subscriber to a whole session takes this union
|
|
79
|
+
* and must handle `idle`. Switching on `type` works the same either way.
|
|
80
|
+
*/
|
|
81
|
+
export type PipelineEvent = TurnEvent | SessionEvent;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ISODate, MimeType } from './primitives.js';
|
|
2
|
+
|
|
3
|
+
// ── Files ─────────────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export interface FileMetaData {
|
|
6
|
+
id: string;
|
|
7
|
+
version: string;
|
|
8
|
+
name: string;
|
|
9
|
+
mimeType: MimeType;
|
|
10
|
+
size: number;
|
|
11
|
+
createdAt: ISODate;
|
|
12
|
+
sessionId?: string;
|
|
13
|
+
messageId?: string;
|
|
14
|
+
namespace?: string;
|
|
15
|
+
/** Whether this file may be served at a public URL. Default-deny: absent ⇒ not servable. The
|
|
16
|
+
* flag rides in the file's own persisted metadata (no separate allow-list), so the same read that
|
|
17
|
+
* resolves a request also gates it. Producers opt in per put (the workspace tool sets it true). */
|
|
18
|
+
allowed?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface FileHandle extends FileMetaData {
|
|
22
|
+
stream(signal?: AbortSignal): AsyncIterable<Uint8Array>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface FileFilter {
|
|
26
|
+
sessionId?: string;
|
|
27
|
+
mimeType?: string;
|
|
28
|
+
namespace?: string;
|
|
29
|
+
createdAfter?: ISODate;
|
|
30
|
+
createdBefore?: ISODate;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface FileStore {
|
|
34
|
+
/** Store a file. When `name` is provided, upserts by (name + namespace); otherwise always creates a new entry. */
|
|
35
|
+
put(
|
|
36
|
+
name: string | undefined,
|
|
37
|
+
mimeType: MimeType,
|
|
38
|
+
data: AsyncIterable<Uint8Array>,
|
|
39
|
+
meta?: { sessionId?: string; messageId?: string; namespace?: string; allowed?: boolean }
|
|
40
|
+
): Promise<FileHandle>;
|
|
41
|
+
get(id: string): Promise<FileHandle | null>;
|
|
42
|
+
getByName(name: string, namespace?: string): Promise<FileHandle | null>;
|
|
43
|
+
delete(id: string): Promise<void>;
|
|
44
|
+
list(filter?: FileFilter): AsyncIterable<FileHandle>;
|
|
45
|
+
putTemp(name: string, mimeType: MimeType, data: AsyncIterable<Uint8Array>): Promise<FileHandle>;
|
|
46
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// ── Frontend ──────────────────────────────────────────────────────────────────
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Passed to `services.registerFrontend()` by a plugin declaring itself a frontend. A frontend
|
|
5
|
+
* owns its own I/O (an HTTP server, a bot connection, a REPL); matbot only needs to know it
|
|
6
|
+
* exists. This object is the growth point for frontend-level capability advertisement
|
|
7
|
+
* (accepted/produced MIME types, size limits, …) as media support lands.
|
|
8
|
+
*/
|
|
9
|
+
export interface FrontendInfo {
|
|
10
|
+
name: string;
|
|
11
|
+
}
|