#!/usr/bin/env bun // @effect-diagnostics nodeBuiltinImport:off import { appendFileSync } from "node:fs"; import * as Effect from "effect/Effect"; import * as NodeServices from "@effect/platform-node/NodeServices"; import * as NodeRuntime from "@effect/platform-node/NodeRuntime"; import * as EffectAcpAgent from "effect-acp/agent"; import * as AcpError from "effect-acp/errors"; import type * as AcpSchema from "effect-acp/schema"; const requestLogPath = process.env.T3_ACP_REQUEST_LOG_PATH; const exitLogPath = process.env.T3_ACP_EXIT_LOG_PATH; const emitToolCalls = process.env.T3_ACP_EMIT_TOOL_CALLS === "1"; const emitInterleavedAssistantToolCalls = process.env.T3_ACP_EMIT_INTERLEAVED_ASSISTANT_TOOL_CALLS === "1"; const emitGenericToolPlaceholders = process.env.T3_ACP_EMIT_GENERIC_TOOL_PLACEHOLDERS === "1"; const emitAskQuestion = process.env.T3_ACP_EMIT_ASK_QUESTION === "1"; const failSetConfigOption = process.env.T3_ACP_FAIL_SET_CONFIG_OPTION === "1"; const exitOnSetConfigOption = process.env.T3_ACP_EXIT_ON_SET_CONFIG_OPTION === "1"; const promptResponseText = process.env.T3_ACP_PROMPT_RESPONSE_TEXT; const sessionId = "mock-session-1"; let currentModeId = "ask"; let currentModelId = "default"; let parameterizedModelPicker = false; let currentReasoning = "medium"; let currentContext = "272k"; let currentFast = false; const cancelledSessions = new Set(); function logExit(reason: string): void { if (!exitLogPath) { return; } appendFileSync(exitLogPath, `${reason}\n`, "utf8"); } process.once("SIGTERM", () => { logExit("SIGTERM"); process.exit(0); }); process.once("SIGINT", () => { logExit("SIGINT"); process.exit(0); }); process.once("exit", (code) => { logExit(`exit:${code}`); }); function configOptions(): ReadonlyArray { if (parameterizedModelPicker) { const baseOptions: Array = [ { id: "mode", name: "Mode", category: "mode", type: "select", currentValue: currentModeId, options: availableModes.map((mode) => ({ value: mode.id, name: mode.name, ...(mode.description ? { description: mode.description } : {}), })), }, { id: "model", name: "Model", category: "model", type: "select", currentValue: currentModelId, options: [ { value: "default", name: "Auto" }, { value: "composer-2", name: "Composer 2" }, { value: "gpt-5.4", name: "GPT-5.4" }, { value: "claude-opus-4-6", name: "Opus 4.6" }, ], }, ]; switch (currentModelId) { case "gpt-5.4": return [ ...baseOptions, { id: "reasoning", name: "Reasoning", category: "thought_level", type: "select", currentValue: currentReasoning, options: [ { value: "none", name: "None" }, { value: "low", name: "Low" }, { value: "medium", name: "Medium" }, { value: "high", name: "High" }, { value: "extra-high", name: "Extra High" }, ], }, { id: "context", name: "Context", category: "model_config", type: "select", currentValue: currentContext, options: [ { value: "272k", name: "272K" }, { value: "1m", name: "1M" }, ], }, { id: "fast", name: "Fast", category: "model_config", type: "select", currentValue: String(currentFast), options: [ { value: "false", name: "Off" }, { value: "true", name: "Fast" }, ], }, ]; case "composer-2": return [ ...baseOptions, { id: "fast", name: "Fast", category: "model_config", type: "select", currentValue: String(currentFast), options: [ { value: "false", name: "Off" }, { value: "true", name: "Fast" }, ], }, ]; case "claude-opus-4-6": return [ ...baseOptions, { id: "reasoning", name: "Reasoning", category: "thought_level", type: "select", currentValue: currentReasoning, options: [ { value: "low", name: "Low" }, { value: "medium", name: "Medium" }, { value: "high", name: "High" }, ], }, { id: "thinking", name: "Thinking", category: "model_config", type: "boolean", currentValue: true, }, ]; default: return baseOptions; } } return [ { id: "model", name: "Model", category: "model", type: "select" as const, currentValue: currentModelId, options: [ { value: "default", name: "Auto" }, { value: "composer-2", name: "Composer 2" }, { value: "composer-2[fast=true]", name: "Composer 2 Fast" }, { value: "gpt-5.3-codex[reasoning=medium,fast=false]", name: "Codex 5.3" }, ], }, ]; } const availableModes: ReadonlyArray = [ { id: "ask", name: "Ask", description: "Request permission before making any changes", }, { id: "architect", name: "Architect", description: "Design and plan software systems without implementation", }, { id: "code", name: "Code", description: "Write and modify code with full tool access", }, ]; function modeState(): AcpSchema.SessionModeState { return { currentModeId, availableModes, }; } const program = Effect.gen(function* () { const agent = yield* EffectAcpAgent.AcpAgent; yield* agent.handleInitialize((request) => Effect.sync(() => { parameterizedModelPicker = request.clientCapabilities?._meta?.parameterizedModelPicker === true; return { protocolVersion: 1, agentCapabilities: { loadSession: true }, }; }), ); yield* agent.handleAuthenticate(() => Effect.succeed({})); yield* agent.handleCreateSession(() => Effect.succeed({ sessionId, modes: modeState(), configOptions: configOptions(), }), ); yield* agent.handleLoadSession((request) => agent.client .sessionUpdate({ sessionId: String(request.sessionId ?? sessionId), update: { sessionUpdate: "user_message_chunk", content: { type: "text", text: "replay" }, }, }) .pipe( Effect.as({ modes: modeState(), configOptions: configOptions(), }), ), ); yield* agent.handleSetSessionConfigOption((request) => Effect.gen(function* () { if (exitOnSetConfigOption) { return yield* Effect.sync(() => { process.exit(7); }); } if (failSetConfigOption) { return yield* AcpError.AcpRequestError.invalidParams( "Mock invalid params for session/set_config_option", { method: "session/set_config_option", params: request, }, ); } if (request.configId === "mode" && typeof request.value === "string") { currentModeId = request.value; } if (request.configId === "model" && typeof request.value === "string") { currentModelId = request.value; } if (request.configId === "reasoning" && typeof request.value === "string") { currentReasoning = request.value; } if (request.configId === "context" && typeof request.value === "string") { currentContext = request.value; } if (request.configId === "fast") { currentFast = request.value === true || request.value === "true"; } return { configOptions: configOptions(), }; }), ); yield* agent.handleCancel(({ sessionId }) => Effect.sync(() => { cancelledSessions.add(String(sessionId ?? "mock-session-1")); }), ); yield* agent.handlePrompt((request) => Effect.gen(function* () { const requestedSessionId = String(request.sessionId ?? sessionId); if (emitInterleavedAssistantToolCalls) { const toolCallId = "tool-call-1"; yield* agent.client.sessionUpdate({ sessionId: requestedSessionId, update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text: "before tool" }, }, }); yield* agent.client.sessionUpdate({ sessionId: requestedSessionId, update: { sessionUpdate: "tool_call", toolCallId, title: "Terminal", kind: "execute", status: "pending", rawInput: { command: ["echo", "hello"], }, }, }); yield* agent.client.sessionUpdate({ sessionId: requestedSessionId, update: { sessionUpdate: "tool_call_update", toolCallId, status: "completed", rawOutput: { exitCode: 0, stdout: "hello", stderr: "", }, }, }); yield* agent.client.sessionUpdate({ sessionId: requestedSessionId, update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text: "after tool" }, }, }); return { stopReason: "end_turn" }; } if (emitToolCalls) { const toolCallId = "tool-call-1"; yield* agent.client.sessionUpdate({ sessionId: requestedSessionId, update: { sessionUpdate: "tool_call", toolCallId, title: "Terminal", kind: "execute", status: "pending", rawInput: { command: ["cat", "server/package.json"], }, }, }); yield* agent.client.sessionUpdate({ sessionId: requestedSessionId, update: { sessionUpdate: "tool_call_update", toolCallId, status: "in_progress", }, }); const permission = yield* agent.client.requestPermission({ sessionId: requestedSessionId, toolCall: { toolCallId, title: "`cat server/package.json`", kind: "execute", status: "pending", content: [ { type: "content", content: { type: "text", text: "Not in allowlist: cat server/package.json", }, }, ], }, options: [ { optionId: "allow-once", name: "Allow once", kind: "allow_once" }, { optionId: "allow-always", name: "Allow always", kind: "allow_always" }, { optionId: "reject-once", name: "Reject", kind: "reject_once" }, ], }); const cancelled = cancelledSessions.delete(requestedSessionId) || permission.outcome.outcome === "cancelled"; yield* agent.client.sessionUpdate({ sessionId: requestedSessionId, update: { sessionUpdate: "tool_call_update", toolCallId, title: "Terminal", kind: "execute", status: "completed", rawOutput: { exitCode: 0, stdout: '{ "name": "t3" }', stderr: "", }, }, }); yield* agent.client.sessionUpdate({ sessionId: requestedSessionId, update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text: "hello from mock" }, }, }); return { stopReason: cancelled ? "cancelled" : "end_turn" }; } if (emitGenericToolPlaceholders) { const toolCallId = "tool-call-generic-1"; yield* agent.client.sessionUpdate({ sessionId: requestedSessionId, update: { sessionUpdate: "tool_call", toolCallId, title: "Read File", kind: "read", status: "pending", rawInput: {}, }, }); yield* agent.client.sessionUpdate({ sessionId: requestedSessionId, update: { sessionUpdate: "tool_call_update", toolCallId, status: "in_progress", }, }); yield* agent.client.sessionUpdate({ sessionId: requestedSessionId, update: { sessionUpdate: "tool_call_update", toolCallId, status: "completed", rawOutput: { content: "package.json\n", }, }, }); return { stopReason: "end_turn" }; } if (emitAskQuestion) { yield* agent.client.extRequest("cursor/ask_question", { toolCallId: "ask-question-tool-call-1", title: "Question", questions: [ { id: "scope", prompt: "Which scope?", options: [ { id: "workspace", label: "Workspace" }, { id: "session", label: "Session" }, ], }, ], }); return { stopReason: "end_turn" }; } yield* agent.client.sessionUpdate({ sessionId: requestedSessionId, update: { sessionUpdate: "plan", entries: [ { content: "Inspect mock ACP state", priority: "high", status: "completed", }, { content: "Implement the requested change", priority: "high", status: "in_progress", }, ], }, }); yield* agent.client.sessionUpdate({ sessionId: requestedSessionId, update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text: promptResponseText ?? "hello from mock" }, }, }); return { stopReason: "end_turn" }; }), ); yield* agent.handleUnknownExtRequest((method, params) => { if (method !== "session/mode/set") { return Effect.fail(AcpError.AcpRequestError.methodNotFound(method)); } const nextModeId = typeof params === "object" && params !== null && "modeId" in params && typeof params.modeId === "string" ? params.modeId : typeof params === "object" && params !== null && "mode" in params && typeof params.mode === "string" ? params.mode : undefined; const requestedSessionId = typeof params === "object" && params !== null && "sessionId" in params && typeof params.sessionId === "string" ? params.sessionId : sessionId; if (typeof nextModeId === "string" && nextModeId.trim()) { currentModeId = nextModeId.trim(); return agent.client .sessionUpdate({ sessionId: requestedSessionId, update: { sessionUpdate: "current_mode_update", currentModeId, }, }) .pipe(Effect.as({})); } return Effect.succeed({}); }); return yield* Effect.never; }).pipe( Effect.provide( EffectAcpAgent.layerStdio( requestLogPath ? { logIncoming: true, logger: (event) => { if (event.direction !== "incoming" || event.stage !== "raw") { return Effect.void; } if (typeof event.payload !== "string") { return Effect.void; } const payload = event.payload; return Effect.sync(() => { appendFileSync( requestLogPath, payload.endsWith("\n") ? payload : `${payload}\n`, "utf8", ); }); }, } : {}, ), ), Effect.scoped, Effect.provide(NodeServices.layer), ); NodeRuntime.runMain(program);