@jackchen_me/open-multi-agent 0.2.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 (117) hide show
  1. package/README.md +87 -20
  2. package/dist/agent/agent.d.ts +15 -1
  3. package/dist/agent/agent.d.ts.map +1 -1
  4. package/dist/agent/agent.js +144 -10
  5. package/dist/agent/agent.js.map +1 -1
  6. package/dist/agent/loop-detector.d.ts +39 -0
  7. package/dist/agent/loop-detector.d.ts.map +1 -0
  8. package/dist/agent/loop-detector.js +122 -0
  9. package/dist/agent/loop-detector.js.map +1 -0
  10. package/dist/agent/pool.d.ts +2 -1
  11. package/dist/agent/pool.d.ts.map +1 -1
  12. package/dist/agent/pool.js +4 -2
  13. package/dist/agent/pool.js.map +1 -1
  14. package/dist/agent/runner.d.ts +23 -1
  15. package/dist/agent/runner.d.ts.map +1 -1
  16. package/dist/agent/runner.js +113 -12
  17. package/dist/agent/runner.js.map +1 -1
  18. package/dist/index.d.ts +3 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +2 -0
  21. package/dist/index.js.map +1 -1
  22. package/dist/llm/adapter.d.ts +4 -1
  23. package/dist/llm/adapter.d.ts.map +1 -1
  24. package/dist/llm/adapter.js +11 -0
  25. package/dist/llm/adapter.js.map +1 -1
  26. package/dist/llm/copilot.d.ts.map +1 -1
  27. package/dist/llm/copilot.js +2 -1
  28. package/dist/llm/copilot.js.map +1 -1
  29. package/dist/llm/gemini.d.ts +65 -0
  30. package/dist/llm/gemini.d.ts.map +1 -0
  31. package/dist/llm/gemini.js +317 -0
  32. package/dist/llm/gemini.js.map +1 -0
  33. package/dist/llm/grok.d.ts +21 -0
  34. package/dist/llm/grok.d.ts.map +1 -0
  35. package/dist/llm/grok.js +24 -0
  36. package/dist/llm/grok.js.map +1 -0
  37. package/dist/llm/openai-common.d.ts +8 -1
  38. package/dist/llm/openai-common.d.ts.map +1 -1
  39. package/dist/llm/openai-common.js +35 -2
  40. package/dist/llm/openai-common.js.map +1 -1
  41. package/dist/llm/openai.d.ts +1 -1
  42. package/dist/llm/openai.d.ts.map +1 -1
  43. package/dist/llm/openai.js +20 -2
  44. package/dist/llm/openai.js.map +1 -1
  45. package/dist/orchestrator/orchestrator.d.ts.map +1 -1
  46. package/dist/orchestrator/orchestrator.js +89 -9
  47. package/dist/orchestrator/orchestrator.js.map +1 -1
  48. package/dist/task/queue.d.ts +31 -2
  49. package/dist/task/queue.d.ts.map +1 -1
  50. package/dist/task/queue.js +69 -2
  51. package/dist/task/queue.js.map +1 -1
  52. package/dist/tool/text-tool-extractor.d.ts +32 -0
  53. package/dist/tool/text-tool-extractor.d.ts.map +1 -0
  54. package/dist/tool/text-tool-extractor.js +187 -0
  55. package/dist/tool/text-tool-extractor.js.map +1 -0
  56. package/dist/types.d.ts +139 -7
  57. package/dist/types.d.ts.map +1 -1
  58. package/dist/utils/trace.d.ts +12 -0
  59. package/dist/utils/trace.d.ts.map +1 -0
  60. package/dist/utils/trace.js +30 -0
  61. package/dist/utils/trace.js.map +1 -0
  62. package/package.json +18 -2
  63. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -40
  64. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -23
  65. package/.github/pull_request_template.md +0 -14
  66. package/.github/workflows/ci.yml +0 -23
  67. package/CLAUDE.md +0 -72
  68. package/CODE_OF_CONDUCT.md +0 -48
  69. package/CONTRIBUTING.md +0 -72
  70. package/DECISIONS.md +0 -43
  71. package/README_zh.md +0 -217
  72. package/SECURITY.md +0 -17
  73. package/examples/01-single-agent.ts +0 -131
  74. package/examples/02-team-collaboration.ts +0 -167
  75. package/examples/03-task-pipeline.ts +0 -201
  76. package/examples/04-multi-model-team.ts +0 -261
  77. package/examples/05-copilot-test.ts +0 -49
  78. package/examples/06-local-model.ts +0 -199
  79. package/examples/07-fan-out-aggregate.ts +0 -209
  80. package/examples/08-gemma4-local.ts +0 -203
  81. package/examples/09-gemma4-auto-orchestration.ts +0 -162
  82. package/src/agent/agent.ts +0 -473
  83. package/src/agent/pool.ts +0 -278
  84. package/src/agent/runner.ts +0 -413
  85. package/src/agent/structured-output.ts +0 -126
  86. package/src/index.ts +0 -167
  87. package/src/llm/adapter.ts +0 -87
  88. package/src/llm/anthropic.ts +0 -389
  89. package/src/llm/copilot.ts +0 -551
  90. package/src/llm/openai-common.ts +0 -255
  91. package/src/llm/openai.ts +0 -272
  92. package/src/memory/shared.ts +0 -181
  93. package/src/memory/store.ts +0 -124
  94. package/src/orchestrator/orchestrator.ts +0 -977
  95. package/src/orchestrator/scheduler.ts +0 -352
  96. package/src/task/queue.ts +0 -394
  97. package/src/task/task.ts +0 -239
  98. package/src/team/messaging.ts +0 -232
  99. package/src/team/team.ts +0 -334
  100. package/src/tool/built-in/bash.ts +0 -187
  101. package/src/tool/built-in/file-edit.ts +0 -154
  102. package/src/tool/built-in/file-read.ts +0 -105
  103. package/src/tool/built-in/file-write.ts +0 -81
  104. package/src/tool/built-in/grep.ts +0 -362
  105. package/src/tool/built-in/index.ts +0 -50
  106. package/src/tool/executor.ts +0 -178
  107. package/src/tool/framework.ts +0 -557
  108. package/src/types.ts +0 -391
  109. package/src/utils/semaphore.ts +0 -89
  110. package/tests/semaphore.test.ts +0 -57
  111. package/tests/shared-memory.test.ts +0 -122
  112. package/tests/structured-output.test.ts +0 -331
  113. package/tests/task-queue.test.ts +0 -244
  114. package/tests/task-retry.test.ts +0 -368
  115. package/tests/task-utils.test.ts +0 -155
  116. package/tests/tool-executor.test.ts +0 -193
  117. package/tsconfig.json +0 -25
