@renseiai/agentfactory 0.8.8 → 0.8.10
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/README.md +2 -2
- 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 +26 -0
- package/dist/src/config/repository-config.d.ts.map +1 -1
- package/dist/src/config/repository-config.js +40 -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/index.d.ts +1 -1
- package/dist/src/orchestrator/index.d.ts.map +1 -1
- package/dist/src/orchestrator/index.js +1 -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-utils.test.js +31 -1
- package/dist/src/orchestrator/orchestrator.d.ts +16 -2
- package/dist/src/orchestrator/orchestrator.d.ts.map +1 -1
- package/dist/src/orchestrator/orchestrator.js +70 -15
- 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/types.d.ts +1 -1
- package/dist/src/orchestrator/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/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,16 @@
|
|
|
1
|
+
import type { AgentProcess } from '../orchestrator/types.js';
|
|
2
|
+
export declare const MAX_EXPECTED_COST = 5;
|
|
3
|
+
export interface RoutingReward {
|
|
4
|
+
taskCompleted: boolean;
|
|
5
|
+
prCreated: boolean;
|
|
6
|
+
qaResult: 'passed' | 'failed' | 'unknown';
|
|
7
|
+
totalCostUsd: number;
|
|
8
|
+
inputTokens: number;
|
|
9
|
+
outputTokens: number;
|
|
10
|
+
wallClockTimeMs: number;
|
|
11
|
+
requiredRefinements: number;
|
|
12
|
+
humanEscalations: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function computeReward(outcome: RoutingReward): number;
|
|
15
|
+
export declare function extractRewardFromProcess(process: AgentProcess): RoutingReward;
|
|
16
|
+
//# sourceMappingURL=reward.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reward.d.ts","sourceRoot":"","sources":["../../../src/routing/reward.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAE5D,eAAO,MAAM,iBAAiB,IAAM,CAAA;AAEpC,MAAM,WAAW,aAAa;IAC5B,aAAa,EAAE,OAAO,CAAA;IACtB,SAAS,EAAE,OAAO,CAAA;IAClB,QAAQ,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAA;IACzC,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,eAAe,EAAE,MAAM,CAAA;IACvB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,gBAAgB,EAAE,MAAM,CAAA;CACzB;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAS5D;AAED,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,YAAY,GAAG,aAAa,CAe7E"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export const MAX_EXPECTED_COST = 5.0; // USD
|
|
2
|
+
export function computeReward(outcome) {
|
|
3
|
+
let reward = 0;
|
|
4
|
+
if (outcome.taskCompleted)
|
|
5
|
+
reward += 0.5;
|
|
6
|
+
if (outcome.prCreated)
|
|
7
|
+
reward += 0.2;
|
|
8
|
+
if (outcome.qaResult === 'passed')
|
|
9
|
+
reward += 0.3;
|
|
10
|
+
// Cost penalty (normalized)
|
|
11
|
+
const costPenalty = Math.min(outcome.totalCostUsd / MAX_EXPECTED_COST, 1);
|
|
12
|
+
reward -= 0.1 * costPenalty;
|
|
13
|
+
return Math.max(0, Math.min(1, reward));
|
|
14
|
+
}
|
|
15
|
+
export function extractRewardFromProcess(process) {
|
|
16
|
+
return {
|
|
17
|
+
taskCompleted: process.status === 'completed',
|
|
18
|
+
prCreated: process.pullRequestUrl !== undefined,
|
|
19
|
+
qaResult: process.workResult ?? 'unknown',
|
|
20
|
+
totalCostUsd: process.totalCostUsd ?? 0,
|
|
21
|
+
inputTokens: process.inputTokens ?? 0,
|
|
22
|
+
outputTokens: process.outputTokens ?? 0,
|
|
23
|
+
wallClockTimeMs: process.completedAt && process.startedAt
|
|
24
|
+
? process.completedAt.getTime() - process.startedAt.getTime()
|
|
25
|
+
: 0,
|
|
26
|
+
requiredRefinements: 0,
|
|
27
|
+
humanEscalations: 0,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reward.test.d.ts","sourceRoot":"","sources":["../../../src/routing/reward.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { computeReward, extractRewardFromProcess, MAX_EXPECTED_COST } from './reward.js';
|
|
3
|
+
function makeReward(overrides = {}) {
|
|
4
|
+
return {
|
|
5
|
+
taskCompleted: false,
|
|
6
|
+
prCreated: false,
|
|
7
|
+
qaResult: 'unknown',
|
|
8
|
+
totalCostUsd: 0,
|
|
9
|
+
inputTokens: 0,
|
|
10
|
+
outputTokens: 0,
|
|
11
|
+
wallClockTimeMs: 0,
|
|
12
|
+
requiredRefinements: 0,
|
|
13
|
+
humanEscalations: 0,
|
|
14
|
+
...overrides,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function makeProcess(overrides = {}) {
|
|
18
|
+
return {
|
|
19
|
+
issueId: 'issue-1',
|
|
20
|
+
identifier: 'SUP-100',
|
|
21
|
+
pid: 1234,
|
|
22
|
+
status: 'running',
|
|
23
|
+
startedAt: new Date('2024-01-01T00:00:00Z'),
|
|
24
|
+
lastActivityAt: new Date('2024-01-01T00:01:00Z'),
|
|
25
|
+
...overrides,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
describe('computeReward', () => {
|
|
29
|
+
it('returns ~1.0 for full success with zero cost', () => {
|
|
30
|
+
const reward = computeReward(makeReward({
|
|
31
|
+
taskCompleted: true,
|
|
32
|
+
prCreated: true,
|
|
33
|
+
qaResult: 'passed',
|
|
34
|
+
totalCostUsd: 0,
|
|
35
|
+
}));
|
|
36
|
+
// 0.5 + 0.2 + 0.3 - 0 = 1.0
|
|
37
|
+
expect(reward).toBe(1.0);
|
|
38
|
+
});
|
|
39
|
+
it('returns 0.0 for full failure', () => {
|
|
40
|
+
const reward = computeReward(makeReward());
|
|
41
|
+
expect(reward).toBe(0.0);
|
|
42
|
+
});
|
|
43
|
+
it('returns 0.5 for taskCompleted only', () => {
|
|
44
|
+
const reward = computeReward(makeReward({ taskCompleted: true }));
|
|
45
|
+
expect(reward).toBe(0.5);
|
|
46
|
+
});
|
|
47
|
+
it('returns 0.2 for prCreated only', () => {
|
|
48
|
+
const reward = computeReward(makeReward({ prCreated: true }));
|
|
49
|
+
expect(reward).toBe(0.2);
|
|
50
|
+
});
|
|
51
|
+
it('returns 0.3 for qaResult passed only', () => {
|
|
52
|
+
const reward = computeReward(makeReward({ qaResult: 'passed' }));
|
|
53
|
+
expect(reward).toBe(0.3);
|
|
54
|
+
});
|
|
55
|
+
it('returns 0.7 for taskCompleted + prCreated', () => {
|
|
56
|
+
const reward = computeReward(makeReward({ taskCompleted: true, prCreated: true }));
|
|
57
|
+
expect(reward).toBe(0.7);
|
|
58
|
+
});
|
|
59
|
+
it('applies full cost penalty of 0.1 when cost equals MAX_EXPECTED_COST', () => {
|
|
60
|
+
const reward = computeReward(makeReward({
|
|
61
|
+
taskCompleted: true,
|
|
62
|
+
prCreated: true,
|
|
63
|
+
qaResult: 'passed',
|
|
64
|
+
totalCostUsd: MAX_EXPECTED_COST,
|
|
65
|
+
}));
|
|
66
|
+
// 0.5 + 0.2 + 0.3 - 0.1 * 1.0 = 0.9
|
|
67
|
+
expect(reward).toBeCloseTo(0.9);
|
|
68
|
+
});
|
|
69
|
+
it('caps cost penalty when cost exceeds MAX_EXPECTED_COST', () => {
|
|
70
|
+
const reward = computeReward(makeReward({
|
|
71
|
+
taskCompleted: true,
|
|
72
|
+
prCreated: true,
|
|
73
|
+
qaResult: 'passed',
|
|
74
|
+
totalCostUsd: 100,
|
|
75
|
+
}));
|
|
76
|
+
// Cost penalty capped at 0.1, so 1.0 - 0.1 = 0.9
|
|
77
|
+
expect(reward).toBeCloseTo(0.9);
|
|
78
|
+
});
|
|
79
|
+
it('applies proportional cost penalty', () => {
|
|
80
|
+
const reward = computeReward(makeReward({
|
|
81
|
+
taskCompleted: true,
|
|
82
|
+
totalCostUsd: MAX_EXPECTED_COST / 2,
|
|
83
|
+
}));
|
|
84
|
+
// 0.5 - 0.1 * 0.5 = 0.45
|
|
85
|
+
expect(reward).toBeCloseTo(0.45);
|
|
86
|
+
});
|
|
87
|
+
it('clamps reward to minimum of 0', () => {
|
|
88
|
+
// Only cost, no success signals: 0 - 0.1 = -0.1 => clamped to 0
|
|
89
|
+
const reward = computeReward(makeReward({ totalCostUsd: MAX_EXPECTED_COST }));
|
|
90
|
+
expect(reward).toBe(0);
|
|
91
|
+
});
|
|
92
|
+
it('clamps reward to maximum of 1', () => {
|
|
93
|
+
// Full success with zero cost should be exactly 1.0 (0.5 + 0.2 + 0.3)
|
|
94
|
+
const reward = computeReward(makeReward({
|
|
95
|
+
taskCompleted: true,
|
|
96
|
+
prCreated: true,
|
|
97
|
+
qaResult: 'passed',
|
|
98
|
+
totalCostUsd: 0,
|
|
99
|
+
}));
|
|
100
|
+
expect(reward).toBeLessThanOrEqual(1);
|
|
101
|
+
expect(reward).toBeGreaterThanOrEqual(0);
|
|
102
|
+
});
|
|
103
|
+
it('does not add reward for qaResult failed', () => {
|
|
104
|
+
const reward = computeReward(makeReward({ qaResult: 'failed' }));
|
|
105
|
+
expect(reward).toBe(0);
|
|
106
|
+
});
|
|
107
|
+
it('does not add reward for qaResult unknown', () => {
|
|
108
|
+
const reward = computeReward(makeReward({ qaResult: 'unknown' }));
|
|
109
|
+
expect(reward).toBe(0);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
describe('extractRewardFromProcess', () => {
|
|
113
|
+
it('maps completed process with PR and passed QA', () => {
|
|
114
|
+
const process = makeProcess({
|
|
115
|
+
status: 'completed',
|
|
116
|
+
pullRequestUrl: 'https://github.com/org/repo/pull/1',
|
|
117
|
+
workResult: 'passed',
|
|
118
|
+
totalCostUsd: 1.5,
|
|
119
|
+
inputTokens: 10000,
|
|
120
|
+
outputTokens: 5000,
|
|
121
|
+
startedAt: new Date('2024-01-01T00:00:00Z'),
|
|
122
|
+
completedAt: new Date('2024-01-01T00:05:00Z'),
|
|
123
|
+
});
|
|
124
|
+
const result = extractRewardFromProcess(process);
|
|
125
|
+
expect(result.taskCompleted).toBe(true);
|
|
126
|
+
expect(result.prCreated).toBe(true);
|
|
127
|
+
expect(result.qaResult).toBe('passed');
|
|
128
|
+
expect(result.totalCostUsd).toBe(1.5);
|
|
129
|
+
expect(result.inputTokens).toBe(10000);
|
|
130
|
+
expect(result.outputTokens).toBe(5000);
|
|
131
|
+
expect(result.wallClockTimeMs).toBe(300000); // 5 minutes
|
|
132
|
+
expect(result.requiredRefinements).toBe(0);
|
|
133
|
+
expect(result.humanEscalations).toBe(0);
|
|
134
|
+
});
|
|
135
|
+
it('maps failed process', () => {
|
|
136
|
+
const process = makeProcess({
|
|
137
|
+
status: 'failed',
|
|
138
|
+
});
|
|
139
|
+
const result = extractRewardFromProcess(process);
|
|
140
|
+
expect(result.taskCompleted).toBe(false);
|
|
141
|
+
expect(result.prCreated).toBe(false);
|
|
142
|
+
expect(result.qaResult).toBe('unknown');
|
|
143
|
+
});
|
|
144
|
+
it('maps running process as not completed', () => {
|
|
145
|
+
const result = extractRewardFromProcess(makeProcess({ status: 'running' }));
|
|
146
|
+
expect(result.taskCompleted).toBe(false);
|
|
147
|
+
});
|
|
148
|
+
it('maps incomplete process as not completed', () => {
|
|
149
|
+
const result = extractRewardFromProcess(makeProcess({ status: 'incomplete' }));
|
|
150
|
+
expect(result.taskCompleted).toBe(false);
|
|
151
|
+
});
|
|
152
|
+
it('maps stopped process as not completed', () => {
|
|
153
|
+
const result = extractRewardFromProcess(makeProcess({ status: 'stopped' }));
|
|
154
|
+
expect(result.taskCompleted).toBe(false);
|
|
155
|
+
});
|
|
156
|
+
it('handles undefined pullRequestUrl', () => {
|
|
157
|
+
const result = extractRewardFromProcess(makeProcess({ pullRequestUrl: undefined }));
|
|
158
|
+
expect(result.prCreated).toBe(false);
|
|
159
|
+
});
|
|
160
|
+
it('handles undefined workResult', () => {
|
|
161
|
+
const result = extractRewardFromProcess(makeProcess({ workResult: undefined }));
|
|
162
|
+
expect(result.qaResult).toBe('unknown');
|
|
163
|
+
});
|
|
164
|
+
it('handles undefined totalCostUsd', () => {
|
|
165
|
+
const result = extractRewardFromProcess(makeProcess({ totalCostUsd: undefined }));
|
|
166
|
+
expect(result.totalCostUsd).toBe(0);
|
|
167
|
+
});
|
|
168
|
+
it('handles undefined inputTokens', () => {
|
|
169
|
+
const result = extractRewardFromProcess(makeProcess({ inputTokens: undefined }));
|
|
170
|
+
expect(result.inputTokens).toBe(0);
|
|
171
|
+
});
|
|
172
|
+
it('handles undefined outputTokens', () => {
|
|
173
|
+
const result = extractRewardFromProcess(makeProcess({ outputTokens: undefined }));
|
|
174
|
+
expect(result.outputTokens).toBe(0);
|
|
175
|
+
});
|
|
176
|
+
it('handles undefined completedAt (wallClockTimeMs is 0)', () => {
|
|
177
|
+
const result = extractRewardFromProcess(makeProcess({
|
|
178
|
+
startedAt: new Date('2024-01-01T00:00:00Z'),
|
|
179
|
+
completedAt: undefined,
|
|
180
|
+
}));
|
|
181
|
+
expect(result.wallClockTimeMs).toBe(0);
|
|
182
|
+
});
|
|
183
|
+
it('computes correct wallClockTimeMs from startedAt and completedAt', () => {
|
|
184
|
+
const result = extractRewardFromProcess(makeProcess({
|
|
185
|
+
startedAt: new Date('2024-01-01T00:00:00Z'),
|
|
186
|
+
completedAt: new Date('2024-01-01T01:00:00Z'),
|
|
187
|
+
}));
|
|
188
|
+
expect(result.wallClockTimeMs).toBe(3600000); // 1 hour
|
|
189
|
+
});
|
|
190
|
+
it('end-to-end: extractRewardFromProcess feeds into computeReward correctly', () => {
|
|
191
|
+
const process = makeProcess({
|
|
192
|
+
status: 'completed',
|
|
193
|
+
pullRequestUrl: 'https://github.com/org/repo/pull/1',
|
|
194
|
+
workResult: 'passed',
|
|
195
|
+
totalCostUsd: 0,
|
|
196
|
+
});
|
|
197
|
+
const routingReward = extractRewardFromProcess(process);
|
|
198
|
+
const reward = computeReward(routingReward);
|
|
199
|
+
expect(reward).toBe(1.0);
|
|
200
|
+
});
|
|
201
|
+
it('end-to-end: failed process with high cost yields 0', () => {
|
|
202
|
+
const process = makeProcess({
|
|
203
|
+
status: 'failed',
|
|
204
|
+
totalCostUsd: 10,
|
|
205
|
+
});
|
|
206
|
+
const routingReward = extractRewardFromProcess(process);
|
|
207
|
+
const reward = computeReward(routingReward);
|
|
208
|
+
expect(reward).toBe(0);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AgentProviderName } from '../providers/types.js';
|
|
2
|
+
import type { AgentWorkType } from '../orchestrator/work-types.js';
|
|
3
|
+
import type { RoutingDecision, RoutingConfig } from './types.js';
|
|
4
|
+
import type { PosteriorStore } from './posterior-store.js';
|
|
5
|
+
export declare const DEFAULT_ROUTING_CONFIG: RoutingConfig;
|
|
6
|
+
export declare function betaMean(alpha: number, beta: number): number;
|
|
7
|
+
export declare function betaVariance(alpha: number, beta: number): number;
|
|
8
|
+
/**
|
|
9
|
+
* Sample from a Beta distribution using the ratio of Gamma variates.
|
|
10
|
+
* If X ~ Gamma(alpha), Y ~ Gamma(beta), then X/(X+Y) ~ Beta(alpha, beta).
|
|
11
|
+
*
|
|
12
|
+
* If a random generator is provided, use it for deterministic testing.
|
|
13
|
+
*/
|
|
14
|
+
export declare function betaSample(alpha: number, beta: number, rng?: () => number): number;
|
|
15
|
+
export interface SelectProviderOptions {
|
|
16
|
+
forcedExploration?: boolean;
|
|
17
|
+
rng?: () => number;
|
|
18
|
+
}
|
|
19
|
+
export declare function selectProvider(posteriorStore: PosteriorStore, workType: AgentWorkType, availableProviders: AgentProviderName[], config?: RoutingConfig, opts?: SelectProviderOptions): Promise<RoutingDecision>;
|
|
20
|
+
//# sourceMappingURL=routing-engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routing-engine.d.ts","sourceRoot":"","sources":["../../../src/routing/routing-engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AAC9D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAA;AAClE,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAChE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAE1D,eAAO,MAAM,sBAAsB,EAAE,aAOpC,CAAA;AAGD,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAGhE;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,MAAM,GAAG,MAAM,CAQlF;AAkCD,MAAM,WAAW,qBAAqB;IACpC,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAED,wBAAsB,cAAc,CAClC,cAAc,EAAE,cAAc,EAC9B,QAAQ,EAAE,aAAa,EACvB,kBAAkB,EAAE,iBAAiB,EAAE,EACvC,MAAM,GAAE,aAAsC,EAC9C,IAAI,CAAC,EAAE,qBAAqB,GAC3B,OAAO,CAAC,eAAe,CAAC,CAgE1B"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
export const DEFAULT_ROUTING_CONFIG = {
|
|
2
|
+
enabled: false,
|
|
3
|
+
explorationRate: 0.1,
|
|
4
|
+
windowSize: 100,
|
|
5
|
+
discountFactor: 0.99,
|
|
6
|
+
minObservationsForExploit: 5,
|
|
7
|
+
changeDetectionThreshold: 0.2,
|
|
8
|
+
};
|
|
9
|
+
// Beta distribution utilities - pure math, no external dependencies
|
|
10
|
+
export function betaMean(alpha, beta) {
|
|
11
|
+
return alpha / (alpha + beta);
|
|
12
|
+
}
|
|
13
|
+
export function betaVariance(alpha, beta) {
|
|
14
|
+
const ab = alpha + beta;
|
|
15
|
+
return (alpha * beta) / (ab * ab * (ab + 1));
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Sample from a Beta distribution using the ratio of Gamma variates.
|
|
19
|
+
* If X ~ Gamma(alpha), Y ~ Gamma(beta), then X/(X+Y) ~ Beta(alpha, beta).
|
|
20
|
+
*
|
|
21
|
+
* If a random generator is provided, use it for deterministic testing.
|
|
22
|
+
*/
|
|
23
|
+
export function betaSample(alpha, beta, rng) {
|
|
24
|
+
const random = rng ?? Math.random;
|
|
25
|
+
const x = gammaSample(alpha, random);
|
|
26
|
+
const y = gammaSample(beta, random);
|
|
27
|
+
if (x + y === 0)
|
|
28
|
+
return 0.5; // Edge case
|
|
29
|
+
return x / (x + y);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Sample from a Gamma distribution using Marsaglia and Tsang's method.
|
|
33
|
+
*/
|
|
34
|
+
function gammaSample(shape, random) {
|
|
35
|
+
if (shape < 1) {
|
|
36
|
+
// For shape < 1, use the trick: Gamma(shape) = Gamma(shape+1) * U^(1/shape)
|
|
37
|
+
return gammaSample(shape + 1, random) * Math.pow(random(), 1 / shape);
|
|
38
|
+
}
|
|
39
|
+
const d = shape - 1 / 3;
|
|
40
|
+
const c = 1 / Math.sqrt(9 * d);
|
|
41
|
+
while (true) {
|
|
42
|
+
let x;
|
|
43
|
+
let v;
|
|
44
|
+
do {
|
|
45
|
+
// Generate standard normal using Box-Muller
|
|
46
|
+
const u1 = random();
|
|
47
|
+
const u2 = random();
|
|
48
|
+
x = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
|
|
49
|
+
v = 1 + c * x;
|
|
50
|
+
} while (v <= 0);
|
|
51
|
+
v = v * v * v;
|
|
52
|
+
const u = random();
|
|
53
|
+
if (u < 1 - 0.0331 * (x * x) * (x * x))
|
|
54
|
+
return d * v;
|
|
55
|
+
if (Math.log(u) < 0.5 * x * x + d * (1 - v + Math.log(v)))
|
|
56
|
+
return d * v;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export async function selectProvider(posteriorStore, workType, availableProviders, config = DEFAULT_ROUTING_CONFIG, opts) {
|
|
60
|
+
const posteriors = await Promise.all(availableProviders.map(p => posteriorStore.getPosterior(p, workType)));
|
|
61
|
+
const rng = opts?.rng;
|
|
62
|
+
// Forced exploration: random selection
|
|
63
|
+
if (opts?.forcedExploration || (rng ?? Math.random)() < config.explorationRate) {
|
|
64
|
+
const idx = Math.floor((rng ?? Math.random)() * availableProviders.length);
|
|
65
|
+
const chosen = posteriors[idx];
|
|
66
|
+
const mean = betaMean(chosen.alpha, chosen.beta);
|
|
67
|
+
const confidence = 1 - Math.sqrt(betaVariance(chosen.alpha, chosen.beta));
|
|
68
|
+
const others = posteriors
|
|
69
|
+
.filter((_, i) => i !== idx)
|
|
70
|
+
.map(p => ({
|
|
71
|
+
provider: p.provider,
|
|
72
|
+
expectedReward: betaMean(p.alpha, p.beta),
|
|
73
|
+
confidence: 1 - Math.sqrt(betaVariance(p.alpha, p.beta)),
|
|
74
|
+
}));
|
|
75
|
+
return {
|
|
76
|
+
selectedProvider: chosen.provider,
|
|
77
|
+
confidence,
|
|
78
|
+
expectedReward: mean,
|
|
79
|
+
explorationReason: 'forced',
|
|
80
|
+
source: 'mab-routing',
|
|
81
|
+
alternatives: others,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// Thompson Sampling: draw from each posterior's Beta distribution
|
|
85
|
+
const samples = posteriors.map(post => ({
|
|
86
|
+
provider: post.provider,
|
|
87
|
+
sample: betaSample(post.alpha, post.beta, rng),
|
|
88
|
+
confidence: 1 - Math.sqrt(betaVariance(post.alpha, post.beta)),
|
|
89
|
+
expectedReward: betaMean(post.alpha, post.beta),
|
|
90
|
+
}));
|
|
91
|
+
// Select highest sample
|
|
92
|
+
samples.sort((a, b) => b.sample - a.sample);
|
|
93
|
+
const selected = samples[0];
|
|
94
|
+
// Only trust posteriors with enough observations
|
|
95
|
+
const selectedPosterior = posteriors.find(p => p.provider === selected.provider);
|
|
96
|
+
const isTrusted = selectedPosterior.totalObservations >= config.minObservationsForExploit;
|
|
97
|
+
return {
|
|
98
|
+
selectedProvider: selected.provider,
|
|
99
|
+
confidence: selected.confidence,
|
|
100
|
+
expectedReward: selected.expectedReward,
|
|
101
|
+
explorationReason: !isTrusted
|
|
102
|
+
? 'uncertainty'
|
|
103
|
+
: selected.confidence < 0.3
|
|
104
|
+
? 'uncertainty'
|
|
105
|
+
: undefined,
|
|
106
|
+
source: 'mab-routing',
|
|
107
|
+
alternatives: samples.slice(1).map(s => ({
|
|
108
|
+
provider: s.provider,
|
|
109
|
+
expectedReward: s.expectedReward,
|
|
110
|
+
confidence: s.confidence,
|
|
111
|
+
})),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routing-engine.test.d.ts","sourceRoot":"","sources":["../../../src/routing/routing-engine.test.ts"],"names":[],"mappings":""}
|