@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/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"node",
|
|
5
5
|
"browser"
|
|
6
6
|
],
|
|
7
|
-
"version": "0.
|
|
7
|
+
"version": "0.4.1",
|
|
8
8
|
"engines": {
|
|
9
9
|
"node": ">=24.0.0"
|
|
10
10
|
},
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"license": "Apache-2.0",
|
|
23
23
|
"type": "module",
|
|
24
24
|
"exports": {
|
|
25
|
-
".": "./src/index.ts"
|
|
25
|
+
".": "./src/index.ts",
|
|
26
|
+
"./host": "./src/host.ts"
|
|
26
27
|
},
|
|
27
28
|
"repository": {
|
|
28
29
|
"type": "git",
|
package/src/hooks.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
Hook, HookPoint, Message, MessageContent, Session, DeferredScreen,
|
|
2
|
+
Hook, HookPoint, HookRegistrar, Message, MessageContent, Session, DeferredScreen,
|
|
3
3
|
ScreenContext, ContributeContext, ToolCallContext, ToolCallResult, ToolResultContext, FollowupContext,
|
|
4
4
|
} from './types.js';
|
|
5
|
+
import { foldOntoUserTurn } from './session.js';
|
|
5
6
|
|
|
6
7
|
const HOOK_ERROR_CREATOR = 'matbot-hooks';
|
|
7
8
|
|
|
8
|
-
export class HookRegistry {
|
|
9
|
+
export class HookRegistry implements HookRegistrar {
|
|
9
10
|
private readonly hooks = new Map<HookPoint, Hook[]>();
|
|
10
11
|
|
|
11
12
|
// A hook handler that *throws* is a failure, not an intentional stop (which is a return value:
|
|
@@ -111,11 +112,10 @@ export class HookRegistry {
|
|
|
111
112
|
// so a user message is present in practice; this is defensive).
|
|
112
113
|
const durable: MessageContent[] = [];
|
|
113
114
|
const foldDurable = (blocks: MessageContent[]): void => {
|
|
114
|
-
const
|
|
115
|
-
if (
|
|
115
|
+
const folded = foldOntoUserTurn(session, blocks);
|
|
116
|
+
if (folded === session) return; // no user message to attach to — dropped, not orphaned
|
|
116
117
|
durable.push(...blocks);
|
|
117
|
-
session =
|
|
118
|
-
i === idx ? { ...m, content: [...m.content, ...blocks] } : m) };
|
|
118
|
+
session = folded;
|
|
119
119
|
};
|
|
120
120
|
for (const hook of this.hooks.get('screen') ?? []) {
|
|
121
121
|
if (hook.on !== 'screen') continue;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CompletionRequest, MatbotMachine, MatbotServices, Mounted, MountConsumeOptions, SingleTurnRequest,
|
|
3
|
+
} from './plugin.js';
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* Host-side machine assembly — the wiring an *embedder* does once at boot, not API a plugin author
|
|
7
|
+
* writes against. It lives behind `@matatbread/matbot-plugin-api/host` so the package root can answer
|
|
8
|
+
* exactly one question: what does a plugin need in order to be a plugin? Publishing these at the root
|
|
9
|
+
* made boot mechanics look like stable author-facing surface, and no plugin in this repo imports one.
|
|
10
|
+
*
|
|
11
|
+
* Split out of plugin.ts as a *file* boundary rather than an export list, so the distinction cannot
|
|
12
|
+
* quietly erode the next time something is added. The plugin-facing halves of the same subsystems stay
|
|
13
|
+
* where they are: `Mounted`/`MountConsumeOptions` (the contract of `services.mounted`) in plugin.ts,
|
|
14
|
+
* `runAs`/`currentPrincipal` in principal-context.ts.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Wrap a services object so every registered service is reachable as a member (`services.InterfaceName`),
|
|
19
|
+
* not only via `services.get('InterfaceName')` — one access surface for base members and plugin-registered
|
|
20
|
+
* services alike. A member read of a key the object doesn't carry falls back to its own `get()` (the
|
|
21
|
+
* registry), so the augmentation that declares `InterfaceName?: InterfaceName` is also the access path;
|
|
22
|
+
* the optional `?` is the single call-site signal of "may be absent, null-check it". Assignment throws,
|
|
23
|
+
* directing callers to `register()` (the swap-aware write path). Applied to both the host services object
|
|
24
|
+
* and the per-plugin scoped object, so plugins see the same surface the host does.
|
|
25
|
+
*
|
|
26
|
+
* Only `get` and `set` are trapped. `has` used to fall through to the registry too, which made the plain
|
|
27
|
+
* language construct `'Foo' in services` perform a registry lookup — no side effect, but a surprise, and
|
|
28
|
+
* inconsistent with `ownKeys`, which never listed registered services. `in` is therefore structural, and
|
|
29
|
+
* the presence check remains the documented one: `services.Foo !== undefined`, or `?.`.
|
|
30
|
+
*/
|
|
31
|
+
export function unifyServices(services: MatbotMachine): MatbotMachine {
|
|
32
|
+
return new Proxy(services, {
|
|
33
|
+
get(target, key, receiver) {
|
|
34
|
+
if (Reflect.has(target, key)) return Reflect.get(target, key, receiver);
|
|
35
|
+
return typeof key === 'string' ? target.get(key as keyof MatbotServices) : undefined;
|
|
36
|
+
},
|
|
37
|
+
set(_target, key) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`Cannot assign services.${String(key)} directly — use services.register('${String(key)}', impl) to register or replace a service.`,
|
|
40
|
+
);
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
// ── Capture-safe swap proxies ───────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
export type SwapFn<T extends object> = (next: T) => void;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A capture-safe forwarding proxy: every trap routes to whatever `getCurrent()` returns *now*, so a
|
|
50
|
+
* reference captured before a register()-driven swap keeps resolving to the live impl. getPrototypeOf
|
|
51
|
+
* is forwarded so `instanceof` sees the real impl (the StorageBackend identity checks depend on it);
|
|
52
|
+
* ownKeys + getOwnPropertyDescriptor keep object spread faithful. Methods bind to the current impl,
|
|
53
|
+
* not the proxy. A nullish current (an optional service with nothing registered yet) reads as empty.
|
|
54
|
+
*/
|
|
55
|
+
export function forwardingProxy<T extends object>(getCurrent: () => T | undefined): T {
|
|
56
|
+
return new Proxy({} as T, {
|
|
57
|
+
get(_t, prop) {
|
|
58
|
+
const cur = getCurrent();
|
|
59
|
+
if (cur === undefined) return undefined;
|
|
60
|
+
const val = Reflect.get(cur, prop, cur);
|
|
61
|
+
return typeof val === 'function' ? (val as (...a: unknown[]) => unknown).bind(cur) : val;
|
|
62
|
+
},
|
|
63
|
+
has(_t, prop) { const cur = getCurrent(); return cur !== undefined && Reflect.has(cur, prop); },
|
|
64
|
+
getPrototypeOf() { const cur = getCurrent(); return cur === undefined ? null : Reflect.getPrototypeOf(cur); },
|
|
65
|
+
ownKeys() { const cur = getCurrent(); return cur === undefined ? [] : Reflect.ownKeys(cur); },
|
|
66
|
+
getOwnPropertyDescriptor(_t, prop) {
|
|
67
|
+
const cur = getCurrent();
|
|
68
|
+
if (cur === undefined) return undefined;
|
|
69
|
+
const d = Reflect.getOwnPropertyDescriptor(cur, prop);
|
|
70
|
+
if (d !== undefined) d.configurable = true; // Proxy invariant: props absent from the {} target must be configurable.
|
|
71
|
+
return d;
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Returns [proxy, swap]: the Store/FileStore handle plugins capture, plus the fn register() calls to
|
|
78
|
+
* repoint it at a new backend's store. Built on forwardingProxy so capture-safety is uniform.
|
|
79
|
+
*/
|
|
80
|
+
export function makeSwappable<T extends object>(initial: T): [T, SwapFn<T>] {
|
|
81
|
+
let current = initial;
|
|
82
|
+
return [forwardingProxy<T>(() => current), (next: T) => { current = next; }];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Mount table (host half) ──────────────────────────────────
|
|
86
|
+
|
|
87
|
+
/** The host-side mount table: {@link Mounted} for plugins, plus the producer half the host drives from
|
|
88
|
+
* its register/unregister and quiescent-edge flush. */
|
|
89
|
+
export interface MountTable {
|
|
90
|
+
readonly mounted: Mounted;
|
|
91
|
+
/** Record that a key's presence may have changed since the last edge (called by register/unregister). */
|
|
92
|
+
markDirty(key: keyof MatbotServices): void;
|
|
93
|
+
/** At a quiescent edge, compute each dirty key's net presence transition and multicast it. */
|
|
94
|
+
flush(): void;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
interface MountInterest {
|
|
98
|
+
readonly handler: (machine: MatbotMachine) => void | Promise<void>;
|
|
99
|
+
readonly onUnmount: ((machine: MatbotMachine) => void | Promise<void>) | undefined;
|
|
100
|
+
readonly signal: AbortSignal | undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function reportMountHandlerError(e: unknown): void {
|
|
104
|
+
console.error('[matbot] mounted handler threw:', e instanceof Error ? e.message : e);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Build a {@link MountTable} over a lazily-read machine. Notifications batch to the quiescent edge
|
|
109
|
+
* ({@link flush}), where each dirty key's net presence (absent→present = mount, present→present =
|
|
110
|
+
* remount, present→absent = committed unload) is multicast to that key's subscribers. The clock holds
|
|
111
|
+
* the last-committed presence per key, so a reload collapses to one remount and a committed unload is
|
|
112
|
+
* well-defined. Presence is read by member access on the unified machine, which resolves both the core
|
|
113
|
+
* getters (StorageBackend/Vault/KnowledgeIndex) and the registry-backed augmented keys.
|
|
114
|
+
*/
|
|
115
|
+
export function createMountTable(getMachine: () => MatbotMachine): MountTable {
|
|
116
|
+
const interests = new Map<string, Set<MountInterest>>();
|
|
117
|
+
const committed = new Map<string, boolean>(); // last-committed presence per key (the clock)
|
|
118
|
+
const dirty = new Set<string>();
|
|
119
|
+
|
|
120
|
+
const present = (key: string): boolean => (getMachine() as unknown as Record<string, unknown>)[key] !== undefined;
|
|
121
|
+
|
|
122
|
+
const run = (fn: (machine: MatbotMachine) => void | Promise<void>, machine: MatbotMachine): void => {
|
|
123
|
+
try {
|
|
124
|
+
const r = fn(machine);
|
|
125
|
+
if (r instanceof Promise) r.catch(reportMountHandlerError);
|
|
126
|
+
} catch (e) { reportMountHandlerError(e); }
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const mounted: Mounted = {
|
|
130
|
+
observe(options, handler) {
|
|
131
|
+
const { key, replay, signal, onUnmount } = options;
|
|
132
|
+
if (signal?.aborted === true) return;
|
|
133
|
+
const interest: MountInterest = {
|
|
134
|
+
handler: handler as MountInterest['handler'],
|
|
135
|
+
onUnmount: onUnmount as MountInterest['onUnmount'],
|
|
136
|
+
signal,
|
|
137
|
+
};
|
|
138
|
+
let set = interests.get(key);
|
|
139
|
+
if (set === undefined) { set = new Set(); interests.set(key, set); }
|
|
140
|
+
const subs = set;
|
|
141
|
+
subs.add(interest);
|
|
142
|
+
signal?.addEventListener('abort', () => { subs.delete(interest); }, { once: true });
|
|
143
|
+
// Replay on the next microtask (async-iterator parity — never inline in the consume() frame).
|
|
144
|
+
// Reads the live machine, not `committed`: replay is "current state", separate from the clock.
|
|
145
|
+
if (replay === true) queueMicrotask(() => {
|
|
146
|
+
if (signal?.aborted === true) return;
|
|
147
|
+
if (present(key as string)) run(interest.handler, getMachine());
|
|
148
|
+
});
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
mounted,
|
|
154
|
+
markDirty(key) { dirty.add(key as string); },
|
|
155
|
+
flush() {
|
|
156
|
+
if (dirty.size === 0) return;
|
|
157
|
+
const keys = [...dirty];
|
|
158
|
+
dirty.clear();
|
|
159
|
+
const machine = getMachine();
|
|
160
|
+
for (const key of keys) {
|
|
161
|
+
const before = committed.get(key) ?? false;
|
|
162
|
+
const after = present(key);
|
|
163
|
+
committed.set(key, after);
|
|
164
|
+
const subs = interests.get(key);
|
|
165
|
+
if (subs === undefined) continue;
|
|
166
|
+
if (after) {
|
|
167
|
+
for (const i of subs) run(i.handler, machine); // mount / remount
|
|
168
|
+
} else if (before) {
|
|
169
|
+
for (const i of subs) if (i.onUnmount !== undefined) run(i.onUnmount, machine); // committed unload
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Build the one-message CompletionRequest for a {@link MatbotRuntime.singleTurn} call, hiding the
|
|
178
|
+
* otherwise-mandatory and meaningless Message fields (id/traceId/createdAt) an out-of-band one-shot
|
|
179
|
+
* has no use for. Pure; the host invokes its own complete() with the result.
|
|
180
|
+
*/
|
|
181
|
+
export function singleTurnRequest(req: SingleTurnRequest): CompletionRequest {
|
|
182
|
+
return {
|
|
183
|
+
provider: req.provider,
|
|
184
|
+
messages: [{
|
|
185
|
+
id: '', traceId: '', createdAt: new Date().toISOString(), role: 'user',
|
|
186
|
+
content: [{ type: 'text', text: req.prompt }],
|
|
187
|
+
}],
|
|
188
|
+
...(req.system !== undefined ? { system: req.system } : {}),
|
|
189
|
+
...(req.parameters !== undefined ? { parameters: req.parameters } : {}),
|
|
190
|
+
...(req.signal !== undefined ? { signal: req.signal } : {}),
|
|
191
|
+
};
|
|
192
|
+
}
|
package/src/host.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* `@matatbread/matbot-plugin-api/host` — the boot-time wiring an *embedder* performs to stand a machine
|
|
3
|
+
* up, as opposed to what a *plugin* is written against.
|
|
4
|
+
*
|
|
5
|
+
* The package root used to export both, which made carrier installers, swap proxies, the mount table and
|
|
6
|
+
* the hook registry implementation look like stable author-facing API. They are not: no plugin in this
|
|
7
|
+
* repo imports one, and only `apps/*` and `core` ever did. Splitting them here lets the root answer one
|
|
8
|
+
* question — what does a plugin need in order to be a plugin? — and lets these change without it being a
|
|
9
|
+
* plugin-API break.
|
|
10
|
+
*
|
|
11
|
+
* Three of these subsystems are deliberately *split* rather than moved whole, because each has a genuine
|
|
12
|
+
* author-facing half that stays at the root:
|
|
13
|
+
* - principal: `runAs` / `currentPrincipal` / `tryCurrentPrincipal` are read and established by plugins
|
|
14
|
+
* (a frontend scopes each inbound message); only *installing the carrier* is the host's job.
|
|
15
|
+
* - notifications: every plugin publishes and consumes; only *minting* the process notifier is the host's.
|
|
16
|
+
* - mount table: `services.mounted` (`Mounted`, `MountConsumeOptions`) is the plugin contract; the
|
|
17
|
+
* producer half (`MountTable`, `markDirty`/`flush`) is driven by the host's register/quiescent edge.
|
|
18
|
+
*
|
|
19
|
+
* `core` re-exports everything here, so a host that already depends on `@matatbread/matbot-core` needs no
|
|
20
|
+
* direct import of this subpath.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
// Machine assembly: member-access unification, capture-safe swap proxies, the mount table's producer
|
|
24
|
+
// half, and the one-shot CompletionRequest builder behind MatbotRuntime.singleTurn.
|
|
25
|
+
export * from './host-machine.js';
|
|
26
|
+
|
|
27
|
+
// Ambient scope carriers. The host installs a platform impl once at boot (node: AsyncLocalStorage;
|
|
28
|
+
// browser/single-principal: a constant carrier) and never touches them again.
|
|
29
|
+
export { installPrincipalCarrier, enterPrincipal, createConstantPrincipalCarrier } from './principal-context.js';
|
|
30
|
+
export type { PrincipalCarrier } from './principal-context.js';
|
|
31
|
+
export * from './usage-context.js';
|
|
32
|
+
|
|
33
|
+
// The machine half of a context switch: quiescent-edge flushers and the deferred-swap application point.
|
|
34
|
+
export * from './context-switch.js';
|
|
35
|
+
|
|
36
|
+
// Hook dispatch implementation. Plugins register hooks through `services.hooks`; only the host constructs
|
|
37
|
+
// the registry that backs it.
|
|
38
|
+
export { HookRegistry } from './hooks.js';
|
|
39
|
+
|
|
40
|
+
// The multi-subscriber fan-out primitive under `Notifier`. A plugin that wants to publish an event uses
|
|
41
|
+
// the Notifier — that is the sanctioned path, and the reason this stays host-side.
|
|
42
|
+
export * from './broadcast.js';
|
|
43
|
+
export { createNotifier, scopedNotifier } from './notify.js';
|
package/src/index.ts
CHANGED
|
@@ -1,14 +1,32 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* `@matatbread/matbot-plugin-api` — everything a plugin author needs, and nothing a host needs to *build*
|
|
3
|
+
* a machine. Boot wiring (carrier installers, swap proxies, the mount table's producer half, the hook
|
|
4
|
+
* registry impl, the broadcaster) lives behind the `/host` subpath; see host.ts for the reasoning.
|
|
5
|
+
*
|
|
6
|
+
* Star-exported where a file is wholly plugin-facing; enumerated where a file is deliberately split. The
|
|
7
|
+
* two enumerated cases use `export type *` for the shapes plus a named list of runtime values, so a new
|
|
8
|
+
* *type* flows out automatically and a new *value* is a decision someone has to make here.
|
|
9
|
+
*/
|
|
10
|
+
|
|
1
11
|
export type * from './types.js';
|
|
2
|
-
export { CONFIRM_YES, CONFIRM_NO } from './types.js';
|
|
12
|
+
export { CONFIRM_YES, CONFIRM_NO, isTruncatedToolResult } from './types.js';
|
|
13
|
+
|
|
3
14
|
export * from './store-query.js';
|
|
4
|
-
export * from './vault.js';
|
|
5
15
|
export * from './errors.js';
|
|
6
|
-
export * from './
|
|
7
|
-
export * from './usage-context.js';
|
|
16
|
+
export * from './vault.js';
|
|
8
17
|
export * from './session.js';
|
|
9
|
-
export * from './context-switch.js';
|
|
10
|
-
export * from './hooks.js';
|
|
11
|
-
export * from './plugin.js';
|
|
12
|
-
export * from './broadcast.js';
|
|
13
|
-
export * from './notify.js';
|
|
14
18
|
export * from './invoke-tool.js';
|
|
19
|
+
|
|
20
|
+
// The plugin half of the machine: MatbotPlugin/MatbotServices/MatbotRuntime, the mount-table *consumer*
|
|
21
|
+
// contract, and the API version a plugin declares. Host assembly is in ./host-machine.ts.
|
|
22
|
+
export type * from './plugin.js';
|
|
23
|
+
export { PLUGIN_API_VERSION } from './plugin.js';
|
|
24
|
+
|
|
25
|
+
// Ambient principal: read it, or establish one for an inbound request/message. Installing the carrier is
|
|
26
|
+
// the host's job (`/host`).
|
|
27
|
+
export { currentPrincipal, tryCurrentPrincipal, runAs } from './principal-context.js';
|
|
28
|
+
|
|
29
|
+
// Notifications: publish, consume, and the two kinds plugin-api itself defines. Minting the process
|
|
30
|
+
// notifier is the host's job (`/host`).
|
|
31
|
+
export type * from './notify.js';
|
|
32
|
+
export { ItemChangeKind, RegistryChangeKind, notifyingStore } from './notify.js';
|
package/src/invoke-tool.ts
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
|
-
import type { MatbotMachine } from './plugin.js';
|
|
1
|
+
import type { MatbotMachine, MatbotPlugin } from './plugin.js';
|
|
2
2
|
import type { ToolContext, ToolEvent, ToolResultFor, ToolProxy, PromptFn, FormField } from './types.js';
|
|
3
3
|
|
|
4
|
+
/** The host's plugin hot-load ops, as both the runner and {@link invokeTool} hold them. */
|
|
5
|
+
export interface PluginOps {
|
|
6
|
+
loadPlugin(specifier: string, prompt?: PromptFn, refresh?: boolean): Promise<MatbotPlugin>;
|
|
7
|
+
unloadPlugin(specifier: string): Promise<boolean>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Bind the plugin hot-load ops for a {@link ToolContext}, closing over the turn's `prompt`.
|
|
12
|
+
*
|
|
13
|
+
* That closure is the whole point of `ToolContext.loadPlugin` existing alongside
|
|
14
|
+
* `MatbotRuntime.loadPlugin`: a tool cannot forget to pass the prompt, so an interactive load cannot
|
|
15
|
+
* silently become non-interactive at one call site out of six — the same reasoning that makes the
|
|
16
|
+
* principal ambient rather than threaded. What it should not be is written out twice, once in the runner
|
|
17
|
+
* and once here, since the injected middle argument is exactly the shape a signature change breaks
|
|
18
|
+
* quietly.
|
|
19
|
+
*/
|
|
20
|
+
export function bindPluginOps(host: PluginOps, prompt: PromptFn): Pick<ToolContext, 'loadPlugin' | 'unloadPlugin'> {
|
|
21
|
+
return {
|
|
22
|
+
loadPlugin: (specifier, refresh) => host.loadPlugin(specifier, prompt, refresh),
|
|
23
|
+
unloadPlugin: (specifier) => host.unloadPlugin(specifier),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
4
27
|
/**
|
|
5
28
|
* Inputs to {@link invokeTool} that a one-shot caller can't derive from the machine. A tool
|
|
6
29
|
* forwarding a call to another tool should pass its own {@link ToolContext} straight through as
|
|
@@ -41,8 +64,7 @@ export function invokeTool<K extends string, const P>(
|
|
|
41
64
|
signal: opts.signal,
|
|
42
65
|
vault: machine.Vault,
|
|
43
66
|
prompt,
|
|
44
|
-
|
|
45
|
-
unloadPlugin: (specifier) => machine.unloadPlugin(specifier),
|
|
67
|
+
...bindPluginOps(machine, prompt),
|
|
46
68
|
...(opts.provider !== undefined ? { provider: opts.provider } : {}),
|
|
47
69
|
...(machine.workdir !== undefined ? { workdir: machine.workdir } : {}),
|
|
48
70
|
...(machine.configPath !== undefined ? { configPath: machine.configPath } : {}),
|
package/src/notify.ts
CHANGED
|
@@ -98,6 +98,9 @@ export const RegistryChangeKind = '@matatbread/matbot-plugin-api#RegistryChange'
|
|
|
98
98
|
* exhaustiveness checking is unsound here (unlike a closed discriminated union). A bridge that wants a
|
|
99
99
|
* foreign kind typed augments this itself; otherwise it validates at its own boundary, like any other
|
|
100
100
|
* system edge.
|
|
101
|
+
*
|
|
102
|
+
* One of matbot's five open-registry augmentation points — same technique at each; see
|
|
103
|
+
* docs/DEVELOPING.md *Open-registry augmentation* for the shared shape and the rules that follow from it.
|
|
101
104
|
*/
|
|
102
105
|
export interface Notifications {
|
|
103
106
|
'@matatbread/matbot-plugin-api#ItemChange': ItemChange;
|