package/src/types.ts DELETED
@@ -1,391 +0,0 @@
1
- /**
2
- * @fileoverview Core type definitions for the open-multi-agent orchestration framework.
3
- *
4
- * All public types are exported from this single module. Downstream modules
5
- * import only what they need, keeping the dependency graph acyclic.
6
- */
7
-
8
- import type { ZodSchema } from 'zod'
9
-
10
- // ---------------------------------------------------------------------------
11
- // Content blocks
12
- // ---------------------------------------------------------------------------
13
-
14
- /** Plain-text content produced by a model or supplied by the user. */
15
- export interface TextBlock {
16
- readonly type: 'text'
17
- readonly text: string
18
- }
19
-
20
- /**
21
- * A request by the model to invoke a named tool with a structured input.
22
- * The `id` is unique per turn and is referenced by {@link ToolResultBlock}.
23
- */
24
- export interface ToolUseBlock {
25
- readonly type: 'tool_use'
26
- readonly id: string
27
- readonly name: string
28
- readonly input: Record<string, unknown>
29
- }
30
-
31
- /**
32
- * The result of executing a tool, keyed back to the originating
33
- * {@link ToolUseBlock} via `tool_use_id`.
34
- */
35
- export interface ToolResultBlock {
36
- readonly type: 'tool_result'
37
- readonly tool_use_id: string
38
- readonly content: string
39
- readonly is_error?: boolean
40
- }
41
-
42
- /** A base64-encoded image passed to or returned from a model. */
43
- export interface ImageBlock {
44
- readonly type: 'image'
45
- readonly source: {
46
- readonly type: 'base64'
47
- readonly media_type: string
48
- readonly data: string
49
- }
50
- }
51
-
52
- /** Union of all content block variants that may appear in a message. */
53
- export type ContentBlock = TextBlock | ToolUseBlock | ToolResultBlock | ImageBlock
54
-
55
- // ---------------------------------------------------------------------------
56
- // LLM messages & responses
57
- // ---------------------------------------------------------------------------
58
-
59
- /**
60
- * A single message in a conversation thread.
61
- * System messages are passed separately via {@link LLMChatOptions.systemPrompt}.
62
- */
63
- export interface LLMMessage {
64
- readonly role: 'user' | 'assistant'
65
- readonly content: ContentBlock[]
66
- }
67
-
68
- /** Token accounting for a single API call. */
69
- export interface TokenUsage {
70
- readonly input_tokens: number
71
- readonly output_tokens: number
72
- }
73
-
74
- /** Normalised response returned by every {@link LLMAdapter} implementation. */
75
- export interface LLMResponse {
76
- readonly id: string
77
- readonly content: ContentBlock[]
78
- readonly model: string
79
- readonly stop_reason: string
80
- readonly usage: TokenUsage
81
- }
82
-
83
- // ---------------------------------------------------------------------------
84
- // Streaming
85
- // ---------------------------------------------------------------------------
86
-
87
- /**
88
- * A discrete event emitted during streaming generation.
89
- *
90
- * - `text` — incremental text delta
91
- * - `tool_use` — the model has begun or completed a tool-use block
92
- * - `tool_result` — a tool result has been appended to the stream
93
- * - `done` — the stream has ended; `data` is the final {@link LLMResponse}
94
- * - `error` — an unrecoverable error occurred; `data` is an `Error`
95
- */
96
- export interface StreamEvent {
97
- readonly type: 'text' | 'tool_use' | 'tool_result' | 'done' | 'error'
98
- readonly data: unknown
99
- }
100
-
101
- // ---------------------------------------------------------------------------
102
- // Tool definitions
103
- // ---------------------------------------------------------------------------
104
-
105
- /** The serialisable tool schema sent to the LLM provider. */
106
- export interface LLMToolDef {
107
- readonly name: string
108
- readonly description: string
109
- /** JSON Schema object describing the tool's `input` parameter. */
110
- readonly inputSchema: Record<string, unknown>
111
- }
112
-
113
- /**
114
- * Context injected into every tool execution.
115
- *
116
- * Both `abortSignal` and `abortController` are provided so that tools and the
117
- * executor can choose the most ergonomic API for their use-case:
118
- *
119
- * - Long-running shell commands that need to kill a child process can use
120
- * `abortController.signal` directly.
121
- * - Simple cancellation checks can read `abortSignal?.aborted`.
122
- *
123
- * When constructing a context, set `abortController` and derive `abortSignal`
124
- * from it, or provide both independently.
125
- */
126
- export interface ToolUseContext {
127
- /** High-level description of the agent invoking this tool. */
128
- readonly agent: AgentInfo
129
- /** Team context, present when the tool runs inside a multi-agent team. */
130
- readonly team?: TeamInfo
131
- /**
132
- * Convenience reference to the abort signal.
133
- * Equivalent to `abortController?.signal` when an `abortController` is set.
134
- */
135
- readonly abortSignal?: AbortSignal
136
- /**
137
- * Full abort controller, available when the caller needs to inspect or
138
- * programmatically abort the signal.
139
- * Tools should prefer `abortSignal` for simple cancellation checks.
140
- */
141
- readonly abortController?: AbortController
142
- /** Working directory hint for file-system tools. */
143
- readonly cwd?: string
144
- /** Arbitrary caller-supplied metadata (session ID, request ID, etc.). */
145
- readonly metadata?: Readonly<Record<string, unknown>>
146
- }
147
-
148
- /** Minimal descriptor for the agent that is invoking a tool. */
149
- export interface AgentInfo {
150
- readonly name: string
151
- readonly role: string
152
- readonly model: string
153
- }
154
-
155
- /** Descriptor for a team of agents with shared memory. */
156
- export interface TeamInfo {
157
- readonly name: string
158
- readonly agents: readonly string[]
159
- readonly sharedMemory: MemoryStore
160
- }
161
-
162
- /** Value returned by a tool's `execute` function. */
163
- export interface ToolResult {
164
- readonly data: string
165
- readonly isError?: boolean
166
- }
167
-
168
- /**
169
- * A tool registered with the framework.
170
- *
171
- * `inputSchema` is a Zod schema used for validation before `execute` is called.
172
- * At API call time it is converted to JSON Schema via {@link LLMToolDef}.
173
- */
174
- export interface ToolDefinition<TInput = Record<string, unknown>> {
175
- readonly name: string
176
- readonly description: string
177
- readonly inputSchema: ZodSchema<TInput>
178
- execute(input: TInput, context: ToolUseContext): Promise<ToolResult>
179
- }
180
-
181
- // ---------------------------------------------------------------------------
182
- // Agent
183
- // ---------------------------------------------------------------------------
184
-
185
- /** Static configuration for a single agent. */
186
- export interface AgentConfig {
187
- readonly name: string
188
- readonly model: string
189
- readonly provider?: 'anthropic' | 'copilot' | 'openai'
190
- /**
191
- * Custom base URL for OpenAI-compatible APIs (Ollama, vLLM, LM Studio, etc.).
192
- * Note: local servers that don't require auth still need `apiKey` set to a
193
- * non-empty placeholder (e.g. `'ollama'`) because the OpenAI SDK validates it.
194
- */
195
- readonly baseURL?: string
196
- /** API key override; falls back to the provider's standard env var. */
197
- readonly apiKey?: string
198
- readonly systemPrompt?: string
199
- /** Names of tools (from the tool registry) available to this agent. */
200
- readonly tools?: readonly string[]
201
- readonly maxTurns?: number
202
- readonly maxTokens?: number
203
- readonly temperature?: number
204
- /**
205
- * Optional Zod schema for structured output. When set, the agent's final
206
- * output is parsed as JSON and validated against this schema. A single
207
- * retry with error feedback is attempted on validation failure.
208
- */
209
- readonly outputSchema?: ZodSchema
210
- }
211
-
212
- /** Lifecycle state tracked during an agent run. */
213
- export interface AgentState {
214
- status: 'idle' | 'running' | 'completed' | 'error'
215
- messages: LLMMessage[]
216
- tokenUsage: TokenUsage
217
- error?: Error
218
- }
219
-
220
- /** A single recorded tool invocation within a run. */
221
- export interface ToolCallRecord {
222
- readonly toolName: string
223
- readonly input: Record<string, unknown>
224
- readonly output: string
225
- /** Wall-clock duration in milliseconds. */
226
- readonly duration: number
227
- }
228
-
229
- /** The final result produced when an agent run completes (or fails). */
230
- export interface AgentRunResult {
231
- readonly success: boolean
232
- readonly output: string
233
- readonly messages: LLMMessage[]
234
- readonly tokenUsage: TokenUsage
235
- readonly toolCalls: ToolCallRecord[]
236
- /**
237
- * Parsed and validated structured output when `outputSchema` is set on the
238
- * agent config. `undefined` when no schema is configured or validation
239
- * failed after retry.
240
- */
241
- readonly structured?: unknown
242
- }
243
-
244
- // ---------------------------------------------------------------------------
245
- // Team
246
- // ---------------------------------------------------------------------------
247
-
248
- /** Static configuration for a team of cooperating agents. */
249
- export interface TeamConfig {
250
- readonly name: string
251
- readonly agents: readonly AgentConfig[]
252
- readonly sharedMemory?: boolean
253
- readonly maxConcurrency?: number
254
- }
255
-
256
- /** Aggregated result for a full team run. */
257
- export interface TeamRunResult {
258
- readonly success: boolean
259
- /** Keyed by agent name. */
260
- readonly agentResults: Map<string, AgentRunResult>
261
- readonly totalTokenUsage: TokenUsage
262
- }
263
-
264
- // ---------------------------------------------------------------------------
265
- // Task
266
- // ---------------------------------------------------------------------------
267
-
268
- /** Valid states for a {@link Task}. */
269
- export type TaskStatus = 'pending' | 'in_progress' | 'completed' | 'failed' | 'blocked'
270
-
271
- /** A discrete unit of work tracked by the orchestrator. */
272
- export interface Task {
273
- readonly id: string
274
- readonly title: string
275
- readonly description: string
276
- status: TaskStatus
277
- /** Agent name responsible for executing this task. */
278
- assignee?: string
279
- /** IDs of tasks that must complete before this one can start. */
280
- dependsOn?: readonly string[]
281
- result?: string
282
- readonly createdAt: Date
283
- updatedAt: Date
284
- /** Maximum number of retry attempts on failure (default: 0 — no retry). */
285
- readonly maxRetries?: number
286
- /** Base delay in ms before the first retry (default: 1000). */
287
- readonly retryDelayMs?: number
288
- /** Exponential backoff multiplier (default: 2). */
289
- readonly retryBackoff?: number
290
- }
291
-
292
- // ---------------------------------------------------------------------------
293
- // Orchestrator
294
- // ---------------------------------------------------------------------------
295
-
296
- /** Progress event emitted by the orchestrator during a run. */
297
- export interface OrchestratorEvent {
298
- readonly type:
299
- | 'agent_start'
300
- | 'agent_complete'
301
- | 'task_start'
302
- | 'task_complete'
303
- | 'task_retry'
304
- | 'message'
305
- | 'error'
306
- readonly agent?: string
307
- readonly task?: string
308
- readonly data?: unknown
309
- }
310
-
311
- /** Top-level configuration for the orchestrator. */
312
- export interface OrchestratorConfig {
313
- readonly maxConcurrency?: number
314
- readonly defaultModel?: string
315
- readonly defaultProvider?: 'anthropic' | 'copilot' | 'openai'
316
- readonly defaultBaseURL?: string
317
- readonly defaultApiKey?: string
318
- onProgress?: (event: OrchestratorEvent) => void
319
- }
320
-
321
- // ---------------------------------------------------------------------------
322
- // Memory
323
- // ---------------------------------------------------------------------------
324
-
325
- /** A single key-value record stored in a {@link MemoryStore}. */
326
- export interface MemoryEntry {
327
- readonly key: string
328
- readonly value: string
329
- readonly metadata?: Readonly<Record<string, unknown>>
330
- readonly createdAt: Date
331
- }
332
-
333
- /**
334
- * Persistent (or in-memory) key-value store shared across agents.
335
- * Implementations may be backed by Redis, SQLite, or plain objects.
336
- */
337
- export interface MemoryStore {
338
- get(key: string): Promise<MemoryEntry | null>
339
- set(key: string, value: string, metadata?: Record<string, unknown>): Promise<void>
340
- list(): Promise<MemoryEntry[]>
341
- delete(key: string): Promise<void>
342
- clear(): Promise<void>
343
- }
344
-
345
- // ---------------------------------------------------------------------------
346
- // LLM adapter
347
- // ---------------------------------------------------------------------------
348
-
349
- /** Options shared by both chat and streaming calls. */
350
- export interface LLMChatOptions {
351
- readonly model: string
352
- readonly tools?: readonly LLMToolDef[]
353
- readonly maxTokens?: number
354
- readonly temperature?: number
355
- readonly systemPrompt?: string
356
- readonly abortSignal?: AbortSignal
357
- }
358
-
359
- /**
360
- * Options for streaming calls.
361
- * Extends {@link LLMChatOptions} without additional fields — the separation
362
- * exists so callers can type-narrow and implementations can diverge later.
363
- */
364
- export interface LLMStreamOptions extends LLMChatOptions {}
365
-
366
- /**
367
- * Provider-agnostic interface that every LLM backend must implement.
368
- *
369
- * @example
370
- * ```ts
371
- * const adapter: LLMAdapter = createAdapter('anthropic')
372
- * const response = await adapter.chat(messages, { model: 'claude-opus-4-6' })
373
- * ```
374
- */
375
- export interface LLMAdapter {
376
- /** Human-readable provider name, e.g. `'anthropic'` or `'openai'`. */
377
- readonly name: string
378
-
379
- /**
380
- * Send a chat request and return the complete response.
381
- * Throws on non-retryable API errors.
382
- */
383
- chat(messages: LLMMessage[], options: LLMChatOptions): Promise<LLMResponse>
384
-
385
- /**
386
- * Send a chat request and yield {@link StreamEvent}s incrementally.
387
- * The final event in the sequence always has `type === 'done'` on success,
388
- * or `type === 'error'` on failure.
389
- */
390
- stream(messages: LLMMessage[], options: LLMStreamOptions): AsyncIterable<StreamEvent>
391
- }
@@ -1,89 +0,0 @@
1
- /**
2
- * @fileoverview Shared counting semaphore for concurrency control.
3
- *
4
- * Used by both {@link ToolExecutor} and {@link AgentPool} to cap the number of
5
- * concurrent async operations without requiring any third-party dependencies.
6
- *
7
- * This is intentionally self-contained and tuned for Promise/async use —
8
- * not a general OS-semaphore replacement.
9
- */
10
-
11
- // ---------------------------------------------------------------------------
12
- // Semaphore
13
- // ---------------------------------------------------------------------------
14
-
15
- /**
16
- * Classic counting semaphore for concurrency control.
17
- *
18
- * `acquire()` resolves immediately if a slot is free, otherwise queues the
19
- * caller. `release()` unblocks the next waiter in FIFO order.
20
- *
21
- * Node.js is single-threaded, so this is safe without atomics or mutex
22
- * primitives — the semaphore gates concurrent async operations, not CPU threads.
23
- */
24
- export class Semaphore {
25
- private current = 0
26
- private readonly queue: Array<() => void> = []
27
-
28
- /**
29
- * @param max - Maximum number of concurrent holders. Must be >= 1.
30
- */
31
- constructor(private readonly max: number) {
32
- if (max < 1) {
33
- throw new RangeError(`Semaphore max must be at least 1, got ${max}`)
34
- }
35
- }
36
-
37
- /**
38
- * Acquire a slot. Resolves immediately when one is free, or waits until a
39
- * holder calls `release()`.
40
- */
41
- acquire(): Promise<void> {
42
- if (this.current < this.max) {
43
- this.current++
44
- return Promise.resolve()
45
- }
46
-
47
- return new Promise<void>(resolve => {
48
- this.queue.push(resolve)
49
- })
50
- }
51
-
52
- /**
53
- * Release a previously acquired slot.
54
- * If callers are queued, the next one is unblocked synchronously.
55
- */
56
- release(): void {
57
- const next = this.queue.shift()
58
- if (next !== undefined) {
59
- // A queued caller is waiting — hand the slot directly to it.
60
- // `current` stays the same: we consumed the slot immediately.
61
- next()
62
- } else {
63
- this.current--
64
- }
65
- }
66
-
67
- /**
68
- * Run `fn` while holding one slot, automatically releasing it afterward
69
- * even if `fn` throws.
70
- */
71
- async run<T>(fn: () => Promise<T>): Promise<T> {
72
- await this.acquire()
73
- try {
74
- return await fn()
75
- } finally {
76
- this.release()
77
- }
78
- }
79
-
80
- /** Number of slots currently in use. */
81
- get active(): number {
82
- return this.current
83
- }
84
-
85
- /** Number of callers waiting for a slot. */
86
- get pending(): number {
87
- return this.queue.length
88
- }
89
- }
@@ -1,57 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { Semaphore } from '../src/utils/semaphore.js'
3
-
4
- describe('Semaphore', () => {
5
- it('throws on max < 1', () => {
6
- expect(() => new Semaphore(0)).toThrow()
7
- })
8
-
9
- it('allows up to max concurrent holders', async () => {
10
- const sem = new Semaphore(2)
11
- let running = 0
12
- let peak = 0
13
-
14
- const work = async () => {
15
- await sem.acquire()
16
- running++
17
- peak = Math.max(peak, running)
18
- await new Promise((r) => setTimeout(r, 30))
19
- running--
20
- sem.release()
21
- }
22
-
23
- await Promise.all([work(), work(), work(), work()])
24
- expect(peak).toBeLessThanOrEqual(2)
25
- })
26
-
27
- it('run() auto-releases on success', async () => {
28
- const sem = new Semaphore(1)
29
- const result = await sem.run(async () => 42)
30
- expect(result).toBe(42)
31
- expect(sem.active).toBe(0)
32
- })
33
-
34
- it('run() auto-releases on error', async () => {
35
- const sem = new Semaphore(1)
36
- await expect(sem.run(async () => { throw new Error('oops') })).rejects.toThrow('oops')
37
- expect(sem.active).toBe(0)
38
- })
39
-
40
- it('tracks active and pending counts', async () => {
41
- const sem = new Semaphore(1)
42
- await sem.acquire()
43
- expect(sem.active).toBe(1)
44
-
45
- // This will queue
46
- const p = sem.acquire()
47
- expect(sem.pending).toBe(1)
48
-
49
- sem.release()
50
- await p
51
- expect(sem.active).toBe(1)
52
- expect(sem.pending).toBe(0)
53
-
54
- sem.release()
55
- expect(sem.active).toBe(0)
56
- })
57
- })
@@ -1,122 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { SharedMemory } from '../src/memory/shared.js'
3
-
4
- describe('SharedMemory', () => {
5
- // -------------------------------------------------------------------------
6
- // Write & read
7
- // -------------------------------------------------------------------------
8
-
9
- it('writes and reads a value under a namespaced key', async () => {
10
- const mem = new SharedMemory()
11
- await mem.write('researcher', 'findings', 'TS 5.5 ships const type params')
12
-
13
- const entry = await mem.read('researcher/findings')
14
- expect(entry).not.toBeNull()
15
- expect(entry!.value).toBe('TS 5.5 ships const type params')
16
- })
17
-
18
- it('returns null for a non-existent key', async () => {
19
- const mem = new SharedMemory()
20
- expect(await mem.read('nope/nothing')).toBeNull()
21
- })
22
-
23
- // -------------------------------------------------------------------------
24
- // Namespace isolation
25
- // -------------------------------------------------------------------------
26
-
27
- it('isolates writes between agents', async () => {
28
- const mem = new SharedMemory()
29
- await mem.write('alice', 'plan', 'plan A')
30
- await mem.write('bob', 'plan', 'plan B')
31
-
32
- const alice = await mem.read('alice/plan')
33
- const bob = await mem.read('bob/plan')
34
- expect(alice!.value).toBe('plan A')
35
- expect(bob!.value).toBe('plan B')
36
- })
37
-
38
- it('listByAgent returns only that agent\'s entries', async () => {
39
- const mem = new SharedMemory()
40
- await mem.write('alice', 'a1', 'v1')
41
- await mem.write('alice', 'a2', 'v2')
42
- await mem.write('bob', 'b1', 'v3')
43
-
44
- const aliceEntries = await mem.listByAgent('alice')
45
- expect(aliceEntries).toHaveLength(2)
46
- expect(aliceEntries.every((e) => e.key.startsWith('alice/'))).toBe(true)
47
- })
48
-
49
- // -------------------------------------------------------------------------
50
- // Overwrite
51
- // -------------------------------------------------------------------------
52
-
53
- it('overwrites a value and preserves createdAt', async () => {
54
- const mem = new SharedMemory()
55
- await mem.write('agent', 'key', 'first')
56
- const first = await mem.read('agent/key')
57
-
58
- await mem.write('agent', 'key', 'second')
59
- const second = await mem.read('agent/key')
60
-
61
- expect(second!.value).toBe('second')
62
- expect(second!.createdAt.getTime()).toBe(first!.createdAt.getTime())
63
- })
64
-
65
- // -------------------------------------------------------------------------
66
- // Metadata
67
- // -------------------------------------------------------------------------
68
-
69
- it('stores metadata alongside the value', async () => {
70
- const mem = new SharedMemory()
71
- await mem.write('agent', 'key', 'val', { priority: 'high' })
72
-
73
- const entry = await mem.read('agent/key')
74
- expect(entry!.metadata).toMatchObject({ priority: 'high', agent: 'agent' })
75
- })
76
-
77
- // -------------------------------------------------------------------------
78
- // Summary
79
- // -------------------------------------------------------------------------
80
-
81
- it('returns empty string for an empty store', async () => {
82
- const mem = new SharedMemory()
83
- expect(await mem.getSummary()).toBe('')
84
- })
85
-
86
- it('produces a markdown summary grouped by agent', async () => {
87
- const mem = new SharedMemory()
88
- await mem.write('researcher', 'findings', 'result A')
89
- await mem.write('coder', 'plan', 'implement X')
90
-
91
- const summary = await mem.getSummary()
92
- expect(summary).toContain('## Shared Team Memory')
93
- expect(summary).toContain('### researcher')
94
- expect(summary).toContain('### coder')
95
- expect(summary).toContain('findings: result A')
96
- expect(summary).toContain('plan: implement X')
97
- })
98
-
99
- it('truncates long values in the summary', async () => {
100
- const mem = new SharedMemory()
101
- const longValue = 'x'.repeat(300)
102
- await mem.write('agent', 'big', longValue)
103
-
104
- const summary = await mem.getSummary()
105
- // Summary truncates at 200 chars → 197 + '…'
106
- expect(summary.length).toBeLessThan(longValue.length)
107
- expect(summary).toContain('…')
108
- })
109
-
110
- // -------------------------------------------------------------------------
111
- // listAll
112
- // -------------------------------------------------------------------------
113
-
114
- it('listAll returns entries from all agents', async () => {
115
- const mem = new SharedMemory()
116
- await mem.write('a', 'k1', 'v1')
117
- await mem.write('b', 'k2', 'v2')
118
-
119
- const all = await mem.listAll()
120
- expect(all).toHaveLength(2)
121
- })
122
- })