@renseiai/agentfactory 0.8.8 → 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 +23 -0
- package/dist/src/config/repository-config.d.ts.map +1 -1
- package/dist/src/config/repository-config.js +27 -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/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/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.map +1 -1
- package/dist/src/orchestrator/orchestrator.js +24 -0
- 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/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/types.d.ts +3 -0
- package/dist/src/templates/types.d.ts.map +1 -1
- package/dist/src/templates/types.js +2 -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/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/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 +23 -2
- package/dist/src/workflow/index.d.ts.map +1 -1
- package/dist/src/workflow/index.js +15 -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/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.map +1 -1
- package/dist/src/workflow/transition-engine.js +12 -0
- package/dist/src/workflow/transition-engine.test.js +92 -0
- package/dist/src/workflow/workflow-registry.d.ts +5 -1
- package/dist/src/workflow/workflow-registry.d.ts.map +1 -1
- package/dist/src/workflow/workflow-registry.js +8 -0
- package/dist/src/workflow/workflow-registry.test.js +54 -0
- package/dist/src/workflow/workflow-types.d.ts +151 -6
- package/dist/src/workflow/workflow-types.d.ts.map +1 -1
- package/dist/src/workflow/workflow-types.js +71 -1
- package/dist/src/workflow/workflow-types.test.js +293 -2
- package/package.json +2 -2
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { generateWebhookToken, buildCallbackUrl, validateWebhookCallback, evaluateWebhookGate, createWebhookGateActivation, } from './webhook-gate.js';
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Helpers
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
function makeGateDefinition(overrides = {}) {
|
|
7
|
+
return {
|
|
8
|
+
name: 'test-webhook',
|
|
9
|
+
type: 'webhook',
|
|
10
|
+
trigger: { endpoint: '/api/gates' },
|
|
11
|
+
...overrides,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function makeGateState(overrides = {}) {
|
|
15
|
+
return {
|
|
16
|
+
issueId: 'issue-1',
|
|
17
|
+
gateName: 'test-webhook',
|
|
18
|
+
gateType: 'webhook',
|
|
19
|
+
status: 'active',
|
|
20
|
+
activatedAt: Date.now(),
|
|
21
|
+
...overrides,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// generateWebhookToken
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
describe('generateWebhookToken', () => {
|
|
28
|
+
it('returns a 64-character hex string', () => {
|
|
29
|
+
const token = generateWebhookToken();
|
|
30
|
+
expect(token).toHaveLength(64);
|
|
31
|
+
expect(token).toMatch(/^[a-f0-9]{64}$/);
|
|
32
|
+
});
|
|
33
|
+
it('generates unique tokens', () => {
|
|
34
|
+
const token1 = generateWebhookToken();
|
|
35
|
+
const token2 = generateWebhookToken();
|
|
36
|
+
expect(token1).not.toBe(token2);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// buildCallbackUrl
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
describe('buildCallbackUrl', () => {
|
|
43
|
+
it('builds correct URL format', () => {
|
|
44
|
+
const url = buildCallbackUrl('https://api.example.com', 'issue-1', 'my-gate', 'abc123');
|
|
45
|
+
expect(url).toBe('https://api.example.com/api/gates/issue-1/my-gate?token=abc123');
|
|
46
|
+
});
|
|
47
|
+
it('URI-encodes special characters in issueId', () => {
|
|
48
|
+
const url = buildCallbackUrl('https://api.example.com', 'issue/1', 'gate', 'token');
|
|
49
|
+
expect(url).toContain('issue%2F1');
|
|
50
|
+
});
|
|
51
|
+
it('URI-encodes special characters in gateName', () => {
|
|
52
|
+
const url = buildCallbackUrl('https://api.example.com', 'issue-1', 'my gate', 'token');
|
|
53
|
+
expect(url).toContain('my%20gate');
|
|
54
|
+
});
|
|
55
|
+
it('normalizes trailing slashes on base URL', () => {
|
|
56
|
+
const url = buildCallbackUrl('https://api.example.com/', 'issue-1', 'gate', 'token');
|
|
57
|
+
expect(url).toBe('https://api.example.com/api/gates/issue-1/gate?token=token');
|
|
58
|
+
});
|
|
59
|
+
it('normalizes multiple trailing slashes', () => {
|
|
60
|
+
const url = buildCallbackUrl('https://api.example.com///', 'issue-1', 'gate', 'token');
|
|
61
|
+
expect(url).toBe('https://api.example.com/api/gates/issue-1/gate?token=token');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// validateWebhookCallback
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
describe('validateWebhookCallback', () => {
|
|
68
|
+
it('returns true for matching tokens', () => {
|
|
69
|
+
const token = 'test-webhook-token-value';
|
|
70
|
+
expect(validateWebhookCallback(token, token)).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
it('returns false for mismatched tokens', () => {
|
|
73
|
+
expect(validateWebhookCallback('token-a', 'token-b')).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
it('returns false for empty token', () => {
|
|
76
|
+
expect(validateWebhookCallback('', 'expected')).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
it('returns false for empty expected token', () => {
|
|
79
|
+
expect(validateWebhookCallback('token', '')).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
it('returns false for different length tokens', () => {
|
|
82
|
+
expect(validateWebhookCallback('short', 'a-much-longer-token')).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// evaluateWebhookGate
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
describe('evaluateWebhookGate', () => {
|
|
89
|
+
beforeEach(() => {
|
|
90
|
+
vi.useFakeTimers();
|
|
91
|
+
vi.setSystemTime(new Date('2025-06-01T12:00:00Z'));
|
|
92
|
+
});
|
|
93
|
+
afterEach(() => {
|
|
94
|
+
vi.useRealTimers();
|
|
95
|
+
});
|
|
96
|
+
it('returns not satisfied for null state', () => {
|
|
97
|
+
const gate = makeGateDefinition();
|
|
98
|
+
const result = evaluateWebhookGate(gate, null);
|
|
99
|
+
expect(result.satisfied).toBe(false);
|
|
100
|
+
expect(result.callbackUrl).toBeUndefined();
|
|
101
|
+
expect(result.timedOut).toBeUndefined();
|
|
102
|
+
});
|
|
103
|
+
it('returns satisfied for satisfied state', () => {
|
|
104
|
+
const gate = makeGateDefinition();
|
|
105
|
+
const state = makeGateState({ status: 'satisfied', satisfiedAt: Date.now() });
|
|
106
|
+
const result = evaluateWebhookGate(gate, state);
|
|
107
|
+
expect(result.satisfied).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
it('returns timedOut for timed-out state', () => {
|
|
110
|
+
const gate = makeGateDefinition();
|
|
111
|
+
const state = makeGateState({ status: 'timed-out', timedOutAt: Date.now() });
|
|
112
|
+
const result = evaluateWebhookGate(gate, state);
|
|
113
|
+
expect(result.satisfied).toBe(false);
|
|
114
|
+
expect(result.timedOut).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
it('returns callbackUrl for active state', () => {
|
|
117
|
+
const gate = makeGateDefinition();
|
|
118
|
+
const state = makeGateState({
|
|
119
|
+
status: 'active',
|
|
120
|
+
signalSource: 'https://api.example.com/api/gates/issue-1/test-webhook?token=abc',
|
|
121
|
+
});
|
|
122
|
+
const result = evaluateWebhookGate(gate, state);
|
|
123
|
+
expect(result.satisfied).toBe(false);
|
|
124
|
+
expect(result.callbackUrl).toBe(state.signalSource);
|
|
125
|
+
});
|
|
126
|
+
it('returns timedOut when active gate has expired deadline', () => {
|
|
127
|
+
const gate = makeGateDefinition();
|
|
128
|
+
const state = makeGateState({
|
|
129
|
+
status: 'active',
|
|
130
|
+
timeoutDeadline: Date.now() - 1000, // deadline in the past
|
|
131
|
+
});
|
|
132
|
+
const result = evaluateWebhookGate(gate, state);
|
|
133
|
+
expect(result.satisfied).toBe(false);
|
|
134
|
+
expect(result.timedOut).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
it('returns not timed out when active gate has future deadline', () => {
|
|
137
|
+
const gate = makeGateDefinition();
|
|
138
|
+
const state = makeGateState({
|
|
139
|
+
status: 'active',
|
|
140
|
+
timeoutDeadline: Date.now() + 60_000, // deadline in the future
|
|
141
|
+
});
|
|
142
|
+
const result = evaluateWebhookGate(gate, state);
|
|
143
|
+
expect(result.satisfied).toBe(false);
|
|
144
|
+
expect(result.timedOut).toBeUndefined();
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// createWebhookGateActivation
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
describe('createWebhookGateActivation', () => {
|
|
151
|
+
beforeEach(() => {
|
|
152
|
+
vi.useFakeTimers();
|
|
153
|
+
vi.setSystemTime(new Date('2025-06-01T12:00:00Z'));
|
|
154
|
+
});
|
|
155
|
+
afterEach(() => {
|
|
156
|
+
vi.useRealTimers();
|
|
157
|
+
});
|
|
158
|
+
it('generates a token', () => {
|
|
159
|
+
const gate = makeGateDefinition();
|
|
160
|
+
const activation = createWebhookGateActivation('issue-1', gate, 'https://api.example.com');
|
|
161
|
+
expect(activation.token).toHaveLength(64);
|
|
162
|
+
expect(activation.token).toMatch(/^[a-f0-9]{64}$/);
|
|
163
|
+
});
|
|
164
|
+
it('builds a callback URL with the generated token', () => {
|
|
165
|
+
const gate = makeGateDefinition({ name: 'review-gate' });
|
|
166
|
+
const activation = createWebhookGateActivation('issue-1', gate, 'https://api.example.com');
|
|
167
|
+
expect(activation.callbackUrl).toContain('https://api.example.com/api/gates/issue-1/review-gate');
|
|
168
|
+
expect(activation.callbackUrl).toContain(`token=${activation.token}`);
|
|
169
|
+
});
|
|
170
|
+
it('computes expiresAt from timeout duration', () => {
|
|
171
|
+
const gate = makeGateDefinition({
|
|
172
|
+
timeout: { duration: '4h', action: 'fail' },
|
|
173
|
+
});
|
|
174
|
+
const activation = createWebhookGateActivation('issue-1', gate, 'https://api.example.com');
|
|
175
|
+
expect(activation.expiresAt).toBe(Date.now() + 14_400_000);
|
|
176
|
+
});
|
|
177
|
+
it('does not set expiresAt when no timeout configured', () => {
|
|
178
|
+
const gate = makeGateDefinition();
|
|
179
|
+
const activation = createWebhookGateActivation('issue-1', gate, 'https://api.example.com');
|
|
180
|
+
expect(activation.expiresAt).toBeUndefined();
|
|
181
|
+
});
|
|
182
|
+
});
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Declarative workflow graph definitions using YAML (v1.1 schema extension).
|
|
5
5
|
* Defines phases, transitions, escalation ladder, gates, and parallelism.
|
|
6
6
|
*/
|
|
7
|
-
export type { EscalationStrategy, PhaseDefinition, TransitionDefinition, EscalationLadderRung, EscalationConfig, GateDefinition, ParallelismGroupDefinition, WorkflowDefinition, BranchingDefinition, TemplateRetryConfig, TemplateTimeoutConfig, } from './workflow-types.js';
|
|
8
|
-
export { PhaseDefinitionSchema, TransitionDefinitionSchema, EscalationLadderRungSchema, EscalationConfigSchema, GateDefinitionSchema, ParallelismGroupDefinitionSchema, WorkflowDefinitionSchema, BranchingDefinitionSchema, TemplateRetryConfigSchema, TemplateTimeoutConfigSchema, validateWorkflowDefinition, } from './workflow-types.js';
|
|
7
|
+
export type { EscalationStrategy, PhaseDefinition, PhaseOutputDeclaration, PhaseInputDeclaration, TransitionDefinition, EscalationLadderRung, EscalationConfig, GateDefinition, ParallelismGroupDefinition, WorkflowDefinition, BranchingDefinition, TemplateRetryConfig, TemplateTimeoutConfig, } from './workflow-types.js';
|
|
8
|
+
export { PhaseDefinitionSchema, PhaseOutputDeclarationSchema, PhaseInputDeclarationSchema, TransitionDefinitionSchema, EscalationLadderRungSchema, EscalationConfigSchema, GateDefinitionSchema, ParallelismGroupDefinitionSchema, WorkflowDefinitionSchema, BranchingDefinitionSchema, TemplateRetryConfigSchema, TemplateTimeoutConfigSchema, validateWorkflowDefinition, } from './workflow-types.js';
|
|
9
9
|
export { loadWorkflowDefinitionFile, getBuiltinWorkflowDir, getBuiltinWorkflowPath, } from './workflow-loader.js';
|
|
10
10
|
export type { WorkflowRegistryConfig, WorkflowStoreSource } from './workflow-registry.js';
|
|
11
11
|
export { WorkflowRegistry } from './workflow-registry.js';
|
|
@@ -18,4 +18,25 @@ export type { ResolvedRetryConfig, ResolvedTimeoutConfig } from './retry-resolve
|
|
|
18
18
|
export { resolveRetryConfig, resolveTimeoutConfig } from './retry-resolver.js';
|
|
19
19
|
export type { EvaluationContext } from './expression/index.js';
|
|
20
20
|
export { buildEvaluationContext, evaluateCondition } from './expression/index.js';
|
|
21
|
+
export type { GateState, GateStorage } from './gate-state.js';
|
|
22
|
+
export { InMemoryGateStorage, initGateStorage, activateGate, satisfyGate, timeoutGate, } from './gate-state.js';
|
|
23
|
+
export { parseDuration as parseGateDuration } from './gate-state.js';
|
|
24
|
+
export type { SignalGateTrigger, SignalGateResult } from './gates/signal-gate.js';
|
|
25
|
+
export { isSignalGateTrigger, evaluateSignalGate, getApplicableSignalGates, createImplicitHoldGate, createImplicitResumeGate, IMPLICIT_HOLD_GATE_NAME, IMPLICIT_RESUME_GATE_NAME, } from './gates/signal-gate.js';
|
|
26
|
+
export type { WebhookGateTrigger, WebhookGateResult, WebhookGateActivation } from './gates/webhook-gate.js';
|
|
27
|
+
export { generateWebhookToken, buildCallbackUrl, validateWebhookCallback, evaluateWebhookGate, isWebhookGateTrigger, getApplicableWebhookGates, createWebhookGateActivation, } from './gates/webhook-gate.js';
|
|
28
|
+
export type { TimeoutCheckResult, TimedOutGate, TimeoutResolution } from './gates/timeout-engine.js';
|
|
29
|
+
export { checkGateTimeout, checkAllGateTimeouts, resolveTimeoutAction, processGateTimeouts, } from './gates/timeout-engine.js';
|
|
30
|
+
export type { TimerGateTrigger, TimerGateResult } from './gates/timer-gate.js';
|
|
31
|
+
export { evaluateTimerGate, computeNextCronFireTime, isTimerGateTrigger, getApplicableTimerGates, parseCronField, parseCronExpression, } from './gates/timer-gate.js';
|
|
32
|
+
export type { GateEvaluationOptions, GateEvaluationResult } from './gates/gate-evaluator.js';
|
|
33
|
+
export { evaluateGatesForPhase, activateGatesForPhase, clearGatesForIssue, getApplicableGates, } from './gates/gate-evaluator.js';
|
|
34
|
+
export { PhaseOutputCollector } from './phase-output-collector.js';
|
|
35
|
+
export { PhaseContextInjector } from './phase-context-injector.js';
|
|
36
|
+
export type { ParallelTask, ParallelTaskResult, ParallelTaskError, ParallelismResult, ParallelismStrategy, ParallelismStrategyOptions, } from './parallelism-types.js';
|
|
37
|
+
export { ConcurrencySemaphore } from './concurrency-semaphore.js';
|
|
38
|
+
export { ParallelismExecutor } from './parallelism-executor.js';
|
|
39
|
+
export type { AgentCancellation } from './agent-cancellation.js';
|
|
40
|
+
export { InMemoryAgentCancellation } from './agent-cancellation.js';
|
|
41
|
+
export { FanOutStrategy, FanInStrategy, RaceStrategy } from './strategies/index.js';
|
|
21
42
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/workflow/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,YAAY,EACV,kBAAkB,EAClB,eAAe,EACf,oBAAoB,EACpB,oBAAoB,EACpB,gBAAgB,EAChB,cAAc,EACd,0BAA0B,EAC1B,kBAAkB,EAClB,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,GACtB,MAAM,qBAAqB,CAAA;AAE5B,OAAO,EACL,qBAAqB,EACrB,0BAA0B,EAC1B,0BAA0B,EAC1B,sBAAsB,EACtB,oBAAoB,EACpB,gCAAgC,EAChC,wBAAwB,EACxB,yBAAyB,EACzB,yBAAyB,EACzB,2BAA2B,EAC3B,0BAA0B,GAC3B,MAAM,qBAAqB,CAAA;AAE5B,OAAO,EACL,0BAA0B,EAC1B,qBAAqB,EACrB,sBAAsB,GACvB,MAAM,sBAAsB,CAAA;AAE7B,YAAY,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAA;AACzF,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAEzD,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AACjF,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAA;AAE5D,YAAY,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAC5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AAGzD,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAA;AAGjE,YAAY,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAA;AACrF,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAA;AAG9E,YAAY,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AAC9D,OAAO,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/workflow/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Declarative workflow graph definitions using YAML (v1.1 schema extension).
|
|
5
5
|
* Defines phases, transitions, escalation ladder, gates, and parallelism.
|
|
6
6
|
*/
|
|
7
|
-
export { PhaseDefinitionSchema, TransitionDefinitionSchema, EscalationLadderRungSchema, EscalationConfigSchema, GateDefinitionSchema, ParallelismGroupDefinitionSchema, WorkflowDefinitionSchema, BranchingDefinitionSchema, TemplateRetryConfigSchema, TemplateTimeoutConfigSchema, validateWorkflowDefinition, } from './workflow-types.js';
|
|
7
|
+
export { PhaseDefinitionSchema, PhaseOutputDeclarationSchema, PhaseInputDeclarationSchema, TransitionDefinitionSchema, EscalationLadderRungSchema, EscalationConfigSchema, GateDefinitionSchema, ParallelismGroupDefinitionSchema, WorkflowDefinitionSchema, BranchingDefinitionSchema, TemplateRetryConfigSchema, TemplateTimeoutConfigSchema, validateWorkflowDefinition, } from './workflow-types.js';
|
|
8
8
|
export { loadWorkflowDefinitionFile, getBuiltinWorkflowDir, getBuiltinWorkflowPath, } from './workflow-loader.js';
|
|
9
9
|
export { WorkflowRegistry } from './workflow-registry.js';
|
|
10
10
|
export { evaluateTransitions } from './transition-engine.js';
|
|
@@ -13,3 +13,17 @@ export { evaluateBranching } from './branching-router.js';
|
|
|
13
13
|
export { parseDuration, DurationParseError } from './duration.js';
|
|
14
14
|
export { resolveRetryConfig, resolveTimeoutConfig } from './retry-resolver.js';
|
|
15
15
|
export { buildEvaluationContext, evaluateCondition } from './expression/index.js';
|
|
16
|
+
export { InMemoryGateStorage, initGateStorage, activateGate, satisfyGate, timeoutGate, } from './gate-state.js';
|
|
17
|
+
export { parseDuration as parseGateDuration } from './gate-state.js';
|
|
18
|
+
export { isSignalGateTrigger, evaluateSignalGate, getApplicableSignalGates, createImplicitHoldGate, createImplicitResumeGate, IMPLICIT_HOLD_GATE_NAME, IMPLICIT_RESUME_GATE_NAME, } from './gates/signal-gate.js';
|
|
19
|
+
export { generateWebhookToken, buildCallbackUrl, validateWebhookCallback, evaluateWebhookGate, isWebhookGateTrigger, getApplicableWebhookGates, createWebhookGateActivation, } from './gates/webhook-gate.js';
|
|
20
|
+
export { checkGateTimeout, checkAllGateTimeouts, resolveTimeoutAction, processGateTimeouts, } from './gates/timeout-engine.js';
|
|
21
|
+
export { evaluateTimerGate, computeNextCronFireTime, isTimerGateTrigger, getApplicableTimerGates, parseCronField, parseCronExpression, } from './gates/timer-gate.js';
|
|
22
|
+
export { evaluateGatesForPhase, activateGatesForPhase, clearGatesForIssue, getApplicableGates, } from './gates/gate-evaluator.js';
|
|
23
|
+
// Phase output/input handling
|
|
24
|
+
export { PhaseOutputCollector } from './phase-output-collector.js';
|
|
25
|
+
export { PhaseContextInjector } from './phase-context-injector.js';
|
|
26
|
+
export { ConcurrencySemaphore } from './concurrency-semaphore.js';
|
|
27
|
+
export { ParallelismExecutor } from './parallelism-executor.js';
|
|
28
|
+
export { InMemoryAgentCancellation } from './agent-cancellation.js';
|
|
29
|
+
export { FanOutStrategy, FanInStrategy, RaceStrategy } from './strategies/index.js';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ParallelismExecutor
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates parallel task execution using strategy pattern.
|
|
5
|
+
* Each parallelism group in the workflow definition is executed
|
|
6
|
+
* through its configured strategy with concurrency limiting.
|
|
7
|
+
*/
|
|
8
|
+
import type { ParallelismGroupDefinition } from './workflow-types.js';
|
|
9
|
+
import type { ParallelTask, ParallelTaskResult, ParallelismResult, ParallelismStrategy } from './parallelism-types.js';
|
|
10
|
+
export declare class ParallelismExecutor {
|
|
11
|
+
private readonly strategies;
|
|
12
|
+
/**
|
|
13
|
+
* Register a strategy implementation for a strategy name.
|
|
14
|
+
*/
|
|
15
|
+
registerStrategy(name: string, strategy: ParallelismStrategy): void;
|
|
16
|
+
/**
|
|
17
|
+
* Get a registered strategy by name.
|
|
18
|
+
*/
|
|
19
|
+
getStrategy(name: string): ParallelismStrategy | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Execute a parallelism group with the configured strategy.
|
|
22
|
+
*/
|
|
23
|
+
execute(group: ParallelismGroupDefinition, tasks: ParallelTask[], dispatch: (task: ParallelTask) => Promise<ParallelTaskResult>): Promise<ParallelismResult>;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=parallelism-executor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parallelism-executor.d.ts","sourceRoot":"","sources":["../../../src/workflow/parallelism-executor.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAA;AACrE,OAAO,KAAK,EACV,YAAY,EACZ,kBAAkB,EAClB,iBAAiB,EACjB,mBAAmB,EAEpB,MAAM,wBAAwB,CAAA;AAG/B,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA8C;IAEzE;;OAEG;IACH,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,mBAAmB,GAAG,IAAI;IAInE;;OAEG;IACH,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS;IAI1D;;OAEG;IACG,OAAO,CACX,KAAK,EAAE,0BAA0B,EACjC,KAAK,EAAE,YAAY,EAAE,EACrB,QAAQ,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,OAAO,CAAC,kBAAkB,CAAC,GAC5D,OAAO,CAAC,iBAAiB,CAAC;CA8B9B"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ParallelismExecutor
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates parallel task execution using strategy pattern.
|
|
5
|
+
* Each parallelism group in the workflow definition is executed
|
|
6
|
+
* through its configured strategy with concurrency limiting.
|
|
7
|
+
*/
|
|
8
|
+
import { ConcurrencySemaphore } from './concurrency-semaphore.js';
|
|
9
|
+
export class ParallelismExecutor {
|
|
10
|
+
strategies = new Map();
|
|
11
|
+
/**
|
|
12
|
+
* Register a strategy implementation for a strategy name.
|
|
13
|
+
*/
|
|
14
|
+
registerStrategy(name, strategy) {
|
|
15
|
+
this.strategies.set(name, strategy);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Get a registered strategy by name.
|
|
19
|
+
*/
|
|
20
|
+
getStrategy(name) {
|
|
21
|
+
return this.strategies.get(name);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Execute a parallelism group with the configured strategy.
|
|
25
|
+
*/
|
|
26
|
+
async execute(group, tasks, dispatch) {
|
|
27
|
+
const strategy = this.strategies.get(group.strategy);
|
|
28
|
+
if (!strategy) {
|
|
29
|
+
throw new Error(`No strategy registered for "${group.strategy}"`);
|
|
30
|
+
}
|
|
31
|
+
// Wrap dispatch with semaphore if maxConcurrent is set
|
|
32
|
+
const semaphore = group.maxConcurrent
|
|
33
|
+
? new ConcurrencySemaphore(group.maxConcurrent)
|
|
34
|
+
: null;
|
|
35
|
+
const wrappedDispatch = async (task) => {
|
|
36
|
+
if (semaphore)
|
|
37
|
+
await semaphore.acquire();
|
|
38
|
+
try {
|
|
39
|
+
return await dispatch(task);
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
if (semaphore)
|
|
43
|
+
semaphore.release();
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const options = {
|
|
47
|
+
maxConcurrent: group.maxConcurrent,
|
|
48
|
+
waitForAll: group.waitForAll,
|
|
49
|
+
dispatch: wrappedDispatch,
|
|
50
|
+
};
|
|
51
|
+
return strategy.execute(tasks, options);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parallelism-executor.test.d.ts","sourceRoot":"","sources":["../../../src/workflow/parallelism-executor.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { ParallelismExecutor } from './parallelism-executor.js';
|
|
3
|
+
/** Helper: create a mock strategy that records calls and delegates to dispatch */
|
|
4
|
+
function createMockStrategy() {
|
|
5
|
+
const executeCalls = [];
|
|
6
|
+
return {
|
|
7
|
+
executeCalls,
|
|
8
|
+
async execute(tasks, options) {
|
|
9
|
+
executeCalls.push({ tasks, options });
|
|
10
|
+
// Use the dispatch function to run all tasks, collecting results
|
|
11
|
+
const completed = [];
|
|
12
|
+
for (const task of tasks) {
|
|
13
|
+
const result = await options.dispatch(task);
|
|
14
|
+
completed.push(result);
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
strategy: 'fan-out',
|
|
18
|
+
completed,
|
|
19
|
+
cancelled: [],
|
|
20
|
+
failed: [],
|
|
21
|
+
outputs: {},
|
|
22
|
+
};
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** Helper: create a simple dispatch function */
|
|
27
|
+
function createDispatch() {
|
|
28
|
+
return async (task) => ({
|
|
29
|
+
id: task.id,
|
|
30
|
+
issueId: task.issueId,
|
|
31
|
+
success: true,
|
|
32
|
+
durationMs: 10,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/** Helper: create a sample group definition */
|
|
36
|
+
function createGroup(overrides = {}) {
|
|
37
|
+
return {
|
|
38
|
+
name: 'test-group',
|
|
39
|
+
phases: ['development'],
|
|
40
|
+
strategy: 'fan-out',
|
|
41
|
+
...overrides,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** Helper: create sample tasks */
|
|
45
|
+
function createTasks(count) {
|
|
46
|
+
return Array.from({ length: count }, (_, i) => ({
|
|
47
|
+
id: `task-${i + 1}`,
|
|
48
|
+
issueId: `SUP-${100 + i + 1}`,
|
|
49
|
+
phaseName: 'development',
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
describe('ParallelismExecutor', () => {
|
|
53
|
+
describe('registerStrategy and getStrategy', () => {
|
|
54
|
+
it('registers and retrieves a strategy', () => {
|
|
55
|
+
const executor = new ParallelismExecutor();
|
|
56
|
+
const strategy = createMockStrategy();
|
|
57
|
+
executor.registerStrategy('fan-out', strategy);
|
|
58
|
+
expect(executor.getStrategy('fan-out')).toBe(strategy);
|
|
59
|
+
});
|
|
60
|
+
it('returns undefined for unregistered strategy', () => {
|
|
61
|
+
const executor = new ParallelismExecutor();
|
|
62
|
+
expect(executor.getStrategy('fan-out')).toBeUndefined();
|
|
63
|
+
});
|
|
64
|
+
it('overwrites a previously registered strategy', () => {
|
|
65
|
+
const executor = new ParallelismExecutor();
|
|
66
|
+
const strategy1 = createMockStrategy();
|
|
67
|
+
const strategy2 = createMockStrategy();
|
|
68
|
+
executor.registerStrategy('fan-out', strategy1);
|
|
69
|
+
executor.registerStrategy('fan-out', strategy2);
|
|
70
|
+
expect(executor.getStrategy('fan-out')).toBe(strategy2);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
describe('execute with mock strategy', () => {
|
|
74
|
+
it('delegates to the registered strategy and returns result', async () => {
|
|
75
|
+
const executor = new ParallelismExecutor();
|
|
76
|
+
const strategy = createMockStrategy();
|
|
77
|
+
executor.registerStrategy('fan-out', strategy);
|
|
78
|
+
const group = createGroup();
|
|
79
|
+
const tasks = createTasks(3);
|
|
80
|
+
const dispatch = createDispatch();
|
|
81
|
+
const result = await executor.execute(group, tasks, dispatch);
|
|
82
|
+
expect(result.strategy).toBe('fan-out');
|
|
83
|
+
expect(result.completed).toHaveLength(3);
|
|
84
|
+
expect(result.completed[0].issueId).toBe('SUP-101');
|
|
85
|
+
expect(result.completed[1].issueId).toBe('SUP-102');
|
|
86
|
+
expect(result.completed[2].issueId).toBe('SUP-103');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
describe('execute throws when strategy not registered', () => {
|
|
90
|
+
it('throws with a descriptive error', async () => {
|
|
91
|
+
const executor = new ParallelismExecutor();
|
|
92
|
+
const group = createGroup({ strategy: 'race' });
|
|
93
|
+
const tasks = createTasks(1);
|
|
94
|
+
const dispatch = createDispatch();
|
|
95
|
+
await expect(executor.execute(group, tasks, dispatch)).rejects.toThrow('No strategy registered for "race"');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe('execute wraps dispatch with semaphore when maxConcurrent set', () => {
|
|
99
|
+
it('limits concurrency via semaphore', async () => {
|
|
100
|
+
const executor = new ParallelismExecutor();
|
|
101
|
+
let peakConcurrent = 0;
|
|
102
|
+
let currentConcurrent = 0;
|
|
103
|
+
// Create a strategy that dispatches all tasks in parallel
|
|
104
|
+
const concurrentStrategy = {
|
|
105
|
+
async execute(tasks, options) {
|
|
106
|
+
const promises = tasks.map((task) => options.dispatch(task));
|
|
107
|
+
const completed = await Promise.all(promises);
|
|
108
|
+
return {
|
|
109
|
+
strategy: 'fan-out',
|
|
110
|
+
completed,
|
|
111
|
+
cancelled: [],
|
|
112
|
+
failed: [],
|
|
113
|
+
outputs: {},
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
executor.registerStrategy('fan-out', concurrentStrategy);
|
|
118
|
+
const dispatch = async (task) => {
|
|
119
|
+
currentConcurrent++;
|
|
120
|
+
peakConcurrent = Math.max(peakConcurrent, currentConcurrent);
|
|
121
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
122
|
+
currentConcurrent--;
|
|
123
|
+
return { id: task.id, issueId: task.issueId, success: true };
|
|
124
|
+
};
|
|
125
|
+
const group = createGroup({ maxConcurrent: 2 });
|
|
126
|
+
const tasks = createTasks(5);
|
|
127
|
+
await executor.execute(group, tasks, dispatch);
|
|
128
|
+
expect(peakConcurrent).toBeLessThanOrEqual(2);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
describe('execute passes options correctly', () => {
|
|
132
|
+
it('passes maxConcurrent and waitForAll to strategy', async () => {
|
|
133
|
+
const executor = new ParallelismExecutor();
|
|
134
|
+
const strategy = createMockStrategy();
|
|
135
|
+
executor.registerStrategy('fan-out', strategy);
|
|
136
|
+
const group = createGroup({ maxConcurrent: 3, waitForAll: true });
|
|
137
|
+
const tasks = createTasks(1);
|
|
138
|
+
const dispatch = createDispatch();
|
|
139
|
+
await executor.execute(group, tasks, dispatch);
|
|
140
|
+
expect(strategy.executeCalls).toHaveLength(1);
|
|
141
|
+
const passedOptions = strategy.executeCalls[0].options;
|
|
142
|
+
expect(passedOptions.maxConcurrent).toBe(3);
|
|
143
|
+
expect(passedOptions.waitForAll).toBe(true);
|
|
144
|
+
expect(typeof passedOptions.dispatch).toBe('function');
|
|
145
|
+
});
|
|
146
|
+
it('passes tasks through to the strategy', async () => {
|
|
147
|
+
const executor = new ParallelismExecutor();
|
|
148
|
+
const strategy = createMockStrategy();
|
|
149
|
+
executor.registerStrategy('fan-out', strategy);
|
|
150
|
+
const group = createGroup();
|
|
151
|
+
const tasks = createTasks(2);
|
|
152
|
+
const dispatch = createDispatch();
|
|
153
|
+
await executor.execute(group, tasks, dispatch);
|
|
154
|
+
expect(strategy.executeCalls[0].tasks).toBe(tasks);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
describe('execute without maxConcurrent (no semaphore wrapping)', () => {
|
|
158
|
+
it('does not limit concurrency when maxConcurrent is not set', async () => {
|
|
159
|
+
const executor = new ParallelismExecutor();
|
|
160
|
+
let peakConcurrent = 0;
|
|
161
|
+
let currentConcurrent = 0;
|
|
162
|
+
const concurrentStrategy = {
|
|
163
|
+
async execute(tasks, options) {
|
|
164
|
+
const promises = tasks.map((task) => options.dispatch(task));
|
|
165
|
+
const completed = await Promise.all(promises);
|
|
166
|
+
return {
|
|
167
|
+
strategy: 'fan-out',
|
|
168
|
+
completed,
|
|
169
|
+
cancelled: [],
|
|
170
|
+
failed: [],
|
|
171
|
+
outputs: {},
|
|
172
|
+
};
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
executor.registerStrategy('fan-out', concurrentStrategy);
|
|
176
|
+
const dispatch = async (task) => {
|
|
177
|
+
currentConcurrent++;
|
|
178
|
+
peakConcurrent = Math.max(peakConcurrent, currentConcurrent);
|
|
179
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
180
|
+
currentConcurrent--;
|
|
181
|
+
return { id: task.id, issueId: task.issueId, success: true };
|
|
182
|
+
};
|
|
183
|
+
// No maxConcurrent set on the group
|
|
184
|
+
const group = createGroup();
|
|
185
|
+
const tasks = createTasks(5);
|
|
186
|
+
await executor.execute(group, tasks, dispatch);
|
|
187
|
+
// All 5 tasks should have been able to run concurrently
|
|
188
|
+
expect(peakConcurrent).toBe(5);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parallelism Types
|
|
3
|
+
*
|
|
4
|
+
* Type definitions for the parallel task execution system.
|
|
5
|
+
* These types define the contract between the ParallelismExecutor,
|
|
6
|
+
* strategy implementations, and calling code.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* A single task to be executed in parallel.
|
|
10
|
+
*/
|
|
11
|
+
export interface ParallelTask {
|
|
12
|
+
/** Unique task identifier (typically the issue identifier) */
|
|
13
|
+
id: string;
|
|
14
|
+
/** Issue identifier (e.g., "SUP-123") */
|
|
15
|
+
issueId: string;
|
|
16
|
+
/** Phase name this task belongs to */
|
|
17
|
+
phaseName: string;
|
|
18
|
+
/** Additional context for the task */
|
|
19
|
+
context?: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Result of a single parallel task execution.
|
|
23
|
+
*/
|
|
24
|
+
export interface ParallelTaskResult {
|
|
25
|
+
/** Task identifier */
|
|
26
|
+
id: string;
|
|
27
|
+
/** Issue identifier */
|
|
28
|
+
issueId: string;
|
|
29
|
+
/** Whether the task completed successfully */
|
|
30
|
+
success: boolean;
|
|
31
|
+
/** Collected phase outputs (if any) */
|
|
32
|
+
outputs?: Record<string, unknown>;
|
|
33
|
+
/** Duration in milliseconds */
|
|
34
|
+
durationMs?: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Error details for a failed parallel task.
|
|
38
|
+
*/
|
|
39
|
+
export interface ParallelTaskError {
|
|
40
|
+
/** Task identifier */
|
|
41
|
+
id: string;
|
|
42
|
+
/** Issue identifier */
|
|
43
|
+
issueId: string;
|
|
44
|
+
/** Error message */
|
|
45
|
+
error: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Result of executing a parallelism group.
|
|
49
|
+
*/
|
|
50
|
+
export interface ParallelismResult {
|
|
51
|
+
/** Strategy that was used */
|
|
52
|
+
strategy: 'fan-out' | 'fan-in' | 'race';
|
|
53
|
+
/** Tasks that completed (successfully or not) */
|
|
54
|
+
completed: ParallelTaskResult[];
|
|
55
|
+
/** Issue IDs that were cancelled (race strategy) */
|
|
56
|
+
cancelled: string[];
|
|
57
|
+
/** Tasks that failed with errors */
|
|
58
|
+
failed: ParallelTaskError[];
|
|
59
|
+
/** Per-issue phase outputs collected from completed tasks */
|
|
60
|
+
outputs: Record<string, Record<string, unknown>>;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Interface for parallelism strategy implementations.
|
|
64
|
+
*/
|
|
65
|
+
export interface ParallelismStrategy {
|
|
66
|
+
/** Execute a set of parallel tasks with the given options */
|
|
67
|
+
execute(tasks: ParallelTask[], options: ParallelismStrategyOptions): Promise<ParallelismResult>;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Options passed to strategy implementations.
|
|
71
|
+
*/
|
|
72
|
+
export interface ParallelismStrategyOptions {
|
|
73
|
+
/** Maximum concurrent executions */
|
|
74
|
+
maxConcurrent?: number;
|
|
75
|
+
/** Whether to wait for all tasks to complete */
|
|
76
|
+
waitForAll?: boolean;
|
|
77
|
+
/** Dispatch function that actually starts the agent work */
|
|
78
|
+
dispatch: (task: ParallelTask) => Promise<ParallelTaskResult>;
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=parallelism-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parallelism-types.d.ts","sourceRoot":"","sources":["../../../src/workflow/parallelism-types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,8DAA8D;IAC9D,EAAE,EAAE,MAAM,CAAA;IACV,yCAAyC;IACzC,OAAO,EAAE,MAAM,CAAA;IACf,sCAAsC;IACtC,SAAS,EAAE,MAAM,CAAA;IACjB,sCAAsC;IACtC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAClC;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,sBAAsB;IACtB,EAAE,EAAE,MAAM,CAAA;IACV,uBAAuB;IACvB,OAAO,EAAE,MAAM,CAAA;IACf,8CAA8C;IAC9C,OAAO,EAAE,OAAO,CAAA;IAChB,uCAAuC;IACvC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACjC,+BAA+B;IAC/B,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,sBAAsB;IACtB,EAAE,EAAE,MAAM,CAAA;IACV,uBAAuB;IACvB,OAAO,EAAE,MAAM,CAAA;IACf,oBAAoB;IACpB,KAAK,EAAE,MAAM,CAAA;CACd;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,6BAA6B;IAC7B,QAAQ,EAAE,SAAS,GAAG,QAAQ,GAAG,MAAM,CAAA;IACvC,iDAAiD;IACjD,SAAS,EAAE,kBAAkB,EAAE,CAAA;IAC/B,oDAAoD;IACpD,SAAS,EAAE,MAAM,EAAE,CAAA;IACnB,oCAAoC;IACpC,MAAM,EAAE,iBAAiB,EAAE,CAAA;IAC3B,6DAA6D;IAC7D,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;CACjD;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,6DAA6D;IAC7D,OAAO,CACL,KAAK,EAAE,YAAY,EAAE,EACrB,OAAO,EAAE,0BAA0B,GAClC,OAAO,CAAC,iBAAiB,CAAC,CAAA;CAC9B;AAED;;GAEG;AACH,MAAM,WAAW,0BAA0B;IACzC,oCAAoC;IACpC,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,gDAAgD;IAChD,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,4DAA4D;IAC5D,QAAQ,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAAA;CAC9D"}
|