@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 @@
|
|
|
1
|
+
{"version":3,"file":"concurrency-semaphore.d.ts","sourceRoot":"","sources":["../../../src/workflow/concurrency-semaphore.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,qBAAa,oBAAoB;IAInB,OAAO,CAAC,QAAQ,CAAC,aAAa;IAH1C,OAAO,CAAC,OAAO,CAAI;IACnB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;gBAEnB,aAAa,EAAE,MAAM;IAMlD,qCAAqC;IACrC,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,qCAAqC;IACrC,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,yDAAyD;IACnD,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAa9B,uDAAuD;IACvD,OAAO,IAAI,IAAI;CAOhB"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Concurrency Semaphore
|
|
3
|
+
*
|
|
4
|
+
* Limits the number of concurrent operations. When the limit is reached,
|
|
5
|
+
* subsequent acquire() calls wait until a slot is released.
|
|
6
|
+
*/
|
|
7
|
+
export class ConcurrencySemaphore {
|
|
8
|
+
maxConcurrent;
|
|
9
|
+
current = 0;
|
|
10
|
+
waiters = [];
|
|
11
|
+
constructor(maxConcurrent) {
|
|
12
|
+
this.maxConcurrent = maxConcurrent;
|
|
13
|
+
if (maxConcurrent < 1) {
|
|
14
|
+
throw new Error('maxConcurrent must be at least 1');
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Current number of active slots */
|
|
18
|
+
get activeCount() {
|
|
19
|
+
return this.current;
|
|
20
|
+
}
|
|
21
|
+
/** Number of waiters in the queue */
|
|
22
|
+
get waitingCount() {
|
|
23
|
+
return this.waiters.length;
|
|
24
|
+
}
|
|
25
|
+
/** Acquire a slot. Resolves when a slot is available. */
|
|
26
|
+
async acquire() {
|
|
27
|
+
if (this.current < this.maxConcurrent) {
|
|
28
|
+
this.current++;
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
this.waiters.push(() => {
|
|
33
|
+
this.current++;
|
|
34
|
+
resolve();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/** Release a slot. Wakes up the next waiter if any. */
|
|
39
|
+
release() {
|
|
40
|
+
this.current--;
|
|
41
|
+
if (this.waiters.length > 0) {
|
|
42
|
+
const next = this.waiters.shift();
|
|
43
|
+
next();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"concurrency-semaphore.test.d.ts","sourceRoot":"","sources":["../../../src/workflow/concurrency-semaphore.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { ConcurrencySemaphore } from './concurrency-semaphore.js';
|
|
3
|
+
describe('ConcurrencySemaphore', () => {
|
|
4
|
+
describe('construction', () => {
|
|
5
|
+
it('creates with valid maxConcurrent', () => {
|
|
6
|
+
const sem = new ConcurrencySemaphore(3);
|
|
7
|
+
expect(sem.activeCount).toBe(0);
|
|
8
|
+
expect(sem.waitingCount).toBe(0);
|
|
9
|
+
});
|
|
10
|
+
it('creates with maxConcurrent = 1', () => {
|
|
11
|
+
const sem = new ConcurrencySemaphore(1);
|
|
12
|
+
expect(sem.activeCount).toBe(0);
|
|
13
|
+
});
|
|
14
|
+
it('throws when maxConcurrent is 0', () => {
|
|
15
|
+
expect(() => new ConcurrencySemaphore(0)).toThrow('maxConcurrent must be at least 1');
|
|
16
|
+
});
|
|
17
|
+
it('throws when maxConcurrent is negative', () => {
|
|
18
|
+
expect(() => new ConcurrencySemaphore(-1)).toThrow('maxConcurrent must be at least 1');
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
describe('acquire/release basic flow', () => {
|
|
22
|
+
it('acquires immediately when under capacity', async () => {
|
|
23
|
+
const sem = new ConcurrencySemaphore(2);
|
|
24
|
+
await sem.acquire();
|
|
25
|
+
expect(sem.activeCount).toBe(1);
|
|
26
|
+
await sem.acquire();
|
|
27
|
+
expect(sem.activeCount).toBe(2);
|
|
28
|
+
});
|
|
29
|
+
it('releases correctly and decrements activeCount', async () => {
|
|
30
|
+
const sem = new ConcurrencySemaphore(2);
|
|
31
|
+
await sem.acquire();
|
|
32
|
+
await sem.acquire();
|
|
33
|
+
expect(sem.activeCount).toBe(2);
|
|
34
|
+
sem.release();
|
|
35
|
+
expect(sem.activeCount).toBe(1);
|
|
36
|
+
sem.release();
|
|
37
|
+
expect(sem.activeCount).toBe(0);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
describe('blocking behavior', () => {
|
|
41
|
+
it('blocks acquire when at capacity', async () => {
|
|
42
|
+
const sem = new ConcurrencySemaphore(1);
|
|
43
|
+
await sem.acquire();
|
|
44
|
+
expect(sem.activeCount).toBe(1);
|
|
45
|
+
let acquired = false;
|
|
46
|
+
const pending = sem.acquire().then(() => {
|
|
47
|
+
acquired = true;
|
|
48
|
+
});
|
|
49
|
+
// The waiter should be queued but not yet resolved
|
|
50
|
+
await Promise.resolve(); // flush microtasks
|
|
51
|
+
expect(acquired).toBe(false);
|
|
52
|
+
expect(sem.waitingCount).toBe(1);
|
|
53
|
+
// Release to unblock
|
|
54
|
+
sem.release();
|
|
55
|
+
await pending;
|
|
56
|
+
expect(acquired).toBe(true);
|
|
57
|
+
expect(sem.activeCount).toBe(1);
|
|
58
|
+
expect(sem.waitingCount).toBe(0);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
describe('FIFO waiter ordering', () => {
|
|
62
|
+
it('releases waiters in FIFO order', async () => {
|
|
63
|
+
const sem = new ConcurrencySemaphore(1);
|
|
64
|
+
await sem.acquire();
|
|
65
|
+
const order = [];
|
|
66
|
+
const p1 = sem.acquire().then(() => {
|
|
67
|
+
order.push(1);
|
|
68
|
+
});
|
|
69
|
+
const p2 = sem.acquire().then(() => {
|
|
70
|
+
order.push(2);
|
|
71
|
+
});
|
|
72
|
+
const p3 = sem.acquire().then(() => {
|
|
73
|
+
order.push(3);
|
|
74
|
+
});
|
|
75
|
+
expect(sem.waitingCount).toBe(3);
|
|
76
|
+
// Release one at a time and let the microtask queue flush
|
|
77
|
+
sem.release();
|
|
78
|
+
await p1;
|
|
79
|
+
sem.release();
|
|
80
|
+
await p2;
|
|
81
|
+
sem.release();
|
|
82
|
+
await p3;
|
|
83
|
+
expect(order).toEqual([1, 2, 3]);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
describe('activeCount and waitingCount tracking', () => {
|
|
87
|
+
it('tracks counts accurately through lifecycle', async () => {
|
|
88
|
+
const sem = new ConcurrencySemaphore(2);
|
|
89
|
+
expect(sem.activeCount).toBe(0);
|
|
90
|
+
expect(sem.waitingCount).toBe(0);
|
|
91
|
+
await sem.acquire();
|
|
92
|
+
expect(sem.activeCount).toBe(1);
|
|
93
|
+
expect(sem.waitingCount).toBe(0);
|
|
94
|
+
await sem.acquire();
|
|
95
|
+
expect(sem.activeCount).toBe(2);
|
|
96
|
+
expect(sem.waitingCount).toBe(0);
|
|
97
|
+
// Third acquire should wait
|
|
98
|
+
const pending = sem.acquire();
|
|
99
|
+
// Flush microtasks to make sure the promise callback has run
|
|
100
|
+
await Promise.resolve();
|
|
101
|
+
expect(sem.activeCount).toBe(2);
|
|
102
|
+
expect(sem.waitingCount).toBe(1);
|
|
103
|
+
sem.release();
|
|
104
|
+
await pending;
|
|
105
|
+
expect(sem.activeCount).toBe(2);
|
|
106
|
+
expect(sem.waitingCount).toBe(0);
|
|
107
|
+
sem.release();
|
|
108
|
+
expect(sem.activeCount).toBe(1);
|
|
109
|
+
sem.release();
|
|
110
|
+
expect(sem.activeCount).toBe(0);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
describe('maxConcurrent=1 enforces serial execution', () => {
|
|
114
|
+
it('only one task runs at a time', async () => {
|
|
115
|
+
const sem = new ConcurrencySemaphore(1);
|
|
116
|
+
const log = [];
|
|
117
|
+
const runTask = async (name) => {
|
|
118
|
+
await sem.acquire();
|
|
119
|
+
log.push(`${name}:start`);
|
|
120
|
+
// Simulate async work
|
|
121
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
122
|
+
log.push(`${name}:end`);
|
|
123
|
+
sem.release();
|
|
124
|
+
};
|
|
125
|
+
await Promise.all([runTask('a'), runTask('b'), runTask('c')]);
|
|
126
|
+
// Each task must start after the previous one ends
|
|
127
|
+
// The pattern should be: start, end, start, end, start, end
|
|
128
|
+
for (let i = 0; i < log.length - 1; i += 2) {
|
|
129
|
+
expect(log[i]).toMatch(/:start$/);
|
|
130
|
+
expect(log[i + 1]).toMatch(/:end$/);
|
|
131
|
+
}
|
|
132
|
+
// Verify no two starts happen before an end
|
|
133
|
+
let active = 0;
|
|
134
|
+
for (const entry of log) {
|
|
135
|
+
if (entry.endsWith(':start'))
|
|
136
|
+
active++;
|
|
137
|
+
if (entry.endsWith(':end'))
|
|
138
|
+
active--;
|
|
139
|
+
expect(active).toBeLessThanOrEqual(1);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
describe('maxConcurrent=2 with 5 tasks verifies queuing behavior', () => {
|
|
144
|
+
it('runs at most 2 tasks concurrently', async () => {
|
|
145
|
+
const sem = new ConcurrencySemaphore(2);
|
|
146
|
+
let peakConcurrent = 0;
|
|
147
|
+
let currentConcurrent = 0;
|
|
148
|
+
const taskOrder = [];
|
|
149
|
+
const runTask = async (id) => {
|
|
150
|
+
await sem.acquire();
|
|
151
|
+
currentConcurrent++;
|
|
152
|
+
peakConcurrent = Math.max(peakConcurrent, currentConcurrent);
|
|
153
|
+
taskOrder.push(`start:${id}`);
|
|
154
|
+
// Simulate varying amounts of work
|
|
155
|
+
await new Promise((resolve) => setTimeout(resolve, 5 + id * 2));
|
|
156
|
+
taskOrder.push(`end:${id}`);
|
|
157
|
+
currentConcurrent--;
|
|
158
|
+
sem.release();
|
|
159
|
+
};
|
|
160
|
+
await Promise.all([
|
|
161
|
+
runTask(1),
|
|
162
|
+
runTask(2),
|
|
163
|
+
runTask(3),
|
|
164
|
+
runTask(4),
|
|
165
|
+
runTask(5),
|
|
166
|
+
]);
|
|
167
|
+
// Peak concurrency should never exceed 2
|
|
168
|
+
expect(peakConcurrent).toBe(2);
|
|
169
|
+
// All 5 tasks should have started and ended
|
|
170
|
+
expect(taskOrder.filter((e) => e.startsWith('start:'))).toHaveLength(5);
|
|
171
|
+
expect(taskOrder.filter((e) => e.startsWith('end:'))).toHaveLength(5);
|
|
172
|
+
// Verify at no point more than 2 tasks are active
|
|
173
|
+
let active = 0;
|
|
174
|
+
for (const entry of taskOrder) {
|
|
175
|
+
if (entry.startsWith('start:'))
|
|
176
|
+
active++;
|
|
177
|
+
if (entry.startsWith('end:'))
|
|
178
|
+
active--;
|
|
179
|
+
expect(active).toBeLessThanOrEqual(2);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate State Persistence Layer
|
|
3
|
+
*
|
|
4
|
+
* Manages gate lifecycle state for workflow execution gates (signal, timer, webhook).
|
|
5
|
+
* Uses a storage adapter pattern so that packages/core does not depend on
|
|
6
|
+
* packages/server (Redis) directly.
|
|
7
|
+
*
|
|
8
|
+
* Gates are external conditions that can pause workflow phase transitions until
|
|
9
|
+
* a condition is met (e.g., human approval signal, timer expiration, webhook callback).
|
|
10
|
+
*/
|
|
11
|
+
import type { GateDefinition } from './workflow-types.js';
|
|
12
|
+
/**
|
|
13
|
+
* Persisted state for a single gate instance tied to an issue
|
|
14
|
+
*/
|
|
15
|
+
export interface GateState {
|
|
16
|
+
/** The issue this gate is associated with */
|
|
17
|
+
issueId: string;
|
|
18
|
+
/** Unique gate name (from GateDefinition) */
|
|
19
|
+
gateName: string;
|
|
20
|
+
/** Gate type: signal (external event), timer (time-based), webhook (HTTP callback) */
|
|
21
|
+
gateType: 'signal' | 'timer' | 'webhook';
|
|
22
|
+
/** Current gate status */
|
|
23
|
+
status: 'pending' | 'active' | 'satisfied' | 'timed-out';
|
|
24
|
+
/** When the gate was triggered (phase entered), epoch ms */
|
|
25
|
+
activatedAt: number;
|
|
26
|
+
/** When the gate condition was met, epoch ms */
|
|
27
|
+
satisfiedAt?: number;
|
|
28
|
+
/** When the timeout fired, epoch ms */
|
|
29
|
+
timedOutAt?: number;
|
|
30
|
+
/** Action to take when gate times out */
|
|
31
|
+
timeoutAction?: 'escalate' | 'skip' | 'fail';
|
|
32
|
+
/** What satisfied the gate (comment ID, webhook payload hash, timer ID) */
|
|
33
|
+
signalSource?: string;
|
|
34
|
+
/** Authentication token for webhook gates (used to validate incoming callbacks) */
|
|
35
|
+
webhookToken?: string;
|
|
36
|
+
/** Duration string from gate definition (e.g., "4h") */
|
|
37
|
+
timeoutDuration?: string;
|
|
38
|
+
/** Computed absolute timestamp for timeout deadline, epoch ms */
|
|
39
|
+
timeoutDeadline?: number;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Storage adapter for gate state persistence.
|
|
43
|
+
* Implementations can back this with Redis, in-memory maps, etc.
|
|
44
|
+
*/
|
|
45
|
+
export interface GateStorage {
|
|
46
|
+
/** Get the state of a specific gate for an issue */
|
|
47
|
+
getGateState(issueId: string, gateName: string): Promise<GateState | null>;
|
|
48
|
+
/** Set the state of a specific gate for an issue */
|
|
49
|
+
setGateState(issueId: string, gateName: string, state: GateState): Promise<void>;
|
|
50
|
+
/** Get all active (status === 'active') gates for an issue */
|
|
51
|
+
getActiveGates(issueId: string): Promise<GateState[]>;
|
|
52
|
+
/** Clear all gate states for an issue */
|
|
53
|
+
clearGateStates(issueId: string): Promise<void>;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* In-memory gate storage for testing and local development
|
|
57
|
+
*/
|
|
58
|
+
export declare class InMemoryGateStorage implements GateStorage {
|
|
59
|
+
private store;
|
|
60
|
+
/** Build a composite key from issueId and gateName */
|
|
61
|
+
private key;
|
|
62
|
+
getGateState(issueId: string, gateName: string): Promise<GateState | null>;
|
|
63
|
+
setGateState(issueId: string, gateName: string, state: GateState): Promise<void>;
|
|
64
|
+
getActiveGates(issueId: string): Promise<GateState[]>;
|
|
65
|
+
clearGateStates(issueId: string): Promise<void>;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Initialize the gate state manager with a storage adapter.
|
|
69
|
+
* Must be called before using gate state management functions.
|
|
70
|
+
*/
|
|
71
|
+
export declare function initGateStorage(storage: GateStorage): void;
|
|
72
|
+
/**
|
|
73
|
+
* Parse a duration string into milliseconds.
|
|
74
|
+
*
|
|
75
|
+
* Supported formats:
|
|
76
|
+
* - "15s" - seconds
|
|
77
|
+
* - "30m" - minutes
|
|
78
|
+
* - "4h" - hours
|
|
79
|
+
* - "2d" - days
|
|
80
|
+
*
|
|
81
|
+
* @param duration - Duration string (e.g., "4h", "30m", "2d", "15s")
|
|
82
|
+
* @returns Duration in milliseconds
|
|
83
|
+
* @throws Error if the duration format is invalid
|
|
84
|
+
*/
|
|
85
|
+
export declare function parseDuration(duration: string): number;
|
|
86
|
+
/**
|
|
87
|
+
* Activate a gate for an issue, creating an active gate state with a computed
|
|
88
|
+
* timeout deadline (if the gate definition includes a timeout).
|
|
89
|
+
*
|
|
90
|
+
* @param issueId - The issue identifier
|
|
91
|
+
* @param gateDef - The gate definition from the workflow
|
|
92
|
+
* @param storage - The gate storage adapter to use
|
|
93
|
+
* @returns The created GateState
|
|
94
|
+
*/
|
|
95
|
+
export declare function activateGate(issueId: string, gateDef: GateDefinition, storage: GateStorage): Promise<GateState>;
|
|
96
|
+
/**
|
|
97
|
+
* Mark a gate as satisfied, recording what source satisfied it.
|
|
98
|
+
*
|
|
99
|
+
* @param issueId - The issue identifier
|
|
100
|
+
* @param gateName - The gate name to satisfy
|
|
101
|
+
* @param source - What satisfied the gate (e.g., comment ID, webhook payload hash, timer ID)
|
|
102
|
+
* @param storage - The gate storage adapter to use
|
|
103
|
+
* @returns The updated GateState, or null if the gate was not found
|
|
104
|
+
*/
|
|
105
|
+
export declare function satisfyGate(issueId: string, gateName: string, source: string, storage: GateStorage): Promise<GateState | null>;
|
|
106
|
+
/**
|
|
107
|
+
* Mark a gate as timed-out.
|
|
108
|
+
*
|
|
109
|
+
* @param issueId - The issue identifier
|
|
110
|
+
* @param gateName - The gate name to time out
|
|
111
|
+
* @param storage - The gate storage adapter to use
|
|
112
|
+
* @returns The updated GateState, or null if the gate was not found
|
|
113
|
+
*/
|
|
114
|
+
export declare function timeoutGate(issueId: string, gateName: string, storage: GateStorage): Promise<GateState | null>;
|
|
115
|
+
//# sourceMappingURL=gate-state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gate-state.d.ts","sourceRoot":"","sources":["../../../src/workflow/gate-state.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate State Persistence Layer
|
|
3
|
+
*
|
|
4
|
+
* Manages gate lifecycle state for workflow execution gates (signal, timer, webhook).
|
|
5
|
+
* Uses a storage adapter pattern so that packages/core does not depend on
|
|
6
|
+
* packages/server (Redis) directly.
|
|
7
|
+
*
|
|
8
|
+
* Gates are external conditions that can pause workflow phase transitions until
|
|
9
|
+
* a condition is met (e.g., human approval signal, timer expiration, webhook callback).
|
|
10
|
+
*/
|
|
11
|
+
const log = {
|
|
12
|
+
info: (msg, data) => console.log(`[gate-state] ${msg}`, data ? JSON.stringify(data) : ''),
|
|
13
|
+
warn: (msg, data) => console.warn(`[gate-state] ${msg}`, data ? JSON.stringify(data) : ''),
|
|
14
|
+
error: (msg, data) => console.error(`[gate-state] ${msg}`, data ? JSON.stringify(data) : ''),
|
|
15
|
+
debug: (_msg, _data) => { },
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* In-memory gate storage for testing and local development
|
|
19
|
+
*/
|
|
20
|
+
export class InMemoryGateStorage {
|
|
21
|
+
store = new Map();
|
|
22
|
+
/** Build a composite key from issueId and gateName */
|
|
23
|
+
key(issueId, gateName) {
|
|
24
|
+
return `${issueId}:${gateName}`;
|
|
25
|
+
}
|
|
26
|
+
async getGateState(issueId, gateName) {
|
|
27
|
+
return this.store.get(this.key(issueId, gateName)) ?? null;
|
|
28
|
+
}
|
|
29
|
+
async setGateState(issueId, gateName, state) {
|
|
30
|
+
this.store.set(this.key(issueId, gateName), state);
|
|
31
|
+
}
|
|
32
|
+
async getActiveGates(issueId) {
|
|
33
|
+
const results = [];
|
|
34
|
+
for (const [key, state] of this.store) {
|
|
35
|
+
if (key.startsWith(`${issueId}:`) && state.status === 'active') {
|
|
36
|
+
results.push(state);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return results;
|
|
40
|
+
}
|
|
41
|
+
async clearGateStates(issueId) {
|
|
42
|
+
const keysToDelete = [];
|
|
43
|
+
for (const key of this.store.keys()) {
|
|
44
|
+
if (key.startsWith(`${issueId}:`)) {
|
|
45
|
+
keysToDelete.push(key);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
for (const key of keysToDelete) {
|
|
49
|
+
this.store.delete(key);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// ============================================
|
|
54
|
+
// Module-level storage reference
|
|
55
|
+
// ============================================
|
|
56
|
+
let _storage = null;
|
|
57
|
+
/**
|
|
58
|
+
* Initialize the gate state manager with a storage adapter.
|
|
59
|
+
* Must be called before using gate state management functions.
|
|
60
|
+
*/
|
|
61
|
+
export function initGateStorage(storage) {
|
|
62
|
+
_storage = storage;
|
|
63
|
+
log.info('Gate storage initialized');
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Get the current storage adapter, throwing if not initialized
|
|
67
|
+
*/
|
|
68
|
+
function getStorage() {
|
|
69
|
+
if (!_storage) {
|
|
70
|
+
throw new Error('Gate storage not initialized. Call initGateStorage() first.');
|
|
71
|
+
}
|
|
72
|
+
return _storage;
|
|
73
|
+
}
|
|
74
|
+
// ============================================
|
|
75
|
+
// Duration Parsing
|
|
76
|
+
// ============================================
|
|
77
|
+
/**
|
|
78
|
+
* Parse a duration string into milliseconds.
|
|
79
|
+
*
|
|
80
|
+
* Supported formats:
|
|
81
|
+
* - "15s" - seconds
|
|
82
|
+
* - "30m" - minutes
|
|
83
|
+
* - "4h" - hours
|
|
84
|
+
* - "2d" - days
|
|
85
|
+
*
|
|
86
|
+
* @param duration - Duration string (e.g., "4h", "30m", "2d", "15s")
|
|
87
|
+
* @returns Duration in milliseconds
|
|
88
|
+
* @throws Error if the duration format is invalid
|
|
89
|
+
*/
|
|
90
|
+
export function parseDuration(duration) {
|
|
91
|
+
const match = duration.match(/^(\d+)(s|m|h|d)$/);
|
|
92
|
+
if (!match) {
|
|
93
|
+
throw new Error(`Invalid duration format: "${duration}". Expected format like "4h", "30m", "2d", "15s".`);
|
|
94
|
+
}
|
|
95
|
+
const value = parseInt(match[1], 10);
|
|
96
|
+
const unit = match[2];
|
|
97
|
+
switch (unit) {
|
|
98
|
+
case 's': return value * 1000;
|
|
99
|
+
case 'm': return value * 60 * 1000;
|
|
100
|
+
case 'h': return value * 60 * 60 * 1000;
|
|
101
|
+
case 'd': return value * 24 * 60 * 60 * 1000;
|
|
102
|
+
default: throw new Error(`Unknown duration unit: "${unit}"`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// ============================================
|
|
106
|
+
// Gate Lifecycle Helpers
|
|
107
|
+
// ============================================
|
|
108
|
+
/**
|
|
109
|
+
* Activate a gate for an issue, creating an active gate state with a computed
|
|
110
|
+
* timeout deadline (if the gate definition includes a timeout).
|
|
111
|
+
*
|
|
112
|
+
* @param issueId - The issue identifier
|
|
113
|
+
* @param gateDef - The gate definition from the workflow
|
|
114
|
+
* @param storage - The gate storage adapter to use
|
|
115
|
+
* @returns The created GateState
|
|
116
|
+
*/
|
|
117
|
+
export async function activateGate(issueId, gateDef, storage) {
|
|
118
|
+
const now = Date.now();
|
|
119
|
+
const state = {
|
|
120
|
+
issueId,
|
|
121
|
+
gateName: gateDef.name,
|
|
122
|
+
gateType: gateDef.type,
|
|
123
|
+
status: 'active',
|
|
124
|
+
activatedAt: now,
|
|
125
|
+
};
|
|
126
|
+
// Compute timeout deadline if gate has a timeout configuration
|
|
127
|
+
if (gateDef.timeout) {
|
|
128
|
+
state.timeoutAction = gateDef.timeout.action;
|
|
129
|
+
state.timeoutDuration = gateDef.timeout.duration;
|
|
130
|
+
state.timeoutDeadline = now + parseDuration(gateDef.timeout.duration);
|
|
131
|
+
}
|
|
132
|
+
await storage.setGateState(issueId, gateDef.name, state);
|
|
133
|
+
log.info('Gate activated', { issueId, gateName: gateDef.name, gateType: gateDef.type });
|
|
134
|
+
return state;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Mark a gate as satisfied, recording what source satisfied it.
|
|
138
|
+
*
|
|
139
|
+
* @param issueId - The issue identifier
|
|
140
|
+
* @param gateName - The gate name to satisfy
|
|
141
|
+
* @param source - What satisfied the gate (e.g., comment ID, webhook payload hash, timer ID)
|
|
142
|
+
* @param storage - The gate storage adapter to use
|
|
143
|
+
* @returns The updated GateState, or null if the gate was not found
|
|
144
|
+
*/
|
|
145
|
+
export async function satisfyGate(issueId, gateName, source, storage) {
|
|
146
|
+
const state = await storage.getGateState(issueId, gateName);
|
|
147
|
+
if (!state) {
|
|
148
|
+
log.warn('Cannot satisfy gate: not found', { issueId, gateName });
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
if (state.status !== 'active') {
|
|
152
|
+
log.warn('Cannot satisfy gate: not in active status', { issueId, gateName, status: state.status });
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
state.status = 'satisfied';
|
|
156
|
+
state.satisfiedAt = Date.now();
|
|
157
|
+
state.signalSource = source;
|
|
158
|
+
await storage.setGateState(issueId, gateName, state);
|
|
159
|
+
log.info('Gate satisfied', { issueId, gateName, source });
|
|
160
|
+
return state;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Mark a gate as timed-out.
|
|
164
|
+
*
|
|
165
|
+
* @param issueId - The issue identifier
|
|
166
|
+
* @param gateName - The gate name to time out
|
|
167
|
+
* @param storage - The gate storage adapter to use
|
|
168
|
+
* @returns The updated GateState, or null if the gate was not found
|
|
169
|
+
*/
|
|
170
|
+
export async function timeoutGate(issueId, gateName, storage) {
|
|
171
|
+
const state = await storage.getGateState(issueId, gateName);
|
|
172
|
+
if (!state) {
|
|
173
|
+
log.warn('Cannot timeout gate: not found', { issueId, gateName });
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
if (state.status !== 'active') {
|
|
177
|
+
log.warn('Cannot timeout gate: not in active status', { issueId, gateName, status: state.status });
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
state.status = 'timed-out';
|
|
181
|
+
state.timedOutAt = Date.now();
|
|
182
|
+
await storage.setGateState(issueId, gateName, state);
|
|
183
|
+
log.info('Gate timed out', { issueId, gateName, timeoutAction: state.timeoutAction });
|
|
184
|
+
return state;
|
|
185
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gate-state.test.d.ts","sourceRoot":"","sources":["../../../src/workflow/gate-state.test.ts"],"names":[],"mappings":""}
|