mu-core 0.15.0 → 0.16.3
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/esm/agent.d.ts +39 -0
- package/esm/agent.js +96 -0
- package/esm/index.d.ts +4 -0
- package/esm/index.js +2 -0
- package/esm/package.json +3 -0
- package/esm/types.d.ts +59 -0
- package/esm/types.js +3 -0
- package/package.json +14 -10
- package/script/agent.d.ts +39 -0
- package/script/agent.js +101 -0
- package/script/index.d.ts +4 -0
- package/script/index.js +10 -0
- package/script/package.json +3 -0
- package/script/types.d.ts +59 -0
- package/script/types.js +9 -0
- package/README.md +0 -110
- package/src/activity.test.ts +0 -44
- package/src/activity.ts +0 -83
- package/src/agent.test.ts +0 -91
- package/src/agent.ts +0 -249
- package/src/channel.test.ts +0 -52
- package/src/channel.ts +0 -77
- package/src/hooks.test.ts +0 -105
- package/src/hooks.ts +0 -112
- package/src/host/index.ts +0 -135
- package/src/host/startMu.test.ts +0 -66
- package/src/index.ts +0 -74
- package/src/plugin.ts +0 -389
- package/src/provider/adapter.ts +0 -100
- package/src/provider/registry.test.ts +0 -37
- package/src/provider/registry.ts +0 -26
- package/src/provider/transport.test.ts +0 -58
- package/src/provider/transport.ts +0 -103
- package/src/registry.context.test.ts +0 -71
- package/src/registry.ts +0 -484
- package/src/session.test.ts +0 -99
- package/src/session.ts +0 -248
- package/src/types/llm.ts +0 -120
- package/src/ui.ts +0 -49
package/src/plugin.ts
DELETED
|
@@ -1,389 +0,0 @@
|
|
|
1
|
-
import type { ActivityBus } from './activity';
|
|
2
|
-
import type { ChannelRegistry } from './channel';
|
|
3
|
-
import type { ProviderRegistry } from './provider/registry';
|
|
4
|
-
import type { SessionManager } from './session';
|
|
5
|
-
import type { ChatMessage, ProviderConfig, ToolCall, ToolDefinition } from './types/llm';
|
|
6
|
-
|
|
7
|
-
/** Source of agent definitions on disk; implemented by mu-agents. */
|
|
8
|
-
export interface AgentSourceRegistry {
|
|
9
|
-
registerSource: (absoluteDirPath: string) => () => void;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
import type { UIService } from './ui';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* MessageBus lets plugins inject synthetic messages into the live chat
|
|
16
|
-
* transcript without participating in the LLM streaming loop.
|
|
17
|
-
*
|
|
18
|
-
* - `append(msg)` pushes a message into the **on-screen** transcript right
|
|
19
|
-
* now. The host preserves it across subsequent agent `messages` events
|
|
20
|
-
* when the message looks plugin-synthetic (carries `customType`, `meta`,
|
|
21
|
-
* or `display.hidden`). The LLM does NOT see appended entries — it only
|
|
22
|
-
* sees what was sent in the most recent turn. Use for banners / status
|
|
23
|
-
* entries that should persist in the UI but not influence the model.
|
|
24
|
-
* - `injectNext(msg)` queues a message that's spliced in alongside the
|
|
25
|
-
* *next* user turn. The message reaches the LLM and is persisted with
|
|
26
|
-
* the rest of the transcript. Use for "system reminder" injections that
|
|
27
|
-
* should travel with the user's next message.
|
|
28
|
-
* - `subscribe(fn)` notifies on any transcript change. The listener fires
|
|
29
|
-
* once on subscribe with the current snapshot.
|
|
30
|
-
*/
|
|
31
|
-
export interface MessageBus {
|
|
32
|
-
append: (message: ChatMessage) => void;
|
|
33
|
-
injectNext: (message: ChatMessage) => void;
|
|
34
|
-
drainNext: () => ChatMessage[];
|
|
35
|
-
subscribe: (listener: (messages: ChatMessage[]) => void) => () => void;
|
|
36
|
-
get: () => ChatMessage[];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Renderer signature used by `registerMessageRenderer`. The host calls
|
|
41
|
-
* `render(message)` whenever it encounters a message whose `customType`
|
|
42
|
-
* matches the registered key. The return value is renderer-defined — for the
|
|
43
|
-
* mu-coding host, it is a React element; renderer-agnostic hosts may use a
|
|
44
|
-
* different shape.
|
|
45
|
-
*/
|
|
46
|
-
export type MessageRenderer = (message: ChatMessage) => unknown;
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Handler for plugin-registered keyboard shortcuts. Registering a handler
|
|
50
|
-
* always consumes the key for that frame — the default editor binding is
|
|
51
|
-
* skipped. Async handlers fire-and-forget; the input loop does not await.
|
|
52
|
-
*/
|
|
53
|
-
export type ShortcutHandler = () => void | Promise<void>;
|
|
54
|
-
|
|
55
|
-
export interface MentionCompletion {
|
|
56
|
-
/** Value inserted into the input (replaces `@partial`). */
|
|
57
|
-
value: string;
|
|
58
|
-
/** Display label in the picker. Defaults to `value`. */
|
|
59
|
-
label?: string;
|
|
60
|
-
/** Secondary text shown dimly in the picker. */
|
|
61
|
-
description?: string;
|
|
62
|
-
/**
|
|
63
|
-
* Optional grouping label rendered as a section header in the picker
|
|
64
|
-
* (e.g. "agents", "files"). When unset, completions are rendered without
|
|
65
|
-
* a group header. The host hides the header when only one group is shown.
|
|
66
|
-
*/
|
|
67
|
-
category?: string;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export type MentionProvider = (partial: string) => MentionCompletion[] | Promise<MentionCompletion[]>;
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Side-channel registries the host exposes to plugins. None are guaranteed —
|
|
74
|
-
* plugins should null-check before calling so they degrade gracefully on
|
|
75
|
-
* non-TUI hosts (single-shot CLI, tests).
|
|
76
|
-
*/
|
|
77
|
-
export interface PluginExtras {
|
|
78
|
-
/** Inject / observe synthetic chat messages. */
|
|
79
|
-
messages?: MessageBus;
|
|
80
|
-
/** Register a custom renderer for `ChatMessage.customType`. */
|
|
81
|
-
registerMessageRenderer?: (customType: string, renderer: MessageRenderer) => () => void;
|
|
82
|
-
/**
|
|
83
|
-
* Claim a key combo. The id mirrors the input handler's internal key ids
|
|
84
|
-
* (`tab`, `escape`, `ctrl+t`, ...). Returns an unregister fn.
|
|
85
|
-
*/
|
|
86
|
-
registerShortcut?: (key: string, handler: ShortcutHandler) => () => void;
|
|
87
|
-
/**
|
|
88
|
-
* Provide @mention completions. Trigger char defaults to `@`. Returning an
|
|
89
|
-
* empty array hides the picker for that prefix.
|
|
90
|
-
*/
|
|
91
|
-
registerMentionProvider?: (trigger: string, provider: MentionProvider) => () => void;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Read-only registry surface exposed to plugins via `PluginContext.registry`.
|
|
96
|
-
* Lets a plugin enumerate tools or hand them off to a nested run (e.g. a
|
|
97
|
-
* subagent loop) without circular type imports.
|
|
98
|
-
*/
|
|
99
|
-
export interface PluginRegistryView {
|
|
100
|
-
getTools: () => PluginTool[];
|
|
101
|
-
getFilteredTools: () => Promise<PluginTool[]>;
|
|
102
|
-
getHooks: () => LifecycleHooks[];
|
|
103
|
-
getSystemPrompts: () => Promise<string[]>;
|
|
104
|
-
applySystemPromptTransforms: (prompt: string) => Promise<string>;
|
|
105
|
-
/**
|
|
106
|
-
* Provider registry handle (or `undefined` if the host didn't supply one).
|
|
107
|
-
* Exposed so plugins that re-issue LLM calls (e.g. the mu-agents subagent
|
|
108
|
-
* loop) can resolve the configured provider exactly like `runAgent` does.
|
|
109
|
-
*/
|
|
110
|
-
getProviders: () => ProviderRegistry | undefined;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export interface PluginContext extends PluginExtras {
|
|
114
|
-
cwd: string;
|
|
115
|
-
config: Record<string, unknown>;
|
|
116
|
-
/**
|
|
117
|
-
* Host-provided UI service. Available when the host (e.g. mu-coding) supplies
|
|
118
|
-
* one; otherwise plugins should fall back to a no-op or `ConsoleUIService`.
|
|
119
|
-
*/
|
|
120
|
-
ui?: UIService;
|
|
121
|
-
getPlugin?: <T extends Plugin>(name: string) => T | undefined;
|
|
122
|
-
/**
|
|
123
|
-
* Read-only handle to the live registry. Plugins use this for advanced
|
|
124
|
-
* scenarios — e.g. running subagent loops via `runAgent` over a custom
|
|
125
|
-
* tool subset. Most plugins should rely on hooks + their own `tools`
|
|
126
|
-
* field instead.
|
|
127
|
-
*/
|
|
128
|
-
registry?: PluginRegistryView;
|
|
129
|
-
/**
|
|
130
|
-
* Push status segments for this plugin into the registry. Replaces the older
|
|
131
|
-
* polling-based `Plugin.statusLine()` getter. Pass `[]` to clear.
|
|
132
|
-
*/
|
|
133
|
-
setStatusLine?: (segments: StatusSegment[]) => void;
|
|
134
|
-
/**
|
|
135
|
-
* Push info chips into the host's input footer (e.g. "Coding" agent
|
|
136
|
-
* label). Replaces the segments previously pushed by *this* plugin. Pass
|
|
137
|
-
* `[]` to clear. Hosts that don't render an input footer are free to
|
|
138
|
-
* ignore the call.
|
|
139
|
-
*/
|
|
140
|
-
setInputInfo?: (segments: InputInfoSegment[]) => void;
|
|
141
|
-
/**
|
|
142
|
-
* Host-provided graceful shutdown hook. When supplied, plugins should prefer
|
|
143
|
-
* this over `process.exit(...)` so the host can deactivate plugins and restore
|
|
144
|
-
* terminal state.
|
|
145
|
-
*/
|
|
146
|
-
shutdown?: (code?: number) => Promise<void> | void;
|
|
147
|
-
/** LLM provider registry. Plugins implementing providers register here. */
|
|
148
|
-
providers?: ProviderRegistry;
|
|
149
|
-
/** Channel registry — input surfaces (TUI, Telegram, websocket, ...). */
|
|
150
|
-
channels?: ChannelRegistry;
|
|
151
|
-
/** Session manager — owns conversation state per `sessionId`. */
|
|
152
|
-
sessions?: SessionManager;
|
|
153
|
-
/** Activity bus — pub/sub for agent + tool events (timeline, broadcast). */
|
|
154
|
-
activity?: ActivityBus;
|
|
155
|
-
/** Agent source registry (file-based agent definitions). */
|
|
156
|
-
agents?: AgentSourceRegistry;
|
|
157
|
-
/**
|
|
158
|
-
* Plugins that *implement* an `AgentSourceRegistry` (i.e. mu-agents)
|
|
159
|
-
* publish it here so subsequent plugins (mu-coding-agents, user packages)
|
|
160
|
-
* see it in their own `ctx.agents`. The registry mutates the registry's
|
|
161
|
-
* shared context so every following `register()` propagates the value.
|
|
162
|
-
*/
|
|
163
|
-
setAgentsRegistry?: (registry: AgentSourceRegistry) => void;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* What a tool's `execute` may return:
|
|
168
|
-
* - a plain string (legacy / convenience): an error is heuristically inferred
|
|
169
|
-
* when the string starts with `"Error:"`. Convenient for quick tools but
|
|
170
|
-
* fragile (collisions with legitimate output that begins with that prefix).
|
|
171
|
-
* - a `ToolExecutorResult`: explicit `error` flag, no heuristics. Preferred
|
|
172
|
-
* for new tools and for any tool whose output may legitimately start with
|
|
173
|
-
* "Error:".
|
|
174
|
-
*
|
|
175
|
-
* The agent runtime accepts both forms; the registry doesn't care.
|
|
176
|
-
*/
|
|
177
|
-
export interface ToolExecutorResult {
|
|
178
|
-
content: string;
|
|
179
|
-
error?: boolean;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
export type ToolExecutor = (
|
|
183
|
-
args: Record<string, unknown>,
|
|
184
|
-
signal?: AbortSignal,
|
|
185
|
-
) => Promise<string | ToolExecutorResult> | string | ToolExecutorResult;
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Optional rendering hints the host can use when displaying a tool call.
|
|
189
|
-
* The host (e.g. mu-coding's TUI) maps `kind` to a renderer; tools without a
|
|
190
|
-
* `display` hint fall back to a generic preview.
|
|
191
|
-
*
|
|
192
|
-
* Kept renderer-agnostic on purpose — `mu-agents` has no React / Ink dependency.
|
|
193
|
-
*/
|
|
194
|
-
export interface ToolDisplayHint {
|
|
195
|
-
/** Verb shown in the spinner line, e.g. "reading", "editing". */
|
|
196
|
-
verb?: string;
|
|
197
|
-
/** Renderer kind. Hosts decide how to render each kind. Built-ins use
|
|
198
|
-
* 'file-read' | 'file-write' | 'diff' | 'shell'. */
|
|
199
|
-
kind?: string;
|
|
200
|
-
/** Semantic field mapping from rendering concepts to actual JSON arg names.
|
|
201
|
-
* Examples: { path: 'path' }, { from: 'from', to: 'to' }. */
|
|
202
|
-
fields?: Record<string, string>;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Permission descriptor. `matchKey` extracts the value to glob-match from
|
|
207
|
-
* call args (e.g. `cmd` for bash, `path` for file tools). Tools without a
|
|
208
|
-
* `matchKey` may only be configured with simple actions (`allow|deny|ask`).
|
|
209
|
-
*
|
|
210
|
-
* Validated at agent-definition load time by `mu-agents`.
|
|
211
|
-
*/
|
|
212
|
-
export interface PluginToolPermission {
|
|
213
|
-
matchKey?: (args: Record<string, unknown>) => string | undefined;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
export interface PluginTool {
|
|
217
|
-
definition: ToolDefinition;
|
|
218
|
-
execute: ToolExecutor;
|
|
219
|
-
display?: ToolDisplayHint;
|
|
220
|
-
permission?: PluginToolPermission;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
export interface ToolResult {
|
|
224
|
-
tool_call_id: string;
|
|
225
|
-
name: string;
|
|
226
|
-
content: string;
|
|
227
|
-
error?: boolean;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
export interface TurnResult {
|
|
231
|
-
content: string;
|
|
232
|
-
reasoning: string;
|
|
233
|
-
toolCalls: ToolCall[];
|
|
234
|
-
usage: number;
|
|
235
|
-
/** Input tokens sent to the model for this turn (prompt size). */
|
|
236
|
-
promptTokens: number;
|
|
237
|
-
/** Subset of prompt tokens served from the server's prompt cache, when
|
|
238
|
-
* reported. 0 when unsupported or no cache hit. */
|
|
239
|
-
cachedPromptTokens?: number;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
export type AgentEndReason = 'complete' | 'aborted';
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Result a `beforeToolExec` hook may return. Either:
|
|
246
|
-
* - a `ToolCall` (possibly mutated) — execution proceeds normally
|
|
247
|
-
* - a `ToolBlock` — the host short-circuits execution and uses the
|
|
248
|
-
* supplied content as the tool result (rendered as if the tool ran).
|
|
249
|
-
* Lets policy plugins reject calls without throwing.
|
|
250
|
-
*/
|
|
251
|
-
export interface ToolBlock {
|
|
252
|
-
blocked: true;
|
|
253
|
-
content: string;
|
|
254
|
-
error?: boolean;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
export type BeforeToolExecResult = ToolCall | ToolBlock;
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Result a `transformUserInput` hook may return.
|
|
261
|
-
* - `pass` (or `undefined`) — leave the user's text untouched
|
|
262
|
-
* - `transform` — replace the text but still send it as a user message
|
|
263
|
-
* - `intercept` — suppress the input entirely; the host should not call the
|
|
264
|
-
* LLM. Plugins typically pair this with `MessageBus.append` to surface a
|
|
265
|
-
* reply or status entry.
|
|
266
|
-
* - `continue` — the hook has appended the user message itself (e.g. via
|
|
267
|
-
* `MessageBus.append` so it shows up live in the transcript). The host
|
|
268
|
-
* should NOT push another user message but should still run a turn:
|
|
269
|
-
* drain the injectNext queue and stream the LLM. Used by the subagent
|
|
270
|
-
* `@`-mention dispatch path so the user's message lands first, the
|
|
271
|
-
* subagent runs live, and the parent agent then takes a real turn.
|
|
272
|
-
*/
|
|
273
|
-
export type UserInputTransform =
|
|
274
|
-
| { kind: 'pass' }
|
|
275
|
-
| { kind: 'transform'; text: string }
|
|
276
|
-
| { kind: 'intercept' }
|
|
277
|
-
| { kind: 'continue' };
|
|
278
|
-
|
|
279
|
-
export interface LifecycleHooks {
|
|
280
|
-
beforeLlmCall?: (messages: ChatMessage[], config: ProviderConfig) => ChatMessage[] | Promise<ChatMessage[]>;
|
|
281
|
-
afterLlmCall?: (result: TurnResult) => TurnResult | Promise<TurnResult>;
|
|
282
|
-
beforeToolExec?: (toolCall: ToolCall) => BeforeToolExecResult | Promise<BeforeToolExecResult>;
|
|
283
|
-
afterToolExec?: (toolCall: ToolCall, result: string) => string | Promise<string>;
|
|
284
|
-
/**
|
|
285
|
-
* Restrict the tool set the LLM can see for the next turn. Plugins return
|
|
286
|
-
* the subset of tools they want exposed. Multiple plugins compose by
|
|
287
|
-
* intersection — each hook narrows the previous result.
|
|
288
|
-
*/
|
|
289
|
-
filterTools?: (tools: PluginTool[]) => PluginTool[] | Promise<PluginTool[]>;
|
|
290
|
-
/**
|
|
291
|
-
* Mutate the merged system prompt right before it goes to the provider.
|
|
292
|
-
* Composes left-to-right; later plugins see the prior plugin's output.
|
|
293
|
-
* Useful for per-agent prompt wrapping.
|
|
294
|
-
*/
|
|
295
|
-
transformSystemPrompt?: (prompt: string) => string | Promise<string>;
|
|
296
|
-
/**
|
|
297
|
-
* Inspect / transform / intercept user input on submit. Composes by
|
|
298
|
-
* threading the current text through each plugin; an `intercept` short-
|
|
299
|
-
* circuits and stops the chain. Hosts call this before constructing the
|
|
300
|
-
* user `ChatMessage`.
|
|
301
|
-
*/
|
|
302
|
-
transformUserInput?: (text: string) => UserInputTransform | Promise<UserInputTransform>;
|
|
303
|
-
/**
|
|
304
|
-
* Fires once per `runAgent` invocation, after the loop exits — whether the
|
|
305
|
-
* agent finished normally (LLM produced a final response with no tool calls)
|
|
306
|
-
* or was aborted via the signal. Plugins should use this for end-of-agent
|
|
307
|
-
* cleanup; per-turn cleanup belongs in `afterLlmCall`.
|
|
308
|
-
*/
|
|
309
|
-
afterAgentRun?: (reason: AgentEndReason) => void | Promise<void>;
|
|
310
|
-
/**
|
|
311
|
-
* Decorate a freshly built `ChatMessage` (user / assistant / tool) before
|
|
312
|
-
* it's appended to the transcript. Plugins typically use this to stamp
|
|
313
|
-
* `display.badge` / `display.color` (e.g. with the active agent name +
|
|
314
|
-
* color) or augment `meta`. Hooks compose left-to-right; later hooks see
|
|
315
|
-
* the prior hook's output. Should not change `role` or `content`.
|
|
316
|
-
*/
|
|
317
|
-
decorateMessage?: (msg: ChatMessage) => ChatMessage | Promise<ChatMessage>;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
export interface CommandContext {
|
|
321
|
-
messages: ChatMessage[];
|
|
322
|
-
cwd: string;
|
|
323
|
-
config: ProviderConfig;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
export interface SlashCommand {
|
|
327
|
-
name: string;
|
|
328
|
-
description: string;
|
|
329
|
-
execute: (args: string, context: CommandContext) => Promise<string | undefined>;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
export type AgentEvent =
|
|
333
|
-
| { type: 'content'; text: string }
|
|
334
|
-
| { type: 'reasoning'; text: string }
|
|
335
|
-
| { type: 'usage'; totalTokens: number; promptTokens: number; cachedTokens?: number }
|
|
336
|
-
| { type: 'messages'; messages: ChatMessage[] }
|
|
337
|
-
| { type: 'turn_end' };
|
|
338
|
-
|
|
339
|
-
export interface AgentLoopStrategy {
|
|
340
|
-
name: string;
|
|
341
|
-
run: (
|
|
342
|
-
messages: ChatMessage[],
|
|
343
|
-
config: ProviderConfig,
|
|
344
|
-
model: string,
|
|
345
|
-
signal: AbortSignal,
|
|
346
|
-
tools: PluginTool[],
|
|
347
|
-
hooks: LifecycleHooks[],
|
|
348
|
-
) => AsyncGenerator<AgentEvent>;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
export interface StatusSegment {
|
|
352
|
-
text: string;
|
|
353
|
-
color?: string;
|
|
354
|
-
dim?: boolean;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/**
|
|
358
|
-
* Info chip a plugin pushes into the input footer (e.g. active agent name).
|
|
359
|
-
* Aggregated across plugins by `PluginRegistry.getInputInfoSegments()` and
|
|
360
|
-
* surfaced to the host's input UI in registration order.
|
|
361
|
-
*/
|
|
362
|
-
export interface InputInfoSegment {
|
|
363
|
-
/** Stable key — used by the renderer for list reconciliation. */
|
|
364
|
-
key: string;
|
|
365
|
-
text: string;
|
|
366
|
-
color?: string;
|
|
367
|
-
bold?: boolean;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
export interface Plugin {
|
|
371
|
-
name: string;
|
|
372
|
-
version?: string;
|
|
373
|
-
|
|
374
|
-
tools?: PluginTool[];
|
|
375
|
-
systemPrompt?: string | ((context: PluginContext) => string | Promise<string>);
|
|
376
|
-
hooks?: LifecycleHooks;
|
|
377
|
-
commands?: SlashCommand[];
|
|
378
|
-
agentLoop?: AgentLoopStrategy;
|
|
379
|
-
|
|
380
|
-
activate?: (context: PluginContext) => Promise<void> | void;
|
|
381
|
-
deactivate?: () => Promise<void> | void;
|
|
382
|
-
|
|
383
|
-
/**
|
|
384
|
-
* Plugins may attach arbitrary public fields (e.g. an ApprovalGateway
|
|
385
|
-
* instance, a SourceManager). Sibling plugins fetch them via
|
|
386
|
-
* `ctx.getPlugin<MyPlugin>('name')` and dot into typed fields.
|
|
387
|
-
*/
|
|
388
|
-
[extra: string]: unknown;
|
|
389
|
-
}
|
package/src/provider/adapter.ts
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ApiModel,
|
|
3
|
-
ChatMessage,
|
|
4
|
-
ProviderConfig,
|
|
5
|
-
StreamChunk,
|
|
6
|
-
StreamOptions,
|
|
7
|
-
ToolDefinition,
|
|
8
|
-
Usage,
|
|
9
|
-
} from '../types/llm';
|
|
10
|
-
import { fetchWithIdleTimeout, readNDJSON, readSSE } from './transport';
|
|
11
|
-
|
|
12
|
-
export interface RequestSpec {
|
|
13
|
-
url: string;
|
|
14
|
-
method: 'GET' | 'POST';
|
|
15
|
-
headers: Record<string, string>;
|
|
16
|
-
body?: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface ChatRequestInput {
|
|
20
|
-
messages: ChatMessage[];
|
|
21
|
-
config: ProviderConfig;
|
|
22
|
-
model: string;
|
|
23
|
-
tools?: ToolDefinition[];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface ModelsRequestInput {
|
|
27
|
-
baseUrl: string;
|
|
28
|
-
config: ProviderConfig;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export type ParsedChatEvent =
|
|
32
|
-
| { kind: 'chunk'; chunk: StreamChunk }
|
|
33
|
-
| { kind: 'usage'; usage: Usage }
|
|
34
|
-
| { kind: 'done' }
|
|
35
|
-
| null;
|
|
36
|
-
|
|
37
|
-
export interface ProviderAdapter {
|
|
38
|
-
id: string;
|
|
39
|
-
transport: 'sse' | 'ndjson';
|
|
40
|
-
buildChatRequest: (input: ChatRequestInput) => RequestSpec;
|
|
41
|
-
parseChatEvent: (raw: string) => ParsedChatEvent;
|
|
42
|
-
buildModelsRequest: (input: ModelsRequestInput) => RequestSpec;
|
|
43
|
-
parseModelsResponse: (body: unknown) => ApiModel[];
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface Provider {
|
|
47
|
-
id: string;
|
|
48
|
-
streamChat: (
|
|
49
|
-
messages: ChatMessage[],
|
|
50
|
-
config: ProviderConfig,
|
|
51
|
-
model: string,
|
|
52
|
-
options: StreamOptions,
|
|
53
|
-
) => AsyncIterable<StreamChunk>;
|
|
54
|
-
listModels: (config: ProviderConfig) => Promise<ApiModel[]>;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function createProvider(adapter: ProviderAdapter): Provider {
|
|
58
|
-
return {
|
|
59
|
-
id: adapter.id,
|
|
60
|
-
async *streamChat(messages, config, model, options) {
|
|
61
|
-
const spec = adapter.buildChatRequest({ messages, config, model, tools: options.tools });
|
|
62
|
-
const { response, resetIdle, cancel } = await fetchWithIdleTimeout(
|
|
63
|
-
spec.url,
|
|
64
|
-
{ method: spec.method, headers: spec.headers, body: spec.body, signal: options.signal },
|
|
65
|
-
config.streamTimeoutMs,
|
|
66
|
-
);
|
|
67
|
-
if (!response.ok) {
|
|
68
|
-
const text = await response.text().catch(() => '');
|
|
69
|
-
cancel();
|
|
70
|
-
throw new Error(`HTTP ${response.status} ${response.statusText}: ${text}`);
|
|
71
|
-
}
|
|
72
|
-
const lines =
|
|
73
|
-
adapter.transport === 'sse' ? readSSE(response, options.signal) : readNDJSON(response, options.signal);
|
|
74
|
-
try {
|
|
75
|
-
for await (const raw of lines) {
|
|
76
|
-
resetIdle();
|
|
77
|
-
const evt = adapter.parseChatEvent(raw);
|
|
78
|
-
if (!evt) continue;
|
|
79
|
-
if (evt.kind === 'done') break;
|
|
80
|
-
if (evt.kind === 'usage') {
|
|
81
|
-
options.onUsage?.(evt.usage);
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
if (evt.kind === 'chunk') {
|
|
85
|
-
yield evt.chunk;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
} finally {
|
|
89
|
-
cancel();
|
|
90
|
-
}
|
|
91
|
-
},
|
|
92
|
-
async listModels(config) {
|
|
93
|
-
const spec = adapter.buildModelsRequest({ baseUrl: config.baseUrl, config });
|
|
94
|
-
const res = await fetch(spec.url, { method: spec.method, headers: spec.headers });
|
|
95
|
-
if (!res.ok) throw new Error(`HTTP ${res.status} fetching models`);
|
|
96
|
-
const body = await res.json();
|
|
97
|
-
return adapter.parseModelsResponse(body);
|
|
98
|
-
},
|
|
99
|
-
};
|
|
100
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'bun:test';
|
|
2
|
-
import type { Provider } from './adapter';
|
|
3
|
-
import { createProviderRegistry } from './registry';
|
|
4
|
-
|
|
5
|
-
function fakeProvider(id: string): Provider {
|
|
6
|
-
return {
|
|
7
|
-
id,
|
|
8
|
-
async *streamChat() {
|
|
9
|
-
/* no chunks */
|
|
10
|
-
},
|
|
11
|
-
async listModels() {
|
|
12
|
-
return [];
|
|
13
|
-
},
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
describe('ProviderRegistry', () => {
|
|
18
|
-
it('register / get / list', () => {
|
|
19
|
-
const r = createProviderRegistry();
|
|
20
|
-
r.register(fakeProvider('openai'));
|
|
21
|
-
expect(r.get('openai')?.id).toBe('openai');
|
|
22
|
-
expect(r.list()).toHaveLength(1);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('rejects duplicate ids', () => {
|
|
26
|
-
const r = createProviderRegistry();
|
|
27
|
-
r.register(fakeProvider('openai'));
|
|
28
|
-
expect(() => r.register(fakeProvider('openai'))).toThrow();
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('unregister callback removes', () => {
|
|
32
|
-
const r = createProviderRegistry();
|
|
33
|
-
const off = r.register(fakeProvider('a'));
|
|
34
|
-
off();
|
|
35
|
-
expect(r.get('a')).toBeUndefined();
|
|
36
|
-
});
|
|
37
|
-
});
|
package/src/provider/registry.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import type { Provider } from './adapter';
|
|
2
|
-
|
|
3
|
-
export interface ProviderRegistry {
|
|
4
|
-
register: (provider: Provider) => () => void;
|
|
5
|
-
get: (id: string) => Provider | undefined;
|
|
6
|
-
list: () => Provider[];
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function createProviderRegistry(): ProviderRegistry {
|
|
10
|
-
const providers = new Map<string, Provider>();
|
|
11
|
-
return {
|
|
12
|
-
register(p) {
|
|
13
|
-
if (providers.has(p.id)) throw new Error(`Provider already registered: ${p.id}`);
|
|
14
|
-
providers.set(p.id, p);
|
|
15
|
-
return () => {
|
|
16
|
-
providers.delete(p.id);
|
|
17
|
-
};
|
|
18
|
-
},
|
|
19
|
-
get(id) {
|
|
20
|
-
return providers.get(id);
|
|
21
|
-
},
|
|
22
|
-
list() {
|
|
23
|
-
return Array.from(providers.values());
|
|
24
|
-
},
|
|
25
|
-
};
|
|
26
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'bun:test';
|
|
2
|
-
import { fetchWithIdleTimeout, readNDJSON, readSSE } from './transport';
|
|
3
|
-
|
|
4
|
-
function bodyResponse(text: string): Response {
|
|
5
|
-
return new Response(text, { status: 200, headers: { 'content-type': 'text/event-stream' } });
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
describe('readSSE', () => {
|
|
9
|
-
it('yields data lines from one event', async () => {
|
|
10
|
-
const r = bodyResponse('data: hello\n\n');
|
|
11
|
-
const out: string[] = [];
|
|
12
|
-
for await (const v of readSSE(r)) out.push(v);
|
|
13
|
-
expect(out).toEqual(['hello']);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it('handles multiple events split by blank lines', async () => {
|
|
17
|
-
const r = bodyResponse('data: a\n\ndata: b\n\n');
|
|
18
|
-
const out: string[] = [];
|
|
19
|
-
for await (const v of readSSE(r)) out.push(v);
|
|
20
|
-
expect(out).toEqual(['a', 'b']);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('ignores non-data lines', async () => {
|
|
24
|
-
const r = bodyResponse(': comment\nevent: x\ndata: payload\n\n');
|
|
25
|
-
const out: string[] = [];
|
|
26
|
-
for await (const v of readSSE(r)) out.push(v);
|
|
27
|
-
expect(out).toEqual(['payload']);
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
describe('readNDJSON', () => {
|
|
32
|
-
it('yields one line per JSON record', async () => {
|
|
33
|
-
const r = bodyResponse('{"a":1}\n{"b":2}\n');
|
|
34
|
-
const out: string[] = [];
|
|
35
|
-
for await (const v of readNDJSON(r)) out.push(v);
|
|
36
|
-
expect(out).toEqual(['{"a":1}', '{"b":2}']);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('emits trailing line without final newline', async () => {
|
|
40
|
-
const r = bodyResponse('{"a":1}');
|
|
41
|
-
const out: string[] = [];
|
|
42
|
-
for await (const v of readNDJSON(r)) out.push(v);
|
|
43
|
-
expect(out).toEqual(['{"a":1}']);
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
describe('fetchWithIdleTimeout', () => {
|
|
48
|
-
it('cancels idle timer when fetch itself rejects', async () => {
|
|
49
|
-
// Use a URL that rejects immediately. We can't directly observe the
|
|
50
|
-
// timer, but we can check that the call rejects synchronously and that
|
|
51
|
-
// the function does not leak a hanging timer (verified via the test
|
|
52
|
-
// process exiting promptly after this expect).
|
|
53
|
-
const start = Date.now();
|
|
54
|
-
await expect(fetchWithIdleTimeout('http://127.0.0.1:1', {}, 5000)).rejects.toBeDefined();
|
|
55
|
-
// If the timer leaked, the test would block until 5s; we assert <1s.
|
|
56
|
-
expect(Date.now() - start).toBeLessThan(1000);
|
|
57
|
-
});
|
|
58
|
-
});
|