@kodrunhq/opencode-autopilot 1.18.0 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -13
- package/assets/commands/oc-update-docs.md +1 -1
- package/package.json +1 -1
- package/src/agents/index.ts +0 -12
- package/src/agents/pipeline/index.ts +0 -4
- package/src/autonomy/completion.ts +52 -0
- package/src/autonomy/controller.ts +144 -0
- package/src/autonomy/index.ts +25 -0
- package/src/autonomy/injector.ts +49 -0
- package/src/autonomy/state.ts +91 -0
- package/src/autonomy/types.ts +30 -0
- package/src/autonomy/verification.ts +86 -0
- package/src/background/database.ts +170 -0
- package/src/background/executor.ts +174 -0
- package/src/background/index.ts +8 -0
- package/src/background/manager.ts +232 -0
- package/src/background/repository.ts +174 -0
- package/src/background/schema.ts +24 -0
- package/src/background/sdk-runner.ts +40 -0
- package/src/background/slot-manager.ts +41 -0
- package/src/background/state-machine.ts +19 -0
- package/src/context/budget.ts +45 -0
- package/src/context/compaction-handler.ts +58 -0
- package/src/context/discovery.ts +94 -0
- package/src/context/index.ts +14 -0
- package/src/context/injector.ts +119 -0
- package/src/context/types.ts +24 -0
- package/src/health/checks.ts +145 -2
- package/src/health/index.ts +7 -1
- package/src/health/runner.ts +6 -0
- package/src/index.ts +113 -6
- package/src/installer.ts +13 -0
- package/src/kernel/index.ts +6 -0
- package/src/kernel/migrations.ts +50 -0
- package/src/kernel/retry.ts +49 -0
- package/src/kernel/schema.ts +9 -1
- package/src/kernel/transaction.ts +40 -12
- package/src/logging/forensic-writer.ts +6 -2
- package/src/logging/index.ts +2 -0
- package/src/mcp/index.ts +34 -0
- package/src/mcp/manager.ts +206 -0
- package/src/mcp/scope-filter.ts +44 -0
- package/src/mcp/types.ts +38 -0
- package/src/orchestrator/arena.ts +7 -1
- package/src/orchestrator/fallback/event-handler.ts +12 -1
- package/src/orchestrator/handlers/challenge.ts +8 -1
- package/src/orchestrator/handlers/plan.ts +8 -1
- package/src/orchestrator/handlers/recon.ts +8 -1
- package/src/orchestrator/handlers/types.ts +2 -2
- package/src/orchestrator/lesson-memory.ts +6 -1
- package/src/orchestrator/orchestration-logger.ts +15 -3
- package/src/orchestrator/skill-injection.ts +7 -1
- package/src/orchestrator/state.ts +6 -1
- package/src/recovery/classifier.ts +127 -0
- package/src/recovery/event-handler.ts +263 -0
- package/src/recovery/index.ts +20 -0
- package/src/recovery/orchestrator.ts +180 -0
- package/src/recovery/persistence.ts +87 -0
- package/src/recovery/strategies.ts +107 -0
- package/src/recovery/types.ts +31 -0
- package/src/registry/model-groups.ts +2 -19
- package/src/registry/resolver.ts +38 -9
- package/src/review/agent-catalog.ts +83 -251
- package/src/review/agents/architecture-verifier.ts +41 -0
- package/src/review/agents/code-hygiene-auditor.ts +40 -0
- package/src/review/agents/correctness-auditor.ts +41 -0
- package/src/review/agents/frontend-auditor.ts +39 -0
- package/src/review/agents/index.ts +15 -42
- package/src/review/agents/language-idioms-auditor.ts +39 -0
- package/src/review/agents/security-auditor.ts +12 -8
- package/src/review/stack-gate.ts +2 -6
- package/src/routing/categories.ts +111 -0
- package/src/routing/classifier.ts +152 -0
- package/src/routing/engine.ts +89 -0
- package/src/routing/index.ts +4 -0
- package/src/routing/types.ts +14 -0
- package/src/skills/adaptive-injector.ts +34 -3
- package/src/skills/loader.ts +4 -0
- package/src/tools/background.ts +196 -0
- package/src/tools/delegate.ts +205 -0
- package/src/tools/loop.ts +94 -0
- package/src/tools/recover.ts +172 -0
- package/src/types/recovery.ts +10 -0
- package/src/ux/context-warnings.ts +81 -0
- package/src/ux/error-hints.ts +38 -0
- package/src/ux/index.ts +7 -0
- package/src/ux/notifications.ts +67 -0
- package/src/ux/progress.ts +77 -0
- package/src/ux/session-summary.ts +67 -0
- package/src/ux/task-status.ts +109 -0
- package/src/ux/types.ts +24 -0
- package/src/agents/db-specialist.ts +0 -295
- package/src/agents/devops.ts +0 -352
- package/src/agents/documenter.ts +0 -44
- package/src/agents/frontend-engineer.ts +0 -541
- package/src/agents/pipeline/oc-explorer.ts +0 -46
- package/src/agents/pipeline/oc-retrospector.ts +0 -42
- package/src/review/agents/auth-flow-verifier.ts +0 -47
- package/src/review/agents/concurrency-checker.ts +0 -47
- package/src/review/agents/dead-code-scanner.ts +0 -47
- package/src/review/agents/go-idioms-auditor.ts +0 -46
- package/src/review/agents/python-django-auditor.ts +0 -46
- package/src/review/agents/react-patterns-auditor.ts +0 -46
- package/src/review/agents/rust-safety-auditor.ts +0 -46
- package/src/review/agents/scope-intent-verifier.ts +0 -45
- package/src/review/agents/silent-failure-hunter.ts +0 -45
- package/src/review/agents/spec-checker.ts +0 -45
- package/src/review/agents/state-mgmt-auditor.ts +0 -46
- package/src/review/agents/type-soundness.ts +0 -46
- package/src/review/agents/wiring-inspector.ts +0 -46
package/src/skills/loader.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { readdir, readFile } from "node:fs/promises";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import { parse } from "yaml";
|
|
12
|
+
import { skillMcpConfigSchema } from "../mcp/types";
|
|
12
13
|
import { isEnoentError } from "../utils/fs-helpers";
|
|
13
14
|
|
|
14
15
|
export interface SkillFrontmatter {
|
|
@@ -16,6 +17,7 @@ export interface SkillFrontmatter {
|
|
|
16
17
|
readonly description: string;
|
|
17
18
|
readonly stacks: readonly string[];
|
|
18
19
|
readonly requires: readonly string[];
|
|
20
|
+
readonly mcp: import("../mcp/types").SkillMcpConfig | null;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
export interface LoadedSkill {
|
|
@@ -36,6 +38,7 @@ export function parseSkillFrontmatter(content: string): SkillFrontmatter | null
|
|
|
36
38
|
const parsed = parse(match[1]);
|
|
37
39
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
38
40
|
const fm = parsed as Record<string, unknown>;
|
|
41
|
+
const parsedMcp = fm.mcp === undefined ? null : skillMcpConfigSchema.safeParse(fm.mcp);
|
|
39
42
|
return {
|
|
40
43
|
name: typeof fm.name === "string" ? fm.name : "",
|
|
41
44
|
description: typeof fm.description === "string" ? fm.description : "",
|
|
@@ -45,6 +48,7 @@ export function parseSkillFrontmatter(content: string): SkillFrontmatter | null
|
|
|
45
48
|
requires: Array.isArray(fm.requires)
|
|
46
49
|
? fm.requires.filter((s): s is string => typeof s === "string")
|
|
47
50
|
: [],
|
|
51
|
+
mcp: parsedMcp?.success === true ? parsedMcp.data : null,
|
|
48
52
|
};
|
|
49
53
|
} catch {
|
|
50
54
|
return null;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import { tool } from "@opencode-ai/plugin";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { BackgroundManager } from "../background/manager";
|
|
5
|
+
import { type BackgroundSdkOperations, createSdkRunner } from "../background/sdk-runner";
|
|
6
|
+
import { openKernelDb } from "../kernel/database";
|
|
7
|
+
import type { TaskStatus } from "../types/background";
|
|
8
|
+
|
|
9
|
+
const backgroundActionSchema = z.enum(["spawn", "status", "list", "cancel", "result"]);
|
|
10
|
+
|
|
11
|
+
interface BackgroundToolOptions {
|
|
12
|
+
readonly sessionId?: string;
|
|
13
|
+
readonly taskId?: string;
|
|
14
|
+
readonly description?: string;
|
|
15
|
+
readonly category?: string;
|
|
16
|
+
readonly agent?: string;
|
|
17
|
+
readonly priority?: number;
|
|
18
|
+
readonly status?: TaskStatus;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let defaultManager: BackgroundManager | null = null;
|
|
22
|
+
let backgroundSdkOps: BackgroundSdkOperations | null = null;
|
|
23
|
+
|
|
24
|
+
export function setBackgroundSdkOperations(ops: BackgroundSdkOperations): void {
|
|
25
|
+
backgroundSdkOps = ops;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getDefaultManager(): BackgroundManager {
|
|
29
|
+
if (defaultManager) {
|
|
30
|
+
return defaultManager;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const runTask = backgroundSdkOps ? createSdkRunner(backgroundSdkOps) : undefined;
|
|
34
|
+
defaultManager = new BackgroundManager({ db: openKernelDb(), runTask });
|
|
35
|
+
return defaultManager;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createDisplayText(title: string, lines: readonly string[]): string {
|
|
39
|
+
return [title, ...lines].join("\n");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function backgroundCore(
|
|
43
|
+
action: z.infer<typeof backgroundActionSchema>,
|
|
44
|
+
options: BackgroundToolOptions = {},
|
|
45
|
+
db?: Database,
|
|
46
|
+
): Promise<string> {
|
|
47
|
+
const isTransientManager = db !== undefined;
|
|
48
|
+
const manager = isTransientManager ? new BackgroundManager({ db }) : getDefaultManager();
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
switch (action) {
|
|
52
|
+
case "spawn": {
|
|
53
|
+
if (!options.description) {
|
|
54
|
+
return JSON.stringify({ action: "error", message: "description required" });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const taskId = manager.spawn(options.sessionId ?? "unknown-session", options.description, {
|
|
58
|
+
category: options.category,
|
|
59
|
+
agent: options.agent,
|
|
60
|
+
priority: options.priority,
|
|
61
|
+
});
|
|
62
|
+
if (isTransientManager) {
|
|
63
|
+
await manager.waitForIdle();
|
|
64
|
+
}
|
|
65
|
+
const task = manager.getStatus(taskId);
|
|
66
|
+
return JSON.stringify({
|
|
67
|
+
action: "background_spawn",
|
|
68
|
+
task,
|
|
69
|
+
displayText: createDisplayText("Background task queued", [
|
|
70
|
+
`Task ID: ${taskId}`,
|
|
71
|
+
`Session: ${options.sessionId ?? "unknown-session"}`,
|
|
72
|
+
`Description: ${options.description}`,
|
|
73
|
+
]),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
case "status": {
|
|
78
|
+
if (!options.taskId) {
|
|
79
|
+
return JSON.stringify({ action: "error", message: "taskId required" });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const task = manager.getStatus(options.taskId);
|
|
83
|
+
if (!task) {
|
|
84
|
+
return JSON.stringify({
|
|
85
|
+
action: "error",
|
|
86
|
+
message: `Task '${options.taskId}' not found.`,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return JSON.stringify({
|
|
91
|
+
action: "background_status",
|
|
92
|
+
task,
|
|
93
|
+
displayText: createDisplayText("Background task status", [
|
|
94
|
+
`Task ID: ${task.id}`,
|
|
95
|
+
`Status: ${task.status}`,
|
|
96
|
+
`Description: ${task.description}`,
|
|
97
|
+
]),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
case "list": {
|
|
102
|
+
const tasks = manager.list(options.sessionId, options.status);
|
|
103
|
+
const taskLines =
|
|
104
|
+
tasks.length > 0
|
|
105
|
+
? tasks.map((task) => `${task.id} | ${task.status} | ${task.description}`)
|
|
106
|
+
: ["No background tasks found."];
|
|
107
|
+
return JSON.stringify({
|
|
108
|
+
action: "background_list",
|
|
109
|
+
tasks,
|
|
110
|
+
displayText: createDisplayText("Background tasks", taskLines),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
case "cancel": {
|
|
115
|
+
if (!options.taskId) {
|
|
116
|
+
return JSON.stringify({ action: "error", message: "taskId required" });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const cancelled = manager.cancel(options.taskId);
|
|
120
|
+
const task = manager.getStatus(options.taskId);
|
|
121
|
+
return JSON.stringify({
|
|
122
|
+
action: "background_cancel",
|
|
123
|
+
cancelled,
|
|
124
|
+
task,
|
|
125
|
+
displayText: createDisplayText("Background cancel", [
|
|
126
|
+
`Task ID: ${options.taskId}`,
|
|
127
|
+
cancelled ? "Cancelled." : "Task could not be cancelled.",
|
|
128
|
+
]),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
case "result": {
|
|
133
|
+
if (!options.taskId) {
|
|
134
|
+
return JSON.stringify({ action: "error", message: "taskId required" });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const result = manager.getResult(options.taskId);
|
|
138
|
+
if (!result) {
|
|
139
|
+
return JSON.stringify({
|
|
140
|
+
action: "error",
|
|
141
|
+
message: `Task '${options.taskId}' not found.`,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return JSON.stringify({
|
|
146
|
+
action: "background_result",
|
|
147
|
+
result,
|
|
148
|
+
displayText: createDisplayText("Background task result", [
|
|
149
|
+
`Task ID: ${options.taskId}`,
|
|
150
|
+
`Status: ${result.status}`,
|
|
151
|
+
`Result: ${result.result ?? "<none>"}`,
|
|
152
|
+
`Error: ${result.error ?? "<none>"}`,
|
|
153
|
+
]),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} finally {
|
|
158
|
+
if (isTransientManager && action === "spawn") {
|
|
159
|
+
await manager.dispose();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return JSON.stringify({ action: "error", message: `Unsupported action: ${action}` });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export const ocBackground = tool({
|
|
167
|
+
description:
|
|
168
|
+
"Manage background tasks. Actions: spawn, status, list, cancel, result. Returns JSON with displayText for presentation.",
|
|
169
|
+
args: {
|
|
170
|
+
action: backgroundActionSchema.describe("Background task action"),
|
|
171
|
+
sessionId: z.string().min(1).optional().describe("Session ID to scope tasks to"),
|
|
172
|
+
taskId: z.string().min(1).optional().describe("Background task ID"),
|
|
173
|
+
description: z.string().min(1).optional().describe("Background task description for spawn"),
|
|
174
|
+
category: z.string().min(1).optional().describe("Optional task category for spawn/list"),
|
|
175
|
+
agent: z.string().min(1).optional().describe("Optional agent hint for spawn"),
|
|
176
|
+
priority: z.number().int().min(0).max(100).optional().describe("Optional task priority"),
|
|
177
|
+
status: z
|
|
178
|
+
.enum(["pending", "running", "completed", "failed", "cancelled"])
|
|
179
|
+
.optional()
|
|
180
|
+
.describe("Optional status filter for list"),
|
|
181
|
+
},
|
|
182
|
+
async execute(
|
|
183
|
+
{ action, sessionId, taskId, description, category, agent, priority, status },
|
|
184
|
+
context,
|
|
185
|
+
) {
|
|
186
|
+
return backgroundCore(action, {
|
|
187
|
+
sessionId: sessionId ?? context.sessionID,
|
|
188
|
+
taskId,
|
|
189
|
+
description,
|
|
190
|
+
category,
|
|
191
|
+
agent,
|
|
192
|
+
priority,
|
|
193
|
+
status,
|
|
194
|
+
});
|
|
195
|
+
},
|
|
196
|
+
});
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import { tool } from "@opencode-ai/plugin";
|
|
3
|
+
import { BackgroundManager } from "../background/manager";
|
|
4
|
+
import { type BackgroundSdkOperations, createSdkRunner } from "../background/sdk-runner";
|
|
5
|
+
import { loadConfig } from "../config";
|
|
6
|
+
import { openKernelDb } from "../kernel/database";
|
|
7
|
+
import { makeRoutingDecision } from "../routing";
|
|
8
|
+
import { getCategoryDefinition } from "../routing/categories";
|
|
9
|
+
import { type Category, CategoryConfigSchema, CategorySchema } from "../types/routing";
|
|
10
|
+
|
|
11
|
+
function buildDisplayText(input: {
|
|
12
|
+
readonly task: string;
|
|
13
|
+
readonly category: Category;
|
|
14
|
+
readonly confidence: number;
|
|
15
|
+
readonly reasoning: string;
|
|
16
|
+
readonly modelGroup: string;
|
|
17
|
+
readonly skills: readonly string[];
|
|
18
|
+
readonly taskId?: string;
|
|
19
|
+
}): string {
|
|
20
|
+
const skillLine = input.skills.length > 0 ? input.skills.join(", ") : "none";
|
|
21
|
+
const lines = [
|
|
22
|
+
"Routing decision",
|
|
23
|
+
`Task: ${input.task}`,
|
|
24
|
+
`Category: ${input.category}`,
|
|
25
|
+
`Confidence: ${input.confidence.toFixed(2)}`,
|
|
26
|
+
`Model group: ${input.modelGroup}`,
|
|
27
|
+
`Skills: ${skillLine}`,
|
|
28
|
+
`Reasoning: ${input.reasoning}`,
|
|
29
|
+
];
|
|
30
|
+
if (input.taskId) {
|
|
31
|
+
lines.push(`Background task ID: ${input.taskId}`);
|
|
32
|
+
}
|
|
33
|
+
return lines.join("\n");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface DelegateCoreOptions {
|
|
37
|
+
readonly sessionId?: string;
|
|
38
|
+
readonly spawn?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface ResolvedRouting {
|
|
42
|
+
readonly category: Category;
|
|
43
|
+
readonly confidence: number;
|
|
44
|
+
readonly reasoning: string;
|
|
45
|
+
readonly modelGroup: string;
|
|
46
|
+
readonly agentId: string | undefined;
|
|
47
|
+
readonly skills: readonly string[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resolveRoutingDecision(
|
|
51
|
+
task: string,
|
|
52
|
+
category: string | undefined,
|
|
53
|
+
config: Awaited<ReturnType<typeof loadConfig>>,
|
|
54
|
+
): ResolvedRouting | null {
|
|
55
|
+
if (category !== undefined) {
|
|
56
|
+
const parsedCategory = CategorySchema.safeParse(category);
|
|
57
|
+
if (!parsedCategory.success) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const explicitDefinition = getCategoryDefinition(parsedCategory.data);
|
|
62
|
+
const override = config?.routing?.categories?.[parsedCategory.data];
|
|
63
|
+
const appliedConfig = CategoryConfigSchema.parse({
|
|
64
|
+
enabled: override?.enabled ?? true,
|
|
65
|
+
agentId: override?.agentId,
|
|
66
|
+
modelGroup: override?.modelGroup ?? explicitDefinition.modelGroup,
|
|
67
|
+
timeoutSeconds: override?.timeoutSeconds ?? explicitDefinition.timeoutSeconds,
|
|
68
|
+
skills: override?.skills ?? [...explicitDefinition.skills],
|
|
69
|
+
metadata: {
|
|
70
|
+
maxIterations: explicitDefinition.maxIterations,
|
|
71
|
+
...(override?.metadata ?? {}),
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
category: parsedCategory.data,
|
|
77
|
+
confidence: 1,
|
|
78
|
+
reasoning: `Category explicitly provided as '${parsedCategory.data}'.`,
|
|
79
|
+
modelGroup: appliedConfig?.modelGroup ?? explicitDefinition.modelGroup,
|
|
80
|
+
agentId: appliedConfig?.agentId,
|
|
81
|
+
skills: appliedConfig?.skills ?? explicitDefinition.skills,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const decision = makeRoutingDecision(task, config?.routing);
|
|
86
|
+
const definition = getCategoryDefinition(decision.category);
|
|
87
|
+
return {
|
|
88
|
+
category: decision.category,
|
|
89
|
+
confidence: decision.confidence,
|
|
90
|
+
reasoning: decision.reasoning ?? "No routing reasoning available.",
|
|
91
|
+
modelGroup: decision.appliedConfig?.modelGroup ?? definition.modelGroup,
|
|
92
|
+
agentId: decision.agentId,
|
|
93
|
+
skills: decision.appliedConfig?.skills ?? definition.skills,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let defaultDelegateManager: BackgroundManager | null = null;
|
|
98
|
+
let delegateSdkOps: BackgroundSdkOperations | null = null;
|
|
99
|
+
|
|
100
|
+
export function setDelegateSdkOperations(ops: BackgroundSdkOperations): void {
|
|
101
|
+
delegateSdkOps = ops;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getDelegateManager(db?: Database): BackgroundManager {
|
|
105
|
+
if (db) {
|
|
106
|
+
return new BackgroundManager({ db });
|
|
107
|
+
}
|
|
108
|
+
if (defaultDelegateManager) {
|
|
109
|
+
return defaultDelegateManager;
|
|
110
|
+
}
|
|
111
|
+
const runTask = delegateSdkOps ? createSdkRunner(delegateSdkOps) : undefined;
|
|
112
|
+
defaultDelegateManager = new BackgroundManager({ db: openKernelDb(), runTask });
|
|
113
|
+
return defaultDelegateManager;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function delegateCore(
|
|
117
|
+
task: string,
|
|
118
|
+
category?: string,
|
|
119
|
+
db?: Database,
|
|
120
|
+
options?: DelegateCoreOptions,
|
|
121
|
+
): Promise<string> {
|
|
122
|
+
if (task.trim().length === 0) {
|
|
123
|
+
return JSON.stringify({
|
|
124
|
+
action: "error",
|
|
125
|
+
message: "Task is required.",
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const config = await loadConfig();
|
|
130
|
+
|
|
131
|
+
if (category !== undefined) {
|
|
132
|
+
const parsedCategory = CategorySchema.safeParse(category);
|
|
133
|
+
if (!parsedCategory.success) {
|
|
134
|
+
return JSON.stringify({
|
|
135
|
+
action: "error",
|
|
136
|
+
message: `Invalid category '${category}'.`,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const routing = resolveRoutingDecision(task, category, config);
|
|
142
|
+
if (!routing) {
|
|
143
|
+
return JSON.stringify({
|
|
144
|
+
action: "error",
|
|
145
|
+
message: `Invalid category '${category}'.`,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const shouldSpawn = options?.spawn !== false;
|
|
150
|
+
let taskId: string | undefined;
|
|
151
|
+
|
|
152
|
+
if (shouldSpawn) {
|
|
153
|
+
const manager = getDelegateManager(db);
|
|
154
|
+
const sessionId = options?.sessionId ?? "delegate-session";
|
|
155
|
+
taskId = manager.spawn(sessionId, task, {
|
|
156
|
+
category: routing.category,
|
|
157
|
+
agent: routing.agentId,
|
|
158
|
+
model: routing.modelGroup,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (db) {
|
|
162
|
+
await manager.waitForIdle();
|
|
163
|
+
await manager.dispose();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return JSON.stringify({
|
|
168
|
+
action: shouldSpawn ? "delegated" : "routing_only",
|
|
169
|
+
category: routing.category,
|
|
170
|
+
confidence: routing.confidence,
|
|
171
|
+
reasoning: routing.reasoning,
|
|
172
|
+
suggestedModelGroup: routing.modelGroup,
|
|
173
|
+
suggestedSkills: routing.skills,
|
|
174
|
+
...(taskId ? { taskId } : {}),
|
|
175
|
+
displayText: buildDisplayText({
|
|
176
|
+
task,
|
|
177
|
+
category: routing.category,
|
|
178
|
+
confidence: routing.confidence,
|
|
179
|
+
reasoning: routing.reasoning,
|
|
180
|
+
modelGroup: routing.modelGroup,
|
|
181
|
+
skills: routing.skills,
|
|
182
|
+
taskId,
|
|
183
|
+
}),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export const ocDelegate = tool({
|
|
188
|
+
description:
|
|
189
|
+
"Route a task to the best category and spawn a background task with the routing decision. Returns routing info and a background task ID.",
|
|
190
|
+
args: {
|
|
191
|
+
task: tool.schema.string().min(1).max(4096).describe("Task description to route"),
|
|
192
|
+
category: CategorySchema.optional().describe("Optional explicit routing category override"),
|
|
193
|
+
spawn: tool.schema
|
|
194
|
+
.boolean()
|
|
195
|
+
.optional()
|
|
196
|
+
.default(true)
|
|
197
|
+
.describe("Whether to spawn a background task (default: true)"),
|
|
198
|
+
},
|
|
199
|
+
async execute(args, context) {
|
|
200
|
+
return delegateCore(args.task, args.category, undefined, {
|
|
201
|
+
sessionId: context.sessionID,
|
|
202
|
+
spawn: args.spawn,
|
|
203
|
+
});
|
|
204
|
+
},
|
|
205
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { getLoopController, type LoopController } from "../autonomy";
|
|
4
|
+
|
|
5
|
+
type LoopAction = "status" | "abort" | "start" | "pause" | "resume" | "iterate";
|
|
6
|
+
|
|
7
|
+
function buildDisplayText(action: LoopAction, controller: LoopController): string {
|
|
8
|
+
const context = controller.getStatus();
|
|
9
|
+
const lines = [
|
|
10
|
+
`Loop action: ${action}`,
|
|
11
|
+
`State: ${context.state}`,
|
|
12
|
+
`Task: ${context.taskDescription || "No active task"}`,
|
|
13
|
+
`Iteration: ${context.currentIteration}/${context.maxIterations}`,
|
|
14
|
+
`Contexts: ${context.accumulatedContext.length}`,
|
|
15
|
+
`Verification runs: ${context.verificationResults.length}`,
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
return lines.join("\n");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function loopCore(
|
|
22
|
+
action: LoopAction,
|
|
23
|
+
options?: {
|
|
24
|
+
readonly taskDescription?: string;
|
|
25
|
+
readonly maxIterations?: number;
|
|
26
|
+
readonly iterationResult?: string;
|
|
27
|
+
},
|
|
28
|
+
controller: LoopController = getLoopController(),
|
|
29
|
+
): Promise<string> {
|
|
30
|
+
switch (action) {
|
|
31
|
+
case "start": {
|
|
32
|
+
const description = options?.taskDescription ?? "Untitled task";
|
|
33
|
+
controller.start(description, {
|
|
34
|
+
maxIterations: options?.maxIterations,
|
|
35
|
+
});
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
case "iterate": {
|
|
40
|
+
const result = options?.iterationResult ?? "";
|
|
41
|
+
await controller.iterate(result);
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
case "pause": {
|
|
46
|
+
controller.pause();
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
case "resume": {
|
|
51
|
+
controller.resume();
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
case "abort": {
|
|
56
|
+
controller.abort();
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
case "status":
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return JSON.stringify({
|
|
65
|
+
action: `loop_${action}`,
|
|
66
|
+
context: controller.getStatus(),
|
|
67
|
+
displayText: buildDisplayText(action, controller),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const ocLoop = tool({
|
|
72
|
+
description:
|
|
73
|
+
"Manage the autonomy loop. Actions: start (begin new loop), iterate (advance with result), pause, resume, abort, status.",
|
|
74
|
+
args: {
|
|
75
|
+
action: z
|
|
76
|
+
.enum(["status", "abort", "start", "pause", "resume", "iterate"])
|
|
77
|
+
.describe("Loop action to perform"),
|
|
78
|
+
taskDescription: z.string().min(1).optional().describe("Task description for the start action"),
|
|
79
|
+
maxIterations: z
|
|
80
|
+
.number()
|
|
81
|
+
.int()
|
|
82
|
+
.min(1)
|
|
83
|
+
.max(100)
|
|
84
|
+
.optional()
|
|
85
|
+
.describe("Max iterations for the start action"),
|
|
86
|
+
iterationResult: z
|
|
87
|
+
.string()
|
|
88
|
+
.optional()
|
|
89
|
+
.describe("Result of the current iteration for the iterate action"),
|
|
90
|
+
},
|
|
91
|
+
async execute({ action, taskDescription, maxIterations, iterationResult }) {
|
|
92
|
+
return loopCore(action, { taskDescription, maxIterations, iterationResult });
|
|
93
|
+
},
|
|
94
|
+
});
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import { tool } from "@opencode-ai/plugin";
|
|
3
|
+
import { openKernelDb } from "../kernel/database";
|
|
4
|
+
import {
|
|
5
|
+
getDefaultRecoveryOrchestrator,
|
|
6
|
+
getStrategy,
|
|
7
|
+
type RecoveryOrchestrator,
|
|
8
|
+
} from "../recovery";
|
|
9
|
+
import { clearRecoveryState, loadRecoveryState } from "../recovery/persistence";
|
|
10
|
+
import type { RecoveryState } from "../recovery/types";
|
|
11
|
+
|
|
12
|
+
const recoverActionSchema = tool.schema.enum(["status", "retry", "reset", "history"]);
|
|
13
|
+
type RecoverToolAction = "status" | "retry" | "reset" | "history";
|
|
14
|
+
|
|
15
|
+
interface RecoverToolOptions {
|
|
16
|
+
readonly sessionId?: string;
|
|
17
|
+
readonly orchestrator?: RecoveryOrchestrator;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function createDisplayText(title: string, lines: readonly string[]): string {
|
|
21
|
+
return [title, ...lines].join("\n");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getOrchestrator(options?: RecoverToolOptions): RecoveryOrchestrator {
|
|
25
|
+
return options?.orchestrator ?? getDefaultRecoveryOrchestrator();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getState(
|
|
29
|
+
orchestrator: RecoveryOrchestrator,
|
|
30
|
+
sessionId: string,
|
|
31
|
+
db?: Database,
|
|
32
|
+
): RecoveryState | null {
|
|
33
|
+
const inMemoryState = orchestrator.getState(sessionId);
|
|
34
|
+
if (inMemoryState) {
|
|
35
|
+
return inMemoryState;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return db ? loadRecoveryState(db, sessionId) : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function recoverCore(
|
|
42
|
+
action: RecoverToolAction,
|
|
43
|
+
options: RecoverToolOptions = {},
|
|
44
|
+
db?: Database,
|
|
45
|
+
): Promise<string> {
|
|
46
|
+
const sessionId = options.sessionId;
|
|
47
|
+
if (!sessionId) {
|
|
48
|
+
return JSON.stringify({ action: "error", message: "sessionId required" });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const orchestrator = getOrchestrator(options);
|
|
52
|
+
const state = getState(orchestrator, sessionId, db);
|
|
53
|
+
|
|
54
|
+
switch (action) {
|
|
55
|
+
case "status": {
|
|
56
|
+
if (!state) {
|
|
57
|
+
return JSON.stringify({
|
|
58
|
+
action: "recovery_status",
|
|
59
|
+
sessionId,
|
|
60
|
+
state: null,
|
|
61
|
+
displayText: createDisplayText("Recovery status", [
|
|
62
|
+
`Session: ${sessionId}`,
|
|
63
|
+
"No recovery state found.",
|
|
64
|
+
]),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return JSON.stringify({
|
|
69
|
+
action: "recovery_status",
|
|
70
|
+
sessionId,
|
|
71
|
+
state,
|
|
72
|
+
displayText: createDisplayText("Recovery status", [
|
|
73
|
+
`Session: ${sessionId}`,
|
|
74
|
+
`Attempts: ${state.attempts.length}/${state.maxAttempts}`,
|
|
75
|
+
`Recovering: ${state.isRecovering ? "yes" : "no"}`,
|
|
76
|
+
`Current strategy: ${state.currentStrategy ?? "<none>"}`,
|
|
77
|
+
`Last error: ${state.lastError ?? "<none>"}`,
|
|
78
|
+
]),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
case "history": {
|
|
83
|
+
const history = state?.attempts ?? [];
|
|
84
|
+
return JSON.stringify({
|
|
85
|
+
action: "recovery_history",
|
|
86
|
+
sessionId,
|
|
87
|
+
history,
|
|
88
|
+
displayText: createDisplayText(
|
|
89
|
+
"Recovery history",
|
|
90
|
+
history.length > 0
|
|
91
|
+
? history.map(
|
|
92
|
+
(attempt) =>
|
|
93
|
+
`#${attempt.attemptNumber} ${attempt.errorCategory} -> ${attempt.strategy} (${attempt.success ? "success" : "pending"})`,
|
|
94
|
+
)
|
|
95
|
+
: [`Session: ${sessionId}`, "No recovery attempts found."],
|
|
96
|
+
),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
case "reset": {
|
|
101
|
+
orchestrator.reset(sessionId);
|
|
102
|
+
if (db) {
|
|
103
|
+
clearRecoveryState(db, sessionId);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return JSON.stringify({
|
|
107
|
+
action: "recovery_reset",
|
|
108
|
+
sessionId,
|
|
109
|
+
ok: true,
|
|
110
|
+
displayText: createDisplayText("Recovery reset", [
|
|
111
|
+
`Session: ${sessionId}`,
|
|
112
|
+
"Recovery state cleared.",
|
|
113
|
+
]),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
case "retry": {
|
|
118
|
+
if (!state || state.attempts.length === 0) {
|
|
119
|
+
return JSON.stringify({
|
|
120
|
+
action: "error",
|
|
121
|
+
message: "No recovery history available for retry",
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const lastAttempt = state.attempts[state.attempts.length - 1];
|
|
126
|
+
orchestrator.reset(sessionId);
|
|
127
|
+
if (db) {
|
|
128
|
+
clearRecoveryState(db, sessionId);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const nextAction = getStrategy(lastAttempt.errorCategory)({
|
|
132
|
+
sessionId,
|
|
133
|
+
attempts: Object.freeze([]),
|
|
134
|
+
currentStrategy: null,
|
|
135
|
+
maxAttempts: state.maxAttempts,
|
|
136
|
+
isRecovering: false,
|
|
137
|
+
lastError: state.lastError,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return JSON.stringify({
|
|
141
|
+
action: "recovery_retry",
|
|
142
|
+
sessionId,
|
|
143
|
+
nextAction,
|
|
144
|
+
displayText: createDisplayText("Recovery retry", [
|
|
145
|
+
`Session: ${sessionId}`,
|
|
146
|
+
`Strategy: ${nextAction.strategy}`,
|
|
147
|
+
`Category: ${nextAction.errorCategory}`,
|
|
148
|
+
`Backoff: ${nextAction.backoffMs}ms`,
|
|
149
|
+
]),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return JSON.stringify({ action: "error", message: `Unsupported action: ${action}` });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export const ocRecover = tool({
|
|
158
|
+
description:
|
|
159
|
+
"Inspect and manage session recovery state. Actions: status, retry, reset, history. Returns JSON with displayText.",
|
|
160
|
+
args: {
|
|
161
|
+
action: recoverActionSchema.describe("Recovery action"),
|
|
162
|
+
sessionId: tool.schema.string().min(1).optional().describe("Session ID to inspect"),
|
|
163
|
+
},
|
|
164
|
+
async execute({ action, sessionId }, context) {
|
|
165
|
+
const db = openKernelDb();
|
|
166
|
+
try {
|
|
167
|
+
return await recoverCore(action, { sessionId: sessionId ?? context.sessionID }, db);
|
|
168
|
+
} finally {
|
|
169
|
+
db.close();
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
});
|