@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.
Files changed (41) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +21 -0
  3. package/README.md +285 -0
  4. package/media/demo.mp4 +0 -0
  5. package/media/pi-loop.jpg +0 -0
  6. package/media/screenshot.png +0 -0
  7. package/package.json +89 -0
  8. package/src/core/analyzer.ts +51 -0
  9. package/src/core/content-extractor.ts +79 -0
  10. package/src/core/inference.ts +137 -0
  11. package/src/core/prompt-builder.ts +217 -0
  12. package/src/core/prompt-loader.ts +126 -0
  13. package/src/core/reframe.ts +30 -0
  14. package/src/core/snapshot-builder.ts +252 -0
  15. package/src/global-config.ts +38 -0
  16. package/src/index.ts +532 -0
  17. package/src/session/client.ts +47 -0
  18. package/src/session/loop-session.ts +102 -0
  19. package/src/session/response-parser.ts +37 -0
  20. package/src/state/manager.ts +164 -0
  21. package/src/state/patterns.ts +81 -0
  22. package/src/state/reframe.ts +33 -0
  23. package/src/subagent-detector.ts +94 -0
  24. package/src/types.ts +83 -0
  25. package/src/ui/animations.ts +70 -0
  26. package/src/ui/model-picker.ts +79 -0
  27. package/src/ui/renderer.ts +257 -0
  28. package/src/ui/status-widget.ts +30 -0
  29. package/src/ui/types.ts +48 -0
  30. package/tests/compaction.test.ts +754 -0
  31. package/tests/continue-action-regression.test.ts +456 -0
  32. package/tests/engine.test.ts +770 -0
  33. package/tests/ephemeral-supervision.test.ts +391 -0
  34. package/tests/full-fidelity-snapshot.test.ts +843 -0
  35. package/tests/parsing.test.ts +303 -0
  36. package/tests/state.test.ts +525 -0
  37. package/tests/status-widget.test.ts +703 -0
  38. package/tests/subagent-detector.test.ts +191 -0
  39. package/tests/supervise-command.test.ts +381 -0
  40. package/tsconfig.json +14 -0
  41. package/vitest.config.ts +15 -0
