@monotykamary/pi-supervisor 0.5.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/CHANGELOG.md +120 -0
- package/LICENSE +21 -0
- package/README.md +341 -0
- package/media/demo.mp4 +0 -0
- package/media/screenshot.png +0 -0
- package/package.json +87 -0
- package/src/compaction/brief.ts +841 -0
- package/src/compaction/build-sections.ts +340 -0
- package/src/compaction/causal-keys.ts +138 -0
- package/src/compaction/content.ts +68 -0
- package/src/compaction/extract/commits.ts +78 -0
- package/src/compaction/extract/goals.ts +79 -0
- package/src/compaction/extract/preferences.ts +52 -0
- package/src/compaction/extract/shared-symbols.ts +376 -0
- package/src/compaction/filter-noise.ts +47 -0
- package/src/compaction/format.ts +89 -0
- package/src/compaction/index.ts +38 -0
- package/src/compaction/normalize.ts +73 -0
- package/src/compaction/sanitize.ts +5 -0
- package/src/compaction/sections.ts +19 -0
- package/src/compaction/skill-collapse.ts +35 -0
- package/src/compaction/tool-args.ts +14 -0
- package/src/compaction/types.ts +26 -0
- package/src/core/analyzer.ts +58 -0
- package/src/core/index.ts +8 -0
- package/src/core/inference.ts +77 -0
- package/src/core/prompt-builder.ts +137 -0
- package/src/core/prompt-loader.ts +125 -0
- package/src/core/reframe.ts +27 -0
- package/src/fabric-provider.ts +115 -0
- package/src/global-config.ts +65 -0
- package/src/index.ts +514 -0
- package/src/session/client.ts +46 -0
- package/src/session/response-parser.ts +37 -0
- package/src/session/supervisor-session.ts +102 -0
- package/src/state/manager.ts +133 -0
- package/src/state/mid-run-signals.ts +103 -0
- package/src/state/patterns.ts +82 -0
- package/src/state/reframe.ts +27 -0
- package/src/subagent-detector.ts +94 -0
- package/src/types.ts +42 -0
- package/src/ui/animations.ts +95 -0
- package/src/ui/model-picker.ts +72 -0
- package/src/ui/model-settings-selector.ts +440 -0
- package/src/ui/model-sort.ts +101 -0
- package/src/ui/renderer.ts +314 -0
- package/src/ui/types.ts +48 -0
- package/tests/compaction.test.ts +507 -0
- package/tests/engine.test.ts +622 -0
- package/tests/ephemeral-supervision.test.ts +347 -0
- package/tests/fabric-provider.test.ts +55 -0
- package/tests/full-fidelity-snapshot.test.ts +250 -0
- package/tests/global-config.test.ts +74 -0
- package/tests/model-sort.test.ts +157 -0
- package/tests/parsing.test.ts +303 -0
- package/tests/state.test.ts +474 -0
- package/tests/status-widget.test.ts +539 -0
- package/tests/subagent-detector.test.ts +191 -0
- package/tests/supervise-command.test.ts +363 -0
- package/tests/supervise-model-command.test.ts +184 -0
- package/tsconfig.json +14 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { SupervisorStateManager } 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/global-config.js', () => ({
|
|
27
|
+
loadGlobalModel: vi.fn().mockReturnValue(null),
|
|
28
|
+
saveGlobalModel: vi.fn(),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
vi.mock('../src/session/client.js', () => ({
|
|
32
|
+
disposeSession: vi.fn(),
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
vi.mock('../src/subagent-detector.js', () => ({
|
|
36
|
+
checkChildPiProcesses: vi.fn().mockResolvedValue({ hasActiveSubagents: false, count: 0 }),
|
|
37
|
+
waitForSubagents: vi
|
|
38
|
+
.fn()
|
|
39
|
+
.mockResolvedValue({ completed: true, finalStatus: { hasActiveSubagents: false, count: 0 } }),
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
import { updateUI } from '../src/ui/renderer.js';
|
|
43
|
+
import { disposeSession } from '../src/session/client.js';
|
|
44
|
+
|
|
45
|
+
function createMockApi() {
|
|
46
|
+
return {
|
|
47
|
+
appendEntry: vi.fn(),
|
|
48
|
+
on: vi.fn(),
|
|
49
|
+
registerCommand: vi.fn(),
|
|
50
|
+
registerTool: vi.fn(),
|
|
51
|
+
sendUserMessage: vi.fn(),
|
|
52
|
+
sendMessage: vi.fn(),
|
|
53
|
+
events: { emit: vi.fn(), on: vi.fn() },
|
|
54
|
+
} as any;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function createMockContext(entries: any[] = [], isIdle = true) {
|
|
58
|
+
return {
|
|
59
|
+
ui: {
|
|
60
|
+
notify: vi.fn(),
|
|
61
|
+
setStatus: vi.fn(),
|
|
62
|
+
setWidget: vi.fn(),
|
|
63
|
+
setWorkingMessage: vi.fn(),
|
|
64
|
+
},
|
|
65
|
+
hasUI: true,
|
|
66
|
+
cwd: '/test',
|
|
67
|
+
sessionManager: {
|
|
68
|
+
getBranch: vi.fn().mockReturnValue(entries),
|
|
69
|
+
},
|
|
70
|
+
modelRegistry: {},
|
|
71
|
+
model: undefined,
|
|
72
|
+
isIdle: vi.fn().mockReturnValue(isIdle),
|
|
73
|
+
abort: vi.fn(),
|
|
74
|
+
hasPendingMessages: vi.fn().mockReturnValue(false),
|
|
75
|
+
shutdown: vi.fn(),
|
|
76
|
+
getContextUsage: vi.fn(),
|
|
77
|
+
compact: vi.fn(),
|
|
78
|
+
getSystemPrompt: vi.fn().mockReturnValue('test'),
|
|
79
|
+
} as any;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function makeSupervisionData(overrides: Record<string, any> = {}) {
|
|
83
|
+
return {
|
|
84
|
+
active: true,
|
|
85
|
+
outcome: 'Test goal',
|
|
86
|
+
provider: 'anthropic',
|
|
87
|
+
modelId: 'claude-haiku',
|
|
88
|
+
interventions: [],
|
|
89
|
+
startedAt: Date.now(),
|
|
90
|
+
reframeTier: 0,
|
|
91
|
+
...overrides,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
describe('Ephemeral Supervision - idle agent clears supervision', () => {
|
|
96
|
+
let api: ReturnType<typeof createMockApi>;
|
|
97
|
+
let state: SupervisorStateManager;
|
|
98
|
+
|
|
99
|
+
beforeEach(() => {
|
|
100
|
+
api = createMockApi();
|
|
101
|
+
state = new SupervisorStateManager(api);
|
|
102
|
+
vi.clearAllMocks();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
function startActiveSupervision() {
|
|
106
|
+
state.start('Test goal', 'anthropic', 'claude-haiku');
|
|
107
|
+
expect(state.isActive()).toBe(true);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function createSessionWithSupervision(overrides: Record<string, any> = {}) {
|
|
111
|
+
return [
|
|
112
|
+
{ type: 'message', message: { role: 'user', content: 'Hello' } },
|
|
113
|
+
{
|
|
114
|
+
type: 'custom',
|
|
115
|
+
customType: 'supervisor-state',
|
|
116
|
+
data: makeSupervisionData(overrides),
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
describe('session_start (crash resume)', () => {
|
|
122
|
+
it('clears supervision when agent is idle', () => {
|
|
123
|
+
startActiveSupervision();
|
|
124
|
+
|
|
125
|
+
const entries = createSessionWithSupervision();
|
|
126
|
+
const ctx = createMockContext(entries, true);
|
|
127
|
+
|
|
128
|
+
state.loadFromSession(ctx);
|
|
129
|
+
|
|
130
|
+
if (state.isActive() && ctx.isIdle()) {
|
|
131
|
+
state.stop();
|
|
132
|
+
disposeSession();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
expect(state.isActive()).toBe(false);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('keeps supervision when agent is working', () => {
|
|
139
|
+
startActiveSupervision();
|
|
140
|
+
|
|
141
|
+
const entries = createSessionWithSupervision();
|
|
142
|
+
const ctx = createMockContext(entries, false);
|
|
143
|
+
|
|
144
|
+
state.loadFromSession(ctx);
|
|
145
|
+
|
|
146
|
+
expect(state.isActive()).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('session_start with resume reason (resume another session)', () => {
|
|
151
|
+
it('clears supervision when agent is idle', () => {
|
|
152
|
+
startActiveSupervision();
|
|
153
|
+
|
|
154
|
+
const entries = createSessionWithSupervision();
|
|
155
|
+
const ctx = createMockContext(entries, true);
|
|
156
|
+
|
|
157
|
+
state.loadFromSession(ctx);
|
|
158
|
+
|
|
159
|
+
if (state.isActive() && ctx.isIdle()) {
|
|
160
|
+
state.stop();
|
|
161
|
+
disposeSession();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
expect(state.isActive()).toBe(false);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('keeps supervision when agent is working', () => {
|
|
168
|
+
startActiveSupervision();
|
|
169
|
+
|
|
170
|
+
const entries = createSessionWithSupervision();
|
|
171
|
+
const ctx = createMockContext(entries, false);
|
|
172
|
+
|
|
173
|
+
state.loadFromSession(ctx);
|
|
174
|
+
|
|
175
|
+
expect(state.isActive()).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('session_tree (navigate history)', () => {
|
|
180
|
+
it('clears supervision when navigating to history while idle', () => {
|
|
181
|
+
startActiveSupervision();
|
|
182
|
+
|
|
183
|
+
const entries = createSessionWithSupervision();
|
|
184
|
+
const ctx = createMockContext(entries, true);
|
|
185
|
+
|
|
186
|
+
state.loadFromSession(ctx);
|
|
187
|
+
|
|
188
|
+
if (state.isActive() && ctx.isIdle()) {
|
|
189
|
+
state.stop();
|
|
190
|
+
disposeSession();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
expect(state.isActive()).toBe(false);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('keeps supervision when at current head with working agent', () => {
|
|
197
|
+
startActiveSupervision();
|
|
198
|
+
|
|
199
|
+
const entries = createSessionWithSupervision();
|
|
200
|
+
const ctx = createMockContext(entries, false);
|
|
201
|
+
|
|
202
|
+
state.loadFromSession(ctx);
|
|
203
|
+
|
|
204
|
+
expect(state.isActive()).toBe(true);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe('session_start with fork reason (fork session)', () => {
|
|
209
|
+
it('clears supervision when agent is idle', () => {
|
|
210
|
+
startActiveSupervision();
|
|
211
|
+
|
|
212
|
+
const entries = createSessionWithSupervision();
|
|
213
|
+
const ctx = createMockContext(entries, true);
|
|
214
|
+
|
|
215
|
+
state.loadFromSession(ctx);
|
|
216
|
+
|
|
217
|
+
if (state.isActive() && ctx.isIdle()) {
|
|
218
|
+
state.stop();
|
|
219
|
+
disposeSession();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
expect(state.isActive()).toBe(false);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('keeps supervision when agent is working', () => {
|
|
226
|
+
startActiveSupervision();
|
|
227
|
+
|
|
228
|
+
const entries = createSessionWithSupervision();
|
|
229
|
+
const ctx = createMockContext(entries, false);
|
|
230
|
+
|
|
231
|
+
state.loadFromSession(ctx);
|
|
232
|
+
|
|
233
|
+
expect(state.isActive()).toBe(true);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe('Ephemeral Supervision - compaction behavior', () => {
|
|
239
|
+
let api: ReturnType<typeof createMockApi>;
|
|
240
|
+
let state: SupervisorStateManager;
|
|
241
|
+
|
|
242
|
+
beforeEach(() => {
|
|
243
|
+
api = createMockApi();
|
|
244
|
+
state = new SupervisorStateManager(api);
|
|
245
|
+
vi.clearAllMocks();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
function createSessionWithSupervision(overrides: Record<string, any> = {}) {
|
|
249
|
+
return [
|
|
250
|
+
{ type: 'compaction', summary: 'Earlier conversation' },
|
|
251
|
+
{ type: 'message', message: { role: 'user', content: 'Continue' } },
|
|
252
|
+
{
|
|
253
|
+
type: 'custom',
|
|
254
|
+
customType: 'supervisor-state',
|
|
255
|
+
data: makeSupervisionData(overrides),
|
|
256
|
+
},
|
|
257
|
+
];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
it('continues supervision when agent is working after compaction (long-horizon sessions)', () => {
|
|
261
|
+
state.start('Test goal', 'anthropic', 'claude-haiku');
|
|
262
|
+
expect(state.isActive()).toBe(true);
|
|
263
|
+
|
|
264
|
+
const entries = createSessionWithSupervision({ reframeTier: 1 });
|
|
265
|
+
const ctx = createMockContext(entries, false);
|
|
266
|
+
|
|
267
|
+
state.loadFromSession(ctx);
|
|
268
|
+
|
|
269
|
+
expect(state.isActive()).toBe(true);
|
|
270
|
+
expect(state.getReframeTier()).toBe(1);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('clears supervision when agent is idle after compaction', () => {
|
|
274
|
+
state.start('Test goal', 'anthropic', 'claude-haiku');
|
|
275
|
+
expect(state.isActive()).toBe(true);
|
|
276
|
+
|
|
277
|
+
const entries = createSessionWithSupervision();
|
|
278
|
+
const ctx = createMockContext(entries, true);
|
|
279
|
+
|
|
280
|
+
state.loadFromSession(ctx);
|
|
281
|
+
|
|
282
|
+
if (state.isActive() && ctx.isIdle()) {
|
|
283
|
+
state.stop();
|
|
284
|
+
disposeSession();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
expect(state.isActive()).toBe(false);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe('Ephemeral Supervision - UI notifications', () => {
|
|
292
|
+
let api: ReturnType<typeof createMockApi>;
|
|
293
|
+
let state: SupervisorStateManager;
|
|
294
|
+
|
|
295
|
+
beforeEach(() => {
|
|
296
|
+
api = createMockApi();
|
|
297
|
+
state = new SupervisorStateManager(api);
|
|
298
|
+
vi.clearAllMocks();
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
function createSessionWithSupervision(overrides: Record<string, any> = {}) {
|
|
302
|
+
return [
|
|
303
|
+
{
|
|
304
|
+
type: 'custom',
|
|
305
|
+
customType: 'supervisor-state',
|
|
306
|
+
data: makeSupervisionData(overrides),
|
|
307
|
+
},
|
|
308
|
+
];
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
it('notifies user when supervision is cleared on idle session load', () => {
|
|
312
|
+
const entries = createSessionWithSupervision();
|
|
313
|
+
const notify = vi.fn();
|
|
314
|
+
const ctx = {
|
|
315
|
+
...createMockContext(entries, true),
|
|
316
|
+
ui: { ...createMockContext().ui, notify },
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
state.loadFromSession(ctx);
|
|
320
|
+
|
|
321
|
+
if (state.isActive() && ctx.isIdle()) {
|
|
322
|
+
notify('Supervision cleared: agent is idle', 'info');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
expect(notify).toHaveBeenCalledWith('Supervision cleared: agent is idle', 'info');
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('notifies user when supervision is cleared after compaction', () => {
|
|
329
|
+
const entries = createSessionWithSupervision();
|
|
330
|
+
const notify = vi.fn();
|
|
331
|
+
const ctx = {
|
|
332
|
+
...createMockContext(entries, true),
|
|
333
|
+
ui: { ...createMockContext().ui, notify },
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
state.loadFromSession(ctx);
|
|
337
|
+
|
|
338
|
+
if (state.isActive() && ctx.isIdle()) {
|
|
339
|
+
notify('Supervision cleared: compaction complete, agent idle', 'info');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
expect(notify).toHaveBeenCalledWith(
|
|
343
|
+
'Supervision cleared: compaction complete, agent idle',
|
|
344
|
+
'info'
|
|
345
|
+
);
|
|
346
|
+
});
|
|
347
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
3
|
+
import { registerFabricProvider } from '../src/fabric-provider.js';
|
|
4
|
+
|
|
5
|
+
const createApi = () => {
|
|
6
|
+
const listeners = new Map<string, (value: unknown) => void>();
|
|
7
|
+
const emit = vi.fn();
|
|
8
|
+
const api = {
|
|
9
|
+
events: {
|
|
10
|
+
emit,
|
|
11
|
+
on: vi.fn((name: string, handler: (value: unknown) => void) => {
|
|
12
|
+
listeners.set(name, handler);
|
|
13
|
+
}),
|
|
14
|
+
},
|
|
15
|
+
} as any as ExtensionAPI;
|
|
16
|
+
return { api, emit, listeners };
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
describe('pi-fabric supervisor provider', () => {
|
|
20
|
+
it('registers eagerly and through discovery', async () => {
|
|
21
|
+
const { api, emit, listeners } = createApi();
|
|
22
|
+
const state = {
|
|
23
|
+
active: true,
|
|
24
|
+
outcome: 'Ship the feature',
|
|
25
|
+
provider: 'test',
|
|
26
|
+
modelId: 'model',
|
|
27
|
+
interventions: [],
|
|
28
|
+
startedAt: 1,
|
|
29
|
+
};
|
|
30
|
+
const start = vi.fn(async () => 'started');
|
|
31
|
+
registerFabricProvider(api, { start, getState: () => state });
|
|
32
|
+
|
|
33
|
+
expect(emit).toHaveBeenCalledWith(
|
|
34
|
+
'pi-fabric:provider:register:v1',
|
|
35
|
+
expect.objectContaining({ version: 1, overwrite: true })
|
|
36
|
+
);
|
|
37
|
+
const registration = emit.mock.calls[0][1];
|
|
38
|
+
const provider = registration.provider;
|
|
39
|
+
expect((await provider.describe('start')).risk).toBe('agent');
|
|
40
|
+
expect(await provider.invoke('status', {}, {})).toBe(state);
|
|
41
|
+
|
|
42
|
+
const context = {} as ExtensionContext;
|
|
43
|
+
await expect(
|
|
44
|
+
provider.invoke('start', { outcome: 'Goal' }, { extensionContext: context })
|
|
45
|
+
).resolves.toEqual({
|
|
46
|
+
message: 'started',
|
|
47
|
+
state,
|
|
48
|
+
});
|
|
49
|
+
expect(start).toHaveBeenCalledWith('Goal', context);
|
|
50
|
+
|
|
51
|
+
const register = vi.fn();
|
|
52
|
+
listeners.get('pi-fabric:provider:discover:v1')?.({ version: 1, register });
|
|
53
|
+
expect(register).toHaveBeenCalledWith(provider, { overwrite: true });
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { normalize } from '../src/compaction/normalize.js';
|
|
3
|
+
import { filterNoise } from '../src/compaction/filter-noise.js';
|
|
4
|
+
import { buildCompactionSummary, formatForSupervisor } from '../src/compaction/index.js';
|
|
5
|
+
import type { NormalizedBlock } from '../src/compaction/types.js';
|
|
6
|
+
|
|
7
|
+
describe('Compaction Pipeline', () => {
|
|
8
|
+
describe('normalize', () => {
|
|
9
|
+
it('normalizes user messages', () => {
|
|
10
|
+
const messages = [{ role: 'user' as const, content: 'Hello world' }];
|
|
11
|
+
const blocks = normalize(messages);
|
|
12
|
+
expect(blocks).toHaveLength(1);
|
|
13
|
+
expect(blocks[0].kind).toBe('user');
|
|
14
|
+
expect(blocks[0]).toHaveProperty('text', 'Hello world');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('normalizes assistant text content', () => {
|
|
18
|
+
const messages = [
|
|
19
|
+
{
|
|
20
|
+
role: 'assistant' as const,
|
|
21
|
+
content: [{ type: 'text' as const, text: 'Working on it' }],
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
const blocks = normalize(messages);
|
|
25
|
+
expect(blocks).toHaveLength(1);
|
|
26
|
+
expect(blocks[0].kind).toBe('assistant');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('normalizes tool calls from assistant content', () => {
|
|
30
|
+
const messages = [
|
|
31
|
+
{
|
|
32
|
+
role: 'assistant' as const,
|
|
33
|
+
content: [
|
|
34
|
+
{
|
|
35
|
+
type: 'toolCall' as const,
|
|
36
|
+
name: 'bash',
|
|
37
|
+
id: 'call_1',
|
|
38
|
+
arguments: { command: 'ls' },
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
const blocks = normalize(messages);
|
|
44
|
+
const toolCall = blocks.find((b) => b.kind === 'tool_call');
|
|
45
|
+
expect(toolCall).toBeDefined();
|
|
46
|
+
if (toolCall && toolCall.kind === 'tool_call') {
|
|
47
|
+
expect(toolCall.name).toBe('bash');
|
|
48
|
+
expect(toolCall.args).toEqual({ command: 'ls' });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('normalizes tool results', () => {
|
|
53
|
+
const messages = [
|
|
54
|
+
{
|
|
55
|
+
role: 'toolResult' as const,
|
|
56
|
+
toolName: 'bash',
|
|
57
|
+
isError: false,
|
|
58
|
+
content: 'file1.txt\nfile2.txt',
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
const blocks = normalize(messages);
|
|
62
|
+
expect(blocks).toHaveLength(1);
|
|
63
|
+
expect(blocks[0].kind).toBe('tool_result');
|
|
64
|
+
if (blocks[0].kind === 'tool_result') {
|
|
65
|
+
expect(blocks[0].name).toBe('bash');
|
|
66
|
+
expect(blocks[0].isError).toBe(false);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('normalizes bashExecution messages', () => {
|
|
71
|
+
const messages = [
|
|
72
|
+
{
|
|
73
|
+
role: 'bashExecution' as any,
|
|
74
|
+
command: 'npm test',
|
|
75
|
+
output: '1 test passed',
|
|
76
|
+
exitCode: 0,
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
const blocks = normalize(messages);
|
|
80
|
+
expect(blocks).toHaveLength(1);
|
|
81
|
+
expect(blocks[0].kind).toBe('bash');
|
|
82
|
+
if (blocks[0].kind === 'bash') {
|
|
83
|
+
expect(blocks[0].command).toBe('npm test');
|
|
84
|
+
expect(blocks[0].exitCode).toBe(0);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('strips thinking blocks from assistant content', () => {
|
|
89
|
+
const messages = [
|
|
90
|
+
{
|
|
91
|
+
role: 'assistant' as const,
|
|
92
|
+
content: [
|
|
93
|
+
{ type: 'thinking' as const, thinking: 'Let me think...', redacted: false },
|
|
94
|
+
{ type: 'text' as const, text: 'Here is my answer' },
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
const blocks = normalize(messages);
|
|
99
|
+
const thinking = blocks.find((b) => b.kind === 'thinking');
|
|
100
|
+
const text = blocks.find((b) => b.kind === 'assistant');
|
|
101
|
+
expect(thinking).toBeDefined();
|
|
102
|
+
expect(text).toBeDefined();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('filterNoise', () => {
|
|
107
|
+
it('removes thinking blocks', () => {
|
|
108
|
+
const blocks: NormalizedBlock[] = [
|
|
109
|
+
{ kind: 'thinking', text: 'Internal reasoning', redacted: false },
|
|
110
|
+
{ kind: 'user', text: 'Hello' },
|
|
111
|
+
];
|
|
112
|
+
const filtered = filterNoise(blocks);
|
|
113
|
+
expect(filtered).toHaveLength(1);
|
|
114
|
+
expect(filtered[0].kind).toBe('user');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('removes noise tool calls (TodoWrite, etc.)', () => {
|
|
118
|
+
const blocks: NormalizedBlock[] = [
|
|
119
|
+
{ kind: 'tool_call', name: 'TodoWrite', args: {} },
|
|
120
|
+
{ kind: 'tool_call', name: 'bash', args: { command: 'ls' } },
|
|
121
|
+
];
|
|
122
|
+
const filtered = filterNoise(blocks);
|
|
123
|
+
expect(filtered).toHaveLength(1);
|
|
124
|
+
if (filtered[0].kind === 'tool_call') {
|
|
125
|
+
expect(filtered[0].name).toBe('bash');
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('removes XML wrapper noise from user messages', () => {
|
|
130
|
+
const blocks: NormalizedBlock[] = [
|
|
131
|
+
{ kind: 'user', text: '<system-reminder>Some directive</system-reminder>' },
|
|
132
|
+
{ kind: 'user', text: 'Real user message' },
|
|
133
|
+
];
|
|
134
|
+
const filtered = filterNoise(blocks);
|
|
135
|
+
expect(filtered).toHaveLength(1);
|
|
136
|
+
if (filtered[0].kind === 'user') {
|
|
137
|
+
expect(filtered[0].text).toBe('Real user message');
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('buildCompactionSummary', () => {
|
|
143
|
+
it('produces structured sections from messages', () => {
|
|
144
|
+
const messages = [
|
|
145
|
+
{ role: 'user' as const, content: 'Implement JWT auth' },
|
|
146
|
+
{
|
|
147
|
+
role: 'assistant' as const,
|
|
148
|
+
content: [{ type: 'text' as const, text: 'I will implement JWT auth now' }],
|
|
149
|
+
},
|
|
150
|
+
];
|
|
151
|
+
const summary = buildCompactionSummary(messages);
|
|
152
|
+
|
|
153
|
+
expect(summary.sessionGoal).toBeDefined();
|
|
154
|
+
expect(summary.briefTranscript).toBeDefined();
|
|
155
|
+
expect(summary.outstandingContext).toBeDefined();
|
|
156
|
+
expect(summary.transcriptEntries).toBeDefined();
|
|
157
|
+
expect(summary.filesAndChanges).toBeDefined();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('extracts goals from user messages', () => {
|
|
161
|
+
const messages = [
|
|
162
|
+
{
|
|
163
|
+
role: 'user' as const,
|
|
164
|
+
content: 'Please implement JWT authentication with refresh tokens',
|
|
165
|
+
},
|
|
166
|
+
];
|
|
167
|
+
const summary = buildCompactionSummary(messages);
|
|
168
|
+
|
|
169
|
+
expect(summary.sessionGoal.length).toBeGreaterThan(0);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('captures tool errors in outstanding context', () => {
|
|
173
|
+
const messages = [
|
|
174
|
+
{ role: 'user' as const, content: 'Fix the build' },
|
|
175
|
+
{
|
|
176
|
+
role: 'assistant' as const,
|
|
177
|
+
content: [
|
|
178
|
+
{
|
|
179
|
+
type: 'toolCall' as const,
|
|
180
|
+
name: 'bash',
|
|
181
|
+
id: '1',
|
|
182
|
+
arguments: { command: 'npm run build' },
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
role: 'toolResult' as const,
|
|
188
|
+
toolName: 'bash',
|
|
189
|
+
isError: true,
|
|
190
|
+
content: 'error TS2304: Cannot find name "auth"',
|
|
191
|
+
},
|
|
192
|
+
];
|
|
193
|
+
const summary = buildCompactionSummary(messages);
|
|
194
|
+
|
|
195
|
+
// Should pick up the error in outstanding context
|
|
196
|
+
expect(summary.outstandingContext.length).toBeGreaterThan(0);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('produces a brief transcript', () => {
|
|
200
|
+
const messages = [
|
|
201
|
+
{ role: 'user' as const, content: 'Hello' },
|
|
202
|
+
{
|
|
203
|
+
role: 'assistant' as const,
|
|
204
|
+
content: [{ type: 'text' as const, text: 'Hi there' }],
|
|
205
|
+
},
|
|
206
|
+
];
|
|
207
|
+
const summary = buildCompactionSummary(messages);
|
|
208
|
+
|
|
209
|
+
expect(summary.briefTranscript).toContain('[user]');
|
|
210
|
+
expect(summary.briefTranscript).toContain('[assistant]');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('handles empty messages gracefully', () => {
|
|
214
|
+
const summary = buildCompactionSummary([]);
|
|
215
|
+
expect(summary.sessionGoal).toEqual([]);
|
|
216
|
+
expect(summary.briefTranscript).toBe('');
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe('formatForSupervisor', () => {
|
|
221
|
+
it('formats sections as text', () => {
|
|
222
|
+
const messages = [{ role: 'user' as const, content: 'Implement auth' }];
|
|
223
|
+
const summary = buildCompactionSummary(messages);
|
|
224
|
+
const text = formatForSupervisor(summary);
|
|
225
|
+
|
|
226
|
+
expect(text.length).toBeGreaterThan(0);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('includes relevant sections for steering decisions', () => {
|
|
230
|
+
const messages = [
|
|
231
|
+
{ role: 'user' as const, content: 'Implement JWT auth with tests' },
|
|
232
|
+
{
|
|
233
|
+
role: 'assistant' as const,
|
|
234
|
+
content: [{ type: 'text' as const, text: 'Working on it' }],
|
|
235
|
+
},
|
|
236
|
+
];
|
|
237
|
+
const summary = buildCompactionSummary(messages);
|
|
238
|
+
const text = formatForSupervisor(summary);
|
|
239
|
+
|
|
240
|
+
// Should contain at least some structured sections
|
|
241
|
+
expect(text).toMatch(/\[.+\]/);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('returns empty string when no data', () => {
|
|
245
|
+
const summary = buildCompactionSummary([]);
|
|
246
|
+
const text = formatForSupervisor(summary);
|
|
247
|
+
expect(text).toBe('');
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
});
|