@jackchen_me/open-multi-agent 0.2.0 → 1.0.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.
Files changed (104) hide show
  1. package/.github/workflows/ci.yml +1 -1
  2. package/CLAUDE.md +11 -3
  3. package/README.md +87 -20
  4. package/README_zh.md +85 -25
  5. package/dist/agent/agent.d.ts +15 -1
  6. package/dist/agent/agent.d.ts.map +1 -1
  7. package/dist/agent/agent.js +144 -10
  8. package/dist/agent/agent.js.map +1 -1
  9. package/dist/agent/loop-detector.d.ts +39 -0
  10. package/dist/agent/loop-detector.d.ts.map +1 -0
  11. package/dist/agent/loop-detector.js +122 -0
  12. package/dist/agent/loop-detector.js.map +1 -0
  13. package/dist/agent/pool.d.ts +2 -1
  14. package/dist/agent/pool.d.ts.map +1 -1
  15. package/dist/agent/pool.js +4 -2
  16. package/dist/agent/pool.js.map +1 -1
  17. package/dist/agent/runner.d.ts +23 -1
  18. package/dist/agent/runner.d.ts.map +1 -1
  19. package/dist/agent/runner.js +113 -12
  20. package/dist/agent/runner.js.map +1 -1
  21. package/dist/index.d.ts +3 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +2 -0
  24. package/dist/index.js.map +1 -1
  25. package/dist/llm/adapter.d.ts +4 -1
  26. package/dist/llm/adapter.d.ts.map +1 -1
  27. package/dist/llm/adapter.js +11 -0
  28. package/dist/llm/adapter.js.map +1 -1
  29. package/dist/llm/copilot.d.ts.map +1 -1
  30. package/dist/llm/copilot.js +2 -1
  31. package/dist/llm/copilot.js.map +1 -1
  32. package/dist/llm/gemini.d.ts +65 -0
  33. package/dist/llm/gemini.d.ts.map +1 -0
  34. package/dist/llm/gemini.js +317 -0
  35. package/dist/llm/gemini.js.map +1 -0
  36. package/dist/llm/grok.d.ts +21 -0
  37. package/dist/llm/grok.d.ts.map +1 -0
  38. package/dist/llm/grok.js +24 -0
  39. package/dist/llm/grok.js.map +1 -0
  40. package/dist/llm/openai-common.d.ts +8 -1
  41. package/dist/llm/openai-common.d.ts.map +1 -1
  42. package/dist/llm/openai-common.js +35 -2
  43. package/dist/llm/openai-common.js.map +1 -1
  44. package/dist/llm/openai.d.ts +1 -1
  45. package/dist/llm/openai.d.ts.map +1 -1
  46. package/dist/llm/openai.js +20 -2
  47. package/dist/llm/openai.js.map +1 -1
  48. package/dist/orchestrator/orchestrator.d.ts.map +1 -1
  49. package/dist/orchestrator/orchestrator.js +89 -9
  50. package/dist/orchestrator/orchestrator.js.map +1 -1
  51. package/dist/task/queue.d.ts +31 -2
  52. package/dist/task/queue.d.ts.map +1 -1
  53. package/dist/task/queue.js +69 -2
  54. package/dist/task/queue.js.map +1 -1
  55. package/dist/tool/text-tool-extractor.d.ts +32 -0
  56. package/dist/tool/text-tool-extractor.d.ts.map +1 -0
  57. package/dist/tool/text-tool-extractor.js +187 -0
  58. package/dist/tool/text-tool-extractor.js.map +1 -0
  59. package/dist/types.d.ts +139 -7
  60. package/dist/types.d.ts.map +1 -1
  61. package/dist/utils/trace.d.ts +12 -0
  62. package/dist/utils/trace.d.ts.map +1 -0
  63. package/dist/utils/trace.js +30 -0
  64. package/dist/utils/trace.js.map +1 -0
  65. package/examples/06-local-model.ts +1 -0
  66. package/examples/08-gemma4-local.ts +76 -87
  67. package/examples/09-structured-output.ts +73 -0
  68. package/examples/10-task-retry.ts +132 -0
  69. package/examples/11-trace-observability.ts +133 -0
  70. package/examples/12-grok.ts +154 -0
  71. package/examples/13-gemini.ts +48 -0
  72. package/package.json +11 -1
  73. package/src/agent/agent.ts +159 -10
  74. package/src/agent/loop-detector.ts +137 -0
  75. package/src/agent/pool.ts +9 -2
  76. package/src/agent/runner.ts +148 -19
  77. package/src/index.ts +15 -0
  78. package/src/llm/adapter.ts +12 -1
  79. package/src/llm/copilot.ts +2 -1
  80. package/src/llm/gemini.ts +378 -0
  81. package/src/llm/grok.ts +29 -0
  82. package/src/llm/openai-common.ts +41 -2
  83. package/src/llm/openai.ts +23 -3
  84. package/src/orchestrator/orchestrator.ts +105 -11
  85. package/src/task/queue.ts +73 -3
  86. package/src/tool/text-tool-extractor.ts +219 -0
  87. package/src/types.ts +157 -6
  88. package/src/utils/trace.ts +34 -0
  89. package/tests/agent-hooks.test.ts +473 -0
  90. package/tests/agent-pool.test.ts +212 -0
  91. package/tests/approval.test.ts +464 -0
  92. package/tests/built-in-tools.test.ts +393 -0
  93. package/tests/gemini-adapter.test.ts +97 -0
  94. package/tests/grok-adapter.test.ts +74 -0
  95. package/tests/llm-adapters.test.ts +357 -0
  96. package/tests/loop-detection.test.ts +456 -0
  97. package/tests/openai-fallback.test.ts +159 -0
  98. package/tests/orchestrator.test.ts +281 -0
  99. package/tests/scheduler.test.ts +221 -0
  100. package/tests/team-messaging.test.ts +329 -0
  101. package/tests/text-tool-extractor.test.ts +170 -0
  102. package/tests/trace.test.ts +453 -0
  103. package/vitest.config.ts +9 -0
  104. package/examples/09-gemma4-auto-orchestration.ts +0 -162
