1#!/usr/bin/env node
2// @effect-diagnostics nodeBuiltinImport:off
3import * as NodeChildProcess from "node:child_process";
4import * as NodeFS from "node:fs";
5
6import * as Effect from "effect/Effect";
7import * as Schema from "effect/Schema";
8import * as Deferred from "effect/Deferred";
9
10import * as NodeServices from "@effect/platform-node/NodeServices";
11import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
12
13import * as EffectAcpAgent from "effect-acp/agent";
14import * as AcpError from "effect-acp/errors";
15import type * as AcpSchema from "effect-acp/schema";
16import type * as AcpCompat from "effect-acp/compat";
17
18import { beginAcpMockPrompt } from "./acpMockCancellationState.ts";
19
20const requestLogPath = process.env.T3_ACP_REQUEST_LOG_PATH;
21const exitLogPath = process.env.T3_ACP_EXIT_LOG_PATH;
22const antigravityProfile = process.env.T3_ACP_ANTIGRAVITY === "1";
23const emitToolCalls = process.env.T3_ACP_EMIT_TOOL_CALLS === "1";
24const emitInterleavedAssistantToolCalls =
25 process.env.T3_ACP_EMIT_INTERLEAVED_ASSISTANT_TOOL_CALLS === "1";
26const emitV2Fidelity = process.env.T3_ACP_EMIT_V2_FIDELITY === "1";
27const vibeRetryOutcome = process.env.T3_ACP_VIBE_RETRY_OUTCOME;
28const emitGenericToolPlaceholders = process.env.T3_ACP_EMIT_GENERIC_TOOL_PLACEHOLDERS === "1";
29const emitPostSettleMonitorFlow = process.env.T3_ACP_EMIT_POST_SETTLE_MONITOR_FLOW === "1";
30const emitInTurnTaskOutputThenLateDuplicate =
31 process.env.T3_ACP_EMIT_IN_TURN_TASKOUTPUT_THEN_LATE_DUPLICATE === "1";
32const injectedReportTriggerPath = process.env.T3_ACP_INJECTED_REPORT_TRIGGER_PATH;
33const emitBackgroundToolDuringAnswer =
34 process.env.T3_ACP_EMIT_BACKGROUND_TOOL_DURING_ANSWER === "1";
35const emitAskQuestion = process.env.T3_ACP_EMIT_ASK_QUESTION === "1";
36const emitElicitation = process.env.T3_ACP_EMIT_ELICITATION === "1";
37const emitMcpToolApprovalElicitation =
38 process.env.T3_ACP_EMIT_MCP_TOOL_APPROVAL_ELICITATION === "1";
39const emitUrlElicitation = process.env.T3_ACP_EMIT_URL_ELICITATION === "1";
40const emitXAiAskUserQuestion = process.env.T3_ACP_EMIT_XAI_ASK_USER_QUESTION === "1";
41const emitXAiExitPlanMode = process.env.T3_ACP_EMIT_XAI_EXIT_PLAN_MODE === "1";
42const emitXAiPlanMdWrite = process.env.T3_ACP_EMIT_XAI_PLAN_MD_WRITE === "1";
43const emitXAiPromptCompleteThenHang = process.env.T3_ACP_EMIT_XAI_PROMPT_COMPLETE_THEN_HANG === "1";
44const emitXAiRateLimitThenHang = process.env.T3_ACP_EMIT_XAI_RATE_LIMIT_THEN_HANG === "1";
45const emitXAiAskUserQuestionThenHang =
46 process.env.T3_ACP_EMIT_XAI_ASK_USER_QUESTION_THEN_HANG === "1";
47const emitContentThenHang = process.env.T3_ACP_EMIT_CONTENT_THEN_HANG === "1";
48const emitPlanThenHang = process.env.T3_ACP_EMIT_PLAN_THEN_HANG === "1";
49const emitActiveToolThenHang = process.env.T3_ACP_EMIT_ACTIVE_TOOL_THEN_HANG === "1";
50const emitGrokMonitorPostTurnPoll = process.env.T3_ACP_EMIT_GROK_MONITOR_POST_TURN_POLL === "1";
51const emitGrokBackgroundTaskStarted = process.env.T3_ACP_EMIT_GROK_BACKGROUND_TASK_STARTED === "1";
52const emitForeignSessionUpdates = process.env.T3_ACP_EMIT_FOREIGN_SESSION_UPDATES === "1";
53const waitForResumeRelease = process.env.T3_ACP_WAIT_FOR_RESUME_RELEASE === "1";
54const completeFirstPromptOnCancel = process.env.T3_ACP_COMPLETE_FIRST_PROMPT_ON_CANCEL === "1";
55const floodStderr = process.env.T3_ACP_FLOOD_STDERR === "1";
56const 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.
60const clientFsProbePath = process.env.T3_ACP_CLIENT_FS_PROBE_PATH;
61const clientFsProbeLogPath = process.env.T3_ACP_CLIENT_FS_PROBE_LOG_PATH;
62const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown));
63const hangAfterPermission = process.env.T3_ACP_HANG_AFTER_PERMISSION === "1";
64const hangFirstPromptForever = process.env.T3_ACP_HANG_FIRST_PROMPT_FOREVER === "1";
65const emitLateUpdateAfterCancel = process.env.T3_ACP_EMIT_LATE_UPDATE_AFTER_CANCEL === "1";
66const emitTaskBackgroundedAfterCancel =
67 process.env.T3_ACP_EMIT_TASK_BACKGROUNDED_AFTER_CANCEL === "1";
68const residualCallbackResponseLogPath = process.env.T3_ACP_RESIDUAL_CALLBACK_RESPONSE_LOG_PATH;
69const residualCallbackTriggerPath = process.env.T3_ACP_RESIDUAL_CALLBACK_TRIGGER_PATH;
70const exitAfterResidualCallbacks = process.env.T3_ACP_EXIT_AFTER_RESIDUAL_CALLBACKS === "1";
71const emitRunningCommandThenHang = process.env.T3_ACP_EMIT_RUNNING_COMMAND_THEN_HANG === "1";
72const emitRunningCommandThenHangOnFirstPrompt =
73 process.env.T3_ACP_EMIT_RUNNING_COMMAND_THEN_HANG_FIRST_PROMPT === "1";
74const emitEmptySuccessfulBash = process.env.T3_ACP_EMIT_EMPTY_SUCCESSFUL_BASH === "1";
75const emitEmptySuccessfulBashThenHang =
76 process.env.T3_ACP_EMIT_EMPTY_SUCCESSFUL_BASH_THEN_HANG === "1";
77const exitOnCancel = process.env.T3_ACP_EXIT_ON_CANCEL === "1";
78const runningCommandIgnoresTerm = process.env.T3_ACP_RUNNING_COMMAND_IGNORE_TERM === "1";
79const runningCommandPidPath = process.env.T3_ACP_RUNNING_COMMAND_PID_PATH;
80const runningCommandSeparateSession = process.env.T3_ACP_RUNNING_COMMAND_SEPARATE_SESSION === "1";
81const exitAfterRunningCommandLaunch = process.env.T3_ACP_EXIT_AFTER_RUNNING_COMMAND_LAUNCH === "1";
82const omitXAiPromptCompleteStopReason =
83 process.env.T3_ACP_OMIT_XAI_PROMPT_COMPLETE_STOP_REASON === "1";
84const failLoadSession = process.env.T3_ACP_FAIL_LOAD_SESSION === "1";
85const failLoadSessionAfterConfigReplay =
86 process.env.T3_ACP_FAIL_LOAD_SESSION_AFTER_CONFIG_REPLAY === "1";
87const emitLoadReplay = process.env.T3_ACP_EMIT_LOAD_REPLAY === "1";
88const hangLoadSessionAfterReplay = process.env.T3_ACP_HANG_LOAD_SESSION_AFTER_REPLAY === "1";
89const delayLoadSessionAfterReplay = process.env.T3_ACP_DELAY_LOAD_SESSION_AFTER_REPLAY === "1";
90const loadSessionDelayMs = Number(process.env.T3_ACP_LOAD_SESSION_DELAY_MS ?? "5000");
91const emitStaleXAiPromptCompleteBeforeSecondHang =
92 process.env.T3_ACP_EMIT_STALE_XAI_PROMPT_COMPLETE_BEFORE_SECOND_HANG === "1";
93const emitOverlappingXAiPromptCompleteOutOfOrder =
94 process.env.T3_ACP_EMIT_OVERLAPPING_XAI_PROMPT_COMPLETE_OUT_OF_ORDER === "1";
95const failPrompt = process.env.T3_ACP_FAIL_PROMPT === "1";
96const failSetConfigOption = process.env.T3_ACP_FAIL_SET_CONFIG_OPTION === "1";
97const failSetModeConfigOption = process.env.T3_ACP_FAIL_SET_MODE_CONFIG_OPTION === "1";
98const exitOnSetConfigOption = process.env.T3_ACP_EXIT_ON_SET_CONFIG_OPTION === "1";
99const omitModelConfigOption = process.env.T3_ACP_OMIT_MODEL_CONFIG_OPTION === "1";
100const emitEmptyDefaultAgentOption = process.env.T3_ACP_EMIT_EMPTY_DEFAULT_AGENT_OPTION === "1";
101const promptResponseText = process.env.T3_ACP_PROMPT_RESPONSE_TEXT;
102const initialGrokReasoningEffort =
103 process.env.T3_ACP_INITIAL_GROK_REASONING_EFFORT?.trim() || undefined;
104const promptDelayMs = Number(process.env.T3_ACP_PROMPT_DELAY_MS ?? "0");
105const supportsSessionLifecycle = process.env.T3_ACP_SESSION_LIFECYCLE === "1";
106const supportsAcpMcp = process.env.T3_ACP_MCP_ACP === "1";
107const supportsV2Management = process.env.T3_ACP_V2_MANAGEMENT === "1";
108const omitSessionListHandler = process.env.T3_ACP_OMIT_SESSION_LIST_HANDLER === "1";
109const advertisedAuthMethodId = process.env.T3_ACP_AUTH_METHOD_ID?.trim();
110const initializeAuthMethodId =
111 advertisedAuthMethodId ?? (supportsSessionLifecycle ? "test" : undefined);
112const requiresAuthentication = process.env.T3_ACP_REQUIRE_AUTH === "1";
113const commandAdvertisementDelayMs = Number(
114 process.env.T3_ACP_COMMAND_ADVERTISEMENT_DELAY_MS ?? "-1",
115);
116const 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};
121const omitAllowAlways = process.env.T3_ACP_OMIT_ALLOW_ALWAYS === "1";
122const permissionRequestCount = Math.max(
123 1,
124 Number(process.env.T3_ACP_PERMISSION_REQUEST_COUNT ?? "1") || 1,
125);
126const sessionId = "mock-session-1";
127
128let currentModeId = antigravityProfile ? "default" : "ask";
129let currentModelId = antigravityProfile ? "gemini-test-low" : "default";
130let parameterizedModelPicker = false;
131let currentReasoning = "medium";
132let currentContext = "272k";
133let currentFast = false;
134let currentAgent = "";
135let authenticated = !requiresAuthentication;
136let promptCount = 0;
137let overlappingFirstPromptId: string | undefined;
138const cancelledSessions = new Set<string>();
139let configuredProvider: AcpSchema.ProviderCurrentConfig | null = null;
140
141function 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
152function logExit(reason: string): void {
153 if (!exitLogPath) {
154 return;
155 }
156 NodeFS.appendFileSync(exitLogPath, `${reason}\n`, "utf8");
157}
158
159function writeJsonRpcNotification(method: string, params: unknown): void {
160 process.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}\n`);
161}
162
163function logResidualCallbackResponse(kind: string): void {
164 if (!residualCallbackResponseLogPath) return;
165 NodeFS.appendFileSync(residualCallbackResponseLogPath, `${kind}\n`, "utf8");
166}
167
168process.once("SIGTERM", () => {
169 logExit("SIGTERM");
170 process.exit(0);
171});
172
173process.once("SIGINT", () => {
174 logExit("SIGINT");
175 process.exit(0);
176});
177
178process.once("exit", (code) => {
179 logExit(`exit:${code}`);
180});
181
182function 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
364function 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
376function 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
393const 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
398const 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
422function 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.
431const 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
449function 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
462const 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
2482NodeRuntime.runMain(program);