@agentuity/opencode 1.0.15 → 1.0.17
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/architect.d.ts +1 -1
- package/dist/agents/architect.d.ts.map +1 -1
- package/dist/agents/architect.js +30 -33
- package/dist/agents/architect.js.map +1 -1
- package/dist/agents/builder.d.ts +1 -1
- package/dist/agents/builder.d.ts.map +1 -1
- package/dist/agents/builder.js +53 -60
- package/dist/agents/builder.js.map +1 -1
- package/dist/agents/expert-backend.d.ts +1 -1
- package/dist/agents/expert-backend.d.ts.map +1 -1
- package/dist/agents/expert-backend.js +32 -40
- package/dist/agents/expert-backend.js.map +1 -1
- package/dist/agents/expert-frontend.d.ts +1 -1
- package/dist/agents/expert-frontend.d.ts.map +1 -1
- package/dist/agents/expert-frontend.js +18 -24
- package/dist/agents/expert-frontend.js.map +1 -1
- package/dist/agents/expert-ops.d.ts +1 -1
- package/dist/agents/expert-ops.d.ts.map +1 -1
- package/dist/agents/expert-ops.js +37 -51
- package/dist/agents/expert-ops.js.map +1 -1
- package/dist/agents/expert.d.ts +1 -1
- package/dist/agents/expert.d.ts.map +1 -1
- package/dist/agents/expert.js +33 -43
- 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 +179 -222
- package/dist/agents/lead.js.map +1 -1
- package/dist/agents/memory.d.ts +1 -1
- package/dist/agents/memory.d.ts.map +1 -1
- package/dist/agents/memory.js +62 -90
- package/dist/agents/memory.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 +84 -44
- package/dist/agents/monitor.js.map +1 -1
- package/dist/agents/product.d.ts +1 -1
- package/dist/agents/product.d.ts.map +1 -1
- package/dist/agents/product.js +16 -22
- package/dist/agents/product.js.map +1 -1
- package/dist/agents/reviewer.d.ts +1 -1
- package/dist/agents/reviewer.d.ts.map +1 -1
- package/dist/agents/reviewer.js +15 -27
- package/dist/agents/reviewer.js.map +1 -1
- package/dist/agents/runner.d.ts +1 -1
- package/dist/agents/runner.d.ts.map +1 -1
- package/dist/agents/runner.js +52 -76
- package/dist/agents/runner.js.map +1 -1
- package/dist/agents/scout.d.ts +1 -1
- package/dist/agents/scout.d.ts.map +1 -1
- package/dist/agents/scout.js +42 -43
- package/dist/agents/scout.js.map +1 -1
- package/dist/agents/types.d.ts +8 -0
- package/dist/agents/types.d.ts.map +1 -1
- package/dist/background/manager.d.ts +18 -0
- package/dist/background/manager.d.ts.map +1 -1
- package/dist/background/manager.js +201 -33
- package/dist/background/manager.js.map +1 -1
- package/dist/background/types.d.ts +3 -0
- package/dist/background/types.d.ts.map +1 -1
- package/dist/config/loader.js +2 -2
- 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 -70
- 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/completion.d.ts +14 -0
- package/dist/plugin/hooks/completion.d.ts.map +1 -0
- package/dist/plugin/hooks/completion.js +45 -0
- package/dist/plugin/hooks/completion.js.map +1 -0
- package/dist/plugin/hooks/params.d.ts +47 -2
- package/dist/plugin/hooks/params.d.ts.map +1 -1
- package/dist/plugin/hooks/params.js +82 -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 +101 -48
- package/dist/plugin/hooks/session-memory.js.map +1 -1
- package/dist/plugin/hooks/tools.d.ts.map +1 -1
- package/dist/plugin/hooks/tools.js +26 -1
- package/dist/plugin/hooks/tools.js.map +1 -1
- package/dist/plugin/plugin.d.ts.map +1 -1
- package/dist/plugin/plugin.js +38 -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.map +1 -1
- package/dist/tools/background.js +15 -0
- package/dist/tools/background.js.map +1 -1
- package/dist/types.d.ts +46 -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/architect.ts +30 -33
- package/src/agents/builder.ts +53 -60
- package/src/agents/expert-backend.ts +32 -40
- package/src/agents/expert-frontend.ts +18 -24
- package/src/agents/expert-ops.ts +37 -51
- package/src/agents/expert.ts +33 -43
- package/src/agents/lead.ts +179 -222
- package/src/agents/memory.ts +62 -90
- package/src/agents/monitor.ts +84 -44
- package/src/agents/product.ts +16 -22
- package/src/agents/reviewer.ts +15 -27
- package/src/agents/runner.ts +52 -76
- package/src/agents/scout.ts +42 -43
- package/src/agents/types.ts +8 -0
- package/src/background/manager.ts +227 -38
- package/src/background/types.ts +3 -0
- package/src/config/loader.ts +2 -2
- package/src/plugin/hooks/cadence.ts +188 -74
- package/src/plugin/hooks/compaction-utils.ts +291 -0
- package/src/plugin/hooks/completion.ts +61 -0
- package/src/plugin/hooks/params.ts +107 -2
- package/src/plugin/hooks/session-memory.ts +113 -47
- package/src/plugin/hooks/tools.ts +32 -1
- package/src/plugin/plugin.ts +54 -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 +28 -0
- package/src/types.ts +40 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { PluginInput } from '@opencode-ai/plugin';
|
|
2
|
+
import type { CoderConfig } from '../../types';
|
|
3
|
+
|
|
4
|
+
export interface CompletionHooks {
|
|
5
|
+
onParams: (input: unknown) => void;
|
|
6
|
+
onMessage: (input: unknown) => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Creates hooks for logging agent completion metrics.
|
|
11
|
+
*
|
|
12
|
+
* Tracks the start of each LLM call (via chat.params) and logs
|
|
13
|
+
* agent name, model, and duration when the response arrives (via chat.message).
|
|
14
|
+
*/
|
|
15
|
+
export function createCompletionHooks(ctx: PluginInput, _config: CoderConfig): CompletionHooks {
|
|
16
|
+
const startTimes = new Map<string, { startedAt: number; agent?: string; model?: string }>();
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
onParams(input: unknown): void {
|
|
20
|
+
const inp = input as {
|
|
21
|
+
sessionID?: string;
|
|
22
|
+
agent?: string;
|
|
23
|
+
model?: string;
|
|
24
|
+
};
|
|
25
|
+
if (!inp.sessionID) return;
|
|
26
|
+
startTimes.set(inp.sessionID, {
|
|
27
|
+
startedAt: Date.now(),
|
|
28
|
+
agent: inp.agent,
|
|
29
|
+
model: inp.model,
|
|
30
|
+
});
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
onMessage(input: unknown): void {
|
|
34
|
+
const inp = input as { sessionID?: string };
|
|
35
|
+
if (!inp.sessionID) return;
|
|
36
|
+
|
|
37
|
+
const start = startTimes.get(inp.sessionID);
|
|
38
|
+
if (!start) return;
|
|
39
|
+
|
|
40
|
+
const durationMs = Date.now() - start.startedAt;
|
|
41
|
+
const durationSec = (durationMs / 1000).toFixed(1);
|
|
42
|
+
|
|
43
|
+
const logLine = `Completion: agent=${start.agent ?? 'unknown'} model=${start.model ?? 'unknown'} duration=${durationSec}s`;
|
|
44
|
+
|
|
45
|
+
// Verbose local logging for immediate visibility
|
|
46
|
+
console.debug(`[agentuity-coder] ${logLine}`);
|
|
47
|
+
|
|
48
|
+
// Also send to the OpenCode log service
|
|
49
|
+
ctx.client.app.log({
|
|
50
|
+
body: {
|
|
51
|
+
service: 'agentuity-coder',
|
|
52
|
+
level: 'debug',
|
|
53
|
+
message: logLine,
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Clean up after logging
|
|
58
|
+
startTimes.delete(inp.sessionID);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PluginInput } from '@opencode-ai/plugin';
|
|
2
|
-
import type { CoderConfig } from '../../types';
|
|
2
|
+
import type { AgentConfig, CoderConfig } from '../../types';
|
|
3
3
|
|
|
4
4
|
export interface ParamsHooks {
|
|
5
5
|
onParams: (input: unknown, output: unknown) => Promise<void>;
|
|
@@ -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;
|
|
@@ -190,3 +199,99 @@ export function createParamsHooks(ctx: PluginInput, _config: CoderConfig): Param
|
|
|
190
199
|
*
|
|
191
200
|
* Note: Triggers use multi-word phrases to avoid false positives from common words.
|
|
192
201
|
*/
|
|
202
|
+
|
|
203
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
204
|
+
// Model Fallback Chain
|
|
205
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
/** Retryable HTTP status codes that should trigger model fallback */
|
|
208
|
+
export const RETRYABLE_STATUS_CODES = [429, 500, 502, 503] as const;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Tracks API errors per agent to enable model fallback on subsequent calls.
|
|
212
|
+
*
|
|
213
|
+
* When an agent's primary model fails with a retryable error (429, 500, 502, 503),
|
|
214
|
+
* the next `chat.params` call can select a fallback model from the agent's
|
|
215
|
+
* `fallbackModels` list.
|
|
216
|
+
*
|
|
217
|
+
* Current limitation: The `chat.params` hook can modify temperature/topP/topK/options
|
|
218
|
+
* but CANNOT change the model itself (model is in the input, not output). Full model
|
|
219
|
+
* fallback requires one of:
|
|
220
|
+
* 1. A `chat.error` hook that allows retrying with a different model
|
|
221
|
+
* 2. A `chat.model` hook that allows overriding the model selection
|
|
222
|
+
* 3. Adding `model` to the `chat.params` output type
|
|
223
|
+
*
|
|
224
|
+
* TODO: When OpenCode adds a suitable hook, implement the retry logic here:
|
|
225
|
+
* - On API error (429/5xx), record the failure in `agentErrorState`
|
|
226
|
+
* - On next `chat.params` call for the same agent, select next fallback model
|
|
227
|
+
* - Log: `[ModelFallback] Switching from ${currentModel} to ${fallbackModel} due to ${error}`
|
|
228
|
+
* - Reset fallback state after successful completion or after TTL expires
|
|
229
|
+
*/
|
|
230
|
+
export class ModelFallbackTracker {
|
|
231
|
+
/**
|
|
232
|
+
* Map of agent name → { failedModel, failedAt, errorCode, fallbackIndex }
|
|
233
|
+
* Used to track which agents have experienced API errors.
|
|
234
|
+
*/
|
|
235
|
+
private agentErrorState = new Map<
|
|
236
|
+
string,
|
|
237
|
+
{
|
|
238
|
+
failedModel: string;
|
|
239
|
+
failedAt: number;
|
|
240
|
+
errorCode: number;
|
|
241
|
+
fallbackIndex: number;
|
|
242
|
+
}
|
|
243
|
+
>();
|
|
244
|
+
|
|
245
|
+
/** TTL for error state — reset after 5 minutes */
|
|
246
|
+
private readonly ERROR_STATE_TTL_MS = 5 * 60 * 1000;
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Record an API error for an agent. Call this from an event handler
|
|
250
|
+
* when a retryable API error is detected.
|
|
251
|
+
*/
|
|
252
|
+
recordError(agentName: string, model: string, errorCode: number): void {
|
|
253
|
+
const existing = this.agentErrorState.get(agentName);
|
|
254
|
+
const fallbackIndex = existing ? existing.fallbackIndex + 1 : 0;
|
|
255
|
+
this.agentErrorState.set(agentName, {
|
|
256
|
+
failedModel: model,
|
|
257
|
+
failedAt: Date.now(),
|
|
258
|
+
errorCode,
|
|
259
|
+
fallbackIndex,
|
|
260
|
+
});
|
|
261
|
+
console.debug(
|
|
262
|
+
`[ModelFallback] Recorded error for ${agentName}: model=${model} code=${errorCode} fallbackIndex=${fallbackIndex}`
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Get the next fallback model for an agent, if one is available.
|
|
268
|
+
* Returns undefined if no fallback is needed or available.
|
|
269
|
+
*/
|
|
270
|
+
getNextFallback(agentName: string, agentConfig: AgentConfig): string | undefined {
|
|
271
|
+
const state = this.agentErrorState.get(agentName);
|
|
272
|
+
if (!state) return undefined;
|
|
273
|
+
|
|
274
|
+
// Check TTL
|
|
275
|
+
if (Date.now() - state.failedAt > this.ERROR_STATE_TTL_MS) {
|
|
276
|
+
this.agentErrorState.delete(agentName);
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const fallbacks = agentConfig.fallbackModels;
|
|
281
|
+
if (!fallbacks?.length) return undefined;
|
|
282
|
+
|
|
283
|
+
if (state.fallbackIndex >= fallbacks.length) {
|
|
284
|
+
// Exhausted all fallbacks
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return fallbacks[state.fallbackIndex];
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Clear error state for an agent (e.g., after successful completion).
|
|
293
|
+
*/
|
|
294
|
+
clearError(agentName: string): void {
|
|
295
|
+
this.agentErrorState.delete(agentName);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
@@ -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({
|
|
@@ -55,6 +51,10 @@ export function createSessionMemoryHooks(
|
|
|
55
51
|
/**
|
|
56
52
|
* Listen for session.compacted event.
|
|
57
53
|
* The compaction summary is already in context - just tell Lead to save it.
|
|
54
|
+
*
|
|
55
|
+
* Note: Compaction continues in the SAME session (via session.prompt with
|
|
56
|
+
* the existing sessionId), so permissions configured in the config hook
|
|
57
|
+
* (plugin.ts) are automatically inherited — no re-application needed.
|
|
58
58
|
*/
|
|
59
59
|
async onEvent(input: {
|
|
60
60
|
event: { type: string; properties?: Record<string, unknown> };
|
|
@@ -144,7 +144,8 @@ Then continue with the current task if there is one.`,
|
|
|
144
144
|
|
|
145
145
|
/**
|
|
146
146
|
* Inject Memory system info during compaction.
|
|
147
|
-
*
|
|
147
|
+
* Uses output.prompt to REPLACE the default compaction prompt with
|
|
148
|
+
* enriched context (planning state, images, tool calls, diagnostics).
|
|
148
149
|
*/
|
|
149
150
|
async onCompacting(
|
|
150
151
|
input: { sessionID: string },
|
|
@@ -153,12 +154,43 @@ Then continue with the current task if there is one.`,
|
|
|
153
154
|
const sessionId = input.sessionID;
|
|
154
155
|
log(`Compacting session ${sessionId}`);
|
|
155
156
|
|
|
156
|
-
//
|
|
157
|
-
const
|
|
157
|
+
// Config flags for compaction behavior
|
|
158
|
+
const compactionCfg = config?.compaction ?? {};
|
|
159
|
+
const useCustomPrompt = compactionCfg.customPrompt !== false;
|
|
160
|
+
const useInlinePlanning = compactionCfg.inlinePlanning !== false;
|
|
161
|
+
const useImageAwareness = compactionCfg.imageAwareness !== false;
|
|
162
|
+
const useSnapshotToKV = compactionCfg.snapshotToKV !== false;
|
|
163
|
+
const maxTokens = compactionCfg.maxContextTokens ?? 4000;
|
|
164
|
+
|
|
165
|
+
// 1. Build custom compaction instructions
|
|
166
|
+
const instructions = useCustomPrompt ? buildCustomCompactionPrompt('regular') : null;
|
|
167
|
+
|
|
168
|
+
// 2. Gather enrichment data in parallel
|
|
169
|
+
const toolCallLimit = config?.compaction?.toolCallSummaryLimit ?? 5;
|
|
170
|
+
const [branch, planningState, imageDescs, toolSummaries] = await Promise.all([
|
|
171
|
+
getCurrentBranch(),
|
|
172
|
+
useInlinePlanning ? fetchAndFormatPlanningState(sessionId) : Promise.resolve(null),
|
|
173
|
+
useImageAwareness
|
|
174
|
+
? Promise.resolve(getImageDescriptions(dbReader ?? null, sessionId))
|
|
175
|
+
: Promise.resolve(null),
|
|
176
|
+
Promise.resolve(getRecentToolCallSummaries(dbReader ?? null, sessionId, toolCallLimit)),
|
|
177
|
+
]);
|
|
178
|
+
|
|
179
|
+
// 3. Build session state section
|
|
180
|
+
const sessionStateSection = `## Session Memory
|
|
181
|
+
|
|
182
|
+
This session's context is being saved to persistent memory.
|
|
183
|
+
Session record location: \`session:${sessionId}\` in agentuity-opencode-memory
|
|
184
|
+
Current branch: ${branch}
|
|
158
185
|
|
|
159
|
-
|
|
186
|
+
After compaction:
|
|
187
|
+
1. Memory will save this summary to the session record
|
|
188
|
+
2. If planning is active, Memory should update planning.progress with this compaction
|
|
189
|
+
3. Memory will apply inline reasoning if significant patterns/corrections emerged`;
|
|
190
|
+
|
|
191
|
+
// 4. Build background tasks section
|
|
160
192
|
const tasks = backgroundManager?.getTasksByParent(sessionId) ?? [];
|
|
161
|
-
let
|
|
193
|
+
let backgroundSection: string | null = null;
|
|
162
194
|
|
|
163
195
|
if (tasks.length > 0) {
|
|
164
196
|
const taskList = tasks
|
|
@@ -168,38 +200,72 @@ Then continue with the current task if there is one.`,
|
|
|
168
200
|
)
|
|
169
201
|
.join('\n');
|
|
170
202
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
## Active Background Tasks
|
|
203
|
+
backgroundSection = `## Active Background Tasks
|
|
174
204
|
|
|
175
205
|
This session has ${tasks.length} background task(s) running in separate sessions:
|
|
176
206
|
${taskList}
|
|
177
207
|
|
|
178
208
|
**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
|
-
`;
|
|
209
|
+
Use \`agentuity_background_output({ task_id: "..." })\` to check their status.`;
|
|
181
210
|
}
|
|
182
211
|
|
|
183
|
-
|
|
184
|
-
|
|
212
|
+
// 5. Combine everything into the full prompt
|
|
213
|
+
const sections: string[] = [];
|
|
214
|
+
if (instructions) sections.push(instructions);
|
|
215
|
+
sections.push(sessionStateSection);
|
|
216
|
+
if (backgroundSection) sections.push(backgroundSection);
|
|
217
|
+
if (planningState) sections.push(planningState);
|
|
218
|
+
if (imageDescs) sections.push(imageDescs);
|
|
219
|
+
if (toolSummaries) sections.push(toolSummaries);
|
|
220
|
+
|
|
221
|
+
// 6. Add diagnostics
|
|
222
|
+
const stats: CompactionStats = {
|
|
223
|
+
planningPhasesCount: countListItems(planningState),
|
|
224
|
+
backgroundTasksCount: tasks.length,
|
|
225
|
+
imageDescriptionsCount: countListItems(imageDescs),
|
|
226
|
+
toolCallSummariesCount: countListItems(toolSummaries),
|
|
227
|
+
estimatedTokens: Math.ceil(sections.join('\n\n').length / 4),
|
|
228
|
+
};
|
|
229
|
+
const diagnostics = formatCompactionDiagnostics(stats);
|
|
230
|
+
if (diagnostics) sections.push(diagnostics);
|
|
231
|
+
|
|
232
|
+
// 7. Enforce token budget
|
|
233
|
+
let fullPrompt = sections.join('\n\n');
|
|
234
|
+
const estimatedTokens = Math.ceil(fullPrompt.length / 4);
|
|
235
|
+
if (maxTokens > 0 && estimatedTokens > maxTokens) {
|
|
236
|
+
// Trim least-critical sections first
|
|
237
|
+
const trimOrder = [diagnostics, toolSummaries, imageDescs, planningState].filter(
|
|
238
|
+
Boolean
|
|
239
|
+
);
|
|
240
|
+
let trimmed = [...sections];
|
|
241
|
+
for (const candidate of trimOrder) {
|
|
242
|
+
if (Math.ceil(trimmed.join('\n\n').length / 4) <= maxTokens) break;
|
|
243
|
+
trimmed = trimmed.filter((s) => s !== candidate);
|
|
244
|
+
}
|
|
245
|
+
fullPrompt = trimmed.join('\n\n');
|
|
246
|
+
}
|
|
185
247
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
248
|
+
// 8. Set the full prompt or push to context
|
|
249
|
+
if (useCustomPrompt) {
|
|
250
|
+
output.prompt = fullPrompt;
|
|
251
|
+
} else {
|
|
252
|
+
output.context.push(fullPrompt);
|
|
253
|
+
}
|
|
189
254
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
255
|
+
// 9. Store pre-compaction snapshot to KV (fire-and-forget)
|
|
256
|
+
if (useSnapshotToKV) {
|
|
257
|
+
storePreCompactionSnapshot(sessionId, {
|
|
258
|
+
timestamp: new Date().toISOString(),
|
|
259
|
+
sessionId,
|
|
260
|
+
planningState: planningState ? { raw: planningState } : undefined,
|
|
261
|
+
backgroundTasks: tasks.map((t) => ({
|
|
262
|
+
id: t.id,
|
|
263
|
+
description: t.description || 'No description',
|
|
264
|
+
status: t.status,
|
|
265
|
+
})),
|
|
266
|
+
branch,
|
|
267
|
+
}).catch(() => {}); // Fire and forget
|
|
268
|
+
}
|
|
203
269
|
},
|
|
204
270
|
};
|
|
205
271
|
}
|
|
@@ -165,7 +165,38 @@ export function createToolHooks(ctx: PluginInput, config: CoderConfig): ToolHook
|
|
|
165
165
|
}
|
|
166
166
|
},
|
|
167
167
|
|
|
168
|
-
async after(
|
|
168
|
+
async after(input: unknown, output: unknown): Promise<void> {
|
|
169
|
+
// Graceful handling for unavailable tools: if a tool execution produced an
|
|
170
|
+
// error indicating the tool doesn't exist or is unavailable, normalize the
|
|
171
|
+
// output to a helpful message so the session continues instead of crashing.
|
|
172
|
+
const toolName = extractToolName(input);
|
|
173
|
+
if (!toolName) return;
|
|
174
|
+
|
|
175
|
+
const out = output as {
|
|
176
|
+
output?: string;
|
|
177
|
+
title?: string;
|
|
178
|
+
metadata?: Record<string, unknown>;
|
|
179
|
+
};
|
|
180
|
+
if (typeof out.output !== 'string') return;
|
|
181
|
+
|
|
182
|
+
const lower = out.output.toLowerCase();
|
|
183
|
+
const isToolMissing =
|
|
184
|
+
(lower.includes('not found') ||
|
|
185
|
+
lower.includes('not available') ||
|
|
186
|
+
lower.includes('does not exist') ||
|
|
187
|
+
lower.includes('unknown tool') ||
|
|
188
|
+
lower.includes('no such tool')) &&
|
|
189
|
+
(lower.includes('tool') || lower.includes(toolName.toLowerCase()));
|
|
190
|
+
|
|
191
|
+
if (isToolMissing) {
|
|
192
|
+
out.output = JSON.stringify({
|
|
193
|
+
error: `Tool '${toolName}' is not available in this session. It may have been removed or is not installed. Please use an alternative approach or ask the user for guidance.`,
|
|
194
|
+
tool: toolName,
|
|
195
|
+
recoverable: true,
|
|
196
|
+
});
|
|
197
|
+
out.title = `Tool unavailable: ${toolName}`;
|
|
198
|
+
}
|
|
199
|
+
},
|
|
169
200
|
};
|
|
170
201
|
}
|
|
171
202
|
|
package/src/plugin/plugin.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { createKeywordHooks } from './hooks/keyword';
|
|
|
15
15
|
import { createParamsHooks } from './hooks/params';
|
|
16
16
|
import { createCadenceHooks } from './hooks/cadence';
|
|
17
17
|
import { createSessionMemoryHooks } from './hooks/session-memory';
|
|
18
|
+
import { createCompletionHooks } from './hooks/completion';
|
|
18
19
|
import type { AgentRole } from '../types';
|
|
19
20
|
import { BackgroundManager } from '../background';
|
|
20
21
|
import type { SessionTreeNode } from '../sqlite';
|
|
@@ -92,10 +93,15 @@ export async function createCoderPlugin(ctx: PluginInput): Promise<Hooks> {
|
|
|
92
93
|
const resolvedDbPath = resolveOpenCodeDBPath();
|
|
93
94
|
const dbReader = new OpenCodeDBReader(resolvedDbPath ? { dbPath: resolvedDbPath } : undefined);
|
|
94
95
|
|
|
96
|
+
// Shared Map: chat.params stores the user's message text per session,
|
|
97
|
+
// chat.message reads it for trigger detection (avoids scanning model output).
|
|
98
|
+
const lastUserMessages = new Map<string, string>();
|
|
99
|
+
|
|
95
100
|
const sessionHooks = createSessionHooks(ctx, coderConfig);
|
|
96
101
|
const toolHooks = createToolHooks(ctx, coderConfig);
|
|
97
102
|
const keywordHooks = createKeywordHooks(ctx, coderConfig);
|
|
98
103
|
const paramsHooks = createParamsHooks(ctx, coderConfig);
|
|
104
|
+
const completionHooks = createCompletionHooks(ctx, coderConfig);
|
|
99
105
|
const tmuxManager = coderConfig.tmux?.enabled
|
|
100
106
|
? new TmuxSessionManager(ctx, coderConfig.tmux, {
|
|
101
107
|
onLog: (message) =>
|
|
@@ -157,11 +163,22 @@ export async function createCoderPlugin(ctx: PluginInput): Promise<Hooks> {
|
|
|
157
163
|
});
|
|
158
164
|
|
|
159
165
|
// Create hooks that need backgroundManager for task reference injection during compaction
|
|
160
|
-
const cadenceHooks = createCadenceHooks(
|
|
166
|
+
const cadenceHooks = createCadenceHooks(
|
|
167
|
+
ctx,
|
|
168
|
+
coderConfig,
|
|
169
|
+
backgroundManager,
|
|
170
|
+
dbReader,
|
|
171
|
+
lastUserMessages
|
|
172
|
+
);
|
|
161
173
|
|
|
162
174
|
// Session memory hooks handle checkpointing and compaction for non-Cadence sessions
|
|
163
175
|
// Orchestration (deciding which module handles which session) happens below in the hooks
|
|
164
|
-
const sessionMemoryHooks = createSessionMemoryHooks(
|
|
176
|
+
const sessionMemoryHooks = createSessionMemoryHooks(
|
|
177
|
+
ctx,
|
|
178
|
+
coderConfig,
|
|
179
|
+
backgroundManager,
|
|
180
|
+
dbReader
|
|
181
|
+
);
|
|
165
182
|
|
|
166
183
|
const configHandler = createConfigHandler(coderConfig);
|
|
167
184
|
|
|
@@ -193,11 +210,15 @@ export async function createCoderPlugin(ctx: PluginInput): Promise<Hooks> {
|
|
|
193
210
|
...(tools ? { tool: tools } : {}),
|
|
194
211
|
config: configHandler,
|
|
195
212
|
'chat.message': async (input: unknown, output: unknown) => {
|
|
213
|
+
completionHooks.onMessage(input);
|
|
196
214
|
await keywordHooks.onMessage(input, output);
|
|
197
215
|
await sessionHooks.onMessage(input, output);
|
|
198
216
|
await cadenceHooks.onMessage(input, output);
|
|
199
217
|
},
|
|
200
|
-
'chat.params':
|
|
218
|
+
'chat.params': async (input: unknown, output: unknown) => {
|
|
219
|
+
completionHooks.onParams(input);
|
|
220
|
+
await paramsHooks.onParams(input, output);
|
|
221
|
+
},
|
|
201
222
|
'tool.execute.before': toolHooks.before,
|
|
202
223
|
'tool.execute.after': toolHooks.after,
|
|
203
224
|
'shell.env': async (_input: unknown, output: unknown) => {
|
|
@@ -230,16 +251,28 @@ export async function createCoderPlugin(ctx: PluginInput): Promise<Hooks> {
|
|
|
230
251
|
}
|
|
231
252
|
// Orchestrate: route to appropriate module based on session type
|
|
232
253
|
const sessionId = extractSessionIdFromEvent(input);
|
|
233
|
-
if (sessionId
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
)
|
|
254
|
+
if (sessionId) {
|
|
255
|
+
// Try lazy restore from KV if not in memory (survives plugin restarts)
|
|
256
|
+
if (!cadenceHooks.isActiveCadenceSession(sessionId)) {
|
|
257
|
+
await cadenceHooks.tryRestoreFromKV(sessionId);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (cadenceHooks.isActiveCadenceSession(sessionId)) {
|
|
261
|
+
await cadenceHooks.onEvent(input);
|
|
262
|
+
} else {
|
|
263
|
+
// Non-Cadence sessions - handle session.compacted for checkpointing
|
|
264
|
+
await sessionMemoryHooks.onEvent(
|
|
265
|
+
input as { event: { type: string; properties?: Record<string, unknown> } }
|
|
266
|
+
);
|
|
267
|
+
}
|
|
240
268
|
}
|
|
241
269
|
},
|
|
242
270
|
'experimental.session.compacting': async (input, output) => {
|
|
271
|
+
// Try lazy restore from KV if not in memory (survives plugin restarts)
|
|
272
|
+
if (!cadenceHooks.isActiveCadenceSession(input.sessionID)) {
|
|
273
|
+
await cadenceHooks.tryRestoreFromKV(input.sessionID);
|
|
274
|
+
}
|
|
275
|
+
|
|
243
276
|
// Orchestrate: route to appropriate module based on session type
|
|
244
277
|
if (cadenceHooks.isActiveCadenceSession(input.sessionID)) {
|
|
245
278
|
await cadenceHooks.onCompacting(input, output);
|
|
@@ -318,6 +351,16 @@ function createConfigHandler(
|
|
|
318
351
|
};
|
|
319
352
|
}
|
|
320
353
|
|
|
354
|
+
// Compaction config: increase reserved token buffer to accommodate our enriched
|
|
355
|
+
// compaction prompts (planning state, image descriptions, tool summaries, diagnostics).
|
|
356
|
+
// Default OpenCode reserved buffer is too small for the context we inject.
|
|
357
|
+
const existingCompaction = (config.compaction ?? {}) as Record<string, unknown>;
|
|
358
|
+
const existingReserved = existingCompaction.reserved;
|
|
359
|
+
config.compaction = {
|
|
360
|
+
...existingCompaction,
|
|
361
|
+
reserved: typeof existingReserved === 'number' ? existingReserved : 40_000,
|
|
362
|
+
};
|
|
363
|
+
|
|
321
364
|
config.command = {
|
|
322
365
|
...(config.command as Record<string, CommandDefinition> | undefined),
|
|
323
366
|
...commands,
|
|
@@ -360,6 +403,7 @@ function createAgentConfigs(
|
|
|
360
403
|
...(agent.reasoningEffort ? { reasoningEffort: agent.reasoningEffort } : {}),
|
|
361
404
|
...(agent.thinking ? { thinking: agent.thinking } : {}),
|
|
362
405
|
...(agent.hidden ? { hidden: agent.hidden } : {}),
|
|
406
|
+
...(agent.fallbackModels?.length ? { fallbackModels: agent.fallbackModels } : {}),
|
|
363
407
|
};
|
|
364
408
|
}
|
|
365
409
|
|
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;
|