diff --git a/apps/array/src/main/services/agent/schemas.ts b/apps/array/src/main/services/agent/schemas.ts index 0e5d29209..d9083aecc 100644 --- a/apps/array/src/main/services/agent/schemas.ts +++ b/apps/array/src/main/services/agent/schemas.ts @@ -9,10 +9,6 @@ export const credentialsSchema = z.object({ export type Credentials = z.infer; -// Agent framework schema -export const agentFrameworkSchema = z.enum(["claude", "codex"]); -export type AgentFramework = z.infer; - // Execution mode schema export const executionModeSchema = z.enum(["plan", "acceptEdits", "default"]); export type ExecutionMode = z.infer; @@ -26,7 +22,6 @@ export const sessionConfigSchema = z.object({ logUrl: z.string().optional(), sdkSessionId: z.string().optional(), model: z.string().optional(), - framework: agentFrameworkSchema.optional(), executionMode: executionModeSchema.optional(), }); @@ -44,7 +39,6 @@ export const startSessionInput = z.object({ permissionMode: z.string().optional(), autoProgress: z.boolean().optional(), model: z.string().optional(), - framework: agentFrameworkSchema.optional().default("claude"), executionMode: z.enum(["plan", "acceptEdits", "default"]).optional(), runMode: z.enum(["local", "cloud"]).optional(), createPR: z.boolean().optional(), diff --git a/apps/array/src/main/services/agent/service.ts b/apps/array/src/main/services/agent/service.ts index 3f020da1a..8390b3cd8 100644 --- a/apps/array/src/main/services/agent/service.ts +++ b/apps/array/src/main/services/agent/service.ts @@ -129,7 +129,6 @@ interface SessionConfig { logUrl?: string; sdkSessionId?: string; model?: string; - framework?: "claude" | "codex"; executionMode?: "plan" | "acceptEdits" | "default"; } @@ -318,7 +317,6 @@ export class AgentService extends TypedEventEmitter { logUrl, sdkSessionId, model, - framework, executionMode, } = config; @@ -345,7 +343,6 @@ export class AgentService extends TypedEventEmitter { try { const { clientStreams } = await agent.runTaskV2(taskId, taskRunId, { skipGitBranch: true, - framework, isReconnect, }); @@ -790,7 +787,6 @@ export class AgentService extends TypedEventEmitter { logUrl: "logUrl" in params ? params.logUrl : undefined, sdkSessionId: "sdkSessionId" in params ? params.sdkSessionId : undefined, model: "model" in params ? params.model : undefined, - framework: "framework" in params ? params.framework : "claude", executionMode: "executionMode" in params ? params.executionMode : undefined, }; diff --git a/apps/array/src/renderer/features/sessions/components/FrameworkSelector.tsx b/apps/array/src/renderer/features/sessions/components/FrameworkSelector.tsx deleted file mode 100644 index cd92ee4f5..000000000 --- a/apps/array/src/renderer/features/sessions/components/FrameworkSelector.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { useSettingsStore } from "@features/settings/stores/settingsStore"; -import { Select, Text } from "@radix-ui/themes"; -import { - type AgentFramework, - AVAILABLE_FRAMEWORKS, -} from "@shared/types/models"; -import { useSessionForTask } from "../stores/sessionStore"; - -interface FrameworkSelectorProps { - taskId?: string; - disabled?: boolean; - onFrameworkChange?: (framework: AgentFramework) => void; -} - -export function FrameworkSelector({ - taskId, - disabled, - onFrameworkChange, -}: FrameworkSelectorProps) { - const defaultFramework = useSettingsStore((state) => state.defaultFramework); - const setDefaultFramework = useSettingsStore( - (state) => state.setDefaultFramework, - ); - const session = useSessionForTask(taskId); - - // Use session framework if available, otherwise fall back to default - const activeFramework = session?.framework ?? defaultFramework; - - // Disable if there's an active session (can't change framework mid-session) - const isDisabled = disabled || session?.status === "connected"; - - const handleChange = (value: string) => { - const framework = value as AgentFramework; - setDefaultFramework(framework); - onFrameworkChange?.(framework); - }; - - const currentFramework = AVAILABLE_FRAMEWORKS.find( - (f) => f.id === activeFramework, - ); - const displayName = currentFramework?.name ?? activeFramework; - - return ( - - - - {displayName} - - - - {AVAILABLE_FRAMEWORKS.filter((f) => f.enabled).map((framework) => ( - - {framework.name} - - ))} - - - ); -} diff --git a/apps/array/src/renderer/features/sessions/stores/sessionStore.ts b/apps/array/src/renderer/features/sessions/stores/sessionStore.ts index 52a947986..e45c5a6f3 100644 --- a/apps/array/src/renderer/features/sessions/stores/sessionStore.ts +++ b/apps/array/src/renderer/features/sessions/stores/sessionStore.ts @@ -51,7 +51,7 @@ export interface AgentSession { logUrl?: string; processedLineCount?: number; model?: string; - framework?: "claude" | "codex"; + framework?: "claude"; // Current execution mode (plan = read-only, default = manual approve, acceptEdits = auto-approve edits) currentMode: ExecutionMode; // Permission requests waiting for user response @@ -416,7 +416,6 @@ function createBaseSession( taskRunId: string, taskId: string, isCloud: boolean, - framework?: "claude" | "codex", executionMode?: "plan" | "acceptEdits", ): AgentSession { return { @@ -428,7 +427,6 @@ function createBaseSession( status: "connecting", isPromptPending: false, isCloud, - framework, currentMode: executionMode ?? "default", pendingPermissions: new Map(), }; @@ -667,7 +665,7 @@ const useStore = create()( const persistedMode = getPersistedTaskMode(taskId); const effectiveMode = executionMode ?? persistedMode; - const { defaultModel, defaultFramework } = useSettingsStore.getState(); + const { defaultModel } = useSettingsStore.getState(); const result = await trpcVanilla.agent.start.mutate({ taskId, taskRunId: taskRun.id, @@ -676,7 +674,6 @@ const useStore = create()( apiHost: auth.apiHost, projectId: auth.projectId, model: defaultModel, - framework: defaultFramework, executionMode: effectiveMode, }); @@ -684,7 +681,6 @@ const useStore = create()( taskRun.id, taskId, false, - defaultFramework, effectiveMode === "default" ? undefined : effectiveMode, ); session.channel = result.channel; @@ -701,7 +697,6 @@ const useStore = create()( task_id: taskId, execution_type: "local", model: defaultModel, - framework: defaultFramework, }); if (initialPrompt?.length) { diff --git a/apps/array/src/renderer/features/settings/stores/settingsStore.ts b/apps/array/src/renderer/features/settings/stores/settingsStore.ts index 70bc47b7c..05b5a07af 100644 --- a/apps/array/src/renderer/features/settings/stores/settingsStore.ts +++ b/apps/array/src/renderer/features/settings/stores/settingsStore.ts @@ -1,9 +1,5 @@ import type { WorkspaceMode } from "@shared/types"; -import { - type AgentFramework, - DEFAULT_FRAMEWORK, - DEFAULT_MODEL, -} from "@shared/types/models"; +import { DEFAULT_MODEL } from "@shared/types/models"; import { create } from "zustand"; import { persist } from "zustand/middleware"; @@ -18,7 +14,6 @@ interface SettingsStore { lastUsedWorkspaceMode: WorkspaceMode; createPR: boolean; defaultModel: string; - defaultFramework: AgentFramework; desktopNotifications: boolean; setAutoRunTasks: (autoRun: boolean) => void; @@ -28,7 +23,6 @@ interface SettingsStore { setLastUsedWorkspaceMode: (mode: WorkspaceMode) => void; setCreatePR: (createPR: boolean) => void; setDefaultModel: (model: string) => void; - setDefaultFramework: (framework: AgentFramework) => void; setDesktopNotifications: (enabled: boolean) => void; } @@ -42,7 +36,6 @@ export const useSettingsStore = create()( lastUsedWorkspaceMode: "worktree", createPR: true, defaultModel: DEFAULT_MODEL, - defaultFramework: DEFAULT_FRAMEWORK, desktopNotifications: true, setAutoRunTasks: (autoRun) => set({ autoRunTasks: autoRun }), @@ -53,7 +46,6 @@ export const useSettingsStore = create()( setLastUsedWorkspaceMode: (mode) => set({ lastUsedWorkspaceMode: mode }), setCreatePR: (createPR) => set({ createPR }), setDefaultModel: (model) => set({ defaultModel: model }), - setDefaultFramework: (framework) => set({ defaultFramework: framework }), setDesktopNotifications: (enabled) => set({ desktopNotifications: enabled }), }), diff --git a/apps/array/src/renderer/features/task-detail/components/TaskInputEditor.tsx b/apps/array/src/renderer/features/task-detail/components/TaskInputEditor.tsx index 3157b08e6..fe864c256 100644 --- a/apps/array/src/renderer/features/task-detail/components/TaskInputEditor.tsx +++ b/apps/array/src/renderer/features/task-detail/components/TaskInputEditor.tsx @@ -3,7 +3,6 @@ import { EditorToolbar } from "@features/message-editor/components/EditorToolbar import type { MessageEditorHandle } from "@features/message-editor/components/MessageEditor"; import { ModeIndicatorInput } from "@features/message-editor/components/ModeIndicatorInput"; import { useTiptapEditor } from "@features/message-editor/tiptap/useTiptapEditor"; -import { FrameworkSelector } from "@features/sessions/components/FrameworkSelector"; import type { ExecutionMode } from "@features/sessions/stores/sessionStore"; import { ArrowUp, GitBranchIcon } from "@phosphor-icons/react"; import { Box, Flex, IconButton, Text, Tooltip } from "@radix-ui/themes"; @@ -195,15 +194,12 @@ export const TaskInputEditor = forwardRef< - - - - + {!isCloudMode && ( diff --git a/apps/array/src/shared/types/models.ts b/apps/array/src/shared/types/models.ts index ceefc7c6e..7c1a74d52 100644 --- a/apps/array/src/shared/types/models.ts +++ b/apps/array/src/shared/types/models.ts @@ -66,29 +66,7 @@ export const AVAILABLE_MODELS: ModelOption[] = [ export const DEFAULT_MODEL = "claude-opus-4-5"; // Agent frameworks -export type AgentFramework = "claude" | "codex"; - -export interface FrameworkOption { - id: AgentFramework; - name: string; - description: string; - enabled: boolean; -} - -export const AVAILABLE_FRAMEWORKS: FrameworkOption[] = [ - { - id: "claude", - name: "Claude Code", - description: "Anthropic's Claude Code agent", - enabled: true, - }, - { - id: "codex", - name: "OpenAI Codex", - description: "OpenAI's Codex agent", - enabled: true, - }, -]; +export type AgentFramework = "claude"; export const DEFAULT_FRAMEWORK: AgentFramework = "claude"; diff --git a/apps/array/src/types/analytics.ts b/apps/array/src/types/analytics.ts index 26196b358..be99ffb52 100644 --- a/apps/array/src/types/analytics.ts +++ b/apps/array/src/types/analytics.ts @@ -14,7 +14,6 @@ export type GitActionType = | "create-pr"; export type FileOpenSource = "sidebar" | "agent-suggestion" | "search" | "diff"; export type FileChangeType = "added" | "modified" | "deleted"; -export type AgentFramework = "claude" | "codex"; export type StopReason = "user_cancelled" | "completed" | "error" | "timeout"; export type CommandMenuAction = | "home" @@ -62,7 +61,6 @@ export interface TaskRunStartedProperties { task_id: string; execution_type: ExecutionType; model?: string; - framework?: AgentFramework; } export interface TaskRunCompletedProperties { diff --git a/packages/agent/package.json b/packages/agent/package.json index ea9776ae3..af1a2cbe0 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@posthog/agent", - "version": "1.30.0", + "version": "2.0.0", "repository": "https://github.com/PostHog/array", "description": "TypeScript agent framework wrapping Claude Agent SDK with Git-based task execution for PostHog", "main": "./dist/index.js", @@ -45,7 +45,6 @@ "dependencies": { "@agentclientprotocol/sdk": "^0.5.1", "@anthropic-ai/claude-agent-sdk": "^0.1.55", - "@openai/codex-sdk": "0.60.1", "@anthropic-ai/sdk": "^0.71.0", "@modelcontextprotocol/sdk": "^1.23.0", "diff": "^8.0.2", diff --git a/packages/agent/src/adapters/claude/claude.ts b/packages/agent/src/adapters/claude/claude.ts index 5caebea63..191d9f0c3 100644 --- a/packages/agent/src/adapters/claude/claude.ts +++ b/packages/agent/src/adapters/claude/claude.ts @@ -1943,6 +1943,5 @@ export function streamEventToAcpNotifications( } // Note: createAcpConnection has been moved to ../connection.ts -// to support multiple agent frameworks (Claude, Codex). // Import from there instead: // import { createAcpConnection } from "../connection.js"; diff --git a/packages/agent/src/adapters/codex/codex.ts b/packages/agent/src/adapters/codex/codex.ts deleted file mode 100644 index 965afa7bc..000000000 --- a/packages/agent/src/adapters/codex/codex.ts +++ /dev/null @@ -1,788 +0,0 @@ -/** - * Codex ACP Agent - * - * Wraps the OpenAI Codex SDK to implement the ACP Agent interface, - * allowing Codex to be used as an alternative agent framework. - */ - -import { execSync } from "node:child_process"; -import { existsSync } from "node:fs"; -import { - type Agent, - type AgentSideConnection, - type AuthenticateRequest, - type CancelNotification, - type ClientCapabilities, - type InitializeRequest, - type InitializeResponse, - type LoadSessionRequest, - type LoadSessionResponse, - type NewSessionRequest, - type NewSessionResponse, - type PromptRequest, - type PromptResponse, - RequestError, - type SessionModelState, - type SessionNotification, - type SetSessionModelRequest, - type SetSessionModeRequest, - type SetSessionModeResponse, -} from "@agentclientprotocol/sdk"; -import { - Codex, - type CodexOptions, - type CommandExecutionItem, - type FileChangeItem, - type McpToolCallItem, - type Thread, - type ThreadEvent, - type ThreadItem, - type ThreadOptions, - type WebSearchItem, -} from "@openai/codex-sdk"; -import { v7 as uuidv7 } from "uuid"; -import type { - SessionPersistenceConfig, - SessionStore, -} from "@/session-store.js"; -import { Logger } from "@/utils/logger.js"; -import packageJson from "../../../package.json" with { type: "json" }; - -/** - * Find the codex CLI binary path. - * Checks common locations and falls back to PATH lookup. - */ -function findCodexCliPath(): string | undefined { - // Common installation paths - const commonPaths = [ - "/opt/homebrew/bin/codex", // macOS Apple Silicon - "/usr/local/bin/codex", // macOS Intel / Linux - "/usr/bin/codex", // Linux system - ]; - - for (const path of commonPaths) { - if (existsSync(path)) { - return path; - } - } - - // Try to find via which command - try { - const whichResult = execSync("which codex", { - encoding: "utf-8", - timeout: 5000, - }).trim(); - if (whichResult && existsSync(whichResult)) { - return whichResult; - } - } catch { - // which command failed, codex not in PATH - } - - return undefined; -} - -type CodexSession = { - thread: Thread; - threadId?: string; - cancelled: boolean; - notificationHistory: SessionNotification[]; -}; - -export class CodexAcpAgent implements Agent { - private codex: Codex; - private sessions: Map; - private client: AgentSideConnection; - private clientCapabilities?: ClientCapabilities; - private logger: Logger; - private sessionStore?: SessionStore; - - constructor(client: AgentSideConnection, sessionStore?: SessionStore) { - const codexPath = findCodexCliPath(); - const codexOptions: CodexOptions = {}; - - if (codexPath) { - codexOptions.codexPathOverride = codexPath; - } - - let gatewayUrl = process.env.OPENAI_BASE_URL; - - if (!gatewayUrl && process.env.LLM_GATEWAY_URL) { - const baseUrl = process.env.LLM_GATEWAY_URL; - gatewayUrl = baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1`; - } - - const apiKey = - process.env.OPENAI_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN; - - if (gatewayUrl || apiKey) { - const env: Record = { - PATH: process.env.PATH || "", - HOME: process.env.HOME || "", - }; - - if (gatewayUrl) { - env.OPENAI_BASE_URL = gatewayUrl; - } - if (apiKey) { - env.OPENAI_API_KEY = apiKey; - } - - codexOptions.env = env; - } - - this.codex = new Codex(codexOptions); - this.sessions = new Map(); - this.client = client; - this.sessionStore = sessionStore; - this.logger = new Logger({ debug: true, prefix: "[CodexAcpAgent]" }); - - if (codexPath) { - this.logger.info("Using Codex CLI", { - path: codexPath, - gatewayUrl: gatewayUrl || "not set", - }); - } else { - this.logger.warn( - "Codex CLI not found. Install with: npm install -g @openai/codex", - ); - } - } - - async initialize(request: InitializeRequest): Promise { - this.clientCapabilities = request.clientCapabilities; - - return { - protocolVersion: 1, - agentCapabilities: { - promptCapabilities: { - image: true, - embeddedContext: false, - }, - mcpCapabilities: { - http: false, - sse: false, - }, - loadSession: true, - _meta: { - posthog: { - resumeSession: true, - }, - }, - }, - agentInfo: { - name: packageJson.name, - title: "OpenAI Codex", - version: packageJson.version, - }, - authMethods: [], - }; - } - - async newSession(params: NewSessionRequest): Promise { - const sessionId = - (params._meta as { sessionId?: string } | undefined)?.sessionId || - uuidv7(); - - const threadOptions: ThreadOptions = { - workingDirectory: params.cwd, - sandboxMode: "danger-full-access", - skipGitRepoCheck: true, - approvalPolicy: "never", - }; - - this.logger.info("Starting Codex thread", { - cwd: params.cwd, - options: threadOptions, - }); - - const thread = this.codex.startThread(threadOptions); - - const session: CodexSession = { - thread, - cancelled: false, - notificationHistory: [], - }; - - this.sessions.set(sessionId, session); - - const persistence = params._meta?.persistence as - | SessionPersistenceConfig - | undefined; - if (persistence && this.sessionStore) { - this.sessionStore.register(sessionId, persistence); - this.logger.info("Registered session for S3 persistence", { - sessionId, - taskId: persistence.taskId, - runId: persistence.runId, - }); - } - - this.logger.info("Created new Codex session", { sessionId }); - - const models: SessionModelState = { - availableModels: [ - { - modelId: "codex", - name: "OpenAI Codex", - description: "OpenAI Codex agent", - }, - ], - currentModelId: "codex", - }; - - const availableModes = [ - { - id: "default", - name: "Default", - description: "Standard approval mode", - }, - ]; - - return { - sessionId, - models, - modes: { - currentModeId: "default", - availableModes, - }, - }; - } - - async authenticate(_params: AuthenticateRequest): Promise { - throw new Error("Authentication not implemented for Codex"); - } - - async prompt(params: PromptRequest): Promise { - const session = this.sessions.get(params.sessionId); - if (!session) { - throw new Error("Session not found"); - } - - session.cancelled = false; - - const promptText = params.prompt - .filter((p) => p.type === "text") - .map((p) => (p as { type: "text"; text: string }).text) - .join("\n"); - - this.logger.info("Running Codex prompt", { - sessionId: params.sessionId, - promptLength: promptText.length, - promptPreview: promptText.substring(0, 100), - }); - - for (const chunk of params.prompt) { - const userNotification: SessionNotification = { - sessionId: params.sessionId, - update: { - sessionUpdate: "user_message_chunk", - content: chunk, - }, - }; - await this.client.sessionUpdate(userNotification); - this.appendNotification(params.sessionId, userNotification); - } - - try { - const streamedTurn = await session.thread.runStreamed(promptText); - - for await (const event of streamedTurn.events) { - if (session.cancelled) { - return { stopReason: "cancelled" }; - } - - const notifications = this.convertEventToNotifications( - event, - params.sessionId, - ); - - for (const notification of notifications) { - await this.client.sessionUpdate(notification); - this.appendNotification(params.sessionId, notification); - } - - if (event.type === "thread.started" && "id" in event) { - session.threadId = event.id as string; - this.client.extNotification("_posthog/sdk_session", { - sessionId: params.sessionId, - sdkSessionId: event.id as string, - }); - } - } - - return { stopReason: "end_turn" }; - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error); - this.logger.error("Codex prompt failed", { - error: errorMessage, - stack: error instanceof Error ? error.stack : undefined, - }); - throw RequestError.internalError(undefined, errorMessage); - } - } - - async cancel(params: CancelNotification): Promise { - const session = this.sessions.get(params.sessionId); - if (!session) { - throw new Error("Session not found"); - } - session.cancelled = true; - this.logger.info("Cancelled Codex session", { - sessionId: params.sessionId, - }); - } - - async setSessionModel(_params: SetSessionModelRequest): Promise { - // No-op: Codex model is fixed - } - - async setSessionMode( - params: SetSessionModeRequest, - ): Promise { - if (params.modeId !== "default") { - throw new Error(`Mode ${params.modeId} not supported by Codex`); - } - return {}; - } - - async loadSession(params: LoadSessionRequest): Promise { - return this.resumeSession(params); - } - - /** - * Resume a session without replaying history. - * Client is responsible for fetching and rendering history from S3. - */ - async resumeSession( - params: LoadSessionRequest, - ): Promise { - const { sessionId } = params; - - const persistence = params._meta?.persistence as - | SessionPersistenceConfig - | undefined; - - const existingSession = this.sessions.get(sessionId); - const threadId = existingSession?.threadId; - - if (threadId) { - const threadOptions: ThreadOptions = { - workingDirectory: params.cwd, - }; - - const thread = this.codex.resumeThread(threadId, threadOptions); - - const session: CodexSession = { - thread, - threadId, - cancelled: false, - notificationHistory: existingSession?.notificationHistory || [], - }; - - this.sessions.set(sessionId, session); - this.logger.info("Resumed Codex thread", { sessionId, threadId }); - - if (persistence && this.sessionStore) { - this.sessionStore.register(sessionId, persistence); - this.logger.info("Registered resumed session for S3 persistence", { - sessionId, - taskId: persistence.taskId, - runId: persistence.runId, - }); - } - } else { - this.logger.info("No thread ID found, creating new session", { - sessionId, - }); - await this.newSession({ - ...params, - _meta: { ...(params._meta || {}), sessionId, persistence }, - } as NewSessionRequest); - } - - return {}; - } - - /** - * Handle custom extension methods. - * Per ACP spec, extension methods start with underscore. - */ - async extMethod( - method: string, - params: Record, - ): Promise> { - if (method === "_posthog/session/resume") { - await this.resumeSession(params as unknown as LoadSessionRequest); - return {}; - } - - throw RequestError.methodNotFound(method); - } - - private appendNotification( - sessionId: string, - notification: SessionNotification, - ): void { - const session = this.sessions.get(sessionId); - if (session) { - session.notificationHistory.push(notification); - } - } - - private convertEventToNotifications( - event: ThreadEvent, - sessionId: string, - ): SessionNotification[] { - const notifications: SessionNotification[] = []; - - switch (event.type) { - case "thread.started": - case "turn.started": - case "turn.completed": - break; - - case "item.started": - if ("item" in event && event.item) { - const item = event.item as ThreadItem; - notifications.push( - ...this.itemStartedToNotifications(item, sessionId), - ); - } - break; - - case "item.updated": - if ("item" in event && event.item) { - const item = event.item as ThreadItem; - notifications.push( - ...this.itemUpdatedToNotifications(item, sessionId), - ); - } - break; - - case "item.completed": - if ("item" in event && event.item) { - const item = event.item as ThreadItem; - notifications.push( - ...this.itemCompletedToNotifications(item, sessionId), - ); - } - break; - - case "turn.failed": - if ("error" in event) { - const error = event.error as { message?: string } | undefined; - notifications.push({ - sessionId, - update: { - sessionUpdate: "agent_message_chunk", - content: { - type: "text", - text: `Turn failed: ${error?.message || "Unknown error"}`, - }, - }, - }); - } - break; - - case "error": - if ("message" in event) { - notifications.push({ - sessionId, - update: { - sessionUpdate: "agent_message_chunk", - content: { - type: "text", - text: `Error: ${event.message || "Unknown error"}`, - }, - }, - }); - } - break; - } - - return notifications; - } - - private itemStartedToNotifications( - item: ThreadItem, - sessionId: string, - ): SessionNotification[] { - const notifications: SessionNotification[] = []; - - switch (item.type) { - case "command_execution": { - const cmdItem = item as CommandExecutionItem; - notifications.push({ - sessionId, - update: { - sessionUpdate: "tool_call", - toolCallId: cmdItem.id, - title: `Bash: ${this.truncateCommand(cmdItem.command)}`, - status: "in_progress", - rawInput: { command: cmdItem.command }, - _meta: { - codex: { - toolName: "Bash", - command: cmdItem.command, - }, - }, - }, - }); - break; - } - - case "file_change": { - const fileItem = item as FileChangeItem; - const paths = fileItem.changes.map((c) => c.path).join(", "); - notifications.push({ - sessionId, - update: { - sessionUpdate: "tool_call", - toolCallId: fileItem.id, - title: `File changes: ${paths}`, - status: "in_progress", - rawInput: { changes: fileItem.changes }, - _meta: { - codex: { - toolName: "FileChange", - changes: fileItem.changes, - }, - }, - }, - }); - break; - } - - case "mcp_tool_call": { - const mcpItem = item as McpToolCallItem; - notifications.push({ - sessionId, - update: { - sessionUpdate: "tool_call", - toolCallId: mcpItem.id, - title: `MCP: ${mcpItem.server}/${mcpItem.tool}`, - status: "in_progress", - rawInput: - mcpItem.arguments && typeof mcpItem.arguments === "object" - ? (mcpItem.arguments as Record) - : undefined, - _meta: { - codex: { - toolName: "McpToolCall", - server: mcpItem.server, - tool: mcpItem.tool, - }, - }, - }, - }); - break; - } - - case "web_search": { - const searchItem = item as WebSearchItem; - notifications.push({ - sessionId, - update: { - sessionUpdate: "tool_call", - toolCallId: searchItem.id, - title: `Web Search: ${searchItem.query}`, - status: "in_progress", - rawInput: { query: searchItem.query }, - _meta: { - codex: { - toolName: "WebSearch", - query: searchItem.query, - }, - }, - }, - }); - break; - } - } - - return notifications; - } - - private itemUpdatedToNotifications( - item: ThreadItem, - sessionId: string, - ): SessionNotification[] { - const notifications: SessionNotification[] = []; - - if (item.type === "command_execution") { - const cmdItem = item as CommandExecutionItem; - if (cmdItem.aggregated_output) { - notifications.push({ - sessionId, - update: { - sessionUpdate: "tool_call_update", - toolCallId: cmdItem.id, - status: "in_progress", - _meta: { - codex: { - toolName: "Bash", - output: cmdItem.aggregated_output, - }, - }, - }, - }); - } - } - - return notifications; - } - - private itemCompletedToNotifications( - item: ThreadItem, - sessionId: string, - ): SessionNotification[] { - const notifications: SessionNotification[] = []; - - switch (item.type) { - case "agent_message": { - const text = (item as { text?: string }).text; - if (text) { - notifications.push({ - sessionId, - update: { - sessionUpdate: "agent_message_chunk", - content: { - type: "text", - text, - }, - }, - }); - } - break; - } - - case "reasoning": { - const text = (item as { text?: string }).text; - if (text) { - notifications.push({ - sessionId, - update: { - sessionUpdate: "agent_thought_chunk", - content: { - type: "text", - text, - }, - }, - }); - } - break; - } - - case "command_execution": { - const cmdItem = item as CommandExecutionItem; - const success = cmdItem.exit_code === 0; - notifications.push({ - sessionId, - update: { - sessionUpdate: "tool_call_update", - toolCallId: cmdItem.id, - status: success ? "completed" : "failed", - _meta: { - codex: { - toolName: "Bash", - exitCode: cmdItem.exit_code, - output: cmdItem.aggregated_output, - }, - }, - }, - }); - break; - } - - case "file_change": { - const fileItem = item as FileChangeItem; - const success = fileItem.status === "completed"; - notifications.push({ - sessionId, - update: { - sessionUpdate: "tool_call_update", - toolCallId: fileItem.id, - status: success ? "completed" : "failed", - _meta: { - codex: { - toolName: "FileChange", - changes: fileItem.changes, - }, - }, - }, - }); - break; - } - - case "mcp_tool_call": { - const mcpItem = item as McpToolCallItem; - const success = mcpItem.status === "completed"; - notifications.push({ - sessionId, - update: { - sessionUpdate: "tool_call_update", - toolCallId: mcpItem.id, - status: success ? "completed" : "failed", - _meta: { - codex: { - toolName: "McpToolCall", - server: mcpItem.server, - tool: mcpItem.tool, - result: mcpItem.result, - error: mcpItem.error, - }, - }, - }, - }); - break; - } - - case "web_search": { - const searchItem = item as WebSearchItem; - notifications.push({ - sessionId, - update: { - sessionUpdate: "tool_call_update", - toolCallId: searchItem.id, - status: "completed", - _meta: { - codex: { - toolName: "WebSearch", - query: searchItem.query, - }, - }, - }, - }); - break; - } - - case "error": { - const errorItem = item as { id: string; message?: string }; - if (errorItem.message) { - notifications.push({ - sessionId, - update: { - sessionUpdate: "agent_message_chunk", - content: { - type: "text", - text: `Error: ${errorItem.message}`, - }, - }, - }); - } - break; - } - } - - return notifications; - } - - private truncateCommand(command: string, maxLength = 50): string { - if (command.length <= maxLength) { - return command; - } - return `${command.substring(0, maxLength)}...`; - } -} diff --git a/packages/agent/src/adapters/connection.ts b/packages/agent/src/adapters/connection.ts index 863138472..a51533b2a 100644 --- a/packages/agent/src/adapters/connection.ts +++ b/packages/agent/src/adapters/connection.ts @@ -1,8 +1,7 @@ /** - * Shared ACP connection factory with framework routing. + * Shared ACP connection factory. * - * Creates ACP connections that can use different agent frameworks - * (Claude Code, OpenAI Codex) based on the configured framework. + * Creates ACP connections for the Claude Code agent. */ import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk"; @@ -11,9 +10,8 @@ import { Logger } from "@/utils/logger.js"; import { createTappedWritableStream } from "@/utils/tapped-stream.js"; import { ClaudeAcpAgent } from "./claude/claude.js"; import { createBidirectionalStreams, type StreamPair } from "./claude/utils.js"; -import { CodexAcpAgent } from "./codex/codex.js"; -export type AgentFramework = "claude" | "codex"; +export type AgentFramework = "claude"; export type AcpConnectionConfig = { framework?: AgentFramework; @@ -81,16 +79,10 @@ export function createAcpConnection( const agentStream = ndJsonStream(agentWritable, streams.agent.readable); - // Create the appropriate agent based on framework selection + // Create the Claude agent const agentConnection = new AgentSideConnection((client) => { - switch (framework) { - case "codex": - logger.info("Creating Codex agent"); - return new CodexAcpAgent(client, sessionStore); - default: - logger.info("Creating Claude agent"); - return new ClaudeAcpAgent(client, sessionStore); - } + logger.info("Creating Claude agent"); + return new ClaudeAcpAgent(client, sessionStore); }, agentStream); return { diff --git a/packages/agent/src/agents/execution.ts b/packages/agent/src/agents/execution.ts deleted file mode 100644 index 44ed58143..000000000 --- a/packages/agent/src/agents/execution.ts +++ /dev/null @@ -1,37 +0,0 @@ -export const EXECUTION_SYSTEM_PROMPT = ` -PostHog AI Execution Agent — autonomously implement tasks as merge-ready code following project conventions. - - - -You have access to local repository files and PostHog MCP server. Work primarily with local files for implementation. Commit changes regularly. - - - -- Follow existing code style, patterns, and conventions found in the repository -- Minimize new external dependencies — only add when necessary -- Implement structured logging and error handling (never log secrets) -- Avoid destructive shell commands -- Create/update .gitignore to exclude build artifacts, dependencies, and temp files - - - -1. Review the implementation plan if provided, or create your own todo list -2. Execute changes step by step -3. Test thoroughly and verify functionality -4. Commit changes with clear messages - - - -Before completing the task, verify: -- .gitignore includes build artifacts, node_modules, __pycache__, etc. -- Dependency files (package.json, requirements.txt) use exact versions -- Code compiles and tests pass -- Added or updated relevant tests -- Captured meaningful events with PostHog SDK where appropriate -- Wrapped new logic in PostHog feature flags where appropriate -- Updated documentation, README, or type hints as needed - - - -Provide a concise summary of changes made when finished. -`; diff --git a/packages/agent/src/agents/planning.ts b/packages/agent/src/agents/planning.ts deleted file mode 100644 index aa5b2fa27..000000000 --- a/packages/agent/src/agents/planning.ts +++ /dev/null @@ -1,60 +0,0 @@ -export const PLANNING_SYSTEM_PROMPT = ` -PostHog AI Planning Agent — analyze codebases and create actionable implementation plans. - - - -- Read-only: analyze files, search code, explore structure -- No modifications or edits -- Output ONLY the plan markdown — no preamble, no acknowledgment, no meta-commentary - - - -Create a detailed, actionable implementation plan that an execution agent can follow to complete the task successfully. - - - -1. Explore repository structure and identify relevant files/components -2. Understand existing patterns, conventions, and dependencies -3. Break down task requirements and identify technical constraints -4. Define step-by-step implementation approach -5. Specify files to modify/create with exact paths -6. Identify testing requirements and potential risks - - - -Output the plan DIRECTLY as markdown with NO preamble text. Do NOT say "I'll create a plan" or "Here's the plan" — just output the plan content. - -Required sections (follow the template provided in the task prompt): -- Summary: Brief overview of approach -- Files to Create/Modify: Specific paths and purposes -- Implementation Steps: Ordered list of actions -- Testing Strategy: How to verify it works -- Considerations: Dependencies, risks, edge cases - - - - -"Sure! I'll create a detailed implementation plan for you to add authentication. Here's what we'll do..." -Reason: No preamble — output the plan directly - - - -"# Implementation Plan - -## Summary -Add JWT-based authentication to API endpoints using existing middleware pattern... - -## Files to Modify -- src/middleware/auth.ts: Add JWT verification -..." -Reason: Direct plan output with no meta-commentary - - - - -If research findings, context files, or reference materials are provided: -- Incorporate research findings into your analysis -- Follow patterns and approaches identified in research -- Build upon or refine any existing planning work -- Reference specific files and components mentioned in context -`; diff --git a/packages/agent/src/agents/research.ts b/packages/agent/src/agents/research.ts deleted file mode 100644 index d45ba9bfd..000000000 --- a/packages/agent/src/agents/research.ts +++ /dev/null @@ -1,160 +0,0 @@ -export const RESEARCH_SYSTEM_PROMPT = ` -PostHog AI Research Agent — analyze codebases to evaluate task actionability and identify missing information. - - - -- Read-only: analyze files, search code, explore structure -- No modifications or code changes -- Output structured JSON only - - - -Your PRIMARY goal is to evaluate whether a task is actionable and assign an actionability score. - -Calculate an actionabilityScore (0-1) based on: -- **Task clarity** (0.4 weight): Is the task description specific and unambiguous? -- **Codebase context** (0.3 weight): Can you locate the relevant code and patterns? -- **Architectural decisions** (0.2 weight): Are the implementation approaches clear? -- **Dependencies** (0.1 weight): Are required dependencies and constraints understood? - -If actionabilityScore < 0.7, generate specific clarifying questions to increase confidence. - -Questions must present complete implementation choices, NOT request information from the user: -options: array of strings -- GOOD: options: ["Use Redux Toolkit (matches pattern in src/store/)", "Zustand (lighter weight)"] -- BAD: "Tell me which state management library to use" -- GOOD: options: ["Place in Button.tsx (existing component)", "create NewButton.tsx (separate concerns)?"] -- BAD: "Where should I put this code?" - -DO NOT ask questions like "how should I fix this" or "tell me the pattern" — present concrete options that can be directly chosen and acted upon. - - - -1. Explore repository structure and identify relevant files/components -2. Understand existing patterns, conventions, and dependencies -3. Calculate actionabilityScore based on clarity, context, architecture, and dependencies -4. Identify key files that will need modification -5. If score < 0.7: generate 2-4 specific questions to resolve blockers -6. Output JSON matching ResearchEvaluation schema - - - -Output ONLY valid JSON with no markdown wrappers, no preamble, no explanation: - -{ - "actionabilityScore": 0.85, - "context": "Brief 2-3 sentence summary of the task and implementation approach", - "keyFiles": ["path/to/file1.ts", "path/to/file2.ts"], - "blockers": ["Optional: what's preventing full confidence"], - "questions": [ - { - "id": "q1", - "question": "Specific architectural decision needed?", - "options": [ - "First approach with concrete details", - "Alternative approach with concrete details", - "Third option if needed" - ] - } - ] -} - -Rules: -- actionabilityScore: number between 0 and 1 -- context: concise summary for planning phase -- keyFiles: array of file paths that need modification -- blockers: optional array explaining confidence gaps -- questions: ONLY include if actionabilityScore < 0.7 -- Each question must have 2-3 options (maximum 3) -- Max 3 questions total -- Options must be complete, actionable choices that require NO additional user input -- NEVER use options like "Tell me the pattern", "Show me examples", "Specify the approach" -- Each option must be a full implementation decision that can be directly acted upon - - - - -Task: "Fix typo in login button text" -Reasoning: Completely clear task, found exact component, no architectural decisions - - - -Task: "Add caching to API endpoints" -Reasoning: Clear goal, found endpoints, but multiple caching strategies possible - - - -Task: "Improve performance" -Reasoning: Vague task, unclear scope, needs questions about which areas to optimize -Questions needed: Which features are slow? What metrics define success? - - - -Task: "Add the new feature" -Reasoning: Extremely vague, no context, cannot locate relevant code -Questions needed: What feature? Which product area? What should it do? - - - - - -{ - "id": "q1", - "question": "Which caching layer should we use for API responses?", - "options": [ - "Redis with 1-hour TTL (existing infrastructure, requires Redis client setup)", - "In-memory LRU cache with 100MB limit (simpler, single-server only)", - "HTTP Cache-Control headers only (minimal backend changes, relies on browser/CDN)" - ] -} -Reason: Each option is a complete, actionable decision with concrete details - - - -{ - "id": "q2", - "question": "Where should the new analytics tracking code be placed?", - "options": [ - "In the existing UserAnalytics.ts module alongside page view tracking", - "Create a new EventTracking.ts module in src/analytics/ for all event tracking", - "Add directly to each component that needs tracking (no centralized module)" - ] -} -Reason: Specific file paths and architectural patterns, no user input needed - - - -{ - "id": "q1", - "question": "How should I implement this?", - "options": ["One way", "Another way"] -} -Reason: Too vague, doesn't explain the tradeoffs or provide concrete details - - - -{ - "id": "q2", - "question": "Which pattern should we follow for state management?", - "options": [ - "Tell me which pattern the codebase currently uses", - "Show me examples of state management", - "Whatever you think is best" - ] -} -Reason: Options request user input instead of being actionable choices. Should be concrete patterns like "Zustand stores (matching existing patterns in src/stores/)" or "React Context (simpler, no new dependencies)" - - - -{ - "id": "q3", - "question": "What color scheme should the button use?", - "options": [ - "Use the existing theme colors", - "Let me specify custom colors", - "Match the design system" - ] -} -Reason: "Let me specify" requires user input. Should be "Primary blue (#0066FF, existing theme)" or "Secondary gray (#6B7280, existing theme)" - -`; diff --git a/packages/agent/src/prompt-builder.ts b/packages/agent/src/prompt-builder.ts deleted file mode 100644 index 9e837767c..000000000 --- a/packages/agent/src/prompt-builder.ts +++ /dev/null @@ -1,499 +0,0 @@ -import { promises as fs } from "node:fs"; -import { join } from "node:path"; -import type { TemplateVariables } from "./template-manager.js"; -import type { - PostHogResource, - ResourceType, - SupportingFile, - Task, - UrlMention, -} from "./types.js"; -import { Logger } from "./utils/logger.js"; - -export interface PromptBuilderDeps { - getTaskFiles: (taskId: string) => Promise; - generatePlanTemplate: (vars: TemplateVariables) => Promise; - posthogClient?: { - fetchResourceByUrl: (mention: UrlMention) => Promise; - }; - logger?: Logger; -} - -export class PromptBuilder { - private getTaskFiles: PromptBuilderDeps["getTaskFiles"]; - private generatePlanTemplate: PromptBuilderDeps["generatePlanTemplate"]; - private posthogClient?: PromptBuilderDeps["posthogClient"]; - private logger: Logger; - - constructor(deps: PromptBuilderDeps) { - this.getTaskFiles = deps.getTaskFiles; - this.generatePlanTemplate = deps.generatePlanTemplate; - this.posthogClient = deps.posthogClient; - this.logger = - deps.logger || new Logger({ debug: false, prefix: "[PromptBuilder]" }); - } - - /** - * Extract file paths from XML tags in description - * Format: - */ - private extractFilePaths(description: string): string[] { - const fileTagRegex = //g; - const paths: string[] = []; - let match: RegExpExecArray | null; - - match = fileTagRegex.exec(description); - while (match !== null) { - paths.push(match[1]); - match = fileTagRegex.exec(description); - } - - return paths; - } - - /** - * Read file contents from repository - */ - private async readFileContent( - repositoryPath: string, - filePath: string, - ): Promise { - try { - const fullPath = join(repositoryPath, filePath); - const content = await fs.readFile(fullPath, "utf8"); - return content; - } catch (error) { - this.logger.warn(`Failed to read referenced file: ${filePath}`, { - error, - }); - return null; - } - } - - /** - * Extract URL mentions from XML tags in description - * Formats: , , - */ - private extractUrlMentions(description: string): UrlMention[] { - const mentions: UrlMention[] = []; - - // PostHog resource mentions: , , etc. - const resourceRegex = - /<(error|experiment|insight|feature_flag)\s+id="([^"]+)"\s*\/>/g; - let match: RegExpExecArray | null; - - match = resourceRegex.exec(description); - while (match !== null) { - const [, type, id] = match; - mentions.push({ - url: "", // Will be reconstructed if needed - type: type as ResourceType, - id, - label: this.generateUrlLabel("", type as ResourceType), - }); - match = resourceRegex.exec(description); - } - - // Generic URL mentions: - const urlRegex = //g; - match = urlRegex.exec(description); - while (match !== null) { - const [, url] = match; - mentions.push({ - url, - type: "generic", - label: this.generateUrlLabel(url, "generic"), - }); - match = urlRegex.exec(description); - } - - return mentions; - } - - /** - * Generate a display label for a URL mention - */ - private generateUrlLabel(url: string, type: string): string { - try { - const urlObj = new URL(url); - switch (type) { - case "error": { - const errorMatch = url.match(/error_tracking\/([a-f0-9-]+)/); - return errorMatch ? `Error ${errorMatch[1].slice(0, 8)}...` : "Error"; - } - case "experiment": { - const expMatch = url.match(/experiments\/(\d+)/); - return expMatch ? `Experiment #${expMatch[1]}` : "Experiment"; - } - case "insight": - return "Insight"; - case "feature_flag": - return "Feature Flag"; - default: - return urlObj.hostname; - } - } catch { - return "URL"; - } - } - - /** - * Process URL references and fetch their content - */ - private async processUrlReferences( - description: string, - ): Promise<{ description: string; referencedResources: PostHogResource[] }> { - const urlMentions = this.extractUrlMentions(description); - const referencedResources: PostHogResource[] = []; - - if (urlMentions.length === 0 || !this.posthogClient) { - return { description, referencedResources }; - } - - // Fetch all referenced resources - for (const mention of urlMentions) { - try { - const resource = await this.posthogClient.fetchResourceByUrl(mention); - referencedResources.push(resource); - } catch (error) { - this.logger.warn(`Failed to fetch resource from URL: ${mention.url}`, { - error, - }); - // Add a placeholder resource for failed fetches - referencedResources.push({ - type: mention.type, - id: mention.id || "", - url: mention.url, - title: mention.label || "Unknown Resource", - content: `Failed to fetch resource from ${mention.url}: ${error}`, - metadata: {}, - }); - } - } - - // Replace URL tags with just the label for readability - let processedDescription = description; - for (const mention of urlMentions) { - if (mention.type === "generic") { - // Generic URLs: - const escapedUrl = mention.url.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - processedDescription = processedDescription.replace( - new RegExp(``, "g"), - `@${mention.label}`, - ); - } else { - // PostHog resources: , , etc. - const escapedType = mention.type.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - const escapedId = mention.id - ? mention.id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") - : ""; - processedDescription = processedDescription.replace( - new RegExp(`<${escapedType}\\s+id="${escapedId}"\\s*/>`, "g"), - `@${mention.label}`, - ); - } - } - - return { description: processedDescription, referencedResources }; - } - - /** - * Process description to extract file tags and read contents - * Returns processed description and referenced file contents - */ - private async processFileReferences( - description: string, - repositoryPath?: string, - ): Promise<{ - description: string; - referencedFiles: Array<{ path: string; content: string }>; - }> { - const filePaths = this.extractFilePaths(description); - const referencedFiles: Array<{ path: string; content: string }> = []; - - if (filePaths.length === 0 || !repositoryPath) { - return { description, referencedFiles }; - } - - // Read all referenced files, tracking which ones succeed - const successfulPaths = new Set(); - for (const filePath of filePaths) { - const content = await this.readFileContent(repositoryPath, filePath); - if (content !== null) { - referencedFiles.push({ path: filePath, content }); - successfulPaths.add(filePath); - } - } - - // Only replace tags for files that were successfully read - let processedDescription = description; - for (const filePath of successfulPaths) { - const fileName = filePath.split("/").pop() || filePath; - processedDescription = processedDescription.replace( - new RegExp( - ``, - "g", - ), - `@${fileName}`, - ); - } - - return { description: processedDescription, referencedFiles }; - } - - async buildResearchPrompt( - task: Task, - repositoryPath?: string, - ): Promise { - // Process file references in description - const { description: descriptionAfterFiles, referencedFiles } = - await this.processFileReferences(task.description, repositoryPath); - - // Process URL references in description - const { description: processedDescription, referencedResources } = - await this.processUrlReferences(descriptionAfterFiles); - - let prompt = "\n"; - prompt += `${task.title}\n`; - prompt += `${processedDescription}\n`; - - if (task.repository) { - prompt += `${task.repository}\n`; - } - prompt += "\n"; - - // Add referenced files from @ mentions - if (referencedFiles.length > 0) { - prompt += "\n\n"; - for (const file of referencedFiles) { - prompt += `\n\`\`\`\n${file.content}\n\`\`\`\n\n`; - } - prompt += "\n"; - } - - // Add referenced resources from URL mentions - if (referencedResources.length > 0) { - prompt += "\n\n"; - for (const resource of referencedResources) { - prompt += `\n`; - prompt += `${resource.title}\n`; - prompt += `${resource.content}\n`; - prompt += "\n"; - } - prompt += "\n"; - } - - try { - const taskFiles = await this.getTaskFiles(task.id); - const contextFiles = taskFiles.filter( - (f: SupportingFile) => f.type === "context" || f.type === "reference", - ); - if (contextFiles.length > 0) { - prompt += "\n\n"; - for (const file of contextFiles) { - prompt += `\n${file.content}\n\n`; - } - prompt += "\n"; - } - } catch (_error) { - this.logger.debug("No existing task files found for research", { - taskId: task.id, - }); - } - - return prompt; - } - - async buildPlanningPrompt( - task: Task, - repositoryPath?: string, - ): Promise { - // Process file references in description - const { description: descriptionAfterFiles, referencedFiles } = - await this.processFileReferences(task.description, repositoryPath); - - // Process URL references in description - const { description: processedDescription, referencedResources } = - await this.processUrlReferences(descriptionAfterFiles); - - let prompt = "\n"; - prompt += `${task.title}\n`; - prompt += `${processedDescription}\n`; - - if (task.repository) { - prompt += `${task.repository}\n`; - } - prompt += "\n"; - - // Add referenced files from @ mentions - if (referencedFiles.length > 0) { - prompt += "\n\n"; - for (const file of referencedFiles) { - prompt += `\n\`\`\`\n${file.content}\n\`\`\`\n\n`; - } - prompt += "\n"; - } - - // Add referenced resources from URL mentions - if (referencedResources.length > 0) { - prompt += "\n\n"; - for (const resource of referencedResources) { - prompt += `\n`; - prompt += `${resource.title}\n`; - prompt += `${resource.content}\n`; - prompt += "\n"; - } - prompt += "\n"; - } - - try { - const taskFiles = await this.getTaskFiles(task.id); - const contextFiles = taskFiles.filter( - (f: SupportingFile) => f.type === "context" || f.type === "reference", - ); - if (contextFiles.length > 0) { - prompt += "\n\n"; - for (const file of contextFiles) { - prompt += `\n${file.content}\n\n`; - } - prompt += "\n"; - } - } catch (_error) { - this.logger.debug("No existing task files found for planning", { - taskId: task.id, - }); - } - - const templateVariables = { - task_id: task.id, - task_title: task.title, - task_description: processedDescription, - date: new Date().toISOString().split("T")[0], - repository: task.repository || "", - }; - - const planTemplate = await this.generatePlanTemplate(templateVariables); - - prompt += "\n\n"; - prompt += - "Analyze the codebase and create a detailed implementation plan. Use the template structure below, filling each section with specific, actionable information.\n"; - prompt += "\n\n"; - prompt += "\n"; - prompt += planTemplate; - prompt += "\n"; - - return prompt; - } - - async buildExecutionPrompt( - task: Task, - repositoryPath?: string, - ): Promise { - // Process file references in description - const { description: descriptionAfterFiles, referencedFiles } = - await this.processFileReferences(task.description, repositoryPath); - - // Process URL references in description - const { description: processedDescription, referencedResources } = - await this.processUrlReferences(descriptionAfterFiles); - - let prompt = "\n"; - prompt += `${task.title}\n`; - prompt += `${processedDescription}\n`; - - if (task.repository) { - prompt += `${task.repository}\n`; - } - prompt += "\n"; - - // Add referenced files from @ mentions - if (referencedFiles.length > 0) { - prompt += "\n\n"; - for (const file of referencedFiles) { - prompt += `\n\`\`\`\n${file.content}\n\`\`\`\n\n`; - } - prompt += "\n"; - } - - // Add referenced resources from URL mentions - if (referencedResources.length > 0) { - prompt += "\n\n"; - for (const resource of referencedResources) { - prompt += `\n`; - prompt += `${resource.title}\n`; - prompt += `${resource.content}\n`; - prompt += "\n"; - } - prompt += "\n"; - } - - try { - const taskFiles = await this.getTaskFiles(task.id); - const hasPlan = taskFiles.some((f: SupportingFile) => f.type === "plan"); - const todosFile = taskFiles.find( - (f: SupportingFile) => f.name === "todos.json", - ); - - if (taskFiles.length > 0) { - prompt += "\n\n"; - for (const file of taskFiles) { - if (file.type === "plan") { - prompt += `\n${file.content}\n\n`; - } else if (file.name === "todos.json") { - } else { - prompt += `\n${file.content}\n\n`; - } - } - prompt += "\n"; - } - - // Add todos context if resuming work - if (todosFile) { - try { - const todos = JSON.parse(todosFile.content); - if (todos.items && todos.items.length > 0) { - prompt += "\n\n"; - prompt += - "You previously created the following todo list for this task:\n\n"; - for (const item of todos.items) { - const statusIcon = - item.status === "completed" - ? "✓" - : item.status === "in_progress" - ? "▶" - : "○"; - prompt += `${statusIcon} [${item.status}] ${item.content}\n`; - } - prompt += `\nProgress: ${todos.metadata.completed}/${todos.metadata.total} completed\n`; - prompt += - "\nYou can reference this list when resuming work or create an updated list as needed.\n"; - prompt += "\n"; - } - } catch (error) { - this.logger.debug("Failed to parse todos.json for context", { - error, - }); - } - } - - prompt += "\n\n"; - if (hasPlan) { - prompt += - "Implement the changes described in the execution plan. Follow the plan step-by-step and make the necessary file modifications.\n"; - } else { - prompt += - "Implement the changes described in the task. Make the necessary file modifications to complete the task.\n"; - } - prompt += ""; - } catch (_error) { - this.logger.debug("No supporting files found for execution", { - taskId: task.id, - }); - prompt += "\n\n"; - prompt += "Implement the changes described in the task.\n"; - prompt += ""; - } - - return prompt; - } -} diff --git a/packages/agent/src/template-manager.ts b/packages/agent/src/template-manager.ts deleted file mode 100644 index 404b34fe5..000000000 --- a/packages/agent/src/template-manager.ts +++ /dev/null @@ -1,236 +0,0 @@ -import { existsSync, promises as fs } from "node:fs"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; -import { Logger } from "./utils/logger"; - -const logger = new Logger({ prefix: "[TemplateManager]" }); - -export interface TemplateVariables { - task_id: string; - task_title: string; - task_description?: string; - date: string; - repository?: string; - [key: string]: string | undefined; -} - -export class TemplateManager { - private templatesDir: string; - - constructor() { - const __filename = fileURLToPath(import.meta.url); - const __dirname = dirname(__filename); - - // Exhaustive list of possible template locations - const candidateDirs = [ - // Standard build output (dist/src/template-manager.js -> dist/templates) - join(__dirname, "..", "templates"), - - // If preserveModules creates nested structure (dist/src/template-manager.js -> dist/src/templates) - join(__dirname, "templates"), - - // Development scenarios (src/template-manager.ts -> src/templates) - join(__dirname, "..", "..", "src", "templates"), - - // Package root templates directory - join(__dirname, "..", "..", "templates"), - - // When node_modules symlink or installed (node_modules/@posthog/agent/dist/src/... -> node_modules/@posthog/agent/dist/templates) - join(__dirname, "..", "..", "dist", "templates"), - - // When consumed from node_modules deep in tree - join(__dirname, "..", "..", "..", "templates"), - join(__dirname, "..", "..", "..", "dist", "templates"), - join(__dirname, "..", "..", "..", "src", "templates"), - - // When bundled by Vite/Webpack (e.g., .vite/build/index.js -> node_modules/@posthog/agent/dist/templates) - // Try to find node_modules from current location - join( - __dirname, - "..", - "node_modules", - "@posthog", - "agent", - "dist", - "templates", - ), - join( - __dirname, - "..", - "..", - "node_modules", - "@posthog", - "agent", - "dist", - "templates", - ), - join( - __dirname, - "..", - "..", - "..", - "node_modules", - "@posthog", - "agent", - "dist", - "templates", - ), - ]; - - const resolvedDir = candidateDirs.find((dir) => existsSync(dir)); - - if (!resolvedDir) { - logger.error("Could not find templates directory."); - logger.error(`Current file: ${__filename}`); - logger.error(`Current dir: ${__dirname}`); - logger.error( - `Tried: ${candidateDirs.map((d) => `\n - ${d} (exists: ${existsSync(d)})`).join("")}`, - ); - } - - this.templatesDir = resolvedDir ?? candidateDirs[0]; - } - - private async loadTemplate(templateName: string): Promise { - try { - const templatePath = join(this.templatesDir, templateName); - return await fs.readFile(templatePath, "utf8"); - } catch (error) { - throw new Error( - `Failed to load template ${templateName} from ${this.templatesDir}: ${error}`, - ); - } - } - - private substituteVariables( - template: string, - variables: TemplateVariables, - ): string { - let result = template; - - for (const [key, value] of Object.entries(variables)) { - if (value !== undefined) { - const placeholder = new RegExp(`{{${key}}}`, "g"); - result = result.replace(placeholder, value); - } - } - - result = result.replace(/{{[^}]+}}/g, "[PLACEHOLDER]"); - - return result; - } - - async generatePlan(variables: TemplateVariables): Promise { - const template = await this.loadTemplate("plan-template.md"); - return this.substituteVariables(template, { - ...variables, - date: variables.date || new Date().toISOString().split("T")[0], - }); - } - - async generateCustomFile( - templateName: string, - variables: TemplateVariables, - ): Promise { - const template = await this.loadTemplate(templateName); - return this.substituteVariables(template, { - ...variables, - date: variables.date || new Date().toISOString().split("T")[0], - }); - } - - async createTaskStructure( - taskId: string, - taskTitle: string, - options?: { - includePlan?: boolean; - additionalFiles?: Array<{ - name: string; - template?: string; - content?: string; - }>; - }, - ): Promise< - Array<{ - name: string; - content: string; - type: "plan" | "context" | "reference" | "output"; - }> - > { - const files: Array<{ - name: string; - content: string; - type: "plan" | "context" | "reference" | "output"; - }> = []; - - const variables: TemplateVariables = { - task_id: taskId, - task_title: taskTitle, - date: new Date().toISOString().split("T")[0], - }; - - // Generate plan file if requested - if (options?.includePlan !== false) { - const planContent = await this.generatePlan(variables); - files.push({ - name: "plan.md", - content: planContent, - type: "plan", - }); - } - - if (options?.additionalFiles) { - for (const file of options.additionalFiles) { - let content: string; - - if (file.template) { - content = await this.generateCustomFile(file.template, variables); - } else if (file.content) { - content = this.substituteVariables(file.content, variables); - } else { - content = `# ${file.name}\n\nPlaceholder content for ${file.name}`; - } - - files.push({ - name: file.name, - content, - type: file.name.includes("context") ? "context" : "reference", - }); - } - } - - return files; - } - - generatePostHogReadme(): string { - return `# PostHog Task Files - -This directory contains task-related files. - -## Structure - -Each task has its own subdirectory: \`.posthog/{task-id}/\` - -### Common Files - -- **plan.md** - Implementation plan generated during planning phase -- **Supporting files** - Any additional files added for task context -- **artifacts/** - Generated files, outputs, and temporary artifacts - -### Usage - -These files are: -- Version controlled alongside your code -- Used for task context and planning -- Available for review in pull requests -- Organized by task ID for easy reference - -### Gitignore - -Customize \`.posthog/.gitignore\` to control which files are committed: -- Include plans and documentation by default -- Exclude temporary files and sensitive data -- Customize based on your team's needs -`; - } -} diff --git a/packages/agent/src/templates/plan-template.md b/packages/agent/src/templates/plan-template.md deleted file mode 100644 index 5c967eb3c..000000000 --- a/packages/agent/src/templates/plan-template.md +++ /dev/null @@ -1,41 +0,0 @@ -# Implementation Plan: {{task_title}} - -**Task ID:** {{task_id}} -**Generated:** {{date}} - -## Summary - -Brief description of what will be implemented and the overall approach. - -## Implementation Steps - -### 1. Analysis -- [ ] Identify relevant files and components -- [ ] Review existing patterns and constraints - -### 2. Changes Required -- [ ] Files to create/modify -- [ ] Dependencies to add/update - -### 3. Implementation -- [ ] Core functionality changes -- [ ] Tests and validation -- [ ] Documentation updates - -## File Changes - -### New Files -``` -path/to/new/file.ts - Purpose -``` - -### Modified Files -``` -path/to/existing/file.ts - Changes needed -``` - -## Considerations - -- Key architectural decisions -- Potential risks and mitigation -- Testing approach \ No newline at end of file diff --git a/packages/agent/src/types.ts b/packages/agent/src/types.ts index d7799420b..0998475b5 100644 --- a/packages/agent/src/types.ts +++ b/packages/agent/src/types.ts @@ -141,7 +141,7 @@ export interface TaskExecutionOptions { // See: https://docs.claude.com/en/api/agent-sdk/permissions canUseTool?: CanUseTool; skipGitBranch?: boolean; // Skip creating a task-specific git branch - framework?: "claude" | "codex"; // Agent framework to use (defaults to "claude") + framework?: "claude"; // Agent framework to use (defaults to "claude") task?: Task; // Pre-fetched task to avoid redundant API call isReconnect?: boolean; // Session recreation - skip RUN_STARTED notification } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6358623db..7b77b3668 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -412,9 +412,6 @@ importers: '@modelcontextprotocol/sdk': specifier: ^1.23.0 version: 1.23.0(zod@3.25.76) - '@openai/codex-sdk': - specifier: 0.60.1 - version: 0.60.1 diff: specifier: ^8.0.2 version: 8.0.2 @@ -2014,10 +2011,6 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} - '@openai/codex-sdk@0.60.1': - resolution: {integrity: sha512-w7FhUXfqpzw9igTZFfKS7cUNW1FK+tT426ZkClG2X8vufW0jyGqfgPd6Uq8+gJgSTLxayF9I802FDW2KjYcfYQ==} - engines: {node: '>=18'} - '@opentelemetry/api@1.9.0': resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} @@ -9599,8 +9592,6 @@ snapshots: '@open-draft/until@2.1.0': optional: true - '@openai/codex-sdk@0.60.1': {} - '@opentelemetry/api@1.9.0': {} '@oxc-resolver/binding-android-arm-eabi@11.13.2':