@jackchen_me/open-multi-agent 1.0.0 → 1.0.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.
Files changed (80) hide show
  1. package/package.json +8 -2
  2. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -40
  3. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -23
  4. package/.github/pull_request_template.md +0 -14
  5. package/.github/workflows/ci.yml +0 -23
  6. package/CLAUDE.md +0 -80
  7. package/CODE_OF_CONDUCT.md +0 -48
  8. package/CONTRIBUTING.md +0 -72
  9. package/DECISIONS.md +0 -43
  10. package/README_zh.md +0 -277
  11. package/SECURITY.md +0 -17
  12. package/examples/01-single-agent.ts +0 -131
  13. package/examples/02-team-collaboration.ts +0 -167
  14. package/examples/03-task-pipeline.ts +0 -201
  15. package/examples/04-multi-model-team.ts +0 -261
  16. package/examples/05-copilot-test.ts +0 -49
  17. package/examples/06-local-model.ts +0 -200
  18. package/examples/07-fan-out-aggregate.ts +0 -209
  19. package/examples/08-gemma4-local.ts +0 -192
  20. package/examples/09-structured-output.ts +0 -73
  21. package/examples/10-task-retry.ts +0 -132
  22. package/examples/11-trace-observability.ts +0 -133
  23. package/examples/12-grok.ts +0 -154
  24. package/examples/13-gemini.ts +0 -48
  25. package/src/agent/agent.ts +0 -622
  26. package/src/agent/loop-detector.ts +0 -137
  27. package/src/agent/pool.ts +0 -285
  28. package/src/agent/runner.ts +0 -542
  29. package/src/agent/structured-output.ts +0 -126
  30. package/src/index.ts +0 -182
  31. package/src/llm/adapter.ts +0 -98
  32. package/src/llm/anthropic.ts +0 -389
  33. package/src/llm/copilot.ts +0 -552
  34. package/src/llm/gemini.ts +0 -378
  35. package/src/llm/grok.ts +0 -29
  36. package/src/llm/openai-common.ts +0 -294
  37. package/src/llm/openai.ts +0 -292
  38. package/src/memory/shared.ts +0 -181
  39. package/src/memory/store.ts +0 -124
  40. package/src/orchestrator/orchestrator.ts +0 -1071
  41. package/src/orchestrator/scheduler.ts +0 -352
  42. package/src/task/queue.ts +0 -464
  43. package/src/task/task.ts +0 -239
  44. package/src/team/messaging.ts +0 -232
  45. package/src/team/team.ts +0 -334
  46. package/src/tool/built-in/bash.ts +0 -187
  47. package/src/tool/built-in/file-edit.ts +0 -154
  48. package/src/tool/built-in/file-read.ts +0 -105
  49. package/src/tool/built-in/file-write.ts +0 -81
  50. package/src/tool/built-in/grep.ts +0 -362
  51. package/src/tool/built-in/index.ts +0 -50
  52. package/src/tool/executor.ts +0 -178
  53. package/src/tool/framework.ts +0 -557
  54. package/src/tool/text-tool-extractor.ts +0 -219
  55. package/src/types.ts +0 -542
  56. package/src/utils/semaphore.ts +0 -89
  57. package/src/utils/trace.ts +0 -34
  58. package/tests/agent-hooks.test.ts +0 -473
  59. package/tests/agent-pool.test.ts +0 -212
  60. package/tests/approval.test.ts +0 -464
  61. package/tests/built-in-tools.test.ts +0 -393
  62. package/tests/gemini-adapter.test.ts +0 -97
  63. package/tests/grok-adapter.test.ts +0 -74
  64. package/tests/llm-adapters.test.ts +0 -357
  65. package/tests/loop-detection.test.ts +0 -456
  66. package/tests/openai-fallback.test.ts +0 -159
  67. package/tests/orchestrator.test.ts +0 -281
  68. package/tests/scheduler.test.ts +0 -221
  69. package/tests/semaphore.test.ts +0 -57
  70. package/tests/shared-memory.test.ts +0 -122
  71. package/tests/structured-output.test.ts +0 -331
  72. package/tests/task-queue.test.ts +0 -244
  73. package/tests/task-retry.test.ts +0 -368
  74. package/tests/task-utils.test.ts +0 -155
  75. package/tests/team-messaging.test.ts +0 -329
  76. package/tests/text-tool-extractor.test.ts +0 -170
  77. package/tests/tool-executor.test.ts +0 -193
  78. package/tests/trace.test.ts +0 -453
  79. package/tsconfig.json +0 -25
  80. package/vitest.config.ts +0 -9
