@matatbread/matbot-plugin-api 0.2.4 → 0.2.6
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/invoke-tool.ts +100 -0
- package/src/plugin.ts +11 -2
- package/src/types.ts +94 -8
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { MatbotMachine } from './plugin.js';
|
|
2
|
+
import type { ToolContext, ToolEvent, ToolResultFor, PromptFn, FormField } from './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Inputs to {@link invokeTool} that a one-shot caller can't derive from the machine. A tool
|
|
6
|
+
* forwarding a call to another tool should pass its own {@link ToolContext} straight through as
|
|
7
|
+
* `opts` — `session`, `signal`, `prompt` and crucially **`provider`** all propagate, so a callee
|
|
8
|
+
* that needs an LLM (e.g. `find_fact`, or anything using `singleTurn`) inherits the same model
|
|
9
|
+
* rather than finding no provider. Only spell out individual fields when starting a call from
|
|
10
|
+
* scratch or deliberately overriding one; never hand-pick a subset of `ctx` and drop the rest.
|
|
11
|
+
*/
|
|
12
|
+
export type InvokeToolOptions =
|
|
13
|
+
Pick<ToolContext, 'session' | 'signal'>
|
|
14
|
+
& Partial<Pick<ToolContext, 'prompt' | 'provider' | 'callId'>>;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Programmatically invoke a tool by name, the same way the harness does — resolve it off the
|
|
18
|
+
* machine's tool registry, build a `ToolContext` from the machine, and return its event stream.
|
|
19
|
+
* The host-only bits a one-shot caller can't derive (the session under which the call runs, its
|
|
20
|
+
* abort signal, and an optional interactive `prompt`/`provider`) come in via `opts` — pass the
|
|
21
|
+
* calling tool's `ctx` to forward them all (see {@link InvokeToolOptions}); everything else (vault,
|
|
22
|
+
* plugin (un)loading, workdir/configPath/files) is filled from `machine`.
|
|
23
|
+
*
|
|
24
|
+
* Throws synchronously if no tool is registered under `name`. When no `prompt` is supplied the tool
|
|
25
|
+
* runs non-interactively: any attempt to prompt rejects, which a tool surfaces as a normal error
|
|
26
|
+
* event. Pair with {@link toolText} to collapse the stream to its result string.
|
|
27
|
+
*/
|
|
28
|
+
export function invokeTool<K extends string, const P>(
|
|
29
|
+
machine: MatbotMachine,
|
|
30
|
+
name: K,
|
|
31
|
+
params: P,
|
|
32
|
+
opts: InvokeToolOptions,
|
|
33
|
+
): AsyncIterable<ToolEvent<ToolResultFor<K, P>>> {
|
|
34
|
+
const tool = machine.tools.resolve(name);
|
|
35
|
+
if (tool === null) throw new Error(`Tool "${name}" is not registered`);
|
|
36
|
+
|
|
37
|
+
const prompt = opts.prompt ?? rejectingPrompt;
|
|
38
|
+
const ctx: ToolContext = {
|
|
39
|
+
callId: opts.callId ?? crypto.randomUUID(),
|
|
40
|
+
session: opts.session,
|
|
41
|
+
signal: opts.signal,
|
|
42
|
+
vault: machine.Vault,
|
|
43
|
+
prompt,
|
|
44
|
+
loadPlugin: (specifier, refresh) => machine.loadPlugin(specifier, prompt, refresh),
|
|
45
|
+
unloadPlugin: (specifier) => machine.unloadPlugin(specifier),
|
|
46
|
+
...(opts.provider !== undefined ? { provider: opts.provider } : {}),
|
|
47
|
+
...(machine.workdir !== undefined ? { workdir: machine.workdir } : {}),
|
|
48
|
+
...(machine.configPath !== undefined ? { configPath: machine.configPath } : {}),
|
|
49
|
+
...(machine.files !== undefined ? { files: machine.files } : {}),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// The registry is name-keyed and untyped (`ToolExecutor.execute` yields `ToolEvent<unknown>`); the
|
|
53
|
+
// `ToolResults` mapping is the call-site contract, asserted here so callers read a typed result.
|
|
54
|
+
return tool.executor.execute(params, ctx) as AsyncIterable<ToolEvent<ToolResultFor<K, P>>>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Drain a tool event stream (e.g. {@link invokeTool}'s return) to its raw `result` *value*, typed:
|
|
59
|
+
* paired with `invokeTool(machine, name, …)` it returns whatever `ToolResults[name]` declares (or
|
|
60
|
+
* `unknown` for an unregistered tool). This is the structured counterpart to {@link toolText} — use it
|
|
61
|
+
* for any tool that returns data; use `toolText` when you want the result collapsed to a string.
|
|
62
|
+
* Stops and throws on the first `error` event, or if the tool finished without yielding a `result`.
|
|
63
|
+
*/
|
|
64
|
+
export async function toolResult<R>(events: AsyncIterable<ToolEvent<R>>): Promise<R> {
|
|
65
|
+
let value!: R;
|
|
66
|
+
let hadResult = false;
|
|
67
|
+
for await (const ev of events) {
|
|
68
|
+
if (ev.type === 'result') { value = ev.value; hadResult = true; }
|
|
69
|
+
else if (ev.type === 'error') { throw new Error(ev.message); }
|
|
70
|
+
}
|
|
71
|
+
if (!hadResult) throw new Error('Tool produced no result');
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Drain a tool event stream (e.g. {@link invokeTool}'s return) to its result as text. Stops and
|
|
77
|
+
* throws on the first `error` event, or if the tool finished without yielding a `result`. The result
|
|
78
|
+
* value is rendered the way the model would see it: a string verbatim, a `{ content: string }`
|
|
79
|
+
* (the shape `skill_action` and other prose tools return) by its `content`, anything else as JSON.
|
|
80
|
+
*/
|
|
81
|
+
export async function toolText(events: AsyncIterable<ToolEvent>): Promise<string> {
|
|
82
|
+
let result: unknown;
|
|
83
|
+
let hadResult = false;
|
|
84
|
+
for await (const ev of events) {
|
|
85
|
+
if (ev.type === 'result') { result = ev.value; hadResult = true; }
|
|
86
|
+
else if (ev.type === 'error') { throw new Error(ev.message); }
|
|
87
|
+
}
|
|
88
|
+
if (!hadResult) throw new Error('Tool produced no result');
|
|
89
|
+
|
|
90
|
+
if (typeof result === 'string') return result;
|
|
91
|
+
if (result !== null && typeof result === 'object' && typeof (result as { content?: unknown }).content === 'string') {
|
|
92
|
+
return (result as { content: string }).content;
|
|
93
|
+
}
|
|
94
|
+
return JSON.stringify(result, null, 2);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const rejectingPrompt: PromptFn = (((p: string | FormField): Promise<string> => {
|
|
98
|
+
const label = typeof p === 'string' ? p : p.label;
|
|
99
|
+
return Promise.reject(new Error(`Non-interactive context: cannot prompt for "${label}"`));
|
|
100
|
+
}) as PromptFn);
|
package/src/plugin.ts
CHANGED
|
@@ -116,8 +116,11 @@ export interface MatbotRuntime {
|
|
|
116
116
|
* Hot-load a plugin by specifier into the running process. Returns the loaded plugin.
|
|
117
117
|
* `prompt`, when supplied, resolves tool-name collisions interactively during the new
|
|
118
118
|
* plugin's setup(); the runner injects the triggering session's prompt automatically.
|
|
119
|
+
* `refresh` (default false) forces a remote (github/http) plugin's `.plugins/` cache subtree
|
|
120
|
+
* to be evicted and re-downloaded — a reload of changed upstream source. Cache-first otherwise:
|
|
121
|
+
* a programmatic loader gets the cached copy and stays offline-tolerant.
|
|
119
122
|
*/
|
|
120
|
-
loadPlugin(specifier: string, prompt?: PromptFn): Promise<MatbotPlugin>;
|
|
123
|
+
loadPlugin(specifier: string, prompt?: PromptFn, refresh?: boolean): Promise<MatbotPlugin>;
|
|
121
124
|
|
|
122
125
|
/**
|
|
123
126
|
* Hot-unload a plugin by specifier, removing its tools, hooks, and system context contributions.
|
|
@@ -488,7 +491,13 @@ export interface MatbotPluginSpec {
|
|
|
488
491
|
*/
|
|
489
492
|
export interface MatbotPlugin extends MatbotPluginSpec {
|
|
490
493
|
readonly name: string; // resolver.identify(specifier)
|
|
491
|
-
readonly specifier: string; // how it was loaded
|
|
494
|
+
readonly specifier: string; // how it was loaded (config entry / pre-resolved)
|
|
495
|
+
/** The stable URL the loader actually imported, minus any reload cache-bust stamp — a `file:` URL
|
|
496
|
+
* for local/remote-cached plugins, the bare specifier when nothing was pre-resolved. Lets a consumer
|
|
497
|
+
* map a loaded plugin back to its on-disk source without re-running specifier resolution (e.g.
|
|
498
|
+
* skills_compiler builds a types program over the live plugin set). Set by the loader; absent only
|
|
499
|
+
* on hosts that construct MatbotPlugin by hand. */
|
|
500
|
+
readonly resolvedUrl?: string;
|
|
492
501
|
readonly source?: PluginSource;
|
|
493
502
|
// The package.json `matbotRuntime`, captured at load from the same value used for the pre-import
|
|
494
503
|
// gate (the host reads it once; downstream just reads this). Absent ⇒ undeclared. Stored rather
|
package/src/types.ts
CHANGED
|
@@ -431,6 +431,11 @@ export interface KnowledgeEntry {
|
|
|
431
431
|
|
|
432
432
|
export interface KnowledgeIndex {
|
|
433
433
|
index(entry: KnowledgeEntry): Promise<void>;
|
|
434
|
+
/** Remove the entry with this id, if present (idempotent — removing an absent id is a no-op). The
|
|
435
|
+
* id is the index's sole primary key (`index` replaces by it), so retraction is by id alone; the
|
|
436
|
+
* index never inspects an entry's opaque `source`. The party that indexed an entry owns retracting
|
|
437
|
+
* it — e.g. a skill manager retracts a hidden/deleted skill rather than the index policing tenants. */
|
|
438
|
+
remove(id: string): Promise<void>;
|
|
434
439
|
search(terms: Array<{ term: string; context?: string }>, signal: AbortSignal): Promise<KnowledgeEntry[]>;
|
|
435
440
|
/** Enumerate all indexed entries. When present, register('KnowledgeIndex', …) drains these into the incoming backend. */
|
|
436
441
|
entries?(): Iterable<KnowledgeEntry>;
|
|
@@ -438,11 +443,11 @@ export interface KnowledgeIndex {
|
|
|
438
443
|
|
|
439
444
|
// ── Tools ─────────────────────────────────────────────────────────────────────
|
|
440
445
|
|
|
441
|
-
export type ToolEvent =
|
|
446
|
+
export type ToolEvent<Result = unknown> =
|
|
442
447
|
| { type: 'stdout'; chunk: string }
|
|
443
448
|
| { type: 'stderr'; chunk: string }
|
|
444
449
|
| { type: 'progress'; pct: number; message?: string }
|
|
445
|
-
| { type: 'result'; value:
|
|
450
|
+
| { type: 'result'; value: Result }
|
|
446
451
|
| { type: 'file'; handle: FileHandle }
|
|
447
452
|
// A durable, LLM-invisible annotation the tool emits as it runs (a link, a status, a trace of a
|
|
448
453
|
// side-effect). Persisted as a `marker`-role message; elided from provider submission like any
|
|
@@ -451,6 +456,76 @@ export type ToolEvent =
|
|
|
451
456
|
| { type: 'marker'; creator: string; data: unknown }
|
|
452
457
|
| { type: 'error'; message: string; code?: number; stdout?: string; stderr?: string };
|
|
453
458
|
|
|
459
|
+
/**
|
|
460
|
+
* Per-tool result-type registry: maps a tool's `name` to the type of the `value` it yields in its
|
|
461
|
+
* `result` event. Augment it exactly like {@link MarkerData} / `MatbotServices`, so a caller gets the
|
|
462
|
+
* concrete result type back from {@link invokeTool} + `toolResult` instead of `unknown`:
|
|
463
|
+
*
|
|
464
|
+
* declare module '@matatbread/matbot-plugin-api' {
|
|
465
|
+
* interface ToolResults { find_fact: string[] | null }
|
|
466
|
+
* }
|
|
467
|
+
*
|
|
468
|
+
* The tool name is the discriminator: `invokeTool(machine, 'find_fact', …)` is typed
|
|
469
|
+
* `AsyncIterable<ToolEvent<string[] | null>>`, so `toolResult(…)` resolves to `string[] | null`. A
|
|
470
|
+
* name with no registered entry resolves to `unknown` (the caller must narrow). This is a pure
|
|
471
|
+
* type-level construct — no runtime validation; it pins, at the call site, the contract a tool's
|
|
472
|
+
* executor already produces. Keep the entry in sync with what the executor actually yields.
|
|
473
|
+
*
|
|
474
|
+
* **Multi-action tools** are a weird form of overloaded function: the same tool returns different
|
|
475
|
+
* shapes depending on its params. Register such a tool as a union of {@link ToolResult} *arms*, each
|
|
476
|
+
* pairing a result with the discriminating params *pattern* that selects it — `invokeTool` matches the
|
|
477
|
+
* call's literal params against the patterns and narrows the result to the matching arm:
|
|
478
|
+
*
|
|
479
|
+
* declare module '@matatbread/matbot-plugin-api' {
|
|
480
|
+
* interface ToolResults {
|
|
481
|
+
* session_action:
|
|
482
|
+
* | ToolResult<Session, { action: 'get' }>
|
|
483
|
+
* | ToolResult<{ id: string; title: string }, { action: 'rename' }>;
|
|
484
|
+
* }
|
|
485
|
+
* }
|
|
486
|
+
* // invokeTool(machine, 'session_action', { action: 'get', sessionId }) → result is Session
|
|
487
|
+
*
|
|
488
|
+
* The pattern is *any* discriminating field(s), not just `action` (a tool keying on `interval`'s
|
|
489
|
+
* presence registers `ToolResult<…, { interval: string }>`). It is a *pattern*, not the full input:
|
|
490
|
+
* key only on the discriminant so a call carrying just that field still matches. When no arm's pattern
|
|
491
|
+
* matches (a non-literal discriminant, or an absence-discriminant the positive patterns can't express),
|
|
492
|
+
* the result falls back to the union of every arm — always sound, just less narrow.
|
|
493
|
+
*/
|
|
494
|
+
export interface ToolResults {}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* One overload arm of a multi-action tool registered in {@link ToolResults}: the `Result` it yields
|
|
498
|
+
* for a call whose params match the discriminating pattern `Args`. `Args` defaults to `unknown` (an
|
|
499
|
+
* arm that matches any params). Purely type-level — the `__` fields are phantom and never constructed.
|
|
500
|
+
*/
|
|
501
|
+
export interface ToolResult<Result, Args = unknown> {
|
|
502
|
+
readonly __result: Result;
|
|
503
|
+
readonly __args: Args;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
type ToolResultArmed<E> = E extends ToolResult<unknown, unknown> ? true : false;
|
|
507
|
+
type ToolResultUnion<E> = E extends ToolResult<infer R, unknown> ? R : never;
|
|
508
|
+
type ToolResultMatched<E, P> = E extends ToolResult<infer R, infer A> ? (P extends A ? R : never) : never;
|
|
509
|
+
|
|
510
|
+
/** The full set of `result` values a tool named `K` may yield — the union over all its arms (for an
|
|
511
|
+
* arm-based entry), the entry itself (for a plain entry), or `unknown` when `K` is unregistered. This
|
|
512
|
+
* is what an executor binds against (`ToolExecutor<ToolResultOf<'my_tool'>>`): it must cover every arm. */
|
|
513
|
+
export type ToolResultOf<K extends string> =
|
|
514
|
+
K extends keyof ToolResults
|
|
515
|
+
? ToolResultArmed<ToolResults[K]> extends true ? ToolResultUnion<ToolResults[K]> : ToolResults[K]
|
|
516
|
+
: unknown;
|
|
517
|
+
|
|
518
|
+
/** The `result` a call to tool `K` with params `P` yields: for an arm-based entry, the arm whose
|
|
519
|
+
* pattern `P` matches (or the full union when none match — always sound); otherwise {@link ToolResultOf}. */
|
|
520
|
+
export type ToolResultFor<K extends string, P> =
|
|
521
|
+
K extends keyof ToolResults
|
|
522
|
+
? ToolResultArmed<ToolResults[K]> extends true
|
|
523
|
+
? ([ToolResultMatched<ToolResults[K], P>] extends [never]
|
|
524
|
+
? ToolResultUnion<ToolResults[K]>
|
|
525
|
+
: ToolResultMatched<ToolResults[K], P>)
|
|
526
|
+
: ToolResults[K]
|
|
527
|
+
: unknown;
|
|
528
|
+
|
|
454
529
|
/**
|
|
455
530
|
* Ask the user a question and resolve with their answer. The host supplies the
|
|
456
531
|
* implementation — readline in the CLI, an SSE round-trip in the web frontend. This is
|
|
@@ -488,8 +563,10 @@ export interface ToolContext {
|
|
|
488
563
|
files?: FileStore;
|
|
489
564
|
/** Prompt the user for input. The host provides a readline or form implementation. */
|
|
490
565
|
prompt: PromptFn;
|
|
491
|
-
/** Hot-load a plugin by specifier without restarting the process. Returns the loaded plugin.
|
|
492
|
-
|
|
566
|
+
/** Hot-load a plugin by specifier without restarting the process. Returns the loaded plugin.
|
|
567
|
+
* `refresh` (default false) forces a remote (github/http) plugin to be re-downloaded rather than
|
|
568
|
+
* re-loaded from its `.plugins/` cache — pass true to pick up changed upstream source. */
|
|
569
|
+
loadPlugin(specifier: string, refresh?: boolean): Promise<MatbotPlugin>;
|
|
493
570
|
/**
|
|
494
571
|
* Hot-unload a plugin by specifier, removing its tools, hooks, and system context contributions.
|
|
495
572
|
* Resolves `true` if a plugin was actually resident and unloaded, `false` if there was nothing
|
|
@@ -498,15 +575,23 @@ export interface ToolContext {
|
|
|
498
575
|
unloadPlugin(specifier: string): Promise<boolean>;
|
|
499
576
|
}
|
|
500
577
|
|
|
501
|
-
|
|
502
|
-
|
|
578
|
+
/**
|
|
579
|
+
* A tool's runtime. `R` is the type of the `value` carried by its `result` event — declared once,
|
|
580
|
+
* at the source, so the executor's yields and the tool's {@link ToolResults} registry entry can't
|
|
581
|
+
* silently drift. The `unknown` default keeps untyped executors compiling untouched, and covariance
|
|
582
|
+
* means a narrower `ToolExecutor<X>` still satisfies `ToolExecutor` (i.e. `ToolExecutor<unknown>`),
|
|
583
|
+
* so the heterogeneous registry boundary (`Tool[]`) accepts any executor. Bind `R` to the registry
|
|
584
|
+
* entry with `ToolExecutor<ToolResultOf<'my_tool'>>` so the single augmentation is the source of truth.
|
|
585
|
+
*/
|
|
586
|
+
export interface ToolExecutor<R = unknown> {
|
|
587
|
+
execute(input: unknown, ctx: ToolContext): AsyncIterable<ToolEvent<R>>;
|
|
503
588
|
}
|
|
504
589
|
|
|
505
|
-
export interface Tool {
|
|
590
|
+
export interface Tool<R = unknown> {
|
|
506
591
|
name: string;
|
|
507
592
|
description: string;
|
|
508
593
|
inputSchema: JSONSchema;
|
|
509
|
-
executor: ToolExecutor
|
|
594
|
+
executor: ToolExecutor<R>;
|
|
510
595
|
pluginName?: string;
|
|
511
596
|
}
|
|
512
597
|
|
|
@@ -649,6 +734,7 @@ export type PipelineEvent =
|
|
|
649
734
|
| { type: 'tool:start'; callId: string; name: string; input: unknown; traceId: string }
|
|
650
735
|
| { type: 'tool:stdout'; callId: string; chunk: string; traceId: string }
|
|
651
736
|
| { type: 'tool:stderr'; callId: string; chunk: string; traceId: string }
|
|
737
|
+
| { type: 'tool:progress'; callId: string; pct: number; message?: string; traceId: string }
|
|
652
738
|
| { type: 'tool:end'; callId: string; result: unknown; isError: boolean; traceId: string }
|
|
653
739
|
| { type: 'file'; handle: FileHandle; traceId: string }
|
|
654
740
|
| ({ type: 'usage'; traceId: string } & Usage)
|