mu-core 0.16.1 → 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 +13 -9
- 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/src/agent.test.ts +0 -45
- package/src/agent.ts +0 -140
- package/src/index.ts +0 -5
- package/src/types.ts +0 -37
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
package/esm/package.json
ADDED
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
package/package.json
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mu-core",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.3",
|
|
4
4
|
"description": "Standalone multimodal agentic loop: content, messages, tools, provider interface, createAgent",
|
|
5
|
-
"
|
|
6
|
-
"main": "./
|
|
7
|
-
"
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"main": "./script/index.js",
|
|
7
|
+
"module": "./esm/index.js",
|
|
8
8
|
"exports": {
|
|
9
|
-
".":
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./esm/index.js",
|
|
11
|
+
"require": "./script/index.js"
|
|
12
|
+
}
|
|
10
13
|
},
|
|
11
|
-
"
|
|
12
|
-
|
|
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;
|
package/script/agent.js
ADDED
|
@@ -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';
|
package/script/index.js
ADDED
|
@@ -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,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/script/types.js
ADDED
|
@@ -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/src/agent.test.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
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;
|
|
11
|
-
},
|
|
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]) }],
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
|
|
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' }]);
|
|
45
|
-
});
|
package/src/agent.ts
DELETED
|
@@ -1,140 +0,0 @@
|
|
|
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);
|
|
27
|
-
}
|
|
28
|
-
};
|
|
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}` }];
|
|
33
|
-
try {
|
|
34
|
-
return await tool.run(call.input, { signal });
|
|
35
|
-
} catch (err) {
|
|
36
|
-
return [{ type: 'text', text: err instanceof Error ? err.message : String(err) }];
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
export interface RunOptions {
|
|
41
|
-
provider: Provider;
|
|
42
|
-
model: string;
|
|
43
|
-
messages: Message[];
|
|
44
|
-
tools?: Tool[];
|
|
45
|
-
signal?: AbortSignal;
|
|
46
|
-
}
|
|
47
|
-
|
|
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);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const message: Message = { role: 'assistant', content };
|
|
69
|
-
messages.push(message);
|
|
70
|
-
yield { type: 'message', message };
|
|
71
|
-
|
|
72
|
-
if (calls.length === 0) break;
|
|
73
|
-
|
|
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 };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
yield { type: 'done', messages };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export interface AgentConfig {
|
|
90
|
-
provider: Provider;
|
|
91
|
-
model: string;
|
|
92
|
-
tools?: Tool[];
|
|
93
|
-
system?: string;
|
|
94
|
-
signal?: AbortSignal;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export type Input = string | ContentPart[] | Message[];
|
|
98
|
-
|
|
99
|
-
export interface AgentResult {
|
|
100
|
-
message: Message;
|
|
101
|
-
messages: Message[];
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export interface Agent {
|
|
105
|
-
stream(input: Input): AsyncIterable<LoopEvent>;
|
|
106
|
-
run(input: Input): Promise<AgentResult>;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const isMessages = (input: ContentPart[] | Message[]): input is Message[] => input.length > 0 && 'role' in input[0];
|
|
110
|
-
|
|
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 });
|
|
128
|
-
|
|
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;
|
|
135
|
-
}
|
|
136
|
-
return { message, messages };
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
return { stream, run: runToEnd };
|
|
140
|
-
};
|
package/src/index.ts
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
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
|
-
}
|