@open-multi-agent/core 1.4.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.
- package/LICENSE +21 -0
- package/README.md +373 -0
- package/dist/agent/agent.d.ts +153 -0
- package/dist/agent/agent.d.ts.map +1 -0
- package/dist/agent/agent.js +559 -0
- package/dist/agent/agent.js.map +1 -0
- package/dist/agent/loop-detector.d.ts +39 -0
- package/dist/agent/loop-detector.d.ts.map +1 -0
- package/dist/agent/loop-detector.js +122 -0
- package/dist/agent/loop-detector.js.map +1 -0
- package/dist/agent/pool.d.ts +158 -0
- package/dist/agent/pool.d.ts.map +1 -0
- package/dist/agent/pool.js +320 -0
- package/dist/agent/pool.js.map +1 -0
- package/dist/agent/runner.d.ts +242 -0
- package/dist/agent/runner.d.ts.map +1 -0
- package/dist/agent/runner.js +943 -0
- package/dist/agent/runner.js.map +1 -0
- package/dist/agent/structured-output.d.ts +33 -0
- package/dist/agent/structured-output.d.ts.map +1 -0
- package/dist/agent/structured-output.js +116 -0
- package/dist/agent/structured-output.js.map +1 -0
- package/dist/cli/oma.d.ts +30 -0
- package/dist/cli/oma.d.ts.map +1 -0
- package/dist/cli/oma.js +433 -0
- package/dist/cli/oma.js.map +1 -0
- package/dist/dashboard/layout-tasks.d.ts +23 -0
- package/dist/dashboard/layout-tasks.d.ts.map +1 -0
- package/dist/dashboard/layout-tasks.js +79 -0
- package/dist/dashboard/layout-tasks.js.map +1 -0
- package/dist/dashboard/render-team-run-dashboard.d.ts +11 -0
- package/dist/dashboard/render-team-run-dashboard.d.ts.map +1 -0
- package/dist/dashboard/render-team-run-dashboard.js +456 -0
- package/dist/dashboard/render-team-run-dashboard.js.map +1 -0
- package/dist/errors.d.ts +14 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +20 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +79 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +92 -0
- package/dist/index.js.map +1 -0
- package/dist/llm/adapter.d.ts +54 -0
- package/dist/llm/adapter.d.ts.map +1 -0
- package/dist/llm/adapter.js +101 -0
- package/dist/llm/adapter.js.map +1 -0
- package/dist/llm/anthropic.d.ts +57 -0
- package/dist/llm/anthropic.d.ts.map +1 -0
- package/dist/llm/anthropic.js +432 -0
- package/dist/llm/anthropic.js.map +1 -0
- package/dist/llm/azure-openai.d.ts +74 -0
- package/dist/llm/azure-openai.d.ts.map +1 -0
- package/dist/llm/azure-openai.js +267 -0
- package/dist/llm/azure-openai.js.map +1 -0
- package/dist/llm/bedrock.d.ts +41 -0
- package/dist/llm/bedrock.d.ts.map +1 -0
- package/dist/llm/bedrock.js +345 -0
- package/dist/llm/bedrock.js.map +1 -0
- package/dist/llm/copilot.d.ts +92 -0
- package/dist/llm/copilot.d.ts.map +1 -0
- package/dist/llm/copilot.js +433 -0
- package/dist/llm/copilot.js.map +1 -0
- package/dist/llm/deepseek.d.ts +21 -0
- package/dist/llm/deepseek.d.ts.map +1 -0
- package/dist/llm/deepseek.js +24 -0
- package/dist/llm/deepseek.js.map +1 -0
- package/dist/llm/gemini.d.ts +65 -0
- package/dist/llm/gemini.d.ts.map +1 -0
- package/dist/llm/gemini.js +427 -0
- package/dist/llm/gemini.js.map +1 -0
- package/dist/llm/grok.d.ts +21 -0
- package/dist/llm/grok.d.ts.map +1 -0
- package/dist/llm/grok.js +24 -0
- package/dist/llm/grok.js.map +1 -0
- package/dist/llm/minimax.d.ts +21 -0
- package/dist/llm/minimax.d.ts.map +1 -0
- package/dist/llm/minimax.js +24 -0
- package/dist/llm/minimax.js.map +1 -0
- package/dist/llm/openai-common.d.ts +65 -0
- package/dist/llm/openai-common.d.ts.map +1 -0
- package/dist/llm/openai-common.js +286 -0
- package/dist/llm/openai-common.js.map +1 -0
- package/dist/llm/openai.d.ts +63 -0
- package/dist/llm/openai.d.ts.map +1 -0
- package/dist/llm/openai.js +256 -0
- package/dist/llm/openai.js.map +1 -0
- package/dist/llm/qiniu.d.ts +21 -0
- package/dist/llm/qiniu.d.ts.map +1 -0
- package/dist/llm/qiniu.js +24 -0
- package/dist/llm/qiniu.js.map +1 -0
- package/dist/mcp.d.ts +3 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +2 -0
- package/dist/mcp.js.map +1 -0
- package/dist/memory/shared.d.ts +162 -0
- package/dist/memory/shared.d.ts.map +1 -0
- package/dist/memory/shared.js +294 -0
- package/dist/memory/shared.js.map +1 -0
- package/dist/memory/store.d.ts +72 -0
- package/dist/memory/store.d.ts.map +1 -0
- package/dist/memory/store.js +121 -0
- package/dist/memory/store.js.map +1 -0
- package/dist/orchestrator/orchestrator.d.ts +245 -0
- package/dist/orchestrator/orchestrator.d.ts.map +1 -0
- package/dist/orchestrator/orchestrator.js +1400 -0
- package/dist/orchestrator/orchestrator.js.map +1 -0
- package/dist/orchestrator/scheduler.d.ts +112 -0
- package/dist/orchestrator/scheduler.d.ts.map +1 -0
- package/dist/orchestrator/scheduler.js +256 -0
- package/dist/orchestrator/scheduler.js.map +1 -0
- package/dist/task/queue.d.ts +191 -0
- package/dist/task/queue.d.ts.map +1 -0
- package/dist/task/queue.js +408 -0
- package/dist/task/queue.js.map +1 -0
- package/dist/task/task.d.ts +90 -0
- package/dist/task/task.d.ts.map +1 -0
- package/dist/task/task.js +206 -0
- package/dist/task/task.js.map +1 -0
- package/dist/team/messaging.d.ts +106 -0
- package/dist/team/messaging.d.ts.map +1 -0
- package/dist/team/messaging.js +183 -0
- package/dist/team/messaging.js.map +1 -0
- package/dist/team/team.d.ts +141 -0
- package/dist/team/team.d.ts.map +1 -0
- package/dist/team/team.js +293 -0
- package/dist/team/team.js.map +1 -0
- package/dist/tool/built-in/bash.d.ts +12 -0
- package/dist/tool/built-in/bash.d.ts.map +1 -0
- package/dist/tool/built-in/bash.js +133 -0
- package/dist/tool/built-in/bash.js.map +1 -0
- package/dist/tool/built-in/delegate.d.ts +29 -0
- package/dist/tool/built-in/delegate.d.ts.map +1 -0
- package/dist/tool/built-in/delegate.js +92 -0
- package/dist/tool/built-in/delegate.js.map +1 -0
- package/dist/tool/built-in/file-edit.d.ts +14 -0
- package/dist/tool/built-in/file-edit.d.ts.map +1 -0
- package/dist/tool/built-in/file-edit.js +130 -0
- package/dist/tool/built-in/file-edit.js.map +1 -0
- package/dist/tool/built-in/file-read.d.ts +12 -0
- package/dist/tool/built-in/file-read.d.ts.map +1 -0
- package/dist/tool/built-in/file-read.js +82 -0
- package/dist/tool/built-in/file-read.js.map +1 -0
- package/dist/tool/built-in/file-write.d.ts +11 -0
- package/dist/tool/built-in/file-write.d.ts.map +1 -0
- package/dist/tool/built-in/file-write.js +70 -0
- package/dist/tool/built-in/file-write.js.map +1 -0
- package/dist/tool/built-in/fs-walk.d.ts +23 -0
- package/dist/tool/built-in/fs-walk.d.ts.map +1 -0
- package/dist/tool/built-in/fs-walk.js +78 -0
- package/dist/tool/built-in/fs-walk.js.map +1 -0
- package/dist/tool/built-in/glob.d.ts +12 -0
- package/dist/tool/built-in/glob.d.ts.map +1 -0
- package/dist/tool/built-in/glob.js +82 -0
- package/dist/tool/built-in/glob.js.map +1 -0
- package/dist/tool/built-in/grep.d.ts +15 -0
- package/dist/tool/built-in/grep.d.ts.map +1 -0
- package/dist/tool/built-in/grep.js +218 -0
- package/dist/tool/built-in/grep.js.map +1 -0
- package/dist/tool/built-in/index.d.ts +48 -0
- package/dist/tool/built-in/index.d.ts.map +1 -0
- package/dist/tool/built-in/index.js +56 -0
- package/dist/tool/built-in/index.js.map +1 -0
- package/dist/tool/executor.d.ts +100 -0
- package/dist/tool/executor.d.ts.map +1 -0
- package/dist/tool/executor.js +184 -0
- package/dist/tool/executor.js.map +1 -0
- package/dist/tool/framework.d.ts +167 -0
- package/dist/tool/framework.d.ts.map +1 -0
- package/dist/tool/framework.js +402 -0
- package/dist/tool/framework.js.map +1 -0
- package/dist/tool/mcp.d.ts +31 -0
- package/dist/tool/mcp.d.ts.map +1 -0
- package/dist/tool/mcp.js +175 -0
- package/dist/tool/mcp.js.map +1 -0
- package/dist/tool/text-tool-extractor.d.ts +32 -0
- package/dist/tool/text-tool-extractor.d.ts.map +1 -0
- package/dist/tool/text-tool-extractor.js +195 -0
- package/dist/tool/text-tool-extractor.js.map +1 -0
- package/dist/types.d.ts +916 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/keywords.d.ts +18 -0
- package/dist/utils/keywords.d.ts.map +1 -0
- package/dist/utils/keywords.js +32 -0
- package/dist/utils/keywords.js.map +1 -0
- package/dist/utils/semaphore.d.ts +49 -0
- package/dist/utils/semaphore.d.ts.map +1 -0
- package/dist/utils/semaphore.js +89 -0
- package/dist/utils/semaphore.js.map +1 -0
- package/dist/utils/tokens.d.ts +7 -0
- package/dist/utils/tokens.d.ts.map +1 -0
- package/dist/utils/tokens.js +30 -0
- package/dist/utils/tokens.js.map +1 -0
- package/dist/utils/trace.d.ts +12 -0
- package/dist/utils/trace.d.ts.map +1 -0
- package/dist/utils/trace.js +30 -0
- package/dist/utils/trace.js.map +1 -0
- package/docs/DECISIONS.md +49 -0
- package/docs/cli.md +265 -0
- package/docs/context-management.md +24 -0
- package/docs/featured-partner.md +28 -0
- package/docs/observability.md +56 -0
- package/docs/providers.md +78 -0
- package/docs/shared-memory.md +27 -0
- package/docs/tool-configuration.md +152 -0
- package/package.json +96 -0
|
@@ -0,0 +1,122 @@
|
|
|
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
|
+
// Helpers
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
/**
|
|
12
|
+
* Recursively sort object keys so that `{b:1, a:2}` and `{a:2, b:1}` produce
|
|
13
|
+
* the same JSON string.
|
|
14
|
+
*/
|
|
15
|
+
function sortKeys(value) {
|
|
16
|
+
if (value === null || typeof value !== 'object')
|
|
17
|
+
return value;
|
|
18
|
+
if (Array.isArray(value))
|
|
19
|
+
return value.map(sortKeys);
|
|
20
|
+
const sorted = {};
|
|
21
|
+
for (const key of Object.keys(value).sort()) {
|
|
22
|
+
sorted[key] = sortKeys(value[key]);
|
|
23
|
+
}
|
|
24
|
+
return sorted;
|
|
25
|
+
}
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// LoopDetector
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
export class LoopDetector {
|
|
30
|
+
maxRepeats;
|
|
31
|
+
windowSize;
|
|
32
|
+
toolSignatures = [];
|
|
33
|
+
textOutputs = [];
|
|
34
|
+
constructor(config = {}) {
|
|
35
|
+
this.maxRepeats = config.maxRepetitions ?? 3;
|
|
36
|
+
const requestedWindow = config.loopDetectionWindow ?? 4;
|
|
37
|
+
// Window must be >= threshold, otherwise detection can never trigger.
|
|
38
|
+
this.windowSize = Math.max(requestedWindow, this.maxRepeats);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Record a turn's tool calls. Returns detection info when a loop is found.
|
|
42
|
+
*/
|
|
43
|
+
recordToolCalls(blocks) {
|
|
44
|
+
if (blocks.length === 0)
|
|
45
|
+
return null;
|
|
46
|
+
const signature = this.computeToolSignature(blocks);
|
|
47
|
+
this.push(this.toolSignatures, signature);
|
|
48
|
+
const count = this.consecutiveRepeats(this.toolSignatures);
|
|
49
|
+
if (count >= this.maxRepeats) {
|
|
50
|
+
const names = blocks.map(b => b.name).join(', ');
|
|
51
|
+
return {
|
|
52
|
+
kind: 'tool_repetition',
|
|
53
|
+
repetitions: count,
|
|
54
|
+
detail: `Tool call "${names}" with identical arguments has repeated ` +
|
|
55
|
+
`${count} times consecutively. The agent appears to be stuck in a loop.`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Record a turn's text output. Returns detection info when a loop is found.
|
|
62
|
+
*/
|
|
63
|
+
recordText(text) {
|
|
64
|
+
const normalised = text.trim().replace(/\s+/g, ' ');
|
|
65
|
+
if (normalised.length === 0)
|
|
66
|
+
return null;
|
|
67
|
+
this.push(this.textOutputs, normalised);
|
|
68
|
+
const count = this.consecutiveRepeats(this.textOutputs);
|
|
69
|
+
if (count >= this.maxRepeats) {
|
|
70
|
+
return {
|
|
71
|
+
kind: 'text_repetition',
|
|
72
|
+
repetitions: count,
|
|
73
|
+
detail: `The agent has produced the same text response ${count} times ` +
|
|
74
|
+
`consecutively. It appears to be stuck in a loop.`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
// -------------------------------------------------------------------------
|
|
80
|
+
// Private
|
|
81
|
+
// -------------------------------------------------------------------------
|
|
82
|
+
/**
|
|
83
|
+
* Deterministic JSON signature for a set of tool calls.
|
|
84
|
+
* Sorts calls by name (for multi-tool turns) and keys within each input.
|
|
85
|
+
*/
|
|
86
|
+
computeToolSignature(blocks) {
|
|
87
|
+
const items = blocks
|
|
88
|
+
.map(b => ({ name: b.name, input: sortKeys(b.input) }))
|
|
89
|
+
.sort((a, b) => {
|
|
90
|
+
const cmp = a.name.localeCompare(b.name);
|
|
91
|
+
if (cmp !== 0)
|
|
92
|
+
return cmp;
|
|
93
|
+
return JSON.stringify(a.input).localeCompare(JSON.stringify(b.input));
|
|
94
|
+
});
|
|
95
|
+
return JSON.stringify(items);
|
|
96
|
+
}
|
|
97
|
+
/** Push an entry and trim the buffer to `windowSize`. */
|
|
98
|
+
push(buffer, entry) {
|
|
99
|
+
buffer.push(entry);
|
|
100
|
+
while (buffer.length > this.windowSize) {
|
|
101
|
+
buffer.shift();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Count how many consecutive identical entries exist at the tail of `buffer`.
|
|
106
|
+
* Returns 1 when the last entry is unique.
|
|
107
|
+
*/
|
|
108
|
+
consecutiveRepeats(buffer) {
|
|
109
|
+
if (buffer.length === 0)
|
|
110
|
+
return 0;
|
|
111
|
+
const last = buffer[buffer.length - 1];
|
|
112
|
+
let count = 0;
|
|
113
|
+
for (let i = buffer.length - 1; i >= 0; i--) {
|
|
114
|
+
if (buffer[i] === last)
|
|
115
|
+
count++;
|
|
116
|
+
else
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
return count;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=loop-detector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loop-detector.js","sourceRoot":"","sources":["../../src/agent/loop-detector.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,8EAA8E;AAC9E,UAAU;AACV,8EAA8E;AAE9E;;;GAGG;AACH,SAAS,QAAQ,CAAC,KAAc;IAC9B,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAA;IAC7D,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IACpD,MAAM,MAAM,GAA4B,EAAE,CAAA;IAC1C,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,KAAgC,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;QACvE,MAAM,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAE,KAAiC,CAAC,GAAG,CAAC,CAAC,CAAA;IACjE,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC;AAED,8EAA8E;AAC9E,eAAe;AACf,8EAA8E;AAE9E,MAAM,OAAO,YAAY;IACN,UAAU,CAAQ;IAClB,UAAU,CAAQ;IAElB,cAAc,GAAa,EAAE,CAAA;IAC7B,WAAW,GAAa,EAAE,CAAA;IAE3C,YAAY,SAA8B,EAAE;QAC1C,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,cAAc,IAAI,CAAC,CAAA;QAC5C,MAAM,eAAe,GAAG,MAAM,CAAC,mBAAmB,IAAI,CAAC,CAAA;QACvD,sEAAsE;QACtE,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,eAAe,EAAE,IAAI,CAAC,UAAU,CAAC,CAAA;IAC9D,CAAC;IAED;;OAEG;IACH,eAAe,CACb,MAAuE;QAEvE,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAA;QAEpC,MAAM,SAAS,GAAG,IAAI,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAA;QACnD,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,SAAS,CAAC,CAAA;QAEzC,MAAM,KAAK,GAAG,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,cAAc,CAAC,CAAA;QAC1D,IAAI,KAAK,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YAC7B,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YAChD,OAAO;gBACL,IAAI,EAAE,iBAAiB;gBACvB,WAAW,EAAE,KAAK;gBAClB,MAAM,EACJ,cAAc,KAAK,0CAA0C;oBAC7D,GAAG,KAAK,gEAAgE;aAC3E,CAAA;QACH,CAAC;QACD,OAAO,IAAI,CAAA;IACb,CAAC;IAED;;OAEG;IACH,UAAU,CAAC,IAAY;QACrB,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;QACnD,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAA;QAExC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,UAAU,CAAC,CAAA;QAEvC,MAAM,KAAK,GAAG,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;QACvD,IAAI,KAAK,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YAC7B,OAAO;gBACL,IAAI,EAAE,iBAAiB;gBACvB,WAAW,EAAE,KAAK;gBAClB,MAAM,EACJ,iDAAiD,KAAK,SAAS;oBAC/D,kDAAkD;aACrD,CAAA;QACH,CAAC;QACD,OAAO,IAAI,CAAA;IACb,CAAC;IAED,4EAA4E;IAC5E,UAAU;IACV,4EAA4E;IAE5E;;;OAGG;IACK,oBAAoB,CAC1B,MAAuE;QAEvE,MAAM,KAAK,GAAG,MAAM;aACjB,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;aACtD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;YACb,MAAM,GAAG,GAAG,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;YACxC,IAAI,GAAG,KAAK,CAAC;gBAAE,OAAO,GAAG,CAAA;YACzB,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,aAAa,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAA;QACvE,CAAC,CAAC,CAAA;QACJ,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;IAC9B,CAAC;IAED,yDAAyD;IACjD,IAAI,CAAC,MAAgB,EAAE,KAAa;QAC1C,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAClB,OAAO,MAAM,CAAC,MAAM,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC;YACvC,MAAM,CAAC,KAAK,EAAE,CAAA;QAChB,CAAC;IACH,CAAC;IAED;;;OAGG;IACK,kBAAkB,CAAC,MAAgB;QACzC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,CAAC,CAAA;QACjC,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;QACtC,IAAI,KAAK,GAAG,CAAC,CAAA;QACb,KAAK,IAAI,CAAC,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YAC5C,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,IAAI;gBAAE,KAAK,EAAE,CAAA;;gBAC1B,MAAK;QACZ,CAAC;QACD,OAAO,KAAK,CAAA;IACd,CAAC;CACF"}
|
|
@@ -0,0 +1,158 @@
|
|
|
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
|
+
import type { AgentRunResult, StreamEvent } from '../types.js';
|
|
23
|
+
import type { RunOptions } from './runner.js';
|
|
24
|
+
import type { Agent } from './agent.js';
|
|
25
|
+
export { Semaphore } from '../utils/semaphore.js';
|
|
26
|
+
export interface PoolStatus {
|
|
27
|
+
/** Total number of agents registered in the pool. */
|
|
28
|
+
readonly total: number;
|
|
29
|
+
/** Agents currently in `idle` state. */
|
|
30
|
+
readonly idle: number;
|
|
31
|
+
/** Agents currently in `running` state. */
|
|
32
|
+
readonly running: number;
|
|
33
|
+
/** Agents currently in `completed` state. */
|
|
34
|
+
readonly completed: number;
|
|
35
|
+
/** Agents currently in `error` state. */
|
|
36
|
+
readonly error: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Registry and scheduler for a collection of {@link Agent} instances.
|
|
40
|
+
*
|
|
41
|
+
* Thread-safety note: Node.js is single-threaded, so the semaphore approach
|
|
42
|
+
* is safe — no atomics or mutex primitives are needed. The semaphore gates
|
|
43
|
+
* concurrent async operations, not CPU threads.
|
|
44
|
+
*/
|
|
45
|
+
export declare class AgentPool {
|
|
46
|
+
private readonly maxConcurrency;
|
|
47
|
+
private readonly agents;
|
|
48
|
+
private readonly semaphore;
|
|
49
|
+
/**
|
|
50
|
+
* Per-agent mutex (Semaphore(1)) to serialize concurrent runs on the same
|
|
51
|
+
* Agent instance. Without this, two tasks assigned to the same agent could
|
|
52
|
+
* race on mutable instance state (`status`, `messages`, `tokenUsage`).
|
|
53
|
+
*
|
|
54
|
+
* @see https://github.com/anthropics/open-multi-agent/issues/72
|
|
55
|
+
*/
|
|
56
|
+
private readonly agentLocks;
|
|
57
|
+
/** Cursor used by `runAny` for round-robin dispatch. */
|
|
58
|
+
private roundRobinIndex;
|
|
59
|
+
/**
|
|
60
|
+
* @param maxConcurrency - Maximum number of agent runs allowed at the same
|
|
61
|
+
* time across the whole pool. Defaults to `5`.
|
|
62
|
+
*/
|
|
63
|
+
constructor(maxConcurrency?: number);
|
|
64
|
+
/**
|
|
65
|
+
* Pool semaphore slots not currently held (`maxConcurrency - active`).
|
|
66
|
+
* Used to avoid deadlocks when a nested `run()` would wait forever for a slot
|
|
67
|
+
* held by the parent run. Best-effort only if multiple nested runs start in
|
|
68
|
+
* parallel after the same synchronous check.
|
|
69
|
+
*/
|
|
70
|
+
get availableRunSlots(): number;
|
|
71
|
+
/**
|
|
72
|
+
* Register an agent with the pool.
|
|
73
|
+
*
|
|
74
|
+
* @throws {Error} If an agent with the same name is already registered.
|
|
75
|
+
*/
|
|
76
|
+
add(agent: Agent): void;
|
|
77
|
+
/**
|
|
78
|
+
* Unregister an agent by name.
|
|
79
|
+
*
|
|
80
|
+
* @throws {Error} If the agent is not found.
|
|
81
|
+
*/
|
|
82
|
+
remove(name: string): void;
|
|
83
|
+
/**
|
|
84
|
+
* Retrieve a registered agent by name, or `undefined` if not found.
|
|
85
|
+
*/
|
|
86
|
+
get(name: string): Agent | undefined;
|
|
87
|
+
/**
|
|
88
|
+
* Return all registered agents in insertion order.
|
|
89
|
+
*/
|
|
90
|
+
list(): Agent[];
|
|
91
|
+
/**
|
|
92
|
+
* Run a single prompt on the named agent, respecting the pool concurrency
|
|
93
|
+
* limit.
|
|
94
|
+
*
|
|
95
|
+
* @throws {Error} If the agent name is not found.
|
|
96
|
+
*/
|
|
97
|
+
run(agentName: string, prompt: string, runOptions?: Partial<RunOptions>, streamCallback?: (event: StreamEvent) => void): Promise<AgentRunResult>;
|
|
98
|
+
/**
|
|
99
|
+
* Run a prompt on a caller-supplied Agent instance, acquiring only the pool
|
|
100
|
+
* semaphore — no per-agent lock, no registry lookup.
|
|
101
|
+
*
|
|
102
|
+
* Designed for delegation: each delegated call should use a **fresh** Agent
|
|
103
|
+
* instance (matching `delegate_to_agent`'s "runs in a fresh conversation"
|
|
104
|
+
* semantics), so the per-agent mutex used by {@link run} would be dead
|
|
105
|
+
* weight and, worse, a deadlock vector for mutual delegation (A→B while
|
|
106
|
+
* B→A, each caller holding its own `run`'s agent lock).
|
|
107
|
+
*
|
|
108
|
+
* The caller is responsible for constructing the Agent; {@link AgentPool}
|
|
109
|
+
* does not register or track it.
|
|
110
|
+
*/
|
|
111
|
+
runEphemeral(agent: Agent, prompt: string, runOptions?: Partial<RunOptions>): Promise<AgentRunResult>;
|
|
112
|
+
/**
|
|
113
|
+
* Run prompts on multiple agents in parallel, subject to the concurrency
|
|
114
|
+
* cap set at construction time.
|
|
115
|
+
*
|
|
116
|
+
* Results are returned as a `Map<agentName, AgentRunResult>`. If two tasks
|
|
117
|
+
* target the same agent name, the map will only contain the last result.
|
|
118
|
+
* Use unique agent names or run tasks sequentially in that case.
|
|
119
|
+
*
|
|
120
|
+
* @param tasks - Array of `{ agent, prompt }` descriptors.
|
|
121
|
+
*/
|
|
122
|
+
runParallel(tasks: ReadonlyArray<{
|
|
123
|
+
readonly agent: string;
|
|
124
|
+
readonly prompt: string;
|
|
125
|
+
}>): Promise<Map<string, AgentRunResult>>;
|
|
126
|
+
/**
|
|
127
|
+
* Run a prompt on the "best available" agent using round-robin selection.
|
|
128
|
+
*
|
|
129
|
+
* Agents are selected in insertion order, cycling back to the start. The
|
|
130
|
+
* concurrency limit is still enforced — if the selected agent is busy the
|
|
131
|
+
* call will queue via the semaphore.
|
|
132
|
+
*
|
|
133
|
+
* @throws {Error} If the pool is empty.
|
|
134
|
+
*/
|
|
135
|
+
runAny(prompt: string): Promise<AgentRunResult>;
|
|
136
|
+
/**
|
|
137
|
+
* Snapshot of how many agents are in each lifecycle state.
|
|
138
|
+
*/
|
|
139
|
+
getStatus(): PoolStatus;
|
|
140
|
+
/**
|
|
141
|
+
* Reset all agents in the pool.
|
|
142
|
+
*
|
|
143
|
+
* Clears their conversation histories and returns them to `idle` state.
|
|
144
|
+
* Does not remove agents from the pool.
|
|
145
|
+
*
|
|
146
|
+
* Async for forward compatibility — shutdown may need to perform async
|
|
147
|
+
* cleanup (e.g. draining in-flight requests) in future versions.
|
|
148
|
+
*/
|
|
149
|
+
shutdown(): Promise<void>;
|
|
150
|
+
private requireAgent;
|
|
151
|
+
/**
|
|
152
|
+
* Build a failure {@link AgentRunResult} from a caught rejection reason.
|
|
153
|
+
* This keeps `runParallel` returning a complete map even when individual
|
|
154
|
+
* agents fail.
|
|
155
|
+
*/
|
|
156
|
+
private errorResult;
|
|
157
|
+
}
|
|
158
|
+
//# sourceMappingURL=pool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pool.d.ts","sourceRoot":"","sources":["../../src/agent/pool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAC9D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAC7C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAGvC,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AAMjD,MAAM,WAAW,UAAU;IACzB,qDAAqD;IACrD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,wCAAwC;IACxC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,2CAA2C;IAC3C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,6CAA6C;IAC7C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,yCAAyC;IACzC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CACvB;AAMD;;;;;;GAMG;AACH,qBAAa,SAAS;IAkBR,OAAO,CAAC,QAAQ,CAAC,cAAc;IAjB3C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgC;IACvD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAW;IACrC;;;;;;OAMG;IACH,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAoC;IAC/D,wDAAwD;IACxD,OAAO,CAAC,eAAe,CAAI;IAE3B;;;OAGG;gBAC0B,cAAc,GAAE,MAAU;IAIvD;;;;;OAKG;IACH,IAAI,iBAAiB,IAAI,MAAM,CAE9B;IAMD;;;;OAIG;IACH,GAAG,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAWvB;;;;OAIG;IACH,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAQ1B;;OAEG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,GAAG,SAAS;IAIpC;;OAEG;IACH,IAAI,IAAI,KAAK,EAAE;IAQf;;;;;OAKG;IACG,GAAG,CACP,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,UAAU,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,EAChC,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,GAC5C,OAAO,CAAC,cAAc,CAAC;IAyC1B;;;;;;;;;;;;OAYG;IACG,YAAY,CAChB,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,MAAM,EACd,UAAU,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,GAC/B,OAAO,CAAC,cAAc,CAAC;IAS1B;;;;;;;;;OASG;IAEG,WAAW,CACf,KAAK,EAAE,aAAa,CAAC;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,GACxE,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IA2BvC;;;;;;;;OAQG;IAEG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IA8BrD;;OAEG;IACH,SAAS,IAAI,UAAU;IAsBvB;;;;;;;;OAQG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAU/B,OAAO,CAAC,YAAY;IAWpB;;;;OAIG;IACH,OAAO,CAAC,WAAW;CAUpB"}
|
|
@@ -0,0 +1,320 @@
|
|
|
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
|
+
import { Semaphore } from '../utils/semaphore.js';
|
|
23
|
+
export { Semaphore } from '../utils/semaphore.js';
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// AgentPool
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
/**
|
|
28
|
+
* Registry and scheduler for a collection of {@link Agent} instances.
|
|
29
|
+
*
|
|
30
|
+
* Thread-safety note: Node.js is single-threaded, so the semaphore approach
|
|
31
|
+
* is safe — no atomics or mutex primitives are needed. The semaphore gates
|
|
32
|
+
* concurrent async operations, not CPU threads.
|
|
33
|
+
*/
|
|
34
|
+
export class AgentPool {
|
|
35
|
+
maxConcurrency;
|
|
36
|
+
agents = new Map();
|
|
37
|
+
semaphore;
|
|
38
|
+
/**
|
|
39
|
+
* Per-agent mutex (Semaphore(1)) to serialize concurrent runs on the same
|
|
40
|
+
* Agent instance. Without this, two tasks assigned to the same agent could
|
|
41
|
+
* race on mutable instance state (`status`, `messages`, `tokenUsage`).
|
|
42
|
+
*
|
|
43
|
+
* @see https://github.com/anthropics/open-multi-agent/issues/72
|
|
44
|
+
*/
|
|
45
|
+
agentLocks = new Map();
|
|
46
|
+
/** Cursor used by `runAny` for round-robin dispatch. */
|
|
47
|
+
roundRobinIndex = 0;
|
|
48
|
+
/**
|
|
49
|
+
* @param maxConcurrency - Maximum number of agent runs allowed at the same
|
|
50
|
+
* time across the whole pool. Defaults to `5`.
|
|
51
|
+
*/
|
|
52
|
+
constructor(maxConcurrency = 5) {
|
|
53
|
+
this.maxConcurrency = maxConcurrency;
|
|
54
|
+
this.semaphore = new Semaphore(maxConcurrency);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Pool semaphore slots not currently held (`maxConcurrency - active`).
|
|
58
|
+
* Used to avoid deadlocks when a nested `run()` would wait forever for a slot
|
|
59
|
+
* held by the parent run. Best-effort only if multiple nested runs start in
|
|
60
|
+
* parallel after the same synchronous check.
|
|
61
|
+
*/
|
|
62
|
+
get availableRunSlots() {
|
|
63
|
+
return this.maxConcurrency - this.semaphore.active;
|
|
64
|
+
}
|
|
65
|
+
// -------------------------------------------------------------------------
|
|
66
|
+
// Registry operations
|
|
67
|
+
// -------------------------------------------------------------------------
|
|
68
|
+
/**
|
|
69
|
+
* Register an agent with the pool.
|
|
70
|
+
*
|
|
71
|
+
* @throws {Error} If an agent with the same name is already registered.
|
|
72
|
+
*/
|
|
73
|
+
add(agent) {
|
|
74
|
+
if (this.agents.has(agent.name)) {
|
|
75
|
+
throw new Error(`AgentPool: agent '${agent.name}' is already registered. ` +
|
|
76
|
+
`Call remove('${agent.name}') before re-adding.`);
|
|
77
|
+
}
|
|
78
|
+
this.agents.set(agent.name, agent);
|
|
79
|
+
this.agentLocks.set(agent.name, new Semaphore(1));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Unregister an agent by name.
|
|
83
|
+
*
|
|
84
|
+
* @throws {Error} If the agent is not found.
|
|
85
|
+
*/
|
|
86
|
+
remove(name) {
|
|
87
|
+
if (!this.agents.has(name)) {
|
|
88
|
+
throw new Error(`AgentPool: agent '${name}' is not registered.`);
|
|
89
|
+
}
|
|
90
|
+
this.agents.delete(name);
|
|
91
|
+
this.agentLocks.delete(name);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Retrieve a registered agent by name, or `undefined` if not found.
|
|
95
|
+
*/
|
|
96
|
+
get(name) {
|
|
97
|
+
return this.agents.get(name);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Return all registered agents in insertion order.
|
|
101
|
+
*/
|
|
102
|
+
list() {
|
|
103
|
+
return Array.from(this.agents.values());
|
|
104
|
+
}
|
|
105
|
+
// -------------------------------------------------------------------------
|
|
106
|
+
// Execution API
|
|
107
|
+
// -------------------------------------------------------------------------
|
|
108
|
+
/**
|
|
109
|
+
* Run a single prompt on the named agent, respecting the pool concurrency
|
|
110
|
+
* limit.
|
|
111
|
+
*
|
|
112
|
+
* @throws {Error} If the agent name is not found.
|
|
113
|
+
*/
|
|
114
|
+
async run(agentName, prompt, runOptions, streamCallback) {
|
|
115
|
+
const agent = this.requireAgent(agentName);
|
|
116
|
+
const agentLock = this.agentLocks.get(agentName);
|
|
117
|
+
// Acquire per-agent lock first so the second call for the same agent waits
|
|
118
|
+
// here without consuming a pool slot. Then acquire the pool semaphore.
|
|
119
|
+
await agentLock.acquire();
|
|
120
|
+
try {
|
|
121
|
+
await this.semaphore.acquire();
|
|
122
|
+
try {
|
|
123
|
+
if (streamCallback) {
|
|
124
|
+
let result = null;
|
|
125
|
+
for await (const event of agent.stream(prompt, runOptions)) {
|
|
126
|
+
try {
|
|
127
|
+
streamCallback(event);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// Streaming callback must not affect execution; mirrors the
|
|
131
|
+
// observability contract of `emitTrace`. A throwing callback
|
|
132
|
+
// here would otherwise be caught by `executeWithRetry` and
|
|
133
|
+
// burn another LLM call on every retry until exhausted.
|
|
134
|
+
}
|
|
135
|
+
if (event.type === 'done')
|
|
136
|
+
result = event.data;
|
|
137
|
+
if (event.type === 'error')
|
|
138
|
+
throw event.data;
|
|
139
|
+
}
|
|
140
|
+
return result ?? {
|
|
141
|
+
success: false,
|
|
142
|
+
output: '',
|
|
143
|
+
messages: [],
|
|
144
|
+
tokenUsage: { input_tokens: 0, output_tokens: 0 },
|
|
145
|
+
toolCalls: [],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return await agent.run(prompt, runOptions);
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
this.semaphore.release();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
agentLock.release();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Run a prompt on a caller-supplied Agent instance, acquiring only the pool
|
|
160
|
+
* semaphore — no per-agent lock, no registry lookup.
|
|
161
|
+
*
|
|
162
|
+
* Designed for delegation: each delegated call should use a **fresh** Agent
|
|
163
|
+
* instance (matching `delegate_to_agent`'s "runs in a fresh conversation"
|
|
164
|
+
* semantics), so the per-agent mutex used by {@link run} would be dead
|
|
165
|
+
* weight and, worse, a deadlock vector for mutual delegation (A→B while
|
|
166
|
+
* B→A, each caller holding its own `run`'s agent lock).
|
|
167
|
+
*
|
|
168
|
+
* The caller is responsible for constructing the Agent; {@link AgentPool}
|
|
169
|
+
* does not register or track it.
|
|
170
|
+
*/
|
|
171
|
+
async runEphemeral(agent, prompt, runOptions) {
|
|
172
|
+
await this.semaphore.acquire();
|
|
173
|
+
try {
|
|
174
|
+
return await agent.run(prompt, runOptions);
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
this.semaphore.release();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Run prompts on multiple agents in parallel, subject to the concurrency
|
|
182
|
+
* cap set at construction time.
|
|
183
|
+
*
|
|
184
|
+
* Results are returned as a `Map<agentName, AgentRunResult>`. If two tasks
|
|
185
|
+
* target the same agent name, the map will only contain the last result.
|
|
186
|
+
* Use unique agent names or run tasks sequentially in that case.
|
|
187
|
+
*
|
|
188
|
+
* @param tasks - Array of `{ agent, prompt }` descriptors.
|
|
189
|
+
*/
|
|
190
|
+
// TODO(#18): accept RunOptions per task to forward trace context
|
|
191
|
+
async runParallel(tasks) {
|
|
192
|
+
const resultMap = new Map();
|
|
193
|
+
const settledResults = await Promise.allSettled(tasks.map(async (task) => {
|
|
194
|
+
const result = await this.run(task.agent, task.prompt);
|
|
195
|
+
return { name: task.agent, result };
|
|
196
|
+
}));
|
|
197
|
+
for (const settled of settledResults) {
|
|
198
|
+
if (settled.status === 'fulfilled') {
|
|
199
|
+
resultMap.set(settled.value.name, settled.value.result);
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
// A rejected run is surfaced as an error AgentRunResult so the caller
|
|
203
|
+
// sees it in the map rather than needing to catch Promise.allSettled.
|
|
204
|
+
// We cannot know the agent name from the rejection alone — find it via
|
|
205
|
+
// the original task list index.
|
|
206
|
+
const idx = settledResults.indexOf(settled);
|
|
207
|
+
const agentName = tasks[idx]?.agent ?? 'unknown';
|
|
208
|
+
resultMap.set(agentName, this.errorResult(settled.reason));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return resultMap;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Run a prompt on the "best available" agent using round-robin selection.
|
|
215
|
+
*
|
|
216
|
+
* Agents are selected in insertion order, cycling back to the start. The
|
|
217
|
+
* concurrency limit is still enforced — if the selected agent is busy the
|
|
218
|
+
* call will queue via the semaphore.
|
|
219
|
+
*
|
|
220
|
+
* @throws {Error} If the pool is empty.
|
|
221
|
+
*/
|
|
222
|
+
// TODO(#18): accept RunOptions to forward trace context
|
|
223
|
+
async runAny(prompt) {
|
|
224
|
+
const allAgents = this.list();
|
|
225
|
+
if (allAgents.length === 0) {
|
|
226
|
+
throw new Error('AgentPool: cannot call runAny on an empty pool.');
|
|
227
|
+
}
|
|
228
|
+
// Wrap the index to keep it in bounds even if agents were removed.
|
|
229
|
+
this.roundRobinIndex = this.roundRobinIndex % allAgents.length;
|
|
230
|
+
const agent = allAgents[this.roundRobinIndex];
|
|
231
|
+
this.roundRobinIndex = (this.roundRobinIndex + 1) % allAgents.length;
|
|
232
|
+
const agentLock = this.agentLocks.get(agent.name);
|
|
233
|
+
await agentLock.acquire();
|
|
234
|
+
try {
|
|
235
|
+
await this.semaphore.acquire();
|
|
236
|
+
try {
|
|
237
|
+
return await agent.run(prompt);
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
this.semaphore.release();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
agentLock.release();
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// -------------------------------------------------------------------------
|
|
248
|
+
// Observability
|
|
249
|
+
// -------------------------------------------------------------------------
|
|
250
|
+
/**
|
|
251
|
+
* Snapshot of how many agents are in each lifecycle state.
|
|
252
|
+
*/
|
|
253
|
+
getStatus() {
|
|
254
|
+
let idle = 0;
|
|
255
|
+
let running = 0;
|
|
256
|
+
let completed = 0;
|
|
257
|
+
let error = 0;
|
|
258
|
+
for (const agent of this.agents.values()) {
|
|
259
|
+
switch (agent.getState().status) {
|
|
260
|
+
case 'idle':
|
|
261
|
+
idle++;
|
|
262
|
+
break;
|
|
263
|
+
case 'running':
|
|
264
|
+
running++;
|
|
265
|
+
break;
|
|
266
|
+
case 'completed':
|
|
267
|
+
completed++;
|
|
268
|
+
break;
|
|
269
|
+
case 'error':
|
|
270
|
+
error++;
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return { total: this.agents.size, idle, running, completed, error };
|
|
275
|
+
}
|
|
276
|
+
// -------------------------------------------------------------------------
|
|
277
|
+
// Lifecycle
|
|
278
|
+
// -------------------------------------------------------------------------
|
|
279
|
+
/**
|
|
280
|
+
* Reset all agents in the pool.
|
|
281
|
+
*
|
|
282
|
+
* Clears their conversation histories and returns them to `idle` state.
|
|
283
|
+
* Does not remove agents from the pool.
|
|
284
|
+
*
|
|
285
|
+
* Async for forward compatibility — shutdown may need to perform async
|
|
286
|
+
* cleanup (e.g. draining in-flight requests) in future versions.
|
|
287
|
+
*/
|
|
288
|
+
async shutdown() {
|
|
289
|
+
for (const agent of this.agents.values()) {
|
|
290
|
+
agent.reset();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
// -------------------------------------------------------------------------
|
|
294
|
+
// Private helpers
|
|
295
|
+
// -------------------------------------------------------------------------
|
|
296
|
+
requireAgent(name) {
|
|
297
|
+
const agent = this.agents.get(name);
|
|
298
|
+
if (agent === undefined) {
|
|
299
|
+
throw new Error(`AgentPool: agent '${name}' is not registered. ` +
|
|
300
|
+
`Registered agents: [${Array.from(this.agents.keys()).join(', ')}]`);
|
|
301
|
+
}
|
|
302
|
+
return agent;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Build a failure {@link AgentRunResult} from a caught rejection reason.
|
|
306
|
+
* This keeps `runParallel` returning a complete map even when individual
|
|
307
|
+
* agents fail.
|
|
308
|
+
*/
|
|
309
|
+
errorResult(reason) {
|
|
310
|
+
const message = reason instanceof Error ? reason.message : String(reason);
|
|
311
|
+
return {
|
|
312
|
+
success: false,
|
|
313
|
+
output: message,
|
|
314
|
+
messages: [],
|
|
315
|
+
tokenUsage: { input_tokens: 0, output_tokens: 0 },
|
|
316
|
+
toolCalls: [],
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
//# sourceMappingURL=pool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pool.js","sourceRoot":"","sources":["../../src/agent/pool.ts"],"names":[],"mappings":"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"}
|