| 1 | #!/usr/bin/env node |
| 2 | // @effect-diagnostics nodeBuiltinImport:off |
| 3 | import * as NodeChildProcess from "node:child_process"; |
| 4 | import * as NodeFS from "node:fs"; |
| 5 | |
| 6 | import * as Effect from "effect/Effect"; |
| 7 | import * as Schema from "effect/Schema"; |
| 8 | import * as Deferred from "effect/Deferred"; |
| 9 | |
| 10 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 11 | import * as NodeRuntime from "@effect/platform-node/NodeRuntime"; |
| 12 | |
| 13 | import * as EffectAcpAgent from "effect-acp/agent"; |
| 14 | import * as AcpError from "effect-acp/errors"; |
| 15 | import type * as AcpSchema from "effect-acp/schema"; |
| 16 | import type * as AcpCompat from "effect-acp/compat"; |
| 17 | |
| 18 | import { beginAcpMockPrompt } from "./acpMockCancellationState.ts"; |
| 19 | |
| 20 | const requestLogPath = process.env.T3_ACP_REQUEST_LOG_PATH; |
| 21 | const exitLogPath = process.env.T3_ACP_EXIT_LOG_PATH; |
| 22 | const antigravityProfile = process.env.T3_ACP_ANTIGRAVITY === "1"; |
| 23 | const emitToolCalls = process.env.T3_ACP_EMIT_TOOL_CALLS === "1"; |
| 24 | const emitInterleavedAssistantToolCalls = |
| 25 | process.env.T3_ACP_EMIT_INTERLEAVED_ASSISTANT_TOOL_CALLS === "1"; |
| 26 | const emitV2Fidelity = process.env.T3_ACP_EMIT_V2_FIDELITY === "1"; |
| 27 | const vibeRetryOutcome = process.env.T3_ACP_VIBE_RETRY_OUTCOME; |
| 28 | const emitGenericToolPlaceholders = process.env.T3_ACP_EMIT_GENERIC_TOOL_PLACEHOLDERS === "1"; |
| 29 | const emitPostSettleMonitorFlow = process.env.T3_ACP_EMIT_POST_SETTLE_MONITOR_FLOW === "1"; |
| 30 | const emitInTurnTaskOutputThenLateDuplicate = |
| 31 | process.env.T3_ACP_EMIT_IN_TURN_TASKOUTPUT_THEN_LATE_DUPLICATE === "1"; |
| 32 | const injectedReportTriggerPath = process.env.T3_ACP_INJECTED_REPORT_TRIGGER_PATH; |
| 33 | const emitBackgroundToolDuringAnswer = |
| 34 | process.env.T3_ACP_EMIT_BACKGROUND_TOOL_DURING_ANSWER === "1"; |
| 35 | const emitAskQuestion = process.env.T3_ACP_EMIT_ASK_QUESTION === "1"; |
| 36 | const emitElicitation = process.env.T3_ACP_EMIT_ELICITATION === "1"; |
| 37 | const emitMcpToolApprovalElicitation = |
| 38 | process.env.T3_ACP_EMIT_MCP_TOOL_APPROVAL_ELICITATION === "1"; |
| 39 | const emitUrlElicitation = process.env.T3_ACP_EMIT_URL_ELICITATION === "1"; |
| 40 | const emitXAiAskUserQuestion = process.env.T3_ACP_EMIT_XAI_ASK_USER_QUESTION === "1"; |
| 41 | const emitXAiExitPlanMode = process.env.T3_ACP_EMIT_XAI_EXIT_PLAN_MODE === "1"; |
| 42 | const emitXAiPlanMdWrite = process.env.T3_ACP_EMIT_XAI_PLAN_MD_WRITE === "1"; |
| 43 | const emitXAiPromptCompleteThenHang = process.env.T3_ACP_EMIT_XAI_PROMPT_COMPLETE_THEN_HANG === "1"; |
| 44 | const emitXAiRateLimitThenHang = process.env.T3_ACP_EMIT_XAI_RATE_LIMIT_THEN_HANG === "1"; |
| 45 | const emitXAiAskUserQuestionThenHang = |
| 46 | process.env.T3_ACP_EMIT_XAI_ASK_USER_QUESTION_THEN_HANG === "1"; |
| 47 | const emitContentThenHang = process.env.T3_ACP_EMIT_CONTENT_THEN_HANG === "1"; |
| 48 | const emitPlanThenHang = process.env.T3_ACP_EMIT_PLAN_THEN_HANG === "1"; |
| 49 | const emitActiveToolThenHang = process.env.T3_ACP_EMIT_ACTIVE_TOOL_THEN_HANG === "1"; |
| 50 | const emitGrokMonitorPostTurnPoll = process.env.T3_ACP_EMIT_GROK_MONITOR_POST_TURN_POLL === "1"; |
| 51 | const emitGrokBackgroundTaskStarted = process.env.T3_ACP_EMIT_GROK_BACKGROUND_TASK_STARTED === "1"; |
| 52 | const emitForeignSessionUpdates = process.env.T3_ACP_EMIT_FOREIGN_SESSION_UPDATES === "1"; |
| 53 | const waitForResumeRelease = process.env.T3_ACP_WAIT_FOR_RESUME_RELEASE === "1"; |
| 54 | const completeFirstPromptOnCancel = process.env.T3_ACP_COMPLETE_FIRST_PROMPT_ON_CANCEL === "1"; |
| 55 | const floodStderr = process.env.T3_ACP_FLOOD_STDERR === "1"; |
| 56 | const hangPromptForever = process.env.T3_ACP_HANG_PROMPT_FOREVER === "1"; |
| 57 | // Sends fs/write_text_file for this path, then fs/read_text_file, at the start of |
| 58 | // each prompt whatever the client advertised, and appends each outcome as a JSON |
| 59 | // line to T3_ACP_CLIENT_FS_PROBE_LOG_PATH. |
| 60 | const clientFsProbePath = process.env.T3_ACP_CLIENT_FS_PROBE_PATH; |
| 61 | const clientFsProbeLogPath = process.env.T3_ACP_CLIENT_FS_PROBE_LOG_PATH; |
| 62 | const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)); |
| 63 | const hangAfterPermission = process.env.T3_ACP_HANG_AFTER_PERMISSION === "1"; |
| 64 | const hangFirstPromptForever = process.env.T3_ACP_HANG_FIRST_PROMPT_FOREVER === "1"; |
| 65 | const emitLateUpdateAfterCancel = process.env.T3_ACP_EMIT_LATE_UPDATE_AFTER_CANCEL === "1"; |
| 66 | const emitTaskBackgroundedAfterCancel = |
| 67 | process.env.T3_ACP_EMIT_TASK_BACKGROUNDED_AFTER_CANCEL === "1"; |
| 68 | const residualCallbackResponseLogPath = process.env.T3_ACP_RESIDUAL_CALLBACK_RESPONSE_LOG_PATH; |
| 69 | const residualCallbackTriggerPath = process.env.T3_ACP_RESIDUAL_CALLBACK_TRIGGER_PATH; |
| 70 | const exitAfterResidualCallbacks = process.env.T3_ACP_EXIT_AFTER_RESIDUAL_CALLBACKS === "1"; |
| 71 | const emitRunningCommandThenHang = process.env.T3_ACP_EMIT_RUNNING_COMMAND_THEN_HANG === "1"; |
| 72 | const emitRunningCommandThenHangOnFirstPrompt = |
| 73 | process.env.T3_ACP_EMIT_RUNNING_COMMAND_THEN_HANG_FIRST_PROMPT === "1"; |
| 74 | const emitEmptySuccessfulBash = process.env.T3_ACP_EMIT_EMPTY_SUCCESSFUL_BASH === "1"; |
| 75 | const emitEmptySuccessfulBashThenHang = |
| 76 | process.env.T3_ACP_EMIT_EMPTY_SUCCESSFUL_BASH_THEN_HANG === "1"; |
| 77 | const exitOnCancel = process.env.T3_ACP_EXIT_ON_CANCEL === "1"; |
| 78 | const runningCommandIgnoresTerm = process.env.T3_ACP_RUNNING_COMMAND_IGNORE_TERM === "1"; |
| 79 | const runningCommandPidPath = process.env.T3_ACP_RUNNING_COMMAND_PID_PATH; |
| 80 | const runningCommandSeparateSession = process.env.T3_ACP_RUNNING_COMMAND_SEPARATE_SESSION === "1"; |
| 81 | const exitAfterRunningCommandLaunch = process.env.T3_ACP_EXIT_AFTER_RUNNING_COMMAND_LAUNCH === "1"; |
| 82 | const omitXAiPromptCompleteStopReason = |
| 83 | process.env.T3_ACP_OMIT_XAI_PROMPT_COMPLETE_STOP_REASON === "1"; |
| 84 | const failLoadSession = process.env.T3_ACP_FAIL_LOAD_SESSION === "1"; |
| 85 | const failLoadSessionAfterConfigReplay = |
| 86 | process.env.T3_ACP_FAIL_LOAD_SESSION_AFTER_CONFIG_REPLAY === "1"; |
| 87 | const emitLoadReplay = process.env.T3_ACP_EMIT_LOAD_REPLAY === "1"; |
| 88 | const hangLoadSessionAfterReplay = process.env.T3_ACP_HANG_LOAD_SESSION_AFTER_REPLAY === "1"; |
| 89 | const delayLoadSessionAfterReplay = process.env.T3_ACP_DELAY_LOAD_SESSION_AFTER_REPLAY === "1"; |
| 90 | const loadSessionDelayMs = Number(process.env.T3_ACP_LOAD_SESSION_DELAY_MS ?? "5000"); |
| 91 | const emitStaleXAiPromptCompleteBeforeSecondHang = |
| 92 | process.env.T3_ACP_EMIT_STALE_XAI_PROMPT_COMPLETE_BEFORE_SECOND_HANG === "1"; |
| 93 | const emitOverlappingXAiPromptCompleteOutOfOrder = |
| 94 | process.env.T3_ACP_EMIT_OVERLAPPING_XAI_PROMPT_COMPLETE_OUT_OF_ORDER === "1"; |
| 95 | const failPrompt = process.env.T3_ACP_FAIL_PROMPT === "1"; |
| 96 | const failSetConfigOption = process.env.T3_ACP_FAIL_SET_CONFIG_OPTION === "1"; |
| 97 | const failSetModeConfigOption = process.env.T3_ACP_FAIL_SET_MODE_CONFIG_OPTION === "1"; |
| 98 | const exitOnSetConfigOption = process.env.T3_ACP_EXIT_ON_SET_CONFIG_OPTION === "1"; |
| 99 | const omitModelConfigOption = process.env.T3_ACP_OMIT_MODEL_CONFIG_OPTION === "1"; |
| 100 | const emitEmptyDefaultAgentOption = process.env.T3_ACP_EMIT_EMPTY_DEFAULT_AGENT_OPTION === "1"; |
| 101 | const promptResponseText = process.env.T3_ACP_PROMPT_RESPONSE_TEXT; |
| 102 | const initialGrokReasoningEffort = |
| 103 | process.env.T3_ACP_INITIAL_GROK_REASONING_EFFORT?.trim() || undefined; |
| 104 | const promptDelayMs = Number(process.env.T3_ACP_PROMPT_DELAY_MS ?? "0"); |
| 105 | const supportsSessionLifecycle = process.env.T3_ACP_SESSION_LIFECYCLE === "1"; |
| 106 | const supportsAcpMcp = process.env.T3_ACP_MCP_ACP === "1"; |
| 107 | const supportsV2Management = process.env.T3_ACP_V2_MANAGEMENT === "1"; |
| 108 | const omitSessionListHandler = process.env.T3_ACP_OMIT_SESSION_LIST_HANDLER === "1"; |
| 109 | const advertisedAuthMethodId = process.env.T3_ACP_AUTH_METHOD_ID?.trim(); |
| 110 | const initializeAuthMethodId = |
| 111 | advertisedAuthMethodId ?? (supportsSessionLifecycle ? "test" : undefined); |
| 112 | const requiresAuthentication = process.env.T3_ACP_REQUIRE_AUTH === "1"; |
| 113 | const commandAdvertisementDelayMs = Number( |
| 114 | process.env.T3_ACP_COMMAND_ADVERTISEMENT_DELAY_MS ?? "-1", |
| 115 | ); |
| 116 | const permissionOptionIds = { |
| 117 | allowOnce: process.env.T3_ACP_ALLOW_ONCE_OPTION_ID ?? "allow-once", |
| 118 | allowAlways: process.env.T3_ACP_ALLOW_ALWAYS_OPTION_ID ?? "allow-always", |
| 119 | rejectOnce: process.env.T3_ACP_REJECT_ONCE_OPTION_ID ?? "reject-once", |
| 120 | }; |
| 121 | const omitAllowAlways = process.env.T3_ACP_OMIT_ALLOW_ALWAYS === "1"; |
| 122 | const permissionRequestCount = Math.max( |
| 123 | 1, |
| 124 | Number(process.env.T3_ACP_PERMISSION_REQUEST_COUNT ?? "1") || 1, |
| 125 | ); |
| 126 | const sessionId = "mock-session-1"; |
| 127 | |
| 128 | let currentModeId = antigravityProfile ? "default" : "ask"; |
| 129 | let currentModelId = antigravityProfile ? "gemini-test-low" : "default"; |
| 130 | let parameterizedModelPicker = false; |
| 131 | let currentReasoning = "medium"; |
| 132 | let currentContext = "272k"; |
| 133 | let currentFast = false; |
| 134 | let currentAgent = ""; |
| 135 | let authenticated = !requiresAuthentication; |
| 136 | let promptCount = 0; |
| 137 | let overlappingFirstPromptId: string | undefined; |
| 138 | const cancelledSessions = new Set<string>(); |
| 139 | let configuredProvider: AcpSchema.ProviderCurrentConfig | null = null; |
| 140 | |
| 141 | function promptIdFromRequestMeta( |
| 142 | request: Pick<AcpSchema.PromptRequest, "_meta">, |
| 143 | ): string | undefined { |
| 144 | const meta = request._meta; |
| 145 | if (meta === null || typeof meta !== "object") { |
| 146 | return undefined; |
| 147 | } |
| 148 | const promptId = meta.promptId ?? meta.requestId; |
| 149 | return typeof promptId === "string" && promptId.length > 0 ? promptId : undefined; |
| 150 | } |
| 151 | |
| 152 | function logExit(reason: string): void { |
| 153 | if (!exitLogPath) { |
| 154 | return; |
| 155 | } |
| 156 | NodeFS.appendFileSync(exitLogPath, `${reason}\n`, "utf8"); |
| 157 | } |
| 158 | |
| 159 | function writeJsonRpcNotification(method: string, params: unknown): void { |
| 160 | process.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}\n`); |
| 161 | } |
| 162 | |
| 163 | function logResidualCallbackResponse(kind: string): void { |
| 164 | if (!residualCallbackResponseLogPath) return; |
| 165 | NodeFS.appendFileSync(residualCallbackResponseLogPath, `${kind}\n`, "utf8"); |
| 166 | } |
| 167 | |
| 168 | process.once("SIGTERM", () => { |
| 169 | logExit("SIGTERM"); |
| 170 | process.exit(0); |
| 171 | }); |
| 172 | |
| 173 | process.once("SIGINT", () => { |
| 174 | logExit("SIGINT"); |
| 175 | process.exit(0); |
| 176 | }); |
| 177 | |
| 178 | process.once("exit", (code) => { |
| 179 | logExit(`exit:${code}`); |
| 180 | }); |
| 181 | |
| 182 | function configOptions(): ReadonlyArray<AcpSchema.SessionConfigOption> { |
| 183 | if (omitModelConfigOption) { |
| 184 | return []; |
| 185 | } |
| 186 | if (antigravityProfile) { |
| 187 | return [ |
| 188 | { |
| 189 | configId: "model", |
| 190 | name: "Model", |
| 191 | category: "model", |
| 192 | type: "select", |
| 193 | currentValue: currentModelId, |
| 194 | options: antigravityModels.map((model) => ({ value: model.modelId, name: model.name })), |
| 195 | }, |
| 196 | { |
| 197 | configId: "mode", |
| 198 | name: "Mode", |
| 199 | category: "mode", |
| 200 | type: "select", |
| 201 | currentValue: currentModeId, |
| 202 | options: availableModes.map((mode) => ({ value: mode.id, name: mode.name })), |
| 203 | }, |
| 204 | ]; |
| 205 | } |
| 206 | if (parameterizedModelPicker) { |
| 207 | const baseOptions: Array<AcpSchema.SessionConfigOption> = [ |
| 208 | { |
| 209 | configId: "mode", |
| 210 | name: "Mode", |
| 211 | category: "mode", |
| 212 | type: "select", |
| 213 | currentValue: currentModeId, |
| 214 | options: availableModes.map((mode) => ({ |
| 215 | value: mode.id, |
| 216 | name: mode.name, |
| 217 | ...(mode.description ? { description: mode.description } : {}), |
| 218 | })), |
| 219 | }, |
| 220 | { |
| 221 | configId: "model", |
| 222 | name: "Model", |
| 223 | category: "model", |
| 224 | type: "select", |
| 225 | currentValue: currentModelId, |
| 226 | options: [ |
| 227 | { value: "default", name: "Auto" }, |
| 228 | { value: "composer-2", name: "Composer 2" }, |
| 229 | { value: "gpt-5.4", name: "GPT-5.4" }, |
| 230 | { value: "claude-opus-4-6", name: "Opus 4.6" }, |
| 231 | ], |
| 232 | }, |
| 233 | ]; |
| 234 | |
| 235 | switch (currentModelId) { |
| 236 | case "gpt-5.4": |
| 237 | return [ |
| 238 | ...baseOptions, |
| 239 | { |
| 240 | configId: "reasoning", |
| 241 | name: "Reasoning", |
| 242 | category: "thought_level", |
| 243 | type: "select", |
| 244 | currentValue: currentReasoning, |
| 245 | options: [ |
| 246 | { value: "none", name: "None" }, |
| 247 | { value: "low", name: "Low" }, |
| 248 | { value: "medium", name: "Medium" }, |
| 249 | { value: "high", name: "High" }, |
| 250 | { value: "extra-high", name: "Extra High" }, |
| 251 | ], |
| 252 | }, |
| 253 | { |
| 254 | configId: "context", |
| 255 | name: "Context", |
| 256 | category: "model_config", |
| 257 | type: "select", |
| 258 | currentValue: currentContext, |
| 259 | options: [ |
| 260 | { value: "272k", name: "272K" }, |
| 261 | { value: "1m", name: "1M" }, |
| 262 | ], |
| 263 | }, |
| 264 | { |
| 265 | configId: "fast", |
| 266 | name: "Fast", |
| 267 | category: "model_config", |
| 268 | type: "select", |
| 269 | currentValue: String(currentFast), |
| 270 | options: [ |
| 271 | { value: "false", name: "Off" }, |
| 272 | { value: "true", name: "Fast" }, |
| 273 | ], |
| 274 | }, |
| 275 | ]; |
| 276 | case "composer-2": |
| 277 | return [ |
| 278 | ...baseOptions, |
| 279 | { |
| 280 | configId: "fast", |
| 281 | name: "Fast", |
| 282 | category: "model_config", |
| 283 | type: "select", |
| 284 | currentValue: String(currentFast), |
| 285 | options: [ |
| 286 | { value: "false", name: "Off" }, |
| 287 | { value: "true", name: "Fast" }, |
| 288 | ], |
| 289 | }, |
| 290 | ]; |
| 291 | case "claude-opus-4-6": |
| 292 | return [ |
| 293 | ...baseOptions, |
| 294 | { |
| 295 | configId: "reasoning", |
| 296 | name: "Reasoning", |
| 297 | category: "thought_level", |
| 298 | type: "select", |
| 299 | currentValue: currentReasoning, |
| 300 | options: [ |
| 301 | { value: "low", name: "Low" }, |
| 302 | { value: "medium", name: "Medium" }, |
| 303 | { value: "high", name: "High" }, |
| 304 | ], |
| 305 | }, |
| 306 | { |
| 307 | configId: "thinking", |
| 308 | name: "Thinking", |
| 309 | category: "model_config", |
| 310 | type: "boolean", |
| 311 | currentValue: true, |
| 312 | }, |
| 313 | ]; |
| 314 | default: |
| 315 | return baseOptions; |
| 316 | } |
| 317 | } |
| 318 | |
| 319 | return [ |
| 320 | // Mirrors GitHub Copilot, whose default agent is the empty-valued choice. |
| 321 | ...(emitEmptyDefaultAgentOption |
| 322 | ? [ |
| 323 | { |
| 324 | configId: "agent", |
| 325 | name: "Agent", |
| 326 | category: "_agent", |
| 327 | type: "select" as const, |
| 328 | currentValue: currentAgent, |
| 329 | options: [ |
| 330 | { value: "", name: "Copilot" }, |
| 331 | { value: "example", name: "example" }, |
| 332 | ], |
| 333 | }, |
| 334 | ] |
| 335 | : []), |
| 336 | { |
| 337 | configId: "mode", |
| 338 | name: "Mode", |
| 339 | category: "mode", |
| 340 | type: "select" as const, |
| 341 | currentValue: currentModeId, |
| 342 | options: availableModes.map((mode) => ({ |
| 343 | value: mode.id, |
| 344 | name: mode.name, |
| 345 | ...(mode.description ? { description: mode.description } : {}), |
| 346 | })), |
| 347 | }, |
| 348 | { |
| 349 | configId: "model", |
| 350 | name: "Model", |
| 351 | category: "model", |
| 352 | type: "select" as const, |
| 353 | currentValue: currentModelId, |
| 354 | options: [ |
| 355 | { value: "default", name: "Auto" }, |
| 356 | { value: "composer-2", name: "Composer 2" }, |
| 357 | { value: "composer-2[fast=true]", name: "Composer 2 Fast" }, |
| 358 | { value: "gpt-5.3-codex[reasoning=medium,fast=false]", name: "Codex 5.3" }, |
| 359 | ], |
| 360 | }, |
| 361 | ]; |
| 362 | } |
| 363 | |
| 364 | function modelConfigOptionsFor(modelId: string): ReadonlyArray<AcpSchema.SessionConfigOption> { |
| 365 | const previousModelId = currentModelId; |
| 366 | try { |
| 367 | currentModelId = modelId; |
| 368 | return configOptions().filter( |
| 369 | (option) => option.category !== "mode" && option.category !== "model", |
| 370 | ); |
| 371 | } finally { |
| 372 | currentModelId = previousModelId; |
| 373 | } |
| 374 | } |
| 375 | |
| 376 | function availableModels(): ReadonlyArray<{ |
| 377 | readonly value: string; |
| 378 | readonly name: string; |
| 379 | readonly configOptions: ReadonlyArray<AcpSchema.SessionConfigOption>; |
| 380 | }> { |
| 381 | return [ |
| 382 | { value: "default", name: "Auto" }, |
| 383 | { value: "composer-2", name: "Composer 2" }, |
| 384 | { value: "gpt-5.4", name: "GPT-5.4" }, |
| 385 | { value: "claude-opus-4-6", name: "Opus 4.6" }, |
| 386 | ].map((model) => ({ |
| 387 | value: model.value, |
| 388 | name: model.name, |
| 389 | configOptions: modelConfigOptionsFor(model.value), |
| 390 | })); |
| 391 | } |
| 392 | |
| 393 | const antigravityModels = [ |
| 394 | { modelId: "gemini-test-low", name: "Gemini Test Low" }, |
| 395 | { modelId: "gemini-test-high", name: "Gemini Test High" }, |
| 396 | ] satisfies ReadonlyArray<AcpCompat.ModelInfo>; |
| 397 | |
| 398 | const availableModes: ReadonlyArray<AcpCompat.SessionMode> = antigravityProfile |
| 399 | ? [ |
| 400 | { id: "default", name: "Default" }, |
| 401 | { id: "auto_edit", name: "Auto edit" }, |
| 402 | { id: "yolo", name: "YOLO" }, |
| 403 | ] |
| 404 | : [ |
| 405 | { |
| 406 | id: "ask", |
| 407 | name: "Ask", |
| 408 | description: "Request permission before making any changes", |
| 409 | }, |
| 410 | { |
| 411 | id: "architect", |
| 412 | name: "Architect", |
| 413 | description: "Design and plan software systems without implementation", |
| 414 | }, |
| 415 | { |
| 416 | id: "code", |
| 417 | name: "Code", |
| 418 | description: "Write and modify code with full tool access", |
| 419 | }, |
| 420 | ]; |
| 421 | |
| 422 | function modeState(): AcpCompat.SessionModeState { |
| 423 | return { |
| 424 | currentModeId, |
| 425 | availableModes, |
| 426 | }; |
| 427 | } |
| 428 | |
| 429 | // Mirrors the real Grok ACP: it advertises versioned model ids, never the CLI's own |
| 430 | // "grok-build" product name. Native helper model switching has its own v1 fixture. |
| 431 | const grokAcpModels: ReadonlyArray<AcpCompat.ModelInfo> = [ |
| 432 | { |
| 433 | modelId: "grok-4.6", |
| 434 | name: "Grok 4.6", |
| 435 | _meta: { |
| 436 | totalContextTokens: 500_000, |
| 437 | supportsReasoningEffort: true, |
| 438 | reasoningEffort: initialGrokReasoningEffort ?? "high", |
| 439 | reasoningEfforts: [ |
| 440 | { id: "xhigh", value: "xhigh", label: "Extra High Effort", default: false }, |
| 441 | { id: "high", value: "high", label: "High Effort", default: true }, |
| 442 | { id: "low", value: "low", label: "Low Effort", default: false }, |
| 443 | ], |
| 444 | }, |
| 445 | }, |
| 446 | { modelId: "grok-mock-alt", name: "Grok Mock Alt" }, |
| 447 | ]; |
| 448 | |
| 449 | function modelState(): AcpCompat.SessionModelState { |
| 450 | if (antigravityProfile) { |
| 451 | return { currentModelId, availableModels: antigravityModels }; |
| 452 | } |
| 453 | const modelId = grokAcpModels.some((model) => model.modelId === currentModelId) |
| 454 | ? currentModelId |
| 455 | : "grok-4.6"; |
| 456 | return { |
| 457 | currentModelId: modelId, |
| 458 | availableModels: grokAcpModels, |
| 459 | }; |
| 460 | } |
| 461 | |
| 462 | const program = Effect.gen(function* () { |
| 463 | const agent = yield* EffectAcpAgent.AcpAgent; |
| 464 | const resumeRelease = yield* Deferred.make<void>(); |
| 465 | const nativeCancelRequested = yield* Deferred.make<void>(); |
| 466 | const nativeCancelRelease = yield* Deferred.make<void>(); |
| 467 | const publishAntigravityCommands = (targetSessionId: string) => |
| 468 | agent.client.sessionUpdate({ |
| 469 | sessionId: targetSessionId, |
| 470 | update: { |
| 471 | sessionUpdate: "available_commands_update", |
| 472 | availableCommands: [ |
| 473 | { name: "plan", description: "Plan a task", input: { hint: "task" } }, |
| 474 | { name: "logout", description: "Sign out" }, |
| 475 | ], |
| 476 | }, |
| 477 | }); |
| 478 | |
| 479 | const finishPrompt = ( |
| 480 | targetSessionId: string, |
| 481 | stopReason: AcpSchema.StopReason, |
| 482 | _meta?: NonNullable<AcpSchema.PromptResponse["_meta"]>, |
| 483 | ) => |
| 484 | agent.client |
| 485 | .sessionUpdate({ |
| 486 | sessionId: targetSessionId, |
| 487 | update: { |
| 488 | sessionUpdate: "state_update", |
| 489 | state: "idle", |
| 490 | stopReason, |
| 491 | ...(_meta === undefined ? {} : { _meta }), |
| 492 | }, |
| 493 | }) |
| 494 | .pipe(Effect.as(_meta === undefined ? {} : { _meta })); |
| 495 | |
| 496 | yield* agent.handleInitialize((request) => |
| 497 | Effect.gen(function* () { |
| 498 | if (floodStderr) { |
| 499 | yield* Effect.promise( |
| 500 | () => |
| 501 | new Promise<void>((resolve) => { |
| 502 | process.stderr.write("stderr".repeat(350_000), () => resolve()); |
| 503 | }), |
| 504 | ); |
| 505 | } |
| 506 | parameterizedModelPicker = request.capabilities?._meta?.parameterizedModelPicker === true; |
| 507 | return { |
| 508 | protocolVersion: 2, |
| 509 | info: { name: "t3-acp-mock-agent", version: "0.0.0" }, |
| 510 | _meta: { modelState: modelState() }, |
| 511 | capabilities: { |
| 512 | session: { |
| 513 | ...(supportsSessionLifecycle ? { fork: {}, additionalDirectories: {} } : {}), |
| 514 | ...(supportsV2Management ? { delete: {} } : {}), |
| 515 | ...(supportsAcpMcp ? { mcp: { acp: {} } } : {}), |
| 516 | }, |
| 517 | ...(supportsV2Management ? { providers: {} } : {}), |
| 518 | }, |
| 519 | ...(initializeAuthMethodId |
| 520 | ? { |
| 521 | authMethods: [ |
| 522 | { |
| 523 | type: "agent" as const, |
| 524 | methodId: initializeAuthMethodId, |
| 525 | name: "Mock agent authentication", |
| 526 | }, |
| 527 | ], |
| 528 | } |
| 529 | : {}), |
| 530 | }; |
| 531 | }), |
| 532 | ); |
| 533 | |
| 534 | // Mirrors the real Antigravity agent: the API key method reads |
| 535 | // GEMINI_API_KEY from the process environment and rejects when it is missing. |
| 536 | yield* agent.handleAuthenticate((request) => |
| 537 | Effect.gen(function* () { |
| 538 | if (antigravityProfile) { |
| 539 | if ( |
| 540 | request.methodId !== "oauth-personal" && |
| 541 | !(request.methodId === "gemini-api-key" && process.env.GEMINI_API_KEY) |
| 542 | ) { |
| 543 | return yield* AcpError.AcpRequestError.invalidParams( |
| 544 | `Mock Antigravity rejected auth method ${request.methodId}.`, |
| 545 | ); |
| 546 | } |
| 547 | } else if (advertisedAuthMethodId && request.methodId !== advertisedAuthMethodId) { |
| 548 | return yield* AcpError.AcpRequestError.invalidParams( |
| 549 | `Unknown mock authentication method: ${request.methodId}`, |
| 550 | ); |
| 551 | } |
| 552 | authenticated = true; |
| 553 | return {}; |
| 554 | }), |
| 555 | ); |
| 556 | if (antigravityProfile) { |
| 557 | yield* agent.handleLogout(() => Effect.succeed({})); |
| 558 | } |
| 559 | |
| 560 | yield* agent.handleLogout(() => |
| 561 | Effect.sync(() => { |
| 562 | authenticated = false; |
| 563 | return {}; |
| 564 | }), |
| 565 | ); |
| 566 | |
| 567 | const requireAuthentication = Effect.fn("acpMockAgent.requireAuthentication")(function* () { |
| 568 | if (!authenticated) { |
| 569 | return yield* AcpError.AcpRequestError.authRequired(); |
| 570 | } |
| 571 | }); |
| 572 | |
| 573 | yield* agent.handleCreateSession(() => |
| 574 | Effect.gen(function* () { |
| 575 | yield* requireAuthentication(); |
| 576 | if (antigravityProfile) { |
| 577 | yield* publishAntigravityCommands(sessionId); |
| 578 | } |
| 579 | if (commandAdvertisementDelayMs >= 0) { |
| 580 | yield* Effect.sleep(`${commandAdvertisementDelayMs} millis`).pipe( |
| 581 | Effect.andThen( |
| 582 | agent.client.sessionUpdate({ |
| 583 | sessionId, |
| 584 | update: { |
| 585 | sessionUpdate: "available_commands_update", |
| 586 | availableCommands: [ |
| 587 | { |
| 588 | name: "review", |
| 589 | description: "Review the current changes", |
| 590 | input: { type: "text", hint: "focus" }, |
| 591 | }, |
| 592 | { |
| 593 | name: "$workspace-skill", |
| 594 | description: "Run the workspace skill", |
| 595 | input: null, |
| 596 | }, |
| 597 | ], |
| 598 | }, |
| 599 | }), |
| 600 | ), |
| 601 | Effect.forkDetach, |
| 602 | ); |
| 603 | } |
| 604 | return { |
| 605 | sessionId, |
| 606 | configOptions: configOptions(), |
| 607 | }; |
| 608 | }), |
| 609 | ); |
| 610 | |
| 611 | yield* agent.handleResumeSession((request) => |
| 612 | Effect.gen(function* () { |
| 613 | yield* requireAuthentication(); |
| 614 | yield* agent.client.sessionUpdate({ |
| 615 | sessionId: request.sessionId, |
| 616 | update: { |
| 617 | sessionUpdate: "user_message_chunk", |
| 618 | content: { type: "text", text: "native-resume-started" }, |
| 619 | }, |
| 620 | }); |
| 621 | if (waitForResumeRelease) { |
| 622 | yield* Deferred.await(resumeRelease); |
| 623 | } |
| 624 | if (antigravityProfile) { |
| 625 | yield* publishAntigravityCommands(request.sessionId); |
| 626 | } |
| 627 | return { |
| 628 | modes: modeState(), |
| 629 | models: modelState(), |
| 630 | configOptions: configOptions(), |
| 631 | _meta: { nativeResume: true }, |
| 632 | }; |
| 633 | }), |
| 634 | ); |
| 635 | |
| 636 | const emitLoadReplayNotifications = (requestedSessionId: string) => { |
| 637 | writeJsonRpcNotification("session/update", { |
| 638 | _meta: { isReplay: true }, |
| 639 | sessionId: requestedSessionId, |
| 640 | update: { |
| 641 | sessionUpdate: "tool_call_update", |
| 642 | toolCallId: "replay-tool-1", |
| 643 | title: "Replay tool", |
| 644 | kind: "search", |
| 645 | status: "completed", |
| 646 | }, |
| 647 | }); |
| 648 | writeJsonRpcNotification("session/update", { |
| 649 | _meta: { isReplay: true }, |
| 650 | sessionId: requestedSessionId, |
| 651 | update: { |
| 652 | sessionUpdate: "agent_message_chunk", |
| 653 | messageId: "mock-agent-message", |
| 654 | content: { type: "text", text: "replayed assistant text" }, |
| 655 | }, |
| 656 | }); |
| 657 | }; |
| 658 | |
| 659 | yield* agent.handleLoadSession((request) => |
| 660 | Effect.gen(function* () { |
| 661 | const requestedSessionId = String(request.sessionId ?? sessionId); |
| 662 | if (failLoadSession) { |
| 663 | return yield* AcpError.AcpRequestError.internalError("Mock load session failure"); |
| 664 | } |
| 665 | if (failLoadSessionAfterConfigReplay) { |
| 666 | yield* agent.client.sessionUpdate({ |
| 667 | sessionId: requestedSessionId, |
| 668 | update: { |
| 669 | sessionUpdate: "config_option_update", |
| 670 | configOptions: [ |
| 671 | { |
| 672 | configId: "candidate-only", |
| 673 | name: "Candidate only", |
| 674 | type: "boolean", |
| 675 | currentValue: true, |
| 676 | }, |
| 677 | ], |
| 678 | }, |
| 679 | }); |
| 680 | return yield* AcpError.AcpRequestError.internalError( |
| 681 | "Mock load session failure after config replay", |
| 682 | ); |
| 683 | } |
| 684 | if (hangLoadSessionAfterReplay || delayLoadSessionAfterReplay) { |
| 685 | emitLoadReplayNotifications(requestedSessionId); |
| 686 | yield* agent.client.sessionUpdate({ |
| 687 | sessionId: requestedSessionId, |
| 688 | update: { |
| 689 | sessionUpdate: "user_message_chunk", |
| 690 | messageId: "mock-user-message", |
| 691 | content: { type: "text", text: "replay-tail" }, |
| 692 | }, |
| 693 | }); |
| 694 | yield* Effect.sleep(loadSessionDelayMs); |
| 695 | return { |
| 696 | configOptions: configOptions(), |
| 697 | }; |
| 698 | } |
| 699 | if (emitLoadReplay) { |
| 700 | emitLoadReplayNotifications(requestedSessionId); |
| 701 | } |
| 702 | yield* agent.client.sessionUpdate({ |
| 703 | sessionId: requestedSessionId, |
| 704 | update: { |
| 705 | sessionUpdate: "user_message_chunk", |
| 706 | messageId: "mock-user-message", |
| 707 | content: { type: "text", text: "replay" }, |
| 708 | }, |
| 709 | }); |
| 710 | return { |
| 711 | configOptions: configOptions(), |
| 712 | }; |
| 713 | }), |
| 714 | ); |
| 715 | |
| 716 | if (!omitSessionListHandler) { |
| 717 | yield* agent.handleListSessions((request) => |
| 718 | Effect.gen(function* () { |
| 719 | yield* requireAuthentication(); |
| 720 | return { |
| 721 | sessions: [ |
| 722 | { |
| 723 | sessionId, |
| 724 | cwd: request.cwd ?? process.cwd(), |
| 725 | title: "Mock session", |
| 726 | updatedAt: "1970-01-01T00:00:00.000Z", |
| 727 | }, |
| 728 | ], |
| 729 | }; |
| 730 | }), |
| 731 | ); |
| 732 | } |
| 733 | |
| 734 | yield* agent.handleForkSession((request) => |
| 735 | Effect.gen(function* () { |
| 736 | yield* requireAuthentication(); |
| 737 | return { |
| 738 | sessionId: `${request.sessionId}-fork`, |
| 739 | configOptions: configOptions(), |
| 740 | }; |
| 741 | }), |
| 742 | ); |
| 743 | |
| 744 | yield* agent.handleCloseSession(() => |
| 745 | Effect.gen(function* () { |
| 746 | yield* requireAuthentication(); |
| 747 | return {}; |
| 748 | }), |
| 749 | ); |
| 750 | |
| 751 | yield* agent.handleDeleteSession(() => |
| 752 | Effect.gen(function* () { |
| 753 | yield* requireAuthentication(); |
| 754 | return {}; |
| 755 | }), |
| 756 | ); |
| 757 | |
| 758 | yield* agent.handleListProviders(() => |
| 759 | Effect.gen(function* () { |
| 760 | yield* requireAuthentication(); |
| 761 | return { |
| 762 | providers: [ |
| 763 | { |
| 764 | providerId: "mock-provider", |
| 765 | supported: ["openai", "anthropic"], |
| 766 | required: false, |
| 767 | current: configuredProvider, |
| 768 | }, |
| 769 | ], |
| 770 | }; |
| 771 | }), |
| 772 | ); |
| 773 | |
| 774 | yield* agent.handleSetProvider((request) => |
| 775 | Effect.gen(function* () { |
| 776 | yield* requireAuthentication(); |
| 777 | configuredProvider = { apiType: request.apiType, baseUrl: request.baseUrl }; |
| 778 | return {}; |
| 779 | }), |
| 780 | ); |
| 781 | |
| 782 | yield* agent.handleDisableProvider(() => |
| 783 | Effect.gen(function* () { |
| 784 | yield* requireAuthentication(); |
| 785 | configuredProvider = null; |
| 786 | return {}; |
| 787 | }), |
| 788 | ); |
| 789 | |
| 790 | yield* agent.handleSetSessionConfigOption((request) => |
| 791 | Effect.gen(function* () { |
| 792 | if (exitOnSetConfigOption) { |
| 793 | return yield* Effect.sync(() => { |
| 794 | process.exit(7); |
| 795 | }); |
| 796 | } |
| 797 | if (failSetConfigOption || (failSetModeConfigOption && request.configId === "mode")) { |
| 798 | return yield* AcpError.AcpRequestError.invalidParams( |
| 799 | "Mock invalid params for session/set_config_option", |
| 800 | { |
| 801 | method: "session/set_config_option", |
| 802 | params: request, |
| 803 | }, |
| 804 | ); |
| 805 | } |
| 806 | if (request.configId === "mode" && typeof request.value === "string") { |
| 807 | currentModeId = request.value; |
| 808 | } |
| 809 | if (request.configId === "model" && typeof request.value === "string") { |
| 810 | currentModelId = request.value; |
| 811 | } |
| 812 | if (request.configId === "agent" && typeof request.value === "string") { |
| 813 | currentAgent = request.value; |
| 814 | } |
| 815 | if (request.configId === "reasoning" && typeof request.value === "string") { |
| 816 | currentReasoning = request.value; |
| 817 | } |
| 818 | if (request.configId === "context" && typeof request.value === "string") { |
| 819 | currentContext = request.value; |
| 820 | } |
| 821 | if (request.configId === "fast") { |
| 822 | currentFast = request.value === true || request.value === "true"; |
| 823 | } |
| 824 | return { |
| 825 | configOptions: configOptions(), |
| 826 | }; |
| 827 | }), |
| 828 | ); |
| 829 | |
| 830 | yield* agent.handleCancel(({ sessionId }) => |
| 831 | Effect.gen(function* () { |
| 832 | const cancelledSessionId = String(sessionId ?? "mock-session-1"); |
| 833 | cancelledSessions.add(cancelledSessionId); |
| 834 | if (exitOnCancel) { |
| 835 | return yield* Effect.sync(() => process.exit(0)); |
| 836 | } |
| 837 | if (completeFirstPromptOnCancel) { |
| 838 | yield* Deferred.succeed(nativeCancelRequested, undefined); |
| 839 | yield* agent.client.sessionUpdate({ |
| 840 | sessionId: cancelledSessionId, |
| 841 | update: { |
| 842 | sessionUpdate: "agent_thought_chunk", |
| 843 | content: { type: "text", text: "native-cancel-received" }, |
| 844 | }, |
| 845 | }); |
| 846 | } |
| 847 | if (emitLateUpdateAfterCancel) { |
| 848 | yield* Effect.sleep("50 millis"); |
| 849 | yield* Effect.sync(() => { |
| 850 | writeJsonRpcNotification("session/update", { |
| 851 | sessionId: cancelledSessionId, |
| 852 | update: { |
| 853 | sessionUpdate: "agent_message_chunk", |
| 854 | messageId: "mock-agent-message", |
| 855 | content: { type: "text", text: "late after cancel" }, |
| 856 | }, |
| 857 | }); |
| 858 | }); |
| 859 | } |
| 860 | if (emitTaskBackgroundedAfterCancel) { |
| 861 | // Grok cancel-as-detach: the foreground command is re-run as a |
| 862 | // background task that later completes on its own. |
| 863 | yield* Effect.sync(() => { |
| 864 | writeJsonRpcNotification("_x.ai/task_backgrounded", { |
| 865 | sessionId: cancelledSessionId, |
| 866 | update: { |
| 867 | sessionUpdate: "task_backgrounded", |
| 868 | tool_call_id: "task-bg-1", |
| 869 | task_id: "task-bg-1", |
| 870 | command: "sleep 30", |
| 871 | }, |
| 872 | }); |
| 873 | }); |
| 874 | yield* Effect.sleep("1200 millis") |
| 875 | .pipe( |
| 876 | Effect.andThen( |
| 877 | Effect.sync(() => { |
| 878 | writeJsonRpcNotification("_x.ai/task_completed", { |
| 879 | sessionId: cancelledSessionId, |
| 880 | update: { |
| 881 | sessionUpdate: "task_completed", |
| 882 | task_snapshot: { task_id: "task-bg-1", command: "sleep 30" }, |
| 883 | }, |
| 884 | }); |
| 885 | }), |
| 886 | ), |
| 887 | ) |
| 888 | .pipe(Effect.forkDetach); |
| 889 | } |
| 890 | }), |
| 891 | ); |
| 892 | |
| 893 | yield* agent.handlePrompt((request) => |
| 894 | Effect.gen(function* () { |
| 895 | const requestedSessionId = String(request.sessionId ?? sessionId); |
| 896 | beginAcpMockPrompt(cancelledSessions, requestedSessionId); |
| 897 | promptCount += 1; |
| 898 | if ( |
| 899 | process.env.T3_ACP_CRASH_PROMPT === "1" && |
| 900 | request.prompt.some((part) => part.type === "text" && part.text === "crash now") |
| 901 | ) { |
| 902 | return yield* Effect.sync(() => process.exit(23)); |
| 903 | } |
| 904 | |
| 905 | if (clientFsProbePath !== undefined && clientFsProbeLogPath !== undefined) { |
| 906 | const probes = [ |
| 907 | [ |
| 908 | "fs/write_text_file", |
| 909 | { sessionId: requestedSessionId, path: clientFsProbePath, content: "probe" }, |
| 910 | ], |
| 911 | ["fs/read_text_file", { sessionId: requestedSessionId, path: clientFsProbePath }], |
| 912 | ] as const; |
| 913 | for (const [method, params] of probes) { |
| 914 | const outcome = yield* agent.raw.request(method, params).pipe( |
| 915 | Effect.map((result) => ({ method, result })), |
| 916 | Effect.catch((error) => |
| 917 | Effect.succeed({ |
| 918 | method, |
| 919 | errorCode: error._tag === "AcpRequestError" ? error.code : error._tag, |
| 920 | }), |
| 921 | ), |
| 922 | ); |
| 923 | NodeFS.appendFileSync(clientFsProbeLogPath, `${encodeJson(outcome)}\n`, "utf8"); |
| 924 | } |
| 925 | } |
| 926 | |
| 927 | if (vibeRetryOutcome !== undefined) { |
| 928 | if (vibeRetryOutcome === "recovered") { |
| 929 | yield* Effect.sync(() => |
| 930 | writeJsonRpcNotification("session/update", { |
| 931 | sessionId: requestedSessionId, |
| 932 | update: { |
| 933 | sessionUpdate: "tool_call", |
| 934 | toolCallId: "tool-before-retry", |
| 935 | title: "Read file", |
| 936 | kind: "read", |
| 937 | status: "in_progress", |
| 938 | }, |
| 939 | }), |
| 940 | ); |
| 941 | } |
| 942 | for (const [index, noticeSessionId] of [ |
| 943 | "unrelated-session", |
| 944 | requestedSessionId, |
| 945 | requestedSessionId, |
| 946 | ].entries()) { |
| 947 | // Progress from an earlier tool must not end the retry. |
| 948 | if (index === 2 && vibeRetryOutcome === "recovered") { |
| 949 | yield* Effect.sync(() => |
| 950 | writeJsonRpcNotification("session/update", { |
| 951 | sessionId: requestedSessionId, |
| 952 | update: { |
| 953 | sessionUpdate: "tool_call_update", |
| 954 | toolCallId: "tool-before-retry", |
| 955 | status: "in_progress", |
| 956 | rawOutput: { progress: "still reading" }, |
| 957 | }, |
| 958 | }), |
| 959 | ); |
| 960 | } |
| 961 | yield* Effect.sync(() => |
| 962 | writeJsonRpcNotification("_session/retrying", { |
| 963 | sessionId: noticeSessionId, |
| 964 | category: "rate_limited", |
| 965 | detail: "Rate limit reached. Retrying. api_key=private-key", |
| 966 | }), |
| 967 | ); |
| 968 | } |
| 969 | if (vibeRetryOutcome === "failed") { |
| 970 | return yield* new AcpError.AcpRequestError({ |
| 971 | code: -31001, |
| 972 | errorMessage: "Rate limit exceeded for mistral (model: mistral-vibe-cli-latest).", |
| 973 | }); |
| 974 | } |
| 975 | if (vibeRetryOutcome === "completed") { |
| 976 | return yield* finishPrompt(requestedSessionId, "end_turn"); |
| 977 | } |
| 978 | if (vibeRetryOutcome === "cancelled") { |
| 979 | return yield* finishPrompt(requestedSessionId, "cancelled"); |
| 980 | } |
| 981 | yield* agent.client.sessionUpdate({ |
| 982 | sessionId: requestedSessionId, |
| 983 | update: { |
| 984 | sessionUpdate: "agent_message_chunk", |
| 985 | content: { type: "text", text: "Recovered answer" }, |
| 986 | }, |
| 987 | }); |
| 988 | return yield* finishPrompt(requestedSessionId, "end_turn"); |
| 989 | } |
| 990 | |
| 991 | if (emitV2Fidelity) { |
| 992 | yield* agent.client.sessionUpdate({ |
| 993 | sessionId: `${requestedSessionId}-child`, |
| 994 | update: { |
| 995 | sessionUpdate: "terminal_update", |
| 996 | terminalId: "standalone-terminal", |
| 997 | command: "printf child", |
| 998 | output: { data: Buffer.from("child").toString("base64") }, |
| 999 | }, |
| 1000 | }); |
| 1001 | const updates: ReadonlyArray<AcpSchema.SessionUpdate> = [ |
| 1002 | { |
| 1003 | sessionUpdate: "user_message_chunk", |
| 1004 | messageId: "user-1", |
| 1005 | content: { type: "text", text: "stale user text" }, |
| 1006 | }, |
| 1007 | { sessionUpdate: "user_message", messageId: "user-1", content: null }, |
| 1008 | { |
| 1009 | sessionUpdate: "agent_thought_chunk", |
| 1010 | messageId: "thought-1", |
| 1011 | content: { type: "text", text: "stale thought" }, |
| 1012 | }, |
| 1013 | { |
| 1014 | sessionUpdate: "agent_message_chunk", |
| 1015 | messageId: "assistant-1", |
| 1016 | content: { type: "text", text: "interleaved answer" }, |
| 1017 | }, |
| 1018 | { |
| 1019 | sessionUpdate: "agent_message", |
| 1020 | messageId: "assistant-1", |
| 1021 | content: [{ type: "text", text: "authoritative answer" }], |
| 1022 | }, |
| 1023 | { sessionUpdate: "agent_message", messageId: "assistant-1" }, |
| 1024 | { |
| 1025 | sessionUpdate: "agent_thought", |
| 1026 | messageId: "thought-1", |
| 1027 | content: [{ type: "text", text: "final thought" }], |
| 1028 | }, |
| 1029 | { |
| 1030 | sessionUpdate: "plan_update", |
| 1031 | plan: { type: "markdown", planId: "plan-a", content: "# Plan A" }, |
| 1032 | }, |
| 1033 | { |
| 1034 | sessionUpdate: "plan_update", |
| 1035 | plan: { |
| 1036 | type: "items", |
| 1037 | planId: "plan-b", |
| 1038 | entries: [{ content: "Ship B", priority: "high", status: "in_progress" }], |
| 1039 | }, |
| 1040 | }, |
| 1041 | { sessionUpdate: "plan_removed", planId: "plan-a" }, |
| 1042 | { |
| 1043 | sessionUpdate: "terminal_update", |
| 1044 | terminalId: "standalone-terminal", |
| 1045 | command: "printf proof", |
| 1046 | cwd: process.cwd(), |
| 1047 | }, |
| 1048 | { |
| 1049 | sessionUpdate: "terminal_output_chunk", |
| 1050 | terminalId: "standalone-terminal", |
| 1051 | data: Buffer.from("proof").toString("base64"), |
| 1052 | }, |
| 1053 | { |
| 1054 | sessionUpdate: "terminal_update", |
| 1055 | terminalId: "standalone-terminal", |
| 1056 | exitStatus: { exitCode: 0 }, |
| 1057 | }, |
| 1058 | { sessionUpdate: "state_update", state: "requires_action" }, |
| 1059 | { sessionUpdate: "state_update", state: "running" }, |
| 1060 | { |
| 1061 | sessionUpdate: "tool_call_update", |
| 1062 | toolCallId: "structured-diff", |
| 1063 | title: "Edit files", |
| 1064 | kind: "edit", |
| 1065 | status: "completed", |
| 1066 | content: [ |
| 1067 | { |
| 1068 | type: "diff", |
| 1069 | changes: [ |
| 1070 | { |
| 1071 | operation: "move", |
| 1072 | oldPath: "/workspace/old.ts", |
| 1073 | path: "/workspace/new.ts", |
| 1074 | fileType: "text", |
| 1075 | mimeType: "text/typescript", |
| 1076 | }, |
| 1077 | ], |
| 1078 | patch: { |
| 1079 | format: "git_patch", |
| 1080 | text: "diff --git a/old.ts b/new.ts\nrename from old.ts\nrename to new.ts\n", |
| 1081 | }, |
| 1082 | }, |
| 1083 | ], |
| 1084 | }, |
| 1085 | { |
| 1086 | sessionUpdate: "tool_call_update", |
| 1087 | toolCallId: "structured-read", |
| 1088 | title: "Read `src/env.ts`", |
| 1089 | kind: "read", |
| 1090 | status: "completed", |
| 1091 | rawInput: { path: "src/env.ts" }, |
| 1092 | locations: [{ path: "src/env.ts" }], |
| 1093 | }, |
| 1094 | { |
| 1095 | sessionUpdate: "tool_call_update", |
| 1096 | toolCallId: "structured-search", |
| 1097 | title: "Grep", |
| 1098 | kind: "search", |
| 1099 | status: "completed", |
| 1100 | rawInput: { query: "TODO", path: "apps/web" }, |
| 1101 | }, |
| 1102 | // Grok backend searches: the query only arrives in the completed rawOutput. |
| 1103 | { |
| 1104 | sessionUpdate: "tool_call_update", |
| 1105 | toolCallId: "grok-x-search", |
| 1106 | title: "X search:", |
| 1107 | kind: "search", |
| 1108 | status: "in_progress", |
| 1109 | rawInput: { variant: "XSearch", backend: true }, |
| 1110 | }, |
| 1111 | { |
| 1112 | sessionUpdate: "tool_call_update", |
| 1113 | toolCallId: "grok-x-search", |
| 1114 | title: "X search:", |
| 1115 | status: "completed", |
| 1116 | rawOutput: { |
| 1117 | call_id: "xs_call-1", |
| 1118 | input: '{"query":"conversation_id:42","limit":"10","mode":"Latest"}', |
| 1119 | name: "x_keyword_search", |
| 1120 | id: "grok-x-search", |
| 1121 | }, |
| 1122 | }, |
| 1123 | { |
| 1124 | sessionUpdate: "tool_call_update", |
| 1125 | toolCallId: "grok-web-search", |
| 1126 | title: "Web search:", |
| 1127 | kind: "search", |
| 1128 | status: "completed", |
| 1129 | rawInput: { variant: "WebSearch", backend: true }, |
| 1130 | rawOutput: { |
| 1131 | action: { |
| 1132 | type: "search", |
| 1133 | query: "t3 code", |
| 1134 | sources: [ |
| 1135 | { type: "url", url: "https://t3.codes" }, |
| 1136 | { type: "url", url: "https://t3.codes" }, |
| 1137 | { type: "url", url: "https://github.com/pingdotgg/t3code" }, |
| 1138 | ], |
| 1139 | }, |
| 1140 | id: "grok-web-search", |
| 1141 | status: "completed", |
| 1142 | }, |
| 1143 | }, |
| 1144 | { |
| 1145 | sessionUpdate: "tool_call_update", |
| 1146 | toolCallId: "grok-web-fetch", |
| 1147 | title: "Fetch: https://t3.codes", |
| 1148 | kind: "fetch", |
| 1149 | status: "completed", |
| 1150 | rawInput: { variant: "WebFetch", url: "https://t3.codes" }, |
| 1151 | rawOutput: { |
| 1152 | type: "WebFetch", |
| 1153 | Content: { url: "https://t3.codes", content: "T3 Code page" }, |
| 1154 | }, |
| 1155 | content: [{ type: "content", content: { type: "text", text: "T3 Code page" } }], |
| 1156 | }, |
| 1157 | { |
| 1158 | sessionUpdate: "tool_call_update", |
| 1159 | toolCallId: "antigravity-shell", |
| 1160 | title: "run_command", |
| 1161 | kind: "execute", |
| 1162 | status: "completed", |
| 1163 | rawInput: { command: "cat probe.txt" }, |
| 1164 | rawOutput: { commandLine: "cat probe.txt", exitCode: 0, combinedOutput: "after\n" }, |
| 1165 | }, |
| 1166 | { |
| 1167 | sessionUpdate: "compaction_update", |
| 1168 | compactionId: "compact-1", |
| 1169 | status: "in_progress", |
| 1170 | }, |
| 1171 | { |
| 1172 | sessionUpdate: "compaction_summary_chunk", |
| 1173 | compactionId: "compact-1", |
| 1174 | content: { type: "text", text: "Retained decisions." }, |
| 1175 | }, |
| 1176 | { |
| 1177 | sessionUpdate: "compaction_update", |
| 1178 | compactionId: "compact-1", |
| 1179 | status: "completed", |
| 1180 | summary: [{ type: "text", text: "Retained decisions." }], |
| 1181 | }, |
| 1182 | ]; |
| 1183 | for (const update of updates) { |
| 1184 | yield* agent.client.sessionUpdate({ sessionId: requestedSessionId, update }); |
| 1185 | } |
| 1186 | return yield* finishPrompt(requestedSessionId, "end_turn"); |
| 1187 | } |
| 1188 | |
| 1189 | if (residualCallbackTriggerPath !== undefined) { |
| 1190 | yield* Effect.gen(function* () { |
| 1191 | while (!(yield* Effect.sync(() => NodeFS.existsSync(residualCallbackTriggerPath)))) { |
| 1192 | yield* Effect.sleep("20 millis"); |
| 1193 | } |
| 1194 | yield* Effect.sync(() => { |
| 1195 | writeJsonRpcNotification("session/update", { |
| 1196 | sessionId: requestedSessionId, |
| 1197 | update: { |
| 1198 | sessionUpdate: "agent_message_chunk", |
| 1199 | messageId: "mock-agent-message", |
| 1200 | content: { type: "text", text: "residual assistant callback" }, |
| 1201 | }, |
| 1202 | }); |
| 1203 | writeJsonRpcNotification("session/update", { |
| 1204 | sessionId: requestedSessionId, |
| 1205 | update: { |
| 1206 | sessionUpdate: "tool_call_update", |
| 1207 | toolCallId: "residual-tool-call", |
| 1208 | title: "Residual tool callback", |
| 1209 | kind: "other", |
| 1210 | status: "pending", |
| 1211 | rawInput: {}, |
| 1212 | }, |
| 1213 | }); |
| 1214 | writeJsonRpcNotification("session/update", { |
| 1215 | sessionId: requestedSessionId, |
| 1216 | update: { |
| 1217 | sessionUpdate: "plan_update", |
| 1218 | plan: { |
| 1219 | type: "items", |
| 1220 | planId: "mock-plan", |
| 1221 | entries: [ |
| 1222 | { content: "Residual plan callback", priority: "high", status: "pending" }, |
| 1223 | ], |
| 1224 | }, |
| 1225 | }, |
| 1226 | }); |
| 1227 | }); |
| 1228 | yield* agent.client |
| 1229 | .requestPermission({ |
| 1230 | sessionId: requestedSessionId, |
| 1231 | title: "Residual permission callback", |
| 1232 | subject: { |
| 1233 | type: "tool_call", |
| 1234 | toolCall: { |
| 1235 | toolCallId: "residual-permission", |
| 1236 | title: "Residual permission callback", |
| 1237 | }, |
| 1238 | }, |
| 1239 | options: [{ optionId: "allow", name: "Allow", kind: "allow_once" }], |
| 1240 | }) |
| 1241 | .pipe( |
| 1242 | Effect.exit, |
| 1243 | Effect.tap(() => Effect.sync(() => logResidualCallbackResponse("permission"))), |
| 1244 | Effect.ignore, |
| 1245 | Effect.forkDetach, |
| 1246 | ); |
| 1247 | yield* agent.client |
| 1248 | .elicit({ |
| 1249 | sessionId: requestedSessionId, |
| 1250 | message: "Residual elicitation callback", |
| 1251 | mode: "form", |
| 1252 | requestedSchema: { |
| 1253 | type: "object", |
| 1254 | properties: { |
| 1255 | approved: { type: "boolean", title: "Approved" }, |
| 1256 | }, |
| 1257 | }, |
| 1258 | }) |
| 1259 | .pipe( |
| 1260 | Effect.exit, |
| 1261 | Effect.tap(() => Effect.sync(() => logResidualCallbackResponse("elicitation"))), |
| 1262 | Effect.ignore, |
| 1263 | Effect.forkDetach, |
| 1264 | ); |
| 1265 | if (exitAfterResidualCallbacks) { |
| 1266 | yield* Effect.sleep("100 millis"); |
| 1267 | return yield* Effect.sync(() => process.exit(0)); |
| 1268 | } |
| 1269 | }).pipe(Effect.forkDetach); |
| 1270 | } |
| 1271 | if (completeFirstPromptOnCancel && promptCount === 1) { |
| 1272 | yield* agent.client.sessionUpdate({ |
| 1273 | sessionId: requestedSessionId, |
| 1274 | update: { |
| 1275 | sessionUpdate: "tool_call_update", |
| 1276 | toolCallId: "native-cancel-tool", |
| 1277 | title: "Long command", |
| 1278 | kind: "execute", |
| 1279 | status: "in_progress", |
| 1280 | }, |
| 1281 | }); |
| 1282 | yield* Deferred.await(nativeCancelRequested); |
| 1283 | yield* Deferred.await(nativeCancelRelease); |
| 1284 | yield* agent.client.sessionUpdate({ |
| 1285 | sessionId: requestedSessionId, |
| 1286 | update: { |
| 1287 | sessionUpdate: "tool_call_update", |
| 1288 | toolCallId: "native-cancel-tool", |
| 1289 | status: "failed", |
| 1290 | content: [{ type: "content", content: { type: "text", text: "Cancelled." } }], |
| 1291 | }, |
| 1292 | }); |
| 1293 | yield* agent.client.sessionUpdate({ |
| 1294 | sessionId: requestedSessionId, |
| 1295 | update: { |
| 1296 | sessionUpdate: "agent_message_chunk", |
| 1297 | content: { type: "text", text: "Request cancelled." }, |
| 1298 | }, |
| 1299 | }); |
| 1300 | return yield* finishPrompt(requestedSessionId, "cancelled", { nativeCancel: true }); |
| 1301 | } |
| 1302 | |
| 1303 | if (Number.isFinite(promptDelayMs) && promptDelayMs > 0) { |
| 1304 | yield* Effect.sleep(`${promptDelayMs} millis`); |
| 1305 | } |
| 1306 | |
| 1307 | if (failPrompt) { |
| 1308 | return yield* AcpError.AcpRequestError.internalError("Mock prompt failure"); |
| 1309 | } |
| 1310 | |
| 1311 | if (emitStaleXAiPromptCompleteBeforeSecondHang && promptCount === 1) { |
| 1312 | return yield* finishPrompt(requestedSessionId, "end_turn", { |
| 1313 | promptId: "mock-stale-xai-prompt-1", |
| 1314 | requestId: "mock-stale-xai-prompt-1", |
| 1315 | }); |
| 1316 | } |
| 1317 | |
| 1318 | if (emitStaleXAiPromptCompleteBeforeSecondHang && promptCount === 2) { |
| 1319 | const currentPromptId = promptIdFromRequestMeta(request) ?? "mock-current-xai-prompt-2"; |
| 1320 | writeJsonRpcNotification("_x.ai/session/prompt_complete", { |
| 1321 | sessionId: requestedSessionId, |
| 1322 | promptId: "mock-stale-xai-prompt-1", |
| 1323 | stopReason: "end_turn", |
| 1324 | agentResult: null, |
| 1325 | }); |
| 1326 | |
| 1327 | writeJsonRpcNotification("_x.ai/session/prompt_complete", { |
| 1328 | sessionId: requestedSessionId, |
| 1329 | promptId: currentPromptId, |
| 1330 | stopReason: "end_turn", |
| 1331 | agentResult: null, |
| 1332 | }); |
| 1333 | |
| 1334 | return yield* Effect.never; |
| 1335 | } |
| 1336 | |
| 1337 | if (emitOverlappingXAiPromptCompleteOutOfOrder && promptCount === 1) { |
| 1338 | overlappingFirstPromptId = promptIdFromRequestMeta(request); |
| 1339 | return yield* Effect.never; |
| 1340 | } |
| 1341 | |
| 1342 | if (emitOverlappingXAiPromptCompleteOutOfOrder && promptCount === 2) { |
| 1343 | const secondPromptId = promptIdFromRequestMeta(request); |
| 1344 | if (overlappingFirstPromptId !== undefined && secondPromptId !== undefined) { |
| 1345 | writeJsonRpcNotification("_x.ai/session/prompt_complete", { |
| 1346 | sessionId: requestedSessionId, |
| 1347 | promptId: secondPromptId, |
| 1348 | stopReason: "end_turn", |
| 1349 | agentResult: null, |
| 1350 | }); |
| 1351 | writeJsonRpcNotification("_x.ai/session/prompt_complete", { |
| 1352 | sessionId: requestedSessionId, |
| 1353 | promptId: overlappingFirstPromptId, |
| 1354 | stopReason: "end_turn", |
| 1355 | agentResult: null, |
| 1356 | }); |
| 1357 | } |
| 1358 | return yield* Effect.never; |
| 1359 | } |
| 1360 | |
| 1361 | if ( |
| 1362 | hangPromptForever || |
| 1363 | (hangFirstPromptForever && promptCount === 1) || |
| 1364 | (emitEmptySuccessfulBashThenHang && promptCount === 2) |
| 1365 | ) { |
| 1366 | return yield* Effect.never; |
| 1367 | } |
| 1368 | |
| 1369 | if ( |
| 1370 | emitRunningCommandThenHang || |
| 1371 | (emitRunningCommandThenHangOnFirstPrompt && promptCount === 1) |
| 1372 | ) { |
| 1373 | const toolCallId = "tool-call-running-1"; |
| 1374 | if (runningCommandPidPath !== undefined) { |
| 1375 | const command = runningCommandIgnoresTerm |
| 1376 | ? 'trap "" TERM; bash -c \'trap "" TERM; while :; do sleep 1; done\' & child=$!; printf "%s %s\n" "$$" "$child" > "$1"; wait "$child"' |
| 1377 | : 'sleep 120 & child=$!; printf "%s %s\n" "$$" "$child" > "$1"; wait "$child"'; |
| 1378 | if (runningCommandSeparateSession) { |
| 1379 | const launcher = [ |
| 1380 | 'const { spawn } = require("node:child_process");', |
| 1381 | "const child = spawn(process.argv[1], process.argv.slice(2), { stdio: 'ignore' });", |
| 1382 | "child.once('exit', (code, signal) => process.exitCode = code ?? (signal ? 1 : 0));", |
| 1383 | ].join(" "); |
| 1384 | const detachedCommand = runningCommandIgnoresTerm |
| 1385 | ? 'trap "" TERM; bash -c \'trap "" TERM; while :; do sleep 1; done\' & child=$!; printf "%s %s %s\n" "$PPID" "$$" "$child" > "$1"; wait "$child"' |
| 1386 | : 'sleep 120 & child=$!; printf "%s %s %s\n" "$PPID" "$$" "$child" > "$1"; wait "$child"'; |
| 1387 | // Nested bash publishes "$PPID $$ $child" once it starts. Do not |
| 1388 | // write the launcher PID alone here: that races with bash and can |
| 1389 | // clobber the triple that interrupt tests wait for. |
| 1390 | const detachedLauncher = NodeChildProcess.spawn( |
| 1391 | process.execPath, |
| 1392 | ["-e", launcher, "bash", "-c", detachedCommand, "bash", runningCommandPidPath], |
| 1393 | { detached: true, stdio: "ignore" }, |
| 1394 | ); |
| 1395 | detachedLauncher.unref(); |
| 1396 | } else { |
| 1397 | NodeChildProcess.spawn("bash", ["-c", command, "bash", runningCommandPidPath], { |
| 1398 | stdio: "ignore", |
| 1399 | }); |
| 1400 | } |
| 1401 | } |
| 1402 | yield* agent.client.sessionUpdate({ |
| 1403 | sessionId: requestedSessionId, |
| 1404 | update: { |
| 1405 | sessionUpdate: "tool_call_update", |
| 1406 | toolCallId, |
| 1407 | title: "Terminal", |
| 1408 | kind: "execute", |
| 1409 | status: "pending", |
| 1410 | rawInput: { |
| 1411 | command: ["sleep", "120"], |
| 1412 | }, |
| 1413 | }, |
| 1414 | }); |
| 1415 | yield* agent.client.sessionUpdate({ |
| 1416 | sessionId: requestedSessionId, |
| 1417 | update: { |
| 1418 | sessionUpdate: "tool_call_update", |
| 1419 | toolCallId, |
| 1420 | title: "Terminal", |
| 1421 | kind: "execute", |
| 1422 | status: "in_progress", |
| 1423 | rawInput: { |
| 1424 | command: ["sleep", "120"], |
| 1425 | }, |
| 1426 | // Grok-like mid-stream Bash re-report: exit_code 0 while still running. |
| 1427 | rawOutput: { type: "Bash", exit_code: 0 }, |
| 1428 | }, |
| 1429 | }); |
| 1430 | yield* agent.client.sessionUpdate({ |
| 1431 | sessionId: requestedSessionId, |
| 1432 | update: { |
| 1433 | sessionUpdate: "tool_call_update", |
| 1434 | toolCallId: "tool-call-output-1", |
| 1435 | title: "get_command_or_subagent_output", |
| 1436 | kind: "other", |
| 1437 | status: "pending", |
| 1438 | rawInput: { task_id: "task-running-1" }, |
| 1439 | }, |
| 1440 | }); |
| 1441 | yield* agent.client.sessionUpdate({ |
| 1442 | sessionId: requestedSessionId, |
| 1443 | update: { |
| 1444 | sessionUpdate: "tool_call_update", |
| 1445 | toolCallId: "tool-call-output-1", |
| 1446 | status: "in_progress", |
| 1447 | rawOutput: { task_id: "task-running-1", status: "running" }, |
| 1448 | }, |
| 1449 | }); |
| 1450 | if (exitAfterRunningCommandLaunch) { |
| 1451 | yield* Effect.sleep("100 millis"); |
| 1452 | return yield* Effect.sync(() => process.exit(0)); |
| 1453 | } |
| 1454 | // Stay open until session/cancel so interrupt tests can observe a running tool. |
| 1455 | while (!cancelledSessions.has(requestedSessionId)) { |
| 1456 | yield* Effect.sleep("25 millis"); |
| 1457 | } |
| 1458 | cancelledSessions.delete(requestedSessionId); |
| 1459 | return yield* finishPrompt(requestedSessionId, "cancelled"); |
| 1460 | } |
| 1461 | |
| 1462 | if (emitXAiRateLimitThenHang) { |
| 1463 | writeJsonRpcNotification("_x.ai/session/prompt_complete", { |
| 1464 | sessionId: requestedSessionId, |
| 1465 | promptId: promptIdFromRequestMeta(request) ?? "mock-xai-rate-limit-prompt-1", |
| 1466 | stopReason: "rate_limit", |
| 1467 | agentResult: null, |
| 1468 | }); |
| 1469 | return yield* Effect.never; |
| 1470 | } |
| 1471 | |
| 1472 | if (emitContentThenHang) { |
| 1473 | yield* agent.client.sessionUpdate({ |
| 1474 | sessionId: requestedSessionId, |
| 1475 | update: { |
| 1476 | sessionUpdate: "agent_message_chunk", |
| 1477 | content: { type: "text", text: "partial before stall" }, |
| 1478 | }, |
| 1479 | }); |
| 1480 | return yield* Effect.never; |
| 1481 | } |
| 1482 | |
| 1483 | if (emitPlanThenHang) { |
| 1484 | yield* agent.client.sessionUpdate({ |
| 1485 | sessionId: requestedSessionId, |
| 1486 | update: { |
| 1487 | sessionUpdate: "plan", |
| 1488 | entries: [ |
| 1489 | { |
| 1490 | content: "Wait for more ACP progress", |
| 1491 | priority: "high", |
| 1492 | status: "in_progress", |
| 1493 | }, |
| 1494 | ], |
| 1495 | }, |
| 1496 | }); |
| 1497 | return yield* Effect.never; |
| 1498 | } |
| 1499 | |
| 1500 | if (emitActiveToolThenHang) { |
| 1501 | const toolCallId = "tool-call-long-running-1"; |
| 1502 | yield* agent.client.sessionUpdate({ |
| 1503 | sessionId: requestedSessionId, |
| 1504 | update: { |
| 1505 | sessionUpdate: "tool_call_update", |
| 1506 | toolCallId, |
| 1507 | title: "Long-running tool", |
| 1508 | kind: "execute", |
| 1509 | status: "pending", |
| 1510 | rawInput: { command: ["long-running-tool"] }, |
| 1511 | }, |
| 1512 | }); |
| 1513 | yield* agent.client.sessionUpdate({ |
| 1514 | sessionId: requestedSessionId, |
| 1515 | update: { |
| 1516 | sessionUpdate: "tool_call_update", |
| 1517 | toolCallId, |
| 1518 | status: "in_progress", |
| 1519 | }, |
| 1520 | }); |
| 1521 | return yield* Effect.never; |
| 1522 | } |
| 1523 | if (emitEmptySuccessfulBash || (emitEmptySuccessfulBashThenHang && promptCount === 1)) { |
| 1524 | const update = { |
| 1525 | sessionId: requestedSessionId, |
| 1526 | update: { |
| 1527 | sessionUpdate: "tool_call_update", |
| 1528 | toolCallId: "tool-call-empty-success-1", |
| 1529 | title: "Terminal", |
| 1530 | kind: "execute", |
| 1531 | status: "completed", |
| 1532 | rawInput: { command: "true" }, |
| 1533 | rawOutput: { type: "Bash", exit_code: 0 }, |
| 1534 | }, |
| 1535 | } as const; |
| 1536 | yield* agent.client.sessionUpdate(update); |
| 1537 | yield* Effect.sleep("25 millis"); |
| 1538 | yield* agent.client.sessionUpdate(update); |
| 1539 | return yield* finishPrompt(requestedSessionId, "end_turn"); |
| 1540 | } |
| 1541 | |
| 1542 | if (emitXAiPromptCompleteThenHang) { |
| 1543 | writeJsonRpcNotification("session/update", { |
| 1544 | sessionId: requestedSessionId, |
| 1545 | update: { |
| 1546 | sessionUpdate: "agent_message_chunk", |
| 1547 | messageId: "mock-agent-message", |
| 1548 | content: { type: "text", text: "hello from" }, |
| 1549 | }, |
| 1550 | }); |
| 1551 | |
| 1552 | if (emitForeignSessionUpdates) { |
| 1553 | writeJsonRpcNotification("session/update", { |
| 1554 | sessionId: "mock-child-session-1", |
| 1555 | update: { |
| 1556 | sessionUpdate: "agent_message_chunk", |
| 1557 | messageId: "mock-agent-message", |
| 1558 | content: { type: "text", text: "child before completion" }, |
| 1559 | }, |
| 1560 | }); |
| 1561 | } |
| 1562 | |
| 1563 | writeJsonRpcNotification("_x.ai/session/prompt_complete", { |
| 1564 | sessionId: requestedSessionId, |
| 1565 | promptId: promptIdFromRequestMeta(request) ?? "mock-xai-prompt-1", |
| 1566 | ...(omitXAiPromptCompleteStopReason ? {} : { stopReason: "end_turn" }), |
| 1567 | agentResult: null, |
| 1568 | }); |
| 1569 | |
| 1570 | if (emitForeignSessionUpdates) { |
| 1571 | writeJsonRpcNotification("session/update", { |
| 1572 | sessionId: "mock-child-session-1", |
| 1573 | update: { |
| 1574 | sessionUpdate: "tool_call_update", |
| 1575 | toolCallId: "child-tool-call-1", |
| 1576 | title: "Child-only tool", |
| 1577 | kind: "other", |
| 1578 | status: "pending", |
| 1579 | rawInput: {}, |
| 1580 | }, |
| 1581 | }); |
| 1582 | writeJsonRpcNotification("session/update", { |
| 1583 | sessionId: "mock-child-session-1", |
| 1584 | update: { |
| 1585 | sessionUpdate: "agent_message_chunk", |
| 1586 | messageId: "mock-agent-message", |
| 1587 | content: { type: "text", text: "child after completion" }, |
| 1588 | }, |
| 1589 | }); |
| 1590 | } |
| 1591 | |
| 1592 | for (const text of [" ", "mo", "ck"]) { |
| 1593 | writeJsonRpcNotification("session/update", { |
| 1594 | sessionId: requestedSessionId, |
| 1595 | update: { |
| 1596 | sessionUpdate: "agent_message_chunk", |
| 1597 | messageId: "mock-agent-message", |
| 1598 | content: { type: "text", text }, |
| 1599 | }, |
| 1600 | }); |
| 1601 | } |
| 1602 | |
| 1603 | return yield* Effect.never; |
| 1604 | } |
| 1605 | |
| 1606 | if (emitGrokMonitorPostTurnPoll) { |
| 1607 | const monitorCallId = "call-monitor-1"; |
| 1608 | const pollCallId = "call-monitor-poll-1"; |
| 1609 | const taskId = "01a05f41-5107-7550-821e-79e8d1cd7687"; |
| 1610 | const description = "Watch count-sheet Typst unit until done"; |
| 1611 | writeJsonRpcNotification("session/update", { |
| 1612 | sessionId: requestedSessionId, |
| 1613 | update: { |
| 1614 | sessionUpdate: "tool_call", |
| 1615 | toolCallId: monitorCallId, |
| 1616 | title: "monitor", |
| 1617 | kind: "other", |
| 1618 | status: "pending", |
| 1619 | rawInput: { description }, |
| 1620 | _meta: { |
| 1621 | "x.ai/tool": { version: 1, name: "monitor", kind: "task", namespace: "grok_build" }, |
| 1622 | }, |
| 1623 | }, |
| 1624 | }); |
| 1625 | writeJsonRpcNotification("session/update", { |
| 1626 | sessionId: requestedSessionId, |
| 1627 | update: { |
| 1628 | sessionUpdate: "tool_call_update", |
| 1629 | toolCallId: monitorCallId, |
| 1630 | status: "completed", |
| 1631 | rawInput: { description }, |
| 1632 | rawOutput: { |
| 1633 | type: "Monitor", |
| 1634 | taskId, |
| 1635 | timeoutMs: 36_000_000, |
| 1636 | }, |
| 1637 | }, |
| 1638 | }); |
| 1639 | writeJsonRpcNotification("_x.ai/session/prompt_complete", { |
| 1640 | sessionId: requestedSessionId, |
| 1641 | promptId: promptIdFromRequestMeta(request) ?? "mock-xai-prompt-1", |
| 1642 | stopReason: "end_turn", |
| 1643 | agentResult: null, |
| 1644 | }); |
| 1645 | yield* Effect.sleep("120 millis"); |
| 1646 | writeJsonRpcNotification("session/update", { |
| 1647 | sessionId: requestedSessionId, |
| 1648 | update: { |
| 1649 | sessionUpdate: "tool_call", |
| 1650 | toolCallId: pollCallId, |
| 1651 | title: "get_command_or_subagent_output", |
| 1652 | kind: "other", |
| 1653 | status: "completed", |
| 1654 | rawInput: { variant: "TaskOutput", task_ids: [taskId], timeout_ms: 0 }, |
| 1655 | rawOutput: { |
| 1656 | type: "TaskOutput", |
| 1657 | Result: { |
| 1658 | task_id: taskId, |
| 1659 | command: `[monitor] ${description}`, |
| 1660 | status: "completed", |
| 1661 | exit_code: 0, |
| 1662 | output: "Monitor finished.", |
| 1663 | }, |
| 1664 | }, |
| 1665 | }, |
| 1666 | }); |
| 1667 | return yield* Effect.never; |
| 1668 | } |
| 1669 | |
| 1670 | if (emitGrokBackgroundTaskStarted) { |
| 1671 | const toolCallId = "call-fb9d0000-0000-0000-0000-000000000026"; |
| 1672 | const command = "sleep 40; echo done-a"; |
| 1673 | writeJsonRpcNotification("session/update", { |
| 1674 | sessionId: requestedSessionId, |
| 1675 | update: { |
| 1676 | sessionUpdate: "tool_call", |
| 1677 | toolCallId, |
| 1678 | title: "run_terminal_command", |
| 1679 | kind: "execute", |
| 1680 | status: "in_progress", |
| 1681 | rawInput: { command }, |
| 1682 | }, |
| 1683 | }); |
| 1684 | writeJsonRpcNotification("session/update", { |
| 1685 | sessionId: requestedSessionId, |
| 1686 | update: { |
| 1687 | sessionUpdate: "tool_call_update", |
| 1688 | toolCallId, |
| 1689 | status: "completed", |
| 1690 | rawOutput: { |
| 1691 | type: "BackgroundTaskStarted", |
| 1692 | task_id: toolCallId, |
| 1693 | task_type: "bash", |
| 1694 | status: "running", |
| 1695 | command, |
| 1696 | }, |
| 1697 | }, |
| 1698 | }); |
| 1699 | writeJsonRpcNotification("_x.ai/session/prompt_complete", { |
| 1700 | sessionId: requestedSessionId, |
| 1701 | promptId: promptIdFromRequestMeta(request) ?? "mock-xai-prompt-1", |
| 1702 | stopReason: "end_turn", |
| 1703 | agentResult: null, |
| 1704 | }); |
| 1705 | return yield* Effect.never; |
| 1706 | } |
| 1707 | |
| 1708 | if (emitBackgroundToolDuringAnswer) { |
| 1709 | // A command backgrounded earlier reports progress and then finishes |
| 1710 | // while the next answer is still streaming. |
| 1711 | const toolCallId = "background-1"; |
| 1712 | const say = (text: string) => |
| 1713 | agent.client.sessionUpdate({ |
| 1714 | sessionId: requestedSessionId, |
| 1715 | update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text } }, |
| 1716 | }); |
| 1717 | const progress = (status: "in_progress" | "completed", stdout: string) => |
| 1718 | agent.client.sessionUpdate({ |
| 1719 | sessionId: requestedSessionId, |
| 1720 | update: { |
| 1721 | sessionUpdate: "tool_call_update", |
| 1722 | toolCallId, |
| 1723 | status, |
| 1724 | rawOutput: { stdout }, |
| 1725 | }, |
| 1726 | }); |
| 1727 | yield* agent.client.sessionUpdate({ |
| 1728 | sessionId: requestedSessionId, |
| 1729 | update: { |
| 1730 | sessionUpdate: "tool_call_update", |
| 1731 | toolCallId, |
| 1732 | title: "Terminal", |
| 1733 | kind: "execute", |
| 1734 | status: "in_progress", |
| 1735 | rawInput: { command: "sleep 3 && echo done" }, |
| 1736 | }, |
| 1737 | }); |
| 1738 | yield* say("| a | b |\n|---|---|\n| 1 "); |
| 1739 | yield* progress("in_progress", "."); |
| 1740 | yield* say("| x |\n"); |
| 1741 | yield* progress("completed", "done"); |
| 1742 | yield* say("| 2 | y |\n"); |
| 1743 | // Agents can repeat a terminal update after the call finished. |
| 1744 | yield* progress("completed", "done"); |
| 1745 | yield* say("| 3 | z |"); |
| 1746 | return yield* finishPrompt(requestedSessionId, "end_turn"); |
| 1747 | } |
| 1748 | |
| 1749 | if (emitInterleavedAssistantToolCalls) { |
| 1750 | const toolCallId = "tool-call-1"; |
| 1751 | |
| 1752 | yield* agent.client.sessionUpdate({ |
| 1753 | sessionId: requestedSessionId, |
| 1754 | update: { |
| 1755 | sessionUpdate: "agent_message_chunk", |
| 1756 | messageId: "mock-agent-message", |
| 1757 | content: { type: "text", text: "before tool" }, |
| 1758 | }, |
| 1759 | }); |
| 1760 | |
| 1761 | yield* agent.client.sessionUpdate({ |
| 1762 | sessionId: requestedSessionId, |
| 1763 | update: { |
| 1764 | sessionUpdate: "tool_call_update", |
| 1765 | toolCallId, |
| 1766 | title: "Terminal", |
| 1767 | kind: "execute", |
| 1768 | status: "pending", |
| 1769 | rawInput: { |
| 1770 | command: ["echo", "hello"], |
| 1771 | }, |
| 1772 | }, |
| 1773 | }); |
| 1774 | |
| 1775 | yield* agent.client.sessionUpdate({ |
| 1776 | sessionId: requestedSessionId, |
| 1777 | update: { |
| 1778 | sessionUpdate: "tool_call_update", |
| 1779 | toolCallId, |
| 1780 | status: "completed", |
| 1781 | rawOutput: { |
| 1782 | exitCode: 0, |
| 1783 | stdout: "hello", |
| 1784 | stderr: "", |
| 1785 | }, |
| 1786 | }, |
| 1787 | }); |
| 1788 | |
| 1789 | yield* agent.client.sessionUpdate({ |
| 1790 | sessionId: requestedSessionId, |
| 1791 | update: { |
| 1792 | sessionUpdate: "agent_message_chunk", |
| 1793 | messageId: "mock-agent-message", |
| 1794 | content: { type: "text", text: "after tool" }, |
| 1795 | }, |
| 1796 | }); |
| 1797 | |
| 1798 | return yield* finishPrompt(requestedSessionId, "end_turn"); |
| 1799 | } |
| 1800 | |
| 1801 | if (emitElicitation || emitMcpToolApprovalElicitation) { |
| 1802 | yield* agent.client.elicit({ |
| 1803 | sessionId: requestedSessionId, |
| 1804 | message: "Approve this request?", |
| 1805 | mode: "form", |
| 1806 | requestedSchema: { |
| 1807 | type: "object", |
| 1808 | properties: { |
| 1809 | approved: { type: "boolean", title: "Approved" }, |
| 1810 | }, |
| 1811 | }, |
| 1812 | ...(emitMcpToolApprovalElicitation |
| 1813 | ? { _meta: { codex_approval_kind: "mcp_tool_call" } } |
| 1814 | : {}), |
| 1815 | }); |
| 1816 | return yield* finishPrompt(requestedSessionId, "end_turn"); |
| 1817 | } |
| 1818 | |
| 1819 | if (emitUrlElicitation) { |
| 1820 | yield* agent.client.elicit({ |
| 1821 | sessionId: requestedSessionId, |
| 1822 | message: "Open authentication page", |
| 1823 | mode: "url", |
| 1824 | url: "https://example.com/auth", |
| 1825 | elicitationId: "url-elicitation-1", |
| 1826 | }); |
| 1827 | return yield* finishPrompt(requestedSessionId, "end_turn"); |
| 1828 | } |
| 1829 | |
| 1830 | if (emitToolCalls) { |
| 1831 | const toolCallId = "tool-call-1"; |
| 1832 | |
| 1833 | yield* agent.client.sessionUpdate({ |
| 1834 | sessionId: requestedSessionId, |
| 1835 | update: { |
| 1836 | sessionUpdate: "tool_call_update", |
| 1837 | toolCallId, |
| 1838 | title: "Terminal", |
| 1839 | kind: "execute", |
| 1840 | status: "pending", |
| 1841 | rawInput: { |
| 1842 | command: ["cat", "server/package.json"], |
| 1843 | }, |
| 1844 | }, |
| 1845 | }); |
| 1846 | |
| 1847 | yield* agent.client.sessionUpdate({ |
| 1848 | sessionId: requestedSessionId, |
| 1849 | update: { |
| 1850 | sessionUpdate: "tool_call_update", |
| 1851 | toolCallId, |
| 1852 | status: "in_progress", |
| 1853 | }, |
| 1854 | }); |
| 1855 | |
| 1856 | const permissionOptions: Array<AcpSchema.PermissionOption> = [ |
| 1857 | { optionId: permissionOptionIds.allowOnce, name: "Allow once", kind: "allow_once" }, |
| 1858 | ...(omitAllowAlways |
| 1859 | ? [] |
| 1860 | : [ |
| 1861 | { |
| 1862 | optionId: permissionOptionIds.allowAlways, |
| 1863 | name: "Allow always", |
| 1864 | kind: "allow_always" as const, |
| 1865 | }, |
| 1866 | ]), |
| 1867 | { optionId: permissionOptionIds.rejectOnce, name: "Reject", kind: "reject_once" }, |
| 1868 | ]; |
| 1869 | |
| 1870 | let cancelled = cancelledSessions.delete(requestedSessionId); |
| 1871 | for (let index = 0; index < permissionRequestCount; index++) { |
| 1872 | const command = |
| 1873 | index > 0 |
| 1874 | ? (process.env.T3_ACP_SECOND_PERMISSION_COMMAND ?? "cat server/package.json") |
| 1875 | : "cat server/package.json"; |
| 1876 | const permission = yield* agent.client.requestPermission({ |
| 1877 | sessionId: requestedSessionId, |
| 1878 | title: process.env.T3_ACP_PERMISSION_TITLE ?? `\`${command}\``, |
| 1879 | subject: { |
| 1880 | type: "tool_call", |
| 1881 | toolCall: { |
| 1882 | toolCallId: index === 0 ? toolCallId : `${toolCallId}-${index + 1}`, |
| 1883 | title: process.env.T3_ACP_PERMISSION_TITLE ?? `\`${command}\``, |
| 1884 | kind: "execute", |
| 1885 | status: "pending", |
| 1886 | rawInput: { |
| 1887 | variant: "Bash", |
| 1888 | command, |
| 1889 | description: index === 0 ? "Read package metadata" : "Read it again", |
| 1890 | }, |
| 1891 | content: [ |
| 1892 | { |
| 1893 | type: "content", |
| 1894 | content: { |
| 1895 | type: "text", |
| 1896 | text: `Not in allowlist: ${command}`, |
| 1897 | }, |
| 1898 | }, |
| 1899 | ], |
| 1900 | }, |
| 1901 | }, |
| 1902 | options: permissionOptions, |
| 1903 | }); |
| 1904 | cancelled = |
| 1905 | cancelled || |
| 1906 | cancelledSessions.delete(requestedSessionId) || |
| 1907 | permission.outcome.outcome === "cancelled"; |
| 1908 | if (cancelled) { |
| 1909 | break; |
| 1910 | } |
| 1911 | } |
| 1912 | |
| 1913 | if (hangAfterPermission) { |
| 1914 | return yield* Effect.never; |
| 1915 | } |
| 1916 | |
| 1917 | yield* agent.client.sessionUpdate({ |
| 1918 | sessionId: requestedSessionId, |
| 1919 | update: { |
| 1920 | sessionUpdate: "tool_call_update", |
| 1921 | toolCallId, |
| 1922 | title: "Terminal", |
| 1923 | kind: "execute", |
| 1924 | status: "completed", |
| 1925 | rawOutput: { |
| 1926 | exitCode: 0, |
| 1927 | stdout: '{ "name": "t3" }', |
| 1928 | stderr: "", |
| 1929 | }, |
| 1930 | }, |
| 1931 | }); |
| 1932 | |
| 1933 | yield* agent.client.sessionUpdate({ |
| 1934 | sessionId: requestedSessionId, |
| 1935 | update: { |
| 1936 | sessionUpdate: "agent_message_chunk", |
| 1937 | messageId: "mock-agent-message", |
| 1938 | content: { type: "text", text: "hello from mock" }, |
| 1939 | }, |
| 1940 | }); |
| 1941 | |
| 1942 | return yield* finishPrompt(requestedSessionId, cancelled ? "cancelled" : "end_turn"); |
| 1943 | } |
| 1944 | |
| 1945 | if (emitGenericToolPlaceholders) { |
| 1946 | const toolCallId = "tool-call-generic-1"; |
| 1947 | |
| 1948 | yield* agent.client.sessionUpdate({ |
| 1949 | sessionId: requestedSessionId, |
| 1950 | update: { |
| 1951 | sessionUpdate: "tool_call_update", |
| 1952 | toolCallId, |
| 1953 | title: "Read File", |
| 1954 | kind: "read", |
| 1955 | status: "pending", |
| 1956 | rawInput: {}, |
| 1957 | }, |
| 1958 | }); |
| 1959 | |
| 1960 | yield* agent.client.sessionUpdate({ |
| 1961 | sessionId: requestedSessionId, |
| 1962 | update: { |
| 1963 | sessionUpdate: "tool_call_update", |
| 1964 | toolCallId, |
| 1965 | status: "in_progress", |
| 1966 | }, |
| 1967 | }); |
| 1968 | |
| 1969 | yield* agent.client.sessionUpdate({ |
| 1970 | sessionId: requestedSessionId, |
| 1971 | update: { |
| 1972 | sessionUpdate: "tool_call_update", |
| 1973 | toolCallId, |
| 1974 | status: "completed", |
| 1975 | rawOutput: { |
| 1976 | content: "package.json\n", |
| 1977 | }, |
| 1978 | }, |
| 1979 | }); |
| 1980 | |
| 1981 | return yield* finishPrompt(requestedSessionId, "end_turn"); |
| 1982 | } |
| 1983 | |
| 1984 | // In-turn monitor + TaskOutput hydrate, then a late post-finalize |
| 1985 | // duplicate terminal TaskOutput for the same task. Exercises the |
| 1986 | // already-handled short-circuit: must not pin hasPendingBackgroundWork. |
| 1987 | if (emitInTurnTaskOutputThenLateDuplicate) { |
| 1988 | const monitorToolCallId = "tool-call-monitor-1"; |
| 1989 | yield* agent.client.sessionUpdate({ |
| 1990 | sessionId: requestedSessionId, |
| 1991 | update: { |
| 1992 | sessionUpdate: "tool_call_update", |
| 1993 | toolCallId: monitorToolCallId, |
| 1994 | title: "Monitor: mock background task", |
| 1995 | kind: "execute", |
| 1996 | status: "pending", |
| 1997 | rawInput: {}, |
| 1998 | }, |
| 1999 | }); |
| 2000 | yield* agent.client.sessionUpdate({ |
| 2001 | sessionId: requestedSessionId, |
| 2002 | update: { |
| 2003 | sessionUpdate: "tool_call_update", |
| 2004 | toolCallId: monitorToolCallId, |
| 2005 | status: "in_progress", |
| 2006 | }, |
| 2007 | }); |
| 2008 | yield* agent.client.sessionUpdate({ |
| 2009 | sessionId: requestedSessionId, |
| 2010 | update: { |
| 2011 | sessionUpdate: "tool_call_update", |
| 2012 | toolCallId: "tool-call-fetch-1", |
| 2013 | title: "get_command_or_subagent_output", |
| 2014 | kind: "other", |
| 2015 | status: "pending", |
| 2016 | rawInput: { task_id: "task-monitor-1" }, |
| 2017 | }, |
| 2018 | }); |
| 2019 | yield* agent.client.sessionUpdate({ |
| 2020 | sessionId: requestedSessionId, |
| 2021 | update: { |
| 2022 | sessionUpdate: "tool_call_update", |
| 2023 | toolCallId: "tool-call-fetch-1", |
| 2024 | status: "completed", |
| 2025 | rawOutput: { output: "MONITOR_LISTING_TOKEN" }, |
| 2026 | }, |
| 2027 | }); |
| 2028 | yield* agent.client.sessionUpdate({ |
| 2029 | sessionId: requestedSessionId, |
| 2030 | update: { |
| 2031 | sessionUpdate: "agent_message_chunk", |
| 2032 | messageId: "mock-agent-message", |
| 2033 | content: { type: "text", text: "Monitor listing ready in-turn." }, |
| 2034 | }, |
| 2035 | }); |
| 2036 | // After deferred finalize (~2s) clears activeTurn, re-emit a terminal |
| 2037 | // TaskOutput for the same task so bufferPostSettleWake sees |
| 2038 | // alreadyHandledToolUpdate with a non-empty wake path. |
| 2039 | yield* Effect.gen(function* () { |
| 2040 | yield* Effect.sleep("2500 millis"); |
| 2041 | yield* Effect.sync(() => { |
| 2042 | writeJsonRpcNotification("session/update", { |
| 2043 | sessionId: requestedSessionId, |
| 2044 | update: { |
| 2045 | sessionUpdate: "tool_call_update", |
| 2046 | toolCallId: "tool-call-fetch-1", |
| 2047 | title: "get_command_or_subagent_output", |
| 2048 | kind: "other", |
| 2049 | status: "completed", |
| 2050 | rawOutput: { output: "MONITOR_LISTING_TOKEN_LATE" }, |
| 2051 | }, |
| 2052 | }); |
| 2053 | }); |
| 2054 | }).pipe(Effect.forkDetach); |
| 2055 | return yield* finishPrompt(requestedSessionId, "end_turn"); |
| 2056 | } |
| 2057 | |
| 2058 | if (emitPostSettleMonitorFlow) { |
| 2059 | const monitorToolCallId = "tool-call-monitor-1"; |
| 2060 | |
| 2061 | yield* agent.client.sessionUpdate({ |
| 2062 | sessionId: requestedSessionId, |
| 2063 | update: { |
| 2064 | sessionUpdate: "tool_call_update", |
| 2065 | toolCallId: monitorToolCallId, |
| 2066 | title: "Monitor: mock background task", |
| 2067 | kind: "execute", |
| 2068 | status: "pending", |
| 2069 | rawInput: {}, |
| 2070 | }, |
| 2071 | }); |
| 2072 | |
| 2073 | yield* agent.client.sessionUpdate({ |
| 2074 | sessionId: requestedSessionId, |
| 2075 | update: { |
| 2076 | sessionUpdate: "tool_call_update", |
| 2077 | toolCallId: monitorToolCallId, |
| 2078 | status: "in_progress", |
| 2079 | }, |
| 2080 | }); |
| 2081 | |
| 2082 | // After the prompt settles, replay the CLI-injected monitor-event |
| 2083 | // turn: end notice, TaskOutput hydration, then (once the trigger |
| 2084 | // file exists) the report chunk. Detached fiber on the real clock; |
| 2085 | // it outlives the prompt handler. |
| 2086 | yield* Effect.gen(function* () { |
| 2087 | yield* Effect.sleep("150 millis"); |
| 2088 | yield* Effect.sync(() => { |
| 2089 | writeJsonRpcNotification("session/update", { |
| 2090 | sessionId: requestedSessionId, |
| 2091 | update: { |
| 2092 | sessionUpdate: "user_message_chunk", |
| 2093 | messageId: "mock-user-message", |
| 2094 | content: { |
| 2095 | type: "text", |
| 2096 | text: 'Monitor "task-monitor-1" ended: [monitor ended: exit 0]', |
| 2097 | }, |
| 2098 | }, |
| 2099 | }); |
| 2100 | writeJsonRpcNotification("session/update", { |
| 2101 | sessionId: requestedSessionId, |
| 2102 | update: { |
| 2103 | sessionUpdate: "tool_call_update", |
| 2104 | toolCallId: "tool-call-fetch-1", |
| 2105 | title: "get_command_or_subagent_output", |
| 2106 | kind: "other", |
| 2107 | status: "pending", |
| 2108 | rawInput: { task_id: "task-monitor-1" }, |
| 2109 | }, |
| 2110 | }); |
| 2111 | writeJsonRpcNotification("session/update", { |
| 2112 | sessionId: requestedSessionId, |
| 2113 | update: { |
| 2114 | sessionUpdate: "tool_call_update", |
| 2115 | toolCallId: "tool-call-fetch-1", |
| 2116 | status: "completed", |
| 2117 | rawOutput: { output: "MONITOR_LISTING_TOKEN" }, |
| 2118 | }, |
| 2119 | }); |
| 2120 | }); |
| 2121 | if (injectedReportTriggerPath === undefined) return; |
| 2122 | while (!(yield* Effect.sync(() => NodeFS.existsSync(injectedReportTriggerPath)))) { |
| 2123 | yield* Effect.sleep("20 millis"); |
| 2124 | } |
| 2125 | yield* Effect.sync(() => { |
| 2126 | writeJsonRpcNotification("session/update", { |
| 2127 | sessionId: requestedSessionId, |
| 2128 | update: { |
| 2129 | sessionUpdate: "agent_message_chunk", |
| 2130 | messageId: "mock-agent-message", |
| 2131 | content: { type: "text", text: "Monitor finished. MONITOR_REPORT_TOKEN" }, |
| 2132 | }, |
| 2133 | }); |
| 2134 | }); |
| 2135 | }).pipe(Effect.forkDetach); |
| 2136 | |
| 2137 | return yield* finishPrompt(requestedSessionId, "end_turn"); |
| 2138 | } |
| 2139 | |
| 2140 | if (emitAskQuestion) { |
| 2141 | yield* agent.client.extRequest("cursor/ask_question", { |
| 2142 | toolCallId: "ask-question-tool-call-1", |
| 2143 | title: "Question", |
| 2144 | questions: [ |
| 2145 | { |
| 2146 | id: "scope", |
| 2147 | prompt: "Which scope?", |
| 2148 | options: [ |
| 2149 | { id: "workspace", label: "Workspace" }, |
| 2150 | { id: "session", label: "Session" }, |
| 2151 | ], |
| 2152 | }, |
| 2153 | ], |
| 2154 | }); |
| 2155 | |
| 2156 | return yield* finishPrompt(requestedSessionId, "end_turn"); |
| 2157 | } |
| 2158 | |
| 2159 | if (emitXAiAskUserQuestion || emitXAiAskUserQuestionThenHang) { |
| 2160 | const result = yield* agent.client.extRequest("_x.ai/ask_user_question", { |
| 2161 | method: "x.ai/ask_user_question", |
| 2162 | params: { |
| 2163 | sessionId: requestedSessionId, |
| 2164 | toolCallId: "ask-user-question-tool-call-1", |
| 2165 | questions: [ |
| 2166 | { |
| 2167 | question: "Which scope should Grok use?", |
| 2168 | multiSelect: null, |
| 2169 | options: [ |
| 2170 | { label: "Workspace", description: "Use the current workspace" }, |
| 2171 | { label: "Session", description: "Only use this session" }, |
| 2172 | ], |
| 2173 | }, |
| 2174 | ], |
| 2175 | mode: "default", |
| 2176 | }, |
| 2177 | }); |
| 2178 | if (typeof result !== "object" || result === null || !("outcome" in result)) { |
| 2179 | throw new Error("Expected _x.ai/ask_user_question response outcome."); |
| 2180 | } |
| 2181 | if (result.outcome === "cancelled") { |
| 2182 | return yield* finishPrompt(requestedSessionId, "end_turn"); |
| 2183 | } |
| 2184 | if ( |
| 2185 | result.outcome !== "accepted" || |
| 2186 | !("answers" in result) || |
| 2187 | typeof result.answers !== "object" || |
| 2188 | result.answers === null |
| 2189 | ) { |
| 2190 | throw new Error("Expected accepted _x.ai/ask_user_question response answers."); |
| 2191 | } |
| 2192 | |
| 2193 | if (emitXAiAskUserQuestionThenHang) { |
| 2194 | return yield* Effect.never; |
| 2195 | } |
| 2196 | |
| 2197 | return yield* finishPrompt(requestedSessionId, "end_turn"); |
| 2198 | } |
| 2199 | |
| 2200 | if (emitXAiPlanMdWrite) { |
| 2201 | // Match Grok's real session layout so isGrokPlanMarkdownPath accepts it. |
| 2202 | const planRoot = process.env.T3_ACP_PLAN_ROOT ?? "/tmp/mock-home/.grok"; |
| 2203 | const planPath = `${planRoot}/sessions/${requestedSessionId}/plan.md`; |
| 2204 | const planBody = "# Mock plan\n\n- Write the feature\n- Add a test\n- Ship it\n"; |
| 2205 | // enter_plan_mode first so the adapter arms planModeActive. |
| 2206 | yield* agent.client.sessionUpdate({ |
| 2207 | sessionId: requestedSessionId, |
| 2208 | update: { |
| 2209 | sessionUpdate: "tool_call_update", |
| 2210 | toolCallId: "enter-plan-mode-1", |
| 2211 | title: "enter_plan_mode", |
| 2212 | kind: "other", |
| 2213 | status: "completed", |
| 2214 | rawInput: { variant: "EnterPlanMode" }, |
| 2215 | }, |
| 2216 | }); |
| 2217 | yield* agent.client.sessionUpdate({ |
| 2218 | sessionId: requestedSessionId, |
| 2219 | update: { |
| 2220 | sessionUpdate: "tool_call_update", |
| 2221 | toolCallId: "plan-md-write-1", |
| 2222 | title: "write", |
| 2223 | kind: "edit", |
| 2224 | status: "pending", |
| 2225 | rawInput: { file_path: planPath, content: planBody }, |
| 2226 | }, |
| 2227 | }); |
| 2228 | yield* agent.client.sessionUpdate({ |
| 2229 | sessionId: requestedSessionId, |
| 2230 | update: { |
| 2231 | sessionUpdate: "tool_call_update", |
| 2232 | toolCallId: "plan-md-write-1", |
| 2233 | kind: "edit", |
| 2234 | status: "completed", |
| 2235 | title: `Write \`${planPath}\``, |
| 2236 | rawInput: { file_path: planPath, content: planBody }, |
| 2237 | content: [ |
| 2238 | { |
| 2239 | type: "diff", |
| 2240 | path: planPath, |
| 2241 | oldText: "", |
| 2242 | newText: planBody, |
| 2243 | }, |
| 2244 | ], |
| 2245 | }, |
| 2246 | }); |
| 2247 | return yield* finishPrompt(requestedSessionId, "end_turn"); |
| 2248 | } |
| 2249 | |
| 2250 | if (emitXAiExitPlanMode) { |
| 2251 | const result = yield* agent.client.extRequest("_x.ai/exit_plan_mode", { |
| 2252 | method: "x.ai/exit_plan_mode", |
| 2253 | params: { |
| 2254 | sessionId: requestedSessionId, |
| 2255 | toolCallId: "exit-plan-mode-tool-call-1", |
| 2256 | planContent: "# Exit plan\n\n- Step one\n- Step two\n", |
| 2257 | }, |
| 2258 | }); |
| 2259 | if (typeof result !== "object" || result === null || !("outcome" in result)) { |
| 2260 | throw new Error("Expected _x.ai/exit_plan_mode response outcome."); |
| 2261 | } |
| 2262 | if ( |
| 2263 | result.outcome !== "abandoned" && |
| 2264 | result.outcome !== "approved" && |
| 2265 | result.outcome !== "request_changes" |
| 2266 | ) { |
| 2267 | throw new Error( |
| 2268 | `Expected exit_plan_mode outcome abandoned|approved|request_changes, got ${String(result.outcome)}`, |
| 2269 | ); |
| 2270 | } |
| 2271 | return yield* finishPrompt(requestedSessionId, "end_turn"); |
| 2272 | } |
| 2273 | |
| 2274 | if (emitForeignSessionUpdates) { |
| 2275 | yield* agent.client.sessionUpdate({ |
| 2276 | sessionId: requestedSessionId, |
| 2277 | update: { |
| 2278 | sessionUpdate: "agent_message_chunk", |
| 2279 | messageId: "mock-agent-message", |
| 2280 | content: { type: "text", text: "root before child" }, |
| 2281 | }, |
| 2282 | }); |
| 2283 | yield* agent.client.sessionUpdate({ |
| 2284 | sessionId: "mock-child-session-1", |
| 2285 | update: { |
| 2286 | sessionUpdate: "agent_message_chunk", |
| 2287 | messageId: "mock-agent-message", |
| 2288 | content: { type: "text", text: "child content" }, |
| 2289 | }, |
| 2290 | }); |
| 2291 | yield* agent.client.sessionUpdate({ |
| 2292 | sessionId: "mock-child-session-1", |
| 2293 | update: { |
| 2294 | sessionUpdate: "tool_call_update", |
| 2295 | toolCallId: "child-tool-call-1", |
| 2296 | title: "Child-only tool", |
| 2297 | kind: "other", |
| 2298 | status: "pending", |
| 2299 | rawInput: {}, |
| 2300 | }, |
| 2301 | }); |
| 2302 | yield* agent.client.sessionUpdate({ |
| 2303 | sessionId: requestedSessionId, |
| 2304 | update: { |
| 2305 | sessionUpdate: "agent_message_chunk", |
| 2306 | messageId: "mock-agent-message", |
| 2307 | content: { type: "text", text: " root after child" }, |
| 2308 | }, |
| 2309 | }); |
| 2310 | return yield* finishPrompt(requestedSessionId, "end_turn"); |
| 2311 | } |
| 2312 | |
| 2313 | yield* agent.client.sessionUpdate({ |
| 2314 | sessionId: requestedSessionId, |
| 2315 | update: { |
| 2316 | sessionUpdate: "plan_update", |
| 2317 | plan: { |
| 2318 | type: "items", |
| 2319 | planId: "mock-plan", |
| 2320 | entries: [ |
| 2321 | { |
| 2322 | content: "Inspect mock ACP state", |
| 2323 | priority: "high", |
| 2324 | status: "completed", |
| 2325 | }, |
| 2326 | { |
| 2327 | content: "Implement the requested change", |
| 2328 | priority: "high", |
| 2329 | status: "in_progress", |
| 2330 | }, |
| 2331 | ], |
| 2332 | }, |
| 2333 | }, |
| 2334 | }); |
| 2335 | |
| 2336 | yield* agent.client.sessionUpdate({ |
| 2337 | sessionId: requestedSessionId, |
| 2338 | update: { |
| 2339 | sessionUpdate: "agent_message_chunk", |
| 2340 | messageId: "mock-agent-message", |
| 2341 | content: { type: "text", text: promptResponseText ?? "hello from mock" }, |
| 2342 | }, |
| 2343 | }); |
| 2344 | |
| 2345 | return yield* finishPrompt(requestedSessionId, "end_turn"); |
| 2346 | }), |
| 2347 | ); |
| 2348 | |
| 2349 | yield* agent.handleUnknownExtRequest((method, params) => { |
| 2350 | if (method === "_test/environment") { |
| 2351 | return Effect.succeed({ |
| 2352 | inherited: process.env.T3_ACP_RUNTIME_AMBIENT === "sentinel", |
| 2353 | explicit: process.env.T3_ACP_RUNTIME_EXPLICIT === "kept", |
| 2354 | }); |
| 2355 | } |
| 2356 | if (method === "_test/release-resume") { |
| 2357 | return Deferred.succeed(resumeRelease, undefined).pipe(Effect.as({})); |
| 2358 | } |
| 2359 | if (method === "_test/finish-cancel") { |
| 2360 | return Deferred.succeed(nativeCancelRelease, undefined).pipe(Effect.as({})); |
| 2361 | } |
| 2362 | if (method === "_test/startup-metadata") { |
| 2363 | return Effect.gen(function* () { |
| 2364 | for (const [metadataSessionId, commandName, modeId] of [ |
| 2365 | [sessionId, "plan", "code"], |
| 2366 | ["child-session", "foreign-command", "ask"], |
| 2367 | ] as const) { |
| 2368 | yield* agent.client.sessionUpdate({ |
| 2369 | sessionId: metadataSessionId, |
| 2370 | update: { |
| 2371 | sessionUpdate: "available_commands_update", |
| 2372 | availableCommands: [{ name: commandName, description: "Native command" }], |
| 2373 | }, |
| 2374 | }); |
| 2375 | currentModeId = modeId; |
| 2376 | yield* agent.client.sessionUpdate({ |
| 2377 | sessionId: metadataSessionId, |
| 2378 | update: { |
| 2379 | sessionUpdate: "config_option_update", |
| 2380 | configOptions: configOptions().map((option) => |
| 2381 | option.type === "select" && option.category === "model" |
| 2382 | ? { |
| 2383 | ...option, |
| 2384 | currentValue: metadataSessionId === sessionId ? "gpt-5.4" : "default", |
| 2385 | } |
| 2386 | : option, |
| 2387 | ), |
| 2388 | }, |
| 2389 | }); |
| 2390 | yield* agent.client.sessionUpdate({ |
| 2391 | sessionId: metadataSessionId, |
| 2392 | update: { |
| 2393 | sessionUpdate: "agent_message_chunk", |
| 2394 | content: { type: "text", text: "Startup transcript must not replay." }, |
| 2395 | }, |
| 2396 | }); |
| 2397 | } |
| 2398 | return {}; |
| 2399 | }); |
| 2400 | } |
| 2401 | if (method === "cursor/list_available_models") { |
| 2402 | return Effect.succeed({ |
| 2403 | models: availableModels(), |
| 2404 | }); |
| 2405 | } |
| 2406 | |
| 2407 | if (method !== "session/mode/set") { |
| 2408 | return Effect.fail(AcpError.AcpRequestError.methodNotFound(method)); |
| 2409 | } |
| 2410 | |
| 2411 | const nextModeId = |
| 2412 | typeof params === "object" && |
| 2413 | params !== null && |
| 2414 | "modeId" in params && |
| 2415 | typeof params.modeId === "string" |
| 2416 | ? params.modeId |
| 2417 | : typeof params === "object" && |
| 2418 | params !== null && |
| 2419 | "mode" in params && |
| 2420 | typeof params.mode === "string" |
| 2421 | ? params.mode |
| 2422 | : undefined; |
| 2423 | const requestedSessionId = |
| 2424 | typeof params === "object" && |
| 2425 | params !== null && |
| 2426 | "sessionId" in params && |
| 2427 | typeof params.sessionId === "string" |
| 2428 | ? params.sessionId |
| 2429 | : sessionId; |
| 2430 | |
| 2431 | if (typeof nextModeId === "string" && nextModeId.trim()) { |
| 2432 | currentModeId = nextModeId.trim(); |
| 2433 | return agent.client |
| 2434 | .sessionUpdate({ |
| 2435 | sessionId: requestedSessionId, |
| 2436 | update: { |
| 2437 | sessionUpdate: "current_mode_update", |
| 2438 | currentModeId, |
| 2439 | }, |
| 2440 | }) |
| 2441 | .pipe(Effect.as({})); |
| 2442 | } |
| 2443 | |
| 2444 | return Effect.succeed({}); |
| 2445 | }); |
| 2446 | |
| 2447 | yield* agent.handleUnknownExtNotification((method) => |
| 2448 | method === "_test/exit" ? Effect.sync(() => process.exit(19)) : Effect.void, |
| 2449 | ); |
| 2450 | |
| 2451 | return yield* Effect.never; |
| 2452 | }).pipe( |
| 2453 | Effect.provide( |
| 2454 | EffectAcpAgent.layerStdio( |
| 2455 | requestLogPath |
| 2456 | ? { |
| 2457 | logIncoming: true, |
| 2458 | logger: (event) => { |
| 2459 | if (event.direction !== "incoming" || event.stage !== "raw") { |
| 2460 | return Effect.void; |
| 2461 | } |
| 2462 | if (typeof event.payload !== "string") { |
| 2463 | return Effect.void; |
| 2464 | } |
| 2465 | const payload = event.payload; |
| 2466 | return Effect.sync(() => { |
| 2467 | NodeFS.appendFileSync( |
| 2468 | requestLogPath, |
| 2469 | payload.endsWith("\n") ? payload : `${payload}\n`, |
| 2470 | "utf8", |
| 2471 | ); |
| 2472 | }); |
| 2473 | }, |
| 2474 | } |
| 2475 | : {}, |
| 2476 | ), |
| 2477 | ), |
| 2478 | Effect.scoped, |
| 2479 | Effect.provide(NodeServices.layer), |
| 2480 | ); |
| 2481 | |
| 2482 | NodeRuntime.runMain(program); |