package/src/types.ts CHANGED
@@ -94,7 +94,7 @@ export interface LLMResponse {
94
94
  * - `error` — an unrecoverable error occurred; `data` is an `Error`
95
95
  */
96
96
  export interface StreamEvent {
97
- readonly type: 'text' | 'tool_use' | 'tool_result' | 'done' | 'error'
97
+ readonly type: 'text' | 'tool_use' | 'tool_result' | 'loop_detected' | 'done' | 'error'
98
98
  readonly data: unknown
99
99
  }
100
100
 
@@ -182,11 +182,19 @@ export interface ToolDefinition<TInput = Record<string, unknown>> {
182
182
  // Agent
183
183
  // ---------------------------------------------------------------------------
184
184
 
185
+ /** Context passed to the {@link AgentConfig.beforeRun} hook. */
186
+ export interface BeforeRunHookContext {
187
+ /** The user prompt text. */
188
+ readonly prompt: string
189
+ /** The agent's static configuration. */
190
+ readonly agent: AgentConfig
191
+ }
192
+
185
193
  /** Static configuration for a single agent. */
186
194
  export interface AgentConfig {
187
195
  readonly name: string
188
196
  readonly model: string
189
- readonly provider?: 'anthropic' | 'copilot' | 'openai'
197
+ readonly provider?: 'anthropic' | 'copilot' | 'grok' | 'openai' | 'gemini'
190
198
  /**
191
199
  * Custom base URL for OpenAI-compatible APIs (Ollama, vLLM, LM Studio, etc.).
192
200
  * Note: local servers that don't require auth still need `apiKey` set to a
@@ -201,12 +209,70 @@ export interface AgentConfig {
201
209
  readonly maxTurns?: number
202
210
  readonly maxTokens?: number
203
211
  readonly temperature?: number
212
+ /**
213
+ * Maximum wall-clock time (in milliseconds) for the entire agent run.
214
+ * When exceeded, the run is aborted via `AbortSignal.timeout()`.
215
+ * Useful for local models where inference can be unpredictably slow.
216
+ */
217
+ readonly timeoutMs?: number
218
+ /**
219
+ * Loop detection configuration. When set, the agent tracks repeated tool
220
+ * calls and text outputs to detect stuck loops before `maxTurns` is reached.
221
+ */
222
+ readonly loopDetection?: LoopDetectionConfig
204
223
  /**
205
224
  * Optional Zod schema for structured output. When set, the agent's final
206
225
  * output is parsed as JSON and validated against this schema. A single
207
226
  * retry with error feedback is attempted on validation failure.
208
227
  */
209
228
  readonly outputSchema?: ZodSchema
229
+ /**
230
+ * Called before each agent run. Receives the prompt and agent config.
231
+ * Return a (possibly modified) context to continue, or throw to abort the run.
232
+ * Only `prompt` from the returned context is applied; `agent` is read-only informational.
233
+ */
234
+ readonly beforeRun?: (context: BeforeRunHookContext) => Promise<BeforeRunHookContext> | BeforeRunHookContext
235
+ /**
236
+ * Called after each agent run completes successfully. Receives the run result.
237
+ * Return a (possibly modified) result, or throw to mark the run as failed.
238
+ * Not called when the run throws. For error observation, handle errors at the call site.
239
+ */
240
+ readonly afterRun?: (result: AgentRunResult) => Promise<AgentRunResult> | AgentRunResult
241
+ }
242
+
243
+ // ---------------------------------------------------------------------------
244
+ // Loop detection
245
+ // ---------------------------------------------------------------------------
246
+
247
+ /** Configuration for agent loop detection. */
248
+ export interface LoopDetectionConfig {
249
+ /**
250
+ * Maximum consecutive times the same tool call (name + args) or text
251
+ * output can repeat before detection triggers. Default: `3`.
252
+ */
253
+ readonly maxRepetitions?: number
254
+ /**
255
+ * Number of recent turns to track for repetition analysis. Default: `4`.
256
+ */
257
+ readonly loopDetectionWindow?: number
258
+ /**
259
+ * Action to take when a loop is detected.
260
+ * - `'warn'` — inject a "you appear stuck" message, give the LLM one
261
+ * more chance; terminate if the loop persists (default)
262
+ * - `'terminate'` — stop the run immediately
263
+ * - `function` — custom callback (sync or async); return `'continue'`,
264
+ * `'inject'`, or `'terminate'` to control the outcome
265
+ */
266
+ readonly onLoopDetected?: 'warn' | 'terminate' | ((info: LoopDetectionInfo) => 'continue' | 'inject' | 'terminate' | Promise<'continue' | 'inject' | 'terminate'>)
267
+ }
268
+
269
+ /** Diagnostic payload emitted when a loop is detected. */
270
+ export interface LoopDetectionInfo {
271
+ readonly kind: 'tool_repetition' | 'text_repetition'
272
+ /** Number of consecutive identical occurrences observed. */
273
+ readonly repetitions: number
274
+ /** Human-readable description of the detected loop. */
275
+ readonly detail: string
210
276
  }
211
277
 
212
278
  /** Lifecycle state tracked during an agent run. */
@@ -239,6 +305,8 @@ export interface AgentRunResult {
239
305
  * failed after retry.
240
306
  */
241
307
  readonly structured?: unknown
308
+ /** True when the run was terminated or warned due to loop detection. */
309
+ readonly loopDetected?: boolean
242
310
  }
243
311
 
244
312
  // ---------------------------------------------------------------------------
@@ -266,7 +334,7 @@ export interface TeamRunResult {
266
334
  // ---------------------------------------------------------------------------
267
335
 
268
336
  /** Valid states for a {@link Task}. */
269
- export type TaskStatus = 'pending' | 'in_progress' | 'completed' | 'failed' | 'blocked'
337
+ export type TaskStatus = 'pending' | 'in_progress' | 'completed' | 'failed' | 'blocked' | 'skipped'
270
338
 
271
339
  /** A discrete unit of work tracked by the orchestrator. */
272
340
  export interface Task {
@@ -293,13 +361,19 @@ export interface Task {
293
361
  // Orchestrator
294
362
  // ---------------------------------------------------------------------------
295
363
 
296
- /** Progress event emitted by the orchestrator during a run. */
364
+ /**
365
+ * Progress event emitted by the orchestrator during a run.
366
+ *
367
+ * **v0.3 addition:** `'task_skipped'` — consumers with exhaustive switches
368
+ * on `type` will need to add a case for this variant.
369
+ */
297
370
  export interface OrchestratorEvent {
298
371
  readonly type:
299
372
  | 'agent_start'
300
373
  | 'agent_complete'
301
374
  | 'task_start'
302
375
  | 'task_complete'
376
+ | 'task_skipped'
303
377
  | 'task_retry'
304
378
  | 'message'
305
379
  | 'error'
@@ -312,12 +386,89 @@ export interface OrchestratorEvent {
312
386
  export interface OrchestratorConfig {
313
387
  readonly maxConcurrency?: number
314
388
  readonly defaultModel?: string
315
- readonly defaultProvider?: 'anthropic' | 'copilot' | 'openai'
389
+ readonly defaultProvider?: 'anthropic' | 'copilot' | 'grok' | 'openai' | 'gemini'
316
390
  readonly defaultBaseURL?: string
317
391
  readonly defaultApiKey?: string
318
- onProgress?: (event: OrchestratorEvent) => void
392
+ readonly onProgress?: (event: OrchestratorEvent) => void
393
+ readonly onTrace?: (event: TraceEvent) => void | Promise<void>
394
+ /**
395
+ * Optional approval gate called between task execution rounds.
396
+ *
397
+ * After a batch of tasks completes, this callback receives all
398
+ * completed {@link Task}s from that round and the list of tasks about
399
+ * to start next. Return `true` to continue or `false` to abort —
400
+ * remaining tasks will be marked `'skipped'`.
401
+ *
402
+ * Not called when:
403
+ * - No tasks succeeded in the round (all failed).
404
+ * - No pending tasks remain after the round (final batch).
405
+ *
406
+ * **Note:** Do not mutate the {@link Task} objects passed to this
407
+ * callback — they are live references to queue state. Mutation is
408
+ * undefined behavior.
409
+ */
410
+ readonly onApproval?: (completedTasks: readonly Task[], nextTasks: readonly Task[]) => Promise<boolean>
411
+ }
412
+
413
+ // ---------------------------------------------------------------------------
414
+ // Trace events — lightweight observability spans
415
+ // ---------------------------------------------------------------------------
416
+
417
+ /** Trace event type discriminants. */
418
+ export type TraceEventType = 'llm_call' | 'tool_call' | 'task' | 'agent'
419
+
420
+ /** Shared fields present on every trace event. */
421
+ export interface TraceEventBase {
422
+ /** Unique identifier for the entire run (runTeam / runTasks / runAgent call). */
423
+ readonly runId: string
424
+ readonly type: TraceEventType
425
+ /** Unix epoch ms when the span started. */
426
+ readonly startMs: number
427
+ /** Unix epoch ms when the span ended. */
428
+ readonly endMs: number
429
+ /** Wall-clock duration in milliseconds (`endMs - startMs`). */
430
+ readonly durationMs: number
431
+ /** Agent name associated with this span. */
432
+ readonly agent: string
433
+ /** Task ID associated with this span. */
434
+ readonly taskId?: string
319
435
  }
320
436
 
437
+ /** Emitted for each LLM API call (one per agent turn). */
438
+ export interface LLMCallTrace extends TraceEventBase {
439
+ readonly type: 'llm_call'
440
+ readonly model: string
441
+ readonly turn: number
442
+ readonly tokens: TokenUsage
443
+ }
444
+
445
+ /** Emitted for each tool execution. */
446
+ export interface ToolCallTrace extends TraceEventBase {
447
+ readonly type: 'tool_call'
448
+ readonly tool: string
449
+ readonly isError: boolean
450
+ }
451
+
452
+ /** Emitted when a task completes (wraps the full retry sequence). */
453
+ export interface TaskTrace extends TraceEventBase {
454
+ readonly type: 'task'
455
+ readonly taskId: string
456
+ readonly taskTitle: string
457
+ readonly success: boolean
458
+ readonly retries: number
459
+ }
460
+
461
+ /** Emitted when an agent run completes (wraps the full conversation loop). */
462
+ export interface AgentTrace extends TraceEventBase {
463
+ readonly type: 'agent'
464
+ readonly turns: number
465
+ readonly tokens: TokenUsage
466
+ readonly toolCalls: number
467
+ }
468
+
469
+ /** Discriminated union of all trace event types. */
470
+ export type TraceEvent = LLMCallTrace | ToolCallTrace | TaskTrace | AgentTrace
471
+
321
472
  // ---------------------------------------------------------------------------
322
473
  // Memory
323
474
  // ---------------------------------------------------------------------------
@@ -0,0 +1,34 @@
1
+ /**
2
+ * @fileoverview Trace emission utilities for the observability layer.
3
+ */
4
+
5
+ import { randomUUID } from 'node:crypto'
6
+ import type { TraceEvent } from '../types.js'
7
+
8
+ /**
9
+ * Safely emit a trace event. Swallows callback errors so a broken
10
+ * subscriber never crashes agent execution.
11
+ */
12
+ export function emitTrace(
13
+ fn: ((event: TraceEvent) => void | Promise<void>) | undefined,
14
+ event: TraceEvent,
15
+ ): void {
16
+ if (!fn) return
17
+ try {
18
+ // Guard async callbacks: if fn returns a Promise, swallow its rejection
19
+ // so an async onTrace never produces an unhandled promise rejection.
20
+ const result = fn(event) as unknown
21
+ if (result && typeof (result as Promise<unknown>).catch === 'function') {
22
+ ;(result as Promise<unknown>).catch(noop)
23
+ }
24
+ } catch {
25
+ // Intentionally swallowed — observability must never break execution.
26
+ }
27
+ }
28
+
29
+ function noop() {}
30
+
31
+ /** Generate a unique run ID for trace correlation. */
32
+ export function generateRunId(): string {
33
+ return randomUUID()
34
+ }