mu-core 0.15.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/package.json +2 -2
- package/src/agent.test.ts +41 -87
- package/src/agent.ts +118 -227
- package/src/index.ts +5 -74
- package/src/types.ts +37 -0
- package/README.md +0 -110
- package/src/activity.test.ts +0 -44
- package/src/activity.ts +0 -83
- 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/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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mu-core",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.16.1",
|
|
4
|
+
"description": "Standalone multimodal agentic loop: content, messages, tools, provider interface, createAgent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
7
7
|
"types": "./src/index.ts",
|
package/src/agent.test.ts
CHANGED
|
@@ -1,91 +1,45 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
*
|
|
10
|
-
|
|
11
|
-
import { describe, expect, it } from 'bun:test';
|
|
12
|
-
import { runAgent } from './agent';
|
|
13
|
-
import { createProviderRegistry } from './provider/registry';
|
|
14
|
-
import { PluginRegistry } from './registry';
|
|
15
|
-
import type { ChatMessage, ProviderConfig, StreamChunk } from './types/llm';
|
|
16
|
-
|
|
17
|
-
interface CapturedCall {
|
|
18
|
-
messages: ChatMessage[];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function fakeRegistry(captured: CapturedCall[]): PluginRegistry {
|
|
22
|
-
const providers = createProviderRegistry();
|
|
23
|
-
providers.register({
|
|
24
|
-
id: 'openai',
|
|
25
|
-
async *streamChat(messages: ChatMessage[]): AsyncIterable<StreamChunk> {
|
|
26
|
-
// Snapshot the exact message list the provider receives so the test
|
|
27
|
-
// can assert on what `streamTurn` actually sent over the wire.
|
|
28
|
-
captured.push({ messages: messages.map((m) => ({ ...m })) });
|
|
29
|
-
yield { type: 'content', text: 'ok' };
|
|
1
|
+
import { assertEquals } from '@std/assert';
|
|
2
|
+
import { createAgent } from './agent';
|
|
3
|
+
import { image } from './types';
|
|
4
|
+
import type { ContentPart, Provider, Tool } from './types';
|
|
5
|
+
|
|
6
|
+
const scripted = (turns: ContentPart[][]): Provider => {
|
|
7
|
+
let i = 0;
|
|
8
|
+
return {
|
|
9
|
+
async *stream() {
|
|
10
|
+
for (const event of turns[i++]) yield event;
|
|
30
11
|
},
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
for await (const _ of runAgent(messages, CFG, 'm', new AbortController().signal, registry)) {
|
|
57
|
-
// drain
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
expect(captured.length).toBe(1);
|
|
61
|
-
const sent = captured[0].messages;
|
|
62
|
-
// Only system + user reach the provider.
|
|
63
|
-
expect(sent.length).toBe(2);
|
|
64
|
-
expect(sent[0].role).toBe('system');
|
|
65
|
-
expect(sent[1].role).toBe('user');
|
|
66
|
-
expect(sent.find((m) => m.content === 'phantom header')).toBeUndefined();
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
Deno.test('loops on a tool_call and returns the final message', async () => {
|
|
16
|
+
const provider = scripted([
|
|
17
|
+
[{ type: 'tool_call', id: '1', name: 'snap', input: {} }],
|
|
18
|
+
[{ type: 'text', text: 'voici' }],
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const snap: Tool = {
|
|
22
|
+
name: 'snap',
|
|
23
|
+
description: 'returns an image',
|
|
24
|
+
parameters: {},
|
|
25
|
+
run: async () => [image('image/png', new Uint8Array([1, 2, 3]))],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const agent = createAgent({ provider, model: 'mock', tools: [snap] });
|
|
29
|
+
const { message, messages } = await agent.run('photo ?');
|
|
30
|
+
|
|
31
|
+
assertEquals(message.content, [{ type: 'text', text: 'voici' }]);
|
|
32
|
+
const tool = messages.find((m) => m.role === 'user' && m.content[0]?.type === 'tool_result');
|
|
33
|
+
assertEquals(tool?.content[0], {
|
|
34
|
+
type: 'tool_result',
|
|
35
|
+
id: '1',
|
|
36
|
+
content: [{ type: 'image', mime: 'image/png', data: new Uint8Array([1, 2, 3]) }],
|
|
67
37
|
});
|
|
38
|
+
});
|
|
68
39
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const registry = fakeRegistry(captured);
|
|
75
|
-
|
|
76
|
-
const messages: ChatMessage[] = [
|
|
77
|
-
{ role: 'system', content: 'sys' },
|
|
78
|
-
{ role: 'user', content: 'silent reminder', display: { hidden: true } },
|
|
79
|
-
{ role: 'user', content: 'hi' },
|
|
80
|
-
];
|
|
81
|
-
|
|
82
|
-
for await (const _ of runAgent(messages, CFG, 'm', new AbortController().signal, registry)) {
|
|
83
|
-
// drain
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
expect(captured.length).toBe(1);
|
|
87
|
-
const sent = captured[0].messages;
|
|
88
|
-
expect(sent.length).toBe(3);
|
|
89
|
-
expect(sent.find((m) => m.content === 'silent reminder')).toBeDefined();
|
|
90
|
-
});
|
|
40
|
+
Deno.test('merges streamed text deltas', async () => {
|
|
41
|
+
const provider = scripted([[{ type: 'text', text: 'bon' }, { type: 'text', text: 'jour' }]]);
|
|
42
|
+
const agent = createAgent({ provider, model: 'mock' });
|
|
43
|
+
const { message } = await agent.run('salut');
|
|
44
|
+
assertEquals(message.content, [{ type: 'text', text: 'bonjour' }]);
|
|
91
45
|
});
|
package/src/agent.ts
CHANGED
|
@@ -1,249 +1,140 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
): AsyncIterable<StreamChunk> {
|
|
28
|
-
const id = config.providerId ?? DEFAULT_PROVIDER_ID;
|
|
29
|
-
const providers = registry.getProviders();
|
|
30
|
-
const provider = providers?.get(id);
|
|
31
|
-
if (!provider) {
|
|
32
|
-
throw new Error(
|
|
33
|
-
`No provider registered for id "${id}". Register one (e.g. via mu-openai-provider) before calling runAgent.`,
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
return provider.streamChat(messages, config, model, options);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async function executeTool(call: ToolCall, tools: PluginTool[], signal?: AbortSignal): Promise<ToolResult> {
|
|
40
|
-
let args: Record<string, unknown>;
|
|
41
|
-
try {
|
|
42
|
-
args = JSON.parse(call.function.arguments);
|
|
43
|
-
} catch {
|
|
44
|
-
return { tool_call_id: call.id, name: call.function.name, content: 'Error: Invalid JSON arguments', error: true };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const tool = tools.find((t) => t.definition.function.name === call.function.name);
|
|
48
|
-
if (!tool) {
|
|
49
|
-
return {
|
|
50
|
-
tool_call_id: call.id,
|
|
51
|
-
name: call.function.name,
|
|
52
|
-
content: `Error: Unknown tool: ${call.function.name}`,
|
|
53
|
-
error: true,
|
|
54
|
-
};
|
|
1
|
+
import type { ContentPart, Message, Provider, Tool, Usage } from './types';
|
|
2
|
+
|
|
3
|
+
type ToolCallPart = Extract<ContentPart, { type: 'tool_call' }>;
|
|
4
|
+
|
|
5
|
+
export type LoopEvent =
|
|
6
|
+
| ContentPart
|
|
7
|
+
| { type: 'usage'; usage: Usage }
|
|
8
|
+
| { type: 'reasoning'; text: string }
|
|
9
|
+
| { type: 'message'; message: Message }
|
|
10
|
+
| { type: 'done'; messages: Message[] };
|
|
11
|
+
|
|
12
|
+
const append = (parts: ContentPart[], part: ContentPart): void => {
|
|
13
|
+
const last = parts[parts.length - 1];
|
|
14
|
+
if (part.type === 'text' && last?.type === 'text') {
|
|
15
|
+
last.text += part.text;
|
|
16
|
+
} else if (part.type === 'audio' && last?.type === 'audio' && last.mime === part.mime) {
|
|
17
|
+
const merged = new Uint8Array(last.data.length + part.data.length);
|
|
18
|
+
merged.set(last.data);
|
|
19
|
+
merged.set(part.data, last.data.length);
|
|
20
|
+
last.data = merged;
|
|
21
|
+
} else if (part.type === 'text') {
|
|
22
|
+
parts.push({ type: 'text', text: part.text });
|
|
23
|
+
} else if (part.type === 'audio') {
|
|
24
|
+
parts.push({ type: 'audio', mime: part.mime, data: part.data });
|
|
25
|
+
} else {
|
|
26
|
+
parts.push(part);
|
|
55
27
|
}
|
|
28
|
+
};
|
|
56
29
|
|
|
30
|
+
const execute = async (tools: Map<string, Tool>, call: ToolCallPart, signal?: AbortSignal): Promise<ContentPart[]> => {
|
|
31
|
+
const tool = tools.get(call.name);
|
|
32
|
+
if (!tool) return [{ type: 'text', text: `Unknown tool: ${call.name}` }];
|
|
57
33
|
try {
|
|
58
|
-
|
|
59
|
-
// Tools may return either a plain string (error inferred from "Error:"
|
|
60
|
-
// prefix — legacy/convenience form) or a typed `{ content, error }`
|
|
61
|
-
// object that makes the error flag explicit. Both forms are accepted to
|
|
62
|
-
// avoid breaking existing plugin authors.
|
|
63
|
-
if (typeof result === 'string') {
|
|
64
|
-
return { tool_call_id: call.id, name: call.function.name, content: result, error: result.startsWith('Error:') };
|
|
65
|
-
}
|
|
66
|
-
return {
|
|
67
|
-
tool_call_id: call.id,
|
|
68
|
-
name: call.function.name,
|
|
69
|
-
content: result.content,
|
|
70
|
-
error: result.error ?? false,
|
|
71
|
-
};
|
|
34
|
+
return await tool.run(call.input, { signal });
|
|
72
35
|
} catch (err) {
|
|
73
|
-
|
|
74
|
-
return { tool_call_id: call.id, name: call.function.name, content: `Error: ${msg}`, error: true };
|
|
36
|
+
return [{ type: 'text', text: err instanceof Error ? err.message : String(err) }];
|
|
75
37
|
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export interface RunOptions {
|
|
41
|
+
provider: Provider;
|
|
42
|
+
model: string;
|
|
43
|
+
messages: Message[];
|
|
44
|
+
tools?: Tool[];
|
|
45
|
+
signal?: AbortSignal;
|
|
76
46
|
}
|
|
77
47
|
|
|
78
|
-
async function*
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
// strips it from the LLM payload. Applied AFTER `beforeLlmHooks` so plugin
|
|
97
|
-
// hooks still see the full transcript if they want it; this is the very
|
|
98
|
-
// last filter before the network call. Inverse of `display.hidden`, which
|
|
99
|
-
// hides from UI but keeps in the LLM payload.
|
|
100
|
-
const llmMessages = hookedMessages.filter((m) => !m.display?.llmHidden);
|
|
101
|
-
const toolDefinitions = toolDefs.map((t) => t.definition);
|
|
102
|
-
|
|
103
|
-
for await (const chunk of streamChatViaRegistry(registry, llmMessages, config, model, {
|
|
104
|
-
signal,
|
|
105
|
-
tools: toolDefinitions,
|
|
106
|
-
onUsage: (u) => {
|
|
107
|
-
usage = u.totalTokens;
|
|
108
|
-
promptTokens = u.promptTokens;
|
|
109
|
-
cachedPromptTokens = u.cachedPromptTokens ?? 0;
|
|
110
|
-
},
|
|
111
|
-
})) {
|
|
112
|
-
if (signal.aborted) {
|
|
113
|
-
break;
|
|
114
|
-
}
|
|
115
|
-
if (chunk.type === 'reasoning') {
|
|
116
|
-
reasoning += chunk.text;
|
|
117
|
-
yield { type: 'reasoning', text: reasoning };
|
|
118
|
-
} else if (chunk.type === 'content') {
|
|
119
|
-
content += chunk.text;
|
|
120
|
-
yield { type: 'content', text: content };
|
|
121
|
-
} else if (chunk.type === 'tool_call') {
|
|
122
|
-
toolCalls.push({ id: chunk.toolCall.id, function: chunk.toolCall.function });
|
|
48
|
+
export async function* run(opts: RunOptions): AsyncIterable<LoopEvent> {
|
|
49
|
+
const { provider, model, signal } = opts;
|
|
50
|
+
const tools = opts.tools ?? [];
|
|
51
|
+
const registry = new Map(tools.map((t) => [t.name, t]));
|
|
52
|
+
const messages = [...opts.messages];
|
|
53
|
+
|
|
54
|
+
while (true) {
|
|
55
|
+
const content: ContentPart[] = [];
|
|
56
|
+
const calls: ToolCallPart[] = [];
|
|
57
|
+
|
|
58
|
+
for await (const event of provider.stream({ model, messages, tools, signal })) {
|
|
59
|
+
if (event.type === 'usage' || event.type === 'reasoning') {
|
|
60
|
+
yield event;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
yield event;
|
|
64
|
+
append(content, event);
|
|
65
|
+
if (event.type === 'tool_call') calls.push(event);
|
|
123
66
|
}
|
|
124
|
-
}
|
|
125
67
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
68
|
+
const message: Message = { role: 'assistant', content };
|
|
69
|
+
messages.push(message);
|
|
70
|
+
yield { type: 'message', message };
|
|
71
|
+
|
|
72
|
+
if (calls.length === 0) break;
|
|
129
73
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const content = await runAfterToolExecHook(hooks, tc, hookOutcome.content);
|
|
141
|
-
return runDecorateMessageHooks(hooks, {
|
|
142
|
-
role: 'tool',
|
|
143
|
-
content,
|
|
144
|
-
toolCallId: tc.id,
|
|
145
|
-
toolResult: { name: tc.function.name, content, error: hookOutcome.error ?? true },
|
|
146
|
-
toolCallArgs: { [tc.function.name]: tc.function.arguments },
|
|
147
|
-
});
|
|
74
|
+
const results: ContentPart[] = await Promise.all(
|
|
75
|
+
calls.map(async (call) => ({
|
|
76
|
+
type: 'tool_result' as const,
|
|
77
|
+
id: call.id,
|
|
78
|
+
content: await execute(registry, call, signal),
|
|
79
|
+
})),
|
|
80
|
+
);
|
|
81
|
+
const toolMessage: Message = { role: 'user', content: results };
|
|
82
|
+
messages.push(toolMessage);
|
|
83
|
+
yield { type: 'message', message: toolMessage };
|
|
148
84
|
}
|
|
149
85
|
|
|
150
|
-
|
|
151
|
-
const content = await runAfterToolExecHook(hooks, hookOutcome, result.content);
|
|
152
|
-
return runDecorateMessageHooks(hooks, {
|
|
153
|
-
role: 'tool',
|
|
154
|
-
content,
|
|
155
|
-
toolCallId: result.tool_call_id,
|
|
156
|
-
toolResult: { name: result.name, content, error: result.error },
|
|
157
|
-
toolCallArgs: { [result.name]: tc.function.arguments },
|
|
158
|
-
});
|
|
86
|
+
yield { type: 'done', messages };
|
|
159
87
|
}
|
|
160
88
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
): AsyncGenerator<AgentEvent, ChatMessage[]> {
|
|
168
|
-
let current = start;
|
|
169
|
-
|
|
170
|
-
for (const tc of calls) {
|
|
171
|
-
if (signal.aborted) break;
|
|
172
|
-
const toolMessage = await executeOneToolCall(tc, tools, signal, registry);
|
|
173
|
-
if (signal.aborted) break;
|
|
174
|
-
current = [...current, toolMessage];
|
|
175
|
-
yield { type: 'messages', messages: current };
|
|
176
|
-
}
|
|
177
|
-
return current;
|
|
89
|
+
export interface AgentConfig {
|
|
90
|
+
provider: Provider;
|
|
91
|
+
model: string;
|
|
92
|
+
tools?: Tool[];
|
|
93
|
+
system?: string;
|
|
94
|
+
signal?: AbortSignal;
|
|
178
95
|
}
|
|
179
96
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
return { ...config, systemPrompt: transformed };
|
|
97
|
+
export type Input = string | ContentPart[] | Message[];
|
|
98
|
+
|
|
99
|
+
export interface AgentResult {
|
|
100
|
+
message: Message;
|
|
101
|
+
messages: Message[];
|
|
186
102
|
}
|
|
187
103
|
|
|
188
|
-
export
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
signal: AbortSignal,
|
|
193
|
-
registry: PluginRegistry,
|
|
194
|
-
): AsyncGenerator<AgentEvent> {
|
|
195
|
-
const mergedConfig = await buildMergedConfig(config, registry);
|
|
196
|
-
const hooks = registry.getHooks();
|
|
197
|
-
|
|
198
|
-
// Wrap the body in try/finally so `afterAgentRun` fires exactly once per
|
|
199
|
-
// `runAgent` call, regardless of how it terminates: normal completion (LLM
|
|
200
|
-
// produced a final response), abort via `signal.aborted`, or generator
|
|
201
|
-
// cancellation by the consumer (Ink unmount, manual `.return()`).
|
|
202
|
-
try {
|
|
203
|
-
const customLoop = registry.getAgentLoop();
|
|
204
|
-
if (customLoop) {
|
|
205
|
-
const filtered = await registry.getFilteredTools();
|
|
206
|
-
yield* customLoop.run(initialMessages, mergedConfig, model, signal, filtered, hooks);
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
104
|
+
export interface Agent {
|
|
105
|
+
stream(input: Input): AsyncIterable<LoopEvent>;
|
|
106
|
+
run(input: Input): Promise<AgentResult>;
|
|
107
|
+
}
|
|
209
108
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
while (!signal.aborted) {
|
|
213
|
-
// Re-evaluate every turn so per-turn agent switches are honoured.
|
|
214
|
-
const tools = await registry.getFilteredTools();
|
|
215
|
-
const { content, reasoning, toolCalls, usage, promptTokens, cachedPromptTokens } = yield* streamTurn(
|
|
216
|
-
current,
|
|
217
|
-
mergedConfig,
|
|
218
|
-
model,
|
|
219
|
-
signal,
|
|
220
|
-
registry,
|
|
221
|
-
tools,
|
|
222
|
-
);
|
|
223
|
-
|
|
224
|
-
if (usage > 0) {
|
|
225
|
-
yield { type: 'usage', totalTokens: usage, promptTokens, cachedTokens: cachedPromptTokens };
|
|
226
|
-
}
|
|
227
|
-
if (signal.aborted) {
|
|
228
|
-
break;
|
|
229
|
-
}
|
|
109
|
+
const isMessages = (input: ContentPart[] | Message[]): input is Message[] => input.length > 0 && 'role' in input[0];
|
|
230
110
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
111
|
+
const toMessages = (input: Input): Message[] => {
|
|
112
|
+
if (typeof input === 'string') return [{ role: 'user', content: [{ type: 'text', text: input }] }];
|
|
113
|
+
if (isMessages(input)) return input;
|
|
114
|
+
return [{ role: 'user', content: input }];
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const createAgent = (config: AgentConfig): Agent => {
|
|
118
|
+
const tools = config.tools ?? [];
|
|
119
|
+
|
|
120
|
+
const build = (input: Input): Message[] => {
|
|
121
|
+
const messages = toMessages(input);
|
|
122
|
+
if (!config.system) return messages;
|
|
123
|
+
return [{ role: 'system', content: [{ type: 'text', text: config.system }] }, ...messages];
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const stream = (input: Input): AsyncIterable<LoopEvent> =>
|
|
127
|
+
run({ provider: config.provider, model: config.model, tools, messages: build(input), signal: config.signal });
|
|
239
128
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
129
|
+
const runToEnd = async (input: Input): Promise<AgentResult> => {
|
|
130
|
+
let message: Message = { role: 'assistant', content: [] };
|
|
131
|
+
let messages: Message[] = [];
|
|
132
|
+
for await (const event of stream(input)) {
|
|
133
|
+
if (event.type === 'message' && event.message.role === 'assistant') message = event.message;
|
|
134
|
+
else if (event.type === 'done') messages = event.messages;
|
|
245
135
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
136
|
+
return { message, messages };
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
return { stream, run: runToEnd };
|
|
140
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -1,74 +1,5 @@
|
|
|
1
|
-
export type {
|
|
2
|
-
export {
|
|
3
|
-
|
|
4
|
-
export type {
|
|
5
|
-
export {
|
|
6
|
-
export { runDecorateMessageHooks, runTransformUserInputHooks } from './hooks';
|
|
7
|
-
export type { MuConfigShape, MuHandle, StartMuOptions } from './host/index';
|
|
8
|
-
export { startMu } from './host/index';
|
|
9
|
-
export type {
|
|
10
|
-
AgentEndReason,
|
|
11
|
-
AgentEvent,
|
|
12
|
-
AgentLoopStrategy,
|
|
13
|
-
AgentSourceRegistry,
|
|
14
|
-
BeforeToolExecResult,
|
|
15
|
-
CommandContext,
|
|
16
|
-
InputInfoSegment,
|
|
17
|
-
LifecycleHooks,
|
|
18
|
-
MentionCompletion,
|
|
19
|
-
MentionProvider,
|
|
20
|
-
MessageBus,
|
|
21
|
-
MessageRenderer,
|
|
22
|
-
Plugin,
|
|
23
|
-
PluginContext,
|
|
24
|
-
PluginExtras,
|
|
25
|
-
PluginRegistryView,
|
|
26
|
-
PluginTool,
|
|
27
|
-
PluginToolPermission,
|
|
28
|
-
ShortcutHandler,
|
|
29
|
-
SlashCommand,
|
|
30
|
-
StatusSegment,
|
|
31
|
-
ToolBlock,
|
|
32
|
-
ToolDisplayHint,
|
|
33
|
-
ToolExecutor,
|
|
34
|
-
ToolExecutorResult,
|
|
35
|
-
ToolResult,
|
|
36
|
-
TurnResult,
|
|
37
|
-
UserInputTransform,
|
|
38
|
-
} from './plugin';
|
|
39
|
-
export type {
|
|
40
|
-
ChatRequestInput,
|
|
41
|
-
ModelsRequestInput,
|
|
42
|
-
ParsedChatEvent,
|
|
43
|
-
Provider,
|
|
44
|
-
ProviderAdapter,
|
|
45
|
-
RequestSpec,
|
|
46
|
-
} from './provider/adapter';
|
|
47
|
-
export { createProvider } from './provider/adapter';
|
|
48
|
-
export type { ProviderRegistry } from './provider/registry';
|
|
49
|
-
export { createProviderRegistry } from './provider/registry';
|
|
50
|
-
export { fetchWithIdleTimeout, readNDJSON, readSSE } from './provider/transport';
|
|
51
|
-
export { PluginRegistry, type PluginRegistryOptions } from './registry';
|
|
52
|
-
export type {
|
|
53
|
-
CreateSessionManagerOptions,
|
|
54
|
-
RunTurnOptions,
|
|
55
|
-
Session,
|
|
56
|
-
SessionEvent,
|
|
57
|
-
SessionInit,
|
|
58
|
-
SessionManager,
|
|
59
|
-
} from './session';
|
|
60
|
-
export { createSessionManager } from './session';
|
|
61
|
-
export type {
|
|
62
|
-
ApiModel,
|
|
63
|
-
ChatMessage,
|
|
64
|
-
ImageAttachment,
|
|
65
|
-
MessageDisplay,
|
|
66
|
-
ProviderConfig,
|
|
67
|
-
StreamChunk,
|
|
68
|
-
StreamOptions,
|
|
69
|
-
ToolCall,
|
|
70
|
-
ToolDefinition,
|
|
71
|
-
ToolResultInfo,
|
|
72
|
-
Usage,
|
|
73
|
-
} from './types/llm';
|
|
74
|
-
export { ConsoleUIService, type UINotifyLevel, type UIService } from './ui';
|
|
1
|
+
export type { ContentPart, Message, Provider, Role, StreamEvent, Tool, Usage } from './types';
|
|
2
|
+
export { audio, image, text } from './types';
|
|
3
|
+
|
|
4
|
+
export type { Agent, AgentConfig, AgentResult, Input, LoopEvent, RunOptions } from './agent';
|
|
5
|
+
export { createAgent, run } from './agent';
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type ContentPart =
|
|
2
|
+
| { type: 'text'; text: string }
|
|
3
|
+
| { type: 'image'; mime: string; data: Uint8Array }
|
|
4
|
+
| { type: 'audio'; mime: string; data: Uint8Array }
|
|
5
|
+
| { type: 'tool_call'; id: string; name: string; input: unknown }
|
|
6
|
+
| { type: 'tool_result'; id: string; content: ContentPart[] };
|
|
7
|
+
|
|
8
|
+
export type Role = 'system' | 'user' | 'assistant';
|
|
9
|
+
export type Message = { role: Role; content: ContentPart[] };
|
|
10
|
+
|
|
11
|
+
export const text = (value: string): ContentPart => ({ type: 'text', text: value });
|
|
12
|
+
export const image = (mime: string, data: Uint8Array): ContentPart => ({ type: 'image', mime, data });
|
|
13
|
+
export const audio = (mime: string, data: Uint8Array): ContentPart => ({ type: 'audio', mime, data });
|
|
14
|
+
|
|
15
|
+
export interface Tool {
|
|
16
|
+
name: string;
|
|
17
|
+
description: string;
|
|
18
|
+
parameters: Record<string, unknown>;
|
|
19
|
+
prompt?: string;
|
|
20
|
+
run(input: unknown, ctx: { signal?: AbortSignal }): Promise<ContentPart[]>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface Usage {
|
|
24
|
+
input?: number;
|
|
25
|
+
output?: number;
|
|
26
|
+
total?: number;
|
|
27
|
+
contextWindow?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type StreamEvent =
|
|
31
|
+
| ContentPart
|
|
32
|
+
| { type: 'usage'; usage: Usage }
|
|
33
|
+
| { type: 'reasoning'; text: string };
|
|
34
|
+
|
|
35
|
+
export interface Provider {
|
|
36
|
+
stream(req: { model: string; messages: Message[]; tools: Tool[]; signal?: AbortSignal }): AsyncIterable<StreamEvent>;
|
|
37
|
+
}
|