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 ADDED
@@ -0,0 +1,39 @@
1
+ import type { ContentPart, Message, Provider, Tool, Usage } from './types.js';
2
+ export type LoopEvent = ContentPart | {
3
+ type: 'usage';
4
+ usage: Usage;
5
+ } | {
6
+ type: 'reasoning';
7
+ text: string;
8
+ } | {
9
+ type: 'message';
10
+ message: Message;
11
+ } | {
12
+ type: 'done';
13
+ messages: Message[];
14
+ };
15
+ export interface RunOptions {
16
+ provider: Provider;
17
+ model: string;
18
+ messages: Message[];
19
+ tools?: Tool[];
20
+ signal?: AbortSignal;
21
+ }
22
+ export declare function run(opts: RunOptions): AsyncIterable<LoopEvent>;
23
+ export interface AgentConfig {
24
+ provider: Provider;
25
+ model: string;
26
+ tools?: Tool[];
27
+ system?: string;
28
+ signal?: AbortSignal;
29
+ }
30
+ export type Input = string | ContentPart[] | Message[];
31
+ export interface AgentResult {
32
+ message: Message;
33
+ messages: Message[];
34
+ }
35
+ export interface Agent {
36
+ stream(input: Input): AsyncIterable<LoopEvent>;
37
+ run(input: Input): Promise<AgentResult>;
38
+ }
39
+ export declare const createAgent: (config: AgentConfig) => Agent;
package/esm/agent.js ADDED
@@ -0,0 +1,96 @@
1
+ const append = (parts, part) => {
2
+ const last = parts[parts.length - 1];
3
+ if (part.type === 'text' && last?.type === 'text') {
4
+ last.text += part.text;
5
+ }
6
+ else if (part.type === 'audio' && last?.type === 'audio' && last.mime === part.mime) {
7
+ const merged = new Uint8Array(last.data.length + part.data.length);
8
+ merged.set(last.data);
9
+ merged.set(part.data, last.data.length);
10
+ last.data = merged;
11
+ }
12
+ else if (part.type === 'text') {
13
+ parts.push({ type: 'text', text: part.text });
14
+ }
15
+ else if (part.type === 'audio') {
16
+ parts.push({ type: 'audio', mime: part.mime, data: part.data });
17
+ }
18
+ else {
19
+ parts.push(part);
20
+ }
21
+ };
22
+ const execute = async (tools, call, signal) => {
23
+ const tool = tools.get(call.name);
24
+ if (!tool)
25
+ return [{ type: 'text', text: `Unknown tool: ${call.name}` }];
26
+ try {
27
+ return await tool.run(call.input, { signal });
28
+ }
29
+ catch (err) {
30
+ return [{ type: 'text', text: err instanceof Error ? err.message : String(err) }];
31
+ }
32
+ };
33
+ export async function* run(opts) {
34
+ const { provider, model, signal } = opts;
35
+ const tools = opts.tools ?? [];
36
+ const registry = new Map(tools.map((t) => [t.name, t]));
37
+ const messages = [...opts.messages];
38
+ while (true) {
39
+ const content = [];
40
+ const calls = [];
41
+ for await (const event of provider.stream({ model, messages, tools, signal })) {
42
+ if (event.type === 'usage' || event.type === 'reasoning') {
43
+ yield event;
44
+ continue;
45
+ }
46
+ yield event;
47
+ append(content, event);
48
+ if (event.type === 'tool_call')
49
+ calls.push(event);
50
+ }
51
+ const message = { role: 'assistant', content };
52
+ messages.push(message);
53
+ yield { type: 'message', message };
54
+ if (calls.length === 0)
55
+ break;
56
+ const results = await Promise.all(calls.map(async (call) => ({
57
+ type: 'tool_result',
58
+ id: call.id,
59
+ content: await execute(registry, call, signal),
60
+ })));
61
+ const toolMessage = { role: 'user', content: results };
62
+ messages.push(toolMessage);
63
+ yield { type: 'message', message: toolMessage };
64
+ }
65
+ yield { type: 'done', messages };
66
+ }
67
+ const isMessages = (input) => input.length > 0 && 'role' in input[0];
68
+ const toMessages = (input) => {
69
+ if (typeof input === 'string')
70
+ return [{ role: 'user', content: [{ type: 'text', text: input }] }];
71
+ if (isMessages(input))
72
+ return input;
73
+ return [{ role: 'user', content: input }];
74
+ };
75
+ export const createAgent = (config) => {
76
+ const tools = config.tools ?? [];
77
+ const build = (input) => {
78
+ const messages = toMessages(input);
79
+ if (!config.system)
80
+ return messages;
81
+ return [{ role: 'system', content: [{ type: 'text', text: config.system }] }, ...messages];
82
+ };
83
+ const stream = (input) => run({ provider: config.provider, model: config.model, tools, messages: build(input), signal: config.signal });
84
+ const runToEnd = async (input) => {
85
+ let message = { role: 'assistant', content: [] };
86
+ let messages = [];
87
+ for await (const event of stream(input)) {
88
+ if (event.type === 'message' && event.message.role === 'assistant')
89
+ message = event.message;
90
+ else if (event.type === 'done')
91
+ messages = event.messages;
92
+ }
93
+ return { message, messages };
94
+ };
95
+ return { stream, run: runToEnd };
96
+ };
package/esm/index.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export type { ContentPart, Message, Provider, Role, StreamEvent, Tool, Usage } from './types.js';
2
+ export { audio, image, text } from './types.js';
3
+ export type { Agent, AgentConfig, AgentResult, Input, LoopEvent, RunOptions } from './agent.js';
4
+ export { createAgent, run } from './agent.js';
package/esm/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { audio, image, text } from './types.js';
2
+ export { createAgent, run } from './agent.js';
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
package/esm/types.d.ts ADDED
@@ -0,0 +1,59 @@
1
+ export type ContentPart = {
2
+ type: 'text';
3
+ text: string;
4
+ } | {
5
+ type: 'image';
6
+ mime: string;
7
+ data: Uint8Array;
8
+ } | {
9
+ type: 'audio';
10
+ mime: string;
11
+ data: Uint8Array;
12
+ } | {
13
+ type: 'tool_call';
14
+ id: string;
15
+ name: string;
16
+ input: unknown;
17
+ } | {
18
+ type: 'tool_result';
19
+ id: string;
20
+ content: ContentPart[];
21
+ };
22
+ export type Role = 'system' | 'user' | 'assistant';
23
+ export type Message = {
24
+ role: Role;
25
+ content: ContentPart[];
26
+ };
27
+ export declare const text: (value: string) => ContentPart;
28
+ export declare const image: (mime: string, data: Uint8Array) => ContentPart;
29
+ export declare const audio: (mime: string, data: Uint8Array) => ContentPart;
30
+ export interface Tool {
31
+ name: string;
32
+ description: string;
33
+ parameters: Record<string, unknown>;
34
+ prompt?: string;
35
+ run(input: unknown, ctx: {
36
+ signal?: AbortSignal;
37
+ }): Promise<ContentPart[]>;
38
+ }
39
+ export interface Usage {
40
+ input?: number;
41
+ output?: number;
42
+ total?: number;
43
+ contextWindow?: number;
44
+ }
45
+ export type StreamEvent = ContentPart | {
46
+ type: 'usage';
47
+ usage: Usage;
48
+ } | {
49
+ type: 'reasoning';
50
+ text: string;
51
+ };
52
+ export interface Provider {
53
+ stream(req: {
54
+ model: string;
55
+ messages: Message[];
56
+ tools: Tool[];
57
+ signal?: AbortSignal;
58
+ }): AsyncIterable<StreamEvent>;
59
+ }
package/esm/types.js ADDED
@@ -0,0 +1,3 @@
1
+ export const text = (value) => ({ type: 'text', text: value });
2
+ export const image = (mime, data) => ({ type: 'image', mime, data });
3
+ export const audio = (mime, data) => ({ type: 'audio', mime, data });
package/package.json CHANGED
@@ -1,14 +1,18 @@
1
1
  {
2
2
  "name": "mu-core",
3
- "version": "0.15.0",
4
- "description": "Agent loop orchestration core: types, plugin SDK, channels, sessions",
5
- "type": "module",
6
- "main": "./src/index.ts",
7
- "types": "./src/index.ts",
3
+ "version": "0.16.3",
4
+ "description": "Standalone multimodal agentic loop: content, messages, tools, provider interface, createAgent",
5
+ "license": "MIT",
6
+ "main": "./script/index.js",
7
+ "module": "./esm/index.js",
8
8
  "exports": {
9
- ".": "./src/index.ts"
9
+ ".": {
10
+ "import": "./esm/index.js",
11
+ "require": "./script/index.js"
12
+ }
10
13
  },
11
- "files": [
12
- "src"
13
- ]
14
- }
14
+ "scripts": {},
15
+ "type": "module",
16
+ "dependencies": {},
17
+ "_generatedBy": "dnt@dev"
18
+ }
@@ -0,0 +1,39 @@
1
+ import type { ContentPart, Message, Provider, Tool, Usage } from './types.js';
2
+ export type LoopEvent = ContentPart | {
3
+ type: 'usage';
4
+ usage: Usage;
5
+ } | {
6
+ type: 'reasoning';
7
+ text: string;
8
+ } | {
9
+ type: 'message';
10
+ message: Message;
11
+ } | {
12
+ type: 'done';
13
+ messages: Message[];
14
+ };
15
+ export interface RunOptions {
16
+ provider: Provider;
17
+ model: string;
18
+ messages: Message[];
19
+ tools?: Tool[];
20
+ signal?: AbortSignal;
21
+ }
22
+ export declare function run(opts: RunOptions): AsyncIterable<LoopEvent>;
23
+ export interface AgentConfig {
24
+ provider: Provider;
25
+ model: string;
26
+ tools?: Tool[];
27
+ system?: string;
28
+ signal?: AbortSignal;
29
+ }
30
+ export type Input = string | ContentPart[] | Message[];
31
+ export interface AgentResult {
32
+ message: Message;
33
+ messages: Message[];
34
+ }
35
+ export interface Agent {
36
+ stream(input: Input): AsyncIterable<LoopEvent>;
37
+ run(input: Input): Promise<AgentResult>;
38
+ }
39
+ export declare const createAgent: (config: AgentConfig) => Agent;
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createAgent = void 0;
4
+ exports.run = run;
5
+ const append = (parts, part) => {
6
+ const last = parts[parts.length - 1];
7
+ if (part.type === 'text' && last?.type === 'text') {
8
+ last.text += part.text;
9
+ }
10
+ else if (part.type === 'audio' && last?.type === 'audio' && last.mime === part.mime) {
11
+ const merged = new Uint8Array(last.data.length + part.data.length);
12
+ merged.set(last.data);
13
+ merged.set(part.data, last.data.length);
14
+ last.data = merged;
15
+ }
16
+ else if (part.type === 'text') {
17
+ parts.push({ type: 'text', text: part.text });
18
+ }
19
+ else if (part.type === 'audio') {
20
+ parts.push({ type: 'audio', mime: part.mime, data: part.data });
21
+ }
22
+ else {
23
+ parts.push(part);
24
+ }
25
+ };
26
+ const execute = async (tools, call, signal) => {
27
+ const tool = tools.get(call.name);
28
+ if (!tool)
29
+ return [{ type: 'text', text: `Unknown tool: ${call.name}` }];
30
+ try {
31
+ return await tool.run(call.input, { signal });
32
+ }
33
+ catch (err) {
34
+ return [{ type: 'text', text: err instanceof Error ? err.message : String(err) }];
35
+ }
36
+ };
37
+ async function* run(opts) {
38
+ const { provider, model, signal } = opts;
39
+ const tools = opts.tools ?? [];
40
+ const registry = new Map(tools.map((t) => [t.name, t]));
41
+ const messages = [...opts.messages];
42
+ while (true) {
43
+ const content = [];
44
+ const calls = [];
45
+ for await (const event of provider.stream({ model, messages, tools, signal })) {
46
+ if (event.type === 'usage' || event.type === 'reasoning') {
47
+ yield event;
48
+ continue;
49
+ }
50
+ yield event;
51
+ append(content, event);
52
+ if (event.type === 'tool_call')
53
+ calls.push(event);
54
+ }
55
+ const message = { role: 'assistant', content };
56
+ messages.push(message);
57
+ yield { type: 'message', message };
58
+ if (calls.length === 0)
59
+ break;
60
+ const results = await Promise.all(calls.map(async (call) => ({
61
+ type: 'tool_result',
62
+ id: call.id,
63
+ content: await execute(registry, call, signal),
64
+ })));
65
+ const toolMessage = { role: 'user', content: results };
66
+ messages.push(toolMessage);
67
+ yield { type: 'message', message: toolMessage };
68
+ }
69
+ yield { type: 'done', messages };
70
+ }
71
+ const isMessages = (input) => input.length > 0 && 'role' in input[0];
72
+ const toMessages = (input) => {
73
+ if (typeof input === 'string')
74
+ return [{ role: 'user', content: [{ type: 'text', text: input }] }];
75
+ if (isMessages(input))
76
+ return input;
77
+ return [{ role: 'user', content: input }];
78
+ };
79
+ const createAgent = (config) => {
80
+ const tools = config.tools ?? [];
81
+ const build = (input) => {
82
+ const messages = toMessages(input);
83
+ if (!config.system)
84
+ return messages;
85
+ return [{ role: 'system', content: [{ type: 'text', text: config.system }] }, ...messages];
86
+ };
87
+ const stream = (input) => run({ provider: config.provider, model: config.model, tools, messages: build(input), signal: config.signal });
88
+ const runToEnd = async (input) => {
89
+ let message = { role: 'assistant', content: [] };
90
+ let messages = [];
91
+ for await (const event of stream(input)) {
92
+ if (event.type === 'message' && event.message.role === 'assistant')
93
+ message = event.message;
94
+ else if (event.type === 'done')
95
+ messages = event.messages;
96
+ }
97
+ return { message, messages };
98
+ };
99
+ return { stream, run: runToEnd };
100
+ };
101
+ exports.createAgent = createAgent;
@@ -0,0 +1,4 @@
1
+ export type { ContentPart, Message, Provider, Role, StreamEvent, Tool, Usage } from './types.js';
2
+ export { audio, image, text } from './types.js';
3
+ export type { Agent, AgentConfig, AgentResult, Input, LoopEvent, RunOptions } from './agent.js';
4
+ export { createAgent, run } from './agent.js';
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.run = exports.createAgent = exports.text = exports.image = exports.audio = void 0;
4
+ var types_js_1 = require("./types.js");
5
+ Object.defineProperty(exports, "audio", { enumerable: true, get: function () { return types_js_1.audio; } });
6
+ Object.defineProperty(exports, "image", { enumerable: true, get: function () { return types_js_1.image; } });
7
+ Object.defineProperty(exports, "text", { enumerable: true, get: function () { return types_js_1.text; } });
8
+ var agent_js_1 = require("./agent.js");
9
+ Object.defineProperty(exports, "createAgent", { enumerable: true, get: function () { return agent_js_1.createAgent; } });
10
+ Object.defineProperty(exports, "run", { enumerable: true, get: function () { return agent_js_1.run; } });
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -0,0 +1,59 @@
1
+ export type ContentPart = {
2
+ type: 'text';
3
+ text: string;
4
+ } | {
5
+ type: 'image';
6
+ mime: string;
7
+ data: Uint8Array;
8
+ } | {
9
+ type: 'audio';
10
+ mime: string;
11
+ data: Uint8Array;
12
+ } | {
13
+ type: 'tool_call';
14
+ id: string;
15
+ name: string;
16
+ input: unknown;
17
+ } | {
18
+ type: 'tool_result';
19
+ id: string;
20
+ content: ContentPart[];
21
+ };
22
+ export type Role = 'system' | 'user' | 'assistant';
23
+ export type Message = {
24
+ role: Role;
25
+ content: ContentPart[];
26
+ };
27
+ export declare const text: (value: string) => ContentPart;
28
+ export declare const image: (mime: string, data: Uint8Array) => ContentPart;
29
+ export declare const audio: (mime: string, data: Uint8Array) => ContentPart;
30
+ export interface Tool {
31
+ name: string;
32
+ description: string;
33
+ parameters: Record<string, unknown>;
34
+ prompt?: string;
35
+ run(input: unknown, ctx: {
36
+ signal?: AbortSignal;
37
+ }): Promise<ContentPart[]>;
38
+ }
39
+ export interface Usage {
40
+ input?: number;
41
+ output?: number;
42
+ total?: number;
43
+ contextWindow?: number;
44
+ }
45
+ export type StreamEvent = ContentPart | {
46
+ type: 'usage';
47
+ usage: Usage;
48
+ } | {
49
+ type: 'reasoning';
50
+ text: string;
51
+ };
52
+ export interface Provider {
53
+ stream(req: {
54
+ model: string;
55
+ messages: Message[];
56
+ tools: Tool[];
57
+ signal?: AbortSignal;
58
+ }): AsyncIterable<StreamEvent>;
59
+ }
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.audio = exports.image = exports.text = void 0;
4
+ const text = (value) => ({ type: 'text', text: value });
5
+ exports.text = text;
6
+ const image = (mime, data) => ({ type: 'image', mime, data });
7
+ exports.image = image;
8
+ const audio = (mime, data) => ({ type: 'audio', mime, data });
9
+ exports.audio = audio;
package/README.md DELETED
@@ -1,110 +0,0 @@
1
- # mu-core
2
-
3
- The mu plugin SDK. Provides the agent loop, plugin registry, LLM types, and
4
- the multi-host primitives (channels, sessions, activity bus, providers).
5
- Provider implementations are separate packages — for OpenAI-compatible APIs,
6
- add `mu-openai-provider` and register its plugin alongside.
7
-
8
- ## Install
9
-
10
- ```bash
11
- npm install mu-core mu-openai-provider
12
- ```
13
-
14
- ## Usage
15
-
16
- ```ts
17
- import { runAgent, PluginRegistry } from "mu-core";
18
- import type { ChatMessage, ProviderConfig } from "mu-core";
19
- import { createOpenAIProviderPlugin } from "mu-openai-provider";
20
-
21
- const config: ProviderConfig = {
22
- baseUrl: "http://localhost:11434/v1",
23
- maxTokens: 4096,
24
- temperature: 0.7,
25
- streamTimeoutMs: 30000,
26
- // providerId defaults to 'openai' — register at least one provider
27
- // implementation (e.g. via createOpenAIProviderPlugin) before running.
28
- };
29
-
30
- const registry = new PluginRegistry({ cwd: process.cwd(), config: {} });
31
- await registry.register(createOpenAIProviderPlugin());
32
-
33
- const messages: ChatMessage[] = [
34
- { role: "user", content: "Hello" },
35
- ];
36
-
37
- const controller = new AbortController();
38
-
39
- for await (const event of runAgent(messages, config, "qwen2.5", controller.signal, registry)) {
40
- if (event.type === "content") process.stdout.write(event.text);
41
- }
42
- ```
43
-
44
- For a higher-level API that owns conversation state, channel I/O, and
45
- multi-session lifecycle, see `startMu` and `Session` (`createSessionManager`).
46
-
47
- ## Plugin System
48
-
49
- Plugins can provide tools, system prompts, lifecycle hooks, slash commands,
50
- custom agent loops, and side-channel registries (channels, providers,
51
- activity bus, agent sources).
52
-
53
- ```ts
54
- import type { Plugin } from "mu-core";
55
-
56
- const myPlugin: Plugin = {
57
- name: "my-plugin",
58
- tools: [
59
- {
60
- definition: {
61
- type: "function",
62
- function: {
63
- name: "hello",
64
- description: "Say hello",
65
- parameters: { type: "object", properties: {} },
66
- },
67
- },
68
- execute: async () => "Hello, world!",
69
- },
70
- ],
71
- hooks: {
72
- beforeLlmCall: (messages, config) => messages,
73
- afterLlmCall: (result) => result,
74
- beforeToolExec: (toolCall) => toolCall,
75
- afterToolExec: (toolCall, result) => result,
76
- },
77
- };
78
-
79
- await registry.register(myPlugin);
80
- ```
81
-
82
- Filesystem and shell tools live in `mu-coding` (`createCodingToolsPlugin`),
83
- not in mu-core — keeps the SDK host-agnostic.
84
-
85
- ## API
86
-
87
- ### Agent loop
88
- - `runAgent(messages, config, model, signal, registry)` — async generator yielding `AgentEvent` (`content`, `reasoning`, `messages`, `usage`, `turn_end`).
89
- - Provider resolution: looks up `config.providerId ?? 'openai'` in the registered `ProviderRegistry`. Throws if no provider is registered.
90
-
91
- ### Sessions
92
- - `createSessionManager({ registry, config, model })` returns a `SessionManager`.
93
- - `session.runTurn({ userMessage, ... })` — appends, drains queue, runs agent loop, emits events.
94
- - `session.subscribe(listener)` — `messages_changed`, `stream_partial`, `stream_started`, `stream_ended`, `usage`, `error`.
95
-
96
- ### Channels
97
- - `Channel` interface (`id`, `start`, `stop`) — input surfaces (TUI, Telegram, websocket).
98
- - `createChannelRegistry()` — host-managed registry; `startAll()` / `stopAll()` for lifecycle.
99
-
100
- ### Providers
101
- - `ProviderAdapter` + `createProvider(adapter)` — build a `Provider` from raw HTTP semantics.
102
- - `readSSE`, `readNDJSON`, `fetchWithIdleTimeout` — transport primitives.
103
- - `ProviderRegistry` — host-managed; populated by provider plugins.
104
-
105
- ### Host
106
- - `startMu(options)` — generic bootstrap: loads config, builds registries, activates plugins (config-listed via `options.resolvePlugin`, then code-passed), starts channels.
107
-
108
- ## License
109
-
110
- MIT
@@ -1,44 +0,0 @@
1
- import { describe, expect, it } from 'bun:test';
2
- import { createActivityBus } from './activity';
3
-
4
- describe('ActivityBus', () => {
5
- it('emits events to multiple subscribers', () => {
6
- const bus = createActivityBus();
7
- const a: string[] = [];
8
- const b: string[] = [];
9
- bus.subscribe((e) => a.push(e.summary));
10
- bus.subscribe((e) => b.push(e.summary));
11
- bus.emit('tool_start', 'bash', 'running git status');
12
- expect(a).toEqual(['running git status']);
13
- expect(b).toEqual(['running git status']);
14
- });
15
-
16
- it('unsubscribes', () => {
17
- const bus = createActivityBus();
18
- const seen: string[] = [];
19
- const off = bus.subscribe((e) => seen.push(e.summary));
20
- bus.emit('tool_start', 'bash', 'first');
21
- off();
22
- bus.emit('tool_start', 'bash', 'second');
23
- expect(seen).toEqual(['first']);
24
- });
25
-
26
- it('subagent stream is independent', () => {
27
- const bus = createActivityBus();
28
- const sub: string[] = [];
29
- bus.subscribeSubAgent((e) => sub.push(e.kind));
30
- bus.emitSubAgent({ runId: 'r1', agentId: 'review', kind: 'invocation_start', ts: 1, data: {} });
31
- expect(sub).toEqual(['invocation_start']);
32
- });
33
-
34
- it('throwing listener does not break the bus', () => {
35
- const bus = createActivityBus();
36
- bus.subscribe(() => {
37
- throw new Error('boom');
38
- });
39
- const seen: string[] = [];
40
- bus.subscribe((e) => seen.push(e.summary));
41
- bus.emit('tool_end', 'bash', 'ok');
42
- expect(seen).toEqual(['ok']);
43
- });
44
- });