refactor(opencode): replace OpenCode config with agents markdown file

This commit is contained in:
Gautam Tayal 2026-04-11 18:20:33 +05:30
parent fdf46653fc
commit 054e601167
7 changed files with 61 additions and 90 deletions

View File

@ -5,7 +5,7 @@ import {
} from "node:fs";
import { join } from "node:path";
import { createSessionManager } from "../../session-manager.js";
import { getWorkspaceOpenCodeConfigPath } from "../../opencode-config.js";
import { getWorkspaceAgentsMdPath } from "../../opencode-agents-md.js";
import {
writeMetadata,
readMetadataRaw,
@ -475,10 +475,10 @@ describe("restore", () => {
expect(createCall.launchCommand).toBe("claude --resume abc123");
});
it("passes OPENCODE_CONFIG when restoring OpenCode orchestrators", async () => {
it("does not need extra OpenCode config when restoring OpenCode orchestrators", async () => {
const wsPath = join(tmpDir, "ws-app-orchestrator-opencode-restore");
mkdirSync(join(wsPath, ".ao"), { recursive: true });
writeFileSync(getWorkspaceOpenCodeConfigPath(wsPath), "{}\n", "utf-8");
mkdirSync(wsPath, { recursive: true });
writeFileSync(getWorkspaceAgentsMdPath(wsPath), "## Agent Orchestrator\n", "utf-8");
const mockOpenCodeAgentWithRestore: Agent = {
...mockAgent,
@ -537,8 +537,8 @@ describe("restore", () => {
expect(mockRuntime.create).toHaveBeenCalledWith(
expect.objectContaining({
environment: expect.objectContaining({
OPENCODE_CONFIG: getWorkspaceOpenCodeConfigPath(wsPath),
environment: expect.not.objectContaining({
OPENCODE_CONFIG: expect.any(String),
}),
}),
);

View File

@ -3,7 +3,7 @@ import { mkdirSync, readFileSync, existsSync } from "node:fs";
import { join } from "node:path";
import { createSessionManager } from "../../session-manager.js";
import { validateConfig } from "../../config.js";
import { OPENCODE_INTERNAL_ORCHESTRATOR_AGENT_NAME, getWorkspaceOpenCodeConfigPath } from "../../opencode-config.js";
import { getWorkspaceAgentsMdPath } from "../../opencode-agents-md.js";
import {
writeMetadata,
readMetadata,
@ -1710,7 +1710,7 @@ describe("spawn", () => {
expect(readFileSync(promptFile, "utf-8")).toBe("You are the orchestrator.");
});
it("generates .ao/opencode.json and passes OPENCODE_CONFIG for OpenCode orchestrators", async () => {
it("writes workspace AGENTS.md for OpenCode orchestrators", async () => {
const opencodeAgent: Agent = {
...mockAgent,
name: "opencode",
@ -1746,33 +1746,15 @@ describe("spawn", () => {
systemPrompt: "You are the orchestrator.",
});
const opencodeConfigPath = getWorkspaceOpenCodeConfigPath("/tmp/ws");
expect(existsSync(opencodeConfigPath)).toBe(true);
expect(JSON.parse(readFileSync(opencodeConfigPath, "utf-8"))).toEqual({
$schema: "https://opencode.ai/config.json",
default_agent: OPENCODE_INTERNAL_ORCHESTRATOR_AGENT_NAME,
agent: {
[OPENCODE_INTERNAL_ORCHESTRATOR_AGENT_NAME]: {
description: "Agent Orchestrator internal orchestrator agent",
mode: "primary",
prompt: expect.stringContaining("orchestrator-prompt-app-orchestrator-1.md"),
},
},
});
const agentsMdPath = getWorkspaceAgentsMdPath("/tmp/ws");
expect(existsSync(agentsMdPath)).toBe(true);
expect(readFileSync(agentsMdPath, "utf-8")).toContain("You are the orchestrator.");
expect(opencodeAgent.getLaunchCommand).toHaveBeenCalledWith(
expect.objectContaining({
systemPromptFile: expect.stringContaining("orchestrator-prompt-app-orchestrator-1.md"),
}),
);
expect(mockRuntime.create).toHaveBeenCalledWith(
expect.objectContaining({
environment: expect.objectContaining({
OPENCODE_CONFIG: opencodeConfigPath,
}),
}),
);
});
it("throws for unknown project", async () => {

View File

@ -102,10 +102,9 @@ export {
} from "./scm-webhook-utils.js";
export { asValidOpenCodeSessionId } from "./opencode-session-id.js";
export {
OPENCODE_INTERNAL_ORCHESTRATOR_AGENT_NAME,
getWorkspaceOpenCodeConfigPath,
writeWorkspaceOpenCodeConfig,
} from "./opencode-config.js";
getWorkspaceAgentsMdPath,
writeWorkspaceOpenCodeAgentsMd,
} from "./opencode-agents-md.js";
export { normalizeOrchestratorSessionStrategy } from "./orchestrator-session-strategy.js";
// Activity log — JSONL activity tracking for agents without native JSONL

View File

@ -0,0 +1,43 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
const AO_OPENCODE_SECTION_START = "<!-- AO_OPENCODE_ORCHESTRATOR_PROMPT_START -->";
const AO_OPENCODE_SECTION_END = "<!-- AO_OPENCODE_ORCHESTRATOR_PROMPT_END -->";
export function getWorkspaceAgentsMdPath(workspacePath: string): string {
return join(workspacePath, "AGENTS.md");
}
function stripExistingAoOpenCodeSection(content: string): string {
const start = content.indexOf(AO_OPENCODE_SECTION_START);
const end = content.indexOf(AO_OPENCODE_SECTION_END);
if (start === -1 || end === -1 || end < start) {
return content;
}
const before = content.slice(0, start).trimEnd();
const after = content.slice(end + AO_OPENCODE_SECTION_END.length).trimStart();
if (before && after) return `${before}\n\n${after}`;
return before || after;
}
export function writeWorkspaceOpenCodeAgentsMd(workspacePath: string, promptFile: string): string {
const agentsMdPath = getWorkspaceAgentsMdPath(workspacePath);
mkdirSync(workspacePath, { recursive: true });
const existing = existsSync(agentsMdPath) ? readFileSync(agentsMdPath, "utf-8") : "";
const baseContent = stripExistingAoOpenCodeSection(existing).trimEnd();
const prompt = readFileSync(promptFile, "utf-8").trim();
const aoSection = [
AO_OPENCODE_SECTION_START,
"## Agent Orchestrator",
"",
prompt,
AO_OPENCODE_SECTION_END,
].join("\n");
const nextContent = baseContent ? `${baseContent}\n\n${aoSection}\n` : `${aoSection}\n`;
writeFileSync(agentsMdPath, nextContent, "utf-8");
return agentsMdPath;
}

View File

@ -1,29 +0,0 @@
import { mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
export const OPENCODE_INTERNAL_ORCHESTRATOR_AGENT_NAME = "__ao_orchestrator_internal";
export function getWorkspaceOpenCodeConfigPath(workspacePath: string): string {
return join(workspacePath, ".ao", "opencode.json");
}
export function writeWorkspaceOpenCodeConfig(workspacePath: string, promptFile: string): string {
const configPath = getWorkspaceOpenCodeConfigPath(workspacePath);
const configDir = join(workspacePath, ".ao");
mkdirSync(configDir, { recursive: true });
const config = {
$schema: "https://opencode.ai/config.json",
default_agent: OPENCODE_INTERNAL_ORCHESTRATOR_AGENT_NAME,
agent: {
[OPENCODE_INTERNAL_ORCHESTRATOR_AGENT_NAME]: {
description: "Agent Orchestrator internal orchestrator agent",
mode: "primary",
prompt: `{file:${promptFile}}`,
},
},
};
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
return configPath;
}

View File

@ -63,9 +63,8 @@ import {
} from "./paths.js";
import { asValidOpenCodeSessionId } from "./opencode-session-id.js";
import {
getWorkspaceOpenCodeConfigPath,
writeWorkspaceOpenCodeConfig,
} from "./opencode-config.js";
writeWorkspaceOpenCodeAgentsMd,
} from "./opencode-agents-md.js";
import { normalizeOrchestratorSessionStrategy } from "./orchestrator-session-strategy.js";
import { sessionFromMetadata } from "./utils/session-from-metadata.js";
import { safeJsonParse } from "./utils/validation.js";
@ -75,12 +74,6 @@ const execFileAsync = promisify(execFile);
const OPENCODE_DISCOVERY_TIMEOUT_MS = 10_000;
const OPENCODE_INTERACTIVE_DISCOVERY_TIMEOUT_MS = 10_000;
function getExistingWorkspaceOpenCodeConfigPath(workspacePath: string | null | undefined): string | undefined {
if (!workspacePath) return undefined;
const configPath = getWorkspaceOpenCodeConfigPath(workspacePath);
return existsSync(configPath) ? configPath : undefined;
}
function errorIncludesSessionNotFound(err: unknown): boolean {
if (!(err instanceof Error)) return false;
const e = err as Error & { stderr?: string; stdout?: string };
@ -1386,10 +1379,9 @@ export function createSessionManager(deps: SessionManagerDeps): OpenCodeSessionM
}
}
let openCodeConfigPath: string | undefined;
if (plugins.agent.name === "opencode" && systemPromptFile) {
try {
openCodeConfigPath = writeWorkspaceOpenCodeConfig(workspacePath, systemPromptFile);
writeWorkspaceOpenCodeAgentsMd(workspacePath, systemPromptFile);
} catch (err) {
await cleanupWorktreeAndMetadata(systemPromptFile);
throw err;
@ -1439,9 +1431,6 @@ export function createSessionManager(deps: SessionManagerDeps): OpenCodeSessionM
const launchCommand = plugins.agent.getLaunchCommand(agentLaunchConfig);
const environment = plugins.agent.getEnvironment(agentLaunchConfig);
if (openCodeConfigPath) {
environment["OPENCODE_CONFIG"] = openCodeConfigPath;
}
// Create runtime — clean up worktree and metadata on failure
let handle: RuntimeHandle;
@ -2421,10 +2410,6 @@ export function createSessionManager(deps: SessionManagerDeps): OpenCodeSessionM
// 7. Get launch command — try restore command first, fall back to fresh launch
let launchCommand: string;
const openCodeConfigPath =
selectedAgent === "opencode"
? getExistingWorkspaceOpenCodeConfigPath(workspacePath)
: undefined;
const projectConfigForLaunch: ProjectConfig = {
...project,
agentConfig: {
@ -2452,9 +2437,6 @@ export function createSessionManager(deps: SessionManagerDeps): OpenCodeSessionM
}
const environment = plugins.agent.getEnvironment(agentLaunchConfig);
if (openCodeConfigPath) {
environment["OPENCODE_CONFIG"] = openCodeConfigPath;
}
// 8. Create runtime (reuse tmuxName from metadata)
const tmuxName = raw["tmuxName"];

View File

@ -1,5 +1,4 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { mkdirSync, writeFileSync } from "node:fs";
import { type Session, type RuntimeHandle, type AgentLaunchConfig } from "@aoagents/ao-core";
const { mockAppendActivityEntry, mockReadLastActivityEntry, mockRecordTerminalActivity } =
@ -756,14 +755,9 @@ describe("getRestoreCommand", () => {
expect(cmd).toContain("--model 'claude-sonnet-4-5-20250929'");
});
it("does not inject an internal agent when workspace opencode.json exists", async () => {
const workspacePath = "/tmp/test-restore-opencode-config";
mkdirSync(`${workspacePath}/.ao`, { recursive: true });
writeFileSync(`${workspacePath}/.ao/opencode.json`, "{}\n", "utf-8");
it("does not inject an internal agent during restore", async () => {
const cmd = await agent.getRestoreCommand!(
makeSession({
workspacePath,
metadata: { opencodeSessionId: "ses_abc123" },
}),
{ name: "proj", repo: "o/r", path: "/p", defaultBranch: "main", sessionPrefix: "p" },