pi-continuous-learning 0.3.0 → 0.5.0
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/README.md +38 -9
- package/dist/cli/analyze-logger.d.ts +63 -0
- package/dist/cli/analyze-logger.d.ts.map +1 -0
- package/dist/cli/analyze-logger.js +140 -0
- package/dist/cli/analyze-logger.js.map +1 -0
- package/dist/cli/analyze-prompt.d.ts.map +1 -1
- package/dist/cli/analyze-prompt.js +20 -0
- package/dist/cli/analyze-prompt.js.map +1 -1
- package/dist/cli/analyze.js +126 -32
- package/dist/cli/analyze.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -1
- package/dist/prompt-observer.d.ts.map +1 -1
- package/dist/prompt-observer.js +3 -0
- package/dist/prompt-observer.js.map +1 -1
- package/dist/session-observer.d.ts +42 -0
- package/dist/session-observer.d.ts.map +1 -0
- package/dist/session-observer.js +115 -0
- package/dist/session-observer.js.map +1 -0
- package/dist/types.d.ts +12 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/analyze-logger.ts +209 -0
- package/src/cli/analyze-prompt.ts +20 -0
- package/src/cli/analyze.ts +173 -32
- package/src/config.ts +1 -0
- package/src/index.ts +52 -0
- package/src/prompt-observer.ts +4 -0
- package/src/session-observer.ts +185 -0
- package/src/types.ts +17 -1
package/src/cli/analyze.ts
CHANGED
|
@@ -37,13 +37,14 @@ import {
|
|
|
37
37
|
import { readAgentsMd } from "../agents-md.js";
|
|
38
38
|
import { homedir } from "node:os";
|
|
39
39
|
import type { InstalledSkill } from "../types.js";
|
|
40
|
+
import { AnalyzeLogger, type ProjectRunStats, type RunSummary } from "./analyze-logger.js";
|
|
40
41
|
|
|
41
42
|
// ---------------------------------------------------------------------------
|
|
42
|
-
// Lockfile guard
|
|
43
|
+
// Lockfile guard - ensures only one instance runs at a time
|
|
43
44
|
// ---------------------------------------------------------------------------
|
|
44
45
|
|
|
45
46
|
const LOCKFILE_NAME = "analyze.lock";
|
|
46
|
-
const LOCK_STALE_MS = 10 * 60 * 1000; // 10 minutes
|
|
47
|
+
const LOCK_STALE_MS = 10 * 60 * 1000; // 10 minutes - stale lock threshold
|
|
47
48
|
|
|
48
49
|
function getLockfilePath(baseDir: string): string {
|
|
49
50
|
return join(baseDir, LOCKFILE_NAME);
|
|
@@ -64,12 +65,12 @@ function acquireLock(baseDir: string): boolean {
|
|
|
64
65
|
if (age < LOCK_STALE_MS) {
|
|
65
66
|
return false; // Process alive and lock is fresh
|
|
66
67
|
}
|
|
67
|
-
// Process alive but lock is stale
|
|
68
|
+
// Process alive but lock is stale - treat as abandoned
|
|
68
69
|
} catch {
|
|
69
|
-
// Process is dead
|
|
70
|
+
// Process is dead - lock is orphaned, safe to take over
|
|
70
71
|
}
|
|
71
72
|
} catch {
|
|
72
|
-
// Malformed lockfile
|
|
73
|
+
// Malformed lockfile - remove and proceed
|
|
73
74
|
}
|
|
74
75
|
}
|
|
75
76
|
|
|
@@ -86,7 +87,7 @@ function releaseLock(baseDir: string): void {
|
|
|
86
87
|
try {
|
|
87
88
|
if (existsSync(lockPath)) unlinkSync(lockPath);
|
|
88
89
|
} catch {
|
|
89
|
-
// Best effort
|
|
90
|
+
// Best effort - don't crash on cleanup
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
93
|
|
|
@@ -96,13 +97,79 @@ function releaseLock(baseDir: string): void {
|
|
|
96
97
|
|
|
97
98
|
const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes total
|
|
98
99
|
|
|
99
|
-
function startGlobalTimeout(timeoutMs: number): void {
|
|
100
|
+
function startGlobalTimeout(timeoutMs: number, logger: AnalyzeLogger): void {
|
|
100
101
|
setTimeout(() => {
|
|
101
|
-
|
|
102
|
+
logger.error("Global timeout reached, forcing exit");
|
|
102
103
|
process.exit(2);
|
|
103
104
|
}, timeoutMs).unref();
|
|
104
105
|
}
|
|
105
106
|
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Instinct operation tracking
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
interface InstinctOpCounts {
|
|
112
|
+
created: number;
|
|
113
|
+
updated: number;
|
|
114
|
+
deleted: number;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Wraps instinct tools to count create/update/delete operations.
|
|
119
|
+
* Returns new tool instances that increment the provided counts.
|
|
120
|
+
*/
|
|
121
|
+
function wrapInstinctToolsWithTracking(
|
|
122
|
+
projectId: string,
|
|
123
|
+
projectName: string,
|
|
124
|
+
baseDir: string,
|
|
125
|
+
counts: InstinctOpCounts
|
|
126
|
+
) {
|
|
127
|
+
const writeTool = createInstinctWriteTool(projectId, projectName, baseDir);
|
|
128
|
+
const deleteTool = createInstinctDeleteTool(projectId, baseDir);
|
|
129
|
+
|
|
130
|
+
const trackedWrite = {
|
|
131
|
+
...writeTool,
|
|
132
|
+
async execute(
|
|
133
|
+
toolCallId: string,
|
|
134
|
+
params: Parameters<typeof writeTool.execute>[1],
|
|
135
|
+
signal: AbortSignal | undefined,
|
|
136
|
+
onUpdate: unknown,
|
|
137
|
+
ctx: unknown
|
|
138
|
+
) {
|
|
139
|
+
const result = await writeTool.execute(toolCallId, params, signal, onUpdate, ctx);
|
|
140
|
+
const details = result.details as { action?: string } | undefined;
|
|
141
|
+
if (details?.action === "created") {
|
|
142
|
+
counts.created++;
|
|
143
|
+
} else {
|
|
144
|
+
counts.updated++;
|
|
145
|
+
}
|
|
146
|
+
return result;
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const trackedDelete = {
|
|
151
|
+
...deleteTool,
|
|
152
|
+
async execute(
|
|
153
|
+
toolCallId: string,
|
|
154
|
+
params: Parameters<typeof deleteTool.execute>[1],
|
|
155
|
+
signal: AbortSignal | undefined,
|
|
156
|
+
onUpdate: unknown,
|
|
157
|
+
ctx: unknown
|
|
158
|
+
) {
|
|
159
|
+
const result = await deleteTool.execute(toolCallId, params, signal, onUpdate, ctx);
|
|
160
|
+
counts.deleted++;
|
|
161
|
+
return result;
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
listTool: createInstinctListTool(projectId, baseDir),
|
|
167
|
+
readTool: createInstinctReadTool(projectId, baseDir),
|
|
168
|
+
writeTool: trackedWrite,
|
|
169
|
+
deleteTool: trackedDelete,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
106
173
|
// ---------------------------------------------------------------------------
|
|
107
174
|
// Per-project analysis
|
|
108
175
|
// ---------------------------------------------------------------------------
|
|
@@ -150,27 +217,39 @@ function hasNewObservations(projectId: string, meta: ProjectMeta, baseDir: strin
|
|
|
150
217
|
return true;
|
|
151
218
|
}
|
|
152
219
|
|
|
220
|
+
interface AnalyzeResult {
|
|
221
|
+
readonly ran: boolean;
|
|
222
|
+
readonly stats?: ProjectRunStats;
|
|
223
|
+
readonly skippedReason?: string;
|
|
224
|
+
}
|
|
225
|
+
|
|
153
226
|
async function analyzeProject(
|
|
154
227
|
project: ProjectEntry,
|
|
155
228
|
config: ReturnType<typeof loadConfig>,
|
|
156
|
-
baseDir: string
|
|
157
|
-
|
|
229
|
+
baseDir: string,
|
|
230
|
+
logger: AnalyzeLogger
|
|
231
|
+
): Promise<AnalyzeResult> {
|
|
158
232
|
const meta = loadProjectMeta(project.id, baseDir);
|
|
159
233
|
|
|
160
|
-
if (!hasNewObservations(project.id, meta, baseDir))
|
|
234
|
+
if (!hasNewObservations(project.id, meta, baseDir)) {
|
|
235
|
+
return { ran: false, skippedReason: "no new observations" };
|
|
236
|
+
}
|
|
161
237
|
|
|
162
238
|
const obsPath = getObservationsPath(project.id, baseDir);
|
|
163
239
|
const sinceLineCount = meta.last_observation_line_count ?? 0;
|
|
164
240
|
const { lines: newObsLines, totalLineCount } = tailObservationsSince(obsPath, sinceLineCount);
|
|
165
241
|
|
|
166
|
-
if (newObsLines.length === 0)
|
|
242
|
+
if (newObsLines.length === 0) {
|
|
243
|
+
return { ran: false, skippedReason: "no new observation lines" };
|
|
244
|
+
}
|
|
167
245
|
|
|
168
246
|
const obsCount = countObservations(project.id, baseDir);
|
|
169
|
-
if (obsCount < config.min_observations_to_analyze)
|
|
247
|
+
if (obsCount < config.min_observations_to_analyze) {
|
|
248
|
+
return { ran: false, skippedReason: `below threshold (${obsCount}/${config.min_observations_to_analyze})` };
|
|
249
|
+
}
|
|
170
250
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
);
|
|
251
|
+
const startTime = Date.now();
|
|
252
|
+
logger.projectStart(project.id, project.name, newObsLines.length, obsCount);
|
|
174
253
|
|
|
175
254
|
runDecayPass(project.id, baseDir);
|
|
176
255
|
|
|
@@ -188,7 +267,7 @@ async function analyzeProject(
|
|
|
188
267
|
description: s.description,
|
|
189
268
|
}));
|
|
190
269
|
} catch {
|
|
191
|
-
// Skills loading is best-effort
|
|
270
|
+
// Skills loading is best-effort - continue without them
|
|
192
271
|
}
|
|
193
272
|
|
|
194
273
|
const userPrompt = buildAnalyzerUserPrompt(obsPath, instinctsDir, project, {
|
|
@@ -203,11 +282,15 @@ async function analyzeProject(
|
|
|
203
282
|
const modelId = (config.model || DEFAULT_CONFIG.model) as Parameters<typeof getModel>[1];
|
|
204
283
|
const model = getModel("anthropic", modelId);
|
|
205
284
|
|
|
285
|
+
// Track instinct operations
|
|
286
|
+
const instinctCounts: InstinctOpCounts = { created: 0, updated: 0, deleted: 0 };
|
|
287
|
+
const trackedTools = wrapInstinctToolsWithTracking(project.id, project.name, baseDir, instinctCounts);
|
|
288
|
+
|
|
206
289
|
const customTools = [
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
290
|
+
trackedTools.listTool,
|
|
291
|
+
trackedTools.readTool,
|
|
292
|
+
trackedTools.writeTool,
|
|
293
|
+
trackedTools.deleteTool,
|
|
211
294
|
];
|
|
212
295
|
|
|
213
296
|
const loader = new DefaultResourceLoader({
|
|
@@ -230,13 +313,37 @@ async function analyzeProject(
|
|
|
230
313
|
session.dispose();
|
|
231
314
|
}
|
|
232
315
|
|
|
316
|
+
// Collect stats after session completes
|
|
317
|
+
const sessionStats = session.getSessionStats();
|
|
318
|
+
const durationMs = Date.now() - startTime;
|
|
319
|
+
|
|
320
|
+
const stats: ProjectRunStats = {
|
|
321
|
+
project_id: project.id,
|
|
322
|
+
project_name: project.name,
|
|
323
|
+
duration_ms: durationMs,
|
|
324
|
+
observations_processed: newObsLines.length,
|
|
325
|
+
observations_total: obsCount,
|
|
326
|
+
instincts_created: instinctCounts.created,
|
|
327
|
+
instincts_updated: instinctCounts.updated,
|
|
328
|
+
instincts_deleted: instinctCounts.deleted,
|
|
329
|
+
tokens_input: sessionStats.tokens.input,
|
|
330
|
+
tokens_output: sessionStats.tokens.output,
|
|
331
|
+
tokens_cache_read: sessionStats.tokens.cacheRead,
|
|
332
|
+
tokens_cache_write: sessionStats.tokens.cacheWrite,
|
|
333
|
+
tokens_total: sessionStats.tokens.total,
|
|
334
|
+
cost_usd: sessionStats.cost,
|
|
335
|
+
model: modelId,
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
logger.projectComplete(stats);
|
|
339
|
+
|
|
233
340
|
saveProjectMeta(
|
|
234
341
|
project.id,
|
|
235
342
|
{ ...meta, last_analyzed_at: new Date().toISOString(), last_observation_line_count: totalLineCount },
|
|
236
343
|
baseDir
|
|
237
344
|
);
|
|
238
|
-
|
|
239
|
-
return true;
|
|
345
|
+
|
|
346
|
+
return { ran: true, stats };
|
|
240
347
|
}
|
|
241
348
|
|
|
242
349
|
// ---------------------------------------------------------------------------
|
|
@@ -245,35 +352,67 @@ async function analyzeProject(
|
|
|
245
352
|
|
|
246
353
|
async function main(): Promise<void> {
|
|
247
354
|
const baseDir = getBaseDir();
|
|
355
|
+
const config = loadConfig();
|
|
356
|
+
const logger = new AnalyzeLogger(config.log_path);
|
|
248
357
|
|
|
249
358
|
if (!acquireLock(baseDir)) {
|
|
250
|
-
|
|
359
|
+
logger.info("Another instance is already running, exiting");
|
|
251
360
|
process.exit(0);
|
|
252
361
|
}
|
|
253
362
|
|
|
254
|
-
startGlobalTimeout(DEFAULT_TIMEOUT_MS);
|
|
363
|
+
startGlobalTimeout(DEFAULT_TIMEOUT_MS, logger);
|
|
364
|
+
|
|
365
|
+
const runStart = Date.now();
|
|
255
366
|
|
|
256
367
|
try {
|
|
257
|
-
const config = loadConfig();
|
|
258
368
|
const registry = loadProjectsRegistry(baseDir);
|
|
259
369
|
const projects = Object.values(registry);
|
|
260
370
|
|
|
261
371
|
if (projects.length === 0) {
|
|
262
|
-
|
|
372
|
+
logger.info("No projects registered");
|
|
263
373
|
return;
|
|
264
374
|
}
|
|
265
375
|
|
|
376
|
+
logger.runStart(projects.length);
|
|
377
|
+
|
|
266
378
|
let processed = 0;
|
|
379
|
+
let skipped = 0;
|
|
380
|
+
let errored = 0;
|
|
381
|
+
const allProjectStats: ProjectRunStats[] = [];
|
|
382
|
+
|
|
267
383
|
for (const project of projects) {
|
|
268
384
|
try {
|
|
269
|
-
const
|
|
270
|
-
if (
|
|
385
|
+
const result = await analyzeProject(project, config, baseDir, logger);
|
|
386
|
+
if (result.ran && result.stats) {
|
|
387
|
+
processed++;
|
|
388
|
+
allProjectStats.push(result.stats);
|
|
389
|
+
} else {
|
|
390
|
+
skipped++;
|
|
391
|
+
if (result.skippedReason) {
|
|
392
|
+
logger.projectSkipped(project.id, project.name, result.skippedReason);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
271
395
|
} catch (err) {
|
|
272
|
-
|
|
396
|
+
errored++;
|
|
397
|
+
logger.projectError(project.id, project.name, err);
|
|
273
398
|
}
|
|
274
399
|
}
|
|
275
400
|
|
|
276
|
-
|
|
401
|
+
const summary: RunSummary = {
|
|
402
|
+
total_duration_ms: Date.now() - runStart,
|
|
403
|
+
projects_processed: processed,
|
|
404
|
+
projects_skipped: skipped,
|
|
405
|
+
projects_errored: errored,
|
|
406
|
+
projects_total: projects.length,
|
|
407
|
+
total_tokens: allProjectStats.reduce((sum, s) => sum + s.tokens_total, 0),
|
|
408
|
+
total_cost_usd: allProjectStats.reduce((sum, s) => sum + s.cost_usd, 0),
|
|
409
|
+
total_instincts_created: allProjectStats.reduce((sum, s) => sum + s.instincts_created, 0),
|
|
410
|
+
total_instincts_updated: allProjectStats.reduce((sum, s) => sum + s.instincts_updated, 0),
|
|
411
|
+
total_instincts_deleted: allProjectStats.reduce((sum, s) => sum + s.instincts_deleted, 0),
|
|
412
|
+
project_stats: allProjectStats,
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
logger.runComplete(summary);
|
|
277
416
|
} finally {
|
|
278
417
|
releaseLock(baseDir);
|
|
279
418
|
}
|
|
@@ -281,6 +420,8 @@ async function main(): Promise<void> {
|
|
|
281
420
|
|
|
282
421
|
main().catch((err) => {
|
|
283
422
|
releaseLock(getBaseDir());
|
|
284
|
-
|
|
423
|
+
// Last-resort logging - config may not have loaded
|
|
424
|
+
const logger = new AnalyzeLogger();
|
|
425
|
+
logger.error("Fatal error", err);
|
|
285
426
|
process.exit(1);
|
|
286
427
|
});
|
package/src/config.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -15,6 +15,13 @@ import { ensureStorageLayout } from "./storage.js";
|
|
|
15
15
|
import { cleanOldArchives } from "./observations.js";
|
|
16
16
|
import { handleToolStart, handleToolEnd } from "./tool-observer.js";
|
|
17
17
|
import { handleBeforeAgentStart, handleAgentEnd } from "./prompt-observer.js";
|
|
18
|
+
import {
|
|
19
|
+
handleTurnStart,
|
|
20
|
+
handleTurnEnd,
|
|
21
|
+
handleUserBash,
|
|
22
|
+
handleSessionCompact,
|
|
23
|
+
handleModelSelect,
|
|
24
|
+
} from "./session-observer.js";
|
|
18
25
|
import {
|
|
19
26
|
handleBeforeAgentStartInjection,
|
|
20
27
|
handleAgentEndClearInstincts,
|
|
@@ -98,6 +105,51 @@ export default function (pi: ExtensionAPI): void {
|
|
|
98
105
|
}
|
|
99
106
|
});
|
|
100
107
|
|
|
108
|
+
pi.on("turn_start", (event, ctx) => {
|
|
109
|
+
try {
|
|
110
|
+
if (!project) return;
|
|
111
|
+
handleTurnStart(event, ctx, project);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
logError(project?.id ?? null, "turn_start", err);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
pi.on("turn_end", (event, ctx) => {
|
|
118
|
+
try {
|
|
119
|
+
if (!project) return;
|
|
120
|
+
handleTurnEnd(event, ctx, project);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
logError(project?.id ?? null, "turn_end", err);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
pi.on("user_bash", (event, ctx) => {
|
|
127
|
+
try {
|
|
128
|
+
if (!project) return;
|
|
129
|
+
handleUserBash(event, ctx, project);
|
|
130
|
+
} catch (err) {
|
|
131
|
+
logError(project?.id ?? null, "user_bash", err);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
pi.on("session_compact", (event, ctx) => {
|
|
136
|
+
try {
|
|
137
|
+
if (!project) return;
|
|
138
|
+
handleSessionCompact(event, ctx, project);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
logError(project?.id ?? null, "session_compact", err);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
pi.on("model_select", (event, ctx) => {
|
|
145
|
+
try {
|
|
146
|
+
if (!project) return;
|
|
147
|
+
handleModelSelect(event, ctx, project);
|
|
148
|
+
} catch (err) {
|
|
149
|
+
logError(project?.id ?? null, "model_select", err);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
101
153
|
pi.registerCommand(STATUS_CMD, {
|
|
102
154
|
description: "Show all instincts grouped by domain with confidence scores",
|
|
103
155
|
handler: (args: string, ctx: ExtensionCommandContext) =>
|
package/src/prompt-observer.ts
CHANGED
|
@@ -76,12 +76,16 @@ export function handleAgentEnd(
|
|
|
76
76
|
try {
|
|
77
77
|
if (shouldSkipObservation()) return;
|
|
78
78
|
|
|
79
|
+
const contextUsage = ctx.getContextUsage();
|
|
80
|
+
const tokensUsed = contextUsage?.tokens ?? undefined;
|
|
81
|
+
|
|
79
82
|
const observation: Observation = {
|
|
80
83
|
timestamp: new Date().toISOString(),
|
|
81
84
|
event: "agent_end",
|
|
82
85
|
session: getSessionId(ctx),
|
|
83
86
|
project_id: project.id,
|
|
84
87
|
project_name: project.name,
|
|
88
|
+
...(tokensUsed != null ? { tokens_used: tokensUsed } : {}),
|
|
85
89
|
...buildActiveInstincts(),
|
|
86
90
|
};
|
|
87
91
|
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
import { getCurrentActiveInstincts } from "./active-instincts.js";
|
|
4
|
+
import { appendObservation } from "./observations.js";
|
|
5
|
+
import { scrubSecrets } from "./scrubber.js";
|
|
6
|
+
import { logError } from "./error-logger.js";
|
|
7
|
+
import type { Observation, ProjectEntry } from "./types.js";
|
|
8
|
+
|
|
9
|
+
export interface TurnStartEvent {
|
|
10
|
+
type: "turn_start";
|
|
11
|
+
turnIndex: number;
|
|
12
|
+
timestamp: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface TurnEndEvent {
|
|
16
|
+
type: "turn_end";
|
|
17
|
+
turnIndex: number;
|
|
18
|
+
message: unknown;
|
|
19
|
+
toolResults: unknown[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface UserBashEvent {
|
|
23
|
+
type: "user_bash";
|
|
24
|
+
command: string;
|
|
25
|
+
excludeFromContext: boolean;
|
|
26
|
+
cwd: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SessionCompactEvent {
|
|
30
|
+
type: "session_compact";
|
|
31
|
+
compactionEntry: unknown;
|
|
32
|
+
fromExtension: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ModelSelectEvent {
|
|
36
|
+
type: "model_select";
|
|
37
|
+
model: { id?: string; name?: string };
|
|
38
|
+
previousModel: { id?: string; name?: string } | undefined;
|
|
39
|
+
source: "set" | "cycle" | "restore";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getSessionId(ctx: ExtensionContext): string {
|
|
43
|
+
return ctx.sessionManager.getSessionId();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function buildActiveInstincts(): Pick<Observation, "active_instincts"> {
|
|
47
|
+
const ids = getCurrentActiveInstincts();
|
|
48
|
+
return ids.length > 0 ? { active_instincts: ids } : {};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function handleTurnStart(
|
|
52
|
+
event: TurnStartEvent,
|
|
53
|
+
ctx: ExtensionContext,
|
|
54
|
+
project: ProjectEntry,
|
|
55
|
+
baseDir?: string
|
|
56
|
+
): void {
|
|
57
|
+
try {
|
|
58
|
+
const observation: Observation = {
|
|
59
|
+
timestamp: new Date().toISOString(),
|
|
60
|
+
event: "turn_start",
|
|
61
|
+
session: getSessionId(ctx),
|
|
62
|
+
project_id: project.id,
|
|
63
|
+
project_name: project.name,
|
|
64
|
+
turn_index: event.turnIndex,
|
|
65
|
+
...buildActiveInstincts(),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
appendObservation(observation, project.id, baseDir);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
logError(project.id, "session-observer:handleTurnStart", err, baseDir);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function handleTurnEnd(
|
|
75
|
+
event: TurnEndEvent,
|
|
76
|
+
ctx: ExtensionContext,
|
|
77
|
+
project: ProjectEntry,
|
|
78
|
+
baseDir?: string
|
|
79
|
+
): void {
|
|
80
|
+
try {
|
|
81
|
+
const toolCount = event.toolResults?.length ?? 0;
|
|
82
|
+
const errorCount = Array.isArray(event.toolResults)
|
|
83
|
+
? event.toolResults.filter((r: unknown) => {
|
|
84
|
+
if (r && typeof r === "object" && "isError" in r) {
|
|
85
|
+
return (r as { isError: boolean }).isError;
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}).length
|
|
89
|
+
: 0;
|
|
90
|
+
|
|
91
|
+
const contextUsage = ctx.getContextUsage();
|
|
92
|
+
const tokensUsed = contextUsage?.tokens ?? undefined;
|
|
93
|
+
|
|
94
|
+
const observation: Observation = {
|
|
95
|
+
timestamp: new Date().toISOString(),
|
|
96
|
+
event: "turn_end",
|
|
97
|
+
session: getSessionId(ctx),
|
|
98
|
+
project_id: project.id,
|
|
99
|
+
project_name: project.name,
|
|
100
|
+
turn_index: event.turnIndex,
|
|
101
|
+
tool_count: toolCount,
|
|
102
|
+
error_count: errorCount,
|
|
103
|
+
...(tokensUsed != null ? { tokens_used: tokensUsed } : {}),
|
|
104
|
+
...buildActiveInstincts(),
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
appendObservation(observation, project.id, baseDir);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
logError(project.id, "session-observer:handleTurnEnd", err, baseDir);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function handleUserBash(
|
|
114
|
+
event: UserBashEvent,
|
|
115
|
+
ctx: ExtensionContext,
|
|
116
|
+
project: ProjectEntry,
|
|
117
|
+
baseDir?: string
|
|
118
|
+
): void {
|
|
119
|
+
try {
|
|
120
|
+
const observation: Observation = {
|
|
121
|
+
timestamp: new Date().toISOString(),
|
|
122
|
+
event: "user_bash",
|
|
123
|
+
session: getSessionId(ctx),
|
|
124
|
+
project_id: project.id,
|
|
125
|
+
project_name: project.name,
|
|
126
|
+
command: scrubSecrets(event.command),
|
|
127
|
+
cwd: event.cwd,
|
|
128
|
+
...buildActiveInstincts(),
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
appendObservation(observation, project.id, baseDir);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
logError(project.id, "session-observer:handleUserBash", err, baseDir);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function handleSessionCompact(
|
|
138
|
+
event: SessionCompactEvent,
|
|
139
|
+
ctx: ExtensionContext,
|
|
140
|
+
project: ProjectEntry,
|
|
141
|
+
baseDir?: string
|
|
142
|
+
): void {
|
|
143
|
+
try {
|
|
144
|
+
const observation: Observation = {
|
|
145
|
+
timestamp: new Date().toISOString(),
|
|
146
|
+
event: "session_compact",
|
|
147
|
+
session: getSessionId(ctx),
|
|
148
|
+
project_id: project.id,
|
|
149
|
+
project_name: project.name,
|
|
150
|
+
from_extension: event.fromExtension,
|
|
151
|
+
...buildActiveInstincts(),
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
appendObservation(observation, project.id, baseDir);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
logError(project.id, "session-observer:handleSessionCompact", err, baseDir);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function handleModelSelect(
|
|
161
|
+
event: ModelSelectEvent,
|
|
162
|
+
ctx: ExtensionContext,
|
|
163
|
+
project: ProjectEntry,
|
|
164
|
+
baseDir?: string
|
|
165
|
+
): void {
|
|
166
|
+
try {
|
|
167
|
+
const modelName = event.model?.id ?? event.model?.name ?? "unknown";
|
|
168
|
+
const previousModelName = event.previousModel?.id ?? event.previousModel?.name;
|
|
169
|
+
|
|
170
|
+
const observation: Observation = {
|
|
171
|
+
timestamp: new Date().toISOString(),
|
|
172
|
+
event: "model_select",
|
|
173
|
+
session: getSessionId(ctx),
|
|
174
|
+
project_id: project.id,
|
|
175
|
+
project_name: project.name,
|
|
176
|
+
model: modelName,
|
|
177
|
+
...(previousModelName ? { previous_model: previousModelName } : {}),
|
|
178
|
+
model_change_source: event.source,
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
appendObservation(observation, project.id, baseDir);
|
|
182
|
+
} catch (err) {
|
|
183
|
+
logError(project.id, "session-observer:handleModelSelect", err, baseDir);
|
|
184
|
+
}
|
|
185
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -11,7 +11,12 @@ export type ObservationEvent =
|
|
|
11
11
|
| "tool_start"
|
|
12
12
|
| "tool_complete"
|
|
13
13
|
| "user_prompt"
|
|
14
|
-
| "agent_end"
|
|
14
|
+
| "agent_end"
|
|
15
|
+
| "turn_start"
|
|
16
|
+
| "turn_end"
|
|
17
|
+
| "user_bash"
|
|
18
|
+
| "session_compact"
|
|
19
|
+
| "model_select";
|
|
15
20
|
|
|
16
21
|
export interface Observation {
|
|
17
22
|
timestamp: string; // ISO 8601 UTC
|
|
@@ -24,6 +29,16 @@ export interface Observation {
|
|
|
24
29
|
output?: string;
|
|
25
30
|
is_error?: boolean;
|
|
26
31
|
active_instincts?: string[];
|
|
32
|
+
turn_index?: number;
|
|
33
|
+
tool_count?: number;
|
|
34
|
+
error_count?: number;
|
|
35
|
+
tokens_used?: number;
|
|
36
|
+
command?: string;
|
|
37
|
+
cwd?: string;
|
|
38
|
+
from_extension?: boolean;
|
|
39
|
+
model?: string;
|
|
40
|
+
previous_model?: string;
|
|
41
|
+
model_change_source?: string;
|
|
27
42
|
}
|
|
28
43
|
|
|
29
44
|
// ---------------------------------------------------------------------------
|
|
@@ -87,4 +102,5 @@ export interface Config {
|
|
|
87
102
|
active_hours_start: number; // 0-23
|
|
88
103
|
active_hours_end: number; // 0-23
|
|
89
104
|
max_idle_seconds: number;
|
|
105
|
+
log_path?: string; // Override analyzer log location (default: ~/.pi/continuous-learning/analyzer.log)
|
|
90
106
|
}
|