@@ -1,393 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest'
2
- import { mkdtemp, rm, writeFile, readFile } from 'fs/promises'
3
- import { join } from 'path'
4
- import { tmpdir } from 'os'
5
- import { fileReadTool } from '../src/tool/built-in/file-read.js'
6
- import { fileWriteTool } from '../src/tool/built-in/file-write.js'
7
- import { fileEditTool } from '../src/tool/built-in/file-edit.js'
8
- import { bashTool } from '../src/tool/built-in/bash.js'
9
- import { grepTool } from '../src/tool/built-in/grep.js'
10
- import { registerBuiltInTools, BUILT_IN_TOOLS } from '../src/tool/built-in/index.js'
11
- import { ToolRegistry } from '../src/tool/framework.js'
12
- import type { ToolUseContext } from '../src/types.js'
13
-
14
- // ---------------------------------------------------------------------------
15
- // Helpers
16
- // ---------------------------------------------------------------------------
17
-
18
- const defaultContext: ToolUseContext = {
19
- agent: { name: 'test-agent', role: 'tester', model: 'test' },
20
- }
21
-
22
- let tmpDir: string
23
-
24
- beforeEach(async () => {
25
- tmpDir = await mkdtemp(join(tmpdir(), 'oma-test-'))
26
- })
27
-
28
- afterEach(async () => {
29
- await rm(tmpDir, { recursive: true, force: true })
30
- })
31
-
32
- // ===========================================================================
33
- // registerBuiltInTools
34
- // ===========================================================================
35
-
36
- describe('registerBuiltInTools', () => {
37
- it('registers all 5 built-in tools', () => {
38
- const registry = new ToolRegistry()
39
- registerBuiltInTools(registry)
40
-
41
- expect(registry.get('bash')).toBeDefined()
42
- expect(registry.get('file_read')).toBeDefined()
43
- expect(registry.get('file_write')).toBeDefined()
44
- expect(registry.get('file_edit')).toBeDefined()
45
- expect(registry.get('grep')).toBeDefined()
46
- })
47
-
48
- it('BUILT_IN_TOOLS has correct length', () => {
49
- expect(BUILT_IN_TOOLS).toHaveLength(5)
50
- })
51
- })
52
-
53
- // ===========================================================================
54
- // file_read
55
- // ===========================================================================
56
-
57
- describe('file_read', () => {
58
- it('reads a file with line numbers', async () => {
59
- const filePath = join(tmpDir, 'test.txt')
60
- await writeFile(filePath, 'line one\nline two\nline three\n')
61
-
62
- const result = await fileReadTool.execute({ path: filePath }, defaultContext)
63
-
64
- expect(result.isError).toBe(false)
65
- expect(result.data).toContain('1\tline one')
66
- expect(result.data).toContain('2\tline two')
67
- expect(result.data).toContain('3\tline three')
68
- })
69
-
70
- it('reads a slice with offset and limit', async () => {
71
- const filePath = join(tmpDir, 'test.txt')
72
- await writeFile(filePath, 'a\nb\nc\nd\ne\n')
73
-
74
- const result = await fileReadTool.execute(
75
- { path: filePath, offset: 2, limit: 2 },
76
- defaultContext,
77
- )
78
-
79
- expect(result.isError).toBe(false)
80
- expect(result.data).toContain('2\tb')
81
- expect(result.data).toContain('3\tc')
82
- expect(result.data).not.toContain('1\ta')
83
- })
84
-
85
- it('errors on non-existent file', async () => {
86
- const result = await fileReadTool.execute(
87
- { path: join(tmpDir, 'nope.txt') },
88
- defaultContext,
89
- )
90
-
91
- expect(result.isError).toBe(true)
92
- expect(result.data).toContain('Could not read file')
93
- })
94
-
95
- it('errors when offset is beyond end of file', async () => {
96
- const filePath = join(tmpDir, 'short.txt')
97
- await writeFile(filePath, 'one line\n')
98
-
99
- const result = await fileReadTool.execute(
100
- { path: filePath, offset: 100 },
101
- defaultContext,
102
- )
103
-
104
- expect(result.isError).toBe(true)
105
- expect(result.data).toContain('beyond the end')
106
- })
107
-
108
- it('shows truncation note when not reading entire file', async () => {
109
- const filePath = join(tmpDir, 'multi.txt')
110
- await writeFile(filePath, 'a\nb\nc\nd\ne\n')
111
-
112
- const result = await fileReadTool.execute(
113
- { path: filePath, limit: 2 },
114
- defaultContext,
115
- )
116
-
117
- expect(result.data).toContain('showing lines')
118
- })
119
- })
120
-
121
- // ===========================================================================
122
- // file_write
123
- // ===========================================================================
124
-
125
- describe('file_write', () => {
126
- it('creates a new file', async () => {
127
- const filePath = join(tmpDir, 'new-file.txt')
128
-
129
- const result = await fileWriteTool.execute(
130
- { path: filePath, content: 'hello world' },
131
- defaultContext,
132
- )
133
-
134
- expect(result.isError).toBe(false)
135
- expect(result.data).toContain('Created')
136
- const content = await readFile(filePath, 'utf8')
137
- expect(content).toBe('hello world')
138
- })
139
-
140
- it('overwrites an existing file', async () => {
141
- const filePath = join(tmpDir, 'existing.txt')
142
- await writeFile(filePath, 'old content')
143
-
144
- const result = await fileWriteTool.execute(
145
- { path: filePath, content: 'new content' },
146
- defaultContext,
147
- )
148
-
149
- expect(result.isError).toBe(false)
150
- expect(result.data).toContain('Updated')
151
- const content = await readFile(filePath, 'utf8')
152
- expect(content).toBe('new content')
153
- })
154
-
155
- it('creates parent directories', async () => {
156
- const filePath = join(tmpDir, 'deep', 'nested', 'file.txt')
157
-
158
- const result = await fileWriteTool.execute(
159
- { path: filePath, content: 'deep file' },
160
- defaultContext,
161
- )
162
-
163
- expect(result.isError).toBe(false)
164
- const content = await readFile(filePath, 'utf8')
165
- expect(content).toBe('deep file')
166
- })
167
-
168
- it('reports line and byte counts', async () => {
169
- const filePath = join(tmpDir, 'counted.txt')
170
-
171
- const result = await fileWriteTool.execute(
172
- { path: filePath, content: 'line1\nline2\nline3' },
173
- defaultContext,
174
- )
175
-
176
- expect(result.data).toContain('3 lines')
177
- })
178
- })
179
-
180
- // ===========================================================================
181
- // file_edit
182
- // ===========================================================================
183
-
184
- describe('file_edit', () => {
185
- it('replaces a unique string', async () => {
186
- const filePath = join(tmpDir, 'edit.txt')
187
- await writeFile(filePath, 'hello world\ngoodbye world\n')
188
-
189
- const result = await fileEditTool.execute(
190
- { path: filePath, old_string: 'hello', new_string: 'hi' },
191
- defaultContext,
192
- )
193
-
194
- expect(result.isError).toBe(false)
195
- expect(result.data).toContain('Replaced 1 occurrence')
196
- const content = await readFile(filePath, 'utf8')
197
- expect(content).toContain('hi world')
198
- expect(content).toContain('goodbye world')
199
- })
200
-
201
- it('errors when old_string not found', async () => {
202
- const filePath = join(tmpDir, 'edit.txt')
203
- await writeFile(filePath, 'hello world\n')
204
-
205
- const result = await fileEditTool.execute(
206
- { path: filePath, old_string: 'nonexistent', new_string: 'x' },
207
- defaultContext,
208
- )
209
-
210
- expect(result.isError).toBe(true)
211
- expect(result.data).toContain('not found')
212
- })
213
-
214
- it('errors on ambiguous match without replace_all', async () => {
215
- const filePath = join(tmpDir, 'edit.txt')
216
- await writeFile(filePath, 'foo bar foo\n')
217
-
218
- const result = await fileEditTool.execute(
219
- { path: filePath, old_string: 'foo', new_string: 'baz' },
220
- defaultContext,
221
- )
222
-
223
- expect(result.isError).toBe(true)
224
- expect(result.data).toContain('2 times')
225
- })
226
-
227
- it('replaces all when replace_all is true', async () => {
228
- const filePath = join(tmpDir, 'edit.txt')
229
- await writeFile(filePath, 'foo bar foo\n')
230
-
231
- const result = await fileEditTool.execute(
232
- { path: filePath, old_string: 'foo', new_string: 'baz', replace_all: true },
233
- defaultContext,
234
- )
235
-
236
- expect(result.isError).toBe(false)
237
- expect(result.data).toContain('Replaced 2 occurrences')
238
- const content = await readFile(filePath, 'utf8')
239
- expect(content).toBe('baz bar baz\n')
240
- })
241
-
242
- it('errors on non-existent file', async () => {
243
- const result = await fileEditTool.execute(
244
- { path: join(tmpDir, 'nope.txt'), old_string: 'x', new_string: 'y' },
245
- defaultContext,
246
- )
247
-
248
- expect(result.isError).toBe(true)
249
- expect(result.data).toContain('Could not read')
250
- })
251
- })
252
-
253
- // ===========================================================================
254
- // bash
255
- // ===========================================================================
256
-
257
- describe('bash', () => {
258
- it('executes a simple command', async () => {
259
- const result = await bashTool.execute(
260
- { command: 'echo "hello bash"' },
261
- defaultContext,
262
- )
263
-
264
- expect(result.isError).toBe(false)
265
- expect(result.data).toContain('hello bash')
266
- })
267
-
268
- it('captures stderr on failed command', async () => {
269
- const result = await bashTool.execute(
270
- { command: 'ls /nonexistent/path/xyz 2>&1' },
271
- defaultContext,
272
- )
273
-
274
- expect(result.isError).toBe(true)
275
- })
276
-
277
- it('supports custom working directory', async () => {
278
- const result = await bashTool.execute(
279
- { command: 'pwd', cwd: tmpDir },
280
- defaultContext,
281
- )
282
-
283
- expect(result.isError).toBe(false)
284
- expect(result.data).toContain(tmpDir)
285
- })
286
-
287
- it('returns exit code for failing commands', async () => {
288
- const result = await bashTool.execute(
289
- { command: 'exit 42' },
290
- defaultContext,
291
- )
292
-
293
- expect(result.isError).toBe(true)
294
- expect(result.data).toContain('42')
295
- })
296
-
297
- it('handles commands with no output', async () => {
298
- const result = await bashTool.execute(
299
- { command: 'true' },
300
- defaultContext,
301
- )
302
-
303
- expect(result.isError).toBe(false)
304
- expect(result.data).toContain('command completed with no output')
305
- })
306
- })
307
-
308
- // ===========================================================================
309
- // grep (Node.js fallback — tests do not depend on ripgrep availability)
310
- // ===========================================================================
311
-
312
- describe('grep', () => {
313
- it('finds matching lines in a file', async () => {
314
- const filePath = join(tmpDir, 'search.txt')
315
- await writeFile(filePath, 'apple\nbanana\napricot\ncherry\n')
316
-
317
- const result = await grepTool.execute(
318
- { pattern: 'ap', path: filePath },
319
- defaultContext,
320
- )
321
-
322
- expect(result.isError).toBe(false)
323
- expect(result.data).toContain('apple')
324
- expect(result.data).toContain('apricot')
325
- expect(result.data).not.toContain('cherry')
326
- })
327
-
328
- it('returns "No matches found" when nothing matches', async () => {
329
- const filePath = join(tmpDir, 'search.txt')
330
- await writeFile(filePath, 'hello world\n')
331
-
332
- const result = await grepTool.execute(
333
- { pattern: 'zzz', path: filePath },
334
- defaultContext,
335
- )
336
-
337
- expect(result.isError).toBe(false)
338
- expect(result.data).toContain('No matches found')
339
- })
340
-
341
- it('errors on invalid regex', async () => {
342
- const result = await grepTool.execute(
343
- { pattern: '[invalid', path: tmpDir },
344
- defaultContext,
345
- )
346
-
347
- expect(result.isError).toBe(true)
348
- expect(result.data).toContain('Invalid regular expression')
349
- })
350
-
351
- it('searches recursively in a directory', async () => {
352
- const subDir = join(tmpDir, 'sub')
353
- await writeFile(join(tmpDir, 'a.txt'), 'findme here\n')
354
- // Create subdir and file
355
- const { mkdir } = await import('fs/promises')
356
- await mkdir(subDir, { recursive: true })
357
- await writeFile(join(subDir, 'b.txt'), 'findme there\n')
358
-
359
- const result = await grepTool.execute(
360
- { pattern: 'findme', path: tmpDir },
361
- defaultContext,
362
- )
363
-
364
- expect(result.isError).toBe(false)
365
- expect(result.data).toContain('findme here')
366
- expect(result.data).toContain('findme there')
367
- })
368
-
369
- it('respects glob filter', async () => {
370
- await writeFile(join(tmpDir, 'code.ts'), 'const x = 1\n')
371
- await writeFile(join(tmpDir, 'readme.md'), 'const y = 2\n')
372
-
373
- const result = await grepTool.execute(
374
- { pattern: 'const', path: tmpDir, glob: '*.ts' },
375
- defaultContext,
376
- )
377
-
378
- expect(result.isError).toBe(false)
379
- expect(result.data).toContain('code.ts')
380
- expect(result.data).not.toContain('readme.md')
381
- })
382
-
383
- it('errors on inaccessible path', async () => {
384
- const result = await grepTool.execute(
385
- { pattern: 'test', path: '/nonexistent/path/xyz' },
386
- defaultContext,
387
- )
388
-
389
- expect(result.isError).toBe(true)
390
- // May hit ripgrep path or Node fallback — both report an error
391
- expect(result.data.toLowerCase()).toContain('no such file')
392
- })
393
- })
@@ -1,97 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest'
2
-
3
- // ---------------------------------------------------------------------------
4
- // Mock GoogleGenAI constructor (must be hoisted for Vitest)
5
- // ---------------------------------------------------------------------------
6
- const GoogleGenAIMock = vi.hoisted(() => vi.fn())
7
-
8
- vi.mock('@google/genai', () => ({
9
- GoogleGenAI: GoogleGenAIMock,
10
- FunctionCallingConfigMode: { AUTO: 'AUTO' },
11
- }))
12
-
13
- import { GeminiAdapter } from '../src/llm/gemini.js'
14
- import { createAdapter } from '../src/llm/adapter.js'
15
-
16
- // ---------------------------------------------------------------------------
17
- // GeminiAdapter tests
18
- // ---------------------------------------------------------------------------
19
-
20
- describe('GeminiAdapter', () => {
21
- beforeEach(() => {
22
- GoogleGenAIMock.mockClear()
23
- })
24
-
25
- it('has name "gemini"', () => {
26
- const adapter = new GeminiAdapter()
27
- expect(adapter.name).toBe('gemini')
28
- })
29
-
30
- it('uses GEMINI_API_KEY by default', () => {
31
- const originalGemini = process.env['GEMINI_API_KEY']
32
- const originalGoogle = process.env['GOOGLE_API_KEY']
33
- process.env['GEMINI_API_KEY'] = 'gemini-env-key'
34
- delete process.env['GOOGLE_API_KEY']
35
-
36
- try {
37
- new GeminiAdapter()
38
- expect(GoogleGenAIMock).toHaveBeenCalledWith(
39
- expect.objectContaining({
40
- apiKey: 'gemini-env-key',
41
- }),
42
- )
43
- } finally {
44
- if (originalGemini === undefined) {
45
- delete process.env['GEMINI_API_KEY']
46
- } else {
47
- process.env['GEMINI_API_KEY'] = originalGemini
48
- }
49
- if (originalGoogle === undefined) {
50
- delete process.env['GOOGLE_API_KEY']
51
- } else {
52
- process.env['GOOGLE_API_KEY'] = originalGoogle
53
- }
54
- }
55
- })
56
-
57
- it('falls back to GOOGLE_API_KEY when GEMINI_API_KEY is unset', () => {
58
- const originalGemini = process.env['GEMINI_API_KEY']
59
- const originalGoogle = process.env['GOOGLE_API_KEY']
60
- delete process.env['GEMINI_API_KEY']
61
- process.env['GOOGLE_API_KEY'] = 'google-env-key'
62
-
63
- try {
64
- new GeminiAdapter()
65
- expect(GoogleGenAIMock).toHaveBeenCalledWith(
66
- expect.objectContaining({
67
- apiKey: 'google-env-key',
68
- }),
69
- )
70
- } finally {
71
- if (originalGemini === undefined) {
72
- delete process.env['GEMINI_API_KEY']
73
- } else {
74
- process.env['GEMINI_API_KEY'] = originalGemini
75
- }
76
- if (originalGoogle === undefined) {
77
- delete process.env['GOOGLE_API_KEY']
78
- } else {
79
- process.env['GOOGLE_API_KEY'] = originalGoogle
80
- }
81
- }
82
- })
83
-
84
- it('allows overriding apiKey explicitly', () => {
85
- new GeminiAdapter('explicit-key')
86
- expect(GoogleGenAIMock).toHaveBeenCalledWith(
87
- expect.objectContaining({
88
- apiKey: 'explicit-key',
89
- }),
90
- )
91
- })
92
-
93
- it('createAdapter("gemini") returns GeminiAdapter instance', async () => {
94
- const adapter = await createAdapter('gemini')
95
- expect(adapter).toBeInstanceOf(GeminiAdapter)
96
- })
97
- })
@@ -1,74 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest'
2
-
3
- // ---------------------------------------------------------------------------
4
- // Mock OpenAI constructor (must be hoisted for Vitest)
5
- // ---------------------------------------------------------------------------
6
- const OpenAIMock = vi.hoisted(() => vi.fn())
7
-
8
- vi.mock('openai', () => ({
9
- default: OpenAIMock,
10
- }))
11
-
12
- import { GrokAdapter } from '../src/llm/grok.js'
13
- import { createAdapter } from '../src/llm/adapter.js'
14
-
15
- // ---------------------------------------------------------------------------
16
- // GrokAdapter tests
17
- // ---------------------------------------------------------------------------
18
-
19
- describe('GrokAdapter', () => {
20
- beforeEach(() => {
21
- OpenAIMock.mockClear()
22
- })
23
-
24
- it('has name "grok"', () => {
25
- const adapter = new GrokAdapter()
26
- expect(adapter.name).toBe('grok')
27
- })
28
-
29
- it('uses XAI_API_KEY by default', () => {
30
- const original = process.env['XAI_API_KEY']
31
- process.env['XAI_API_KEY'] = 'xai-test-key-123'
32
-
33
- try {
34
- new GrokAdapter()
35
- expect(OpenAIMock).toHaveBeenCalledWith(
36
- expect.objectContaining({
37
- apiKey: 'xai-test-key-123',
38
- baseURL: 'https://api.x.ai/v1',
39
- })
40
- )
41
- } finally {
42
- if (original === undefined) {
43
- delete process.env['XAI_API_KEY']
44
- } else {
45
- process.env['XAI_API_KEY'] = original
46
- }
47
- }
48
- })
49
-
50
- it('uses official xAI baseURL by default', () => {
51
- new GrokAdapter('some-key')
52
- expect(OpenAIMock).toHaveBeenCalledWith(
53
- expect.objectContaining({
54
- apiKey: 'some-key',
55
- baseURL: 'https://api.x.ai/v1',
56
- })
57
- )
58
- })
59
-
60
- it('allows overriding apiKey and baseURL', () => {
61
- new GrokAdapter('custom-key', 'https://custom.endpoint/v1')
62
- expect(OpenAIMock).toHaveBeenCalledWith(
63
- expect.objectContaining({
64
- apiKey: 'custom-key',
65
- baseURL: 'https://custom.endpoint/v1',
66
- })
67
- )
68
- })
69
-
70
- it('createAdapter("grok") returns GrokAdapter instance', async () => {
71
- const adapter = await createAdapter('grok')
72
- expect(adapter).toBeInstanceOf(GrokAdapter)
73
- })
74
- })