@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.
Files changed (178) hide show
  1. package/dist/src/config/index.d.ts +1 -1
  2. package/dist/src/config/index.d.ts.map +1 -1
  3. package/dist/src/config/index.js +1 -1
  4. package/dist/src/config/repository-config.d.ts +23 -0
  5. package/dist/src/config/repository-config.d.ts.map +1 -1
  6. package/dist/src/config/repository-config.js +27 -0
  7. package/dist/src/config/repository-config.test.js +140 -1
  8. package/dist/src/governor/decision-engine.d.ts +3 -0
  9. package/dist/src/governor/decision-engine.d.ts.map +1 -1
  10. package/dist/src/governor/decision-engine.js +11 -0
  11. package/dist/src/governor/decision-engine.test.js +33 -0
  12. package/dist/src/governor/governor-types.d.ts +1 -1
  13. package/dist/src/governor/governor-types.d.ts.map +1 -1
  14. package/dist/src/governor/governor.d.ts +17 -1
  15. package/dist/src/governor/governor.d.ts.map +1 -1
  16. package/dist/src/governor/governor.js +112 -1
  17. package/dist/src/governor/governor.test.js +155 -0
  18. package/dist/src/index.d.ts +1 -0
  19. package/dist/src/index.d.ts.map +1 -1
  20. package/dist/src/index.js +1 -0
  21. package/dist/src/orchestrator/issue-tracker-client.d.ts +4 -0
  22. package/dist/src/orchestrator/issue-tracker-client.d.ts.map +1 -1
  23. package/dist/src/orchestrator/orchestrator.d.ts.map +1 -1
  24. package/dist/src/orchestrator/orchestrator.js +24 -0
  25. package/dist/src/orchestrator/parse-work-result.d.ts.map +1 -1
  26. package/dist/src/orchestrator/parse-work-result.js +6 -0
  27. package/dist/src/orchestrator/parse-work-result.test.js +19 -0
  28. package/dist/src/providers/index.d.ts +64 -1
  29. package/dist/src/providers/index.d.ts.map +1 -1
  30. package/dist/src/providers/index.js +132 -1
  31. package/dist/src/providers/index.test.js +340 -2
  32. package/dist/src/routing/index.d.ts +7 -0
  33. package/dist/src/routing/index.d.ts.map +1 -0
  34. package/dist/src/routing/index.js +6 -0
  35. package/dist/src/routing/observation-recorder.d.ts +19 -0
  36. package/dist/src/routing/observation-recorder.d.ts.map +1 -0
  37. package/dist/src/routing/observation-recorder.js +73 -0
  38. package/dist/src/routing/observation-recorder.test.d.ts +2 -0
  39. package/dist/src/routing/observation-recorder.test.d.ts.map +1 -0
  40. package/dist/src/routing/observation-recorder.test.js +322 -0
  41. package/dist/src/routing/observation-store.d.ts +40 -0
  42. package/dist/src/routing/observation-store.d.ts.map +1 -0
  43. package/dist/src/routing/observation-store.js +1 -0
  44. package/dist/src/routing/observation-store.test.d.ts +2 -0
  45. package/dist/src/routing/observation-store.test.d.ts.map +1 -0
  46. package/dist/src/routing/observation-store.test.js +138 -0
  47. package/dist/src/routing/posterior-store.d.ts +12 -0
  48. package/dist/src/routing/posterior-store.d.ts.map +1 -0
  49. package/dist/src/routing/posterior-store.js +13 -0
  50. package/dist/src/routing/posterior-store.test.d.ts +2 -0
  51. package/dist/src/routing/posterior-store.test.d.ts.map +1 -0
  52. package/dist/src/routing/posterior-store.test.js +37 -0
  53. package/dist/src/routing/reward.d.ts +16 -0
  54. package/dist/src/routing/reward.d.ts.map +1 -0
  55. package/dist/src/routing/reward.js +29 -0
  56. package/dist/src/routing/reward.test.d.ts +2 -0
  57. package/dist/src/routing/reward.test.d.ts.map +1 -0
  58. package/dist/src/routing/reward.test.js +210 -0
  59. package/dist/src/routing/routing-engine.d.ts +20 -0
  60. package/dist/src/routing/routing-engine.d.ts.map +1 -0
  61. package/dist/src/routing/routing-engine.js +113 -0
  62. package/dist/src/routing/routing-engine.test.d.ts +2 -0
  63. package/dist/src/routing/routing-engine.test.d.ts.map +1 -0
  64. package/dist/src/routing/routing-engine.test.js +310 -0
  65. package/dist/src/routing/types.d.ts +157 -0
  66. package/dist/src/routing/types.d.ts.map +1 -0
  67. package/dist/src/routing/types.js +68 -0
  68. package/dist/src/routing/types.test.d.ts +2 -0
  69. package/dist/src/routing/types.test.d.ts.map +1 -0
  70. package/dist/src/routing/types.test.js +184 -0
  71. package/dist/src/templates/types.d.ts +3 -0
  72. package/dist/src/templates/types.d.ts.map +1 -1
  73. package/dist/src/templates/types.js +2 -0
  74. package/dist/src/workflow/agent-cancellation.d.ts +37 -0
  75. package/dist/src/workflow/agent-cancellation.d.ts.map +1 -0
  76. package/dist/src/workflow/agent-cancellation.js +41 -0
  77. package/dist/src/workflow/agent-cancellation.test.d.ts +2 -0
  78. package/dist/src/workflow/agent-cancellation.test.d.ts.map +1 -0
  79. package/dist/src/workflow/agent-cancellation.test.js +86 -0
  80. package/dist/src/workflow/concurrency-semaphore.d.ts +21 -0
  81. package/dist/src/workflow/concurrency-semaphore.d.ts.map +1 -0
  82. package/dist/src/workflow/concurrency-semaphore.js +46 -0
  83. package/dist/src/workflow/concurrency-semaphore.test.d.ts +2 -0
  84. package/dist/src/workflow/concurrency-semaphore.test.d.ts.map +1 -0
  85. package/dist/src/workflow/concurrency-semaphore.test.js +183 -0
  86. package/dist/src/workflow/gate-state.d.ts +115 -0
  87. package/dist/src/workflow/gate-state.d.ts.map +1 -0
  88. package/dist/src/workflow/gate-state.js +185 -0
  89. package/dist/src/workflow/gate-state.test.d.ts +2 -0
  90. package/dist/src/workflow/gate-state.test.d.ts.map +1 -0
  91. package/dist/src/workflow/gate-state.test.js +251 -0
  92. package/dist/src/workflow/gates/gate-evaluator.d.ts +119 -0
  93. package/dist/src/workflow/gates/gate-evaluator.d.ts.map +1 -0
  94. package/dist/src/workflow/gates/gate-evaluator.js +243 -0
  95. package/dist/src/workflow/gates/gate-evaluator.test.d.ts +2 -0
  96. package/dist/src/workflow/gates/gate-evaluator.test.d.ts.map +1 -0
  97. package/dist/src/workflow/gates/gate-evaluator.test.js +240 -0
  98. package/dist/src/workflow/gates/signal-gate.d.ts +114 -0
  99. package/dist/src/workflow/gates/signal-gate.d.ts.map +1 -0
  100. package/dist/src/workflow/gates/signal-gate.js +216 -0
  101. package/dist/src/workflow/gates/signal-gate.test.d.ts +2 -0
  102. package/dist/src/workflow/gates/signal-gate.test.d.ts.map +1 -0
  103. package/dist/src/workflow/gates/signal-gate.test.js +199 -0
  104. package/dist/src/workflow/gates/timeout-engine.d.ts +96 -0
  105. package/dist/src/workflow/gates/timeout-engine.d.ts.map +1 -0
  106. package/dist/src/workflow/gates/timeout-engine.js +162 -0
  107. package/dist/src/workflow/gates/timeout-engine.test.d.ts +2 -0
  108. package/dist/src/workflow/gates/timeout-engine.test.d.ts.map +1 -0
  109. package/dist/src/workflow/gates/timeout-engine.test.js +186 -0
  110. package/dist/src/workflow/gates/timer-gate.d.ts +125 -0
  111. package/dist/src/workflow/gates/timer-gate.d.ts.map +1 -0
  112. package/dist/src/workflow/gates/timer-gate.js +381 -0
  113. package/dist/src/workflow/gates/timer-gate.test.d.ts +2 -0
  114. package/dist/src/workflow/gates/timer-gate.test.d.ts.map +1 -0
  115. package/dist/src/workflow/gates/timer-gate.test.js +211 -0
  116. package/dist/src/workflow/gates/webhook-gate.d.ts +132 -0
  117. package/dist/src/workflow/gates/webhook-gate.d.ts.map +1 -0
  118. package/dist/src/workflow/gates/webhook-gate.js +216 -0
  119. package/dist/src/workflow/gates/webhook-gate.test.d.ts +2 -0
  120. package/dist/src/workflow/gates/webhook-gate.test.d.ts.map +1 -0
  121. package/dist/src/workflow/gates/webhook-gate.test.js +182 -0
  122. package/dist/src/workflow/index.d.ts +23 -2
  123. package/dist/src/workflow/index.d.ts.map +1 -1
  124. package/dist/src/workflow/index.js +15 -1
  125. package/dist/src/workflow/parallelism-executor.d.ts +25 -0
  126. package/dist/src/workflow/parallelism-executor.d.ts.map +1 -0
  127. package/dist/src/workflow/parallelism-executor.js +53 -0
  128. package/dist/src/workflow/parallelism-executor.test.d.ts +2 -0
  129. package/dist/src/workflow/parallelism-executor.test.d.ts.map +1 -0
  130. package/dist/src/workflow/parallelism-executor.test.js +191 -0
  131. package/dist/src/workflow/parallelism-types.d.ts +80 -0
  132. package/dist/src/workflow/parallelism-types.d.ts.map +1 -0
  133. package/dist/src/workflow/parallelism-types.js +8 -0
  134. package/dist/src/workflow/phase-context-injector.d.ts +29 -0
  135. package/dist/src/workflow/phase-context-injector.d.ts.map +1 -0
  136. package/dist/src/workflow/phase-context-injector.js +43 -0
  137. package/dist/src/workflow/phase-context-injector.test.d.ts +2 -0
  138. package/dist/src/workflow/phase-context-injector.test.d.ts.map +1 -0
  139. package/dist/src/workflow/phase-context-injector.test.js +123 -0
  140. package/dist/src/workflow/phase-output-collector.d.ts +39 -0
  141. package/dist/src/workflow/phase-output-collector.d.ts.map +1 -0
  142. package/dist/src/workflow/phase-output-collector.js +141 -0
  143. package/dist/src/workflow/phase-output-collector.test.d.ts +2 -0
  144. package/dist/src/workflow/phase-output-collector.test.d.ts.map +1 -0
  145. package/dist/src/workflow/phase-output-collector.test.js +179 -0
  146. package/dist/src/workflow/strategies/fan-in-strategy.d.ts +21 -0
  147. package/dist/src/workflow/strategies/fan-in-strategy.d.ts.map +1 -0
  148. package/dist/src/workflow/strategies/fan-in-strategy.js +92 -0
  149. package/dist/src/workflow/strategies/fan-in-strategy.test.d.ts +2 -0
  150. package/dist/src/workflow/strategies/fan-in-strategy.test.d.ts.map +1 -0
  151. package/dist/src/workflow/strategies/fan-in-strategy.test.js +182 -0
  152. package/dist/src/workflow/strategies/fan-out-strategy.d.ts +16 -0
  153. package/dist/src/workflow/strategies/fan-out-strategy.d.ts.map +1 -0
  154. package/dist/src/workflow/strategies/fan-out-strategy.js +47 -0
  155. package/dist/src/workflow/strategies/fan-out-strategy.test.d.ts +2 -0
  156. package/dist/src/workflow/strategies/fan-out-strategy.test.d.ts.map +1 -0
  157. package/dist/src/workflow/strategies/fan-out-strategy.test.js +97 -0
  158. package/dist/src/workflow/strategies/index.d.ts +4 -0
  159. package/dist/src/workflow/strategies/index.d.ts.map +1 -0
  160. package/dist/src/workflow/strategies/index.js +3 -0
  161. package/dist/src/workflow/strategies/race-strategy.d.ts +19 -0
  162. package/dist/src/workflow/strategies/race-strategy.d.ts.map +1 -0
  163. package/dist/src/workflow/strategies/race-strategy.js +92 -0
  164. package/dist/src/workflow/strategies/race-strategy.test.d.ts +2 -0
  165. package/dist/src/workflow/strategies/race-strategy.test.d.ts.map +1 -0
  166. package/dist/src/workflow/strategies/race-strategy.test.js +318 -0
  167. package/dist/src/workflow/transition-engine.d.ts.map +1 -1
  168. package/dist/src/workflow/transition-engine.js +12 -0
  169. package/dist/src/workflow/transition-engine.test.js +92 -0
  170. package/dist/src/workflow/workflow-registry.d.ts +5 -1
  171. package/dist/src/workflow/workflow-registry.d.ts.map +1 -1
  172. package/dist/src/workflow/workflow-registry.js +8 -0
  173. package/dist/src/workflow/workflow-registry.test.js +54 -0
  174. package/dist/src/workflow/workflow-types.d.ts +151 -6
  175. package/dist/src/workflow/workflow-types.d.ts.map +1 -1
  176. package/dist/src/workflow/workflow-types.js +71 -1
  177. package/dist/src/workflow/workflow-types.test.js +293 -2
  178. 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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=observation-store.test.d.ts.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=posterior-store.test.d.ts.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=reward.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reward.test.d.ts","sourceRoot":"","sources":["../../../src/routing/reward.test.ts"],"names":[],"mappings":""}