@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,352 +0,0 @@
1
- /**
2
- * @fileoverview Task scheduling strategies for the open-multi-agent orchestrator.
3
- *
4
- * The {@link Scheduler} class encapsulates four distinct strategies for
5
- * mapping a set of pending {@link Task}s onto a pool of available agents:
6
- *
7
- * - `round-robin` — Distribute tasks evenly across agents by index.
8
- * - `least-busy` — Assign to whichever agent has the fewest active tasks.
9
- * - `capability-match` — Score agents by keyword overlap with the task description.
10
- * - `dependency-first` — Prioritise tasks on the critical path (most blocked dependents).
11
- *
12
- * The scheduler is stateless between calls. All mutable task state lives in the
13
- * {@link TaskQueue} that is passed to {@link Scheduler.autoAssign}.
14
- */
15
-
16
- import type { AgentConfig, Task } from '../types.js'
17
- import type { TaskQueue } from '../task/queue.js'
18
-
19
- // ---------------------------------------------------------------------------
20
- // Public types
21
- // ---------------------------------------------------------------------------
22
-
23
- /**
24
- * The four scheduling strategies available to the {@link Scheduler}.
25
- *
26
- * - `round-robin` — Equal distribution by agent index.
27
- * - `least-busy` — Prefers the agent with the fewest `in_progress` tasks.
28
- * - `capability-match` — Keyword-based affinity between task text and agent role.
29
- * - `dependency-first` — Prioritise tasks that unblock the most other tasks.
30
- */
31
- export type SchedulingStrategy =
32
- | 'round-robin'
33
- | 'least-busy'
34
- | 'capability-match'
35
- | 'dependency-first'
36
-
37
- // ---------------------------------------------------------------------------
38
- // Internal helpers
39
- // ---------------------------------------------------------------------------
40
-
41
- /**
42
- * Count how many tasks in `allTasks` are (transitively) blocked waiting for
43
- * `taskId` to complete. Used by the `dependency-first` strategy to compute
44
- * the "criticality" of each pending task.
45
- *
46
- * The algorithm is a forward BFS over the dependency graph: for each task
47
- * whose `dependsOn` includes `taskId`, we add it to the result set and
48
- * recurse — without revisiting nodes.
49
- */
50
- function countBlockedDependents(taskId: string, allTasks: Task[]): number {
51
- const idToTask = new Map<string, Task>(allTasks.map((t) => [t.id, t]))
52
- // Build reverse adjacency: dependencyId -> tasks that depend on it
53
- const dependents = new Map<string, string[]>()
54
- for (const t of allTasks) {
55
- for (const depId of t.dependsOn ?? []) {
56
- const list = dependents.get(depId) ?? []
57
- list.push(t.id)
58
- dependents.set(depId, list)
59
- }
60
- }
61
-
62
- const visited = new Set<string>()
63
- const queue: string[] = [taskId]
64
- while (queue.length > 0) {
65
- const current = queue.shift()!
66
- for (const depId of dependents.get(current) ?? []) {
67
- if (!visited.has(depId) && idToTask.has(depId)) {
68
- visited.add(depId)
69
- queue.push(depId)
70
- }
71
- }
72
- }
73
- // Exclude the seed task itself from the count
74
- return visited.size
75
- }
76
-
77
- /**
78
- * Compute a simple keyword-overlap score between `text` and `keywords`.
79
- *
80
- * Both the text and keywords are normalised to lower-case before comparison.
81
- * Each keyword that appears in the text contributes +1 to the score.
82
- */
83
- function keywordScore(text: string, keywords: string[]): number {
84
- const lower = text.toLowerCase()
85
- return keywords.reduce((acc, kw) => acc + (lower.includes(kw.toLowerCase()) ? 1 : 0), 0)
86
- }
87
-
88
- /**
89
- * Extract a list of meaningful keywords from a string for capability matching.
90
- *
91
- * Strips common stop-words so that incidental matches (e.g. "the", "and") do
92
- * not inflate scores. Returns unique words longer than three characters.
93
- */
94
- function extractKeywords(text: string): string[] {
95
- const STOP_WORDS = new Set([
96
- 'the', 'and', 'for', 'that', 'this', 'with', 'are', 'from', 'have',
97
- 'will', 'your', 'you', 'can', 'all', 'each', 'when', 'then', 'they',
98
- 'them', 'their', 'about', 'into', 'more', 'also', 'should', 'must',
99
- ])
100
-
101
- return [...new Set(
102
- text
103
- .toLowerCase()
104
- .split(/\W+/)
105
- .filter((w) => w.length > 3 && !STOP_WORDS.has(w)),
106
- )]
107
- }
108
-
109
- // ---------------------------------------------------------------------------
110
- // Scheduler
111
- // ---------------------------------------------------------------------------
112
-
113
- /**
114
- * Maps pending tasks to available agents using one of four configurable strategies.
115
- *
116
- * @example
117
- * ```ts
118
- * const scheduler = new Scheduler('capability-match')
119
- *
120
- * // Get a full assignment map from tasks to agent names
121
- * const assignments = scheduler.schedule(pendingTasks, teamAgents)
122
- *
123
- * // Or let the scheduler directly update a TaskQueue
124
- * scheduler.autoAssign(queue, teamAgents)
125
- * ```
126
- */
127
- export class Scheduler {
128
- /** Rolling cursor used by `round-robin` to distribute tasks sequentially. */
129
- private roundRobinCursor = 0
130
-
131
- /**
132
- * @param strategy - The scheduling algorithm to apply. Defaults to
133
- * `'dependency-first'` which is the safest default for
134
- * complex multi-step pipelines.
135
- */
136
- constructor(private readonly strategy: SchedulingStrategy = 'dependency-first') {}
137
-
138
- // -------------------------------------------------------------------------
139
- // Primary API
140
- // -------------------------------------------------------------------------
141
-
142
- /**
143
- * Given a list of pending `tasks` and `agents`, return a mapping from
144
- * `taskId` to `agentName` representing the recommended assignment.
145
- *
146
- * Only tasks without an existing `assignee` are considered. Tasks that are
147
- * already assigned are preserved unchanged.
148
- *
149
- * The method is deterministic for all strategies except `round-robin`, which
150
- * advances an internal cursor and therefore produces different results across
151
- * successive calls with the same inputs.
152
- *
153
- * @param tasks - Snapshot of all tasks in the current run (any status).
154
- * @param agents - Available agent configurations.
155
- * @returns A `Map<taskId, agentName>` for every unassigned pending task.
156
- */
157
- schedule(tasks: Task[], agents: AgentConfig[]): Map<string, string> {
158
- if (agents.length === 0) return new Map()
159
-
160
- const unassigned = tasks.filter(
161
- (t) => t.status === 'pending' && !t.assignee,
162
- )
163
-
164
- switch (this.strategy) {
165
- case 'round-robin':
166
- return this.scheduleRoundRobin(unassigned, agents)
167
- case 'least-busy':
168
- return this.scheduleLeastBusy(unassigned, agents, tasks)
169
- case 'capability-match':
170
- return this.scheduleCapabilityMatch(unassigned, agents)
171
- case 'dependency-first':
172
- return this.scheduleDependencyFirst(unassigned, agents, tasks)
173
- }
174
- }
175
-
176
- /**
177
- * Convenience method that applies assignments returned by {@link schedule}
178
- * directly to a live `TaskQueue`.
179
- *
180
- * Iterates all pending, unassigned tasks in the queue and sets `assignee` for
181
- * each according to the current strategy. Skips tasks that are already
182
- * assigned, non-pending, or whose IDs are not found in the queue snapshot.
183
- *
184
- * @param queue - The live task queue to mutate.
185
- * @param agents - Available agent configurations.
186
- */
187
- autoAssign(queue: TaskQueue, agents: AgentConfig[]): void {
188
- const allTasks = queue.list()
189
- const assignments = this.schedule(allTasks, agents)
190
-
191
- for (const [taskId, agentName] of assignments) {
192
- try {
193
- queue.update(taskId, { assignee: agentName })
194
- } catch {
195
- // Task may have been completed/failed between snapshot and now — skip.
196
- }
197
- }
198
- }
199
-
200
- // -------------------------------------------------------------------------
201
- // Strategy implementations
202
- // -------------------------------------------------------------------------
203
-
204
- /**
205
- * Round-robin: assign tasks to agents in order, cycling back to the start.
206
- *
207
- * The cursor advances with every call so that repeated calls with the same
208
- * task set continue distributing work — rather than always starting from
209
- * agent[0].
210
- */
211
- private scheduleRoundRobin(
212
- unassigned: Task[],
213
- agents: AgentConfig[],
214
- ): Map<string, string> {
215
- const result = new Map<string, string>()
216
- for (const task of unassigned) {
217
- const agent = agents[this.roundRobinCursor % agents.length]!
218
- result.set(task.id, agent.name)
219
- this.roundRobinCursor = (this.roundRobinCursor + 1) % agents.length
220
- }
221
- return result
222
- }
223
-
224
- /**
225
- * Least-busy: assign each task to the agent with the fewest `in_progress`
226
- * tasks at the time the schedule is computed.
227
- *
228
- * Agent load is derived from the `in_progress` count in `allTasks`. Ties are
229
- * broken by the agent's position in the `agents` array (earlier = preferred).
230
- */
231
- private scheduleLeastBusy(
232
- unassigned: Task[],
233
- agents: AgentConfig[],
234
- allTasks: Task[],
235
- ): Map<string, string> {
236
- // Build initial in-progress count per agent.
237
- const load = new Map<string, number>(agents.map((a) => [a.name, 0]))
238
- for (const task of allTasks) {
239
- if (task.status === 'in_progress' && task.assignee) {
240
- const current = load.get(task.assignee) ?? 0
241
- load.set(task.assignee, current + 1)
242
- }
243
- }
244
-
245
- const result = new Map<string, string>()
246
- for (const task of unassigned) {
247
- // Pick the agent with the lowest current load.
248
- let bestAgent = agents[0]!
249
- let bestLoad = load.get(bestAgent.name) ?? 0
250
-
251
- for (let i = 1; i < agents.length; i++) {
252
- const agent = agents[i]!
253
- const agentLoad = load.get(agent.name) ?? 0
254
- if (agentLoad < bestLoad) {
255
- bestLoad = agentLoad
256
- bestAgent = agent
257
- }
258
- }
259
-
260
- result.set(task.id, bestAgent.name)
261
- // Increment the simulated load so subsequent tasks in this batch avoid
262
- // piling onto the same agent.
263
- load.set(bestAgent.name, (load.get(bestAgent.name) ?? 0) + 1)
264
- }
265
-
266
- return result
267
- }
268
-
269
- /**
270
- * Capability-match: score each agent against each task by keyword overlap
271
- * between the task's title/description and the agent's `systemPrompt` and
272
- * `name`. The highest-scoring agent wins.
273
- *
274
- * Falls back to round-robin when no agent has any positive score.
275
- */
276
- private scheduleCapabilityMatch(
277
- unassigned: Task[],
278
- agents: AgentConfig[],
279
- ): Map<string, string> {
280
- const result = new Map<string, string>()
281
-
282
- // Pre-compute keyword lists for each agent to avoid re-extracting per task.
283
- const agentKeywords = new Map<string, string[]>(
284
- agents.map((a) => [
285
- a.name,
286
- extractKeywords(`${a.name} ${a.systemPrompt ?? ''} ${a.model}`),
287
- ]),
288
- )
289
-
290
- for (const task of unassigned) {
291
- const taskText = `${task.title} ${task.description}`
292
- const taskKeywords = extractKeywords(taskText)
293
-
294
- let bestAgent = agents[0]!
295
- let bestScore = -1
296
-
297
- for (const agent of agents) {
298
- // Score in both directions: task keywords vs agent text, and agent
299
- // keywords vs task text, then take the max.
300
- const agentText = `${agent.name} ${agent.systemPrompt ?? ''}`
301
- const scoreA = keywordScore(agentText, taskKeywords)
302
- const scoreB = keywordScore(taskText, agentKeywords.get(agent.name) ?? [])
303
- const score = scoreA + scoreB
304
-
305
- if (score > bestScore) {
306
- bestScore = score
307
- bestAgent = agent
308
- }
309
- }
310
-
311
- result.set(task.id, bestAgent.name)
312
- }
313
-
314
- return result
315
- }
316
-
317
- /**
318
- * Dependency-first: prioritise tasks by how many other tasks are blocked
319
- * waiting for them (the "critical path" heuristic).
320
- *
321
- * Tasks with more downstream dependents are assigned to agents first. Within
322
- * the same criticality tier the agents are selected round-robin so no single
323
- * agent is overloaded.
324
- */
325
- private scheduleDependencyFirst(
326
- unassigned: Task[],
327
- agents: AgentConfig[],
328
- allTasks: Task[],
329
- ): Map<string, string> {
330
- // Sort by descending blocked-dependent count so high-criticality tasks
331
- // get first choice of agents.
332
- const ranked = [...unassigned].sort((a, b) => {
333
- const critA = countBlockedDependents(a.id, allTasks)
334
- const critB = countBlockedDependents(b.id, allTasks)
335
- return critB - critA
336
- })
337
-
338
- const result = new Map<string, string>()
339
- let cursor = this.roundRobinCursor
340
-
341
- for (const task of ranked) {
342
- const agent = agents[cursor % agents.length]!
343
- result.set(task.id, agent.name)
344
- cursor = (cursor + 1) % agents.length
345
- }
346
-
347
- // Advance the shared cursor for consistency with round-robin.
348
- this.roundRobinCursor = cursor
349
-
350
- return result
351
- }
352
- }