@matatbread/matbot-plugin-api 0.3.7 → 0.3.8
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 +1 -1
- package/src/index.ts +1 -0
- package/src/notify.ts +239 -0
- package/src/plugin.ts +13 -11
- package/src/store-query.ts +12 -0
- package/src/types.ts +23 -35
package/package.json
CHANGED
package/src/index.ts
CHANGED
package/src/notify.ts
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import type { Principal, Store } from './types.js';
|
|
2
|
+
import { createBroadcaster } from './broadcast.js';
|
|
3
|
+
import { tryCurrentPrincipal } from './principal-context.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* What every notification carries, whoever produced it and however it arrived.
|
|
7
|
+
*
|
|
8
|
+
* Two independent axes ride here, and conflating them is the mistake this shape exists to prevent:
|
|
9
|
+
*
|
|
10
|
+
* - **attribution** — `instance`/`plugin`/`source`: who *produced* the fact. Drives per-producer
|
|
11
|
+
* filtering, and lets a bridge suppress its own echo.
|
|
12
|
+
* - **ownership** — `principal`: whose *data* the fact is about. This is the visibility axis, the
|
|
13
|
+
* input to `WatchVisibility.visible`. A notification with no principal is a global fact.
|
|
14
|
+
*
|
|
15
|
+
* A producer never sets `instance`: undefined means "this matbot". A bridge stamps it on ingress, so a
|
|
16
|
+
* forwarded event stays attributable to the server that produced it and never loops back out.
|
|
17
|
+
*
|
|
18
|
+
* `kind` is deliberately absent here: it is not something an arm declares but something its
|
|
19
|
+
* {@link Notifications} key *is*, grafted on by {@link Notification}. Declaring it on the arm as well
|
|
20
|
+
* would be the same string written twice with nothing keeping the two honest.
|
|
21
|
+
*/
|
|
22
|
+
export interface NotificationBase {
|
|
23
|
+
/** Producing plugin's name; `core` is reserved for the host and core itself. Defaulted from the
|
|
24
|
+
* emitting plugin's scope, so an ordinary producer never sets it. It stays overridable rather than
|
|
25
|
+
* enforced because relaying another producer's event is a legitimate act — that is exactly what a
|
|
26
|
+
* bridge does with a notification it received from another instance. */
|
|
27
|
+
readonly plugin: string;
|
|
28
|
+
/** Producer-defined sub-channel within that plugin (`tools`, `save`, `job-output`, …). Opaque to core. */
|
|
29
|
+
readonly source: string;
|
|
30
|
+
/** Originating matbot instance. Undefined ⇒ produced locally. Set only by a bridge, on ingress. */
|
|
31
|
+
readonly instance?: string;
|
|
32
|
+
/** Whose data this is about — the visibility axis, NOT attribution. Undefined ⇒ a global fact. */
|
|
33
|
+
readonly principal?: Principal;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The thing addressed by `(namespace, id)` is no longer what you last read — re-read it.
|
|
38
|
+
*
|
|
39
|
+
* Deliberately named for that contract and not for a medium: this is published by `Store` writes
|
|
40
|
+
* (via {@link notifyingStore}), by `FileStore` writes, and by a share/unshare that passes through
|
|
41
|
+
* neither. **Use it whenever the change is an invalidation of an addressable item**, wherever the item
|
|
42
|
+
* lives — including a plugin watching something matbot doesn't own. Define a `kind` of your own only
|
|
43
|
+
* when you carry information a consumer *cannot* get by re-reading (progress, a measurement, an
|
|
44
|
+
* external event); squeezing that in here as `detail` is the mistake this note exists to prevent.
|
|
45
|
+
*
|
|
46
|
+
* Carries identity, never the value: an event is queued per subscriber and is stale the moment a
|
|
47
|
+
* concurrent writer lands, so a consumer that acts on a payload races the source it is mirroring.
|
|
48
|
+
*
|
|
49
|
+
* `namespace` is the ROUTING namespace (`files`, `skills`, `sessions`, a document namespace) — the axis
|
|
50
|
+
* a profile isolates and `visible` routes on — not a content sub-namespace. `detail` is advisory only:
|
|
51
|
+
* a cheap payload for an in-place UI update (a file's size) that a consumer may render but must never
|
|
52
|
+
* treat as authoritative.
|
|
53
|
+
*/
|
|
54
|
+
export interface ItemChange extends NotificationBase {
|
|
55
|
+
readonly namespace: string;
|
|
56
|
+
readonly id: string;
|
|
57
|
+
readonly operation: 'saved' | 'deleted';
|
|
58
|
+
readonly detail?: unknown;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A process-global registry gained or lost a member (tools, plugins). No owner, no item identity. */
|
|
62
|
+
export interface RegistryChange extends NotificationBase {
|
|
63
|
+
readonly registry: string;
|
|
64
|
+
readonly name: string;
|
|
65
|
+
readonly operation: 'added' | 'removed';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The wire tags for the two kinds defined here. A `kind` is a globally-scoped token — unlike a type
|
|
70
|
+
* name, which an importer can rename out of a clash — so these exist to give one back: import the const
|
|
71
|
+
* under whatever local name suits (`import { ItemChangeKind as Changed }`) and use it as a `case` label
|
|
72
|
+
* or in a filter. `satisfies` ties each to a real key, so a typo fails the build here instead of
|
|
73
|
+
* becoming a predicate that silently never matches.
|
|
74
|
+
*/
|
|
75
|
+
export const ItemChangeKind = '@matatbread/matbot-plugin-api#ItemChange' satisfies keyof Notifications;
|
|
76
|
+
export const RegistryChangeKind = '@matatbread/matbot-plugin-api#RegistryChange' satisfies keyof Notifications;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The registry of notification shapes, keyed by `kind` — augment it exactly like `MatbotServices` /
|
|
80
|
+
* `MarkerData` / `ToolContracts` to add your own:
|
|
81
|
+
*
|
|
82
|
+
* export interface JobProgress extends NotificationBase { done: number; total: number }
|
|
83
|
+
*
|
|
84
|
+
* declare module '@matatbread/matbot-plugin-api' {
|
|
85
|
+
* interface Notifications { '@fnarr/jobs#JobProgress': JobProgress }
|
|
86
|
+
* }
|
|
87
|
+
*
|
|
88
|
+
* **The key is `<package-name>#<InterfaceName>` — no translation, no invention.** As with the service
|
|
89
|
+
* registry, the string is the erasure-time stand-in for a type identity; unlike a type name, it is
|
|
90
|
+
* globally scoped and an importer cannot rename it out of a collision, so the package name (already
|
|
91
|
+
* unique, npm guarantees it) qualifies it. The prefix names the package that *defines* the shape, never
|
|
92
|
+
* the one emitting it — `plugin` is the emitter, and four different plugins emit `ItemChange`. The rule
|
|
93
|
+
* is enforced by {@link Qualified} at the `notify` call; export the key as a const beside the interface
|
|
94
|
+
* (see {@link ItemChangeKind}) so consumers get a renameable handle back.
|
|
95
|
+
*
|
|
96
|
+
* **The set is open at runtime.** A kind can arrive from a plugin that isn't in this build, or bridged
|
|
97
|
+
* from another server — so a `switch (n.kind)` over notifications must always have a `default`, and
|
|
98
|
+
* exhaustiveness checking is unsound here (unlike a closed discriminated union). A bridge that wants a
|
|
99
|
+
* foreign kind typed augments this itself; otherwise it validates at its own boundary, like any other
|
|
100
|
+
* system edge.
|
|
101
|
+
*/
|
|
102
|
+
export interface Notifications {
|
|
103
|
+
'@matatbread/matbot-plugin-api#ItemChange': ItemChange;
|
|
104
|
+
'@matatbread/matbot-plugin-api#RegistryChange': RegistryChange;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** A notification as a consumer sees it: the arm, plus the `kind` its registry key supplies. Grafting
|
|
108
|
+
* `kind` on here rather than declaring it per-arm is what makes "the tag equals the key" unrepresentable
|
|
109
|
+
* rather than a convention to police. */
|
|
110
|
+
export type Notification = {
|
|
111
|
+
[K in keyof Notifications]: Notifications[K] & { readonly kind: K };
|
|
112
|
+
}[keyof Notifications];
|
|
113
|
+
|
|
114
|
+
/** One arm as a standalone type, for the rare annotation that wants a single kind rather than the union. */
|
|
115
|
+
export type Notified<K extends keyof Notifications> = Notifications[K] & { readonly kind: K };
|
|
116
|
+
|
|
117
|
+
/** Fails an arm whose key isn't `<package>#<Name>`. Intersected into {@link NotifyInput}, so a malformed
|
|
118
|
+
* key surfaces at the `notify` call — in the compilation that declared the augmentation, since the
|
|
119
|
+
* mapped type is resolved at the use site. Nothing at runtime; see `createNotifier` for the producers
|
|
120
|
+
* TypeScript can't see. */
|
|
121
|
+
type Qualified<K> = K extends `${string}#${string}` ? unknown
|
|
122
|
+
: { 'ERROR — notification kind must be `<package-name>#<InterfaceName>`': K };
|
|
123
|
+
|
|
124
|
+
/** What a producer passes to {@link Notifier.notify}: any registered arm, plus its key as `kind`, minus
|
|
125
|
+
* the `plugin` the runtime stamps. Mapped over the registry (not `Omit<Notification, …>`) so it
|
|
126
|
+
* distributes across arms and picks up augmentations. `plugin` stays settable for the one caller that
|
|
127
|
+
* legitimately relays another's event — a bridge replaying a remote notification. */
|
|
128
|
+
export type NotifyInput = {
|
|
129
|
+
[K in keyof Notifications]: Qualified<K> & Omit<Notifications[K], 'plugin'> & { readonly kind: K; plugin?: string };
|
|
130
|
+
}[keyof Notifications];
|
|
131
|
+
|
|
132
|
+
/** Per-subscriber predicate. Applied at emit time, so a subscriber never queues what it didn't ask for. */
|
|
133
|
+
export type NotificationFilter = (notification: Notification) => boolean;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The notification bus: one fan-out for every "something changed" fact a frontend, cache, or bridge
|
|
137
|
+
* needs, replacing the per-kind bespoke streams each producer used to hand out.
|
|
138
|
+
*
|
|
139
|
+
* A swappable {@link MatbotServices} member, not fixed runtime plumbing. The host boots an in-process
|
|
140
|
+
* broadcaster; `register('Notifier', …)` swaps in a distributed one (Redis, NATS, a matbot-to-matbot
|
|
141
|
+
* bridge) behind the usual capture-safe proxy, and unloading that plugin reverts to the host's default.
|
|
142
|
+
* Matbot owns the envelope and the registration surface; it does not own delivery.
|
|
143
|
+
*
|
|
144
|
+
* **What matbot guarantees, and an alternative implementation must preserve:**
|
|
145
|
+
* - `notify` never throws and never blocks — enqueue and return; a slow subscriber blocks nobody.
|
|
146
|
+
* - Ordered per producer to each subscriber. No ordering across producers.
|
|
147
|
+
* - Local delivery is at-most-once, in-memory. **No persistence, no replay, no delivery guarantee.**
|
|
148
|
+
* A sink attaching mid-flight has missed whatever preceded it — so a sink re-queries on attach and
|
|
149
|
+
* treats notifications purely as invalidation. Never put current state in the bus.
|
|
150
|
+
* - Filters are applied before queueing.
|
|
151
|
+
* - An implementation that forwards off-box MUST stamp `instance` on ingress and MUST NOT forward a
|
|
152
|
+
* notification that already carries a foreign `instance` — that is the loop break.
|
|
153
|
+
*/
|
|
154
|
+
export interface Notifier {
|
|
155
|
+
/** Publish. `plugin` is stamped from the calling plugin's scope unless explicitly relayed. */
|
|
156
|
+
notify(notification: NotifyInput): void;
|
|
157
|
+
subscribe(signal?: AbortSignal, filter?: NotificationFilter): AsyncIterable<Notification>;
|
|
158
|
+
/** Detached observation loop: awaits each handler before pulling the next, isolates a throwing
|
|
159
|
+
* handler, ends when `signal` aborts. The fire-and-forget form of the for-await every sink writes. */
|
|
160
|
+
consume(handler: (notification: Notification) => void | Promise<void>, signal?: AbortSignal, filter?: NotificationFilter): void;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The host's boot default: in-process fan-out over the shared broadcaster, one queue per subscriber.
|
|
165
|
+
* `defaultPlugin` is stamped on anything published without one — the host passes `core`, and the
|
|
166
|
+
* per-plugin scope built in setupPlugin passes the plugin's own name.
|
|
167
|
+
*/
|
|
168
|
+
export function createNotifier(defaultPlugin: string): Notifier {
|
|
169
|
+
const events = createBroadcaster<Notification>();
|
|
170
|
+
return {
|
|
171
|
+
notify(notification) {
|
|
172
|
+
const plugin = notification.plugin ?? defaultPlugin;
|
|
173
|
+
// The type-level rule is erased by the time this runs, and TypeScript never saw the producers that
|
|
174
|
+
// matter most here: a plain-JS plugin, or a bridge injecting another instance's traffic. Warn, don't
|
|
175
|
+
// throw — `notify` is contractually non-throwing, and a badly-named kind is a hygiene fault, not a
|
|
176
|
+
// delivery failure. Only the shape is checkable; that the prefix names the *definer* is not, since
|
|
177
|
+
// the emitter legitimately differs from it.
|
|
178
|
+
if (!notification.kind.includes('#')) {
|
|
179
|
+
console.warn(`[matbot] ${plugin} published unqualified notification kind "${notification.kind}" — expected <package-name>#<InterfaceName>`);
|
|
180
|
+
}
|
|
181
|
+
events.emit({ ...notification, plugin } as Notification);
|
|
182
|
+
},
|
|
183
|
+
async *subscribe(signal?: AbortSignal, filter?: NotificationFilter) {
|
|
184
|
+
for await (const event of events.subscribe(signal, filter ? e => filter(e.value) : undefined)) yield event.value;
|
|
185
|
+
},
|
|
186
|
+
consume(handler, signal?, filter?) {
|
|
187
|
+
events.consume(e => handler(e.value), signal, filter ? e => filter(e.value) : undefined);
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Re-scope a notifier so everything published through it is attributed to `plugin`. */
|
|
193
|
+
export function scopedNotifier(notifier: Notifier, plugin: string): Notifier {
|
|
194
|
+
return {
|
|
195
|
+
notify(notification) { notifier.notify({ ...notification, plugin: notification.plugin ?? plugin } as NotifyInput); },
|
|
196
|
+
subscribe: (signal, filter) => notifier.subscribe(signal, filter),
|
|
197
|
+
consume: (handler, signal, filter) => notifier.consume(handler, signal, filter),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Wrap a store so every successful write announces itself. For a namespace with ONE writer, an explicit
|
|
203
|
+
* `notify` at that writer is clearer; use this where a namespace has many — `sessions` is written by the
|
|
204
|
+
* turn pump (title derivation, persist-at-turn-start, the assistant commit), the `session_action` tool
|
|
205
|
+
* (rename/hide/unhide), `session_edit` (fork/split), and the frontend (a session minted over HTTP). One
|
|
206
|
+
* wrapper covers them all and cannot be forgotten by the next writer to arrive.
|
|
207
|
+
*
|
|
208
|
+
* It sits ABOVE the backend, so it is storage-independent (no backend implements anything) and profile-
|
|
209
|
+
* independent (the principal is read ambiently at write time, which is also what makes it the correct
|
|
210
|
+
* partition for visibility filtering). It observes writes made THROUGH it — a second process, or a
|
|
211
|
+
* writer that bypassed this store, is not seen; that is what a backend's own watch, where one exists,
|
|
212
|
+
* is for.
|
|
213
|
+
*/
|
|
214
|
+
export function notifyingStore<T extends { id: string; version: string }>(
|
|
215
|
+
store: Store<T>, notifier: Notifier, namespace: string, source: string,
|
|
216
|
+
): Store<T> {
|
|
217
|
+
const announce = (operation: 'saved' | 'deleted', id: string): void => {
|
|
218
|
+
const principal = tryCurrentPrincipal();
|
|
219
|
+
notifier.notify({
|
|
220
|
+
kind: ItemChangeKind, source, operation, namespace, id,
|
|
221
|
+
...(principal !== undefined ? { principal } : {}),
|
|
222
|
+
});
|
|
223
|
+
};
|
|
224
|
+
return {
|
|
225
|
+
get: id => store.get(id),
|
|
226
|
+
query: q => store.query(q),
|
|
227
|
+
async set(id, value) { await store.set(id, value); announce('saved', id); },
|
|
228
|
+
async cas(id, expected, next) {
|
|
229
|
+
const res = await store.cas(id, expected, next);
|
|
230
|
+
if (res.ok) announce('saved', id);
|
|
231
|
+
return res;
|
|
232
|
+
},
|
|
233
|
+
async delete(id, expectedVersion) {
|
|
234
|
+
const deleted = await store.delete(id, expectedVersion);
|
|
235
|
+
if (deleted) announce('deleted', id);
|
|
236
|
+
return deleted;
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
}
|
package/src/plugin.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
FileStore,
|
|
2
|
+
FileStore, Principal, Vault, Message, ModelParameters,
|
|
3
3
|
ProviderAdapter, ProviderConfig, ProviderRegistry, Tool, ToolRegistry, FrontendInfo,
|
|
4
4
|
Store, Session, SystemContextRegistry, KnowledgeIndex, PromptFn, SessionRunner, Usage,
|
|
5
5
|
TypeScriptStripper, ToolTypeIndex, ToolPresenter, SteeringPolicy,
|
|
6
6
|
} from './types.js';
|
|
7
7
|
import type { Routed } from './broadcast.js';
|
|
8
|
+
import type { Notifier } from './notify.js';
|
|
8
9
|
import type { HookRegistry } from './hooks.js';
|
|
9
10
|
|
|
10
11
|
export const PLUGIN_API_VERSION = '0.1';
|
|
@@ -114,6 +115,12 @@ export interface MatbotServices {
|
|
|
114
115
|
* reference held across a swap keeps resolving to the live backend. Always present (boot default). */
|
|
115
116
|
readonly Vault: Vault;
|
|
116
117
|
readonly KnowledgeIndex: KnowledgeIndex;
|
|
118
|
+
/** The notification bus — every "something changed" fact, one fan-out. Also the `register('Notifier',
|
|
119
|
+
* impl)` swap key: the host boots an in-process broadcaster, a plugin may swap in a distributed one
|
|
120
|
+
* (and unloading it reverts to the boot default). Capture-safe behind a proxy. Always present.
|
|
121
|
+
* Within a plugin's `setup()` this is scoped, so published notifications carry that plugin's name.
|
|
122
|
+
* See {@link Notifier}. */
|
|
123
|
+
readonly Notifier: Notifier;
|
|
117
124
|
/** Optional tool-presentation policy: chooses which tools are advertised to the model per provider
|
|
118
125
|
* call (a large-library search/deferral plugin registers one). Absent ⇒ the runner advertises the
|
|
119
126
|
* whole turn snapshot. A plain registered service (not a swap-member); consumed as a member. See
|
|
@@ -504,18 +511,13 @@ export interface StorageBackend {
|
|
|
504
511
|
}
|
|
505
512
|
|
|
506
513
|
/**
|
|
507
|
-
* The
|
|
508
|
-
*
|
|
509
|
-
*
|
|
510
|
-
*
|
|
511
|
-
*
|
|
512
|
-
* The origin/visibility split is generic (files today; the same shape serves skills/stores later).
|
|
514
|
+
* The per-connection visibility layer, registered by a storage backend that partitions per principal (the
|
|
515
|
+
* profiles backend). Consumed by a frontend firehose to decide, per SSE connection, whether a given
|
|
516
|
+
* notification is visible to that connection's principal. Absent ⇒ no partitioning, and every connection
|
|
517
|
+
* sees everything. Generic across kinds: it answers on (`namespace`, `id`, `origin`), so files, skills and
|
|
518
|
+
* stores all route through the one predicate.
|
|
513
519
|
*/
|
|
514
520
|
export interface WatchVisibility {
|
|
515
|
-
/** Every partition's file events, merged into one stream, each a self-describing {@link StoreChange}
|
|
516
|
-
* (routing namespace `'files'`, the file id, and the {@link FileMetaData} in `detail`) tagged with the
|
|
517
|
-
* origin partition — so the firehose filters and routes files through the same predicate as any stream. */
|
|
518
|
-
watchFiles(signal?: AbortSignal): AsyncIterable<Routed<StoreChange>>;
|
|
519
521
|
/**
|
|
520
522
|
* The per-connection visibility predicate for ANY partitioned event stream (files, skills, …): would a
|
|
521
523
|
* connection owned by `viewer` see the change to (`namespace`, `id`) produced by `origin`? True iff the
|
package/src/store-query.ts
CHANGED
|
@@ -39,6 +39,18 @@ export interface StoreQuery {
|
|
|
39
39
|
sort?: SortSpec[];
|
|
40
40
|
limit?: number;
|
|
41
41
|
cursor?: string | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* The caller's promise not to mutate the returned documents, freeing a backend to hand back shared
|
|
44
|
+
* instances instead of freshly-materialised ones. A pure optimisation hint: a backend may ignore it
|
|
45
|
+
* and nothing changes, so it is never load-bearing for correctness — but a backend that honours it
|
|
46
|
+
* can skip re-materialising documents that have not changed since the last query, which for a
|
|
47
|
+
* summary listing (read every document, use four fields) is the entire cost of the call.
|
|
48
|
+
*
|
|
49
|
+
* Set it only where the promise is actually kept. A read-modify-write path — pull a page, edit a
|
|
50
|
+
* document, `cas` it back — must NOT set it: the instance it edits may be one another caller is
|
|
51
|
+
* still reading, and a backend caching it would then be caching an edit that never reached disk.
|
|
52
|
+
*/
|
|
53
|
+
immutable?: boolean;
|
|
42
54
|
}
|
|
43
55
|
|
|
44
56
|
export interface QueryResult<T> {
|
package/src/types.ts
CHANGED
|
@@ -730,6 +730,25 @@ export interface ToolContext {
|
|
|
730
730
|
unloadPlugin(specifier: string): Promise<boolean>;
|
|
731
731
|
}
|
|
732
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
|
+
|
|
733
752
|
/**
|
|
734
753
|
* A tool's runtime. `R` is the type of the `value` carried by its `result` event — declared once,
|
|
735
754
|
* at the source, so the executor's yields and the tool's {@link ToolContracts} registry entry can't
|
|
@@ -780,28 +799,6 @@ export interface FileHandle extends FileMetaData {
|
|
|
780
799
|
stream(signal?: AbortSignal): AsyncIterable<Uint8Array>;
|
|
781
800
|
}
|
|
782
801
|
|
|
783
|
-
export type FileEvent = FileMetaData & { changed: Array<keyof FileMetaData> };
|
|
784
|
-
|
|
785
|
-
/**
|
|
786
|
-
* The generic, self-describing change envelope every partitioned CRUD stream emits (files, skills, and
|
|
787
|
-
* future partitioned stores) — the payload half of a {@link Routed} event, whose `origin` carries the
|
|
788
|
-
* acting principal/partition. It supersedes the per-stream bespoke shapes (the old `SkillEvent`, the raw
|
|
789
|
-
* `FileEvent` on the wire) so a frontend firehose can filter and route every stream through ONE predicate.
|
|
790
|
-
*
|
|
791
|
-
* `namespace` is the ROUTING namespace the change lives under — `'files'`, `'skills'`, or a document
|
|
792
|
-
* namespace — NOT a content sub-namespace (a file's own `metadata.namespace`, e.g. `'workspace'`, rides in
|
|
793
|
-
* `detail`). Pairing `namespace` + `id` is exactly what `WatchVisibility.visible` needs to decide, per
|
|
794
|
-
* connection, whether the viewer routes that item to the same partition (or has it shared in). `detail`
|
|
795
|
-
* carries an optional rich payload for in-place UI updates — a file change ships its {@link FileMetaData}
|
|
796
|
-
* so a sidebar row can update its size without a re-fetch; a skill change needs none (a refresh ping).
|
|
797
|
-
*/
|
|
798
|
-
export interface StoreChange {
|
|
799
|
-
readonly operation: 'saved' | 'deleted';
|
|
800
|
-
readonly namespace: string;
|
|
801
|
-
readonly id: string;
|
|
802
|
-
readonly detail?: unknown;
|
|
803
|
-
}
|
|
804
|
-
|
|
805
802
|
export interface FileFilter {
|
|
806
803
|
sessionId?: string;
|
|
807
804
|
mimeType?: string;
|
|
@@ -823,8 +820,6 @@ export interface FileStore {
|
|
|
823
820
|
delete(id: string): Promise<void>;
|
|
824
821
|
list(filter?: FileFilter): AsyncIterable<FileHandle>;
|
|
825
822
|
putTemp(name: string, mimeType: MimeType, data: AsyncIterable<Uint8Array>): Promise<FileHandle>;
|
|
826
|
-
/** Observe file changes. Implementations that cannot watch their backing store omit this. */
|
|
827
|
-
watch(signal?: AbortSignal): AsyncIterable<FileEvent>;
|
|
828
823
|
}
|
|
829
824
|
|
|
830
825
|
// ── Frontend ──────────────────────────────────────────────────────────────────
|
|
@@ -886,19 +881,16 @@ export type HealthStatus =
|
|
|
886
881
|
|
|
887
882
|
// ── Registries ────────────────────────────────────────────────────────────────
|
|
888
883
|
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
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. */
|
|
893
888
|
export interface ToolRegistry {
|
|
894
889
|
register(tool: Tool): void;
|
|
895
890
|
remove(name: string): void;
|
|
896
891
|
resolve(name: string): Tool | null;
|
|
897
892
|
list(): readonly Tool[];
|
|
898
893
|
removeByPlugin(pluginName: string): void;
|
|
899
|
-
/** Observe tool CRUD as it happens. Read-only — observers cannot veto a registration. One event
|
|
900
|
-
* per tool (removeByPlugin emits a `removed` per matched tool). The stream ends when `signal` aborts. */
|
|
901
|
-
watch(signal?: AbortSignal): AsyncIterable<ToolRegistryEvent>;
|
|
902
894
|
}
|
|
903
895
|
|
|
904
896
|
/** What the runner tells a {@link ToolPresenter} about the call it's about to make. */
|
|
@@ -948,10 +940,6 @@ export interface ProviderRegistry extends ReadonlyMap<string, ProviderConfig> {
|
|
|
948
940
|
revert(name: string): void;
|
|
949
941
|
}
|
|
950
942
|
|
|
951
|
-
export type PluginRegistryEvent =
|
|
952
|
-
| { type: 'loaded'; name: string }
|
|
953
|
-
| { type: 'unloaded'; name: string };
|
|
954
|
-
|
|
955
943
|
// ── Pipeline events ─────────────────────────────────────────────────────────────
|
|
956
944
|
|
|
957
945
|
/**
|