refactor(opencode): replace OpenCode config with agents markdown file
This commit is contained in:
parent
fdf46653fc
commit
054e601167
|
|
@ -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),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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"];
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
|
|
|
|||
Loading…
Reference in New Issue