@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,368 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest'
2
- import { createTask } from '../src/task/task.js'
3
- import { executeWithRetry, computeRetryDelay } from '../src/orchestrator/orchestrator.js'
4
- import type { AgentRunResult } from '../src/types.js'
5
-
6
- // ---------------------------------------------------------------------------
7
- // Helpers
8
- // ---------------------------------------------------------------------------
9
-
10
- const SUCCESS_RESULT: AgentRunResult = {
11
- success: true,
12
- output: 'done',
13
- messages: [],
14
- tokenUsage: { input_tokens: 10, output_tokens: 20 },
15
- toolCalls: [],
16
- }
17
-
18
- const FAILURE_RESULT: AgentRunResult = {
19
- success: false,
20
- output: 'agent failed',
21
- messages: [],
22
- tokenUsage: { input_tokens: 10, output_tokens: 20 },
23
- toolCalls: [],
24
- }
25
-
26
- /** No-op delay for tests. */
27
- const noDelay = () => Promise.resolve()
28
-
29
- // ---------------------------------------------------------------------------
30
- // computeRetryDelay
31
- // ---------------------------------------------------------------------------
32
-
33
- describe('computeRetryDelay', () => {
34
- it('computes exponential backoff', () => {
35
- expect(computeRetryDelay(1000, 2, 1)).toBe(1000) // 1000 * 2^0
36
- expect(computeRetryDelay(1000, 2, 2)).toBe(2000) // 1000 * 2^1
37
- expect(computeRetryDelay(1000, 2, 3)).toBe(4000) // 1000 * 2^2
38
- })
39
-
40
- it('caps at 30 seconds', () => {
41
- // 1000 * 2^20 = 1,048,576,000 — way over cap
42
- expect(computeRetryDelay(1000, 2, 21)).toBe(30_000)
43
- })
44
-
45
- it('handles backoff of 1 (constant delay)', () => {
46
- expect(computeRetryDelay(500, 1, 1)).toBe(500)
47
- expect(computeRetryDelay(500, 1, 5)).toBe(500)
48
- })
49
- })
50
-
51
- // ---------------------------------------------------------------------------
52
- // createTask: retry fields
53
- // ---------------------------------------------------------------------------
54
-
55
- describe('createTask with retry fields', () => {
56
- it('passes through retry config', () => {
57
- const t = createTask({
58
- title: 'Retry task',
59
- description: 'test',
60
- maxRetries: 3,
61
- retryDelayMs: 500,
62
- retryBackoff: 1.5,
63
- })
64
- expect(t.maxRetries).toBe(3)
65
- expect(t.retryDelayMs).toBe(500)
66
- expect(t.retryBackoff).toBe(1.5)
67
- })
68
-
69
- it('defaults retry fields to undefined', () => {
70
- const t = createTask({ title: 'No retry', description: 'test' })
71
- expect(t.maxRetries).toBeUndefined()
72
- expect(t.retryDelayMs).toBeUndefined()
73
- expect(t.retryBackoff).toBeUndefined()
74
- })
75
- })
76
-
77
- // ---------------------------------------------------------------------------
78
- // executeWithRetry — tests the real exported function
79
- // ---------------------------------------------------------------------------
80
-
81
- describe('executeWithRetry', () => {
82
- it('succeeds on first attempt with no retry config', async () => {
83
- const run = vi.fn().mockResolvedValue(SUCCESS_RESULT)
84
- const task = createTask({ title: 'Simple', description: 'test' })
85
-
86
- const result = await executeWithRetry(run, task, undefined, noDelay)
87
-
88
- expect(result.success).toBe(true)
89
- expect(result.output).toBe('done')
90
- expect(run).toHaveBeenCalledTimes(1)
91
- })
92
-
93
- it('succeeds on first attempt even when maxRetries > 0', async () => {
94
- const run = vi.fn().mockResolvedValue(SUCCESS_RESULT)
95
- const task = createTask({
96
- title: 'Has retries',
97
- description: 'test',
98
- maxRetries: 3,
99
- })
100
-
101
- const result = await executeWithRetry(run, task, undefined, noDelay)
102
-
103
- expect(result.success).toBe(true)
104
- expect(run).toHaveBeenCalledTimes(1)
105
- })
106
-
107
- it('retries on exception and succeeds on second attempt', async () => {
108
- const run = vi.fn()
109
- .mockRejectedValueOnce(new Error('transient error'))
110
- .mockResolvedValueOnce(SUCCESS_RESULT)
111
-
112
- const task = createTask({
113
- title: 'Retry task',
114
- description: 'test',
115
- maxRetries: 2,
116
- retryDelayMs: 100,
117
- retryBackoff: 2,
118
- })
119
-
120
- const retryEvents: unknown[] = []
121
- const result = await executeWithRetry(
122
- run,
123
- task,
124
- (data) => retryEvents.push(data),
125
- noDelay,
126
- )
127
-
128
- expect(result.success).toBe(true)
129
- expect(run).toHaveBeenCalledTimes(2)
130
- expect(retryEvents).toHaveLength(1)
131
- expect(retryEvents[0]).toEqual({
132
- attempt: 1,
133
- maxAttempts: 3,
134
- error: 'transient error',
135
- nextDelayMs: 100, // 100 * 2^0
136
- })
137
- })
138
-
139
- it('retries on success:false and succeeds on second attempt', async () => {
140
- const run = vi.fn()
141
- .mockResolvedValueOnce(FAILURE_RESULT)
142
- .mockResolvedValueOnce(SUCCESS_RESULT)
143
-
144
- const task = createTask({
145
- title: 'Retry task',
146
- description: 'test',
147
- maxRetries: 1,
148
- retryDelayMs: 50,
149
- })
150
-
151
- const result = await executeWithRetry(run, task, undefined, noDelay)
152
-
153
- expect(result.success).toBe(true)
154
- expect(run).toHaveBeenCalledTimes(2)
155
- })
156
-
157
- it('exhausts all retries on persistent exception', async () => {
158
- const run = vi.fn().mockRejectedValue(new Error('persistent error'))
159
-
160
- const task = createTask({
161
- title: 'Always fails',
162
- description: 'test',
163
- maxRetries: 2,
164
- retryDelayMs: 10,
165
- retryBackoff: 1,
166
- })
167
-
168
- const retryEvents: unknown[] = []
169
- const result = await executeWithRetry(
170
- run,
171
- task,
172
- (data) => retryEvents.push(data),
173
- noDelay,
174
- )
175
-
176
- expect(result.success).toBe(false)
177
- expect(result.output).toBe('persistent error')
178
- expect(run).toHaveBeenCalledTimes(3) // 1 initial + 2 retries
179
- expect(retryEvents).toHaveLength(2)
180
- })
181
-
182
- it('exhausts all retries on persistent success:false', async () => {
183
- const run = vi.fn().mockResolvedValue(FAILURE_RESULT)
184
-
185
- const task = createTask({
186
- title: 'Always fails',
187
- description: 'test',
188
- maxRetries: 1,
189
- })
190
-
191
- const result = await executeWithRetry(run, task, undefined, noDelay)
192
-
193
- expect(result.success).toBe(false)
194
- expect(result.output).toBe('agent failed')
195
- expect(run).toHaveBeenCalledTimes(2)
196
- })
197
-
198
- it('emits correct exponential backoff delays', async () => {
199
- const run = vi.fn().mockRejectedValue(new Error('error'))
200
-
201
- const task = createTask({
202
- title: 'Backoff test',
203
- description: 'test',
204
- maxRetries: 3,
205
- retryDelayMs: 100,
206
- retryBackoff: 2,
207
- })
208
-
209
- const retryEvents: Array<{ nextDelayMs: number }> = []
210
- await executeWithRetry(
211
- run,
212
- task,
213
- (data) => retryEvents.push(data),
214
- noDelay,
215
- )
216
-
217
- expect(retryEvents).toHaveLength(3)
218
- expect(retryEvents[0]!.nextDelayMs).toBe(100) // 100 * 2^0
219
- expect(retryEvents[1]!.nextDelayMs).toBe(200) // 100 * 2^1
220
- expect(retryEvents[2]!.nextDelayMs).toBe(400) // 100 * 2^2
221
- })
222
-
223
- it('no retry events when maxRetries is 0 (default)', async () => {
224
- const run = vi.fn().mockRejectedValue(new Error('fail'))
225
- const task = createTask({ title: 'No retry', description: 'test' })
226
-
227
- const retryEvents: unknown[] = []
228
- const result = await executeWithRetry(
229
- run,
230
- task,
231
- (data) => retryEvents.push(data),
232
- noDelay,
233
- )
234
-
235
- expect(result.success).toBe(false)
236
- expect(run).toHaveBeenCalledTimes(1)
237
- expect(retryEvents).toHaveLength(0)
238
- })
239
-
240
- it('calls the delay function with computed delay', async () => {
241
- const run = vi.fn()
242
- .mockRejectedValueOnce(new Error('error'))
243
- .mockResolvedValueOnce(SUCCESS_RESULT)
244
-
245
- const task = createTask({
246
- title: 'Delay test',
247
- description: 'test',
248
- maxRetries: 1,
249
- retryDelayMs: 250,
250
- retryBackoff: 3,
251
- })
252
-
253
- const mockDelay = vi.fn().mockResolvedValue(undefined)
254
- await executeWithRetry(run, task, undefined, mockDelay)
255
-
256
- expect(mockDelay).toHaveBeenCalledTimes(1)
257
- expect(mockDelay).toHaveBeenCalledWith(250) // 250 * 3^0
258
- })
259
-
260
- it('caps delay at 30 seconds', async () => {
261
- const run = vi.fn()
262
- .mockRejectedValueOnce(new Error('error'))
263
- .mockResolvedValueOnce(SUCCESS_RESULT)
264
-
265
- const task = createTask({
266
- title: 'Cap test',
267
- description: 'test',
268
- maxRetries: 1,
269
- retryDelayMs: 50_000,
270
- retryBackoff: 2,
271
- })
272
-
273
- const mockDelay = vi.fn().mockResolvedValue(undefined)
274
- await executeWithRetry(run, task, undefined, mockDelay)
275
-
276
- expect(mockDelay).toHaveBeenCalledWith(30_000) // capped
277
- })
278
-
279
- it('accumulates token usage across retry attempts', async () => {
280
- const failResult: AgentRunResult = {
281
- ...FAILURE_RESULT,
282
- tokenUsage: { input_tokens: 100, output_tokens: 50 },
283
- }
284
- const successResult: AgentRunResult = {
285
- ...SUCCESS_RESULT,
286
- tokenUsage: { input_tokens: 200, output_tokens: 80 },
287
- }
288
-
289
- const run = vi.fn()
290
- .mockResolvedValueOnce(failResult)
291
- .mockResolvedValueOnce(failResult)
292
- .mockResolvedValueOnce(successResult)
293
-
294
- const task = createTask({
295
- title: 'Token test',
296
- description: 'test',
297
- maxRetries: 2,
298
- retryDelayMs: 10,
299
- })
300
-
301
- const result = await executeWithRetry(run, task, undefined, noDelay)
302
-
303
- expect(result.success).toBe(true)
304
- // 100+100+200 input, 50+50+80 output
305
- expect(result.tokenUsage.input_tokens).toBe(400)
306
- expect(result.tokenUsage.output_tokens).toBe(180)
307
- })
308
-
309
- it('accumulates token usage even when all retries fail', async () => {
310
- const failResult: AgentRunResult = {
311
- ...FAILURE_RESULT,
312
- tokenUsage: { input_tokens: 50, output_tokens: 30 },
313
- }
314
-
315
- const run = vi.fn().mockResolvedValue(failResult)
316
-
317
- const task = createTask({
318
- title: 'Token fail test',
319
- description: 'test',
320
- maxRetries: 1,
321
- })
322
-
323
- const result = await executeWithRetry(run, task, undefined, noDelay)
324
-
325
- expect(result.success).toBe(false)
326
- // 50+50 input, 30+30 output (2 attempts)
327
- expect(result.tokenUsage.input_tokens).toBe(100)
328
- expect(result.tokenUsage.output_tokens).toBe(60)
329
- })
330
-
331
- it('clamps negative maxRetries to 0 (single attempt)', async () => {
332
- const run = vi.fn().mockRejectedValue(new Error('fail'))
333
-
334
- const task = createTask({
335
- title: 'Negative retry',
336
- description: 'test',
337
- maxRetries: -5,
338
- })
339
- // Manually set negative value since createTask doesn't validate
340
- ;(task as any).maxRetries = -5
341
-
342
- const result = await executeWithRetry(run, task, undefined, noDelay)
343
-
344
- expect(result.success).toBe(false)
345
- expect(run).toHaveBeenCalledTimes(1) // exactly 1 attempt, no retries
346
- })
347
-
348
- it('clamps backoff below 1 to 1 (constant delay)', async () => {
349
- const run = vi.fn()
350
- .mockRejectedValueOnce(new Error('error'))
351
- .mockResolvedValueOnce(SUCCESS_RESULT)
352
-
353
- const task = createTask({
354
- title: 'Bad backoff',
355
- description: 'test',
356
- maxRetries: 1,
357
- retryDelayMs: 100,
358
- retryBackoff: -2,
359
- })
360
- ;(task as any).retryBackoff = -2
361
-
362
- const mockDelay = vi.fn().mockResolvedValue(undefined)
363
- await executeWithRetry(run, task, undefined, mockDelay)
364
-
365
- // backoff clamped to 1, so delay = 100 * 1^0 = 100
366
- expect(mockDelay).toHaveBeenCalledWith(100)
367
- })
368
- })
@@ -1,155 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import {
3
- createTask,
4
- isTaskReady,
5
- getTaskDependencyOrder,
6
- validateTaskDependencies,
7
- } from '../src/task/task.js'
8
- import type { Task } from '../src/types.js'
9
-
10
- // ---------------------------------------------------------------------------
11
- // Helpers
12
- // ---------------------------------------------------------------------------
13
-
14
- function task(id: string, opts: { dependsOn?: string[]; status?: Task['status'] } = {}): Task {
15
- const t = createTask({ title: id, description: `task ${id}` })
16
- return { ...t, id, dependsOn: opts.dependsOn, status: opts.status ?? 'pending' }
17
- }
18
-
19
- // ---------------------------------------------------------------------------
20
- // createTask
21
- // ---------------------------------------------------------------------------
22
-
23
- describe('createTask', () => {
24
- it('creates a task with pending status and timestamps', () => {
25
- const t = createTask({ title: 'Test', description: 'A test task' })
26
- expect(t.id).toBeDefined()
27
- expect(t.status).toBe('pending')
28
- expect(t.createdAt).toBeInstanceOf(Date)
29
- expect(t.updatedAt).toBeInstanceOf(Date)
30
- })
31
-
32
- it('copies dependsOn array (no shared reference)', () => {
33
- const deps = ['a']
34
- const t = createTask({ title: 'T', description: 'D', dependsOn: deps })
35
- deps.push('b')
36
- expect(t.dependsOn).toEqual(['a'])
37
- })
38
- })
39
-
40
- // ---------------------------------------------------------------------------
41
- // isTaskReady
42
- // ---------------------------------------------------------------------------
43
-
44
- describe('isTaskReady', () => {
45
- it('returns true for a pending task with no dependencies', () => {
46
- const t = task('a')
47
- expect(isTaskReady(t, [t])).toBe(true)
48
- })
49
-
50
- it('returns false for a non-pending task', () => {
51
- const t = task('a', { status: 'blocked' })
52
- expect(isTaskReady(t, [t])).toBe(false)
53
- })
54
-
55
- it('returns true when all dependencies are completed', () => {
56
- const dep = task('dep', { status: 'completed' })
57
- const t = task('a', { dependsOn: ['dep'] })
58
- expect(isTaskReady(t, [dep, t])).toBe(true)
59
- })
60
-
61
- it('returns false when a dependency is not yet completed', () => {
62
- const dep = task('dep', { status: 'in_progress' })
63
- const t = task('a', { dependsOn: ['dep'] })
64
- expect(isTaskReady(t, [dep, t])).toBe(false)
65
- })
66
-
67
- it('returns false when a dependency is missing from the task set', () => {
68
- const t = task('a', { dependsOn: ['ghost'] })
69
- expect(isTaskReady(t, [t])).toBe(false)
70
- })
71
- })
72
-
73
- // ---------------------------------------------------------------------------
74
- // getTaskDependencyOrder
75
- // ---------------------------------------------------------------------------
76
-
77
- describe('getTaskDependencyOrder', () => {
78
- it('returns empty array for empty input', () => {
79
- expect(getTaskDependencyOrder([])).toEqual([])
80
- })
81
-
82
- it('returns tasks with no deps first', () => {
83
- const a = task('a')
84
- const b = task('b', { dependsOn: ['a'] })
85
- const ordered = getTaskDependencyOrder([b, a])
86
- expect(ordered[0].id).toBe('a')
87
- expect(ordered[1].id).toBe('b')
88
- })
89
-
90
- it('handles a diamond dependency (a → b,c → d)', () => {
91
- const a = task('a')
92
- const b = task('b', { dependsOn: ['a'] })
93
- const c = task('c', { dependsOn: ['a'] })
94
- const d = task('d', { dependsOn: ['b', 'c'] })
95
-
96
- const ordered = getTaskDependencyOrder([d, c, b, a])
97
- const ids = ordered.map((t) => t.id)
98
-
99
- // a must come before b and c; b and c must come before d
100
- expect(ids.indexOf('a')).toBeLessThan(ids.indexOf('b'))
101
- expect(ids.indexOf('a')).toBeLessThan(ids.indexOf('c'))
102
- expect(ids.indexOf('b')).toBeLessThan(ids.indexOf('d'))
103
- expect(ids.indexOf('c')).toBeLessThan(ids.indexOf('d'))
104
- })
105
-
106
- it('returns partial result when a cycle exists', () => {
107
- const a = task('a', { dependsOn: ['b'] })
108
- const b = task('b', { dependsOn: ['a'] })
109
- const ordered = getTaskDependencyOrder([a, b])
110
- // Neither can be ordered — result should be empty (or partial)
111
- expect(ordered.length).toBeLessThan(2)
112
- })
113
- })
114
-
115
- // ---------------------------------------------------------------------------
116
- // validateTaskDependencies
117
- // ---------------------------------------------------------------------------
118
-
119
- describe('validateTaskDependencies', () => {
120
- it('returns valid for tasks with no deps', () => {
121
- const result = validateTaskDependencies([task('a'), task('b')])
122
- expect(result.valid).toBe(true)
123
- expect(result.errors).toHaveLength(0)
124
- })
125
-
126
- it('detects self-dependency', () => {
127
- const t = task('a', { dependsOn: ['a'] })
128
- const result = validateTaskDependencies([t])
129
- expect(result.valid).toBe(false)
130
- expect(result.errors[0]).toContain('depends on itself')
131
- })
132
-
133
- it('detects unknown dependency', () => {
134
- const t = task('a', { dependsOn: ['ghost'] })
135
- const result = validateTaskDependencies([t])
136
- expect(result.valid).toBe(false)
137
- expect(result.errors[0]).toContain('unknown dependency')
138
- })
139
-
140
- it('detects a cycle (a → b → a)', () => {
141
- const a = task('a', { dependsOn: ['b'] })
142
- const b = task('b', { dependsOn: ['a'] })
143
- const result = validateTaskDependencies([a, b])
144
- expect(result.valid).toBe(false)
145
- expect(result.errors.some((e) => e.toLowerCase().includes('cyclic'))).toBe(true)
146
- })
147
-
148
- it('detects a longer cycle (a → b → c → a)', () => {
149
- const a = task('a', { dependsOn: ['c'] })
150
- const b = task('b', { dependsOn: ['a'] })
151
- const c = task('c', { dependsOn: ['b'] })
152
- const result = validateTaskDependencies([a, b, c])
153
- expect(result.valid).toBe(false)
154
- })
155
- })