@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,137 +0,0 @@
1
- /**
2
- * @fileoverview Sliding-window loop detector for the agent conversation loop.
3
- *
4
- * Tracks tool-call signatures and text outputs across turns to detect when an
5
- * agent is stuck repeating the same actions. Used by {@link AgentRunner} when
6
- * {@link LoopDetectionConfig} is provided.
7
- */
8
-
9
- import type { LoopDetectionConfig, LoopDetectionInfo } from '../types.js'
10
-
11
- // ---------------------------------------------------------------------------
12
- // Helpers
13
- // ---------------------------------------------------------------------------
14
-
15
- /**
16
- * Recursively sort object keys so that `{b:1, a:2}` and `{a:2, b:1}` produce
17
- * the same JSON string.
18
- */
19
- function sortKeys(value: unknown): unknown {
20
- if (value === null || typeof value !== 'object') return value
21
- if (Array.isArray(value)) return value.map(sortKeys)
22
- const sorted: Record<string, unknown> = {}
23
- for (const key of Object.keys(value as Record<string, unknown>).sort()) {
24
- sorted[key] = sortKeys((value as Record<string, unknown>)[key])
25
- }
26
- return sorted
27
- }
28
-
29
- // ---------------------------------------------------------------------------
30
- // LoopDetector
31
- // ---------------------------------------------------------------------------
32
-
33
- export class LoopDetector {
34
- private readonly maxRepeats: number
35
- private readonly windowSize: number
36
-
37
- private readonly toolSignatures: string[] = []
38
- private readonly textOutputs: string[] = []
39
-
40
- constructor(config: LoopDetectionConfig = {}) {
41
- this.maxRepeats = config.maxRepetitions ?? 3
42
- const requestedWindow = config.loopDetectionWindow ?? 4
43
- // Window must be >= threshold, otherwise detection can never trigger.
44
- this.windowSize = Math.max(requestedWindow, this.maxRepeats)
45
- }
46
-
47
- /**
48
- * Record a turn's tool calls. Returns detection info when a loop is found.
49
- */
50
- recordToolCalls(
51
- blocks: ReadonlyArray<{ name: string; input: Record<string, unknown> }>,
52
- ): LoopDetectionInfo | null {
53
- if (blocks.length === 0) return null
54
-
55
- const signature = this.computeToolSignature(blocks)
56
- this.push(this.toolSignatures, signature)
57
-
58
- const count = this.consecutiveRepeats(this.toolSignatures)
59
- if (count >= this.maxRepeats) {
60
- const names = blocks.map(b => b.name).join(', ')
61
- return {
62
- kind: 'tool_repetition',
63
- repetitions: count,
64
- detail:
65
- `Tool call "${names}" with identical arguments has repeated ` +
66
- `${count} times consecutively. The agent appears to be stuck in a loop.`,
67
- }
68
- }
69
- return null
70
- }
71
-
72
- /**
73
- * Record a turn's text output. Returns detection info when a loop is found.
74
- */
75
- recordText(text: string): LoopDetectionInfo | null {
76
- const normalised = text.trim().replace(/\s+/g, ' ')
77
- if (normalised.length === 0) return null
78
-
79
- this.push(this.textOutputs, normalised)
80
-
81
- const count = this.consecutiveRepeats(this.textOutputs)
82
- if (count >= this.maxRepeats) {
83
- return {
84
- kind: 'text_repetition',
85
- repetitions: count,
86
- detail:
87
- `The agent has produced the same text response ${count} times ` +
88
- `consecutively. It appears to be stuck in a loop.`,
89
- }
90
- }
91
- return null
92
- }
93
-
94
- // -------------------------------------------------------------------------
95
- // Private
96
- // -------------------------------------------------------------------------
97
-
98
- /**
99
- * Deterministic JSON signature for a set of tool calls.
100
- * Sorts calls by name (for multi-tool turns) and keys within each input.
101
- */
102
- private computeToolSignature(
103
- blocks: ReadonlyArray<{ name: string; input: Record<string, unknown> }>,
104
- ): string {
105
- const items = blocks
106
- .map(b => ({ name: b.name, input: sortKeys(b.input) }))
107
- .sort((a, b) => {
108
- const cmp = a.name.localeCompare(b.name)
109
- if (cmp !== 0) return cmp
110
- return JSON.stringify(a.input).localeCompare(JSON.stringify(b.input))
111
- })
112
- return JSON.stringify(items)
113
- }
114
-
115
- /** Push an entry and trim the buffer to `windowSize`. */
116
- private push(buffer: string[], entry: string): void {
117
- buffer.push(entry)
118
- while (buffer.length > this.windowSize) {
119
- buffer.shift()
120
- }
121
- }
122
-
123
- /**
124
- * Count how many consecutive identical entries exist at the tail of `buffer`.
125
- * Returns 1 when the last entry is unique.
126
- */
127
- private consecutiveRepeats(buffer: string[]): number {
128
- if (buffer.length === 0) return 0
129
- const last = buffer[buffer.length - 1]
130
- let count = 0
131
- for (let i = buffer.length - 1; i >= 0; i--) {
132
- if (buffer[i] === last) count++
133
- else break
134
- }
135
- return count
136
- }
137
- }
package/src/agent/pool.ts DELETED
@@ -1,285 +0,0 @@
1
- /**
2
- * @fileoverview Agent pool for managing and scheduling multiple agents.
3
- *
4
- * {@link AgentPool} is a registry + scheduler that:
5
- * - Holds any number of named {@link Agent} instances
6
- * - Enforces a concurrency cap across parallel runs via {@link Semaphore}
7
- * - Provides `runParallel` for fan-out and `runAny` for round-robin dispatch
8
- * - Reports aggregate pool health via `getStatus()`
9
- *
10
- * @example
11
- * ```ts
12
- * const pool = new AgentPool(3)
13
- * pool.add(researchAgent)
14
- * pool.add(writerAgent)
15
- *
16
- * const results = await pool.runParallel([
17
- * { agent: 'researcher', prompt: 'Find recent AI papers.' },
18
- * { agent: 'writer', prompt: 'Draft an intro section.' },
19
- * ])
20
- * ```
21
- */
22
-
23
- import type { AgentRunResult } from '../types.js'
24
- import type { RunOptions } from './runner.js'
25
- import type { Agent } from './agent.js'
26
- import { Semaphore } from '../utils/semaphore.js'
27
-
28
- export { Semaphore } from '../utils/semaphore.js'
29
-
30
- // ---------------------------------------------------------------------------
31
- // Pool status snapshot
32
- // ---------------------------------------------------------------------------
33
-
34
- export interface PoolStatus {
35
- /** Total number of agents registered in the pool. */
36
- readonly total: number
37
- /** Agents currently in `idle` state. */
38
- readonly idle: number
39
- /** Agents currently in `running` state. */
40
- readonly running: number
41
- /** Agents currently in `completed` state. */
42
- readonly completed: number
43
- /** Agents currently in `error` state. */
44
- readonly error: number
45
- }
46
-
47
- // ---------------------------------------------------------------------------
48
- // AgentPool
49
- // ---------------------------------------------------------------------------
50
-
51
- /**
52
- * Registry and scheduler for a collection of {@link Agent} instances.
53
- *
54
- * Thread-safety note: Node.js is single-threaded, so the semaphore approach
55
- * is safe — no atomics or mutex primitives are needed. The semaphore gates
56
- * concurrent async operations, not CPU threads.
57
- */
58
- export class AgentPool {
59
- private readonly agents: Map<string, Agent> = new Map()
60
- private readonly semaphore: Semaphore
61
- /** Cursor used by `runAny` for round-robin dispatch. */
62
- private roundRobinIndex = 0
63
-
64
- /**
65
- * @param maxConcurrency - Maximum number of agent runs allowed at the same
66
- * time across the whole pool. Defaults to `5`.
67
- */
68
- constructor(private readonly maxConcurrency: number = 5) {
69
- this.semaphore = new Semaphore(maxConcurrency)
70
- }
71
-
72
- // -------------------------------------------------------------------------
73
- // Registry operations
74
- // -------------------------------------------------------------------------
75
-
76
- /**
77
- * Register an agent with the pool.
78
- *
79
- * @throws {Error} If an agent with the same name is already registered.
80
- */
81
- add(agent: Agent): void {
82
- if (this.agents.has(agent.name)) {
83
- throw new Error(
84
- `AgentPool: agent '${agent.name}' is already registered. ` +
85
- `Call remove('${agent.name}') before re-adding.`,
86
- )
87
- }
88
- this.agents.set(agent.name, agent)
89
- }
90
-
91
- /**
92
- * Unregister an agent by name.
93
- *
94
- * @throws {Error} If the agent is not found.
95
- */
96
- remove(name: string): void {
97
- if (!this.agents.has(name)) {
98
- throw new Error(`AgentPool: agent '${name}' is not registered.`)
99
- }
100
- this.agents.delete(name)
101
- }
102
-
103
- /**
104
- * Retrieve a registered agent by name, or `undefined` if not found.
105
- */
106
- get(name: string): Agent | undefined {
107
- return this.agents.get(name)
108
- }
109
-
110
- /**
111
- * Return all registered agents in insertion order.
112
- */
113
- list(): Agent[] {
114
- return Array.from(this.agents.values())
115
- }
116
-
117
- // -------------------------------------------------------------------------
118
- // Execution API
119
- // -------------------------------------------------------------------------
120
-
121
- /**
122
- * Run a single prompt on the named agent, respecting the pool concurrency
123
- * limit.
124
- *
125
- * @throws {Error} If the agent name is not found.
126
- */
127
- async run(
128
- agentName: string,
129
- prompt: string,
130
- runOptions?: Partial<RunOptions>,
131
- ): Promise<AgentRunResult> {
132
- const agent = this.requireAgent(agentName)
133
-
134
- await this.semaphore.acquire()
135
- try {
136
- return await agent.run(prompt, runOptions)
137
- } finally {
138
- this.semaphore.release()
139
- }
140
- }
141
-
142
- /**
143
- * Run prompts on multiple agents in parallel, subject to the concurrency
144
- * cap set at construction time.
145
- *
146
- * Results are returned as a `Map<agentName, AgentRunResult>`. If two tasks
147
- * target the same agent name, the map will only contain the last result.
148
- * Use unique agent names or run tasks sequentially in that case.
149
- *
150
- * @param tasks - Array of `{ agent, prompt }` descriptors.
151
- */
152
- // TODO(#18): accept RunOptions per task to forward trace context
153
- async runParallel(
154
- tasks: ReadonlyArray<{ readonly agent: string; readonly prompt: string }>,
155
- ): Promise<Map<string, AgentRunResult>> {
156
- const resultMap = new Map<string, AgentRunResult>()
157
-
158
- const settledResults = await Promise.allSettled(
159
- tasks.map(async task => {
160
- const result = await this.run(task.agent, task.prompt)
161
- return { name: task.agent, result }
162
- }),
163
- )
164
-
165
- for (const settled of settledResults) {
166
- if (settled.status === 'fulfilled') {
167
- resultMap.set(settled.value.name, settled.value.result)
168
- } else {
169
- // A rejected run is surfaced as an error AgentRunResult so the caller
170
- // sees it in the map rather than needing to catch Promise.allSettled.
171
- // We cannot know the agent name from the rejection alone — find it via
172
- // the original task list index.
173
- const idx = settledResults.indexOf(settled)
174
- const agentName = tasks[idx]?.agent ?? 'unknown'
175
- resultMap.set(agentName, this.errorResult(settled.reason))
176
- }
177
- }
178
-
179
- return resultMap
180
- }
181
-
182
- /**
183
- * Run a prompt on the "best available" agent using round-robin selection.
184
- *
185
- * Agents are selected in insertion order, cycling back to the start. The
186
- * concurrency limit is still enforced — if the selected agent is busy the
187
- * call will queue via the semaphore.
188
- *
189
- * @throws {Error} If the pool is empty.
190
- */
191
- // TODO(#18): accept RunOptions to forward trace context
192
- async runAny(prompt: string): Promise<AgentRunResult> {
193
- const allAgents = this.list()
194
- if (allAgents.length === 0) {
195
- throw new Error('AgentPool: cannot call runAny on an empty pool.')
196
- }
197
-
198
- // Wrap the index to keep it in bounds even if agents were removed.
199
- this.roundRobinIndex = this.roundRobinIndex % allAgents.length
200
- const agent = allAgents[this.roundRobinIndex]!
201
- this.roundRobinIndex = (this.roundRobinIndex + 1) % allAgents.length
202
-
203
- await this.semaphore.acquire()
204
- try {
205
- return await agent.run(prompt)
206
- } finally {
207
- this.semaphore.release()
208
- }
209
- }
210
-
211
- // -------------------------------------------------------------------------
212
- // Observability
213
- // -------------------------------------------------------------------------
214
-
215
- /**
216
- * Snapshot of how many agents are in each lifecycle state.
217
- */
218
- getStatus(): PoolStatus {
219
- let idle = 0
220
- let running = 0
221
- let completed = 0
222
- let error = 0
223
-
224
- for (const agent of this.agents.values()) {
225
- switch (agent.getState().status) {
226
- case 'idle': idle++; break
227
- case 'running': running++; break
228
- case 'completed': completed++; break
229
- case 'error': error++; break
230
- }
231
- }
232
-
233
- return { total: this.agents.size, idle, running, completed, error }
234
- }
235
-
236
- // -------------------------------------------------------------------------
237
- // Lifecycle
238
- // -------------------------------------------------------------------------
239
-
240
- /**
241
- * Reset all agents in the pool.
242
- *
243
- * Clears their conversation histories and returns them to `idle` state.
244
- * Does not remove agents from the pool.
245
- *
246
- * Async for forward compatibility — shutdown may need to perform async
247
- * cleanup (e.g. draining in-flight requests) in future versions.
248
- */
249
- async shutdown(): Promise<void> {
250
- for (const agent of this.agents.values()) {
251
- agent.reset()
252
- }
253
- }
254
-
255
- // -------------------------------------------------------------------------
256
- // Private helpers
257
- // -------------------------------------------------------------------------
258
-
259
- private requireAgent(name: string): Agent {
260
- const agent = this.agents.get(name)
261
- if (agent === undefined) {
262
- throw new Error(
263
- `AgentPool: agent '${name}' is not registered. ` +
264
- `Registered agents: [${Array.from(this.agents.keys()).join(', ')}]`,
265
- )
266
- }
267
- return agent
268
- }
269
-
270
- /**
271
- * Build a failure {@link AgentRunResult} from a caught rejection reason.
272
- * This keeps `runParallel` returning a complete map even when individual
273
- * agents fail.
274
- */
275
- private errorResult(reason: unknown): AgentRunResult {
276
- const message = reason instanceof Error ? reason.message : String(reason)
277
- return {
278
- success: false,
279
- output: message,
280
- messages: [],
281
- tokenUsage: { input_tokens: 0, output_tokens: 0 },
282
- toolCalls: [],
283
- }
284
- }
285
- }