@@ -0,0 +1,391 @@
1
+ import { describe, expect, it, vi, beforeEach } from 'vitest';
2
+ import { LoopStateManager } from '../src/state/manager.js';
3
+
4
+ // Mock dependencies
5
+ vi.mock('../src/core/analyzer.js', () => ({
6
+ analyze: vi.fn(),
7
+ }));
8
+
9
+ vi.mock('../src/core/inference.js', () => ({
10
+ inferOutcome: vi.fn(),
11
+ }));
12
+
13
+ vi.mock('../src/core/prompt-loader.js', () => ({
14
+ loadSystemPrompt: vi.fn().mockReturnValue({ prompt: 'test prompt', source: 'built-in' }),
15
+ }));
16
+
17
+ vi.mock('../src/ui/renderer.js', () => ({
18
+ updateUI: vi.fn(),
19
+ toggleWidget: vi.fn(),
20
+ }));
21
+
22
+ vi.mock('../src/ui/model-picker.js', () => ({
23
+ pickModel: vi.fn(),
24
+ }));
25
+
26
+ vi.mock('../src/ui/settings-panel.js', () => ({
27
+ openSettings: vi.fn(),
28
+ }));
29
+
30
+ vi.mock('../src/global-config.js', () => ({
31
+ loadGlobalModel: vi.fn().mockReturnValue(null),
32
+ saveGlobalModel: vi.fn(),
33
+ }));
34
+
35
+ vi.mock('../src/session/client.js', () => ({
36
+ disposeSession: vi.fn(),
37
+ }));
38
+
39
+ vi.mock('../src/subagent-detector.js', () => ({
40
+ checkChildPiProcesses: vi.fn().mockResolvedValue({ hasActiveSubagents: false, count: 0 }),
41
+ waitForSubagents: vi
42
+ .fn()
43
+ .mockResolvedValue({ completed: true, finalStatus: { hasActiveSubagents: false, count: 0 } }),
44
+ }));
45
+
46
+ import { updateUI } from '../src/ui/renderer.js';
47
+ import { disposeSession } from '../src/session/client.js';
48
+
49
+ // Mock ExtensionAPI
50
+ function createMockApi() {
51
+ return {
52
+ appendEntry: vi.fn(),
53
+ on: vi.fn(),
54
+ registerCommand: vi.fn(),
55
+ registerTool: vi.fn(),
56
+ sendUserMessage: vi.fn(),
57
+ sendMessage: vi.fn(),
58
+ events: { emit: vi.fn(), on: vi.fn() },
59
+ } as any;
60
+ }
61
+
62
+ function createMockContext(entries: any[] = [], isIdle = true) {
63
+ return {
64
+ ui: {
65
+ notify: vi.fn(),
66
+ setStatus: vi.fn(),
67
+ setWidget: vi.fn(),
68
+ setWorkingMessage: vi.fn(),
69
+ },
70
+ hasUI: true,
71
+ cwd: '/test',
72
+ sessionManager: {
73
+ getBranch: vi.fn().mockReturnValue(entries),
74
+ },
75
+ modelRegistry: {},
76
+ model: undefined,
77
+ isIdle: vi.fn().mockReturnValue(isIdle),
78
+ abort: vi.fn(),
79
+ hasPendingMessages: vi.fn().mockReturnValue(false),
80
+ shutdown: vi.fn(),
81
+ getContextUsage: vi.fn(),
82
+ compact: vi.fn(),
83
+ getSystemPrompt: vi.fn().mockReturnValue('test'),
84
+ } as any;
85
+ }
86
+
87
+ describe('Ephemeral Supervision - idle agent clears supervision', () => {
88
+ let api: ReturnType<typeof createMockApi>;
89
+ let state: LoopStateManager;
90
+
91
+ beforeEach(() => {
92
+ api = createMockApi();
93
+ state = new LoopStateManager(api);
94
+ vi.clearAllMocks();
95
+ });
96
+
97
+ function startActiveSupervision() {
98
+ state.start('Test goal', 'anthropic', 'claude-haiku');
99
+ state.incrementTurnCount();
100
+ expect(state.isActive()).toBe(true);
101
+ }
102
+
103
+ function createSessionWithSupervision() {
104
+ return [
105
+ { type: 'message', message: { role: 'user', content: 'Hello' } },
106
+ {
107
+ type: 'custom',
108
+ customType: 'loop-state',
109
+ data: {
110
+ active: true,
111
+ outcome: 'Test goal',
112
+ provider: 'anthropic',
113
+ modelId: 'claude-haiku',
114
+ interventions: [],
115
+ startedAt: Date.now(),
116
+ turnCount: 1,
117
+ reframeTier: 0,
118
+ lastSteerTurn: 0,
119
+ },
120
+ },
121
+ ];
122
+ }
123
+
124
+ describe('session_start (crash resume)', () => {
125
+ it('clears supervision when agent is idle', () => {
126
+ startActiveSupervision();
127
+
128
+ // Simulate session_start with idle agent (e.g., after crash)
129
+ const entries = createSessionWithSupervision();
130
+ const ctx = createMockContext(entries, true /* idle */);
131
+
132
+ state.loadFromSession(ctx);
133
+
134
+ // Ephemeral rule: idle agent means supervision is cleared
135
+ expect(state.isActive()).toBe(true); // Still active after load
136
+
137
+ // But onSessionLoad handler should stop it
138
+ if (state.isActive() && ctx.isIdle()) {
139
+ state.stop();
140
+ disposeSession();
141
+ }
142
+
143
+ expect(state.isActive()).toBe(false);
144
+ });
145
+
146
+ it('keeps supervision when agent is working', () => {
147
+ startActiveSupervision();
148
+
149
+ // Simulate session_start with working agent
150
+ const entries = createSessionWithSupervision();
151
+ const ctx = createMockContext(entries, false /* working */);
152
+
153
+ state.loadFromSession(ctx);
154
+
155
+ // Ephemeral rule: working agent means supervision continues
156
+ expect(state.isActive()).toBe(true);
157
+ });
158
+ });
159
+
160
+ describe('session_start with resume reason (resume another session)', () => {
161
+ it('clears supervision when agent is idle', () => {
162
+ startActiveSupervision();
163
+
164
+ const entries = createSessionWithSupervision();
165
+ const ctx = createMockContext(entries, true /* idle */);
166
+
167
+ state.loadFromSession(ctx);
168
+
169
+ // Ephemeral rule applies
170
+ if (state.isActive() && ctx.isIdle()) {
171
+ state.stop();
172
+ disposeSession();
173
+ }
174
+
175
+ expect(state.isActive()).toBe(false);
176
+ });
177
+
178
+ it('keeps supervision when agent is working', () => {
179
+ startActiveSupervision();
180
+
181
+ const entries = createSessionWithSupervision();
182
+ const ctx = createMockContext(entries, false /* working */);
183
+
184
+ state.loadFromSession(ctx);
185
+
186
+ expect(state.isActive()).toBe(true);
187
+ });
188
+ });
189
+
190
+ describe('session_tree (navigate history)', () => {
191
+ it('clears supervision when navigating to history while idle', () => {
192
+ startActiveSupervision();
193
+
194
+ const entries = createSessionWithSupervision();
195
+ const ctx = createMockContext(entries, true /* idle */);
196
+
197
+ state.loadFromSession(ctx);
198
+
199
+ // Ephemeral rule: viewing history + idle = no supervision
200
+ if (state.isActive() && ctx.isIdle()) {
201
+ state.stop();
202
+ disposeSession();
203
+ }
204
+
205
+ expect(state.isActive()).toBe(false);
206
+ });
207
+
208
+ it('keeps supervision when at current head with working agent', () => {
209
+ startActiveSupervision();
210
+
211
+ const entries = createSessionWithSupervision();
212
+ const ctx = createMockContext(entries, false /* working */);
213
+
214
+ state.loadFromSession(ctx);
215
+
216
+ expect(state.isActive()).toBe(true);
217
+ });
218
+ });
219
+
220
+ describe('session_start with fork reason (fork session)', () => {
221
+ it('clears supervision when agent is idle', () => {
222
+ startActiveSupervision();
223
+
224
+ const entries = createSessionWithSupervision();
225
+ const ctx = createMockContext(entries, true /* idle */);
226
+
227
+ state.loadFromSession(ctx);
228
+
229
+ if (state.isActive() && ctx.isIdle()) {
230
+ state.stop();
231
+ disposeSession();
232
+ }
233
+
234
+ expect(state.isActive()).toBe(false);
235
+ });
236
+
237
+ it('keeps supervision when agent is working', () => {
238
+ startActiveSupervision();
239
+
240
+ const entries = createSessionWithSupervision();
241
+ const ctx = createMockContext(entries, false /* working */);
242
+
243
+ state.loadFromSession(ctx);
244
+
245
+ expect(state.isActive()).toBe(true);
246
+ });
247
+ });
248
+ });
249
+
250
+ describe('Ephemeral Supervision - compaction behavior', () => {
251
+ let api: ReturnType<typeof createMockApi>;
252
+ let state: LoopStateManager;
253
+
254
+ beforeEach(() => {
255
+ api = createMockApi();
256
+ state = new LoopStateManager(api);
257
+ vi.clearAllMocks();
258
+ });
259
+
260
+ function createSessionWithSupervision() {
261
+ return [
262
+ { type: 'compaction', summary: 'Earlier conversation' },
263
+ { type: 'message', message: { role: 'user', content: 'Continue' } },
264
+ {
265
+ type: 'custom',
266
+ customType: 'loop-state',
267
+ data: {
268
+ active: true,
269
+ outcome: 'Test goal',
270
+ provider: 'anthropic',
271
+ modelId: 'claude-haiku',
272
+ interventions: [],
273
+ startedAt: Date.now(),
274
+ turnCount: 5,
275
+ reframeTier: 1,
276
+ lastSteerTurn: 3,
277
+ },
278
+ },
279
+ ];
280
+ }
281
+
282
+ it('continues supervision when agent is working after compaction (long-horizon sessions)', () => {
283
+ // Start supervision
284
+ state.start('Test goal', 'anthropic', 'claude-haiku');
285
+ state.incrementTurnCount();
286
+ expect(state.isActive()).toBe(true);
287
+
288
+ // Simulate post-compaction state with WORKING agent
289
+ const entries = createSessionWithSupervision();
290
+ const ctx = createMockContext(entries, false /* working, not idle */);
291
+
292
+ state.loadFromSession(ctx);
293
+
294
+ // Ephemeral rule: working agent means supervision continues
295
+ // This enables long-horizon supervised sessions that auto-compact
296
+ expect(state.isActive()).toBe(true);
297
+ expect(state.getState()?.turnCount).toBe(5);
298
+ expect(state.getReframeTier()).toBe(1);
299
+ });
300
+
301
+ it('clears supervision when agent is idle after compaction', () => {
302
+ // Start supervision
303
+ state.start('Test goal', 'anthropic', 'claude-haiku');
304
+ state.incrementTurnCount();
305
+ expect(state.isActive()).toBe(true);
306
+
307
+ // Simulate post-compaction state with IDLE agent
308
+ const entries = createSessionWithSupervision();
309
+ const ctx = createMockContext(entries, true /* idle */);
310
+
311
+ state.loadFromSession(ctx);
312
+
313
+ // Ephemeral rule: idle agent means supervision is cleared
314
+ if (state.isActive() && ctx.isIdle()) {
315
+ state.stop();
316
+ disposeSession();
317
+ }
318
+
319
+ expect(state.isActive()).toBe(false);
320
+ });
321
+ });
322
+
323
+ describe('Ephemeral Supervision - UI notifications', () => {
324
+ let api: ReturnType<typeof createMockApi>;
325
+ let state: LoopStateManager;
326
+
327
+ beforeEach(() => {
328
+ api = createMockApi();
329
+ state = new LoopStateManager(api);
330
+ vi.clearAllMocks();
331
+ });
332
+
333
+ function createSessionWithSupervision() {
334
+ return [
335
+ {
336
+ type: 'custom',
337
+ customType: 'loop-state',
338
+ data: {
339
+ active: true,
340
+ outcome: 'Test goal',
341
+ provider: 'anthropic',
342
+ modelId: 'claude-haiku',
343
+ interventions: [],
344
+ startedAt: Date.now(),
345
+ turnCount: 1,
346
+ reframeTier: 0,
347
+ lastSteerTurn: 0,
348
+ },
349
+ },
350
+ ];
351
+ }
352
+
353
+ it('notifies user when supervision is cleared on idle session load', () => {
354
+ const entries = createSessionWithSupervision();
355
+ const notify = vi.fn();
356
+ const ctx = {
357
+ ...createMockContext(entries, true /* idle */),
358
+ ui: { ...createMockContext().ui, notify },
359
+ };
360
+
361
+ state.loadFromSession(ctx);
362
+
363
+ // Simulate onSessionLoad notification
364
+ if (state.isActive() && ctx.isIdle()) {
365
+ notify('Supervision cleared: agent is idle', 'info');
366
+ }
367
+
368
+ expect(notify).toHaveBeenCalledWith('Supervision cleared: agent is idle', 'info');
369
+ });
370
+
371
+ it('notifies user when supervision is cleared after compaction', () => {
372
+ const entries = createSessionWithSupervision();
373
+ const notify = vi.fn();
374
+ const ctx = {
375
+ ...createMockContext(entries, true /* idle */),
376
+ ui: { ...createMockContext().ui, notify },
377
+ };
378
+
379
+ state.loadFromSession(ctx);
380
+
381
+ // Simulate session_compact handler notification
382
+ if (state.isActive() && ctx.isIdle()) {
383
+ notify('Supervision cleared: compaction complete, agent idle', 'info');
384
+ }
385
+
386
+ expect(notify).toHaveBeenCalledWith(
387
+ 'Supervision cleared: compaction complete, agent idle',
388
+ 'info'
389
+ );
390
+ });
391
+ });