@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,303 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { parseDecision } from '../src/session/response-parser.js';
|
|
3
|
+
import { extractThinking } from '../src/index.js';
|
|
4
|
+
|
|
5
|
+
describe('parseDecision', () => {
|
|
6
|
+
it('parses valid continue response', () => {
|
|
7
|
+
const text = JSON.stringify({
|
|
8
|
+
action: 'continue',
|
|
9
|
+
reasoning: 'Making good progress',
|
|
10
|
+
confidence: 0.9,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const result = parseDecision(text);
|
|
14
|
+
expect(result.action).toBe('continue');
|
|
15
|
+
expect(result.reasoning).toBe('Making good progress');
|
|
16
|
+
expect(result.confidence).toBe(0.9);
|
|
17
|
+
expect(result.message).toBeUndefined();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('parses valid steer response', () => {
|
|
21
|
+
const text = JSON.stringify({
|
|
22
|
+
action: 'steer',
|
|
23
|
+
message: 'Focus on the tests first',
|
|
24
|
+
reasoning: 'Agent is skipping tests',
|
|
25
|
+
confidence: 0.95,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const result = parseDecision(text);
|
|
29
|
+
expect(result.action).toBe('steer');
|
|
30
|
+
expect(result.message).toBe('Focus on the tests first');
|
|
31
|
+
expect(result.reasoning).toBe('Agent is skipping tests');
|
|
32
|
+
expect(result.confidence).toBe(0.95);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('parses valid done response', () => {
|
|
36
|
+
const text = JSON.stringify({
|
|
37
|
+
action: 'done',
|
|
38
|
+
reasoning: 'Goal achieved',
|
|
39
|
+
confidence: 0.99,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const result = parseDecision(text);
|
|
43
|
+
expect(result.action).toBe('done');
|
|
44
|
+
expect(result.reasoning).toBe('Goal achieved');
|
|
45
|
+
expect(result.confidence).toBe(0.99);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('extracts JSON from markdown code block', () => {
|
|
49
|
+
const json = JSON.stringify({
|
|
50
|
+
action: 'steer',
|
|
51
|
+
message: 'Fix the error',
|
|
52
|
+
reasoning: 'Test failed',
|
|
53
|
+
confidence: 0.85,
|
|
54
|
+
});
|
|
55
|
+
const text = '```json\n' + json + '\n```';
|
|
56
|
+
|
|
57
|
+
const result = parseDecision(text);
|
|
58
|
+
expect(result.action).toBe('steer');
|
|
59
|
+
expect(result.message).toBe('Fix the error');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('extracts JSON from plain code block', () => {
|
|
63
|
+
const json = JSON.stringify({
|
|
64
|
+
action: 'continue',
|
|
65
|
+
reasoning: 'On track',
|
|
66
|
+
confidence: 0.8,
|
|
67
|
+
});
|
|
68
|
+
const text = '```\n' + json + '\n```';
|
|
69
|
+
|
|
70
|
+
const result = parseDecision(text);
|
|
71
|
+
expect(result.action).toBe('continue');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('extracts JSON from curly braces when no code block', () => {
|
|
75
|
+
const text =
|
|
76
|
+
'Some text before { "action": "steer", "message": "Help", "reasoning": "Test", "confidence": 0.7 } some after';
|
|
77
|
+
|
|
78
|
+
const result = parseDecision(text);
|
|
79
|
+
expect(result.action).toBe('steer');
|
|
80
|
+
expect(result.message).toBe('Help');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('returns continue on invalid JSON', () => {
|
|
84
|
+
const text = 'not valid json at all';
|
|
85
|
+
|
|
86
|
+
const result = parseDecision(text);
|
|
87
|
+
expect(result.action).toBe('continue');
|
|
88
|
+
expect(result.reasoning).toBe('Failed to parse supervisor JSON decision');
|
|
89
|
+
expect(result.confidence).toBe(0);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('returns continue on invalid action', () => {
|
|
93
|
+
const text = JSON.stringify({
|
|
94
|
+
action: 'invalid',
|
|
95
|
+
reasoning: 'Something',
|
|
96
|
+
confidence: 0.5,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const result = parseDecision(text);
|
|
100
|
+
expect(result.action).toBe('continue');
|
|
101
|
+
expect(result.reasoning).toBe('Invalid action in supervisor response');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('handles missing fields with defaults', () => {
|
|
105
|
+
const text = JSON.stringify({
|
|
106
|
+
action: 'continue',
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const result = parseDecision(text);
|
|
110
|
+
expect(result.action).toBe('continue');
|
|
111
|
+
expect(result.reasoning).toBe('');
|
|
112
|
+
expect(result.confidence).toBe(0.5);
|
|
113
|
+
expect(result.message).toBeUndefined();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('trims message whitespace', () => {
|
|
117
|
+
const text = JSON.stringify({
|
|
118
|
+
action: 'steer',
|
|
119
|
+
message: ' Message with whitespace ',
|
|
120
|
+
reasoning: 'Test',
|
|
121
|
+
confidence: 0.8,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const result = parseDecision(text);
|
|
125
|
+
expect(result.message).toBe('Message with whitespace');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('handles escaped quotes in reasoning', () => {
|
|
129
|
+
const text = JSON.stringify({
|
|
130
|
+
action: 'steer',
|
|
131
|
+
message: 'Say "hello"',
|
|
132
|
+
reasoning: 'The agent said "test"',
|
|
133
|
+
confidence: 0.9,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const result = parseDecision(text);
|
|
137
|
+
expect(result.message).toBe('Say "hello"');
|
|
138
|
+
expect(result.reasoning).toBe('The agent said "test"');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('parses ASI from steer response', () => {
|
|
142
|
+
const text = JSON.stringify({
|
|
143
|
+
action: 'steer',
|
|
144
|
+
message: 'Focus on tests',
|
|
145
|
+
reasoning: 'Agent drifting',
|
|
146
|
+
confidence: 0.9,
|
|
147
|
+
asi: {
|
|
148
|
+
why_stuck: 'refactoring without tests',
|
|
149
|
+
strategy_used: 'directive',
|
|
150
|
+
pattern_detected: 'test_skipping',
|
|
151
|
+
confidence_source: 'no test files added',
|
|
152
|
+
would_escalate_sooner: false,
|
|
153
|
+
custom_key: 'custom_value',
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const result = parseDecision(text);
|
|
158
|
+
expect(result.asi).toBeDefined();
|
|
159
|
+
expect(result.asi!.why_stuck).toBe('refactoring without tests');
|
|
160
|
+
expect(result.asi!.strategy_used).toBe('directive');
|
|
161
|
+
expect(result.asi!.pattern_detected).toBe('test_skipping');
|
|
162
|
+
expect(result.asi!.would_escalate_sooner).toBe(false);
|
|
163
|
+
expect(result.asi!.custom_key).toBe('custom_value');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('handles missing ASI gracefully', () => {
|
|
167
|
+
const text = JSON.stringify({
|
|
168
|
+
action: 'steer',
|
|
169
|
+
message: 'Focus',
|
|
170
|
+
reasoning: 'Test',
|
|
171
|
+
confidence: 0.8,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const result = parseDecision(text);
|
|
175
|
+
expect(result.asi).toBeUndefined();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('handles ASI with only partial fields', () => {
|
|
179
|
+
const text = JSON.stringify({
|
|
180
|
+
action: 'done',
|
|
181
|
+
reasoning: 'Complete',
|
|
182
|
+
confidence: 0.99,
|
|
183
|
+
asi: {
|
|
184
|
+
why_stuck: 'already done',
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const result = parseDecision(text);
|
|
189
|
+
expect(result.asi).toBeDefined();
|
|
190
|
+
expect(result.asi!.why_stuck).toBe('already done');
|
|
191
|
+
expect(result.asi!.strategy_used).toBeUndefined();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('parses free-form ASI with arbitrary keys for cheating detection', () => {
|
|
195
|
+
const text = JSON.stringify({
|
|
196
|
+
action: 'steer',
|
|
197
|
+
message: 'Verify your claim',
|
|
198
|
+
reasoning: 'Suspicious unverified claim',
|
|
199
|
+
confidence: 0.9,
|
|
200
|
+
asi: {
|
|
201
|
+
suspicious_claim_detected: true,
|
|
202
|
+
claim_type: 'unverified_test_success',
|
|
203
|
+
evidence: 'agent_said_tests_pass_but_exit_code_1',
|
|
204
|
+
previous_contradiction_turn: 3,
|
|
205
|
+
watch_for: 'fake_test_results',
|
|
206
|
+
custom_observation: 'agent_removed_assertions_in_same_turn',
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const result = parseDecision(text);
|
|
211
|
+
expect(result.asi).toBeDefined();
|
|
212
|
+
expect(result.asi!.suspicious_claim_detected).toBe(true);
|
|
213
|
+
expect(result.asi!.claim_type).toBe('unverified_test_success');
|
|
214
|
+
expect(result.asi!.evidence).toBe('agent_said_tests_pass_but_exit_code_1');
|
|
215
|
+
expect(result.asi!.previous_contradiction_turn).toBe(3);
|
|
216
|
+
expect(result.asi!.watch_for).toBe('fake_test_results');
|
|
217
|
+
expect(result.asi!.custom_observation).toBe('agent_removed_assertions_in_same_turn');
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('parses ASI with cheating-related keywords', () => {
|
|
221
|
+
const text = JSON.stringify({
|
|
222
|
+
action: 'steer',
|
|
223
|
+
message: 'Do not manipulate tests',
|
|
224
|
+
reasoning: 'Test manipulation detected',
|
|
225
|
+
confidence: 0.95,
|
|
226
|
+
asi: {
|
|
227
|
+
pattern: 'test_manipulation',
|
|
228
|
+
indicator: 'fake',
|
|
229
|
+
observation: 'agent_gaming_the_metrics',
|
|
230
|
+
status: 'unverified_claim',
|
|
231
|
+
severity: 'high',
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const result = parseDecision(text);
|
|
236
|
+
expect(result.asi).toBeDefined();
|
|
237
|
+
expect(result.asi!.pattern).toBe('test_manipulation');
|
|
238
|
+
expect(result.asi!.indicator).toBe('fake');
|
|
239
|
+
expect(result.asi!.observation).toBe('agent_gaming_the_metrics');
|
|
240
|
+
expect(result.asi!.status).toBe('unverified_claim');
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe('extractThinking', () => {
|
|
245
|
+
it('returns empty string when no reasoning key', () => {
|
|
246
|
+
const text = '{ "action": "continue" }';
|
|
247
|
+
expect(extractThinking(text)).toBe('');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('extracts reasoning from complete JSON', () => {
|
|
251
|
+
const text = JSON.stringify({
|
|
252
|
+
action: 'steer',
|
|
253
|
+
message: 'Focus',
|
|
254
|
+
reasoning: 'The agent is drifting from the goal',
|
|
255
|
+
confidence: 0.9,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
expect(extractThinking(text)).toBe('The agent is drifting from the goal');
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('extracts reasoning from streaming partial JSON (no closing quote)', () => {
|
|
262
|
+
const text = '{ "action": "steer", "reasoning": "The agent is working on';
|
|
263
|
+
|
|
264
|
+
expect(extractThinking(text)).toBe('The agent is working on');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('extracts reasoning with spaces around colon', () => {
|
|
268
|
+
const text = '{ "reasoning" : "Test reasoning" }';
|
|
269
|
+
|
|
270
|
+
expect(extractThinking(text)).toBe('Test reasoning');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('returns empty string for malformed reasoning key', () => {
|
|
274
|
+
const text = '{ "reasoning": 123 }'; // Not a string
|
|
275
|
+
|
|
276
|
+
expect(extractThinking(text)).toBe('');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('handles escaped newlines in reasoning', () => {
|
|
280
|
+
const text = JSON.stringify({
|
|
281
|
+
reasoning: 'Line 1\nLine 2',
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
expect(extractThinking(text)).toBe('Line 1 Line 2');
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('handles escaped quotes in reasoning', () => {
|
|
288
|
+
const text = '{ "reasoning": "The agent said \\"test\\" here" }';
|
|
289
|
+
|
|
290
|
+
expect(extractThinking(text)).toBe('The agent said "test" here');
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('extracts reasoning that appears later in the JSON', () => {
|
|
294
|
+
const text =
|
|
295
|
+
'{ "action": "steer", "confidence": 0.9, "reasoning": "Late reasoning", "message": "Hi" }';
|
|
296
|
+
|
|
297
|
+
expect(extractThinking(text)).toBe('Late reasoning');
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('returns empty string for empty input', () => {
|
|
301
|
+
expect(extractThinking('')).toBe('');
|
|
302
|
+
});
|
|
303
|
+
});
|