@agentuity/opencode 1.0.14 → 1.0.16
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/dist/agents/expert-backend.js +1 -1
- package/dist/agents/expert-backend.js.map +1 -1
- package/dist/agents/expert-frontend.js +1 -1
- package/dist/agents/expert-frontend.js.map +1 -1
- package/dist/agents/expert-ops.js +1 -1
- package/dist/agents/expert-ops.js.map +1 -1
- package/dist/agents/expert.js +1 -1
- package/dist/agents/expert.js.map +1 -1
- package/dist/agents/lead.d.ts +1 -1
- package/dist/agents/lead.d.ts.map +1 -1
- package/dist/agents/lead.js +34 -7
- package/dist/agents/lead.js.map +1 -1
- package/dist/agents/monitor.d.ts +1 -1
- package/dist/agents/monitor.d.ts.map +1 -1
- package/dist/agents/monitor.js +22 -33
- package/dist/agents/monitor.js.map +1 -1
- package/dist/agents/reviewer.js +1 -1
- package/dist/agents/reviewer.js.map +1 -1
- package/dist/agents/scout.js +2 -2
- package/dist/agents/scout.js.map +1 -1
- package/dist/background/manager.d.ts +27 -0
- package/dist/background/manager.d.ts.map +1 -1
- package/dist/background/manager.js +161 -27
- package/dist/background/manager.js.map +1 -1
- package/dist/plugin/hooks/cadence.d.ts +3 -1
- package/dist/plugin/hooks/cadence.d.ts.map +1 -1
- package/dist/plugin/hooks/cadence.js +167 -66
- package/dist/plugin/hooks/cadence.js.map +1 -1
- package/dist/plugin/hooks/compaction-utils.d.ts +48 -0
- package/dist/plugin/hooks/compaction-utils.d.ts.map +1 -0
- package/dist/plugin/hooks/compaction-utils.js +259 -0
- package/dist/plugin/hooks/compaction-utils.js.map +1 -0
- package/dist/plugin/hooks/params.d.ts +1 -1
- package/dist/plugin/hooks/params.d.ts.map +1 -1
- package/dist/plugin/hooks/params.js +5 -1
- package/dist/plugin/hooks/params.js.map +1 -1
- package/dist/plugin/hooks/session-memory.d.ts +2 -1
- package/dist/plugin/hooks/session-memory.d.ts.map +1 -1
- package/dist/plugin/hooks/session-memory.js +97 -48
- package/dist/plugin/hooks/session-memory.js.map +1 -1
- package/dist/plugin/plugin.d.ts.map +1 -1
- package/dist/plugin/plugin.js +31 -9
- package/dist/plugin/plugin.js.map +1 -1
- package/dist/sqlite/index.d.ts +1 -1
- package/dist/sqlite/index.d.ts.map +1 -1
- package/dist/sqlite/queries.d.ts +1 -0
- package/dist/sqlite/queries.d.ts.map +1 -1
- package/dist/sqlite/queries.js +4 -0
- package/dist/sqlite/queries.js.map +1 -1
- package/dist/sqlite/reader.d.ts +11 -1
- package/dist/sqlite/reader.d.ts.map +1 -1
- package/dist/sqlite/reader.js +62 -0
- package/dist/sqlite/reader.js.map +1 -1
- package/dist/sqlite/types.d.ts +40 -0
- package/dist/sqlite/types.d.ts.map +1 -1
- package/dist/tools/background.d.ts +2 -0
- package/dist/tools/background.d.ts.map +1 -1
- package/dist/tools/background.js +2 -0
- package/dist/tools/background.js.map +1 -1
- package/dist/types.d.ts +36 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -1
- package/package.json +3 -3
- package/src/agents/expert-backend.ts +1 -1
- package/src/agents/expert-frontend.ts +1 -1
- package/src/agents/expert-ops.ts +1 -1
- package/src/agents/expert.ts +1 -1
- package/src/agents/lead.ts +34 -7
- package/src/agents/monitor.ts +22 -33
- package/src/agents/reviewer.ts +1 -1
- package/src/agents/scout.ts +2 -2
- package/src/background/manager.ts +167 -32
- package/src/plugin/hooks/cadence.ts +184 -66
- package/src/plugin/hooks/compaction-utils.ts +291 -0
- package/src/plugin/hooks/params.ts +10 -1
- package/src/plugin/hooks/session-memory.ts +109 -47
- package/src/plugin/plugin.ts +47 -10
- package/src/sqlite/index.ts +4 -0
- package/src/sqlite/queries.ts +5 -0
- package/src/sqlite/reader.ts +69 -0
- package/src/sqlite/types.ts +40 -0
- package/src/tools/background.ts +6 -0
- package/src/types.ts +30 -0
|
@@ -2,6 +2,19 @@ import type { PluginInput } from '@opencode-ai/plugin';
|
|
|
2
2
|
import type { CoderConfig } from '../../types';
|
|
3
3
|
import type { BackgroundManager } from '../../background';
|
|
4
4
|
import type { OpenCodeDBReader, SessionTreeNode } from '../../sqlite';
|
|
5
|
+
import type { CompactionStats } from '../../sqlite/types';
|
|
6
|
+
import {
|
|
7
|
+
getCurrentBranch,
|
|
8
|
+
buildCustomCompactionPrompt,
|
|
9
|
+
fetchAndFormatPlanningState,
|
|
10
|
+
getImageDescriptions,
|
|
11
|
+
getRecentToolCallSummaries,
|
|
12
|
+
storePreCompactionSnapshot,
|
|
13
|
+
persistCadenceStateToKV,
|
|
14
|
+
restoreCadenceStateFromKV,
|
|
15
|
+
formatCompactionDiagnostics,
|
|
16
|
+
countListItems,
|
|
17
|
+
} from './compaction-utils';
|
|
5
18
|
|
|
6
19
|
/** Compacting hook input/output types */
|
|
7
20
|
type CompactingInput = { sessionID: string };
|
|
@@ -13,27 +26,12 @@ export interface CadenceHooks {
|
|
|
13
26
|
onCompacting: (input: CompactingInput, output: CompactingOutput) => Promise<void>;
|
|
14
27
|
/** Check if a session is currently in Cadence mode */
|
|
15
28
|
isActiveCadenceSession: (sessionId: string) => boolean;
|
|
29
|
+
/** Lazy restore: check KV for persisted Cadence state and populate in-memory Map */
|
|
30
|
+
tryRestoreFromKV: (sessionId: string) => Promise<boolean>;
|
|
16
31
|
}
|
|
17
32
|
|
|
18
33
|
const COMPLETION_PATTERN = /<promise>\s*DONE\s*<\/promise>/i;
|
|
19
34
|
|
|
20
|
-
/**
|
|
21
|
-
* Get the current git branch name.
|
|
22
|
-
*/
|
|
23
|
-
async function getCurrentBranch(): Promise<string> {
|
|
24
|
-
try {
|
|
25
|
-
const proc = Bun.spawn(['git', 'branch', '--show-current'], {
|
|
26
|
-
stdout: 'pipe',
|
|
27
|
-
stderr: 'pipe',
|
|
28
|
-
});
|
|
29
|
-
const stdout = await new Response(proc.stdout).text();
|
|
30
|
-
await proc.exited;
|
|
31
|
-
return stdout.trim() || 'unknown';
|
|
32
|
-
} catch {
|
|
33
|
-
return 'unknown';
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
35
|
// Ultrawork trigger keywords - case insensitive matching
|
|
38
36
|
const ULTRAWORK_TRIGGERS = [
|
|
39
37
|
'ultrawork',
|
|
@@ -69,11 +67,14 @@ interface CadenceSessionState {
|
|
|
69
67
|
*/
|
|
70
68
|
export function createCadenceHooks(
|
|
71
69
|
ctx: PluginInput,
|
|
72
|
-
|
|
70
|
+
config: CoderConfig,
|
|
73
71
|
backgroundManager?: BackgroundManager,
|
|
74
|
-
dbReader?: OpenCodeDBReader
|
|
72
|
+
dbReader?: OpenCodeDBReader,
|
|
73
|
+
lastUserMessages?: Map<string, string>
|
|
75
74
|
): CadenceHooks {
|
|
76
75
|
const activeCadenceSessions = new Map<string, CadenceSessionState>();
|
|
76
|
+
const nonCadenceSessions = new Set<string>();
|
|
77
|
+
const NON_CADENCE_CACHE_MAX = 500;
|
|
77
78
|
|
|
78
79
|
const log = (msg: string) => {
|
|
79
80
|
ctx.client.app.log({
|
|
@@ -90,11 +91,14 @@ export function createCadenceHooks(
|
|
|
90
91
|
const sessionId = extractSessionId(input);
|
|
91
92
|
if (!sessionId) return;
|
|
92
93
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
//
|
|
97
|
-
|
|
94
|
+
// Use the USER's message (from chat.params) for trigger detection,
|
|
95
|
+
// not the model's output — avoids false positives when the model
|
|
96
|
+
// uses phrases like "go deep" or "be thorough" in its response.
|
|
97
|
+
// Delete after read — entries are transient (set in chat.params,
|
|
98
|
+
// consumed here in chat.message) so no unbounded Map growth.
|
|
99
|
+
const userText = lastUserMessages?.get(sessionId) ?? '';
|
|
100
|
+
lastUserMessages?.delete(sessionId);
|
|
101
|
+
const cadenceType = getCadenceTriggerType(userText);
|
|
98
102
|
if (cadenceType && !activeCadenceSessions.has(sessionId)) {
|
|
99
103
|
log(`Cadence started for session ${sessionId} via ${cadenceType}`);
|
|
100
104
|
const now = new Date().toISOString();
|
|
@@ -105,6 +109,8 @@ export function createCadenceHooks(
|
|
|
105
109
|
lastActivity: now,
|
|
106
110
|
};
|
|
107
111
|
activeCadenceSessions.set(sessionId, state);
|
|
112
|
+
nonCadenceSessions.delete(sessionId);
|
|
113
|
+
persistCadenceStateToKV(sessionId, { ...state }).catch(() => {});
|
|
108
114
|
|
|
109
115
|
// If triggered by ultrawork keywords, inject [CADENCE MODE] tag
|
|
110
116
|
if (cadenceType === 'ultrawork') {
|
|
@@ -115,6 +121,12 @@ export function createCadenceHooks(
|
|
|
115
121
|
return;
|
|
116
122
|
}
|
|
117
123
|
|
|
124
|
+
// Everything below parses the MODEL's output for structured tags
|
|
125
|
+
// (CADENCE_STATUS, iteration counts, completion signals) that the
|
|
126
|
+
// model intentionally emits — these are NOT false-positive-prone.
|
|
127
|
+
const messageText = extractMessageText(output);
|
|
128
|
+
if (!messageText) return;
|
|
129
|
+
|
|
118
130
|
// Check if this session is in Cadence mode
|
|
119
131
|
const state = activeCadenceSessions.get(sessionId);
|
|
120
132
|
if (!state) {
|
|
@@ -146,6 +158,7 @@ export function createCadenceHooks(
|
|
|
146
158
|
if (changed) {
|
|
147
159
|
const loopInfo = state.loopId ? ` · ${state.loopId}` : '';
|
|
148
160
|
showToast(ctx, `⚡ Cadence · ${state.iteration}/${state.maxIterations}${loopInfo}`);
|
|
161
|
+
persistCadenceStateToKV(sessionId, { ...state }).catch(() => {});
|
|
149
162
|
}
|
|
150
163
|
return;
|
|
151
164
|
}
|
|
@@ -158,6 +171,7 @@ export function createCadenceHooks(
|
|
|
158
171
|
state.iteration = newIteration;
|
|
159
172
|
const loopInfo = state.loopId ? ` · ${state.loopId}` : '';
|
|
160
173
|
showToast(ctx, `⚡ Cadence · ${state.iteration}/${state.maxIterations}${loopInfo}`);
|
|
174
|
+
persistCadenceStateToKV(sessionId, { ...state }).catch(() => {});
|
|
161
175
|
}
|
|
162
176
|
}
|
|
163
177
|
|
|
@@ -285,6 +299,8 @@ Continue Cadence iteration ${state.iteration} of ${state.maxIterations}
|
|
|
285
299
|
/**
|
|
286
300
|
* Called during context compaction to inject Cadence state.
|
|
287
301
|
* This ensures the compaction summary includes critical loop state.
|
|
302
|
+
* Uses output.prompt to REPLACE the default compaction prompt with
|
|
303
|
+
* enriched context (planning state, images, tool calls, diagnostics).
|
|
288
304
|
*/
|
|
289
305
|
async onCompacting(input: CompactingInput, output: CompactingOutput): Promise<void> {
|
|
290
306
|
const sessionId = input.sessionID;
|
|
@@ -298,12 +314,53 @@ Continue Cadence iteration ${state.iteration} of ${state.maxIterations}
|
|
|
298
314
|
log(`Injecting Cadence context during compaction for session ${sessionId}`);
|
|
299
315
|
showToast(ctx, '💾 Compacting Cadence context...');
|
|
300
316
|
|
|
301
|
-
//
|
|
302
|
-
const
|
|
317
|
+
// Config flags for compaction behavior
|
|
318
|
+
const compactionCfg = config?.compaction ?? {};
|
|
319
|
+
const useCustomPrompt = compactionCfg.customPrompt !== false;
|
|
320
|
+
const useInlinePlanning = compactionCfg.inlinePlanning !== false;
|
|
321
|
+
const useImageAwareness = compactionCfg.imageAwareness !== false;
|
|
322
|
+
const useSnapshotToKV = compactionCfg.snapshotToKV !== false;
|
|
323
|
+
const maxTokens = compactionCfg.maxContextTokens ?? 4000;
|
|
324
|
+
|
|
325
|
+
// 1. Build custom compaction instructions
|
|
326
|
+
const instructions = useCustomPrompt ? buildCustomCompactionPrompt('cadence') : null;
|
|
327
|
+
|
|
328
|
+
// 2. Gather enrichment data in parallel
|
|
329
|
+
const toolCallLimit = config?.compaction?.toolCallSummaryLimit ?? 5;
|
|
330
|
+
const [branch, planningState, imageDescs, toolSummaries] = await Promise.all([
|
|
331
|
+
getCurrentBranch(),
|
|
332
|
+
useInlinePlanning ? fetchAndFormatPlanningState(sessionId) : Promise.resolve(null),
|
|
333
|
+
useImageAwareness
|
|
334
|
+
? Promise.resolve(getImageDescriptions(dbReader ?? null, sessionId))
|
|
335
|
+
: Promise.resolve(null),
|
|
336
|
+
Promise.resolve(getRecentToolCallSummaries(dbReader ?? null, sessionId, toolCallLimit)),
|
|
337
|
+
]);
|
|
338
|
+
|
|
339
|
+
// 3. Build Cadence state section
|
|
340
|
+
const cadenceStateSection = `## CADENCE MODE ACTIVE
|
|
341
|
+
|
|
342
|
+
This session is running in Cadence mode (long-running autonomous loop).
|
|
303
343
|
|
|
304
|
-
|
|
344
|
+
**Cadence State:**
|
|
345
|
+
- Session ID: ${sessionId}
|
|
346
|
+
- Loop ID: ${state.loopId ?? 'unknown'}
|
|
347
|
+
- Branch: ${branch}
|
|
348
|
+
- Started: ${state.startedAt}
|
|
349
|
+
- Iteration: ${state.iteration} / ${state.maxIterations}
|
|
350
|
+
- Last activity: ${state.lastActivity}
|
|
351
|
+
|
|
352
|
+
**Session Record Location:**
|
|
353
|
+
\`session:${sessionId}\` in agentuity-opencode-memory
|
|
354
|
+
|
|
355
|
+
After compaction:
|
|
356
|
+
1. Memory will save this summary and update the session record
|
|
357
|
+
2. Memory should update planning.progress with this compaction
|
|
358
|
+
3. Lead will continue the loop from iteration ${state.iteration}
|
|
359
|
+
4. Use 5-Question Reboot to re-orient: Where am I? Where going? Goal? Learned? Done?`;
|
|
360
|
+
|
|
361
|
+
// 4. Build background tasks section
|
|
305
362
|
const tasks = backgroundManager?.getTasksByParent(sessionId) ?? [];
|
|
306
|
-
let
|
|
363
|
+
let backgroundSection: string | null = null;
|
|
307
364
|
|
|
308
365
|
if (tasks.length > 0) {
|
|
309
366
|
const taskList = tasks
|
|
@@ -313,9 +370,7 @@ Continue Cadence iteration ${state.iteration} of ${state.maxIterations}
|
|
|
313
370
|
)
|
|
314
371
|
.join('\n');
|
|
315
372
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
## Active Background Tasks
|
|
373
|
+
backgroundSection = `## Active Background Tasks
|
|
319
374
|
|
|
320
375
|
This session has ${tasks.length} background task(s) running in separate sessions:
|
|
321
376
|
${taskList}
|
|
@@ -331,45 +386,70 @@ agentuity_background_task({
|
|
|
331
386
|
task: "Monitor these background tasks and report when all complete:\\n${tasks.map((t) => `- ${t.id}`).join('\\n')}",
|
|
332
387
|
description: "Monitor child tasks"
|
|
333
388
|
})
|
|
334
|
-
|
|
335
|
-
`;
|
|
389
|
+
\`\`\``;
|
|
336
390
|
}
|
|
337
391
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
392
|
+
// 5. Build SQLite dashboard section
|
|
393
|
+
const dashboardSection = buildSqliteDashboardSummary(dbReader, sessionId);
|
|
394
|
+
|
|
395
|
+
// 6. Combine everything into the full prompt
|
|
396
|
+
const sections: string[] = [];
|
|
397
|
+
if (instructions) sections.push(instructions);
|
|
398
|
+
sections.push(cadenceStateSection);
|
|
399
|
+
if (backgroundSection) sections.push(backgroundSection);
|
|
400
|
+
if (planningState) sections.push(planningState);
|
|
401
|
+
if (imageDescs) sections.push(imageDescs);
|
|
402
|
+
if (toolSummaries) sections.push(toolSummaries);
|
|
403
|
+
if (dashboardSection) sections.push(dashboardSection);
|
|
404
|
+
|
|
405
|
+
// 7. Add diagnostics
|
|
406
|
+
const stats: CompactionStats = {
|
|
407
|
+
planningPhasesCount: countListItems(planningState),
|
|
408
|
+
backgroundTasksCount: tasks.length,
|
|
409
|
+
imageDescriptionsCount: countListItems(imageDescs),
|
|
410
|
+
toolCallSummariesCount: countListItems(toolSummaries),
|
|
411
|
+
estimatedTokens: Math.ceil(sections.join('\n\n').length / 4),
|
|
412
|
+
};
|
|
413
|
+
const diagnostics = formatCompactionDiagnostics(stats);
|
|
414
|
+
if (diagnostics) sections.push(diagnostics);
|
|
415
|
+
|
|
416
|
+
// 8. Enforce token budget
|
|
417
|
+
let fullPrompt = sections.join('\n\n');
|
|
418
|
+
const estimatedTokens = Math.ceil(fullPrompt.length / 4);
|
|
419
|
+
if (maxTokens > 0 && estimatedTokens > maxTokens) {
|
|
420
|
+
// Trim least-critical sections first
|
|
421
|
+
const trimOrder = [diagnostics, toolSummaries, imageDescs, planningState].filter(
|
|
422
|
+
Boolean
|
|
423
|
+
);
|
|
424
|
+
let trimmed = [...sections];
|
|
425
|
+
for (const candidate of trimOrder) {
|
|
426
|
+
if (Math.ceil(trimmed.join('\n\n').length / 4) <= maxTokens) break;
|
|
427
|
+
trimmed = trimmed.filter((s) => s !== candidate);
|
|
428
|
+
}
|
|
429
|
+
fullPrompt = trimmed.join('\n\n');
|
|
430
|
+
}
|
|
353
431
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
- \`planning.findings\` - Discoveries made during work
|
|
361
|
-
- \`planning.errors\` - Failures to avoid repeating
|
|
362
|
-
${backgroundTaskContext}
|
|
363
|
-
After compaction:
|
|
364
|
-
1. Memory will save this summary and update the session record
|
|
365
|
-
2. Memory should update planning.progress with this compaction
|
|
366
|
-
3. Lead will continue the loop from iteration ${state.iteration}
|
|
367
|
-
4. Use 5-Question Reboot to re-orient: Where am I? Where going? Goal? Learned? Done?
|
|
368
|
-
`);
|
|
432
|
+
// 9. Set the full prompt or push to context
|
|
433
|
+
if (useCustomPrompt) {
|
|
434
|
+
output.prompt = fullPrompt;
|
|
435
|
+
} else {
|
|
436
|
+
output.context.push(fullPrompt);
|
|
437
|
+
}
|
|
369
438
|
|
|
370
|
-
|
|
371
|
-
if (
|
|
372
|
-
|
|
439
|
+
// 10. Store pre-compaction snapshot to KV (fire-and-forget)
|
|
440
|
+
if (useSnapshotToKV) {
|
|
441
|
+
storePreCompactionSnapshot(sessionId, {
|
|
442
|
+
timestamp: new Date().toISOString(),
|
|
443
|
+
sessionId,
|
|
444
|
+
planningState: planningState ? { raw: planningState } : undefined,
|
|
445
|
+
backgroundTasks: tasks.map((t) => ({
|
|
446
|
+
id: t.id,
|
|
447
|
+
description: t.description || 'No description',
|
|
448
|
+
status: t.status,
|
|
449
|
+
})),
|
|
450
|
+
cadenceState: state ? { ...state } : undefined,
|
|
451
|
+
branch,
|
|
452
|
+
}).catch(() => {}); // Fire and forget
|
|
373
453
|
}
|
|
374
454
|
},
|
|
375
455
|
|
|
@@ -380,6 +460,44 @@ After compaction:
|
|
|
380
460
|
isActiveCadenceSession(sessionId: string): boolean {
|
|
381
461
|
return activeCadenceSessions.has(sessionId);
|
|
382
462
|
},
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Lazy restore: check KV for persisted Cadence state and populate in-memory Map.
|
|
466
|
+
* Called before routing decisions to recover state after plugin restarts.
|
|
467
|
+
* Returns true if state was found and restored.
|
|
468
|
+
*/
|
|
469
|
+
async tryRestoreFromKV(sessionId: string): Promise<boolean> {
|
|
470
|
+
// Already in memory — nothing to restore
|
|
471
|
+
if (activeCadenceSessions.has(sessionId)) return true;
|
|
472
|
+
// Known non-Cadence session — skip KV lookup
|
|
473
|
+
if (nonCadenceSessions.has(sessionId)) return false;
|
|
474
|
+
|
|
475
|
+
try {
|
|
476
|
+
const kvState = await restoreCadenceStateFromKV(sessionId);
|
|
477
|
+
if (!kvState) {
|
|
478
|
+
if (nonCadenceSessions.size >= NON_CADENCE_CACHE_MAX) {
|
|
479
|
+
nonCadenceSessions.clear();
|
|
480
|
+
}
|
|
481
|
+
nonCadenceSessions.add(sessionId);
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const state: CadenceSessionState = {
|
|
486
|
+
startedAt: (kvState.startedAt as string) ?? new Date().toISOString(),
|
|
487
|
+
loopId: kvState.loopId as string | undefined,
|
|
488
|
+
iteration: (kvState.iteration as number) ?? 1,
|
|
489
|
+
maxIterations: (kvState.maxIterations as number) ?? 50,
|
|
490
|
+
lastActivity: (kvState.lastActivity as string) ?? new Date().toISOString(),
|
|
491
|
+
};
|
|
492
|
+
activeCadenceSessions.set(sessionId, state);
|
|
493
|
+
log(
|
|
494
|
+
`Restored Cadence state from KV for session ${sessionId} (iteration ${state.iteration}/${state.maxIterations})`
|
|
495
|
+
);
|
|
496
|
+
return true;
|
|
497
|
+
} catch {
|
|
498
|
+
return false;
|
|
499
|
+
}
|
|
500
|
+
},
|
|
383
501
|
};
|
|
384
502
|
}
|
|
385
503
|
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import type { OpenCodeDBReader } from '../../sqlite/reader';
|
|
2
|
+
import type { CompactionStats, DBNonTextPart, PreCompactionSnapshot } from '../../sqlite/types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Get the current git branch name.
|
|
6
|
+
* Moved here from cadence.ts and session-memory.ts to deduplicate.
|
|
7
|
+
*/
|
|
8
|
+
export async function getCurrentBranch(): Promise<string> {
|
|
9
|
+
try {
|
|
10
|
+
const proc = Bun.spawn(['git', 'branch', '--show-current'], {
|
|
11
|
+
stdout: 'pipe',
|
|
12
|
+
stderr: 'pipe',
|
|
13
|
+
});
|
|
14
|
+
const stdout = await new Response(proc.stdout).text();
|
|
15
|
+
await proc.exited;
|
|
16
|
+
return stdout.trim() || 'unknown';
|
|
17
|
+
} catch {
|
|
18
|
+
return 'unknown';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Access Agentuity KV storage via CLI.
|
|
24
|
+
* All calls are wrapped in try/catch — returns null on failure.
|
|
25
|
+
*/
|
|
26
|
+
async function kvGet(namespace: string, key: string): Promise<unknown | null> {
|
|
27
|
+
try {
|
|
28
|
+
const proc = Bun.spawn(['agentuity', 'cloud', 'kv', 'get', namespace, key, '--json'], {
|
|
29
|
+
stdout: 'pipe',
|
|
30
|
+
stderr: 'pipe',
|
|
31
|
+
});
|
|
32
|
+
const output = await new Response(proc.stdout).text();
|
|
33
|
+
const exitCode = await proc.exited;
|
|
34
|
+
if (exitCode !== 0) return null;
|
|
35
|
+
return JSON.parse(output);
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function kvSet(namespace: string, key: string, value: unknown): Promise<boolean> {
|
|
42
|
+
try {
|
|
43
|
+
const proc = Bun.spawn(
|
|
44
|
+
['agentuity', 'cloud', 'kv', 'set', namespace, key, JSON.stringify(value)],
|
|
45
|
+
{ stdout: 'pipe', stderr: 'pipe' }
|
|
46
|
+
);
|
|
47
|
+
const exitCode = await proc.exited;
|
|
48
|
+
return exitCode === 0;
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Build the custom compaction prompt for our agent system.
|
|
56
|
+
* This REPLACES the default OpenCode compaction prompt via output.prompt.
|
|
57
|
+
*/
|
|
58
|
+
export function buildCustomCompactionPrompt(mode: 'cadence' | 'regular'): string {
|
|
59
|
+
const cadenceSection =
|
|
60
|
+
mode === 'cadence'
|
|
61
|
+
? `
|
|
62
|
+
|
|
63
|
+
## Cadence Loop State
|
|
64
|
+
- Loop ID, iteration number, max iterations
|
|
65
|
+
- Current phase and what's in progress
|
|
66
|
+
- Whether this is a Lead-of-Leads session with child tasks`
|
|
67
|
+
: '';
|
|
68
|
+
|
|
69
|
+
return `You are generating a continuation context for a multi-agent coding system (Agentuity Coder). Your summary will be the ONLY context the orchestrating Lead agent has after this compaction. Preserve everything needed for seamless continuation.
|
|
70
|
+
|
|
71
|
+
## CRITICAL — Preserve These Verbatim
|
|
72
|
+
1. The current task/objective (quote the user's original request exactly)
|
|
73
|
+
2. All background task IDs (bg_xxx) with status, purpose, and session IDs
|
|
74
|
+
3. Active planning state: current phase, completed phases, next steps, blockers
|
|
75
|
+
4. ALL file paths being actively worked on (with role: created/modified/read)
|
|
76
|
+
5. Key decisions made and their rationale
|
|
77
|
+
6. Any corrections or gotchas discovered during the session
|
|
78
|
+
7. Todo list state (what's done, in progress, pending)
|
|
79
|
+
8. Descriptions of any images or attachments that appeared in conversation${cadenceSection}
|
|
80
|
+
|
|
81
|
+
## Structure Your Summary As:
|
|
82
|
+
|
|
83
|
+
### Active Task
|
|
84
|
+
[Verbatim objective + what the agent was doing when compaction fired]
|
|
85
|
+
|
|
86
|
+
### Planning State
|
|
87
|
+
[Phases with status. Include phase notes, not just titles.]
|
|
88
|
+
|
|
89
|
+
### Background Tasks
|
|
90
|
+
[bg_xxx: description → status (running/completed/errored). Include session IDs.]
|
|
91
|
+
|
|
92
|
+
### Key Context
|
|
93
|
+
[Decisions, constraints, user preferences, corrections discovered]
|
|
94
|
+
|
|
95
|
+
### Active Files
|
|
96
|
+
[filepath → role (creating/modifying/reading) + what's being done to it]
|
|
97
|
+
|
|
98
|
+
### Images & Attachments
|
|
99
|
+
[Describe any images/screenshots: what they showed, when they appeared, why they mattered]
|
|
100
|
+
|
|
101
|
+
### Next Steps
|
|
102
|
+
[What should happen immediately after compaction resumes]
|
|
103
|
+
|
|
104
|
+
## Rules
|
|
105
|
+
- Use specific file paths, task IDs, phase names — NOT vague references.
|
|
106
|
+
- State what tools returned, not just that they were called.
|
|
107
|
+
- NEVER drop background task references — the agent MUST know what's still running.
|
|
108
|
+
- Prefer completeness over brevity — this is the agent's entire working memory.`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Fetch planning state from KV and format as markdown.
|
|
113
|
+
* Returns null if KV is unavailable or no planning state exists.
|
|
114
|
+
*/
|
|
115
|
+
export async function fetchAndFormatPlanningState(sessionId: string): Promise<string | null> {
|
|
116
|
+
try {
|
|
117
|
+
const record = await kvGet('agentuity-opencode-memory', `session:${sessionId}`);
|
|
118
|
+
if (!record || typeof record !== 'object') return null;
|
|
119
|
+
|
|
120
|
+
const data = (record as Record<string, unknown>).data ?? record;
|
|
121
|
+
const planning = (data as Record<string, unknown>).planning as
|
|
122
|
+
| Record<string, unknown>
|
|
123
|
+
| undefined;
|
|
124
|
+
if (!planning) return null;
|
|
125
|
+
|
|
126
|
+
const lines: string[] = ['## Planning State (from KV)'];
|
|
127
|
+
if (planning.objective) lines.push(`**Objective:** ${planning.objective}`);
|
|
128
|
+
if (planning.current) lines.push(`**Current:** ${planning.current}`);
|
|
129
|
+
if (planning.next) lines.push(`**Next:** ${planning.next}`);
|
|
130
|
+
|
|
131
|
+
const phases = planning.phases as Array<Record<string, unknown>> | undefined;
|
|
132
|
+
if (phases?.length) {
|
|
133
|
+
lines.push('', '### Phases:');
|
|
134
|
+
for (const p of phases) {
|
|
135
|
+
const status = p.status ?? 'unknown';
|
|
136
|
+
const title = p.title ?? p.content ?? 'untitled';
|
|
137
|
+
const notes = p.notes ? ` — ${String(p.notes).slice(0, 100)}` : '';
|
|
138
|
+
lines.push(`- [${status}] ${title}${notes}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const findings = planning.findings as string[] | undefined;
|
|
143
|
+
if (findings?.length) {
|
|
144
|
+
lines.push('', '### Key Findings:');
|
|
145
|
+
for (const f of findings.slice(0, 5)) {
|
|
146
|
+
lines.push(`- ${String(f).slice(0, 150)}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const errors = planning.errors as string[] | undefined;
|
|
151
|
+
if (errors?.length) {
|
|
152
|
+
lines.push('', '### Errors to Avoid:');
|
|
153
|
+
for (const e of errors.slice(0, 3)) {
|
|
154
|
+
lines.push(`- ${String(e).slice(0, 150)}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return lines.join('\n');
|
|
159
|
+
} catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get image/attachment descriptions from SQLite for compaction context.
|
|
166
|
+
* Returns brief metadata about non-text parts in the conversation.
|
|
167
|
+
*/
|
|
168
|
+
export function getImageDescriptions(
|
|
169
|
+
dbReader: OpenCodeDBReader | null,
|
|
170
|
+
sessionId: string
|
|
171
|
+
): string | null {
|
|
172
|
+
if (!dbReader?.isAvailable()) return null;
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const parts = dbReader.getNonTextParts(sessionId);
|
|
176
|
+
if (!parts.length) return null;
|
|
177
|
+
|
|
178
|
+
// Filter to image-like parts (not tool calls — those are separate)
|
|
179
|
+
const imageParts = parts.filter(
|
|
180
|
+
(p: DBNonTextPart) => !['tool-invocation', 'tool-result', 'text'].includes(p.type)
|
|
181
|
+
);
|
|
182
|
+
if (!imageParts.length) return null;
|
|
183
|
+
|
|
184
|
+
const lines: string[] = ['## Images & Attachments'];
|
|
185
|
+
for (const part of imageParts.slice(0, 10)) {
|
|
186
|
+
const when = part.timestamp ? ` at ${part.timestamp}` : '';
|
|
187
|
+
lines.push(`- [${part.type}]${when}: message ${part.messageId}`);
|
|
188
|
+
}
|
|
189
|
+
return lines.join('\n');
|
|
190
|
+
} catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Get recent tool call summaries for compaction context.
|
|
197
|
+
* CONCISE — capped at limit calls, brief descriptions only.
|
|
198
|
+
*/
|
|
199
|
+
export function getRecentToolCallSummaries(
|
|
200
|
+
dbReader: OpenCodeDBReader | null,
|
|
201
|
+
sessionId: string,
|
|
202
|
+
limit: number = 5
|
|
203
|
+
): string | null {
|
|
204
|
+
if (!dbReader?.isAvailable() || limit <= 0) return null;
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const calls = dbReader.getRecentToolCalls(sessionId, limit);
|
|
208
|
+
if (!calls.length) return null;
|
|
209
|
+
|
|
210
|
+
const lines: string[] = ['## Recent Tool Activity'];
|
|
211
|
+
for (const call of calls) {
|
|
212
|
+
const inputBrief = call.input ? ` — ${String(call.input).slice(0, 80)}` : '';
|
|
213
|
+
const outputBrief = call.output ? ` → ${String(call.output).slice(0, 80)}` : '';
|
|
214
|
+
lines.push(`- ${call.toolName}${inputBrief}${outputBrief}`);
|
|
215
|
+
}
|
|
216
|
+
return lines.join('\n');
|
|
217
|
+
} catch {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Store a pre-compaction snapshot to KV as a recovery mechanism.
|
|
224
|
+
*/
|
|
225
|
+
export async function storePreCompactionSnapshot(
|
|
226
|
+
sessionId: string,
|
|
227
|
+
snapshot: PreCompactionSnapshot
|
|
228
|
+
): Promise<void> {
|
|
229
|
+
try {
|
|
230
|
+
await kvSet('agentuity-opencode-memory', `compaction:snapshot:${sessionId}`, snapshot);
|
|
231
|
+
} catch {
|
|
232
|
+
// Silently fail — this is a best-effort recovery mechanism
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Persist Cadence session state to KV for recovery after plugin restart.
|
|
238
|
+
*/
|
|
239
|
+
export async function persistCadenceStateToKV(
|
|
240
|
+
sessionId: string,
|
|
241
|
+
state: Record<string, unknown>
|
|
242
|
+
): Promise<void> {
|
|
243
|
+
try {
|
|
244
|
+
await kvSet('agentuity-opencode-memory', `cadence:active:${sessionId}`, state);
|
|
245
|
+
} catch {
|
|
246
|
+
// Silently fail
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Restore Cadence session state from KV.
|
|
252
|
+
*/
|
|
253
|
+
export async function restoreCadenceStateFromKV(
|
|
254
|
+
sessionId: string
|
|
255
|
+
): Promise<Record<string, unknown> | null> {
|
|
256
|
+
try {
|
|
257
|
+
const state = await kvGet('agentuity-opencode-memory', `cadence:active:${sessionId}`);
|
|
258
|
+
return state as Record<string, unknown> | null;
|
|
259
|
+
} catch {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Format compaction diagnostics — brief summary of what was preserved.
|
|
266
|
+
*/
|
|
267
|
+
export function formatCompactionDiagnostics(stats: CompactionStats): string {
|
|
268
|
+
const parts: string[] = [];
|
|
269
|
+
if (stats.planningPhasesCount > 0) parts.push(`${stats.planningPhasesCount} planning phases`);
|
|
270
|
+
if (stats.backgroundTasksCount > 0) parts.push(`${stats.backgroundTasksCount} background tasks`);
|
|
271
|
+
if (stats.imageDescriptionsCount > 0) parts.push(`${stats.imageDescriptionsCount} image refs`);
|
|
272
|
+
if (stats.toolCallSummariesCount > 0) parts.push(`${stats.toolCallSummariesCount} tool calls`);
|
|
273
|
+
|
|
274
|
+
if (!parts.length) return '';
|
|
275
|
+
return `> **Compaction preserved:** ${parts.join(', ')} (~${stats.estimatedTokens} tokens injected)`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Count markdown list items in a string */
|
|
279
|
+
export function countListItems(s: string | null): number {
|
|
280
|
+
if (!s) return 0;
|
|
281
|
+
return (s.match(/^- /gm) ?? []).length;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Re-export types used by consumers of this module
|
|
285
|
+
export type { CompactionConfig } from '../../types';
|
|
286
|
+
export type {
|
|
287
|
+
CompactionStats,
|
|
288
|
+
DBNonTextPart,
|
|
289
|
+
DBToolCallSummary,
|
|
290
|
+
PreCompactionSnapshot,
|
|
291
|
+
} from '../../sqlite/types';
|
|
@@ -95,7 +95,11 @@ function detectMode(
|
|
|
95
95
|
return null;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
export function createParamsHooks(
|
|
98
|
+
export function createParamsHooks(
|
|
99
|
+
ctx: PluginInput,
|
|
100
|
+
_config: CoderConfig,
|
|
101
|
+
lastUserMessages?: Map<string, string>
|
|
102
|
+
): ParamsHooks {
|
|
99
103
|
return {
|
|
100
104
|
async onParams(input: unknown, output: unknown): Promise<void> {
|
|
101
105
|
// Input contains: sessionID, agent, model, provider, message
|
|
@@ -117,6 +121,11 @@ export function createParamsHooks(ctx: PluginInput, _config: CoderConfig): Param
|
|
|
117
121
|
const messageContent = inputObj.message?.content || '';
|
|
118
122
|
if (!messageContent) return;
|
|
119
123
|
|
|
124
|
+
// Store user message text for downstream hooks (e.g. cadence trigger detection)
|
|
125
|
+
if (lastUserMessages && inputObj.sessionID) {
|
|
126
|
+
lastUserMessages.set(inputObj.sessionID, messageContent);
|
|
127
|
+
}
|
|
128
|
+
|
|
120
129
|
// Check for dynamic mode triggers
|
|
121
130
|
const detected = detectMode(messageContent);
|
|
122
131
|
if (!detected) return;
|