@agentuity/opencode 1.0.15 → 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/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 +1 -1
- package/dist/agents/scout.js.map +1 -1
- package/dist/background/manager.d.ts +1 -0
- package/dist/background/manager.d.ts.map +1 -1
- package/dist/background/manager.js +60 -26
- 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/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/monitor.ts +22 -33
- package/src/agents/reviewer.ts +1 -1
- package/src/agents/scout.ts +1 -1
- package/src/background/manager.ts +67 -31
- 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/types.ts +30 -0
|
@@ -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;
|
|
@@ -1,23 +1,18 @@
|
|
|
1
1
|
import type { PluginInput } from '@opencode-ai/plugin';
|
|
2
2
|
import type { CoderConfig } from '../../types';
|
|
3
3
|
import type { BackgroundManager } from '../../background';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return stdout.trim() || 'unknown';
|
|
17
|
-
} catch {
|
|
18
|
-
return 'unknown';
|
|
19
|
-
}
|
|
20
|
-
}
|
|
4
|
+
import type { OpenCodeDBReader } from '../../sqlite';
|
|
5
|
+
import type { CompactionStats } from '../../sqlite/types';
|
|
6
|
+
import {
|
|
7
|
+
getCurrentBranch,
|
|
8
|
+
buildCustomCompactionPrompt,
|
|
9
|
+
fetchAndFormatPlanningState,
|
|
10
|
+
getImageDescriptions,
|
|
11
|
+
getRecentToolCallSummaries,
|
|
12
|
+
storePreCompactionSnapshot,
|
|
13
|
+
formatCompactionDiagnostics,
|
|
14
|
+
countListItems,
|
|
15
|
+
} from './compaction-utils';
|
|
21
16
|
|
|
22
17
|
export interface SessionMemoryHooks {
|
|
23
18
|
onEvent: (input: {
|
|
@@ -38,8 +33,9 @@ export interface SessionMemoryHooks {
|
|
|
38
33
|
*/
|
|
39
34
|
export function createSessionMemoryHooks(
|
|
40
35
|
ctx: PluginInput,
|
|
41
|
-
|
|
42
|
-
backgroundManager?: BackgroundManager
|
|
36
|
+
config: CoderConfig,
|
|
37
|
+
backgroundManager?: BackgroundManager,
|
|
38
|
+
dbReader?: OpenCodeDBReader
|
|
43
39
|
): SessionMemoryHooks {
|
|
44
40
|
const log = (msg: string) => {
|
|
45
41
|
ctx.client.app.log({
|
|
@@ -144,7 +140,8 @@ Then continue with the current task if there is one.`,
|
|
|
144
140
|
|
|
145
141
|
/**
|
|
146
142
|
* Inject Memory system info during compaction.
|
|
147
|
-
*
|
|
143
|
+
* Uses output.prompt to REPLACE the default compaction prompt with
|
|
144
|
+
* enriched context (planning state, images, tool calls, diagnostics).
|
|
148
145
|
*/
|
|
149
146
|
async onCompacting(
|
|
150
147
|
input: { sessionID: string },
|
|
@@ -153,12 +150,43 @@ Then continue with the current task if there is one.`,
|
|
|
153
150
|
const sessionId = input.sessionID;
|
|
154
151
|
log(`Compacting session ${sessionId}`);
|
|
155
152
|
|
|
156
|
-
//
|
|
157
|
-
const
|
|
153
|
+
// Config flags for compaction behavior
|
|
154
|
+
const compactionCfg = config?.compaction ?? {};
|
|
155
|
+
const useCustomPrompt = compactionCfg.customPrompt !== false;
|
|
156
|
+
const useInlinePlanning = compactionCfg.inlinePlanning !== false;
|
|
157
|
+
const useImageAwareness = compactionCfg.imageAwareness !== false;
|
|
158
|
+
const useSnapshotToKV = compactionCfg.snapshotToKV !== false;
|
|
159
|
+
const maxTokens = compactionCfg.maxContextTokens ?? 4000;
|
|
160
|
+
|
|
161
|
+
// 1. Build custom compaction instructions
|
|
162
|
+
const instructions = useCustomPrompt ? buildCustomCompactionPrompt('regular') : null;
|
|
163
|
+
|
|
164
|
+
// 2. Gather enrichment data in parallel
|
|
165
|
+
const toolCallLimit = config?.compaction?.toolCallSummaryLimit ?? 5;
|
|
166
|
+
const [branch, planningState, imageDescs, toolSummaries] = await Promise.all([
|
|
167
|
+
getCurrentBranch(),
|
|
168
|
+
useInlinePlanning ? fetchAndFormatPlanningState(sessionId) : Promise.resolve(null),
|
|
169
|
+
useImageAwareness
|
|
170
|
+
? Promise.resolve(getImageDescriptions(dbReader ?? null, sessionId))
|
|
171
|
+
: Promise.resolve(null),
|
|
172
|
+
Promise.resolve(getRecentToolCallSummaries(dbReader ?? null, sessionId, toolCallLimit)),
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
// 3. Build session state section
|
|
176
|
+
const sessionStateSection = `## Session Memory
|
|
177
|
+
|
|
178
|
+
This session's context is being saved to persistent memory.
|
|
179
|
+
Session record location: \`session:${sessionId}\` in agentuity-opencode-memory
|
|
180
|
+
Current branch: ${branch}
|
|
158
181
|
|
|
159
|
-
|
|
182
|
+
After compaction:
|
|
183
|
+
1. Memory will save this summary to the session record
|
|
184
|
+
2. If planning is active, Memory should update planning.progress with this compaction
|
|
185
|
+
3. Memory will apply inline reasoning if significant patterns/corrections emerged`;
|
|
186
|
+
|
|
187
|
+
// 4. Build background tasks section
|
|
160
188
|
const tasks = backgroundManager?.getTasksByParent(sessionId) ?? [];
|
|
161
|
-
let
|
|
189
|
+
let backgroundSection: string | null = null;
|
|
162
190
|
|
|
163
191
|
if (tasks.length > 0) {
|
|
164
192
|
const taskList = tasks
|
|
@@ -168,38 +196,72 @@ Then continue with the current task if there is one.`,
|
|
|
168
196
|
)
|
|
169
197
|
.join('\n');
|
|
170
198
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
## Active Background Tasks
|
|
199
|
+
backgroundSection = `## Active Background Tasks
|
|
174
200
|
|
|
175
201
|
This session has ${tasks.length} background task(s) running in separate sessions:
|
|
176
202
|
${taskList}
|
|
177
203
|
|
|
178
204
|
**CRITICAL:** Task IDs and session IDs persist across compaction - these tasks are still running.
|
|
179
|
-
Use \`agentuity_background_output({ task_id: "..." })\` to check their status
|
|
180
|
-
`;
|
|
205
|
+
Use \`agentuity_background_output({ task_id: "..." })\` to check their status.`;
|
|
181
206
|
}
|
|
182
207
|
|
|
183
|
-
|
|
184
|
-
|
|
208
|
+
// 5. Combine everything into the full prompt
|
|
209
|
+
const sections: string[] = [];
|
|
210
|
+
if (instructions) sections.push(instructions);
|
|
211
|
+
sections.push(sessionStateSection);
|
|
212
|
+
if (backgroundSection) sections.push(backgroundSection);
|
|
213
|
+
if (planningState) sections.push(planningState);
|
|
214
|
+
if (imageDescs) sections.push(imageDescs);
|
|
215
|
+
if (toolSummaries) sections.push(toolSummaries);
|
|
216
|
+
|
|
217
|
+
// 6. Add diagnostics
|
|
218
|
+
const stats: CompactionStats = {
|
|
219
|
+
planningPhasesCount: countListItems(planningState),
|
|
220
|
+
backgroundTasksCount: tasks.length,
|
|
221
|
+
imageDescriptionsCount: countListItems(imageDescs),
|
|
222
|
+
toolCallSummariesCount: countListItems(toolSummaries),
|
|
223
|
+
estimatedTokens: Math.ceil(sections.join('\n\n').length / 4),
|
|
224
|
+
};
|
|
225
|
+
const diagnostics = formatCompactionDiagnostics(stats);
|
|
226
|
+
if (diagnostics) sections.push(diagnostics);
|
|
227
|
+
|
|
228
|
+
// 7. Enforce token budget
|
|
229
|
+
let fullPrompt = sections.join('\n\n');
|
|
230
|
+
const estimatedTokens = Math.ceil(fullPrompt.length / 4);
|
|
231
|
+
if (maxTokens > 0 && estimatedTokens > maxTokens) {
|
|
232
|
+
// Trim least-critical sections first
|
|
233
|
+
const trimOrder = [diagnostics, toolSummaries, imageDescs, planningState].filter(
|
|
234
|
+
Boolean
|
|
235
|
+
);
|
|
236
|
+
let trimmed = [...sections];
|
|
237
|
+
for (const candidate of trimOrder) {
|
|
238
|
+
if (Math.ceil(trimmed.join('\n\n').length / 4) <= maxTokens) break;
|
|
239
|
+
trimmed = trimmed.filter((s) => s !== candidate);
|
|
240
|
+
}
|
|
241
|
+
fullPrompt = trimmed.join('\n\n');
|
|
242
|
+
}
|
|
185
243
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
244
|
+
// 8. Set the full prompt or push to context
|
|
245
|
+
if (useCustomPrompt) {
|
|
246
|
+
output.prompt = fullPrompt;
|
|
247
|
+
} else {
|
|
248
|
+
output.context.push(fullPrompt);
|
|
249
|
+
}
|
|
189
250
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
251
|
+
// 9. Store pre-compaction snapshot to KV (fire-and-forget)
|
|
252
|
+
if (useSnapshotToKV) {
|
|
253
|
+
storePreCompactionSnapshot(sessionId, {
|
|
254
|
+
timestamp: new Date().toISOString(),
|
|
255
|
+
sessionId,
|
|
256
|
+
planningState: planningState ? { raw: planningState } : undefined,
|
|
257
|
+
backgroundTasks: tasks.map((t) => ({
|
|
258
|
+
id: t.id,
|
|
259
|
+
description: t.description || 'No description',
|
|
260
|
+
status: t.status,
|
|
261
|
+
})),
|
|
262
|
+
branch,
|
|
263
|
+
}).catch(() => {}); // Fire and forget
|
|
264
|
+
}
|
|
203
265
|
},
|
|
204
266
|
};
|
|
205
267
|
}
|
package/src/plugin/plugin.ts
CHANGED
|
@@ -92,10 +92,14 @@ export async function createCoderPlugin(ctx: PluginInput): Promise<Hooks> {
|
|
|
92
92
|
const resolvedDbPath = resolveOpenCodeDBPath();
|
|
93
93
|
const dbReader = new OpenCodeDBReader(resolvedDbPath ? { dbPath: resolvedDbPath } : undefined);
|
|
94
94
|
|
|
95
|
+
// Shared Map: chat.params stores the user's message text per session,
|
|
96
|
+
// chat.message reads it for trigger detection (avoids scanning model output).
|
|
97
|
+
const lastUserMessages = new Map<string, string>();
|
|
98
|
+
|
|
95
99
|
const sessionHooks = createSessionHooks(ctx, coderConfig);
|
|
96
100
|
const toolHooks = createToolHooks(ctx, coderConfig);
|
|
97
101
|
const keywordHooks = createKeywordHooks(ctx, coderConfig);
|
|
98
|
-
const paramsHooks = createParamsHooks(ctx, coderConfig);
|
|
102
|
+
const paramsHooks = createParamsHooks(ctx, coderConfig, lastUserMessages);
|
|
99
103
|
const tmuxManager = coderConfig.tmux?.enabled
|
|
100
104
|
? new TmuxSessionManager(ctx, coderConfig.tmux, {
|
|
101
105
|
onLog: (message) =>
|
|
@@ -157,11 +161,22 @@ export async function createCoderPlugin(ctx: PluginInput): Promise<Hooks> {
|
|
|
157
161
|
});
|
|
158
162
|
|
|
159
163
|
// Create hooks that need backgroundManager for task reference injection during compaction
|
|
160
|
-
const cadenceHooks = createCadenceHooks(
|
|
164
|
+
const cadenceHooks = createCadenceHooks(
|
|
165
|
+
ctx,
|
|
166
|
+
coderConfig,
|
|
167
|
+
backgroundManager,
|
|
168
|
+
dbReader,
|
|
169
|
+
lastUserMessages
|
|
170
|
+
);
|
|
161
171
|
|
|
162
172
|
// Session memory hooks handle checkpointing and compaction for non-Cadence sessions
|
|
163
173
|
// Orchestration (deciding which module handles which session) happens below in the hooks
|
|
164
|
-
const sessionMemoryHooks = createSessionMemoryHooks(
|
|
174
|
+
const sessionMemoryHooks = createSessionMemoryHooks(
|
|
175
|
+
ctx,
|
|
176
|
+
coderConfig,
|
|
177
|
+
backgroundManager,
|
|
178
|
+
dbReader
|
|
179
|
+
);
|
|
165
180
|
|
|
166
181
|
const configHandler = createConfigHandler(coderConfig);
|
|
167
182
|
|
|
@@ -230,16 +245,28 @@ export async function createCoderPlugin(ctx: PluginInput): Promise<Hooks> {
|
|
|
230
245
|
}
|
|
231
246
|
// Orchestrate: route to appropriate module based on session type
|
|
232
247
|
const sessionId = extractSessionIdFromEvent(input);
|
|
233
|
-
if (sessionId
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
)
|
|
248
|
+
if (sessionId) {
|
|
249
|
+
// Try lazy restore from KV if not in memory (survives plugin restarts)
|
|
250
|
+
if (!cadenceHooks.isActiveCadenceSession(sessionId)) {
|
|
251
|
+
await cadenceHooks.tryRestoreFromKV(sessionId);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (cadenceHooks.isActiveCadenceSession(sessionId)) {
|
|
255
|
+
await cadenceHooks.onEvent(input);
|
|
256
|
+
} else {
|
|
257
|
+
// Non-Cadence sessions - handle session.compacted for checkpointing
|
|
258
|
+
await sessionMemoryHooks.onEvent(
|
|
259
|
+
input as { event: { type: string; properties?: Record<string, unknown> } }
|
|
260
|
+
);
|
|
261
|
+
}
|
|
240
262
|
}
|
|
241
263
|
},
|
|
242
264
|
'experimental.session.compacting': async (input, output) => {
|
|
265
|
+
// Try lazy restore from KV if not in memory (survives plugin restarts)
|
|
266
|
+
if (!cadenceHooks.isActiveCadenceSession(input.sessionID)) {
|
|
267
|
+
await cadenceHooks.tryRestoreFromKV(input.sessionID);
|
|
268
|
+
}
|
|
269
|
+
|
|
243
270
|
// Orchestrate: route to appropriate module based on session type
|
|
244
271
|
if (cadenceHooks.isActiveCadenceSession(input.sessionID)) {
|
|
245
272
|
await cadenceHooks.onCompacting(input, output);
|
|
@@ -318,6 +345,16 @@ function createConfigHandler(
|
|
|
318
345
|
};
|
|
319
346
|
}
|
|
320
347
|
|
|
348
|
+
// Compaction config: increase reserved token buffer to accommodate our enriched
|
|
349
|
+
// compaction prompts (planning state, image descriptions, tool summaries, diagnostics).
|
|
350
|
+
// Default OpenCode reserved buffer is too small for the context we inject.
|
|
351
|
+
const existingCompaction = (config.compaction ?? {}) as Record<string, unknown>;
|
|
352
|
+
const existingReserved = existingCompaction.reserved;
|
|
353
|
+
config.compaction = {
|
|
354
|
+
...existingCompaction,
|
|
355
|
+
reserved: typeof existingReserved === 'number' ? existingReserved : 40_000,
|
|
356
|
+
};
|
|
357
|
+
|
|
321
358
|
config.command = {
|
|
322
359
|
...(config.command as Record<string, CommandDefinition> | undefined),
|
|
323
360
|
...commands,
|
package/src/sqlite/index.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
export { OpenCodeDBReader } from './reader';
|
|
2
2
|
export type {
|
|
3
|
+
CompactionStats,
|
|
3
4
|
DBMessage,
|
|
5
|
+
DBNonTextPart,
|
|
4
6
|
DBPart,
|
|
5
7
|
DBSession,
|
|
6
8
|
DBTextPart,
|
|
7
9
|
DBTodo,
|
|
8
10
|
DBToolCall,
|
|
11
|
+
DBToolCallSummary,
|
|
9
12
|
MessageTokens,
|
|
10
13
|
OpenCodeDBConfig,
|
|
14
|
+
PreCompactionSnapshot,
|
|
11
15
|
SessionCostSummary,
|
|
12
16
|
SessionStatus,
|
|
13
17
|
SessionSummary,
|
package/src/sqlite/queries.ts
CHANGED
|
@@ -47,4 +47,9 @@ export const QUERIES = {
|
|
|
47
47
|
SEARCH_SESSIONS: `SELECT id, project_id, parent_id, slug, directory, title, version, share_url, summary_additions, summary_deletions, summary_files, summary_diffs, time_created, time_updated, time_compacting, time_archived FROM session WHERE title LIKE ? COLLATE NOCASE ORDER BY time_updated DESC`,
|
|
48
48
|
|
|
49
49
|
SEARCH_SESSIONS_LIMITED: `SELECT id, project_id, parent_id, slug, directory, title, version, share_url, summary_additions, summary_deletions, summary_files, summary_diffs, time_created, time_updated, time_compacting, time_archived FROM session WHERE title LIKE ? COLLATE NOCASE ORDER BY time_updated DESC LIMIT ?`,
|
|
50
|
+
|
|
51
|
+
GET_NON_TEXT_PARTS: `SELECT * FROM part WHERE session_id = ?
|
|
52
|
+
AND json_valid(data)
|
|
53
|
+
AND json_extract(data, '$.type') != 'text'
|
|
54
|
+
ORDER BY time_created DESC LIMIT ?`,
|
|
50
55
|
} as const;
|