@matatbread/matbot-plugin-api 0.3.8 → 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/src/session.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Session } from './types.js';
1
+ import type { MessageContent, Session } from './types.js';
2
2
 
3
3
  /**
4
4
  * A session's `updatedAt` invariant: the timestamp of its last message, or its own `createdAt` when it
@@ -15,3 +15,32 @@ export function lastActivityAt(session: Session): string {
15
15
  const last = session.messages[session.messages.length - 1];
16
16
  return last ? last.createdAt : session.createdAt;
17
17
  }
18
+
19
+ /**
20
+ * Index of the turn's user message — the message a durable fold attaches to, and the point a retraction
21
+ * pops back to. `-1` when the session has none.
22
+ */
23
+ export function lastUserIndex(session: Session): number {
24
+ return session.messages.findLastIndex(m => m.role === 'user');
25
+ }
26
+
27
+ /**
28
+ * Append `blocks` to the turn's user message, returning a new session. This is what "durable" means
29
+ * everywhere it appears: unlike `ephemeral` (tail-folded into the outgoing copy and discarded), the blocks
30
+ * become part of the user turn — persisted, visible to a reader, and carried by every subsequent provider
31
+ * call in the turn and after it.
32
+ *
33
+ * With no user message the blocks are **dropped rather than orphaned**: there is nothing they could belong
34
+ * to, and appending a message of their own would change the role sequence the adapters depend on.
35
+ *
36
+ * Shared because the three callers must agree: the `screen` hook fold, the runner's raced-verdict
37
+ * correction, and the pump's retract-and-rerun rebase. They were three independent copies of one
38
+ * `findLastIndex` + splice, in two different packages.
39
+ */
40
+ export function foldOntoUserTurn(session: Session, blocks: readonly MessageContent[]): Session {
41
+ if (blocks.length === 0) return session;
42
+ const idx = lastUserIndex(session);
43
+ if (idx < 0) return session;
44
+ return { ...session, messages: session.messages.map((m, i) =>
45
+ i === idx ? { ...m, content: [...m.content, ...blocks] } : m) };
46
+ }
@@ -0,0 +1,81 @@
1
+ import type { FileHandle } from './files.js';
2
+ import type { MessageContent, Session } from './messages.js';
3
+ import type { Usage } from './provider.js';
4
+
5
+ // ── Pipeline events ─────────────────────────────────────────────────────────────
6
+
7
+ /**
8
+ * A turn's own events. `traceId` — the turn that produced it — is the correlation key a frontend routes
9
+ * on (see `SessionRunner`), and every arm here carries it. Grouped by what a consumer does with them:
10
+ * streamed model output, tool progress, live delivery of something already persisted, and the terminals.
11
+ *
12
+ * A turn ends with **exactly one** terminal: `done`, `aborted` (interrupted mid-flight), or `error`.
13
+ */
14
+ export type TurnEvent =
15
+ // ── streamed model output ──
16
+ | { type: 'text-delta'; delta: string; traceId: string }
17
+ | { type: 'thinking'; delta: string; traceId: string }
18
+ | { type: 'system-context'; text: string; traceId: string }
19
+ | ({ type: 'usage'; traceId: string } & Usage)
20
+
21
+ // ── tool execution ──
22
+ | { type: 'tool:start'; callId: string; name: string; input: unknown; traceId: string }
23
+ | { type: 'tool:stdout'; callId: string; chunk: string; traceId: string }
24
+ | { type: 'tool:stderr'; callId: string; chunk: string; traceId: string }
25
+ | { type: 'tool:progress'; callId: string; pct: number; message?: string; traceId: string }
26
+ | { type: 'tool:end'; callId: string; result: unknown; isError: boolean; traceId: string }
27
+
28
+ // ── live delivery of state that is ALREADY persisted; never the source of truth ──
29
+ | { type: 'file'; handle: FileHandle; traceId: string }
30
+ // Machine-authored content folded onto the running turn's user message (a `screen` hook's
31
+ // `durable` result — e.g. a fired `contextual` trigger), carried live so a frontend draws it
32
+ // immediately. The blocks are already persisted in that user message (origin: 'robo'); this is
33
+ // purely the live-delivery channel, so a live draw matches the reload (which splits the user
34
+ // turn's robo blocks into their own agent-side bubble).
35
+ | { type: 'robo-user'; content: MessageContent[]; traceId: string }
36
+ // Marker blocks appended to the session this turn (e.g. the dispatcher's record of a hook that
37
+ // threw), carried live so a frontend renders them without waiting for a session reload. The blocks
38
+ // are already persisted in the session; this event is purely the live-delivery channel.
39
+ | { type: 'marker'; content: MessageContent[]; traceId: string }
40
+
41
+ // ── queue disposition ──
42
+ // A queued (not-yet-running) submission, carried on the stream so a frontend renders it as part
43
+ // of the live "delta" (everything after the last committed message), never from stored state.
44
+ // `queued` is the number of submissions ahead of it (0 ⇒ about to run). Emitted live on enqueue
45
+ // and replayed (in queue order) to anyone subscribing mid-flight.
46
+ | { type: 'queued'; content: MessageContent[]; queued: number; concatQueue: boolean; traceId: string; rootTraceId: string }
47
+ // A mid-turn steer that INTERRUPTED the running turn (see `SteeringPolicy`). Emitted synchronously
48
+ // when the steer is accepted, so the new user bubble lands in correct stream order and a frontend
49
+ // knows the imminent `aborted` (reason 'steer') on `interruptedTraceId` is a yield, not a dead-end:
50
+ // keep that turn's partial work rendered and expect a continuation. `traceId`/`rootTraceId` identify
51
+ // the steer submission itself (the continuation turn). Late subscribers reconstruct the bubble from
52
+ // the pump's per-turn `queued` replay seed / committed store order — this is purely live delivery.
53
+ | { type: 'steer'; content: MessageContent[]; interruptedTraceId: string; traceId: string; rootTraceId: string }
54
+
55
+ // ── terminals: exactly one per turn ──
56
+ | { type: 'done'; session: Session; traceId: string }
57
+ | { type: 'aborted'; reason: string; session: Session; traceId: string }
58
+ | { type: 'error'; error: string; traceId: string }
59
+ // A queued submission dropped before it ever ran (e.g. by `SessionRunner.abort`). It carries no
60
+ // session because nothing ran and so nothing was persisted — but it does have a traceId: the
61
+ // submission it cancels was assigned one at enqueue, and a frontend needs it to retire that bubble.
62
+ | { type: 'cancelled'; sessionId: string; traceId: string };
63
+
64
+ /**
65
+ * Events about the *session*, not any one turn — hence keyed on `sessionId` with no `traceId`. Split from
66
+ * {@link TurnEvent} so that absence is type-checkable rather than a comment: `idle` was the single arm of
67
+ * a 20-arm union without a `traceId`, which every consumer had to know by having read the note saying so.
68
+ */
69
+ export type SessionEvent =
70
+ // The runner has fully drained its queue. Emitted once per busy period, *after* `running` flips false,
71
+ // so a consumer can map it to an authoritative busy→idle transition without racing the internal flip.
72
+ | { type: 'idle'; sessionId: string };
73
+
74
+ /**
75
+ * The streaming output of a session: its turns' events, plus the session-level ones.
76
+ *
77
+ * Consumers that only ever see one turn (a tool reporting progress, a per-turn frontend) should narrow to
78
+ * {@link TurnEvent} and get `traceId` unconditionally; a subscriber to a whole session takes this union
79
+ * and must handle `idle`. Switching on `type` works the same either way.
80
+ */
81
+ export type PipelineEvent = TurnEvent | SessionEvent;
@@ -0,0 +1,46 @@
1
+ import type { ISODate, MimeType } from './primitives.js';
2
+
3
+ // ── Files ─────────────────────────────────────────────────────────────────────
4
+
5
+ export interface FileMetaData {
6
+ id: string;
7
+ version: string;
8
+ name: string;
9
+ mimeType: MimeType;
10
+ size: number;
11
+ createdAt: ISODate;
12
+ sessionId?: string;
13
+ messageId?: string;
14
+ namespace?: string;
15
+ /** Whether this file may be served at a public URL. Default-deny: absent ⇒ not servable. The
16
+ * flag rides in the file's own persisted metadata (no separate allow-list), so the same read that
17
+ * resolves a request also gates it. Producers opt in per put (the workspace tool sets it true). */
18
+ allowed?: boolean;
19
+ }
20
+
21
+ export interface FileHandle extends FileMetaData {
22
+ stream(signal?: AbortSignal): AsyncIterable<Uint8Array>;
23
+ }
24
+
25
+ export interface FileFilter {
26
+ sessionId?: string;
27
+ mimeType?: string;
28
+ namespace?: string;
29
+ createdAfter?: ISODate;
30
+ createdBefore?: ISODate;
31
+ }
32
+
33
+ export interface FileStore {
34
+ /** Store a file. When `name` is provided, upserts by (name + namespace); otherwise always creates a new entry. */
35
+ put(
36
+ name: string | undefined,
37
+ mimeType: MimeType,
38
+ data: AsyncIterable<Uint8Array>,
39
+ meta?: { sessionId?: string; messageId?: string; namespace?: string; allowed?: boolean }
40
+ ): Promise<FileHandle>;
41
+ get(id: string): Promise<FileHandle | null>;
42
+ getByName(name: string, namespace?: string): Promise<FileHandle | null>;
43
+ delete(id: string): Promise<void>;
44
+ list(filter?: FileFilter): AsyncIterable<FileHandle>;
45
+ putTemp(name: string, mimeType: MimeType, data: AsyncIterable<Uint8Array>): Promise<FileHandle>;
46
+ }
@@ -0,0 +1,11 @@
1
+ // ── Frontend ──────────────────────────────────────────────────────────────────
2
+
3
+ /**
4
+ * Passed to `services.registerFrontend()` by a plugin declaring itself a frontend. A frontend
5
+ * owns its own I/O (an HTTP server, a bot connection, a REPL); matbot only needs to know it
6
+ * exists. This object is the growth point for frontend-level capability advertisement
7
+ * (accepted/produced MIME types, size limits, …) as media support lands.
8
+ */
9
+ export interface FrontendInfo {
10
+ name: string;
11
+ }
@@ -0,0 +1,6 @@
1
+ // ── Health ────────────────────────────────────────────────────────────────────
2
+
3
+ export type HealthStatus =
4
+ | { status: 'ok'; latencyMs?: number }
5
+ | { status: 'degraded'; reason: string; latencyMs?: number }
6
+ | { status: 'down'; reason: string };
@@ -0,0 +1,217 @@
1
+ import type { Message, MessageContent, Session } from './messages.js';
2
+ import type { PromptFn, Tool } from './tools.js';
3
+
4
+ // ── Pipeline hooks ────────────────────────────────────────────────────────────
5
+
6
+ export interface RunConfig {
7
+ provider: string;
8
+ sessionId?: string;
9
+ traceId?: string;
10
+ }
11
+
12
+ /**
13
+ * Hooks are sorted by the *job* they do, not by lifecycle position — the channel name is the
14
+ * contract. Each channel has a fixed home, cadence, and effect-ceiling (a hook may always do
15
+ * less: returning nothing makes it a pure observer). The discriminated union on `on` is what
16
+ * keeps the effects honest — `contribute` hands you a read-only session, `toolcall`/`react` can't
17
+ * return one at all, so a write that goes nowhere won't type-check.
18
+ *
19
+ * screen runner, once per turn before the first provider call. The only channel that may
20
+ * durably mutate history. Returns any of: a replacement `session` (persisted),
21
+ * turn-scoped `ephemeral` context (appended onto the tail of this turn's outgoing
22
+ * messages — the freshest input the model reads — never persisted, and placed at the
23
+ * tail rather than a system prefix so a "do X now" directive keeps its salience and
24
+ * doesn't bust the cached prefix), `durable` context (the persisted, visible twin of
25
+ * `ephemeral`: folded onto this turn's user message as `origin: 'robo'` blocks and
26
+ * carried live as a `robo-user` event, so it survives into the next turn's history),
27
+ * or `abort`. Mix freely. This is where the durable-vs-ephemeral choice for incoming
28
+ * user input lives.
29
+ * contribute runner, before *every* provider call. Ephemeral by construction (it re-fires, so a
30
+ * durable mutation would accumulate). Returns a transformed copy of `outgoing` — the
31
+ * message array about to be sent — and never touches the stored session. Mind prompt
32
+ * caching: vary the *tail* (newest turn) freely, but a transform that rewrites the
33
+ * cached prefix (system / early history) busts the cache on every call.
34
+ * toolcall runner, before each tool execution. Read-only. Returns `rejectTool` (skip this call,
35
+ * feed an error result back so the model self-corrects, without breaking the
36
+ * tool_use/tool_result pairing) and/or `abort`.
37
+ * toolresult runner, after each tool execution, before the result is recorded/yielded. Folds the
38
+ * result through each hook: return `{ result }` to replace it (hard redaction, truncation),
39
+ * or nothing to observe (auditing — the context carries args, result, isError and
40
+ * `durationMs`). It owns the LLM-facing + persisted result and the `tool:end` event;
41
+ * note it does NOT see the live `tool:stdout/stderr` chunks, which stream before the
42
+ * result exists.
43
+ * followup pump, once after a turn commits (post-persist). May `resubmit` a robo follow-up turn
44
+ * (head-enqueued, so it runs next as its own real turn; `resubmitDepth` is the chain
45
+ * length for the hook's own budget — the runner also hard-caps it), `retractAndRerun`
46
+ * (pop the committed turn into a marker and re-run the originating user turn with
47
+ * ephemeral context — supersede rather than follow), and/or append durable `markers`
48
+ * to the committed session (LLM-invisible annotations — the second durable-write point
49
+ * after `screen`, safe because it too fires once per turn).
50
+ */
51
+ export type HookPoint = 'screen' | 'contribute' | 'toolcall' | 'toolresult' | 'followup';
52
+
53
+ export interface ScreenContext {
54
+ session: Session;
55
+ config: RunConfig;
56
+ signal: AbortSignal;
57
+ /** The turn's interactive prompt, when a frontend supplied one (a live user behind this turn).
58
+ * A hook that drives an interactive tool (e.g. a trigger invoking `ask_user`) forwards this into
59
+ * the tool's context; absent (cron/background/no frontend) the tool gets a rejecting prompt. */
60
+ prompt?: PromptFn;
61
+ /** Unregister the hook currently running. For one-shot hooks that should fire at most once. */
62
+ removeHook(): void;
63
+ }
64
+ /**
65
+ * A raced screen verdict the runner folds in WITHOUT gating the turn on it. A `screen` hook that starts
66
+ * expensive work concurrently (e.g. a triggers classifier judging the user message) returns immediately
67
+ * and hands back one of these instead of blocking on it. The runner polls `claim()` — synchronously,
68
+ * never awaiting — at each turn-loop edge (before a provider call, on every stream event, and just
69
+ * before committing). The first time `claim()` returns blocks (the work has settled WITH a correction),
70
+ * the runner DISCARDS the uncommitted in-progress response and re-runs the loop with those blocks
71
+ * tail-folded as ephemeral context — an in-situ redo: no store pop, no retraction marker, cleaner than a
72
+ * post-commit retract. Because the mid-stream poll runs before each event is emitted, a verdict faster
73
+ * than time-to-first-token is caught before any token reaches the frontend (the clean path); a slower
74
+ * one aborts the in-flight provider request, saving the unstreamed remainder. If it never fires before
75
+ * the turn commits, the turn commits normally and any correction is left to a post-commit `followup`.
76
+ *
77
+ * `claim()` MUST be exactly-once: return the correction on the first successful poll and `undefined`
78
+ * forever after (not settled, no correction, or already claimed). The hook uses that single delivery to
79
+ * coordinate with its own post-commit path — a claimed verdict is never also delivered by `followup`.
80
+ */
81
+ export interface DeferredScreen {
82
+ claim(): DeferredCorrection | undefined;
83
+ }
84
+ /**
85
+ * A claimed raced-verdict correction. `ephemeral` is tail-folded onto the re-run's outgoing messages
86
+ * and never persisted (the transient "for this answer only" twin); `durable` is folded onto the turn's
87
+ * user message — persisted, visible (the hook marks it `origin: 'robo'`), and carried live as a
88
+ * `robo-user` event — so it updates the conversation rather than informing one answer (a `contextual`
89
+ * trigger's semantics, preserved even though the verdict now lands mid-turn instead of before it). At
90
+ * least one is non-empty when returned.
91
+ */
92
+ export interface DeferredCorrection {
93
+ ephemeral?: MessageContent[];
94
+ durable?: MessageContent[];
95
+ }
96
+ export interface ScreenResult {
97
+ session?: Session;
98
+ /**
99
+ * A raced verdict the runner folds in without gating on it (see {@link DeferredScreen}). Lets a
100
+ * screen hook race expensive work — a classifier — against generation: return immediately, hand this
101
+ * back, and the runner restarts the turn in-situ if the verdict fires before commit. A hook that
102
+ * would rather gate the turn (block until the verdict) just returns `ephemeral` as usual instead.
103
+ */
104
+ deferred?: DeferredScreen;
105
+ /** Turn-scoped context appended onto the tail of this turn's outgoing messages (the freshest
106
+ * input the model reads), never persisted. At the tail, not a system prefix, so a directive
107
+ * keeps its salience and the cached system/history prefix stays stable across turns. */
108
+ ephemeral?: MessageContent[];
109
+ /**
110
+ * The persisted, visible twin of `ephemeral`: context that should outlive this turn rather than
111
+ * inform it once. The runner folds these blocks onto this turn's user message (so they ride into
112
+ * the stored history and every subsequent provider call) AND carries them live as a `robo-user`
113
+ * event, so a live draw and a reload render the same thing. They are LLM-visible (unlike
114
+ * `markers`) and machine-authored, so a caller marks them `origin: 'robo'` for presentation.
115
+ * Use when a fired hook produces context that genuinely updates the conversation, not a one-shot
116
+ * corrective for the turn about to run.
117
+ */
118
+ durable?: MessageContent[];
119
+ /**
120
+ * Durable `marker` blocks to append to this turn's session (LLM-invisible). The dispatcher both
121
+ * appends them to the persisted session AND carries them live on the turn's event stream, so a
122
+ * live draw and a reload render the same thing. Use instead of hand-appending to `session` when
123
+ * you just want to annotate (e.g. a fired trigger's silent tool recording what it did).
124
+ */
125
+ markers?: MessageContent[];
126
+ abort?: string;
127
+ }
128
+
129
+ export interface ContributeContext {
130
+ readonly outgoing: readonly Message[];
131
+ readonly session: Session;
132
+ config: RunConfig;
133
+ signal: AbortSignal;
134
+ /** Unregister the hook currently running. For one-shot hooks that should fire at most once. */
135
+ removeHook(): void;
136
+ }
137
+
138
+ export interface ToolCallContext {
139
+ readonly session: Session;
140
+ readonly toolCall: { id: string; name: string; input: unknown };
141
+ readonly tool: Tool;
142
+ config: RunConfig;
143
+ signal: AbortSignal;
144
+ /** Unregister the hook currently running. For one-shot hooks that should fire at most once. */
145
+ removeHook(): void;
146
+ }
147
+ export interface ToolCallResult {
148
+ rejectTool?: { message: string };
149
+ abort?: string;
150
+ }
151
+
152
+ export interface ToolResultContext {
153
+ readonly session: Session;
154
+ readonly toolCall: { id: string; name: string; input: unknown };
155
+ readonly tool: Tool;
156
+ readonly result: unknown;
157
+ readonly isError: boolean;
158
+ readonly durationMs: number;
159
+ config: RunConfig;
160
+ signal: AbortSignal;
161
+ /** Unregister the hook currently running. For one-shot hooks that should fire at most once. */
162
+ removeHook(): void;
163
+ }
164
+ // The toolresult hook returns `{ result }` to replace the tool's result, or nothing to leave it
165
+ // (and just observe) — a trivial single-field return, inlined in the Hook union like `contribute`'s.
166
+
167
+ export interface FollowupContext {
168
+ readonly session: Session;
169
+ readonly resubmitDepth: number;
170
+ config: RunConfig;
171
+ signal: AbortSignal;
172
+ /** The turn's interactive prompt, when a frontend supplied one (a live user behind this turn).
173
+ * A hook that drives an interactive tool (e.g. a trigger invoking `ask_user` as a proactive
174
+ * follow-up question) forwards this into the tool's context; absent (cron/background/no frontend)
175
+ * the tool gets a rejecting prompt. Note this prompt fires *post-commit*, out of band from the
176
+ * turn's `done`, and blocks the pump until the human answers. */
177
+ prompt?: PromptFn;
178
+ /** Unregister the hook currently running. For one-shot hooks that should fire at most once. */
179
+ removeHook(): void;
180
+ }
181
+ export interface FollowupResult {
182
+ resubmit?: { content: MessageContent[] };
183
+ /**
184
+ * Retract-and-rerun: supersede the just-committed turn instead of following it. The pump pops the
185
+ * committed turn back to (and excluding) the last user message, stashes the popped content in a
186
+ * durable retraction marker (LLM-elided like every marker, so a frontend can render it
187
+ * struck-through and a post-mortem can audit it), then re-runs that same user turn with `context`
188
+ * injected EPHEMERALLY (tail-folded, never persisted) — agent-phase injection time-shifted onto a
189
+ * committed turn. This is the inverse of `resubmit`, which leaves the response in place and appends
190
+ * a new robo turn after it. Self-terminating by design: a well-formed trigger fires on a *curable*
191
+ * defect that the injected context dissolves on the redo, so it won't re-fire; `resubmitDepth` (a
192
+ * redo carries parent+1) caps an ill-formed one. `resubmit` and `retractAndRerun` are independent
193
+ * capabilities — a single turn returning both is not expected, but both head-enqueue if it does.
194
+ *
195
+ * `context` is folded EPHEMERALLY onto the redo (for-this-answer-only). `durable` is instead folded
196
+ * onto the re-run's user message — persisted, visible (mark it `origin: 'robo'`), carried live as a
197
+ * `robo-user` event — so a `contextual`-kind correction updates the conversation durably even when it
198
+ * lands post-commit. At least one of the two is present.
199
+ */
200
+ retractAndRerun?: { context?: MessageContent[]; durable?: MessageContent[] };
201
+ /**
202
+ * Durable `marker` blocks to append to the just-committed session (LLM-invisible; for tracing /
203
+ * cross-references). The second durable-write capability after `screen` — safe here for the same
204
+ * reason: `followup` fires once per turn, so an append can't accumulate the way a per-call channel
205
+ * would. The pump persists them AND emits them live (post-commit, like a `queued` event) so a live
206
+ * draw matches a reload. Use for recording what a post-commit reaction (e.g. a fired trigger's
207
+ * silent tool) actually did.
208
+ */
209
+ markers?: MessageContent[];
210
+ }
211
+
212
+ export type Hook =
213
+ | { on: 'screen'; priority?: number; pluginName?: string; handler(ctx: ScreenContext): ScreenResult | void | Promise<ScreenResult | void> }
214
+ | { on: 'contribute'; priority?: number; pluginName?: string; handler(ctx: ContributeContext): Message[] | void | Promise<Message[] | void> }
215
+ | { on: 'toolcall'; priority?: number; pluginName?: string; handler(ctx: ToolCallContext): ToolCallResult | void | Promise<ToolCallResult | void> }
216
+ | { on: 'toolresult'; priority?: number; pluginName?: string; handler(ctx: ToolResultContext): { result: unknown } | void | Promise<{ result: unknown } | void> }
217
+ | { on: 'followup'; priority?: number; pluginName?: string; handler(ctx: FollowupContext): FollowupResult | void | Promise<FollowupResult | void> };
@@ -0,0 +1,27 @@
1
+ // ── Knowledge index ───────────────────────────────────────────────────────────
2
+
3
+ export interface KnowledgeEntry {
4
+ id: string;
5
+ version: string;
6
+ entities: string[];
7
+ tags: string[];
8
+ summary: string;
9
+ content: string;
10
+ contentHash?: string;
11
+ source: { type: string; uuid: string };
12
+ confidence?: number;
13
+ createdAt: string;
14
+ updatedAt: string;
15
+ }
16
+
17
+ export interface KnowledgeIndex {
18
+ index(entry: KnowledgeEntry): Promise<void>;
19
+ /** Remove the entry with this id, if present (idempotent — removing an absent id is a no-op). The
20
+ * id is the index's sole primary key (`index` replaces by it), so retraction is by id alone; the
21
+ * index never inspects an entry's opaque `source`. The party that indexed an entry owns retracting
22
+ * it — e.g. a skill manager retracts a hidden/deleted skill rather than the index policing tenants. */
23
+ remove(id: string): Promise<void>;
24
+ search(terms: Array<{ term: string; context?: string }>, signal: AbortSignal): Promise<KnowledgeEntry[]>;
25
+ /** Enumerate all indexed entries. When present, register('KnowledgeIndex', …) drains these into the incoming backend. */
26
+ entries?(): Iterable<KnowledgeEntry>;
27
+ }
@@ -0,0 +1,154 @@
1
+ import type { HookPoint } from './hooks.js';
2
+ import type { ISODate, MimeType } from './primitives.js';
3
+ import type { ProviderMeta, Usage, UsageRecord } from './provider.js';
4
+
5
+ // ── Messages & Session ────────────────────────────────────────────────────────
6
+
7
+ export type MessageRole = 'user' | 'assistant' | 'tool' | 'system' | 'marker';
8
+
9
+ export type MessageContent = (
10
+ // ── prose and reasoning: what the model said ──
11
+ | { type: 'text'; text: string }
12
+ | { type: 'thinking'; thinking: string; signature: string }
13
+ | { type: 'redacted-thinking'; data: string }
14
+ | { type: 'reasoning'; reasoning: string }
15
+ | { type: 'refusal'; text: string }
16
+
17
+ // ── inline media: bytes plus a mime type. The first three are also `ModelContent` — the arms a tool
18
+ // may hand the model to look at (see the `model-content` ToolEvent). `image-url` and `file-ref`
19
+ // are references rather than data, and nothing produces them yet; they are reconsidered together
20
+ // when durable user-supplied media is built (a `document` / `document-reference` split). ──
21
+ | { type: 'image'; data: string; mimeType: MimeType }
22
+ | { type: 'document'; data: string; mimeType: MimeType; name?: string }
23
+ | { type: 'audio'; data: string; mimeType: MimeType }
24
+ | { type: 'image-url'; url: string; detail?: 'low' | 'high' | 'auto' }
25
+ | { type: 'file-ref'; fileId: string; name: string; mimeType: MimeType }
26
+
27
+ // ── tool use ──
28
+ | { type: 'tool-call'; id: string; name: string; input: unknown;
29
+ /** Provider-specific round-trip metadata, persisted and re-sent verbatim on replay — see
30
+ * `ProviderMeta` and the `tool-call` `CompletionEvent`. */
31
+ meta?: ProviderMeta }
32
+ | { type: 'tool-result'; id: string; result: unknown; isError?: boolean;
33
+ /**
34
+ * Accounting addendum: completions the tool itself ran (e.g. `single_turn`, `dream_time`'s ranker/
35
+ * merger), captured ambiently via the usage carrier and recorded for cost reckoning. Elided from
36
+ * provider submission — adapters serialise only `id`/`result`/`isError`. One entry per provider
37
+ * call, each tagged with the provider billed; never summed at write time, as rates differ.
38
+ */
39
+ usage?: UsageRecord[] }
40
+
41
+ // ── frontend-facing: rendered to a person, never sent to the model ──
42
+ | { type: 'form'; fields: FormField[]; submitLabel?: string }
43
+ | { type: 'form-response'; values: Record<string, string> }
44
+ // Durable, opaque annotation — persisted unchanged, elided from every submission. See MarkerData below.
45
+ | { type: 'marker'; creator: string; data: unknown }
46
+
47
+ // ── a provider block this build does not know: preserved verbatim rather than dropped ──
48
+ | { type: 'unknown-content'; blockType: string; raw: unknown }
49
+ ) & {
50
+ /**
51
+ * Authorship provenance, for *presentation only* — orthogonal to the message's `role`, which is
52
+ * the LLM-protocol identity. Absent ⇒ authored per the role (a human for `user`, the model for
53
+ * `assistant`). `'robo'` ⇒ machine-authored by matbot — a `followup` resubmission, or a hook-
54
+ * injected fragment inside a human turn. It is still carried to the model as ordinary
55
+ * role-appropriate content (the model sees a `user` block either way); the flag is OOB metadata
56
+ * that frontends use to present it agent-side (a robot indicator) rather than as the user's words.
57
+ */
58
+ origin?: 'robo';
59
+ };
60
+
61
+ /**
62
+ * A marker is opaque, durable annotation carried in the message stream: links, status,
63
+ * cross-references that are meaningful to a frontend but transparent to the LLM. They are
64
+ * persisted unchanged, elided from provider submission, and deliberately preserved by
65
+ * session compaction (removing one can break things — e.g. a pointer back to an ancestor
66
+ * session). Any code with session access may emit one, normally as its own message.
67
+ *
68
+ * `creator` is the emitting plugin's reference; `data` is anything serialisable. For
69
+ * per-creator type safety, augment `MarkerData` and read/write via `Marker<'your-creator'>`:
70
+ *
71
+ * declare module '@matatbread/matbot-plugin-api' {
72
+ * interface MarkerData { 'split-session': { peerSessionId: string } }
73
+ * }
74
+ *
75
+ * Unregistered creators fall back to `data: unknown`. The base `MessageContent` member stays
76
+ * loose (`creator: string`) so the union and its exhaustive switches are unaffected.
77
+ *
78
+ * One of matbot's five open-registry augmentation points — same technique at each; see
79
+ * docs/DEVELOPING.md *Open-registry augmentation* for the shared shape and the rules that follow from it.
80
+ */
81
+ export interface MarkerData {
82
+ /** Emitted by the hook dispatcher when a hook handler threw: the hook was skipped (treated as a
83
+ * no-op) and this records it once, so a misconfigured/throwing hook degrades visibly instead of
84
+ * bricking the turn. `channel` is the hook point, `pluginName` the owning plugin if known. */
85
+ 'matbot-hooks': { channel: HookPoint; pluginName?: string; message: string };
86
+ /** Emitted by the runner when a provider call was cut short rather than finishing (see the
87
+ * `truncated` CompletionEvent). Marker-role so the reader and an audit see it while the model does
88
+ * not — the model's own text is already truncated in the transcript; a block telling it so would
89
+ * invite it to narrate the cut-off rather than continue past it. */
90
+ 'matbot-truncation': { reason: 'max-tokens' | 'stream-end'; raw?: string };
91
+ }
92
+
93
+ export type Marker<K extends string = string> = {
94
+ type: 'marker';
95
+ creator: K;
96
+ data: K extends keyof MarkerData ? MarkerData[K] : unknown;
97
+ };
98
+
99
+ export interface FormField {
100
+ name: string;
101
+ label: string;
102
+ type: 'text' | 'password' | 'select' | 'confirm';
103
+ options?: string[];
104
+ /** select-only: render an "Other…" affordance that lets the user type a free-form answer instead
105
+ * of picking an option. The typed value is returned verbatim, on the same channel as a pick —
106
+ * callers never learn whether the answer was a preset or free text. Ignored for other types. */
107
+ allowOther?: boolean;
108
+ default?: string;
109
+ required?: boolean;
110
+ /** Presentation hint only (default true): whether the frontend offers a cancel affordance (the
111
+ * "give up" path — see `PromptFn`). Set false to render a hard-blocking prompt with no out. The
112
+ * runner and server never consult this; a frontend with no cancel UI simply can't fire one. */
113
+ cancelable?: boolean;
114
+ }
115
+
116
+ /**
117
+ * Canonical values a `type: 'confirm'` prompt resolves to. The rendered label and buttons are a
118
+ * cosmetic, host-specific concern (and may be localised), so consumers MUST branch on these tokens
119
+ * — never on the displayed string. Compare case-insensitively to tolerate host casing differences.
120
+ */
121
+ export const CONFIRM_YES = 'yes';
122
+ export const CONFIRM_NO = 'no';
123
+
124
+ export interface Message {
125
+ id: string;
126
+ role: MessageRole;
127
+ content: MessageContent[];
128
+ createdAt: ISODate;
129
+ traceId: string;
130
+ providerName?: string;
131
+ /** Token/cost accounting for the provider call that produced this message (assistant turns). The
132
+ * billed provider is this message's `providerName`. Accounting only — elided from provider submission. */
133
+ usage?: Usage;
134
+ metadata?: Record<string, unknown>;
135
+ }
136
+
137
+ export type SessionStatus = 'active' | 'archived' | 'pinned';
138
+
139
+ export interface Session {
140
+ id: string;
141
+ version: string;
142
+ // Legacy: persisted sessions may still carry `ownerPrincipalId` / `actorPrincipalId` / `persona`, and
143
+ // `contexts` (a required-but-never-read `string[]`, dropped at 0.4.0 — system context is contributed by
144
+ // SystemContextContributor, not carried on the session). Never read — ownership-at-rest is structural
145
+ // (the storage partition), resolved via the backend, not a field. Left undeclared; old data keeps them
146
+ // harmlessly as excess properties.
147
+ title?: string;
148
+ status: SessionStatus;
149
+ messages: Message[];
150
+ parentSessionId?: string;
151
+ branchPointMessageId?: string;
152
+ createdAt: ISODate;
153
+ updatedAt: ISODate;
154
+ }
@@ -0,0 +1,5 @@
1
+ // ── Primitives ────────────────────────────────────────────────────────────────
2
+
3
+ export type ISODate = string;
4
+ export type MimeType = string;
5
+ export type JSONSchema = Record<string, unknown>;
@@ -0,0 +1,10 @@
1
+ // ── Principal ───────────────────────────────────────────────────────────────
2
+
3
+ /** Identity of whatever initiated an operation — a turn, a tool call, a store read/write.
4
+ * Established at each entry point and carried ambiently via the `PrincipalCarrier`
5
+ * (`currentPrincipal()` / `runAs()`), so any layer can attribute or test the origin without it
6
+ * being threaded through every signature. It grants nothing — policy is the service's concern. */
7
+ export interface Principal {
8
+ id: string;
9
+ type: 'user' | 'agent' | 'system';
10
+ }