@jackchen_me/open-multi-agent 0.1.0 → 0.2.0
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/.github/ISSUE_TEMPLATE/bug_report.md +40 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +23 -0
- package/.github/pull_request_template.md +14 -0
- package/.github/workflows/ci.yml +23 -0
- package/CLAUDE.md +72 -0
- package/CODE_OF_CONDUCT.md +48 -0
- package/CONTRIBUTING.md +72 -0
- package/DECISIONS.md +43 -0
- package/README.md +73 -140
- package/README_zh.md +217 -0
- package/SECURITY.md +17 -0
- package/dist/agent/agent.d.ts +5 -0
- package/dist/agent/agent.d.ts.map +1 -1
- package/dist/agent/agent.js +90 -3
- package/dist/agent/agent.js.map +1 -1
- package/dist/agent/structured-output.d.ts +33 -0
- package/dist/agent/structured-output.d.ts.map +1 -0
- package/dist/agent/structured-output.js +116 -0
- package/dist/agent/structured-output.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/llm/adapter.d.ts +9 -4
- package/dist/llm/adapter.d.ts.map +1 -1
- package/dist/llm/adapter.js +17 -5
- package/dist/llm/adapter.js.map +1 -1
- package/dist/llm/anthropic.d.ts +1 -1
- package/dist/llm/anthropic.d.ts.map +1 -1
- package/dist/llm/anthropic.js +2 -1
- package/dist/llm/anthropic.js.map +1 -1
- package/dist/llm/copilot.d.ts +92 -0
- package/dist/llm/copilot.d.ts.map +1 -0
- package/dist/llm/copilot.js +426 -0
- package/dist/llm/copilot.js.map +1 -0
- package/dist/llm/openai-common.d.ts +47 -0
- package/dist/llm/openai-common.d.ts.map +1 -0
- package/dist/llm/openai-common.js +209 -0
- package/dist/llm/openai-common.js.map +1 -0
- package/dist/llm/openai.d.ts +1 -1
- package/dist/llm/openai.d.ts.map +1 -1
- package/dist/llm/openai.js +3 -224
- package/dist/llm/openai.js.map +1 -1
- package/dist/orchestrator/orchestrator.d.ts +25 -1
- package/dist/orchestrator/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator/orchestrator.js +130 -37
- package/dist/orchestrator/orchestrator.js.map +1 -1
- package/dist/task/queue.js +1 -1
- package/dist/task/queue.js.map +1 -1
- package/dist/task/task.d.ts +3 -0
- package/dist/task/task.d.ts.map +1 -1
- package/dist/task/task.js +5 -1
- package/dist/task/task.js.map +1 -1
- package/dist/team/messaging.d.ts.map +1 -1
- package/dist/team/messaging.js +2 -1
- package/dist/team/messaging.js.map +1 -1
- package/dist/types.d.ts +31 -3
- package/dist/types.d.ts.map +1 -1
- package/examples/05-copilot-test.ts +49 -0
- package/examples/06-local-model.ts +199 -0
- package/examples/07-fan-out-aggregate.ts +209 -0
- package/examples/08-gemma4-local.ts +203 -0
- package/examples/09-gemma4-auto-orchestration.ts +162 -0
- package/package.json +4 -3
- package/src/agent/agent.ts +115 -6
- package/src/agent/structured-output.ts +126 -0
- package/src/index.ts +2 -1
- package/src/llm/adapter.ts +18 -5
- package/src/llm/anthropic.ts +2 -1
- package/src/llm/copilot.ts +551 -0
- package/src/llm/openai-common.ts +255 -0
- package/src/llm/openai.ts +8 -258
- package/src/orchestrator/orchestrator.ts +164 -38
- package/src/task/queue.ts +1 -1
- package/src/task/task.ts +8 -1
- package/src/team/messaging.ts +3 -1
- package/src/types.ts +31 -2
- package/tests/semaphore.test.ts +57 -0
- package/tests/shared-memory.test.ts +122 -0
- package/tests/structured-output.test.ts +331 -0
- package/tests/task-queue.test.ts +244 -0
- package/tests/task-retry.test.ts +368 -0
- package/tests/task-utils.test.ts +155 -0
- package/tests/tool-executor.test.ts +193 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { ToolRegistry, defineTool } from '../src/tool/framework.js'
|
|
4
|
+
import { ToolExecutor } from '../src/tool/executor.js'
|
|
5
|
+
import type { ToolUseContext } from '../src/types.js'
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Helpers
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
const dummyContext: ToolUseContext = {
|
|
12
|
+
agent: { name: 'test-agent', role: 'tester', model: 'test-model' },
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function echoTool() {
|
|
16
|
+
return defineTool({
|
|
17
|
+
name: 'echo',
|
|
18
|
+
description: 'Echoes the message.',
|
|
19
|
+
inputSchema: z.object({ message: z.string() }),
|
|
20
|
+
execute: async ({ message }) => ({ data: message, isError: false }),
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function failTool() {
|
|
25
|
+
return defineTool({
|
|
26
|
+
name: 'fail',
|
|
27
|
+
description: 'Always throws.',
|
|
28
|
+
inputSchema: z.object({}),
|
|
29
|
+
execute: async () => {
|
|
30
|
+
throw new Error('intentional failure')
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function makeExecutor(...tools: ReturnType<typeof defineTool>[]) {
|
|
36
|
+
const registry = new ToolRegistry()
|
|
37
|
+
for (const t of tools) registry.register(t)
|
|
38
|
+
return { executor: new ToolExecutor(registry), registry }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Tests
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
describe('ToolExecutor', () => {
|
|
46
|
+
// -------------------------------------------------------------------------
|
|
47
|
+
// Single execution
|
|
48
|
+
// -------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
it('executes a tool and returns its result', async () => {
|
|
51
|
+
const { executor } = makeExecutor(echoTool())
|
|
52
|
+
const result = await executor.execute('echo', { message: 'hello' }, dummyContext)
|
|
53
|
+
expect(result.data).toBe('hello')
|
|
54
|
+
expect(result.isError).toBeFalsy()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('returns an error result for an unknown tool', async () => {
|
|
58
|
+
const { executor } = makeExecutor()
|
|
59
|
+
const result = await executor.execute('ghost', {}, dummyContext)
|
|
60
|
+
expect(result.isError).toBe(true)
|
|
61
|
+
expect(result.data).toContain('not registered')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('returns an error result when Zod validation fails', async () => {
|
|
65
|
+
const { executor } = makeExecutor(echoTool())
|
|
66
|
+
// 'message' is required but missing
|
|
67
|
+
const result = await executor.execute('echo', {}, dummyContext)
|
|
68
|
+
expect(result.isError).toBe(true)
|
|
69
|
+
expect(result.data).toContain('Invalid input')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('catches tool execution errors and returns them as error results', async () => {
|
|
73
|
+
const { executor } = makeExecutor(failTool())
|
|
74
|
+
const result = await executor.execute('fail', {}, dummyContext)
|
|
75
|
+
expect(result.isError).toBe(true)
|
|
76
|
+
expect(result.data).toContain('intentional failure')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('returns an error result when aborted before execution', async () => {
|
|
80
|
+
const { executor } = makeExecutor(echoTool())
|
|
81
|
+
const controller = new AbortController()
|
|
82
|
+
controller.abort()
|
|
83
|
+
|
|
84
|
+
const result = await executor.execute(
|
|
85
|
+
'echo',
|
|
86
|
+
{ message: 'hi' },
|
|
87
|
+
{ ...dummyContext, abortSignal: controller.signal },
|
|
88
|
+
)
|
|
89
|
+
expect(result.isError).toBe(true)
|
|
90
|
+
expect(result.data).toContain('aborted')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// -------------------------------------------------------------------------
|
|
94
|
+
// Batch execution
|
|
95
|
+
// -------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
it('executeBatch runs multiple tools and returns a map of results', async () => {
|
|
98
|
+
const { executor } = makeExecutor(echoTool())
|
|
99
|
+
const results = await executor.executeBatch(
|
|
100
|
+
[
|
|
101
|
+
{ id: 'c1', name: 'echo', input: { message: 'a' } },
|
|
102
|
+
{ id: 'c2', name: 'echo', input: { message: 'b' } },
|
|
103
|
+
],
|
|
104
|
+
dummyContext,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
expect(results.size).toBe(2)
|
|
108
|
+
expect(results.get('c1')!.data).toBe('a')
|
|
109
|
+
expect(results.get('c2')!.data).toBe('b')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('executeBatch isolates errors — one failure does not affect others', async () => {
|
|
113
|
+
const { executor } = makeExecutor(echoTool(), failTool())
|
|
114
|
+
const results = await executor.executeBatch(
|
|
115
|
+
[
|
|
116
|
+
{ id: 'ok', name: 'echo', input: { message: 'fine' } },
|
|
117
|
+
{ id: 'bad', name: 'fail', input: {} },
|
|
118
|
+
],
|
|
119
|
+
dummyContext,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
expect(results.get('ok')!.isError).toBeFalsy()
|
|
123
|
+
expect(results.get('bad')!.isError).toBe(true)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
// -------------------------------------------------------------------------
|
|
127
|
+
// Concurrency control
|
|
128
|
+
// -------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
it('respects maxConcurrency limit', async () => {
|
|
131
|
+
let peak = 0
|
|
132
|
+
let running = 0
|
|
133
|
+
|
|
134
|
+
const trackTool = defineTool({
|
|
135
|
+
name: 'track',
|
|
136
|
+
description: 'Tracks concurrency.',
|
|
137
|
+
inputSchema: z.object({}),
|
|
138
|
+
execute: async () => {
|
|
139
|
+
running++
|
|
140
|
+
peak = Math.max(peak, running)
|
|
141
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
142
|
+
running--
|
|
143
|
+
return { data: 'ok', isError: false }
|
|
144
|
+
},
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
const registry = new ToolRegistry()
|
|
148
|
+
registry.register(trackTool)
|
|
149
|
+
const executor = new ToolExecutor(registry, { maxConcurrency: 2 })
|
|
150
|
+
|
|
151
|
+
await executor.executeBatch(
|
|
152
|
+
Array.from({ length: 5 }, (_, i) => ({ id: `t${i}`, name: 'track', input: {} })),
|
|
153
|
+
dummyContext,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
expect(peak).toBeLessThanOrEqual(2)
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
// ToolRegistry
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
describe('ToolRegistry', () => {
|
|
165
|
+
it('registers and retrieves a tool', () => {
|
|
166
|
+
const registry = new ToolRegistry()
|
|
167
|
+
registry.register(echoTool())
|
|
168
|
+
expect(registry.get('echo')).toBeDefined()
|
|
169
|
+
expect(registry.has('echo')).toBe(true)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('throws on duplicate registration', () => {
|
|
173
|
+
const registry = new ToolRegistry()
|
|
174
|
+
registry.register(echoTool())
|
|
175
|
+
expect(() => registry.register(echoTool())).toThrow('already registered')
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('unregister removes the tool', () => {
|
|
179
|
+
const registry = new ToolRegistry()
|
|
180
|
+
registry.register(echoTool())
|
|
181
|
+
registry.unregister('echo')
|
|
182
|
+
expect(registry.has('echo')).toBe(false)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('toToolDefs produces JSON schema representations', () => {
|
|
186
|
+
const registry = new ToolRegistry()
|
|
187
|
+
registry.register(echoTool())
|
|
188
|
+
const defs = registry.toToolDefs()
|
|
189
|
+
expect(defs).toHaveLength(1)
|
|
190
|
+
expect(defs[0].name).toBe('echo')
|
|
191
|
+
expect(defs[0].inputSchema).toHaveProperty('properties')
|
|
192
|
+
})
|
|
193
|
+
})
|