mu-core 0.13.0 → 0.16.1

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 DELETED
@@ -1,243 +0,0 @@
1
- /**
2
- * Session — owns the message history for a conversation context, runs the
3
- * agent loop on submit, and emits events to subscribers (TUI, persistence,
4
- * HTTP relay, …).
5
- *
6
- * Multi-session: channels emit a `sessionId`; SessionManager lazily
7
- * instantiates a Session per key. mu-coding uses 'tui'; Arya uses
8
- * `telegram:${chatId}`, etc.
9
- */
10
-
11
- import { runAgent } from './agent';
12
- import type { ChannelResponder, InboundMessage } from './channel';
13
- import type { PluginRegistry } from './registry';
14
- import type { ChatMessage, ProviderConfig } from './types/llm';
15
-
16
- export type SessionEvent =
17
- | { type: 'messages_changed'; messages: ChatMessage[] }
18
- | { type: 'stream_partial'; text: string; reasoning?: string }
19
- | { type: 'stream_started' }
20
- | { type: 'stream_ended' }
21
- | { type: 'usage'; totalTokens: number; cachedTokens: number }
22
- | { type: 'error'; message: string };
23
-
24
- export interface RunTurnOptions {
25
- /**
26
- * Pre-built user message to append before running the agent loop.
27
- * Optional: when a plugin's `transformUserInput` returns `'continue'`
28
- * the hook has already appended its own user message via
29
- * `MessageBus.append`, and the host calls `runTurn` without a
30
- * `userMessage` to drain the injectNext queue and stream the LLM
31
- * without pushing a duplicate.
32
- */
33
- userMessage?: ChatMessage;
34
- /** Override config for this single turn (e.g. fresh model id). */
35
- config?: ProviderConfig;
36
- /** Override model for this single turn. */
37
- model?: string;
38
- /** Override registry for this single turn (rare). */
39
- registry?: PluginRegistry;
40
- /**
41
- * Synthetic messages already in state that should NOT be re-appended (the
42
- * caller updated React state imperatively). When omitted, Session works
43
- * off its own tracked transcript.
44
- */
45
- baseMessages?: ChatMessage[];
46
- }
47
-
48
- export interface Session {
49
- readonly id: string;
50
- getMessages: () => ChatMessage[];
51
- setMessages: (messages: ChatMessage[]) => void;
52
- submit: (input: InboundMessage, responder: ChannelResponder) => Promise<void>;
53
- /**
54
- * Lower-level entry point used by hosts that pre-process the user input
55
- * (transformUserInput hooks, attachments). Appends the message, drains
56
- * the next-turn queue, runs the agent loop, and emits events.
57
- */
58
- runTurn: (options: RunTurnOptions) => Promise<ChatMessage[] | null>;
59
- abort: () => void;
60
- appendSynthetic: (msg: ChatMessage) => void;
61
- queueForNextTurn: (msg: ChatMessage) => void;
62
- subscribe: (listener: (event: SessionEvent) => void) => () => void;
63
- }
64
-
65
- export interface SessionInit {
66
- initialMessages?: ChatMessage[];
67
- systemPrompt?: string;
68
- }
69
-
70
- export interface SessionManager {
71
- getOrCreate: (key: string, init?: SessionInit) => Session;
72
- get: (key: string) => Session | undefined;
73
- list: () => Session[];
74
- close: (key: string) => Promise<void>;
75
- }
76
-
77
- export interface CreateSessionManagerOptions {
78
- registry: PluginRegistry;
79
- config: ProviderConfig;
80
- model: string;
81
- }
82
-
83
- class SessionImpl implements Session {
84
- readonly id: string;
85
- private messages: ChatMessage[] = [];
86
- private queue: ChatMessage[] = [];
87
- private listeners = new Set<(e: SessionEvent) => void>();
88
- private abortCtl: AbortController | null = null;
89
- private systemPrompt?: string;
90
-
91
- constructor(
92
- id: string,
93
- private registry: PluginRegistry,
94
- private config: ProviderConfig,
95
- private model: string,
96
- init?: SessionInit,
97
- ) {
98
- this.id = id;
99
- this.systemPrompt = init?.systemPrompt;
100
- if (init?.initialMessages) this.messages = init.initialMessages.slice();
101
- }
102
-
103
- getMessages(): ChatMessage[] {
104
- return this.messages.slice();
105
- }
106
-
107
- setMessages(messages: ChatMessage[]): void {
108
- this.messages = messages.slice();
109
- this.emit({ type: 'messages_changed', messages: this.messages.slice() });
110
- }
111
-
112
- subscribe(listener: (event: SessionEvent) => void): () => void {
113
- this.listeners.add(listener);
114
- return () => this.listeners.delete(listener);
115
- }
116
-
117
- private emit(event: SessionEvent): void {
118
- for (const fn of this.listeners) {
119
- try {
120
- fn(event);
121
- } catch {
122
- // listeners must not break the session
123
- }
124
- }
125
- }
126
-
127
- appendSynthetic(msg: ChatMessage): void {
128
- this.messages.push(msg);
129
- this.emit({ type: 'messages_changed', messages: this.messages.slice() });
130
- }
131
-
132
- queueForNextTurn(msg: ChatMessage): void {
133
- this.queue.push(msg);
134
- }
135
-
136
- abort(): void {
137
- if (this.abortCtl) this.abortCtl.abort();
138
- }
139
-
140
- async submit(input: InboundMessage, _responder: ChannelResponder): Promise<void> {
141
- if (input.text === undefined) return;
142
- const userMsg: ChatMessage = { role: 'user', content: input.text };
143
- await this.runTurn({ userMessage: userMsg });
144
- }
145
-
146
- private async consumeAgentEvents(
147
- cfg: ProviderConfig,
148
- model: string,
149
- registry: PluginRegistry,
150
- signal: AbortSignal,
151
- ): Promise<ChatMessage[] | null> {
152
- let final: ChatMessage[] | null = null;
153
- let partialText = '';
154
- let partialReasoning = '';
155
- for await (const e of runAgent(this.messages, cfg, model, signal, registry)) {
156
- if (e.type === 'content') {
157
- partialText = e.text;
158
- this.emit({ type: 'stream_partial', text: partialText, reasoning: partialReasoning });
159
- } else if (e.type === 'reasoning') {
160
- partialReasoning = e.text;
161
- this.emit({ type: 'stream_partial', text: partialText, reasoning: partialReasoning });
162
- } else if (e.type === 'messages') {
163
- this.messages = e.messages.slice();
164
- final = this.messages.slice();
165
- this.emit({ type: 'messages_changed', messages: this.messages.slice() });
166
- } else if (e.type === 'usage') {
167
- this.emit({ type: 'usage', totalTokens: e.totalTokens, cachedTokens: e.cachedTokens ?? 0 });
168
- } else if (e.type === 'turn_end') {
169
- // Clear the locally-tracked partial buffers AND notify subscribers,
170
- // otherwise the host's `stream` state still holds the previous step's
171
- // reasoning/content between agent loop iterations — visible as a
172
- // stale "thinking…" block lingering after a tool call until the next
173
- // step's first `content`/`reasoning` chunk overwrites it.
174
- partialText = '';
175
- partialReasoning = '';
176
- this.emit({ type: 'stream_partial', text: '', reasoning: '' });
177
- }
178
- }
179
- return final;
180
- }
181
-
182
- async runTurn(options: RunTurnOptions): Promise<ChatMessage[] | null> {
183
- // Re-entrance guard. Concurrent `runTurn` calls would overwrite
184
- // `abortCtl` (orphaning the previous abort controller) and append two
185
- // user messages onto the same transcript, racing the agent loop. Hosts
186
- // are responsible for not interleaving turns; the SDK enforces it.
187
- if (this.abortCtl !== null) {
188
- throw new Error(`Session "${this.id}" already running a turn. Call abort() first or wait for completion.`);
189
- }
190
- if (options.baseMessages) this.messages = options.baseMessages.slice();
191
- // Skip the push when the caller didn't supply a userMessage — that
192
- // happens when a `transformUserInput` hook returned `'continue'` and
193
- // already appended the user's message itself (see `UserInputTransform`).
194
- if (options.userMessage) this.messages.push(options.userMessage);
195
- if (this.queue.length) {
196
- this.messages.push(...this.queue);
197
- this.queue = [];
198
- }
199
- this.emit({ type: 'messages_changed', messages: this.messages.slice() });
200
- this.emit({ type: 'stream_started' });
201
- this.abortCtl = new AbortController();
202
- const cfg: ProviderConfig = { ...(options.config ?? this.config) };
203
- if (this.systemPrompt) cfg.systemPrompt = this.systemPrompt;
204
- const model = options.model ?? this.model;
205
- const registry = options.registry ?? this.registry;
206
- try {
207
- return await this.consumeAgentEvents(cfg, model, registry, this.abortCtl.signal);
208
- } catch (err) {
209
- this.emit({ type: 'error', message: err instanceof Error ? err.message : String(err) });
210
- return null;
211
- } finally {
212
- this.abortCtl = null;
213
- this.emit({ type: 'stream_ended' });
214
- }
215
- }
216
- }
217
-
218
- export function createSessionManager(opts: CreateSessionManagerOptions): SessionManager {
219
- const sessions = new Map<string, SessionImpl>();
220
- return {
221
- getOrCreate(key, init) {
222
- let s = sessions.get(key);
223
- if (!s) {
224
- s = new SessionImpl(key, opts.registry, opts.config, opts.model, init);
225
- sessions.set(key, s);
226
- }
227
- return s;
228
- },
229
- get(key) {
230
- return sessions.get(key);
231
- },
232
- list() {
233
- return Array.from(sessions.values());
234
- },
235
- async close(key) {
236
- const s = sessions.get(key);
237
- if (s) {
238
- s.abort();
239
- sessions.delete(key);
240
- }
241
- },
242
- };
243
- }
package/src/types/llm.ts DELETED
@@ -1,120 +0,0 @@
1
- export interface ProviderConfig {
2
- baseUrl: string;
3
- model?: string;
4
- maxTokens: number;
5
- temperature: number;
6
- streamTimeoutMs: number;
7
- systemPrompt?: string;
8
- /**
9
- * Provider id to dispatch streaming through (`'openai'` by default).
10
- * Resolved by the registry's `ProviderRegistry`. Lets a single host run
11
- * multiple providers concurrently (e.g. coding via openai-compat,
12
- * vision via a different API).
13
- */
14
- providerId?: string;
15
- }
16
-
17
- export interface Usage {
18
- promptTokens: number;
19
- completionTokens: number;
20
- totalTokens: number;
21
- /** Subset of `promptTokens` that hit the prompt cache, when reported by the
22
- * server (OpenAI: `usage.prompt_tokens_details.cached_tokens`; llama.cpp
23
- * newer builds expose the same field). Undefined or 0 when unsupported. */
24
- cachedPromptTokens?: number;
25
- }
26
-
27
- export interface ImageAttachment {
28
- data: string; // base64 encoded
29
- mimeType: string; // e.g. 'image/jpeg'
30
- name: string; // filename for display
31
- }
32
-
33
- export interface ToolCall {
34
- id: string;
35
- function: {
36
- name: string;
37
- arguments: string;
38
- };
39
- }
40
-
41
- export interface ToolDefinition {
42
- type: 'function';
43
- function: {
44
- name: string;
45
- description: string;
46
- parameters: Record<string, unknown>;
47
- };
48
- }
49
-
50
- export interface ToolResultInfo {
51
- name: string;
52
- content: string;
53
- error?: boolean;
54
- }
55
-
56
- /**
57
- * Optional rendering hints carried alongside a ChatMessage. Lets plugins (and
58
- * the host) tweak how a message is presented without committing to a fully
59
- * custom renderer:
60
- * - `color` overrides the role-default text/border color (e.g. agent color)
61
- * - `prefix` is rendered inline before the content (e.g. `│ ` colored bar)
62
- * - `badge` is shown in a small box before the body (e.g. agent name)
63
- * - `hidden` keeps the message in the transcript (sent to the LLM) but skips
64
- * its on-screen rendering — useful for system reminders.
65
- * - `llmHidden` is the inverse: keep the message in the on-screen transcript
66
- * but strip it from the LLM payload right before the network call. Useful
67
- * for UI-only markers (subagent dispatch headers, status pings) that
68
- * plugins want users to see but the model shouldn't read as conversation.
69
- */
70
- export interface MessageDisplay {
71
- color?: string;
72
- prefix?: string;
73
- badge?: string;
74
- hidden?: boolean;
75
- llmHidden?: boolean;
76
- }
77
-
78
- export interface ChatMessage {
79
- role: 'user' | 'assistant' | 'system' | 'tool';
80
- content: string;
81
- reasoning?: string;
82
- images?: ImageAttachment[];
83
- toolCalls?: ToolCall[];
84
- toolCallId?: string;
85
- toolResult?: ToolResultInfo;
86
- toolCallArgs?: Record<string, string>;
87
- /**
88
- * Tag used by plugins to route this message to a custom renderer registered
89
- * with `PluginContext.ui.registerMessageRenderer`. When set and the renderer
90
- * is found, the renderer takes precedence over the role-default renderer.
91
- */
92
- customType?: string;
93
- /** Free-form bag for plugin-private state (e.g. agent name, sub-agent id). */
94
- meta?: Record<string, unknown>;
95
- /** Lightweight display tweaks; see `MessageDisplay`. */
96
- display?: MessageDisplay;
97
- }
98
-
99
- export type StreamChunk =
100
- | { type: 'reasoning'; text: string }
101
- | { type: 'content'; text: string }
102
- | { type: 'tool_call'; toolCall: ToolCall };
103
-
104
- export interface StreamOptions {
105
- signal?: AbortSignal;
106
- onUsage?: (usage: Usage) => void;
107
- tools?: ToolDefinition[];
108
- }
109
-
110
- export interface ApiModel {
111
- id: string;
112
- /**
113
- * Maximum input + output token window the provider advertises for this
114
- * model. OpenAI itself doesn't expose this on `/models`; compat servers
115
- * (llama.cpp, LM Studio, vLLM, Ollama's openai shim, ...) often do under
116
- * names like `context_length`, `max_context_length`, or
117
- * `max_position_embeddings`. Omitted when the provider doesn't report it.
118
- */
119
- contextLimit?: number;
120
- }
package/src/ui.ts DELETED
@@ -1,49 +0,0 @@
1
- /**
2
- * UIService is the host-supplied bridge that lets plugins prompt the user,
3
- * surface notifications, and pin status text without coupling to a specific
4
- * renderer (Ink, plain console, etc.). The TUI host (mu-coding) implements
5
- * this with `InkUIService`; CLI / single-shot hosts can use `ConsoleUIService`.
6
- *
7
- * Plugins receive a `UIService` either directly through their own factory
8
- * config or via `PluginContext.ui` when one is provided.
9
- */
10
- export type UINotifyLevel = 'info' | 'success' | 'warning' | 'error';
11
-
12
- export interface UIService {
13
- notify: (message: string, level?: UINotifyLevel) => void;
14
- confirm: (title: string, message: string) => Promise<boolean>;
15
- select: (title: string, options: string[]) => Promise<string | null>;
16
- input: (title: string, placeholder?: string) => Promise<string | null>;
17
- setStatus: (key: string, text: string) => void;
18
- clearStatus: (key: string) => void;
19
- }
20
-
21
- /**
22
- * Fallback UIService for non-interactive (single-shot) mode.
23
- * Routes notifications to stderr; auto-resolves prompts in deterministic ways
24
- * so non-interactive runs never block.
25
- */
26
- export class ConsoleUIService implements UIService {
27
- notify(message: string, level?: UINotifyLevel): void {
28
- const prefix = level === 'error' ? '[ERROR]' : level === 'warning' ? '[WARN]' : '[INFO]';
29
- console.error(`${prefix} ${message}`);
30
- }
31
- async confirm(_title: string, message: string): Promise<boolean> {
32
- console.error(`[CONFIRM] ${message} (auto-accepting in non-interactive mode)`);
33
- return true;
34
- }
35
- async select(_title: string, options: string[]): Promise<string | null> {
36
- console.error('[SELECT] Auto-selecting first option in non-interactive mode');
37
- return options[0] ?? null;
38
- }
39
- async input(_title: string, _placeholder?: string): Promise<string | null> {
40
- console.error('[INPUT] Cannot prompt in non-interactive mode');
41
- return null;
42
- }
43
- setStatus(_key: string, _text: string): void {
44
- /* no-op in console mode */
45
- }
46
- clearStatus(_key: string): void {
47
- /* no-op in console mode */
48
- }
49
- }