@matatbread/matbot-plugin-api 0.3.7 → 0.4.0
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 +191 -0
- package/src/host.ts +43 -0
- package/src/index.ts +27 -8
- package/src/invoke-tool.ts +25 -3
- package/src/notify.ts +242 -0
- package/src/plugin.ts +40 -205
- package/src/session.ts +30 -1
- package/src/store-query.ts +12 -0
- 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 -1101
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { PipelineEvent } from './events.js';
|
|
2
|
+
import type { MessageContent, Session } from './messages.js';
|
|
3
|
+
import type { Principal } from './principal.js';
|
|
4
|
+
import type { SteeringMode } from './steering.js';
|
|
5
|
+
import type { PromptFn } from './tools.js';
|
|
6
|
+
|
|
7
|
+
// ── Session runner ──────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A view onto a session returned by `SessionRunner.open`. `session` is the authoritative
|
|
11
|
+
* server-side state — committed messages plus an overlay of any queued-but-not-yet-run
|
|
12
|
+
* submissions (each carrying `metadata.pending: true`). `events` is a lazy, per-session live
|
|
13
|
+
* tap: accessing it subscribes to the turn event stream from now (replaying the in-flight
|
|
14
|
+
* turn, if any); never touching it costs nothing.
|
|
15
|
+
*/
|
|
16
|
+
export interface SessionView {
|
|
17
|
+
session: Session;
|
|
18
|
+
/** Submissions waiting behind the current turn (does not count the running turn). */
|
|
19
|
+
queued: number;
|
|
20
|
+
/** Correlation id of the submission this call enqueued — present only when content was supplied. */
|
|
21
|
+
traceId?: string;
|
|
22
|
+
readonly events: AsyncIterable<PipelineEvent>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Observe a session without submitting anything. */
|
|
26
|
+
export interface OpenOpts {
|
|
27
|
+
sessionId: string;
|
|
28
|
+
signal: AbortSignal;
|
|
29
|
+
/** Optional caller-supplied correlation id; one is generated when absent. */
|
|
30
|
+
traceId?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Observe a session AND enqueue a submission. The compiler enforces provider/principal here;
|
|
34
|
+
* a remote frontend deserializing a request body must still validate the wire input itself. */
|
|
35
|
+
export interface SubmitOpenOpts extends OpenOpts {
|
|
36
|
+
content: MessageContent[];
|
|
37
|
+
provider: string;
|
|
38
|
+
principal: Principal;
|
|
39
|
+
/** When true, this submission may be merged with others drained in the same batch. Default false
|
|
40
|
+
* (queue mode: one turn per submission). */
|
|
41
|
+
concatQueue?: boolean;
|
|
42
|
+
/** Disposition for a submission arriving while a turn is running (see {@link SteeringPolicy}):
|
|
43
|
+
* 'queue' waits for the turn boundary (default), 'interrupt' stops the running turn — keeping its
|
|
44
|
+
* committed partial work — and runs this next, 'auto' defers to the registered SteeringPolicy (else
|
|
45
|
+
* the host default). Meaningless when nothing is running (degrades to a plain enqueue). */
|
|
46
|
+
mode?: SteeringMode;
|
|
47
|
+
/** Interactive prompt implementation for this submission's turn. The frontend owns delivery —
|
|
48
|
+
* it must target the frontend's per-session client connections, not a single request. */
|
|
49
|
+
prompt?: PromptFn;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Serialises turns per session. A submission never executes concurrently with another for the
|
|
54
|
+
* same session; the in-memory queue (lost on process restart, by design) absorbs anything that
|
|
55
|
+
* arrives mid-turn. The server is the source of truth: a frontend renders whatever `open()`
|
|
56
|
+
* returns and treats the live `events` stream purely as an optimisation.
|
|
57
|
+
*/
|
|
58
|
+
export interface SessionRunner {
|
|
59
|
+
open(opts: OpenOpts | SubmitOpenOpts): Promise<SessionView>;
|
|
60
|
+
/** Abort the running turn (if any) and drop all queued submissions, emitting `cancelled` for each. */
|
|
61
|
+
abort(sessionId: string): void;
|
|
62
|
+
/** Abandon the running turn (if any) WITHOUT touching the queue — `pump` advances to the next
|
|
63
|
+
* queued submission, or idles. The "give up on this turn" path (a prompt cancel); contrast
|
|
64
|
+
* `abort`, which also clears the queue. A no-op if nothing is running. */
|
|
65
|
+
cancelTurn(sessionId: string): void;
|
|
66
|
+
/** Snapshot of a session's live state: whether a turn is running and how many submissions wait
|
|
67
|
+
* behind it. `busy` is `running || queued > 0`. */
|
|
68
|
+
status(sessionId: string): { busy: boolean; running: boolean; queued: number };
|
|
69
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { MessageContent, Session } from './messages.js';
|
|
2
|
+
|
|
3
|
+
// ── Steering ────────────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
/** Disposition of a submission that arrives while a turn is running. `interrupt` stops the running
|
|
6
|
+
* turn (its committed partial work is preserved) and runs the new message next; `queue` waits for the
|
|
7
|
+
* turn boundary. */
|
|
8
|
+
export type SteeringDecision = 'queue' | 'interrupt';
|
|
9
|
+
/** The wire-level request (see `SubmitOpenOpts.mode`). `auto` defers the decision to the registered
|
|
10
|
+
* {@link SteeringPolicy} (else the host default). */
|
|
11
|
+
export type SteeringMode = SteeringDecision | 'auto';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* How the runner disposes of a mid-turn submission under `mode: 'auto'`, and how an interrupt's
|
|
15
|
+
* continuation is nudged. An optional, registerable service ({@link MatbotServices}); absent ⇒ the
|
|
16
|
+
* runner uses its own defaults. Both members are optional so a plugin may override one, the other, or
|
|
17
|
+
* both. `classify` is deliberately NOT assumed to be an LLM — a regex or a semantic classifier is a
|
|
18
|
+
* first-class implementation (return synchronously); an LLM `singleTurn` returns a promise.
|
|
19
|
+
*/
|
|
20
|
+
export interface SteeringPolicy {
|
|
21
|
+
/**
|
|
22
|
+
* Consulted only for `mode: 'auto'` while a turn is running. `session` is the COMMITTED session
|
|
23
|
+
* (history up to the running turn's start — the in-flight partial is not reachable at submit time);
|
|
24
|
+
* `steer` is the incoming submission. Return synchronously (regex) or async (semantic / singleTurn).
|
|
25
|
+
* Absent ⇒ the host default disposition.
|
|
26
|
+
*/
|
|
27
|
+
classify?(ctx: { session: Session; steer: MessageContent[] }): SteeringDecision | Promise<SteeringDecision>;
|
|
28
|
+
/**
|
|
29
|
+
* The ephemeral "keep going, noting the above" context folded onto an interrupt's continuation turn
|
|
30
|
+
* (never persisted). Absent ⇒ the host default nudge.
|
|
31
|
+
*/
|
|
32
|
+
nudge?(ctx: { session: Session; steer: MessageContent[] }): MessageContent[];
|
|
33
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { QueryResult, StoreQuery } from '../store-query.js';
|
|
2
|
+
|
|
3
|
+
// ── Storage ───────────────────────────────────────────────────────────────────
|
|
4
|
+
// The query grammar (Filter AST, StoreQuery, QueryResult, StoreQueryError) lives in ./store-query.
|
|
5
|
+
|
|
6
|
+
export type CASResult<T> =
|
|
7
|
+
| { ok: true; doc: T }
|
|
8
|
+
| { ok: false; current: T | null };
|
|
9
|
+
|
|
10
|
+
export interface Store<T extends { id: string; version: string }> {
|
|
11
|
+
get(id: string): Promise<T | null>;
|
|
12
|
+
set(id: string, value: T): Promise<void>;
|
|
13
|
+
cas(id: string, expected: string, next: T): Promise<CASResult<T>>;
|
|
14
|
+
delete(id: string, expectedVersion?: string): Promise<boolean>;
|
|
15
|
+
query(q: StoreQuery): Promise<QueryResult<T>>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Session } from './messages.js';
|
|
2
|
+
|
|
3
|
+
// ── System context ────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export type SystemContextContributor = (ctx: {
|
|
6
|
+
session: Session;
|
|
7
|
+
signal: AbortSignal;
|
|
8
|
+
}) => string | null | Promise<string | null>;
|
|
9
|
+
|
|
10
|
+
export interface SystemContextRegistry {
|
|
11
|
+
register(contributor: SystemContextContributor, pluginName?: string): void;
|
|
12
|
+
removeByPlugin(pluginName: string): void;
|
|
13
|
+
/** Calls all contributors and joins non-null, non-empty results with double newlines. */
|
|
14
|
+
build(ctx: { session: Session; signal: AbortSignal }): Promise<string | null>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import type { FileHandle, FileStore } from './files.js';
|
|
2
|
+
import type { FormField, MessageContent, Session } from './messages.js';
|
|
3
|
+
import type { MatbotPlugin } from '../plugin.js';
|
|
4
|
+
import type { JSONSchema } from './primitives.js';
|
|
5
|
+
import type { Vault } from './vault.js';
|
|
6
|
+
|
|
7
|
+
// ── Tools ─────────────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Media a tool hands to the model to *look at*, as opposed to the JSON it reads as a `result`. The
|
|
11
|
+
* inline arms of {@link MessageContent} — bytes plus a mime type, carrying their own meaning with no
|
|
12
|
+
* dependency on anything that could resolve a reference. Where the tool got the bytes (a `FileStore`,
|
|
13
|
+
* an HTTP fetch, a chart rendered in memory) is entirely the tool's business; the harness never asks.
|
|
14
|
+
*/
|
|
15
|
+
export type ModelContent = Extract<MessageContent, { type: 'image' | 'document' | 'audio' }>;
|
|
16
|
+
|
|
17
|
+
export type ToolEvent<Result = unknown> =
|
|
18
|
+
| { type: 'stdout'; chunk: string }
|
|
19
|
+
| { type: 'stderr'; chunk: string }
|
|
20
|
+
| { type: 'progress'; pct: number; message?: string }
|
|
21
|
+
| { type: 'result'; value: Result }
|
|
22
|
+
| { type: 'file'; handle: FileHandle }
|
|
23
|
+
// Media for the model's eyes, pinned to this tool call's result and carried on the wire for the
|
|
24
|
+
// REST OF THIS TURN only — never persisted. Independent of `result`: the durable record of what the
|
|
25
|
+
// tool did is its `result`, and a transcript that re-reads clean is the point. A later turn that
|
|
26
|
+
// needs the bytes again calls the tool again, which is the same pull the model already performed.
|
|
27
|
+
//
|
|
28
|
+
// Rest-of-turn rather than next-call-only because withdrawing content the model has already seen
|
|
29
|
+
// mid-history breaks the prompt cache from that point and leaves it referring to something no longer
|
|
30
|
+
// there. The corollary is cost: a large document is re-sent on every subsequent round of the turn,
|
|
31
|
+
// so a tool should hand over the smallest thing that answers the question (a page range, a thumbnail)
|
|
32
|
+
// and `maxRounds` is the ceiling on how often it is paid for.
|
|
33
|
+
| { type: 'model-content'; content: ModelContent[] }
|
|
34
|
+
// A durable, LLM-invisible annotation the tool emits as it runs (a link, a status, a trace of a
|
|
35
|
+
// side-effect). Persisted as a `marker`-role message; elided from provider submission like any
|
|
36
|
+
// marker. Independent of `result` — a tool may emit markers and no result (a silent side-effect,
|
|
37
|
+
// e.g. a trigger-fired tool), a result and no markers, or both.
|
|
38
|
+
| { type: 'marker'; creator: string; data: unknown }
|
|
39
|
+
| { type: 'error'; message: string; code?: number; stdout?: string; stderr?: string };
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Per-tool call-contract registry: maps a tool's `name` to a {@link ToolContract} arm (or a union of
|
|
43
|
+
* them) pairing the `value` it yields in its `result` event with the params that produce it. Augment it
|
|
44
|
+
* exactly like {@link MarkerData} / `MatbotServices`, so a caller gets the concrete result type back
|
|
45
|
+
* from {@link invokeTool} + `toolResult` instead of `unknown`:
|
|
46
|
+
*
|
|
47
|
+
* declare module '@matatbread/matbot-plugin-api' {
|
|
48
|
+
* interface ToolContracts {
|
|
49
|
+
* find_fact: ToolContract<string[] | null, { question: string; terms: { term: string }[] }>;
|
|
50
|
+
* }
|
|
51
|
+
* }
|
|
52
|
+
*
|
|
53
|
+
* The tool name is the discriminator: `invokeTool(machine, 'find_fact', …)` is typed
|
|
54
|
+
* `AsyncIterable<ToolEvent<string[] | null>>`, so `toolResult(…)` resolves to `string[] | null`. A
|
|
55
|
+
* name with no registered entry resolves to `unknown` (the caller must narrow). This is a pure
|
|
56
|
+
* type-level construct — no runtime validation; it pins, at the call site, the contract a tool's
|
|
57
|
+
* executor already produces. Keep the entry in sync with what the executor actually yields.
|
|
58
|
+
*
|
|
59
|
+
* Even a single-action tool declares an **arm**, never a bare `name: Result`. The bare form still
|
|
60
|
+
* type-checks — `ToolResultOf`/`ToolResultFor` keep a plain-entry path so an unregistered or foreign
|
|
61
|
+
* tool stays loose — but it carries no params, so {@link ToolProxy} derives no call signature from it
|
|
62
|
+
* and `await tool.find_fact(…)` is uncallable.
|
|
63
|
+
*
|
|
64
|
+
* **Multi-action tools** are a weird form of overloaded function: the same tool returns different
|
|
65
|
+
* shapes depending on its params. Register such a tool as a union of {@link ToolContract} *arms*, each
|
|
66
|
+
* pairing a result with the discriminating params *pattern* that selects it — `invokeTool` matches the
|
|
67
|
+
* call's literal params against the patterns and narrows the result to the matching arm:
|
|
68
|
+
*
|
|
69
|
+
* declare module '@matatbread/matbot-plugin-api' {
|
|
70
|
+
* interface ToolContracts {
|
|
71
|
+
* session_action:
|
|
72
|
+
* | ToolContract<Session, { action: 'get' }>
|
|
73
|
+
* | ToolContract<{ id: string; title: string }, { action: 'rename' }>;
|
|
74
|
+
* }
|
|
75
|
+
* }
|
|
76
|
+
* // invokeTool(machine, 'session_action', { action: 'get', sessionId }) → result is Session
|
|
77
|
+
*
|
|
78
|
+
* The pattern is *any* discriminating field(s), not just `action` (a tool keying on `interval`'s
|
|
79
|
+
* presence registers `ToolContract<…, { interval: string }>`). It is a *pattern*, not the full input:
|
|
80
|
+
* key only on the discriminant so a call carrying just that field still matches. When no arm's pattern
|
|
81
|
+
* matches (a non-literal discriminant, or an absence-discriminant the positive patterns can't express),
|
|
82
|
+
* the result falls back to the union of every arm — always sound, just less narrow.
|
|
83
|
+
*
|
|
84
|
+
* One of matbot's five open-registry augmentation points — same technique at each; see
|
|
85
|
+
* docs/DEVELOPING.md *Open-registry augmentation* for the shared shape and the rules that follow from it.
|
|
86
|
+
*/
|
|
87
|
+
export interface ToolContracts {}
|
|
88
|
+
|
|
89
|
+
// Phantom carriers for ToolContract's two parameters. `unique symbol` keys rather than `__result`/`__args`
|
|
90
|
+
// so the type is *uninhabitable*: a plugin author cannot name these keys, so no value of ToolContract can
|
|
91
|
+
// be written down by accident (or on purpose, to smuggle a shape past the checker) — where the previous
|
|
92
|
+
// `__`-prefixed properties were merely conventionally-private and constructible with a couple of casts.
|
|
93
|
+
// A cast is the one hole through which a hallucinated result shape survives to runtime, which is why the
|
|
94
|
+
// generated-code checker closes it deterministically; this closes the same hole in the type itself.
|
|
95
|
+
declare const RESULT: unique symbol;
|
|
96
|
+
declare const ARGS: unique symbol;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* One overload arm of a multi-action tool registered in {@link ToolContracts}: the `Result` it yields
|
|
100
|
+
* for a call whose params match the discriminating pattern `Args`. `Args` defaults to `unknown` (an
|
|
101
|
+
* arm that matches any params). Purely type-level — the fields below are phantom and uninstantiable.
|
|
102
|
+
*/
|
|
103
|
+
export interface ToolContract<Result, Args = unknown> {
|
|
104
|
+
readonly [RESULT]: Result;
|
|
105
|
+
readonly [ARGS]: Args;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
type ToolResultArmed<E> = E extends ToolContract<unknown, unknown> ? true : false;
|
|
109
|
+
type ToolResultUnion<E> = E extends ToolContract<infer R, unknown> ? R : never;
|
|
110
|
+
type ToolResultMatched<E, P> = E extends ToolContract<infer R, infer A> ? (P extends A ? R : never) : never;
|
|
111
|
+
|
|
112
|
+
/** The full set of `result` values a tool named `K` may yield — the union over all its arms (for an
|
|
113
|
+
* arm-based entry), the entry itself (for a plain entry), or `unknown` when `K` is unregistered. This
|
|
114
|
+
* is what an executor binds against (`ToolExecutor<ToolResultOf<'my_tool'>>`): it must cover every arm. */
|
|
115
|
+
export type ToolResultOf<K extends string> =
|
|
116
|
+
K extends keyof ToolContracts
|
|
117
|
+
? ToolResultArmed<ToolContracts[K]> extends true ? ToolResultUnion<ToolContracts[K]> : ToolContracts[K]
|
|
118
|
+
: unknown;
|
|
119
|
+
|
|
120
|
+
/** The `result` a call to tool `K` with params `P` yields: for an arm-based entry, the arm whose
|
|
121
|
+
* pattern `P` matches (or the full union when none match — always sound); otherwise {@link ToolResultOf}. */
|
|
122
|
+
export type ToolResultFor<K extends string, P> =
|
|
123
|
+
K extends keyof ToolContracts
|
|
124
|
+
? ToolResultArmed<ToolContracts[K]> extends true
|
|
125
|
+
? ([ToolResultMatched<ToolContracts[K], P>] extends [never]
|
|
126
|
+
? ToolResultUnion<ToolContracts[K]>
|
|
127
|
+
: ToolResultMatched<ToolContracts[K], P>)
|
|
128
|
+
: ToolContracts[K]
|
|
129
|
+
: unknown;
|
|
130
|
+
|
|
131
|
+
// Distribute a union to its intersection: `A | B` → `A & B`. An intersection of function types is an
|
|
132
|
+
// overload set, so this turns a union of per-arm call signatures into an overloaded function.
|
|
133
|
+
type UnionToIntersection<U> =
|
|
134
|
+
(U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
|
|
135
|
+
|
|
136
|
+
// One `ToolContracts` arm → its call signature. A fully-untyped arm (`ToolContract<unknown, unknown>`, the
|
|
137
|
+
// fallback for a foreign/MCP tool carrying only a JSON-Schema `inputSchema`) keeps a loose, optional-param
|
|
138
|
+
// signature so it stays callable; a typed arm requires its declared params.
|
|
139
|
+
type ArmCallSig<E> =
|
|
140
|
+
E extends ToolContract<infer R, infer P>
|
|
141
|
+
? (unknown extends P ? (params?: unknown) => Promise<R> : (params: P) => Promise<R>)
|
|
142
|
+
: never;
|
|
143
|
+
|
|
144
|
+
// The union of a tool's arm params — the parameter type of the trailing catch-all signature below.
|
|
145
|
+
type ArmArgsUnion<E> = E extends ToolContract<unknown, infer A> ? A : never;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The type of the injected `tool` proxy that `function-tools` and compiled skills call
|
|
149
|
+
* (`await tool.name(params)`). Each key is derived from its {@link ToolContracts} entry: a multi-arm
|
|
150
|
+
* (multi-action) entry becomes an **overload set** — one `(params) => Promise<result>` signature per arm —
|
|
151
|
+
* so `await tool.name(params)` narrows its result by the params passed, exactly the contract a code
|
|
152
|
+
* generator needs to write a correct call first time. A single-arm entry is one signature. This is what the
|
|
153
|
+
* generated `dts()` declares `tool` as; it is *derived* — never hand-authored — from the same `ToolContracts`
|
|
154
|
+
* augmentation that types authors' `invokeTool`/`toolResult` calls, so the human editor and the LLM see one
|
|
155
|
+
* source of truth.
|
|
156
|
+
*
|
|
157
|
+
* The trailing catch-all signature (params union → result union) exists for soundness at the *meta-type*
|
|
158
|
+
* level, not for call sites: overload resolution is first-match, so a well-formed call still hits its
|
|
159
|
+
* specific arm and narrows precisely, and a malformed call matches nothing and still errors. What it
|
|
160
|
+
* changes: `ReturnType<typeof tool.x>` — which on a bare overload set resolves to an arbitrary (last) arm,
|
|
161
|
+
* a real-world codegen trap — now degrades to the sound full union; and a params value typed as a union of
|
|
162
|
+
* arms (dynamic multi-action dispatch) becomes callable, returning the result union.
|
|
163
|
+
*/
|
|
164
|
+
export type ToolProxy = { [K in keyof ToolContracts]:
|
|
165
|
+
UnionToIntersection<ArmCallSig<ToolContracts[K]>>
|
|
166
|
+
& ((params: ArmArgsUnion<ToolContracts[K]>) => Promise<ToolResultUnion<ToolContracts[K]>>) };
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Ask the user a question and resolve with their answer. The host supplies the
|
|
170
|
+
* implementation — readline in the CLI, an SSE round-trip in the web frontend. This is
|
|
171
|
+
* matbot's single mechanism for eliciting user input at runtime; it is injected both into
|
|
172
|
+
* tool execution (`ToolContext.prompt`) and into plugin loading (collision resolution in setup).
|
|
173
|
+
*
|
|
174
|
+
* Two call forms:
|
|
175
|
+
* - `(question, defaultValue?)` — free text; resolves to the typed string (or the default).
|
|
176
|
+
* - `(field: FormField)` — a single structured field (`select`/`confirm`/`password`/`text`),
|
|
177
|
+
* letting rich frontends render real controls (buttons, masked input). Frontends that can't
|
|
178
|
+
* render it fall back to `field.label` as plain text. Resolves to the chosen/typed value.
|
|
179
|
+
*
|
|
180
|
+
* Cancellation is the "we can't proceed — give up" path (distinct from a graceful "decline", which
|
|
181
|
+
* is simply one of the offered `options`). The host rejects the promise with `PromptCancelledError`
|
|
182
|
+
* and abandons the current turn, returning to idle; a caller's surrounding try/catch turns the
|
|
183
|
+
* rejection into a tool error that closes the tool call cleanly.
|
|
184
|
+
* This reuses `FormField` rather than a parallel type; it is a one-shot request/response and
|
|
185
|
+
* deliberately does NOT engage the session-bound `form`/`form-response` flow.
|
|
186
|
+
*/
|
|
187
|
+
export interface PromptFn {
|
|
188
|
+
(question: string, defaultValue?: string): Promise<string>;
|
|
189
|
+
(field: FormField): Promise<string>;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface ToolContext {
|
|
193
|
+
callId: string;
|
|
194
|
+
session: Session;
|
|
195
|
+
signal: AbortSignal;
|
|
196
|
+
vault: Vault;
|
|
197
|
+
/** The provider key driving the current turn (`RunConfig.provider`). A tool that spawns further
|
|
198
|
+
* work should default to this so the child inherits the same model rather than the config default. */
|
|
199
|
+
provider?: string;
|
|
200
|
+
workdir?: string;
|
|
201
|
+
configPath?: string;
|
|
202
|
+
files?: FileStore;
|
|
203
|
+
/** Prompt the user for input. The host provides a readline or form implementation. */
|
|
204
|
+
prompt: PromptFn;
|
|
205
|
+
/** Hot-load a plugin by specifier without restarting the process. Returns the loaded plugin.
|
|
206
|
+
* `refresh` (default false) forces a remote (github/http) plugin to be re-downloaded rather than
|
|
207
|
+
* re-loaded from its `.plugins/` cache — pass true to pick up changed upstream source. */
|
|
208
|
+
loadPlugin(specifier: string, refresh?: boolean): Promise<MatbotPlugin>;
|
|
209
|
+
/**
|
|
210
|
+
* Hot-unload a plugin by specifier, removing its tools, hooks, and system context contributions.
|
|
211
|
+
* Resolves `true` if a plugin was actually resident and unloaded, `false` if there was nothing
|
|
212
|
+
* to unload. A failed `teardown()` (e.g. timeout) still throws — the plugin was resident in that case.
|
|
213
|
+
*/
|
|
214
|
+
unloadPlugin(specifier: string): Promise<boolean>;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* The read-only facts about the call a composed function is running under, injected into a
|
|
219
|
+
* `tool_function` body as the ambient `context` binding beside `tool`/`toolInContext`. Deliberately
|
|
220
|
+
* narrower than {@link ToolContext}: the identity of the call, not its capabilities — `vault`,
|
|
221
|
+
* `files`, `prompt` and plugin (un)loading stay off a surface authored by a model and reachable only
|
|
222
|
+
* through the tool contracts. A tool with real source takes `ToolContext` and needs none of this.
|
|
223
|
+
*
|
|
224
|
+
* Not `ToolCallContext` — that name belongs to the `toolcall` hook's context; this is the composed
|
|
225
|
+
* caller's own view, not a hook's view of a call about to run.
|
|
226
|
+
*/
|
|
227
|
+
export interface ComposedCallContext {
|
|
228
|
+
readonly callId: string;
|
|
229
|
+
readonly sessionId: string;
|
|
230
|
+
/** The provider key driving the turn — the same one nested `tool.x(…)` calls inherit. */
|
|
231
|
+
readonly provider?: string;
|
|
232
|
+
readonly workdir?: string;
|
|
233
|
+
readonly signal: AbortSignal;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* A tool's runtime. `R` is the type of the `value` carried by its `result` event — declared once,
|
|
238
|
+
* at the source, so the executor's yields and the tool's {@link ToolContracts} registry entry can't
|
|
239
|
+
* silently drift. The `unknown` default keeps untyped executors compiling untouched, and covariance
|
|
240
|
+
* means a narrower `ToolExecutor<X>` still satisfies `ToolExecutor` (i.e. `ToolExecutor<unknown>`),
|
|
241
|
+
* so the heterogeneous registry boundary (`Tool[]`) accepts any executor. Bind `R` to the registry
|
|
242
|
+
* entry with `ToolExecutor<ToolResultOf<'my_tool'>>` so the single augmentation is the source of truth.
|
|
243
|
+
*/
|
|
244
|
+
export interface ToolExecutor<R = unknown> {
|
|
245
|
+
execute(input: unknown, ctx: ToolContext): AsyncIterable<ToolEvent<R>>;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export interface Tool<R = unknown> {
|
|
249
|
+
name: string;
|
|
250
|
+
description: string;
|
|
251
|
+
inputSchema: JSONSchema;
|
|
252
|
+
/** The call contract as TypeScript source — a {@link ToolContract}`<Result, Params>` (or a union of arms
|
|
253
|
+
* for a multi-action tool), *identical in shape* to what a `ToolContracts` augmentation declares. Only a
|
|
254
|
+
* tool with **no scannable source** carries this — a `function-tools` function (whose signature supplies
|
|
255
|
+
* both halves), and any similarly runtime-defined tool: the host splices it verbatim into the generated
|
|
256
|
+
* dts's `ToolContracts`, and derives the wire `params`/`result` text from it. A tool WITH source declares
|
|
257
|
+
* its contract as a `ToolContracts` augmentation instead and omits this. Foreign tools (e.g. MCP proxies)
|
|
258
|
+
* that have neither fall back to the loose `inputSchema`. */
|
|
259
|
+
toolContract?: string;
|
|
260
|
+
executor: ToolExecutor<R>;
|
|
261
|
+
pluginName?: string;
|
|
262
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// ── TypeScript ──────────────────────────────────────────────────────────────
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Erases TypeScript types from a source string, returning runnable JavaScript. Type-stripping is
|
|
5
|
+
* foundational — the harness ships raw `.ts` and any code compiled at runtime must have its types
|
|
6
|
+
* removed before it can execute — so every execution environment provides one, and it lives as a fixed
|
|
7
|
+
* runtime capability (on {@link MatbotRuntime}) the host supplies per platform, not a swappable service:
|
|
8
|
+
* node uses its built-in `stripTypeScriptTypes`, the browser bundle uses sucrase. A consumer that
|
|
9
|
+
* compiles source at runtime (e.g. `tool_function`) calls this instead of importing a platform stripper.
|
|
10
|
+
*
|
|
11
|
+
* `strip` may be async: a platform whose stripper loads lazily (the browser fetches sucrase on first use)
|
|
12
|
+
* returns a promise; a synchronous stripper (node) returns the string directly — callers `await` either.
|
|
13
|
+
* It is type-erasure, not full transpilation: node's stripper is erasable-only (enums/namespaces throw),
|
|
14
|
+
* sucrase is more permissive, so authors should keep to erasable TypeScript to stay portable.
|
|
15
|
+
*/
|
|
16
|
+
export interface TypeScriptStripper {
|
|
17
|
+
strip(source: string): string | Promise<string>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Optional, node-only developer-experience service: the live `.d.ts` of the types the loaded tools
|
|
22
|
+
* expose — what `toolResult(invokeTool(…, name, …))`, or a `function-tools` `await tool.name(…)`, resolves
|
|
23
|
+
* to — derived by compiling each loaded plugin's `declare module '@matatbread/matbot-plugin-api'`
|
|
24
|
+
* augmentations. The runtime registry can't supply this (result types are erased); only a TypeScript
|
|
25
|
+
* program reading the source can. Consumers that generate or compose tool-calling code use it so the model
|
|
26
|
+
* isn't guessing return shapes. Absent where no TypeScript program can run (the browser today) — a consumer
|
|
27
|
+
* must degrade (guess-and-run) when `services.ToolTypeIndex` is undefined.
|
|
28
|
+
*
|
|
29
|
+
* The result is rebuilt lazily and cached, invalidated when the tool set changes. Tools the source scan
|
|
30
|
+
* can't reach (a `function-tools` function) contribute their types by declaring a `toolContract` string on
|
|
31
|
+
* their registered {@link Tool} — identical in shape to a `ToolContracts` arm; the index splices it off the
|
|
32
|
+
* live registry, so no separate registration step is needed.
|
|
33
|
+
*/
|
|
34
|
+
export interface ToolTypeIndex {
|
|
35
|
+
/** Self-contained type declarations as a `.d.ts` string: the source-derived `ToolContracts` augmentations
|
|
36
|
+
* merged with the arms of every other live tool, plus `declare const tool: ToolProxy` — the overloaded
|
|
37
|
+
* proxy a generator writes `await tool.x(params)` against. */
|
|
38
|
+
dts(): Promise<string>;
|
|
39
|
+
/** Type-check a TypeScript `snippet` against exactly the {@link dts} above — the `tool` proxy ({@link
|
|
40
|
+
* ToolProxy}: each multi-action tool an overload set, so `await tool.x(params)` narrows its result by the
|
|
41
|
+
* params) is in scope. Returns human-readable diagnostics scoped to the snippet ([] means clean). A
|
|
42
|
+
* composer uses it to catch bad tool-call code before running/registering it. */
|
|
43
|
+
check(snippet: string): Promise<string[]>;
|
|
44
|
+
/** Per live tool (that has a contract): the `params`/`result` wire text, flattened from the one contract —
|
|
45
|
+
* a source tool's `ToolContracts` arms (via the source scan) or a source-less tool's `toolContract` string.
|
|
46
|
+
* The single contract is thus also the source of the wire description; the host folds this into the
|
|
47
|
+
* outgoing tool descriptions at the turn's dispatch edge. A tool with only a loose `inputSchema` (no
|
|
48
|
+
* contract) is absent. */
|
|
49
|
+
wireContracts(): Promise<Record<string, { params: string; result: string }>>;
|
|
50
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// ── Vault ─────────────────────────────────────────────────────────────────────
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The primitives a vault backend must implement. Resolution, redaction, and the three
|
|
5
|
+
* low-level store operations the smart `createSecret` policy composes. A backend implements
|
|
6
|
+
* `Vault` (which is `VaultSpec` plus that policy); plugins are handed a `Vault`.
|
|
7
|
+
*/
|
|
8
|
+
export interface VaultSpec {
|
|
9
|
+
/** Resolve ${NAME} placeholders by looking up the named value; throws MissingSecretError for any miss. */
|
|
10
|
+
resolve(ref: string): Promise<string>;
|
|
11
|
+
scrub(text: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* Store `value` under exactly `name`, overwriting. The literal write; no reference/dedup logic.
|
|
14
|
+
* An empty `value` removes the key instead — there is no such thing as an empty secret, so the
|
|
15
|
+
* empty string is the removal signal (idempotent: removing an absent name is a no-op).
|
|
16
|
+
*/
|
|
17
|
+
writeSecret(name: string, value: string): Promise<void>;
|
|
18
|
+
/** Whether a secret is stored under this exact name. */
|
|
19
|
+
hasKey(name: string): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* The name a value is already stored under, if any. Optional: backends that can't (or won't)
|
|
22
|
+
* reverse-index omit it, and `createSecret`'s dedup step is skipped.
|
|
23
|
+
*/
|
|
24
|
+
findByValue?(value: string): string | undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface Vault extends VaultSpec {
|
|
28
|
+
/**
|
|
29
|
+
* Store a secret coming (often) from a user via the LLM, where we cannot tell a real value
|
|
30
|
+
* from a key name they typed by mistake. Returns the key name callers MUST reference — not
|
|
31
|
+
* necessarily the `name` requested:
|
|
32
|
+
* - `value` is already a known key name → returns `value` (it was a reference, not a secret)
|
|
33
|
+
* - `value` already stored under another name → returns that name (dedup)
|
|
34
|
+
* - otherwise → writes under `name` and returns `name`
|
|
35
|
+
*/
|
|
36
|
+
createSecret(name: string, value: string): Promise<string>;
|
|
37
|
+
}
|