@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,322 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { buildObservation, wrapEventsWithRecorder } from './observation-recorder.js';
|
|
3
|
+
function makeProcess(overrides = {}) {
|
|
4
|
+
return {
|
|
5
|
+
issueId: 'issue-1',
|
|
6
|
+
identifier: 'SUP-100',
|
|
7
|
+
pid: 1234,
|
|
8
|
+
status: 'running',
|
|
9
|
+
startedAt: new Date('2024-01-01T00:00:00Z'),
|
|
10
|
+
lastActivityAt: new Date('2024-01-01T00:01:00Z'),
|
|
11
|
+
...overrides,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function makeMockStores() {
|
|
15
|
+
const observationStore = {
|
|
16
|
+
recordObservation: vi.fn().mockResolvedValue(undefined),
|
|
17
|
+
getObservations: vi.fn().mockResolvedValue([]),
|
|
18
|
+
getRecentObservations: vi.fn().mockResolvedValue([]),
|
|
19
|
+
};
|
|
20
|
+
const posteriorStore = {
|
|
21
|
+
getPosterior: vi.fn().mockResolvedValue({}),
|
|
22
|
+
updatePosterior: vi.fn().mockResolvedValue({}),
|
|
23
|
+
getAllPosteriors: vi.fn().mockResolvedValue([]),
|
|
24
|
+
resetPosterior: vi.fn().mockResolvedValue(undefined),
|
|
25
|
+
};
|
|
26
|
+
return { observationStore, posteriorStore };
|
|
27
|
+
}
|
|
28
|
+
describe('buildObservation', () => {
|
|
29
|
+
it('returns null when providerName is missing', () => {
|
|
30
|
+
const agent = makeProcess({
|
|
31
|
+
providerName: undefined,
|
|
32
|
+
workType: 'development',
|
|
33
|
+
});
|
|
34
|
+
expect(buildObservation(agent)).toBeNull();
|
|
35
|
+
});
|
|
36
|
+
it('returns null when workType is missing', () => {
|
|
37
|
+
const agent = makeProcess({
|
|
38
|
+
providerName: 'claude',
|
|
39
|
+
workType: undefined,
|
|
40
|
+
});
|
|
41
|
+
expect(buildObservation(agent)).toBeNull();
|
|
42
|
+
});
|
|
43
|
+
it('correctly maps completed agent to observation', () => {
|
|
44
|
+
const agent = makeProcess({
|
|
45
|
+
status: 'completed',
|
|
46
|
+
providerName: 'claude',
|
|
47
|
+
workType: 'development',
|
|
48
|
+
pullRequestUrl: 'https://github.com/org/repo/pull/42',
|
|
49
|
+
workResult: 'passed',
|
|
50
|
+
totalCostUsd: 1.5,
|
|
51
|
+
sessionId: 'session-123',
|
|
52
|
+
startedAt: new Date('2024-01-01T00:00:00Z'),
|
|
53
|
+
completedAt: new Date('2024-01-01T00:05:00Z'),
|
|
54
|
+
});
|
|
55
|
+
const obs = buildObservation(agent);
|
|
56
|
+
expect(obs).not.toBeNull();
|
|
57
|
+
expect(obs.provider).toBe('claude');
|
|
58
|
+
expect(obs.workType).toBe('development');
|
|
59
|
+
expect(obs.issueIdentifier).toBe('SUP-100');
|
|
60
|
+
expect(obs.sessionId).toBe('session-123');
|
|
61
|
+
expect(obs.taskCompleted).toBe(true);
|
|
62
|
+
expect(obs.prCreated).toBe(true);
|
|
63
|
+
expect(obs.qaResult).toBe('passed');
|
|
64
|
+
expect(obs.totalCostUsd).toBe(1.5);
|
|
65
|
+
expect(obs.wallClockMs).toBe(300000); // 5 minutes
|
|
66
|
+
// reward for completed + PR + passed QA with some cost
|
|
67
|
+
expect(obs.reward).toBeGreaterThan(0);
|
|
68
|
+
expect(obs.reward).toBeLessThanOrEqual(1);
|
|
69
|
+
expect(obs.confidence).toBe(0);
|
|
70
|
+
expect(obs.explorationReason).toBeUndefined();
|
|
71
|
+
expect(obs.id).toBeDefined();
|
|
72
|
+
expect(obs.timestamp).toBeGreaterThan(0);
|
|
73
|
+
});
|
|
74
|
+
it('correctly maps failed agent to observation with reward near 0', () => {
|
|
75
|
+
const agent = makeProcess({
|
|
76
|
+
status: 'failed',
|
|
77
|
+
providerName: 'codex',
|
|
78
|
+
workType: 'qa',
|
|
79
|
+
totalCostUsd: 3.0,
|
|
80
|
+
});
|
|
81
|
+
const obs = buildObservation(agent);
|
|
82
|
+
expect(obs).not.toBeNull();
|
|
83
|
+
expect(obs.taskCompleted).toBe(false);
|
|
84
|
+
expect(obs.prCreated).toBe(false);
|
|
85
|
+
expect(obs.qaResult).toBe('unknown');
|
|
86
|
+
// No success signals, only cost penalty => reward should be 0
|
|
87
|
+
expect(obs.reward).toBe(0);
|
|
88
|
+
});
|
|
89
|
+
it('correctly maps stopped agent to observation', () => {
|
|
90
|
+
const agent = makeProcess({
|
|
91
|
+
status: 'stopped',
|
|
92
|
+
providerName: 'amp',
|
|
93
|
+
workType: 'research',
|
|
94
|
+
stopReason: 'timeout',
|
|
95
|
+
totalCostUsd: 0.5,
|
|
96
|
+
startedAt: new Date('2024-01-01T00:00:00Z'),
|
|
97
|
+
completedAt: new Date('2024-01-01T00:02:00Z'),
|
|
98
|
+
});
|
|
99
|
+
const obs = buildObservation(agent);
|
|
100
|
+
expect(obs).not.toBeNull();
|
|
101
|
+
expect(obs.provider).toBe('amp');
|
|
102
|
+
expect(obs.workType).toBe('research');
|
|
103
|
+
expect(obs.taskCompleted).toBe(false);
|
|
104
|
+
expect(obs.wallClockMs).toBe(120000); // 2 minutes
|
|
105
|
+
});
|
|
106
|
+
it('correctly maps incomplete agent to observation', () => {
|
|
107
|
+
const agent = makeProcess({
|
|
108
|
+
status: 'incomplete',
|
|
109
|
+
providerName: 'claude',
|
|
110
|
+
workType: 'development',
|
|
111
|
+
incompleteReason: 'no_pr_created',
|
|
112
|
+
totalCostUsd: 2.0,
|
|
113
|
+
});
|
|
114
|
+
const obs = buildObservation(agent);
|
|
115
|
+
expect(obs).not.toBeNull();
|
|
116
|
+
expect(obs.taskCompleted).toBe(false);
|
|
117
|
+
expect(obs.prCreated).toBe(false);
|
|
118
|
+
});
|
|
119
|
+
it('handles missing sessionId by defaulting to empty string', () => {
|
|
120
|
+
const agent = makeProcess({
|
|
121
|
+
providerName: 'claude',
|
|
122
|
+
workType: 'development',
|
|
123
|
+
sessionId: undefined,
|
|
124
|
+
});
|
|
125
|
+
const obs = buildObservation(agent);
|
|
126
|
+
expect(obs).not.toBeNull();
|
|
127
|
+
expect(obs.sessionId).toBe('');
|
|
128
|
+
});
|
|
129
|
+
it('handles missing completedAt by setting wallClockMs to 0', () => {
|
|
130
|
+
const agent = makeProcess({
|
|
131
|
+
providerName: 'claude',
|
|
132
|
+
workType: 'development',
|
|
133
|
+
completedAt: undefined,
|
|
134
|
+
});
|
|
135
|
+
const obs = buildObservation(agent);
|
|
136
|
+
expect(obs).not.toBeNull();
|
|
137
|
+
expect(obs.wallClockMs).toBe(0);
|
|
138
|
+
});
|
|
139
|
+
it('handles missing totalCostUsd by defaulting to 0', () => {
|
|
140
|
+
const agent = makeProcess({
|
|
141
|
+
providerName: 'claude',
|
|
142
|
+
workType: 'development',
|
|
143
|
+
totalCostUsd: undefined,
|
|
144
|
+
});
|
|
145
|
+
const obs = buildObservation(agent);
|
|
146
|
+
expect(obs).not.toBeNull();
|
|
147
|
+
expect(obs.totalCostUsd).toBe(0);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
describe('wrapEventsWithRecorder', () => {
|
|
151
|
+
let stores;
|
|
152
|
+
beforeEach(() => {
|
|
153
|
+
stores = makeMockStores();
|
|
154
|
+
});
|
|
155
|
+
it('calls original onAgentComplete and records observation', async () => {
|
|
156
|
+
const originalComplete = vi.fn();
|
|
157
|
+
const events = { onAgentComplete: originalComplete };
|
|
158
|
+
const wrapped = wrapEventsWithRecorder(events, stores);
|
|
159
|
+
const agent = makeProcess({
|
|
160
|
+
status: 'completed',
|
|
161
|
+
providerName: 'claude',
|
|
162
|
+
workType: 'development',
|
|
163
|
+
});
|
|
164
|
+
wrapped.onAgentComplete(agent);
|
|
165
|
+
expect(originalComplete).toHaveBeenCalledWith(agent);
|
|
166
|
+
// Allow microtask queue to flush for the void promise
|
|
167
|
+
await vi.waitFor(() => {
|
|
168
|
+
expect(stores.observationStore.recordObservation).toHaveBeenCalled();
|
|
169
|
+
});
|
|
170
|
+
expect(stores.posteriorStore.updatePosterior).toHaveBeenCalled();
|
|
171
|
+
});
|
|
172
|
+
it('calls original onAgentStopped and records observation', async () => {
|
|
173
|
+
const originalStopped = vi.fn();
|
|
174
|
+
const events = { onAgentStopped: originalStopped };
|
|
175
|
+
const wrapped = wrapEventsWithRecorder(events, stores);
|
|
176
|
+
const agent = makeProcess({
|
|
177
|
+
status: 'stopped',
|
|
178
|
+
providerName: 'claude',
|
|
179
|
+
workType: 'development',
|
|
180
|
+
});
|
|
181
|
+
wrapped.onAgentStopped(agent);
|
|
182
|
+
expect(originalStopped).toHaveBeenCalledWith(agent);
|
|
183
|
+
await vi.waitFor(() => {
|
|
184
|
+
expect(stores.observationStore.recordObservation).toHaveBeenCalled();
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
it('calls original onAgentError and records observation', async () => {
|
|
188
|
+
const originalError = vi.fn();
|
|
189
|
+
const events = { onAgentError: originalError };
|
|
190
|
+
const wrapped = wrapEventsWithRecorder(events, stores);
|
|
191
|
+
const agent = makeProcess({
|
|
192
|
+
status: 'failed',
|
|
193
|
+
providerName: 'claude',
|
|
194
|
+
workType: 'development',
|
|
195
|
+
});
|
|
196
|
+
const error = new Error('something broke');
|
|
197
|
+
wrapped.onAgentError(agent, error);
|
|
198
|
+
expect(originalError).toHaveBeenCalledWith(agent, error);
|
|
199
|
+
await vi.waitFor(() => {
|
|
200
|
+
expect(stores.observationStore.recordObservation).toHaveBeenCalled();
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
it('calls original onAgentIncomplete and records observation', async () => {
|
|
204
|
+
const originalIncomplete = vi.fn();
|
|
205
|
+
const events = { onAgentIncomplete: originalIncomplete };
|
|
206
|
+
const wrapped = wrapEventsWithRecorder(events, stores);
|
|
207
|
+
const agent = makeProcess({
|
|
208
|
+
status: 'incomplete',
|
|
209
|
+
providerName: 'claude',
|
|
210
|
+
workType: 'development',
|
|
211
|
+
});
|
|
212
|
+
wrapped.onAgentIncomplete(agent);
|
|
213
|
+
expect(originalIncomplete).toHaveBeenCalledWith(agent);
|
|
214
|
+
await vi.waitFor(() => {
|
|
215
|
+
expect(stores.observationStore.recordObservation).toHaveBeenCalled();
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
it('gracefully handles recording failures (logs error, does not throw)', async () => {
|
|
219
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
220
|
+
const recordError = new Error('store down');
|
|
221
|
+
stores.observationStore.recordObservation.mockRejectedValue(recordError);
|
|
222
|
+
const events = { onAgentComplete: vi.fn() };
|
|
223
|
+
const wrapped = wrapEventsWithRecorder(events, stores);
|
|
224
|
+
const agent = makeProcess({
|
|
225
|
+
status: 'completed',
|
|
226
|
+
providerName: 'claude',
|
|
227
|
+
workType: 'development',
|
|
228
|
+
});
|
|
229
|
+
// Should not throw
|
|
230
|
+
expect(() => wrapped.onAgentComplete(agent)).not.toThrow();
|
|
231
|
+
await vi.waitFor(() => {
|
|
232
|
+
expect(consoleSpy).toHaveBeenCalledWith('[routing] Failed to record observation', expect.objectContaining({
|
|
233
|
+
error: 'store down',
|
|
234
|
+
identifier: 'SUP-100',
|
|
235
|
+
}));
|
|
236
|
+
});
|
|
237
|
+
consoleSpy.mockRestore();
|
|
238
|
+
});
|
|
239
|
+
it('passes through non-wrapped events (onAgentStart, onIssueSelected)', () => {
|
|
240
|
+
const originalStart = vi.fn();
|
|
241
|
+
const originalIssueSelected = vi.fn();
|
|
242
|
+
const originalProviderSessionId = vi.fn();
|
|
243
|
+
const originalActivityEmitted = vi.fn();
|
|
244
|
+
const events = {
|
|
245
|
+
onAgentStart: originalStart,
|
|
246
|
+
onIssueSelected: originalIssueSelected,
|
|
247
|
+
onProviderSessionId: originalProviderSessionId,
|
|
248
|
+
onActivityEmitted: originalActivityEmitted,
|
|
249
|
+
};
|
|
250
|
+
const wrapped = wrapEventsWithRecorder(events, stores);
|
|
251
|
+
// Non-wrapped events should be the original references
|
|
252
|
+
expect(wrapped.onAgentStart).toBe(originalStart);
|
|
253
|
+
expect(wrapped.onIssueSelected).toBe(originalIssueSelected);
|
|
254
|
+
expect(wrapped.onProviderSessionId).toBe(originalProviderSessionId);
|
|
255
|
+
expect(wrapped.onActivityEmitted).toBe(originalActivityEmitted);
|
|
256
|
+
});
|
|
257
|
+
it('handles undefined original callbacks gracefully', async () => {
|
|
258
|
+
const events = {};
|
|
259
|
+
const wrapped = wrapEventsWithRecorder(events, stores);
|
|
260
|
+
const agent = makeProcess({
|
|
261
|
+
status: 'completed',
|
|
262
|
+
providerName: 'claude',
|
|
263
|
+
workType: 'development',
|
|
264
|
+
});
|
|
265
|
+
// Should not throw even if original callback was undefined
|
|
266
|
+
expect(() => wrapped.onAgentComplete(agent)).not.toThrow();
|
|
267
|
+
expect(() => wrapped.onAgentStopped(agent)).not.toThrow();
|
|
268
|
+
expect(() => wrapped.onAgentError(agent, new Error('test'))).not.toThrow();
|
|
269
|
+
expect(() => wrapped.onAgentIncomplete(agent)).not.toThrow();
|
|
270
|
+
await vi.waitFor(() => {
|
|
271
|
+
// recordObservation should be called 4 times (once per event)
|
|
272
|
+
expect(stores.observationStore.recordObservation).toHaveBeenCalledTimes(4);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
it('does not record observation when providerName is missing', async () => {
|
|
276
|
+
const events = { onAgentComplete: vi.fn() };
|
|
277
|
+
const wrapped = wrapEventsWithRecorder(events, stores);
|
|
278
|
+
const agent = makeProcess({
|
|
279
|
+
status: 'completed',
|
|
280
|
+
providerName: undefined,
|
|
281
|
+
workType: 'development',
|
|
282
|
+
});
|
|
283
|
+
wrapped.onAgentComplete(agent);
|
|
284
|
+
// Give microtasks a chance to run
|
|
285
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
286
|
+
expect(stores.observationStore.recordObservation).not.toHaveBeenCalled();
|
|
287
|
+
expect(stores.posteriorStore.updatePosterior).not.toHaveBeenCalled();
|
|
288
|
+
});
|
|
289
|
+
it('does not record observation when workType is missing', async () => {
|
|
290
|
+
const events = { onAgentComplete: vi.fn() };
|
|
291
|
+
const wrapped = wrapEventsWithRecorder(events, stores);
|
|
292
|
+
const agent = makeProcess({
|
|
293
|
+
status: 'completed',
|
|
294
|
+
providerName: 'claude',
|
|
295
|
+
workType: undefined,
|
|
296
|
+
});
|
|
297
|
+
wrapped.onAgentComplete(agent);
|
|
298
|
+
// Give microtasks a chance to run
|
|
299
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
300
|
+
expect(stores.observationStore.recordObservation).not.toHaveBeenCalled();
|
|
301
|
+
expect(stores.posteriorStore.updatePosterior).not.toHaveBeenCalled();
|
|
302
|
+
});
|
|
303
|
+
it('updates posterior with correct provider, workType, and reward', async () => {
|
|
304
|
+
const events = { onAgentComplete: vi.fn() };
|
|
305
|
+
const wrapped = wrapEventsWithRecorder(events, stores);
|
|
306
|
+
const agent = makeProcess({
|
|
307
|
+
status: 'completed',
|
|
308
|
+
providerName: 'claude',
|
|
309
|
+
workType: 'development',
|
|
310
|
+
pullRequestUrl: 'https://github.com/org/repo/pull/1',
|
|
311
|
+
workResult: 'passed',
|
|
312
|
+
totalCostUsd: 0,
|
|
313
|
+
});
|
|
314
|
+
wrapped.onAgentComplete(agent);
|
|
315
|
+
await vi.waitFor(() => {
|
|
316
|
+
expect(stores.posteriorStore.updatePosterior).toHaveBeenCalledWith('claude', 'development', expect.any(Number));
|
|
317
|
+
});
|
|
318
|
+
// Verify the reward value is the expected full-success reward (1.0)
|
|
319
|
+
const [, , reward] = stores.posteriorStore.updatePosterior.mock.calls[0];
|
|
320
|
+
expect(reward).toBe(1.0);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { AgentProviderName } from '../providers/types.js';
|
|
2
|
+
import type { AgentWorkType } from '../orchestrator/work-types.js';
|
|
3
|
+
import type { RoutingObservation } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Observation Store Interface
|
|
6
|
+
*
|
|
7
|
+
* Append-only log of routing observations for the MAB router.
|
|
8
|
+
* Implementations may use Redis Streams, SQLite, or in-memory storage.
|
|
9
|
+
* Core defines only the interface — concrete stores live in server or plugin packages.
|
|
10
|
+
*/
|
|
11
|
+
export interface ObservationStore {
|
|
12
|
+
/**
|
|
13
|
+
* Append a single observation to the store.
|
|
14
|
+
*/
|
|
15
|
+
recordObservation(obs: RoutingObservation): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* Query observations with optional filters.
|
|
18
|
+
*
|
|
19
|
+
* @param opts.provider - Filter by agent provider
|
|
20
|
+
* @param opts.workType - Filter by work type
|
|
21
|
+
* @param opts.limit - Maximum number of observations to return
|
|
22
|
+
* @param opts.since - Only return observations with timestamp >= since (epoch ms)
|
|
23
|
+
*/
|
|
24
|
+
getObservations(opts: {
|
|
25
|
+
provider?: AgentProviderName;
|
|
26
|
+
workType?: AgentWorkType;
|
|
27
|
+
limit?: number;
|
|
28
|
+
since?: number;
|
|
29
|
+
}): Promise<RoutingObservation[]>;
|
|
30
|
+
/**
|
|
31
|
+
* Get the most recent observations for a specific provider + work type pair.
|
|
32
|
+
* Results are returned newest-first, up to `windowSize` entries.
|
|
33
|
+
*
|
|
34
|
+
* @param provider - Agent provider name
|
|
35
|
+
* @param workType - Agent work type
|
|
36
|
+
* @param windowSize - Maximum number of recent observations to return
|
|
37
|
+
*/
|
|
38
|
+
getRecentObservations(provider: AgentProviderName, workType: AgentWorkType, windowSize: number): Promise<RoutingObservation[]>;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=observation-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observation-store.d.ts","sourceRoot":"","sources":["../../../src/routing/observation-store.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,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAEpD;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;OAEG;IACH,iBAAiB,CAAC,GAAG,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAEzD;;;;;;;OAOG;IACH,eAAe,CAAC,IAAI,EAAE;QACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAA;QAC5B,QAAQ,CAAC,EAAE,aAAa,CAAA;QACxB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,KAAK,CAAC,EAAE,MAAM,CAAA;KACf,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAAA;IAEjC;;;;;;;OAOG;IACH,qBAAqB,CACnB,QAAQ,EAAE,iBAAiB,EAC3B,QAAQ,EAAE,aAAa,EACvB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAAA;CACjC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observation-store.test.d.ts","sourceRoot":"","sources":["../../../src/routing/observation-store.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
/**
|
|
3
|
+
* Type-level tests for ObservationStore interface.
|
|
4
|
+
*
|
|
5
|
+
* These verify that the interface is well-defined and that a conforming
|
|
6
|
+
* implementation can be constructed without type errors.
|
|
7
|
+
*/
|
|
8
|
+
function makeObservation(overrides) {
|
|
9
|
+
return {
|
|
10
|
+
id: '550e8400-e29b-41d4-a716-446655440000',
|
|
11
|
+
provider: 'claude',
|
|
12
|
+
workType: 'development',
|
|
13
|
+
issueIdentifier: 'SUP-100',
|
|
14
|
+
sessionId: 'session-abc-123',
|
|
15
|
+
reward: 0.85,
|
|
16
|
+
taskCompleted: true,
|
|
17
|
+
prCreated: true,
|
|
18
|
+
qaResult: 'passed',
|
|
19
|
+
totalCostUsd: 0.42,
|
|
20
|
+
wallClockMs: 120000,
|
|
21
|
+
timestamp: 1700000000,
|
|
22
|
+
confidence: 0.9,
|
|
23
|
+
...overrides,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** In-memory ObservationStore for type conformance testing */
|
|
27
|
+
function createInMemoryStore() {
|
|
28
|
+
const observations = [];
|
|
29
|
+
return {
|
|
30
|
+
async recordObservation(obs) {
|
|
31
|
+
observations.push(obs);
|
|
32
|
+
},
|
|
33
|
+
async getObservations(opts) {
|
|
34
|
+
let result = [...observations];
|
|
35
|
+
if (opts.provider) {
|
|
36
|
+
result = result.filter((o) => o.provider === opts.provider);
|
|
37
|
+
}
|
|
38
|
+
if (opts.workType) {
|
|
39
|
+
result = result.filter((o) => o.workType === opts.workType);
|
|
40
|
+
}
|
|
41
|
+
if (opts.since) {
|
|
42
|
+
result = result.filter((o) => o.timestamp >= opts.since);
|
|
43
|
+
}
|
|
44
|
+
if (opts.limit) {
|
|
45
|
+
result = result.slice(0, opts.limit);
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
},
|
|
49
|
+
async getRecentObservations(provider, workType, windowSize) {
|
|
50
|
+
return observations
|
|
51
|
+
.filter((o) => o.provider === provider && o.workType === workType)
|
|
52
|
+
.reverse()
|
|
53
|
+
.slice(0, windowSize);
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
describe('ObservationStore interface', () => {
|
|
58
|
+
it('accepts a conforming in-memory implementation', () => {
|
|
59
|
+
const store = createInMemoryStore();
|
|
60
|
+
expect(store).toBeDefined();
|
|
61
|
+
expect(typeof store.recordObservation).toBe('function');
|
|
62
|
+
expect(typeof store.getObservations).toBe('function');
|
|
63
|
+
expect(typeof store.getRecentObservations).toBe('function');
|
|
64
|
+
});
|
|
65
|
+
it('recordObservation stores an observation', async () => {
|
|
66
|
+
const store = createInMemoryStore();
|
|
67
|
+
const obs = makeObservation();
|
|
68
|
+
await store.recordObservation(obs);
|
|
69
|
+
const all = await store.getObservations({});
|
|
70
|
+
expect(all).toHaveLength(1);
|
|
71
|
+
expect(all[0]).toEqual(obs);
|
|
72
|
+
});
|
|
73
|
+
it('getObservations filters by provider', async () => {
|
|
74
|
+
const store = createInMemoryStore();
|
|
75
|
+
await store.recordObservation(makeObservation({ provider: 'claude' }));
|
|
76
|
+
await store.recordObservation(makeObservation({ provider: 'codex' }));
|
|
77
|
+
await store.recordObservation(makeObservation({ provider: 'claude' }));
|
|
78
|
+
const result = await store.getObservations({ provider: 'claude' });
|
|
79
|
+
expect(result).toHaveLength(2);
|
|
80
|
+
expect(result.every((o) => o.provider === 'claude')).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
it('getObservations filters by workType', async () => {
|
|
83
|
+
const store = createInMemoryStore();
|
|
84
|
+
await store.recordObservation(makeObservation({ workType: 'development' }));
|
|
85
|
+
await store.recordObservation(makeObservation({ workType: 'qa' }));
|
|
86
|
+
const result = await store.getObservations({ workType: 'qa' });
|
|
87
|
+
expect(result).toHaveLength(1);
|
|
88
|
+
expect(result[0].workType).toBe('qa');
|
|
89
|
+
});
|
|
90
|
+
it('getObservations filters by since', async () => {
|
|
91
|
+
const store = createInMemoryStore();
|
|
92
|
+
await store.recordObservation(makeObservation({ timestamp: 1000 }));
|
|
93
|
+
await store.recordObservation(makeObservation({ timestamp: 2000 }));
|
|
94
|
+
await store.recordObservation(makeObservation({ timestamp: 3000 }));
|
|
95
|
+
const result = await store.getObservations({ since: 2000 });
|
|
96
|
+
expect(result).toHaveLength(2);
|
|
97
|
+
expect(result.every((o) => o.timestamp >= 2000)).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
it('getObservations respects limit', async () => {
|
|
100
|
+
const store = createInMemoryStore();
|
|
101
|
+
for (let i = 0; i < 10; i++) {
|
|
102
|
+
await store.recordObservation(makeObservation({ timestamp: i }));
|
|
103
|
+
}
|
|
104
|
+
const result = await store.getObservations({ limit: 3 });
|
|
105
|
+
expect(result).toHaveLength(3);
|
|
106
|
+
});
|
|
107
|
+
it('getRecentObservations returns newest-first for a provider+workType pair', async () => {
|
|
108
|
+
const store = createInMemoryStore();
|
|
109
|
+
await store.recordObservation(makeObservation({ provider: 'claude', workType: 'development', timestamp: 1000 }));
|
|
110
|
+
await store.recordObservation(makeObservation({ provider: 'codex', workType: 'development', timestamp: 2000 }));
|
|
111
|
+
await store.recordObservation(makeObservation({ provider: 'claude', workType: 'development', timestamp: 3000 }));
|
|
112
|
+
await store.recordObservation(makeObservation({ provider: 'claude', workType: 'qa', timestamp: 4000 }));
|
|
113
|
+
const result = await store.getRecentObservations('claude', 'development', 10);
|
|
114
|
+
expect(result).toHaveLength(2);
|
|
115
|
+
// Newest first
|
|
116
|
+
expect(result[0].timestamp).toBe(3000);
|
|
117
|
+
expect(result[1].timestamp).toBe(1000);
|
|
118
|
+
});
|
|
119
|
+
it('getRecentObservations respects windowSize', async () => {
|
|
120
|
+
const store = createInMemoryStore();
|
|
121
|
+
for (let i = 0; i < 10; i++) {
|
|
122
|
+
await store.recordObservation(makeObservation({ provider: 'amp', workType: 'research', timestamp: i }));
|
|
123
|
+
}
|
|
124
|
+
const result = await store.getRecentObservations('amp', 'research', 3);
|
|
125
|
+
expect(result).toHaveLength(3);
|
|
126
|
+
});
|
|
127
|
+
it('getObservations returns empty array when no observations match', async () => {
|
|
128
|
+
const store = createInMemoryStore();
|
|
129
|
+
await store.recordObservation(makeObservation({ provider: 'claude' }));
|
|
130
|
+
const result = await store.getObservations({ provider: 'codex' });
|
|
131
|
+
expect(result).toEqual([]);
|
|
132
|
+
});
|
|
133
|
+
it('getRecentObservations returns empty array when no observations match', async () => {
|
|
134
|
+
const store = createInMemoryStore();
|
|
135
|
+
const result = await store.getRecentObservations('claude', 'development', 10);
|
|
136
|
+
expect(result).toEqual([]);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AgentProviderName } from '../providers/types.js';
|
|
2
|
+
import type { AgentWorkType } from '../orchestrator/work-types.js';
|
|
3
|
+
import type { RoutingPosterior } from './types.js';
|
|
4
|
+
export interface PosteriorStore {
|
|
5
|
+
getPosterior(provider: AgentProviderName, workType: AgentWorkType): Promise<RoutingPosterior>;
|
|
6
|
+
updatePosterior(provider: AgentProviderName, workType: AgentWorkType, reward: number): Promise<RoutingPosterior>;
|
|
7
|
+
getAllPosteriors(): Promise<RoutingPosterior[]>;
|
|
8
|
+
resetPosterior(provider: AgentProviderName, workType: AgentWorkType): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
/** Default Beta(1,1) prior -- uniform distribution (optimistic cold start) */
|
|
11
|
+
export declare function defaultPosterior(provider: AgentProviderName, workType: AgentWorkType): RoutingPosterior;
|
|
12
|
+
//# sourceMappingURL=posterior-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"posterior-store.d.ts","sourceRoot":"","sources":["../../../src/routing/posterior-store.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,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAElD,MAAM,WAAW,cAAc;IAC7B,YAAY,CAAC,QAAQ,EAAE,iBAAiB,EAAE,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAA;IAC7F,eAAe,CAAC,QAAQ,EAAE,iBAAiB,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAA;IAChH,gBAAgB,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAAA;IAC/C,cAAc,CAAC,QAAQ,EAAE,iBAAiB,EAAE,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACpF;AAED,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,iBAAiB,EAAE,QAAQ,EAAE,aAAa,GAAG,gBAAgB,CAWvG"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Default Beta(1,1) prior -- uniform distribution (optimistic cold start) */
|
|
2
|
+
export function defaultPosterior(provider, workType) {
|
|
3
|
+
return {
|
|
4
|
+
provider,
|
|
5
|
+
workType,
|
|
6
|
+
alpha: 1,
|
|
7
|
+
beta: 1,
|
|
8
|
+
totalObservations: 0,
|
|
9
|
+
avgReward: 0,
|
|
10
|
+
avgCostUsd: 0,
|
|
11
|
+
lastUpdated: Date.now(),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"posterior-store.test.d.ts","sourceRoot":"","sources":["../../../src/routing/posterior-store.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { defaultPosterior } from './posterior-store.js';
|
|
3
|
+
describe('defaultPosterior', () => {
|
|
4
|
+
it('returns Beta(1,1) with correct provider and workType', () => {
|
|
5
|
+
const posterior = defaultPosterior('claude', 'development');
|
|
6
|
+
expect(posterior.provider).toBe('claude');
|
|
7
|
+
expect(posterior.workType).toBe('development');
|
|
8
|
+
expect(posterior.alpha).toBe(1);
|
|
9
|
+
expect(posterior.beta).toBe(1);
|
|
10
|
+
});
|
|
11
|
+
it('has totalObservations of 0', () => {
|
|
12
|
+
const posterior = defaultPosterior('codex', 'qa');
|
|
13
|
+
expect(posterior.totalObservations).toBe(0);
|
|
14
|
+
});
|
|
15
|
+
it('has avgReward of 0', () => {
|
|
16
|
+
const posterior = defaultPosterior('amp', 'research');
|
|
17
|
+
expect(posterior.avgReward).toBe(0);
|
|
18
|
+
});
|
|
19
|
+
it('has avgCostUsd of 0', () => {
|
|
20
|
+
const posterior = defaultPosterior('claude', 'inflight');
|
|
21
|
+
expect(posterior.avgCostUsd).toBe(0);
|
|
22
|
+
});
|
|
23
|
+
it('has a reasonable lastUpdated timestamp', () => {
|
|
24
|
+
const before = Date.now();
|
|
25
|
+
const posterior = defaultPosterior('claude', 'development');
|
|
26
|
+
const after = Date.now();
|
|
27
|
+
expect(posterior.lastUpdated).toBeGreaterThanOrEqual(before);
|
|
28
|
+
expect(posterior.lastUpdated).toBeLessThanOrEqual(after);
|
|
29
|
+
});
|
|
30
|
+
it('works with coordination work types', () => {
|
|
31
|
+
const posterior = defaultPosterior('amp', 'qa-coordination');
|
|
32
|
+
expect(posterior.provider).toBe('amp');
|
|
33
|
+
expect(posterior.workType).toBe('qa-coordination');
|
|
34
|
+
expect(posterior.alpha).toBe(1);
|
|
35
|
+
expect(posterior.beta).toBe(1);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -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":""}
|