@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/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 -9
- package/src/invoke-tool.ts +25 -3
- package/src/notify.ts +3 -0
- package/src/plugin.ts +28 -195
- 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/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,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,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
|
+
}
|