@renseiai/agentfactory 0.8.7 → 0.8.9
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/dist/src/config/index.d.ts +1 -1
- package/dist/src/config/index.d.ts.map +1 -1
- package/dist/src/config/index.js +1 -1
- package/dist/src/config/repository-config.d.ts +37 -0
- package/dist/src/config/repository-config.d.ts.map +1 -1
- package/dist/src/config/repository-config.js +47 -0
- package/dist/src/config/repository-config.test.js +140 -1
- package/dist/src/governor/decision-engine.d.ts +3 -0
- package/dist/src/governor/decision-engine.d.ts.map +1 -1
- package/dist/src/governor/decision-engine.js +11 -0
- package/dist/src/governor/decision-engine.test.js +33 -0
- package/dist/src/governor/event-types.d.ts +18 -1
- package/dist/src/governor/event-types.d.ts.map +1 -1
- package/dist/src/governor/event-types.js +4 -0
- package/dist/src/governor/governor-types.d.ts +1 -1
- package/dist/src/governor/governor-types.d.ts.map +1 -1
- package/dist/src/governor/governor.d.ts +17 -1
- package/dist/src/governor/governor.d.ts.map +1 -1
- package/dist/src/governor/governor.js +112 -1
- package/dist/src/governor/governor.test.js +155 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/merge-queue/adapters/github-native.d.ts +22 -0
- package/dist/src/merge-queue/adapters/github-native.d.ts.map +1 -0
- package/dist/src/merge-queue/adapters/github-native.js +243 -0
- package/dist/src/merge-queue/adapters/github-native.test.d.ts +2 -0
- package/dist/src/merge-queue/adapters/github-native.test.d.ts.map +1 -0
- package/dist/src/merge-queue/adapters/github-native.test.js +384 -0
- package/dist/src/merge-queue/index.d.ts +18 -0
- package/dist/src/merge-queue/index.d.ts.map +1 -0
- package/dist/src/merge-queue/index.js +28 -0
- package/dist/src/merge-queue/merge-queue.integration.test.d.ts +2 -0
- package/dist/src/merge-queue/merge-queue.integration.test.d.ts.map +1 -0
- package/dist/src/merge-queue/merge-queue.integration.test.js +128 -0
- package/dist/src/merge-queue/types.d.ts +48 -0
- package/dist/src/merge-queue/types.d.ts.map +1 -0
- package/dist/src/merge-queue/types.js +8 -0
- package/dist/src/orchestrator/artifact-tracker.d.ts +93 -0
- package/dist/src/orchestrator/artifact-tracker.d.ts.map +1 -0
- package/dist/src/orchestrator/artifact-tracker.js +235 -0
- package/dist/src/orchestrator/artifact-tracker.test.d.ts +2 -0
- package/dist/src/orchestrator/artifact-tracker.test.d.ts.map +1 -0
- package/dist/src/orchestrator/artifact-tracker.test.js +189 -0
- package/dist/src/orchestrator/context-manager.d.ts +72 -0
- package/dist/src/orchestrator/context-manager.d.ts.map +1 -0
- package/dist/src/orchestrator/context-manager.js +120 -0
- package/dist/src/orchestrator/context-manager.test.d.ts +2 -0
- package/dist/src/orchestrator/context-manager.test.d.ts.map +1 -0
- package/dist/src/orchestrator/context-manager.test.js +137 -0
- package/dist/src/orchestrator/index.d.ts +8 -2
- package/dist/src/orchestrator/index.d.ts.map +1 -1
- package/dist/src/orchestrator/index.js +8 -1
- package/dist/src/orchestrator/issue-tracker-client.d.ts +4 -0
- package/dist/src/orchestrator/issue-tracker-client.d.ts.map +1 -1
- package/dist/src/orchestrator/orchestrator.d.ts +12 -0
- package/dist/src/orchestrator/orchestrator.d.ts.map +1 -1
- package/dist/src/orchestrator/orchestrator.js +282 -2
- package/dist/src/orchestrator/parse-work-result.d.ts.map +1 -1
- package/dist/src/orchestrator/parse-work-result.js +6 -0
- package/dist/src/orchestrator/parse-work-result.test.js +19 -0
- package/dist/src/orchestrator/state-recovery.d.ts +21 -2
- package/dist/src/orchestrator/state-recovery.d.ts.map +1 -1
- package/dist/src/orchestrator/state-recovery.js +54 -2
- package/dist/src/orchestrator/state-recovery.test.js +106 -2
- package/dist/src/orchestrator/state-types.d.ts +62 -0
- package/dist/src/orchestrator/state-types.d.ts.map +1 -1
- package/dist/src/orchestrator/state-types.js +5 -1
- package/dist/src/orchestrator/summary-builder.d.ts +47 -0
- package/dist/src/orchestrator/summary-builder.d.ts.map +1 -0
- package/dist/src/orchestrator/summary-builder.js +240 -0
- package/dist/src/orchestrator/summary-builder.test.d.ts +2 -0
- package/dist/src/orchestrator/summary-builder.test.d.ts.map +1 -0
- package/dist/src/orchestrator/summary-builder.test.js +236 -0
- package/dist/src/orchestrator/types.d.ts +2 -0
- package/dist/src/orchestrator/types.d.ts.map +1 -1
- package/dist/src/orchestrator/work-types.d.ts +1 -1
- package/dist/src/orchestrator/work-types.d.ts.map +1 -1
- package/dist/src/providers/index.d.ts +64 -1
- package/dist/src/providers/index.d.ts.map +1 -1
- package/dist/src/providers/index.js +132 -1
- package/dist/src/providers/index.test.js +340 -2
- package/dist/src/routing/index.d.ts +7 -0
- package/dist/src/routing/index.d.ts.map +1 -0
- package/dist/src/routing/index.js +6 -0
- package/dist/src/routing/observation-recorder.d.ts +19 -0
- package/dist/src/routing/observation-recorder.d.ts.map +1 -0
- package/dist/src/routing/observation-recorder.js +73 -0
- package/dist/src/routing/observation-recorder.test.d.ts +2 -0
- package/dist/src/routing/observation-recorder.test.d.ts.map +1 -0
- package/dist/src/routing/observation-recorder.test.js +322 -0
- package/dist/src/routing/observation-store.d.ts +40 -0
- package/dist/src/routing/observation-store.d.ts.map +1 -0
- package/dist/src/routing/observation-store.js +1 -0
- package/dist/src/routing/observation-store.test.d.ts +2 -0
- package/dist/src/routing/observation-store.test.d.ts.map +1 -0
- package/dist/src/routing/observation-store.test.js +138 -0
- package/dist/src/routing/posterior-store.d.ts +12 -0
- package/dist/src/routing/posterior-store.d.ts.map +1 -0
- package/dist/src/routing/posterior-store.js +13 -0
- package/dist/src/routing/posterior-store.test.d.ts +2 -0
- package/dist/src/routing/posterior-store.test.d.ts.map +1 -0
- package/dist/src/routing/posterior-store.test.js +37 -0
- package/dist/src/routing/reward.d.ts +16 -0
- package/dist/src/routing/reward.d.ts.map +1 -0
- package/dist/src/routing/reward.js +29 -0
- package/dist/src/routing/reward.test.d.ts +2 -0
- package/dist/src/routing/reward.test.d.ts.map +1 -0
- package/dist/src/routing/reward.test.js +210 -0
- package/dist/src/routing/routing-engine.d.ts +20 -0
- package/dist/src/routing/routing-engine.d.ts.map +1 -0
- package/dist/src/routing/routing-engine.js +113 -0
- package/dist/src/routing/routing-engine.test.d.ts +2 -0
- package/dist/src/routing/routing-engine.test.d.ts.map +1 -0
- package/dist/src/routing/routing-engine.test.js +310 -0
- package/dist/src/routing/types.d.ts +157 -0
- package/dist/src/routing/types.d.ts.map +1 -0
- package/dist/src/routing/types.js +68 -0
- package/dist/src/routing/types.test.d.ts +2 -0
- package/dist/src/routing/types.test.d.ts.map +1 -0
- package/dist/src/routing/types.test.js +184 -0
- package/dist/src/templates/registry.test.js +2 -2
- package/dist/src/templates/types.d.ts +5 -0
- package/dist/src/templates/types.d.ts.map +1 -1
- package/dist/src/templates/types.js +3 -0
- package/dist/src/workflow/agent-cancellation.d.ts +37 -0
- package/dist/src/workflow/agent-cancellation.d.ts.map +1 -0
- package/dist/src/workflow/agent-cancellation.js +41 -0
- package/dist/src/workflow/agent-cancellation.test.d.ts +2 -0
- package/dist/src/workflow/agent-cancellation.test.d.ts.map +1 -0
- package/dist/src/workflow/agent-cancellation.test.js +86 -0
- package/dist/src/workflow/branching-router.d.ts +38 -0
- package/dist/src/workflow/branching-router.d.ts.map +1 -0
- package/dist/src/workflow/branching-router.js +52 -0
- package/dist/src/workflow/branching-router.test.d.ts +2 -0
- package/dist/src/workflow/branching-router.test.d.ts.map +1 -0
- package/dist/src/workflow/branching-router.test.js +209 -0
- package/dist/src/workflow/concurrency-semaphore.d.ts +21 -0
- package/dist/src/workflow/concurrency-semaphore.d.ts.map +1 -0
- package/dist/src/workflow/concurrency-semaphore.js +46 -0
- package/dist/src/workflow/concurrency-semaphore.test.d.ts +2 -0
- package/dist/src/workflow/concurrency-semaphore.test.d.ts.map +1 -0
- package/dist/src/workflow/concurrency-semaphore.test.js +183 -0
- package/dist/src/workflow/duration.d.ts +28 -0
- package/dist/src/workflow/duration.d.ts.map +1 -0
- package/dist/src/workflow/duration.js +57 -0
- package/dist/src/workflow/duration.test.d.ts +2 -0
- package/dist/src/workflow/duration.test.d.ts.map +1 -0
- package/dist/src/workflow/duration.test.js +74 -0
- package/dist/src/workflow/expression/ast.d.ts +53 -0
- package/dist/src/workflow/expression/ast.d.ts.map +1 -0
- package/dist/src/workflow/expression/ast.js +8 -0
- package/dist/src/workflow/expression/context.d.ts +40 -0
- package/dist/src/workflow/expression/context.d.ts.map +1 -0
- package/dist/src/workflow/expression/context.js +37 -0
- package/dist/src/workflow/expression/evaluator.d.ts +28 -0
- package/dist/src/workflow/expression/evaluator.d.ts.map +1 -0
- package/dist/src/workflow/expression/evaluator.js +165 -0
- package/dist/src/workflow/expression/evaluator.test.d.ts +2 -0
- package/dist/src/workflow/expression/evaluator.test.d.ts.map +1 -0
- package/dist/src/workflow/expression/evaluator.test.js +792 -0
- package/dist/src/workflow/expression/expression.test.d.ts +2 -0
- package/dist/src/workflow/expression/expression.test.d.ts.map +1 -0
- package/dist/src/workflow/expression/expression.test.js +516 -0
- package/dist/src/workflow/expression/helpers.d.ts +21 -0
- package/dist/src/workflow/expression/helpers.d.ts.map +1 -0
- package/dist/src/workflow/expression/helpers.js +56 -0
- package/dist/src/workflow/expression/index.d.ts +55 -0
- package/dist/src/workflow/expression/index.d.ts.map +1 -0
- package/dist/src/workflow/expression/index.js +71 -0
- package/dist/src/workflow/expression/lexer.d.ts +37 -0
- package/dist/src/workflow/expression/lexer.d.ts.map +1 -0
- package/dist/src/workflow/expression/lexer.js +166 -0
- package/dist/src/workflow/expression/parser.d.ts +23 -0
- package/dist/src/workflow/expression/parser.d.ts.map +1 -0
- package/dist/src/workflow/expression/parser.js +181 -0
- package/dist/src/workflow/gate-state.d.ts +115 -0
- package/dist/src/workflow/gate-state.d.ts.map +1 -0
- package/dist/src/workflow/gate-state.js +185 -0
- package/dist/src/workflow/gate-state.test.d.ts +2 -0
- package/dist/src/workflow/gate-state.test.d.ts.map +1 -0
- package/dist/src/workflow/gate-state.test.js +251 -0
- package/dist/src/workflow/gates/gate-evaluator.d.ts +119 -0
- package/dist/src/workflow/gates/gate-evaluator.d.ts.map +1 -0
- package/dist/src/workflow/gates/gate-evaluator.js +243 -0
- package/dist/src/workflow/gates/gate-evaluator.test.d.ts +2 -0
- package/dist/src/workflow/gates/gate-evaluator.test.d.ts.map +1 -0
- package/dist/src/workflow/gates/gate-evaluator.test.js +240 -0
- package/dist/src/workflow/gates/signal-gate.d.ts +114 -0
- package/dist/src/workflow/gates/signal-gate.d.ts.map +1 -0
- package/dist/src/workflow/gates/signal-gate.js +216 -0
- package/dist/src/workflow/gates/signal-gate.test.d.ts +2 -0
- package/dist/src/workflow/gates/signal-gate.test.d.ts.map +1 -0
- package/dist/src/workflow/gates/signal-gate.test.js +199 -0
- package/dist/src/workflow/gates/timeout-engine.d.ts +96 -0
- package/dist/src/workflow/gates/timeout-engine.d.ts.map +1 -0
- package/dist/src/workflow/gates/timeout-engine.js +162 -0
- package/dist/src/workflow/gates/timeout-engine.test.d.ts +2 -0
- package/dist/src/workflow/gates/timeout-engine.test.d.ts.map +1 -0
- package/dist/src/workflow/gates/timeout-engine.test.js +186 -0
- package/dist/src/workflow/gates/timer-gate.d.ts +125 -0
- package/dist/src/workflow/gates/timer-gate.d.ts.map +1 -0
- package/dist/src/workflow/gates/timer-gate.js +381 -0
- package/dist/src/workflow/gates/timer-gate.test.d.ts +2 -0
- package/dist/src/workflow/gates/timer-gate.test.d.ts.map +1 -0
- package/dist/src/workflow/gates/timer-gate.test.js +211 -0
- package/dist/src/workflow/gates/webhook-gate.d.ts +132 -0
- package/dist/src/workflow/gates/webhook-gate.d.ts.map +1 -0
- package/dist/src/workflow/gates/webhook-gate.js +216 -0
- package/dist/src/workflow/gates/webhook-gate.test.d.ts +2 -0
- package/dist/src/workflow/gates/webhook-gate.test.d.ts.map +1 -0
- package/dist/src/workflow/gates/webhook-gate.test.js +182 -0
- package/dist/src/workflow/index.d.ts +31 -3
- package/dist/src/workflow/index.d.ts.map +1 -1
- package/dist/src/workflow/index.js +20 -1
- package/dist/src/workflow/parallelism-executor.d.ts +25 -0
- package/dist/src/workflow/parallelism-executor.d.ts.map +1 -0
- package/dist/src/workflow/parallelism-executor.js +53 -0
- package/dist/src/workflow/parallelism-executor.test.d.ts +2 -0
- package/dist/src/workflow/parallelism-executor.test.d.ts.map +1 -0
- package/dist/src/workflow/parallelism-executor.test.js +191 -0
- package/dist/src/workflow/parallelism-types.d.ts +80 -0
- package/dist/src/workflow/parallelism-types.d.ts.map +1 -0
- package/dist/src/workflow/parallelism-types.js +8 -0
- package/dist/src/workflow/phase-context-injector.d.ts +29 -0
- package/dist/src/workflow/phase-context-injector.d.ts.map +1 -0
- package/dist/src/workflow/phase-context-injector.js +43 -0
- package/dist/src/workflow/phase-context-injector.test.d.ts +2 -0
- package/dist/src/workflow/phase-context-injector.test.d.ts.map +1 -0
- package/dist/src/workflow/phase-context-injector.test.js +123 -0
- package/dist/src/workflow/phase-output-collector.d.ts +39 -0
- package/dist/src/workflow/phase-output-collector.d.ts.map +1 -0
- package/dist/src/workflow/phase-output-collector.js +141 -0
- package/dist/src/workflow/phase-output-collector.test.d.ts +2 -0
- package/dist/src/workflow/phase-output-collector.test.d.ts.map +1 -0
- package/dist/src/workflow/phase-output-collector.test.js +179 -0
- package/dist/src/workflow/retry-resolver.d.ts +51 -0
- package/dist/src/workflow/retry-resolver.d.ts.map +1 -0
- package/dist/src/workflow/retry-resolver.js +70 -0
- package/dist/src/workflow/retry-resolver.test.d.ts +2 -0
- package/dist/src/workflow/retry-resolver.test.d.ts.map +1 -0
- package/dist/src/workflow/retry-resolver.test.js +149 -0
- package/dist/src/workflow/strategies/fan-in-strategy.d.ts +21 -0
- package/dist/src/workflow/strategies/fan-in-strategy.d.ts.map +1 -0
- package/dist/src/workflow/strategies/fan-in-strategy.js +92 -0
- package/dist/src/workflow/strategies/fan-in-strategy.test.d.ts +2 -0
- package/dist/src/workflow/strategies/fan-in-strategy.test.d.ts.map +1 -0
- package/dist/src/workflow/strategies/fan-in-strategy.test.js +182 -0
- package/dist/src/workflow/strategies/fan-out-strategy.d.ts +16 -0
- package/dist/src/workflow/strategies/fan-out-strategy.d.ts.map +1 -0
- package/dist/src/workflow/strategies/fan-out-strategy.js +47 -0
- package/dist/src/workflow/strategies/fan-out-strategy.test.d.ts +2 -0
- package/dist/src/workflow/strategies/fan-out-strategy.test.d.ts.map +1 -0
- package/dist/src/workflow/strategies/fan-out-strategy.test.js +97 -0
- package/dist/src/workflow/strategies/index.d.ts +4 -0
- package/dist/src/workflow/strategies/index.d.ts.map +1 -0
- package/dist/src/workflow/strategies/index.js +3 -0
- package/dist/src/workflow/strategies/race-strategy.d.ts +19 -0
- package/dist/src/workflow/strategies/race-strategy.d.ts.map +1 -0
- package/dist/src/workflow/strategies/race-strategy.js +92 -0
- package/dist/src/workflow/strategies/race-strategy.test.d.ts +2 -0
- package/dist/src/workflow/strategies/race-strategy.test.d.ts.map +1 -0
- package/dist/src/workflow/strategies/race-strategy.test.js +318 -0
- package/dist/src/workflow/transition-engine.d.ts +3 -1
- package/dist/src/workflow/transition-engine.d.ts.map +1 -1
- package/dist/src/workflow/transition-engine.js +26 -7
- package/dist/src/workflow/transition-engine.test.js +215 -11
- package/dist/src/workflow/workflow-registry.d.ts +46 -1
- package/dist/src/workflow/workflow-registry.d.ts.map +1 -1
- package/dist/src/workflow/workflow-registry.js +74 -0
- package/dist/src/workflow/workflow-registry.test.js +54 -0
- package/dist/src/workflow/workflow-types.d.ts +330 -12
- package/dist/src/workflow/workflow-types.d.ts.map +1 -1
- package/dist/src/workflow/workflow-types.js +100 -5
- package/dist/src/workflow/workflow-types.test.js +293 -2
- package/package.json +2 -2
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fan-in parallelism strategy.
|
|
3
|
+
*
|
|
4
|
+
* Dispatches N agents in parallel and waits for results.
|
|
5
|
+
*
|
|
6
|
+
* When `waitForAll` is true (default): waits for ALL tasks to settle
|
|
7
|
+
* using Promise.allSettled() semantics.
|
|
8
|
+
*
|
|
9
|
+
* When `waitForAll` is false: resolves on the first successful result,
|
|
10
|
+
* but still waits for all remaining tasks to complete for full result
|
|
11
|
+
* collection.
|
|
12
|
+
*
|
|
13
|
+
* NOTE: Concurrency limiting (maxConcurrent) is handled by the
|
|
14
|
+
* ConcurrencySemaphore in the ParallelismExecutor — this strategy
|
|
15
|
+
* simply calls `options.dispatch()` for each task.
|
|
16
|
+
*/
|
|
17
|
+
export class FanInStrategy {
|
|
18
|
+
async execute(tasks, options) {
|
|
19
|
+
const completed = [];
|
|
20
|
+
const failed = [];
|
|
21
|
+
const outputs = {};
|
|
22
|
+
const waitForAll = options.waitForAll !== false; // default true
|
|
23
|
+
// Dispatch all tasks, wrapping each result
|
|
24
|
+
const taskPromises = tasks.map((task) => options.dispatch(task).then((result) => ({ status: 'fulfilled', value: result }), (error) => ({ status: 'rejected', reason: error, task })));
|
|
25
|
+
if (waitForAll) {
|
|
26
|
+
// Wait for all tasks to settle
|
|
27
|
+
const results = await Promise.allSettled(taskPromises);
|
|
28
|
+
for (const result of results) {
|
|
29
|
+
if (result.status === 'fulfilled') {
|
|
30
|
+
const settled = result.value;
|
|
31
|
+
if (settled.status === 'fulfilled') {
|
|
32
|
+
completed.push(settled.value);
|
|
33
|
+
if (settled.value.outputs) {
|
|
34
|
+
outputs[settled.value.issueId] = settled.value.outputs;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
const errMsg = settled.reason instanceof Error
|
|
39
|
+
? settled.reason.message
|
|
40
|
+
: String(settled.reason);
|
|
41
|
+
failed.push({
|
|
42
|
+
id: settled.task.id,
|
|
43
|
+
issueId: settled.task.issueId,
|
|
44
|
+
error: errMsg,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
// Wait for first success, then collect remaining
|
|
52
|
+
let firstSuccessResolve = null;
|
|
53
|
+
const firstSuccessPromise = new Promise((resolve) => {
|
|
54
|
+
firstSuccessResolve = resolve;
|
|
55
|
+
});
|
|
56
|
+
let pendingCount = taskPromises.length;
|
|
57
|
+
let hasSuccess = false;
|
|
58
|
+
const wrappedPromises = taskPromises.map(async (promise) => {
|
|
59
|
+
const result = await promise;
|
|
60
|
+
if (result.status === 'fulfilled') {
|
|
61
|
+
completed.push(result.value);
|
|
62
|
+
if (result.value.outputs) {
|
|
63
|
+
outputs[result.value.issueId] = result.value.outputs;
|
|
64
|
+
}
|
|
65
|
+
if (!hasSuccess && result.value.success) {
|
|
66
|
+
hasSuccess = true;
|
|
67
|
+
firstSuccessResolve?.(result.value);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
const errMsg = result.reason instanceof Error
|
|
72
|
+
? result.reason.message
|
|
73
|
+
: String(result.reason);
|
|
74
|
+
failed.push({
|
|
75
|
+
id: result.task.id,
|
|
76
|
+
issueId: result.task.issueId,
|
|
77
|
+
error: errMsg,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
pendingCount--;
|
|
81
|
+
if (pendingCount === 0 && !hasSuccess) {
|
|
82
|
+
firstSuccessResolve?.(null);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
// Wait for first success or all to complete
|
|
86
|
+
await firstSuccessPromise;
|
|
87
|
+
// Still wait for all remaining to finish
|
|
88
|
+
await Promise.allSettled(wrappedPromises);
|
|
89
|
+
}
|
|
90
|
+
return { strategy: 'fan-in', completed, cancelled: [], failed, outputs };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fan-in-strategy.test.d.ts","sourceRoot":"","sources":["../../../../src/workflow/strategies/fan-in-strategy.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { FanInStrategy } from './fan-in-strategy.js';
|
|
3
|
+
/** Helper: create sample tasks */
|
|
4
|
+
function createTasks(count) {
|
|
5
|
+
return Array.from({ length: count }, (_, i) => ({
|
|
6
|
+
id: `task-${i + 1}`,
|
|
7
|
+
issueId: `SUP-${100 + i + 1}`,
|
|
8
|
+
phaseName: 'development',
|
|
9
|
+
}));
|
|
10
|
+
}
|
|
11
|
+
/** Helper: create a dispatch that returns success */
|
|
12
|
+
function createSuccessDispatch() {
|
|
13
|
+
return async (task) => ({
|
|
14
|
+
id: task.id,
|
|
15
|
+
issueId: task.issueId,
|
|
16
|
+
success: true,
|
|
17
|
+
durationMs: 10,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/** Helper: create options with a dispatch function */
|
|
21
|
+
function createOptions(dispatch, overrides = {}) {
|
|
22
|
+
return { dispatch, ...overrides };
|
|
23
|
+
}
|
|
24
|
+
describe('FanInStrategy', () => {
|
|
25
|
+
describe('waitForAll=true', () => {
|
|
26
|
+
it('waits for all tasks to complete', async () => {
|
|
27
|
+
const strategy = new FanInStrategy();
|
|
28
|
+
const tasks = createTasks(3);
|
|
29
|
+
const completionOrder = [];
|
|
30
|
+
const dispatch = async (task) => {
|
|
31
|
+
// Stagger completion times
|
|
32
|
+
const delay = task.id === 'task-1' ? 30 : task.id === 'task-2' ? 10 : 20;
|
|
33
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
34
|
+
completionOrder.push(task.id);
|
|
35
|
+
return { id: task.id, issueId: task.issueId, success: true };
|
|
36
|
+
};
|
|
37
|
+
const result = await strategy.execute(tasks, createOptions(dispatch, { waitForAll: true }));
|
|
38
|
+
// All three tasks should be completed
|
|
39
|
+
expect(result.completed).toHaveLength(3);
|
|
40
|
+
// The result should include all task IDs
|
|
41
|
+
const completedIds = result.completed.map((r) => r.id).sort();
|
|
42
|
+
expect(completedIds).toEqual(['task-1', 'task-2', 'task-3']);
|
|
43
|
+
});
|
|
44
|
+
it('handles mix of success and failure', async () => {
|
|
45
|
+
const strategy = new FanInStrategy();
|
|
46
|
+
const tasks = createTasks(4);
|
|
47
|
+
const dispatch = async (task) => {
|
|
48
|
+
if (task.id === 'task-2' || task.id === 'task-4') {
|
|
49
|
+
throw new Error(`failed: ${task.id}`);
|
|
50
|
+
}
|
|
51
|
+
return { id: task.id, issueId: task.issueId, success: true };
|
|
52
|
+
};
|
|
53
|
+
const result = await strategy.execute(tasks, createOptions(dispatch, { waitForAll: true }));
|
|
54
|
+
expect(result.completed).toHaveLength(2);
|
|
55
|
+
expect(result.failed).toHaveLength(2);
|
|
56
|
+
const completedIds = result.completed.map((r) => r.id).sort();
|
|
57
|
+
expect(completedIds).toEqual(['task-1', 'task-3']);
|
|
58
|
+
const failedIds = result.failed.map((r) => r.id).sort();
|
|
59
|
+
expect(failedIds).toEqual(['task-2', 'task-4']);
|
|
60
|
+
expect(result.failed.find((f) => f.id === 'task-2')?.error).toBe('failed: task-2');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe('waitForAll=false', () => {
|
|
64
|
+
it('resolves after first success', async () => {
|
|
65
|
+
const strategy = new FanInStrategy();
|
|
66
|
+
const tasks = createTasks(3);
|
|
67
|
+
const dispatch = async (task) => {
|
|
68
|
+
// task-2 completes first with success
|
|
69
|
+
const delay = task.id === 'task-1' ? 50 : task.id === 'task-2' ? 10 : 50;
|
|
70
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
71
|
+
return { id: task.id, issueId: task.issueId, success: true };
|
|
72
|
+
};
|
|
73
|
+
const result = await strategy.execute(tasks, createOptions(dispatch, { waitForAll: false }));
|
|
74
|
+
// All tasks should still be collected (we wait for remaining after first success)
|
|
75
|
+
expect(result.completed).toHaveLength(3);
|
|
76
|
+
expect(result.strategy).toBe('fan-in');
|
|
77
|
+
});
|
|
78
|
+
it('all fail returns failure result', async () => {
|
|
79
|
+
const strategy = new FanInStrategy();
|
|
80
|
+
const tasks = createTasks(3);
|
|
81
|
+
const dispatch = async (task) => {
|
|
82
|
+
throw new Error(`failed: ${task.id}`);
|
|
83
|
+
};
|
|
84
|
+
const result = await strategy.execute(tasks, createOptions(dispatch, { waitForAll: false }));
|
|
85
|
+
expect(result.completed).toHaveLength(0);
|
|
86
|
+
expect(result.failed).toHaveLength(3);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
it('collects outputs from completed tasks', async () => {
|
|
90
|
+
const strategy = new FanInStrategy();
|
|
91
|
+
const tasks = createTasks(2);
|
|
92
|
+
const dispatch = async (task) => ({
|
|
93
|
+
id: task.id,
|
|
94
|
+
issueId: task.issueId,
|
|
95
|
+
success: true,
|
|
96
|
+
outputs: { artifact: `build-${task.id}` },
|
|
97
|
+
});
|
|
98
|
+
const result = await strategy.execute(tasks, createOptions(dispatch));
|
|
99
|
+
expect(result.outputs['SUP-101']).toEqual({ artifact: 'build-task-1' });
|
|
100
|
+
expect(result.outputs['SUP-102']).toEqual({ artifact: 'build-task-2' });
|
|
101
|
+
});
|
|
102
|
+
it('maxConcurrent=2 with 5 tasks verifies queuing behavior', async () => {
|
|
103
|
+
// Note: The actual concurrency limiting is done by the ParallelismExecutor's
|
|
104
|
+
// semaphore wrapping the dispatch function. This test verifies that the
|
|
105
|
+
// strategy works correctly with a dispatch that simulates semaphore behavior.
|
|
106
|
+
const strategy = new FanInStrategy();
|
|
107
|
+
const tasks = createTasks(5);
|
|
108
|
+
let peakConcurrent = 0;
|
|
109
|
+
let currentConcurrent = 0;
|
|
110
|
+
const taskOrder = [];
|
|
111
|
+
// Simulate the semaphore-wrapped dispatch behavior
|
|
112
|
+
const maxConcurrent = 2;
|
|
113
|
+
let activeSlots = 0;
|
|
114
|
+
const waitQueue = [];
|
|
115
|
+
const dispatch = async (task) => {
|
|
116
|
+
// Simulate semaphore acquire
|
|
117
|
+
if (activeSlots >= maxConcurrent) {
|
|
118
|
+
await new Promise((resolve) => waitQueue.push(resolve));
|
|
119
|
+
}
|
|
120
|
+
activeSlots++;
|
|
121
|
+
currentConcurrent++;
|
|
122
|
+
peakConcurrent = Math.max(peakConcurrent, currentConcurrent);
|
|
123
|
+
taskOrder.push(`start:${task.id}`);
|
|
124
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
125
|
+
taskOrder.push(`end:${task.id}`);
|
|
126
|
+
currentConcurrent--;
|
|
127
|
+
activeSlots--;
|
|
128
|
+
// Simulate semaphore release
|
|
129
|
+
if (waitQueue.length > 0) {
|
|
130
|
+
const next = waitQueue.shift();
|
|
131
|
+
next();
|
|
132
|
+
}
|
|
133
|
+
return { id: task.id, issueId: task.issueId, success: true };
|
|
134
|
+
};
|
|
135
|
+
const result = await strategy.execute(tasks, createOptions(dispatch, { maxConcurrent: 2 }));
|
|
136
|
+
expect(result.completed).toHaveLength(5);
|
|
137
|
+
expect(peakConcurrent).toBeLessThanOrEqual(2);
|
|
138
|
+
// Verify at no point more than 2 tasks are active
|
|
139
|
+
let active = 0;
|
|
140
|
+
for (const entry of taskOrder) {
|
|
141
|
+
if (entry.startsWith('start:'))
|
|
142
|
+
active++;
|
|
143
|
+
if (entry.startsWith('end:'))
|
|
144
|
+
active--;
|
|
145
|
+
expect(active).toBeLessThanOrEqual(2);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
it('returns empty result for empty task list', async () => {
|
|
149
|
+
const strategy = new FanInStrategy();
|
|
150
|
+
const dispatch = createSuccessDispatch();
|
|
151
|
+
const result = await strategy.execute([], createOptions(dispatch));
|
|
152
|
+
expect(result.completed).toEqual([]);
|
|
153
|
+
expect(result.failed).toEqual([]);
|
|
154
|
+
expect(result.cancelled).toEqual([]);
|
|
155
|
+
expect(result.outputs).toEqual({});
|
|
156
|
+
});
|
|
157
|
+
it('sets strategy to fan-in', async () => {
|
|
158
|
+
const strategy = new FanInStrategy();
|
|
159
|
+
const tasks = createTasks(1);
|
|
160
|
+
const dispatch = createSuccessDispatch();
|
|
161
|
+
const result = await strategy.execute(tasks, createOptions(dispatch));
|
|
162
|
+
expect(result.strategy).toBe('fan-in');
|
|
163
|
+
});
|
|
164
|
+
it('defaults to waitForAll=true when not specified', async () => {
|
|
165
|
+
const strategy = new FanInStrategy();
|
|
166
|
+
const tasks = createTasks(3);
|
|
167
|
+
const completionOrder = [];
|
|
168
|
+
const dispatch = async (task) => {
|
|
169
|
+
// Stagger completion: task-3 finishes first, task-1 last
|
|
170
|
+
const delay = task.id === 'task-1' ? 30 : task.id === 'task-2' ? 20 : 10;
|
|
171
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
172
|
+
completionOrder.push(task.id);
|
|
173
|
+
return { id: task.id, issueId: task.issueId, success: true };
|
|
174
|
+
};
|
|
175
|
+
// Do NOT pass waitForAll — it should default to true
|
|
176
|
+
const result = await strategy.execute(tasks, createOptions(dispatch));
|
|
177
|
+
// All three should complete since default is waitForAll=true
|
|
178
|
+
expect(result.completed).toHaveLength(3);
|
|
179
|
+
const completedIds = result.completed.map((r) => r.id).sort();
|
|
180
|
+
expect(completedIds).toEqual(['task-1', 'task-2', 'task-3']);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ParallelTask, ParallelismResult, ParallelismStrategy, ParallelismStrategyOptions } from '../parallelism-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Fan-out parallelism strategy.
|
|
4
|
+
*
|
|
5
|
+
* Dispatches N agents in parallel without waiting for completion.
|
|
6
|
+
* All dispatches are initiated concurrently and results are collected
|
|
7
|
+
* from whatever settles during the dispatch window.
|
|
8
|
+
*
|
|
9
|
+
* NOTE: Concurrency limiting (maxConcurrent) is handled by the
|
|
10
|
+
* ConcurrencySemaphore in the ParallelismExecutor — this strategy
|
|
11
|
+
* simply calls `options.dispatch()` for each task.
|
|
12
|
+
*/
|
|
13
|
+
export declare class FanOutStrategy implements ParallelismStrategy {
|
|
14
|
+
execute(tasks: ParallelTask[], options: ParallelismStrategyOptions): Promise<ParallelismResult>;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=fan-out-strategy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fan-out-strategy.d.ts","sourceRoot":"","sources":["../../../../src/workflow/strategies/fan-out-strategy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EAGZ,iBAAiB,EACjB,mBAAmB,EACnB,0BAA0B,EAC3B,MAAM,yBAAyB,CAAA;AAEhC;;;;;;;;;;GAUG;AACH,qBAAa,cAAe,YAAW,mBAAmB;IAClD,OAAO,CACX,KAAK,EAAE,YAAY,EAAE,EACrB,OAAO,EAAE,0BAA0B,GAClC,OAAO,CAAC,iBAAiB,CAAC;CA2C9B"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fan-out parallelism strategy.
|
|
3
|
+
*
|
|
4
|
+
* Dispatches N agents in parallel without waiting for completion.
|
|
5
|
+
* All dispatches are initiated concurrently and results are collected
|
|
6
|
+
* from whatever settles during the dispatch window.
|
|
7
|
+
*
|
|
8
|
+
* NOTE: Concurrency limiting (maxConcurrent) is handled by the
|
|
9
|
+
* ConcurrencySemaphore in the ParallelismExecutor — this strategy
|
|
10
|
+
* simply calls `options.dispatch()` for each task.
|
|
11
|
+
*/
|
|
12
|
+
export class FanOutStrategy {
|
|
13
|
+
async execute(tasks, options) {
|
|
14
|
+
const completed = [];
|
|
15
|
+
const failed = [];
|
|
16
|
+
const outputs = {};
|
|
17
|
+
// Fire all dispatches concurrently but don't wait for them
|
|
18
|
+
// Use Promise.allSettled with a short window to catch immediate failures
|
|
19
|
+
const dispatchPromises = tasks.map((task) => options.dispatch(task).then((result) => ({ status: 'fulfilled', value: result, task }), (error) => ({ status: 'rejected', reason: error, task })));
|
|
20
|
+
// Wait for all dispatches to settle — since we already wrapped each
|
|
21
|
+
// promise, Promise.allSettled here will always return 'fulfilled' results
|
|
22
|
+
// containing our inner status objects.
|
|
23
|
+
const results = await Promise.allSettled(dispatchPromises);
|
|
24
|
+
for (const result of results) {
|
|
25
|
+
if (result.status === 'fulfilled') {
|
|
26
|
+
const settled = result.value;
|
|
27
|
+
if (settled.status === 'fulfilled') {
|
|
28
|
+
completed.push(settled.value);
|
|
29
|
+
if (settled.value.outputs) {
|
|
30
|
+
outputs[settled.value.issueId] = settled.value.outputs;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
const errMsg = settled.reason instanceof Error
|
|
35
|
+
? settled.reason.message
|
|
36
|
+
: String(settled.reason);
|
|
37
|
+
failed.push({
|
|
38
|
+
id: settled.task.id,
|
|
39
|
+
issueId: settled.task.issueId,
|
|
40
|
+
error: errMsg,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { strategy: 'fan-out', completed, cancelled: [], failed, outputs };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fan-out-strategy.test.d.ts","sourceRoot":"","sources":["../../../../src/workflow/strategies/fan-out-strategy.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { FanOutStrategy } from './fan-out-strategy.js';
|
|
3
|
+
/** Helper: create sample tasks */
|
|
4
|
+
function createTasks(count) {
|
|
5
|
+
return Array.from({ length: count }, (_, i) => ({
|
|
6
|
+
id: `task-${i + 1}`,
|
|
7
|
+
issueId: `SUP-${100 + i + 1}`,
|
|
8
|
+
phaseName: 'development',
|
|
9
|
+
}));
|
|
10
|
+
}
|
|
11
|
+
/** Helper: create a dispatch that returns success */
|
|
12
|
+
function createSuccessDispatch() {
|
|
13
|
+
return async (task) => ({
|
|
14
|
+
id: task.id,
|
|
15
|
+
issueId: task.issueId,
|
|
16
|
+
success: true,
|
|
17
|
+
durationMs: 10,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/** Helper: create options with a dispatch function */
|
|
21
|
+
function createOptions(dispatch, overrides = {}) {
|
|
22
|
+
return { dispatch, ...overrides };
|
|
23
|
+
}
|
|
24
|
+
describe('FanOutStrategy', () => {
|
|
25
|
+
it('dispatches all tasks and returns results', async () => {
|
|
26
|
+
const strategy = new FanOutStrategy();
|
|
27
|
+
const tasks = createTasks(3);
|
|
28
|
+
const dispatched = [];
|
|
29
|
+
const dispatch = async (task) => {
|
|
30
|
+
dispatched.push(task.id);
|
|
31
|
+
return { id: task.id, issueId: task.issueId, success: true };
|
|
32
|
+
};
|
|
33
|
+
const result = await strategy.execute(tasks, createOptions(dispatch));
|
|
34
|
+
expect(dispatched).toEqual(['task-1', 'task-2', 'task-3']);
|
|
35
|
+
expect(result.completed).toHaveLength(3);
|
|
36
|
+
expect(result.completed[0].issueId).toBe('SUP-101');
|
|
37
|
+
expect(result.completed[1].issueId).toBe('SUP-102');
|
|
38
|
+
expect(result.completed[2].issueId).toBe('SUP-103');
|
|
39
|
+
});
|
|
40
|
+
it('handles dispatch failures gracefully', async () => {
|
|
41
|
+
const strategy = new FanOutStrategy();
|
|
42
|
+
const tasks = createTasks(3);
|
|
43
|
+
const dispatch = async (task) => {
|
|
44
|
+
if (task.id === 'task-2') {
|
|
45
|
+
throw new Error('dispatch failed for task-2');
|
|
46
|
+
}
|
|
47
|
+
return { id: task.id, issueId: task.issueId, success: true };
|
|
48
|
+
};
|
|
49
|
+
const result = await strategy.execute(tasks, createOptions(dispatch));
|
|
50
|
+
expect(result.completed).toHaveLength(2);
|
|
51
|
+
expect(result.failed).toHaveLength(1);
|
|
52
|
+
expect(result.failed[0].id).toBe('task-2');
|
|
53
|
+
expect(result.failed[0].issueId).toBe('SUP-102');
|
|
54
|
+
expect(result.failed[0].error).toBe('dispatch failed for task-2');
|
|
55
|
+
});
|
|
56
|
+
it('collects outputs from successful tasks', async () => {
|
|
57
|
+
const strategy = new FanOutStrategy();
|
|
58
|
+
const tasks = createTasks(2);
|
|
59
|
+
const dispatch = async (task) => ({
|
|
60
|
+
id: task.id,
|
|
61
|
+
issueId: task.issueId,
|
|
62
|
+
success: true,
|
|
63
|
+
outputs: { artifact: `build-${task.id}` },
|
|
64
|
+
});
|
|
65
|
+
const result = await strategy.execute(tasks, createOptions(dispatch));
|
|
66
|
+
expect(result.outputs['SUP-101']).toEqual({ artifact: 'build-task-1' });
|
|
67
|
+
expect(result.outputs['SUP-102']).toEqual({ artifact: 'build-task-2' });
|
|
68
|
+
});
|
|
69
|
+
it('returns empty result for empty task list', async () => {
|
|
70
|
+
const strategy = new FanOutStrategy();
|
|
71
|
+
const dispatch = createSuccessDispatch();
|
|
72
|
+
const result = await strategy.execute([], createOptions(dispatch));
|
|
73
|
+
expect(result.completed).toEqual([]);
|
|
74
|
+
expect(result.failed).toEqual([]);
|
|
75
|
+
expect(result.cancelled).toEqual([]);
|
|
76
|
+
expect(result.outputs).toEqual({});
|
|
77
|
+
});
|
|
78
|
+
it('sets strategy to fan-out', async () => {
|
|
79
|
+
const strategy = new FanOutStrategy();
|
|
80
|
+
const tasks = createTasks(1);
|
|
81
|
+
const dispatch = createSuccessDispatch();
|
|
82
|
+
const result = await strategy.execute(tasks, createOptions(dispatch));
|
|
83
|
+
expect(result.strategy).toBe('fan-out');
|
|
84
|
+
});
|
|
85
|
+
it('cancelled array is always empty', async () => {
|
|
86
|
+
const strategy = new FanOutStrategy();
|
|
87
|
+
const tasks = createTasks(3);
|
|
88
|
+
const dispatch = async (task) => {
|
|
89
|
+
if (task.id === 'task-2') {
|
|
90
|
+
throw new Error('failed');
|
|
91
|
+
}
|
|
92
|
+
return { id: task.id, issueId: task.issueId, success: true };
|
|
93
|
+
};
|
|
94
|
+
const result = await strategy.execute(tasks, createOptions(dispatch));
|
|
95
|
+
expect(result.cancelled).toEqual([]);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/workflow/strategies/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ParallelTask, ParallelismResult, ParallelismStrategy, ParallelismStrategyOptions } from '../parallelism-types.js';
|
|
2
|
+
import { InMemoryAgentCancellation } from '../agent-cancellation.js';
|
|
3
|
+
/**
|
|
4
|
+
* Race parallelism strategy.
|
|
5
|
+
*
|
|
6
|
+
* Dispatches N agents in parallel. The first successful completion wins,
|
|
7
|
+
* and cancellation is signaled for all remaining tasks.
|
|
8
|
+
*
|
|
9
|
+
* NOTE: Concurrency limiting (maxConcurrent) is handled by the
|
|
10
|
+
* ConcurrencySemaphore in the ParallelismExecutor — this strategy
|
|
11
|
+
* simply calls `options.dispatch()` for each task.
|
|
12
|
+
*/
|
|
13
|
+
export declare class RaceStrategy implements ParallelismStrategy {
|
|
14
|
+
private readonly cancellation;
|
|
15
|
+
private readonly cancellationTimeoutMs;
|
|
16
|
+
constructor(cancellation?: InMemoryAgentCancellation, cancellationTimeoutMs?: number);
|
|
17
|
+
execute(tasks: ParallelTask[], options: ParallelismStrategyOptions): Promise<ParallelismResult>;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=race-strategy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"race-strategy.d.ts","sourceRoot":"","sources":["../../../../src/workflow/strategies/race-strategy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EAGZ,iBAAiB,EACjB,mBAAmB,EACnB,0BAA0B,EAC3B,MAAM,yBAAyB,CAAA;AAChC,OAAO,EAAE,yBAAyB,EAAE,MAAM,0BAA0B,CAAA;AAEpE;;;;;;;;;GASG;AACH,qBAAa,YAAa,YAAW,mBAAmB;IACtD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA2B;IACxD,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAQ;gBAElC,YAAY,CAAC,EAAE,yBAAyB,EAAE,qBAAqB,CAAC,EAAE,MAAM;IAK9E,OAAO,CACX,KAAK,EAAE,YAAY,EAAE,EACrB,OAAO,EAAE,0BAA0B,GAClC,OAAO,CAAC,iBAAiB,CAAC;CA4F9B"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { InMemoryAgentCancellation } from '../agent-cancellation.js';
|
|
2
|
+
/**
|
|
3
|
+
* Race parallelism strategy.
|
|
4
|
+
*
|
|
5
|
+
* Dispatches N agents in parallel. The first successful completion wins,
|
|
6
|
+
* and cancellation is signaled for all remaining tasks.
|
|
7
|
+
*
|
|
8
|
+
* NOTE: Concurrency limiting (maxConcurrent) is handled by the
|
|
9
|
+
* ConcurrencySemaphore in the ParallelismExecutor — this strategy
|
|
10
|
+
* simply calls `options.dispatch()` for each task.
|
|
11
|
+
*/
|
|
12
|
+
export class RaceStrategy {
|
|
13
|
+
cancellation;
|
|
14
|
+
cancellationTimeoutMs;
|
|
15
|
+
constructor(cancellation, cancellationTimeoutMs) {
|
|
16
|
+
this.cancellation = cancellation ?? new InMemoryAgentCancellation();
|
|
17
|
+
this.cancellationTimeoutMs = cancellationTimeoutMs ?? 30_000;
|
|
18
|
+
}
|
|
19
|
+
async execute(tasks, options) {
|
|
20
|
+
if (tasks.length === 0) {
|
|
21
|
+
return { strategy: 'race', completed: [], cancelled: [], failed: [], outputs: {} };
|
|
22
|
+
}
|
|
23
|
+
const completed = [];
|
|
24
|
+
const failed = [];
|
|
25
|
+
const outputs = {};
|
|
26
|
+
let winner = null;
|
|
27
|
+
const taskPromises = tasks.map((task) => options.dispatch(task).then((result) => ({ status: 'fulfilled', value: result, task }), (error) => ({ status: 'rejected', reason: error, task })));
|
|
28
|
+
// Use a winner-detection approach: wrap each promise to detect the
|
|
29
|
+
// first successful result, signal cancellation, then wait for all to settle.
|
|
30
|
+
let resolveWinner = null;
|
|
31
|
+
const winnerPromise = new Promise((resolve) => {
|
|
32
|
+
resolveWinner = resolve;
|
|
33
|
+
});
|
|
34
|
+
let settledCount = 0;
|
|
35
|
+
const totalTasks = tasks.length;
|
|
36
|
+
const wrappedPromises = taskPromises.map(async (promise) => {
|
|
37
|
+
const settled = await promise;
|
|
38
|
+
settledCount++;
|
|
39
|
+
if (settled.status === 'fulfilled') {
|
|
40
|
+
completed.push(settled.value);
|
|
41
|
+
if (settled.value.success && !winner) {
|
|
42
|
+
// First successful completion — this is the winner
|
|
43
|
+
winner = settled.value;
|
|
44
|
+
if (settled.value.outputs) {
|
|
45
|
+
outputs[settled.value.issueId] = settled.value.outputs;
|
|
46
|
+
}
|
|
47
|
+
// Signal cancellation for all other tasks
|
|
48
|
+
for (const task of tasks) {
|
|
49
|
+
if (task.id !== settled.task.id) {
|
|
50
|
+
await this.cancellation.cancel(task.id);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
resolveWinner?.(settled.value);
|
|
54
|
+
}
|
|
55
|
+
else if (settled.value.outputs && settled.value.success) {
|
|
56
|
+
outputs[settled.value.issueId] = settled.value.outputs;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
const errMsg = settled.reason instanceof Error
|
|
61
|
+
? settled.reason.message
|
|
62
|
+
: String(settled.reason);
|
|
63
|
+
failed.push({
|
|
64
|
+
id: settled.task.id,
|
|
65
|
+
issueId: settled.task.issueId,
|
|
66
|
+
error: errMsg,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
// If all tasks settled and no winner, resolve
|
|
70
|
+
if (settledCount === totalTasks && !winner) {
|
|
71
|
+
resolveWinner?.(null);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
// Wait for the winner or all to fail
|
|
75
|
+
await winnerPromise;
|
|
76
|
+
// Wait for remaining tasks to settle, with a timeout to prevent hanging
|
|
77
|
+
// if agents never check isCancelled() and never complete
|
|
78
|
+
await Promise.race([
|
|
79
|
+
Promise.allSettled(wrappedPromises),
|
|
80
|
+
new Promise((resolve) => setTimeout(resolve, this.cancellationTimeoutMs)),
|
|
81
|
+
]);
|
|
82
|
+
// Build cancelled list (all tasks that were cancelled, excluding winner and failed)
|
|
83
|
+
const cancelledIds = this.cancellation.getCancelledIds();
|
|
84
|
+
return {
|
|
85
|
+
strategy: 'race',
|
|
86
|
+
completed,
|
|
87
|
+
cancelled: cancelledIds,
|
|
88
|
+
failed,
|
|
89
|
+
outputs,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"race-strategy.test.d.ts","sourceRoot":"","sources":["../../../../src/workflow/strategies/race-strategy.test.ts"],"names":[],"mappings":""}
|