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
|
@@ -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
|
-
});
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Transport primitives: SSE / NDJSON readers + fetch-with-idle-timeout.
|
|
3
|
-
*
|
|
4
|
-
* Used by `createProvider` to drive the lower-level HTTP plumbing without
|
|
5
|
-
* each adapter having to re-implement framing.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export async function* readSSE(response: Response, signal?: AbortSignal): AsyncGenerator<string> {
|
|
9
|
-
if (!response.body) throw new Error('Response has no body');
|
|
10
|
-
const reader = response.body.getReader();
|
|
11
|
-
const decoder = new TextDecoder();
|
|
12
|
-
let buffer = '';
|
|
13
|
-
try {
|
|
14
|
-
while (true) {
|
|
15
|
-
if (signal?.aborted) throw new Error('aborted');
|
|
16
|
-
const { value, done } = await reader.read();
|
|
17
|
-
if (done) break;
|
|
18
|
-
buffer += decoder.decode(value, { stream: true });
|
|
19
|
-
// SSE events are separated by blank lines.
|
|
20
|
-
let idx: number;
|
|
21
|
-
// biome-ignore lint/suspicious/noAssignInExpressions: idiomatic
|
|
22
|
-
while ((idx = buffer.indexOf('\n\n')) !== -1) {
|
|
23
|
-
const event = buffer.slice(0, idx);
|
|
24
|
-
buffer = buffer.slice(idx + 2);
|
|
25
|
-
for (const line of event.split('\n')) {
|
|
26
|
-
if (line.startsWith('data:')) {
|
|
27
|
-
yield line.slice(5).trim();
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
} finally {
|
|
33
|
-
reader.releaseLock();
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export async function* readNDJSON(response: Response, signal?: AbortSignal): AsyncGenerator<string> {
|
|
38
|
-
if (!response.body) throw new Error('Response has no body');
|
|
39
|
-
const reader = response.body.getReader();
|
|
40
|
-
const decoder = new TextDecoder();
|
|
41
|
-
let buffer = '';
|
|
42
|
-
try {
|
|
43
|
-
while (true) {
|
|
44
|
-
if (signal?.aborted) throw new Error('aborted');
|
|
45
|
-
const { value, done } = await reader.read();
|
|
46
|
-
if (done) break;
|
|
47
|
-
buffer += decoder.decode(value, { stream: true });
|
|
48
|
-
let nl: number;
|
|
49
|
-
// biome-ignore lint/suspicious/noAssignInExpressions: idiomatic
|
|
50
|
-
while ((nl = buffer.indexOf('\n')) !== -1) {
|
|
51
|
-
const line = buffer.slice(0, nl).trim();
|
|
52
|
-
buffer = buffer.slice(nl + 1);
|
|
53
|
-
if (line) yield line;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
if (buffer.trim()) yield buffer.trim();
|
|
57
|
-
} finally {
|
|
58
|
-
reader.releaseLock();
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Fetch with an idle timeout: aborts if no bytes are received from the
|
|
64
|
-
* server for `timeoutMs` consecutive milliseconds.
|
|
65
|
-
*
|
|
66
|
-
* Returns the Response *and* a `resetIdle()` callback the caller invokes
|
|
67
|
-
* each time it consumes a chunk so the timer slides forward.
|
|
68
|
-
*/
|
|
69
|
-
export async function fetchWithIdleTimeout(
|
|
70
|
-
url: string,
|
|
71
|
-
init: RequestInit,
|
|
72
|
-
timeoutMs: number,
|
|
73
|
-
): Promise<{ response: Response; resetIdle: () => void; cancel: () => void }> {
|
|
74
|
-
const ctl = new AbortController();
|
|
75
|
-
const upstream = init.signal;
|
|
76
|
-
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
77
|
-
const armed = { value: true };
|
|
78
|
-
const resetIdle = () => {
|
|
79
|
-
if (!armed.value) return;
|
|
80
|
-
if (timer) clearTimeout(timer);
|
|
81
|
-
timer = setTimeout(() => ctl.abort(), timeoutMs);
|
|
82
|
-
};
|
|
83
|
-
const cancel = () => {
|
|
84
|
-
armed.value = false;
|
|
85
|
-
if (timer) clearTimeout(timer);
|
|
86
|
-
};
|
|
87
|
-
if (upstream) {
|
|
88
|
-
if (upstream.aborted) ctl.abort();
|
|
89
|
-
else upstream.addEventListener('abort', () => ctl.abort(), { once: true });
|
|
90
|
-
}
|
|
91
|
-
resetIdle();
|
|
92
|
-
let response: Response;
|
|
93
|
-
try {
|
|
94
|
-
response = await fetch(url, { ...init, signal: ctl.signal });
|
|
95
|
-
} catch (err) {
|
|
96
|
-
// Cancel the idle timer so a failed fetch doesn't keep the event loop
|
|
97
|
-
// alive for `timeoutMs` after the rejection has propagated.
|
|
98
|
-
cancel();
|
|
99
|
-
throw err;
|
|
100
|
-
}
|
|
101
|
-
resetIdle();
|
|
102
|
-
return { response, resetIdle, cancel };
|
|
103
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'bun:test';
|
|
2
|
-
import type { AgentSourceRegistry, Plugin } from './plugin';
|
|
3
|
-
import { PluginRegistry } from './registry';
|
|
4
|
-
|
|
5
|
-
describe('PluginRegistry context propagation', () => {
|
|
6
|
-
it('setAgentsRegistry from one plugin reaches subsequent plugins', async () => {
|
|
7
|
-
const reg = new PluginRegistry({ cwd: '/tmp', config: {} });
|
|
8
|
-
const calls: Array<{ name: string; sawAgents: boolean }> = [];
|
|
9
|
-
|
|
10
|
-
const publisher: Plugin = {
|
|
11
|
-
name: 'publisher',
|
|
12
|
-
activate(ctx) {
|
|
13
|
-
const sourceReg: AgentSourceRegistry = {
|
|
14
|
-
registerSource: () =>
|
|
15
|
-
function unregister(): void {
|
|
16
|
-
/* noop */
|
|
17
|
-
},
|
|
18
|
-
};
|
|
19
|
-
ctx.setAgentsRegistry?.(sourceReg);
|
|
20
|
-
calls.push({ name: 'publisher', sawAgents: !!ctx.agents });
|
|
21
|
-
},
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const consumer: Plugin = {
|
|
25
|
-
name: 'consumer',
|
|
26
|
-
activate(ctx) {
|
|
27
|
-
calls.push({ name: 'consumer', sawAgents: !!ctx.agents });
|
|
28
|
-
ctx.agents?.registerSource('/some/dir');
|
|
29
|
-
},
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
await reg.register(publisher);
|
|
33
|
-
await reg.register(consumer);
|
|
34
|
-
|
|
35
|
-
expect(calls.find((c) => c.name === 'consumer')?.sawAgents).toBe(true);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('plugins activated before publisher do not see agents', async () => {
|
|
39
|
-
const reg = new PluginRegistry({ cwd: '/tmp', config: {} });
|
|
40
|
-
let earlySaw = false;
|
|
41
|
-
let lateSaw = false;
|
|
42
|
-
const early: Plugin = {
|
|
43
|
-
name: 'early',
|
|
44
|
-
activate(ctx) {
|
|
45
|
-
earlySaw = !!ctx.agents;
|
|
46
|
-
},
|
|
47
|
-
};
|
|
48
|
-
const publisher: Plugin = {
|
|
49
|
-
name: 'pub',
|
|
50
|
-
activate(ctx) {
|
|
51
|
-
ctx.setAgentsRegistry?.({
|
|
52
|
-
registerSource: () =>
|
|
53
|
-
function unregister(): void {
|
|
54
|
-
/* noop */
|
|
55
|
-
},
|
|
56
|
-
});
|
|
57
|
-
},
|
|
58
|
-
};
|
|
59
|
-
const late: Plugin = {
|
|
60
|
-
name: 'late',
|
|
61
|
-
activate(ctx) {
|
|
62
|
-
lateSaw = !!ctx.agents;
|
|
63
|
-
},
|
|
64
|
-
};
|
|
65
|
-
await reg.register(early);
|
|
66
|
-
await reg.register(publisher);
|
|
67
|
-
await reg.register(late);
|
|
68
|
-
expect(earlySaw).toBe(false);
|
|
69
|
-
expect(lateSaw).toBe(true);
|
|
70
|
-
});
|
|
71
|
-
});
|