@monotykamary/pi-loop 0.1.12
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/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +285 -0
- package/media/demo.mp4 +0 -0
- package/media/pi-loop.jpg +0 -0
- package/media/screenshot.png +0 -0
- package/package.json +89 -0
- package/src/core/analyzer.ts +51 -0
- package/src/core/content-extractor.ts +79 -0
- package/src/core/inference.ts +137 -0
- package/src/core/prompt-builder.ts +217 -0
- package/src/core/prompt-loader.ts +126 -0
- package/src/core/reframe.ts +30 -0
- package/src/core/snapshot-builder.ts +252 -0
- package/src/global-config.ts +38 -0
- package/src/index.ts +532 -0
- package/src/session/client.ts +47 -0
- package/src/session/loop-session.ts +102 -0
- package/src/session/response-parser.ts +37 -0
- package/src/state/manager.ts +164 -0
- package/src/state/patterns.ts +81 -0
- package/src/state/reframe.ts +33 -0
- package/src/subagent-detector.ts +94 -0
- package/src/types.ts +83 -0
- package/src/ui/animations.ts +70 -0
- package/src/ui/model-picker.ts +79 -0
- package/src/ui/renderer.ts +257 -0
- package/src/ui/status-widget.ts +30 -0
- package/src/ui/types.ts +48 -0
- package/tests/compaction.test.ts +754 -0
- package/tests/continue-action-regression.test.ts +456 -0
- package/tests/engine.test.ts +770 -0
- package/tests/ephemeral-supervision.test.ts +391 -0
- package/tests/full-fidelity-snapshot.test.ts +843 -0
- package/tests/parsing.test.ts +303 -0
- package/tests/state.test.ts +525 -0
- package/tests/status-widget.test.ts +703 -0
- package/tests/subagent-detector.test.ts +191 -0
- package/tests/supervise-command.test.ts +381 -0
- package/tsconfig.json +14 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test for: Continue action at agent_end sanity check
|
|
3
|
+
*
|
|
4
|
+
* Bug: When analyzer returned action: 'continue' at agent_end (agent idle),
|
|
5
|
+
* the supervisor would nudge the agent to continue even when work was complete,
|
|
6
|
+
* causing infinite loops.
|
|
7
|
+
*
|
|
8
|
+
* Fix: Added sanity check at agent_end. If 'continue' is received when agent is idle:
|
|
9
|
+
* 1. If confidence >= 0.8 and reasoning suggests completion (contains 'complete',
|
|
10
|
+
* 'verified', 'achieved', 'done', 'implemented'), treat as 'done'
|
|
11
|
+
* 2. Otherwise convert to 'steer' with the provided message or default
|
|
12
|
+
*
|
|
13
|
+
* The prompt also explicitly warns the model that returning 'continue' when idle
|
|
14
|
+
* is an error and will be converted.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
|
|
18
|
+
import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
19
|
+
import type { LoopState, SteeringDecision } from '../src/types.js';
|
|
20
|
+
|
|
21
|
+
// Mocks must be before imports
|
|
22
|
+
vi.mock('../src/core/analyzer.js', () => ({
|
|
23
|
+
analyze: vi.fn(),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
vi.mock('../src/core/inference.js', () => ({
|
|
27
|
+
inferOutcome: vi.fn(),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
vi.mock('../src/core/prompt-loader.js', () => ({
|
|
31
|
+
loadSystemPrompt: vi.fn().mockReturnValue({ prompt: 'test prompt', source: 'built-in' }),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
vi.mock('../src/ui/renderer.js', () => ({
|
|
35
|
+
updateUI: vi.fn(),
|
|
36
|
+
toggleWidget: vi.fn().mockReturnValue(true),
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
vi.mock('../src/ui/model-picker.js', () => ({
|
|
40
|
+
pickModel: vi.fn(),
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
vi.mock('../src/global-config.js', () => ({
|
|
44
|
+
loadGlobalModel: vi.fn().mockReturnValue(null),
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
vi.mock('../src/session/client.js', () => ({
|
|
48
|
+
disposeSession: vi.fn(),
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
vi.mock('../src/subagent-detector.js', () => ({
|
|
52
|
+
checkChildPiProcesses: vi.fn().mockResolvedValue({ hasActiveSubagents: false, count: 0 }),
|
|
53
|
+
waitForSubagents: vi
|
|
54
|
+
.fn()
|
|
55
|
+
.mockResolvedValue({ completed: true, finalStatus: { hasActiveSubagents: false, count: 0 } }),
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
// Now import the mocked modules
|
|
59
|
+
import { analyze } from '../src/core/analyzer.js';
|
|
60
|
+
import { updateUI } from '../src/ui/renderer.js';
|
|
61
|
+
import { checkChildPiProcesses, waitForSubagents } from '../src/subagent-detector.js';
|
|
62
|
+
import { LoopStateManager } from '../src/state/manager.js';
|
|
63
|
+
|
|
64
|
+
describe('Continue action at agent_end - sanity check behavior', () => {
|
|
65
|
+
let mockPi: ExtensionAPI;
|
|
66
|
+
let mockCtx: ExtensionContext;
|
|
67
|
+
let sendUserMessageSpy: ReturnType<typeof vi.fn>;
|
|
68
|
+
let stateManager: LoopStateManager;
|
|
69
|
+
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
vi.clearAllMocks();
|
|
72
|
+
|
|
73
|
+
// Create spy for sendUserMessage
|
|
74
|
+
sendUserMessageSpy = vi.fn();
|
|
75
|
+
|
|
76
|
+
// Mock the ExtensionAPI
|
|
77
|
+
mockPi = {
|
|
78
|
+
appendEntry: vi.fn(),
|
|
79
|
+
on: vi.fn(),
|
|
80
|
+
registerCommand: vi.fn(),
|
|
81
|
+
registerTool: vi.fn(),
|
|
82
|
+
sendUserMessage: sendUserMessageSpy,
|
|
83
|
+
sendMessage: vi.fn(),
|
|
84
|
+
events: { emit: vi.fn(), on: vi.fn() },
|
|
85
|
+
} as any;
|
|
86
|
+
|
|
87
|
+
// Mock the ExtensionContext
|
|
88
|
+
mockCtx = {
|
|
89
|
+
ui: {
|
|
90
|
+
notify: vi.fn(),
|
|
91
|
+
setStatus: vi.fn(),
|
|
92
|
+
setWidget: vi.fn(),
|
|
93
|
+
},
|
|
94
|
+
sessionManager: {
|
|
95
|
+
getBranch: vi.fn().mockReturnValue([]),
|
|
96
|
+
},
|
|
97
|
+
model: { provider: 'anthropic', id: 'claude-haiku' },
|
|
98
|
+
modelRegistry: {
|
|
99
|
+
getApiKeyForProvider: vi.fn().mockResolvedValue('test-key'),
|
|
100
|
+
find: vi.fn().mockReturnValue({ name: 'test-model' }),
|
|
101
|
+
},
|
|
102
|
+
isIdle: vi.fn().mockReturnValue(true),
|
|
103
|
+
cwd: '/test',
|
|
104
|
+
} as any;
|
|
105
|
+
|
|
106
|
+
stateManager = new LoopStateManager(mockPi);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
afterEach(() => {
|
|
110
|
+
vi.restoreAllMocks();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* NEW SANITY CHECK LOGIC:
|
|
115
|
+
* At agent_end, 'continue' is not valid when agent is idle.
|
|
116
|
+
*
|
|
117
|
+
* If confidence >= 0.8 AND reasoning suggests completion:
|
|
118
|
+
* → Treat as 'done'
|
|
119
|
+
* Else:
|
|
120
|
+
* → Treat as 'steer' with message
|
|
121
|
+
*/
|
|
122
|
+
function shouldTreatContinueAsDone(decision: SteeringDecision): boolean {
|
|
123
|
+
return (
|
|
124
|
+
decision.confidence >= 0.8 &&
|
|
125
|
+
(decision.reasoning?.toLowerCase().includes('complete') ||
|
|
126
|
+
decision.reasoning?.toLowerCase().includes('verified') ||
|
|
127
|
+
decision.reasoning?.toLowerCase().includes('achieved') ||
|
|
128
|
+
decision.reasoning?.toLowerCase().includes('done') ||
|
|
129
|
+
decision.reasoning?.toLowerCase().includes('implemented'))
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Simulates the UPDATED agent_end handler behavior with sanity check
|
|
135
|
+
*/
|
|
136
|
+
async function simulateAgentEnd(decision: SteeringDecision): Promise<void> {
|
|
137
|
+
// Start loop state
|
|
138
|
+
stateManager.start('Fix hydration inconsistency', 'anthropic', 'claude-haiku');
|
|
139
|
+
|
|
140
|
+
// Mock analyzer to return our test decision
|
|
141
|
+
vi.mocked(analyze).mockResolvedValue(decision);
|
|
142
|
+
|
|
143
|
+
// Mock subagent checks
|
|
144
|
+
vi.mocked(checkChildPiProcesses).mockResolvedValue({ hasActiveSubagents: false, count: 0 });
|
|
145
|
+
vi.mocked(waitForSubagents).mockResolvedValue({
|
|
146
|
+
completed: true,
|
|
147
|
+
finalStatus: { hasActiveSubagents: false, count: 0 },
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Simulate the agent_end handler logic
|
|
151
|
+
stateManager.incrementTurnCount();
|
|
152
|
+
const s = stateManager.getState()!;
|
|
153
|
+
|
|
154
|
+
// Check subagents (bypassed in test)
|
|
155
|
+
await checkChildPiProcesses();
|
|
156
|
+
|
|
157
|
+
// Call analyzer
|
|
158
|
+
const actualDecision = await analyze(mockCtx, s, true);
|
|
159
|
+
|
|
160
|
+
// UPDATED agent_end handler logic with sanity check
|
|
161
|
+
if (actualDecision.action === 'steer' && actualDecision.message) {
|
|
162
|
+
stateManager.addIntervention({
|
|
163
|
+
turnCount: s.turnCount,
|
|
164
|
+
message: actualDecision.message,
|
|
165
|
+
reasoning: actualDecision.reasoning,
|
|
166
|
+
timestamp: Date.now(),
|
|
167
|
+
asi: actualDecision.asi,
|
|
168
|
+
});
|
|
169
|
+
updateUI(mockCtx, {} as any, stateManager.getState(), {
|
|
170
|
+
type: 'steering',
|
|
171
|
+
message: actualDecision.message,
|
|
172
|
+
reframeTier: stateManager.getReframeTier(),
|
|
173
|
+
});
|
|
174
|
+
mockPi.sendUserMessage(actualDecision.message);
|
|
175
|
+
} else if (actualDecision.action === 'done') {
|
|
176
|
+
stateManager.resetReframeTier();
|
|
177
|
+
updateUI(mockCtx, {} as any, stateManager.getState(), { type: 'done' });
|
|
178
|
+
stateManager.stop();
|
|
179
|
+
} else if (actualDecision.action === 'continue') {
|
|
180
|
+
// SANITY CHECK: At agent_end, the agent is IDLE. The prompt explicitly
|
|
181
|
+
// instructs supervisor to NEVER return 'continue' when agent is idle.
|
|
182
|
+
|
|
183
|
+
const highConfidenceOfCompletion = shouldTreatContinueAsDone(actualDecision);
|
|
184
|
+
|
|
185
|
+
if (highConfidenceOfCompletion) {
|
|
186
|
+
// Model likely meant to return 'done' but returned 'continue' by mistake
|
|
187
|
+
stateManager.resetReframeTier();
|
|
188
|
+
updateUI(mockCtx, {} as any, stateManager.getState(), { type: 'done' });
|
|
189
|
+
stateManager.stop();
|
|
190
|
+
} else {
|
|
191
|
+
// Treat as steer — goal not achieved, agent needs direction
|
|
192
|
+
const steerMessage = actualDecision.message?.trim()
|
|
193
|
+
? actualDecision.message
|
|
194
|
+
: 'Please continue working toward the goal.';
|
|
195
|
+
stateManager.addIntervention({
|
|
196
|
+
turnCount: s.turnCount,
|
|
197
|
+
message: steerMessage,
|
|
198
|
+
reasoning: actualDecision.reasoning || 'Goal not yet achieved, continuing work',
|
|
199
|
+
timestamp: Date.now(),
|
|
200
|
+
asi: { ...actualDecision.asi, _sanity: 'converted_continue_at_idle_to_steer' },
|
|
201
|
+
});
|
|
202
|
+
updateUI(mockCtx, {} as any, stateManager.getState(), {
|
|
203
|
+
type: 'steering',
|
|
204
|
+
message: steerMessage,
|
|
205
|
+
reframeTier: stateManager.getReframeTier(),
|
|
206
|
+
});
|
|
207
|
+
mockPi.sendUserMessage(steerMessage);
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
// Set watching without sending a message
|
|
211
|
+
updateUI(mockCtx, {} as any, stateManager.getState(), {
|
|
212
|
+
type: 'watching',
|
|
213
|
+
reframeTier: stateManager.getReframeTier(),
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
describe('when analyzer returns continue with high confidence of completion', () => {
|
|
219
|
+
it('should treat as DONE - stop loop without sending message', async () => {
|
|
220
|
+
const decision: SteeringDecision = {
|
|
221
|
+
action: 'continue',
|
|
222
|
+
reasoning: 'Both fixes have been fully implemented and verified. All tests pass.',
|
|
223
|
+
confidence: 0.95,
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
await simulateAgentEnd(decision);
|
|
227
|
+
|
|
228
|
+
// With high confidence and completion keywords, should stop loop
|
|
229
|
+
expect(sendUserMessageSpy).not.toHaveBeenCalled();
|
|
230
|
+
expect(stateManager.isActive()).toBe(false);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should treat as DONE when reasoning contains "verified"', async () => {
|
|
234
|
+
const decision: SteeringDecision = {
|
|
235
|
+
action: 'continue',
|
|
236
|
+
reasoning: 'The implementation is verified and complete.',
|
|
237
|
+
confidence: 0.85,
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
await simulateAgentEnd(decision);
|
|
241
|
+
|
|
242
|
+
expect(sendUserMessageSpy).not.toHaveBeenCalled();
|
|
243
|
+
expect(stateManager.isActive()).toBe(false);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('should treat as DONE when reasoning contains "achieved"', async () => {
|
|
247
|
+
const decision: SteeringDecision = {
|
|
248
|
+
action: 'continue',
|
|
249
|
+
reasoning: 'Goal has been achieved.',
|
|
250
|
+
confidence: 0.9,
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
await simulateAgentEnd(decision);
|
|
254
|
+
|
|
255
|
+
expect(sendUserMessageSpy).not.toHaveBeenCalled();
|
|
256
|
+
expect(stateManager.isActive()).toBe(false);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('should treat as DONE when reasoning contains "implemented"', async () => {
|
|
260
|
+
const decision: SteeringDecision = {
|
|
261
|
+
action: 'continue',
|
|
262
|
+
reasoning: 'All changes have been implemented successfully.',
|
|
263
|
+
confidence: 0.88,
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
await simulateAgentEnd(decision);
|
|
267
|
+
|
|
268
|
+
expect(sendUserMessageSpy).not.toHaveBeenCalled();
|
|
269
|
+
expect(stateManager.isActive()).toBe(false);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('should update UI to done state', async () => {
|
|
273
|
+
const decision: SteeringDecision = {
|
|
274
|
+
action: 'continue',
|
|
275
|
+
reasoning: 'Fixes complete and verified.',
|
|
276
|
+
confidence: 0.95,
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
await simulateAgentEnd(decision);
|
|
280
|
+
|
|
281
|
+
const lastCall = vi.mocked(updateUI).mock.calls[vi.mocked(updateUI).mock.calls.length - 1];
|
|
282
|
+
const uiUpdate = lastCall[3] as any;
|
|
283
|
+
expect(uiUpdate.type).toBe('done');
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe('when analyzer returns continue with low confidence or no completion signals', () => {
|
|
288
|
+
it('should convert to STEER and send continuation message', async () => {
|
|
289
|
+
const decision: SteeringDecision = {
|
|
290
|
+
action: 'continue',
|
|
291
|
+
reasoning: 'Goal not yet achieved, need more work',
|
|
292
|
+
confidence: 0.7,
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
await simulateAgentEnd(decision);
|
|
296
|
+
|
|
297
|
+
// Should send message to prompt agent to continue
|
|
298
|
+
expect(sendUserMessageSpy).toHaveBeenCalledTimes(1);
|
|
299
|
+
expect(sendUserMessageSpy).toHaveBeenCalledWith('Please continue working toward the goal.');
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('should convert to STEER when confidence is high but no completion keywords', async () => {
|
|
303
|
+
const decision: SteeringDecision = {
|
|
304
|
+
action: 'continue',
|
|
305
|
+
reasoning: 'Progress looks good so far, agent should keep working',
|
|
306
|
+
confidence: 0.85,
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
await simulateAgentEnd(decision);
|
|
310
|
+
|
|
311
|
+
// Even with high confidence, no completion keywords means steer
|
|
312
|
+
expect(sendUserMessageSpy).toHaveBeenCalledTimes(1);
|
|
313
|
+
expect(stateManager.isActive()).toBe(true); // Loop still active
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('should use custom message when provided', async () => {
|
|
317
|
+
const customMessage = 'Continue working on the ChatMessage textContent fix';
|
|
318
|
+
const decision: SteeringDecision = {
|
|
319
|
+
action: 'continue',
|
|
320
|
+
message: customMessage,
|
|
321
|
+
reasoning: 'More work needed',
|
|
322
|
+
confidence: 0.6,
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
await simulateAgentEnd(decision);
|
|
326
|
+
|
|
327
|
+
expect(sendUserMessageSpy).toHaveBeenCalledTimes(1);
|
|
328
|
+
expect(sendUserMessageSpy).toHaveBeenCalledWith(customMessage);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('should update UI to steering state', async () => {
|
|
332
|
+
const decision: SteeringDecision = {
|
|
333
|
+
action: 'continue',
|
|
334
|
+
reasoning: 'More work needed',
|
|
335
|
+
confidence: 0.6,
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
await simulateAgentEnd(decision);
|
|
339
|
+
|
|
340
|
+
// UI should show steering (active intervention)
|
|
341
|
+
const lastCall = vi.mocked(updateUI).mock.calls[vi.mocked(updateUI).mock.calls.length - 1];
|
|
342
|
+
const uiUpdate = lastCall[3] as any;
|
|
343
|
+
expect(uiUpdate.type).toBe('steering');
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('should record intervention with sanity check marker', async () => {
|
|
347
|
+
const decision: SteeringDecision = {
|
|
348
|
+
action: 'continue',
|
|
349
|
+
reasoning: 'Incomplete',
|
|
350
|
+
confidence: 0.6,
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
await simulateAgentEnd(decision);
|
|
354
|
+
|
|
355
|
+
const state = stateManager.getState()!;
|
|
356
|
+
expect(state.interventions).toHaveLength(1);
|
|
357
|
+
expect(state.interventions[0].asi?._sanity).toBe('converted_continue_at_idle_to_steer');
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('should keep loop active when converting to steer', async () => {
|
|
361
|
+
const decision: SteeringDecision = {
|
|
362
|
+
action: 'continue',
|
|
363
|
+
reasoning: 'Not done yet',
|
|
364
|
+
confidence: 0.5,
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
await simulateAgentEnd(decision);
|
|
368
|
+
|
|
369
|
+
expect(stateManager.isActive()).toBe(true);
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
describe('contrast with proper actions', () => {
|
|
374
|
+
it('steer action: works normally without conversion', async () => {
|
|
375
|
+
const decision: SteeringDecision = {
|
|
376
|
+
action: 'steer',
|
|
377
|
+
message: 'Fix this specific issue',
|
|
378
|
+
reasoning: 'Agent off track',
|
|
379
|
+
confidence: 0.9,
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
await simulateAgentEnd(decision);
|
|
383
|
+
|
|
384
|
+
expect(sendUserMessageSpy).toHaveBeenCalledTimes(1);
|
|
385
|
+
expect(sendUserMessageSpy).toHaveBeenCalledWith('Fix this specific issue');
|
|
386
|
+
expect(stateManager.isActive()).toBe(true);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('done action: stops loop without sending message', async () => {
|
|
390
|
+
const decision: SteeringDecision = {
|
|
391
|
+
action: 'done',
|
|
392
|
+
reasoning: 'Goal achieved',
|
|
393
|
+
confidence: 0.95,
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
await simulateAgentEnd(decision);
|
|
397
|
+
|
|
398
|
+
expect(sendUserMessageSpy).not.toHaveBeenCalled();
|
|
399
|
+
expect(stateManager.isActive()).toBe(false);
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
describe('regression: infinite continue loop', () => {
|
|
404
|
+
it('OLD BEHAVIOR (before fix): would keep sending continue messages forever', async () => {
|
|
405
|
+
// This test documents what used to happen
|
|
406
|
+
stateManager.start('Test goal', 'anthropic', 'claude-haiku');
|
|
407
|
+
|
|
408
|
+
// Simulate multiple agent_end events with continue
|
|
409
|
+
for (let i = 0; i < 5; i++) {
|
|
410
|
+
stateManager.incrementTurnCount();
|
|
411
|
+
const s = stateManager.getState()!;
|
|
412
|
+
|
|
413
|
+
// OLD behavior: always send continue message
|
|
414
|
+
const continueMessage = 'Please continue working toward the goal.';
|
|
415
|
+
stateManager.addIntervention({
|
|
416
|
+
turnCount: s.turnCount,
|
|
417
|
+
message: continueMessage,
|
|
418
|
+
reasoning: 'Goal not yet achieved',
|
|
419
|
+
timestamp: Date.now(),
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Old behavior would accumulate many interventions
|
|
424
|
+
expect(stateManager.getState()!.interventions.length).toBe(5);
|
|
425
|
+
expect(stateManager.isActive()).toBe(true); // Never stops
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it('NEW BEHAVIOR: stops loop when continue looks like completion', async () => {
|
|
429
|
+
const decision: SteeringDecision = {
|
|
430
|
+
action: 'continue',
|
|
431
|
+
reasoning: 'Both fixes fully implemented and verified. All 1375 tests pass.',
|
|
432
|
+
confidence: 0.95,
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
await simulateAgentEnd(decision);
|
|
436
|
+
|
|
437
|
+
// Should stop instead of continuing forever
|
|
438
|
+
expect(stateManager.isActive()).toBe(false);
|
|
439
|
+
expect(sendUserMessageSpy).not.toHaveBeenCalled();
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it('NEW BEHAVIOR: converts incomplete continue to steer', async () => {
|
|
443
|
+
const decision: SteeringDecision = {
|
|
444
|
+
action: 'continue',
|
|
445
|
+
reasoning: 'Need more work',
|
|
446
|
+
confidence: 0.6,
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
await simulateAgentEnd(decision);
|
|
450
|
+
|
|
451
|
+
// Should steer, not just loop
|
|
452
|
+
expect(sendUserMessageSpy).toHaveBeenCalled();
|
|
453
|
+
expect(stateManager.isActive()).toBe(true);
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
});
|