mirror of
https://github.com/VibedByKaKi/t3-code-android-nightly.git
synced 2026-10-09 20:01:15 +02:00
2460 lines
84 KiB
JavaScript
2460 lines
84 KiB
JavaScript
#!/usr/bin/env node
|
|
// @effect-diagnostics nodeBuiltinImport:off
|
|
import * as NodeChildProcess from "node:child_process";
|
|
import * as NodeFS from "node:fs";
|
|
|
|
import * as Effect from "effect/Effect";
|
|
import * as Schema from "effect/Schema";
|
|
import * as Deferred from "effect/Deferred";
|
|
|
|
import * as NodeServices from "@effect/platform-node/NodeServices";
|
|
import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
|
|
|
|
import * as EffectAcpAgent from "effect-acp/agent";
|
|
import * as AcpError from "effect-acp/errors";
|
|
import type * as AcpSchema from "effect-acp/schema";
|
|
import type * as AcpCompat from "effect-acp/compat";
|
|
|
|
import { beginAcpMockPrompt } from "./acpMockCancellationState.ts";
|
|
|
|
const requestLogPath = process.env.T3_ACP_REQUEST_LOG_PATH;
|
|
const exitLogPath = process.env.T3_ACP_EXIT_LOG_PATH;
|
|
const antigravityProfile = process.env.T3_ACP_ANTIGRAVITY === "1";
|
|
const emitToolCalls = process.env.T3_ACP_EMIT_TOOL_CALLS === "1";
|
|
const emitInterleavedAssistantToolCalls =
|
|
process.env.T3_ACP_EMIT_INTERLEAVED_ASSISTANT_TOOL_CALLS === "1";
|
|
const emitV2Fidelity = process.env.T3_ACP_EMIT_V2_FIDELITY === "1";
|
|
const vibeRetryOutcome = process.env.T3_ACP_VIBE_RETRY_OUTCOME;
|
|
const emitGenericToolPlaceholders = process.env.T3_ACP_EMIT_GENERIC_TOOL_PLACEHOLDERS === "1";
|
|
const emitPostSettleMonitorFlow = process.env.T3_ACP_EMIT_POST_SETTLE_MONITOR_FLOW === "1";
|
|
const emitInTurnTaskOutputThenLateDuplicate =
|
|
process.env.T3_ACP_EMIT_IN_TURN_TASKOUTPUT_THEN_LATE_DUPLICATE === "1";
|
|
const injectedReportTriggerPath = process.env.T3_ACP_INJECTED_REPORT_TRIGGER_PATH;
|
|
const emitBackgroundToolDuringAnswer =
|
|
process.env.T3_ACP_EMIT_BACKGROUND_TOOL_DURING_ANSWER === "1";
|
|
const emitAskQuestion = process.env.T3_ACP_EMIT_ASK_QUESTION === "1";
|
|
const emitElicitation = process.env.T3_ACP_EMIT_ELICITATION === "1";
|
|
const emitMcpToolApprovalElicitation =
|
|
process.env.T3_ACP_EMIT_MCP_TOOL_APPROVAL_ELICITATION === "1";
|
|
const emitUrlElicitation = process.env.T3_ACP_EMIT_URL_ELICITATION === "1";
|
|
const emitXAiAskUserQuestion = process.env.T3_ACP_EMIT_XAI_ASK_USER_QUESTION === "1";
|
|
const emitXAiExitPlanMode = process.env.T3_ACP_EMIT_XAI_EXIT_PLAN_MODE === "1";
|
|
const emitXAiPlanMdWrite = process.env.T3_ACP_EMIT_XAI_PLAN_MD_WRITE === "1";
|
|
const emitXAiPromptCompleteThenHang = process.env.T3_ACP_EMIT_XAI_PROMPT_COMPLETE_THEN_HANG === "1";
|
|
const emitXAiRateLimitThenHang = process.env.T3_ACP_EMIT_XAI_RATE_LIMIT_THEN_HANG === "1";
|
|
const emitXAiAskUserQuestionThenHang =
|
|
process.env.T3_ACP_EMIT_XAI_ASK_USER_QUESTION_THEN_HANG === "1";
|
|
const emitContentThenHang = process.env.T3_ACP_EMIT_CONTENT_THEN_HANG === "1";
|
|
const emitPlanThenHang = process.env.T3_ACP_EMIT_PLAN_THEN_HANG === "1";
|
|
const emitActiveToolThenHang = process.env.T3_ACP_EMIT_ACTIVE_TOOL_THEN_HANG === "1";
|
|
const emitGrokMonitorPostTurnPoll = process.env.T3_ACP_EMIT_GROK_MONITOR_POST_TURN_POLL === "1";
|
|
const emitGrokBackgroundTaskStarted = process.env.T3_ACP_EMIT_GROK_BACKGROUND_TASK_STARTED === "1";
|
|
const emitForeignSessionUpdates = process.env.T3_ACP_EMIT_FOREIGN_SESSION_UPDATES === "1";
|
|
const waitForResumeRelease = process.env.T3_ACP_WAIT_FOR_RESUME_RELEASE === "1";
|
|
const completeFirstPromptOnCancel = process.env.T3_ACP_COMPLETE_FIRST_PROMPT_ON_CANCEL === "1";
|
|
const floodStderr = process.env.T3_ACP_FLOOD_STDERR === "1";
|
|
const hangPromptForever = process.env.T3_ACP_HANG_PROMPT_FOREVER === "1";
|
|
// Sends fs/write_text_file for this path, then fs/read_text_file, at the start of
|
|
// each prompt whatever the client advertised, and appends each outcome as a JSON
|
|
// line to T3_ACP_CLIENT_FS_PROBE_LOG_PATH.
|
|
const clientFsProbePath = process.env.T3_ACP_CLIENT_FS_PROBE_PATH;
|
|
const clientFsProbeLogPath = process.env.T3_ACP_CLIENT_FS_PROBE_LOG_PATH;
|
|
const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown));
|
|
const hangAfterPermission = process.env.T3_ACP_HANG_AFTER_PERMISSION === "1";
|
|
const hangFirstPromptForever = process.env.T3_ACP_HANG_FIRST_PROMPT_FOREVER === "1";
|
|
const emitLateUpdateAfterCancel = process.env.T3_ACP_EMIT_LATE_UPDATE_AFTER_CANCEL === "1";
|
|
const emitTaskBackgroundedAfterCancel =
|
|
process.env.T3_ACP_EMIT_TASK_BACKGROUNDED_AFTER_CANCEL === "1";
|
|
const residualCallbackResponseLogPath = process.env.T3_ACP_RESIDUAL_CALLBACK_RESPONSE_LOG_PATH;
|
|
const residualCallbackTriggerPath = process.env.T3_ACP_RESIDUAL_CALLBACK_TRIGGER_PATH;
|
|
const exitAfterResidualCallbacks = process.env.T3_ACP_EXIT_AFTER_RESIDUAL_CALLBACKS === "1";
|
|
const emitRunningCommandThenHang = process.env.T3_ACP_EMIT_RUNNING_COMMAND_THEN_HANG === "1";
|
|
const emitRunningCommandThenHangOnFirstPrompt =
|
|
process.env.T3_ACP_EMIT_RUNNING_COMMAND_THEN_HANG_FIRST_PROMPT === "1";
|
|
const emitEmptySuccessfulBash = process.env.T3_ACP_EMIT_EMPTY_SUCCESSFUL_BASH === "1";
|
|
const emitEmptySuccessfulBashThenHang =
|
|
process.env.T3_ACP_EMIT_EMPTY_SUCCESSFUL_BASH_THEN_HANG === "1";
|
|
const exitOnCancel = process.env.T3_ACP_EXIT_ON_CANCEL === "1";
|
|
const runningCommandIgnoresTerm = process.env.T3_ACP_RUNNING_COMMAND_IGNORE_TERM === "1";
|
|
const runningCommandPidPath = process.env.T3_ACP_RUNNING_COMMAND_PID_PATH;
|
|
const runningCommandSeparateSession = process.env.T3_ACP_RUNNING_COMMAND_SEPARATE_SESSION === "1";
|
|
const exitAfterRunningCommandLaunch = process.env.T3_ACP_EXIT_AFTER_RUNNING_COMMAND_LAUNCH === "1";
|
|
const omitXAiPromptCompleteStopReason =
|
|
process.env.T3_ACP_OMIT_XAI_PROMPT_COMPLETE_STOP_REASON === "1";
|
|
const failLoadSession = process.env.T3_ACP_FAIL_LOAD_SESSION === "1";
|
|
const failLoadSessionAfterConfigReplay =
|
|
process.env.T3_ACP_FAIL_LOAD_SESSION_AFTER_CONFIG_REPLAY === "1";
|
|
const emitLoadReplay = process.env.T3_ACP_EMIT_LOAD_REPLAY === "1";
|
|
const hangLoadSessionAfterReplay = process.env.T3_ACP_HANG_LOAD_SESSION_AFTER_REPLAY === "1";
|
|
const delayLoadSessionAfterReplay = process.env.T3_ACP_DELAY_LOAD_SESSION_AFTER_REPLAY === "1";
|
|
const loadSessionDelayMs = Number(process.env.T3_ACP_LOAD_SESSION_DELAY_MS ?? "5000");
|
|
const emitStaleXAiPromptCompleteBeforeSecondHang =
|
|
process.env.T3_ACP_EMIT_STALE_XAI_PROMPT_COMPLETE_BEFORE_SECOND_HANG === "1";
|
|
const emitOverlappingXAiPromptCompleteOutOfOrder =
|
|
process.env.T3_ACP_EMIT_OVERLAPPING_XAI_PROMPT_COMPLETE_OUT_OF_ORDER === "1";
|
|
const failPrompt = process.env.T3_ACP_FAIL_PROMPT === "1";
|
|
const failSetConfigOption = process.env.T3_ACP_FAIL_SET_CONFIG_OPTION === "1";
|
|
const exitOnSetConfigOption = process.env.T3_ACP_EXIT_ON_SET_CONFIG_OPTION === "1";
|
|
const omitModelConfigOption = process.env.T3_ACP_OMIT_MODEL_CONFIG_OPTION === "1";
|
|
const promptResponseText = process.env.T3_ACP_PROMPT_RESPONSE_TEXT;
|
|
const initialGrokReasoningEffort =
|
|
process.env.T3_ACP_INITIAL_GROK_REASONING_EFFORT?.trim() || undefined;
|
|
const promptDelayMs = Number(process.env.T3_ACP_PROMPT_DELAY_MS ?? "0");
|
|
const supportsSessionLifecycle = process.env.T3_ACP_SESSION_LIFECYCLE === "1";
|
|
const supportsAcpMcp = process.env.T3_ACP_MCP_ACP === "1";
|
|
const supportsV2Management = process.env.T3_ACP_V2_MANAGEMENT === "1";
|
|
const omitSessionListHandler = process.env.T3_ACP_OMIT_SESSION_LIST_HANDLER === "1";
|
|
const advertisedAuthMethodId = process.env.T3_ACP_AUTH_METHOD_ID?.trim();
|
|
const initializeAuthMethodId =
|
|
advertisedAuthMethodId ?? (supportsSessionLifecycle ? "test" : undefined);
|
|
const requiresAuthentication = process.env.T3_ACP_REQUIRE_AUTH === "1";
|
|
const commandAdvertisementDelayMs = Number(
|
|
process.env.T3_ACP_COMMAND_ADVERTISEMENT_DELAY_MS ?? "-1",
|
|
);
|
|
const permissionOptionIds = {
|
|
allowOnce: process.env.T3_ACP_ALLOW_ONCE_OPTION_ID ?? "allow-once",
|
|
allowAlways: process.env.T3_ACP_ALLOW_ALWAYS_OPTION_ID ?? "allow-always",
|
|
rejectOnce: process.env.T3_ACP_REJECT_ONCE_OPTION_ID ?? "reject-once",
|
|
};
|
|
const omitAllowAlways = process.env.T3_ACP_OMIT_ALLOW_ALWAYS === "1";
|
|
const permissionRequestCount = Math.max(
|
|
1,
|
|
Number(process.env.T3_ACP_PERMISSION_REQUEST_COUNT ?? "1") || 1,
|
|
);
|
|
const sessionId = "mock-session-1";
|
|
|
|
let currentModeId = antigravityProfile ? "default" : "ask";
|
|
let currentModelId = antigravityProfile ? "gemini-test-low" : "default";
|
|
let parameterizedModelPicker = false;
|
|
let currentReasoning = "medium";
|
|
let currentContext = "272k";
|
|
let currentFast = false;
|
|
let authenticated = !requiresAuthentication;
|
|
let promptCount = 0;
|
|
let overlappingFirstPromptId: string | undefined;
|
|
const cancelledSessions = new Set<string>();
|
|
let configuredProvider: AcpSchema.ProviderCurrentConfig | null = null;
|
|
|
|
function promptIdFromRequestMeta(
|
|
request: Pick<AcpSchema.PromptRequest, "_meta">,
|
|
): string | undefined {
|
|
const meta = request._meta;
|
|
if (meta === null || typeof meta !== "object") {
|
|
return undefined;
|
|
}
|
|
const promptId = meta.promptId ?? meta.requestId;
|
|
return typeof promptId === "string" && promptId.length > 0 ? promptId : undefined;
|
|
}
|
|
|
|
function logExit(reason: string): void {
|
|
if (!exitLogPath) {
|
|
return;
|
|
}
|
|
NodeFS.appendFileSync(exitLogPath, `${reason}\n`, "utf8");
|
|
}
|
|
|
|
function writeJsonRpcNotification(method: string, params: unknown): void {
|
|
process.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}\n`);
|
|
}
|
|
|
|
function logResidualCallbackResponse(kind: string): void {
|
|
if (!residualCallbackResponseLogPath) return;
|
|
NodeFS.appendFileSync(residualCallbackResponseLogPath, `${kind}\n`, "utf8");
|
|
}
|
|
|
|
process.once("SIGTERM", () => {
|
|
logExit("SIGTERM");
|
|
process.exit(0);
|
|
});
|
|
|
|
process.once("SIGINT", () => {
|
|
logExit("SIGINT");
|
|
process.exit(0);
|
|
});
|
|
|
|
process.once("exit", (code) => {
|
|
logExit(`exit:${code}`);
|
|
});
|
|
|
|
function configOptions(): ReadonlyArray<AcpSchema.SessionConfigOption> {
|
|
if (omitModelConfigOption) {
|
|
return [];
|
|
}
|
|
if (antigravityProfile) {
|
|
return [
|
|
{
|
|
configId: "model",
|
|
name: "Model",
|
|
category: "model",
|
|
type: "select",
|
|
currentValue: currentModelId,
|
|
options: antigravityModels.map((model) => ({ value: model.modelId, name: model.name })),
|
|
},
|
|
{
|
|
configId: "mode",
|
|
name: "Mode",
|
|
category: "mode",
|
|
type: "select",
|
|
currentValue: currentModeId,
|
|
options: availableModes.map((mode) => ({ value: mode.id, name: mode.name })),
|
|
},
|
|
];
|
|
}
|
|
if (parameterizedModelPicker) {
|
|
const baseOptions: Array<AcpSchema.SessionConfigOption> = [
|
|
{
|
|
configId: "mode",
|
|
name: "Mode",
|
|
category: "mode",
|
|
type: "select",
|
|
currentValue: currentModeId,
|
|
options: availableModes.map((mode) => ({
|
|
value: mode.id,
|
|
name: mode.name,
|
|
...(mode.description ? { description: mode.description } : {}),
|
|
})),
|
|
},
|
|
{
|
|
configId: "model",
|
|
name: "Model",
|
|
category: "model",
|
|
type: "select",
|
|
currentValue: currentModelId,
|
|
options: [
|
|
{ value: "default", name: "Auto" },
|
|
{ value: "composer-2", name: "Composer 2" },
|
|
{ value: "gpt-5.4", name: "GPT-5.4" },
|
|
{ value: "claude-opus-4-6", name: "Opus 4.6" },
|
|
],
|
|
},
|
|
];
|
|
|
|
switch (currentModelId) {
|
|
case "gpt-5.4":
|
|
return [
|
|
...baseOptions,
|
|
{
|
|
configId: "reasoning",
|
|
name: "Reasoning",
|
|
category: "thought_level",
|
|
type: "select",
|
|
currentValue: currentReasoning,
|
|
options: [
|
|
{ value: "none", name: "None" },
|
|
{ value: "low", name: "Low" },
|
|
{ value: "medium", name: "Medium" },
|
|
{ value: "high", name: "High" },
|
|
{ value: "extra-high", name: "Extra High" },
|
|
],
|
|
},
|
|
{
|
|
configId: "context",
|
|
name: "Context",
|
|
category: "model_config",
|
|
type: "select",
|
|
currentValue: currentContext,
|
|
options: [
|
|
{ value: "272k", name: "272K" },
|
|
{ value: "1m", name: "1M" },
|
|
],
|
|
},
|
|
{
|
|
configId: "fast",
|
|
name: "Fast",
|
|
category: "model_config",
|
|
type: "select",
|
|
currentValue: String(currentFast),
|
|
options: [
|
|
{ value: "false", name: "Off" },
|
|
{ value: "true", name: "Fast" },
|
|
],
|
|
},
|
|
];
|
|
case "composer-2":
|
|
return [
|
|
...baseOptions,
|
|
{
|
|
configId: "fast",
|
|
name: "Fast",
|
|
category: "model_config",
|
|
type: "select",
|
|
currentValue: String(currentFast),
|
|
options: [
|
|
{ value: "false", name: "Off" },
|
|
{ value: "true", name: "Fast" },
|
|
],
|
|
},
|
|
];
|
|
case "claude-opus-4-6":
|
|
return [
|
|
...baseOptions,
|
|
{
|
|
configId: "reasoning",
|
|
name: "Reasoning",
|
|
category: "thought_level",
|
|
type: "select",
|
|
currentValue: currentReasoning,
|
|
options: [
|
|
{ value: "low", name: "Low" },
|
|
{ value: "medium", name: "Medium" },
|
|
{ value: "high", name: "High" },
|
|
],
|
|
},
|
|
{
|
|
configId: "thinking",
|
|
name: "Thinking",
|
|
category: "model_config",
|
|
type: "boolean",
|
|
currentValue: true,
|
|
},
|
|
];
|
|
default:
|
|
return baseOptions;
|
|
}
|
|
}
|
|
|
|
return [
|
|
{
|
|
configId: "mode",
|
|
name: "Mode",
|
|
category: "mode",
|
|
type: "select" as const,
|
|
currentValue: currentModeId,
|
|
options: availableModes.map((mode) => ({
|
|
value: mode.id,
|
|
name: mode.name,
|
|
...(mode.description ? { description: mode.description } : {}),
|
|
})),
|
|
},
|
|
{
|
|
configId: "model",
|
|
name: "Model",
|
|
category: "model",
|
|
type: "select" as const,
|
|
currentValue: currentModelId,
|
|
options: [
|
|
{ value: "default", name: "Auto" },
|
|
{ value: "composer-2", name: "Composer 2" },
|
|
{ value: "composer-2[fast=true]", name: "Composer 2 Fast" },
|
|
{ value: "gpt-5.3-codex[reasoning=medium,fast=false]", name: "Codex 5.3" },
|
|
],
|
|
},
|
|
];
|
|
}
|
|
|
|
function modelConfigOptionsFor(modelId: string): ReadonlyArray<AcpSchema.SessionConfigOption> {
|
|
const previousModelId = currentModelId;
|
|
try {
|
|
currentModelId = modelId;
|
|
return configOptions().filter(
|
|
(option) => option.category !== "mode" && option.category !== "model",
|
|
);
|
|
} finally {
|
|
currentModelId = previousModelId;
|
|
}
|
|
}
|
|
|
|
function availableModels(): ReadonlyArray<{
|
|
readonly value: string;
|
|
readonly name: string;
|
|
readonly configOptions: ReadonlyArray<AcpSchema.SessionConfigOption>;
|
|
}> {
|
|
return [
|
|
{ value: "default", name: "Auto" },
|
|
{ value: "composer-2", name: "Composer 2" },
|
|
{ value: "gpt-5.4", name: "GPT-5.4" },
|
|
{ value: "claude-opus-4-6", name: "Opus 4.6" },
|
|
].map((model) => ({
|
|
value: model.value,
|
|
name: model.name,
|
|
configOptions: modelConfigOptionsFor(model.value),
|
|
}));
|
|
}
|
|
|
|
const antigravityModels = [
|
|
{ modelId: "gemini-test-low", name: "Gemini Test Low" },
|
|
{ modelId: "gemini-test-high", name: "Gemini Test High" },
|
|
] satisfies ReadonlyArray<AcpCompat.ModelInfo>;
|
|
|
|
const availableModes: ReadonlyArray<AcpCompat.SessionMode> = antigravityProfile
|
|
? [
|
|
{ id: "default", name: "Default" },
|
|
{ id: "auto_edit", name: "Auto edit" },
|
|
{ id: "yolo", name: "YOLO" },
|
|
]
|
|
: [
|
|
{
|
|
id: "ask",
|
|
name: "Ask",
|
|
description: "Request permission before making any changes",
|
|
},
|
|
{
|
|
id: "architect",
|
|
name: "Architect",
|
|
description: "Design and plan software systems without implementation",
|
|
},
|
|
{
|
|
id: "code",
|
|
name: "Code",
|
|
description: "Write and modify code with full tool access",
|
|
},
|
|
];
|
|
|
|
function modeState(): AcpCompat.SessionModeState {
|
|
return {
|
|
currentModeId,
|
|
availableModes,
|
|
};
|
|
}
|
|
|
|
// Mirrors the real Grok ACP: it advertises versioned model ids, never the CLI's own
|
|
// "grok-build" product name. Native helper model switching has its own v1 fixture.
|
|
const grokAcpModels: ReadonlyArray<AcpCompat.ModelInfo> = [
|
|
{
|
|
modelId: "grok-4.6",
|
|
name: "Grok 4.6",
|
|
_meta: {
|
|
totalContextTokens: 500_000,
|
|
supportsReasoningEffort: true,
|
|
reasoningEffort: initialGrokReasoningEffort ?? "high",
|
|
reasoningEfforts: [
|
|
{ id: "xhigh", value: "xhigh", label: "Extra High Effort", default: false },
|
|
{ id: "high", value: "high", label: "High Effort", default: true },
|
|
{ id: "low", value: "low", label: "Low Effort", default: false },
|
|
],
|
|
},
|
|
},
|
|
{ modelId: "grok-mock-alt", name: "Grok Mock Alt" },
|
|
];
|
|
|
|
function modelState(): AcpCompat.SessionModelState {
|
|
if (antigravityProfile) {
|
|
return { currentModelId, availableModels: antigravityModels };
|
|
}
|
|
const modelId = grokAcpModels.some((model) => model.modelId === currentModelId)
|
|
? currentModelId
|
|
: "grok-4.6";
|
|
return {
|
|
currentModelId: modelId,
|
|
availableModels: grokAcpModels,
|
|
};
|
|
}
|
|
|
|
const program = Effect.gen(function* () {
|
|
const agent = yield* EffectAcpAgent.AcpAgent;
|
|
const resumeRelease = yield* Deferred.make<void>();
|
|
const nativeCancelRequested = yield* Deferred.make<void>();
|
|
const nativeCancelRelease = yield* Deferred.make<void>();
|
|
const publishAntigravityCommands = (targetSessionId: string) =>
|
|
agent.client.sessionUpdate({
|
|
sessionId: targetSessionId,
|
|
update: {
|
|
sessionUpdate: "available_commands_update",
|
|
availableCommands: [
|
|
{ name: "plan", description: "Plan a task", input: { hint: "task" } },
|
|
{ name: "logout", description: "Sign out" },
|
|
],
|
|
},
|
|
});
|
|
|
|
const finishPrompt = (
|
|
targetSessionId: string,
|
|
stopReason: AcpSchema.StopReason,
|
|
_meta?: NonNullable<AcpSchema.PromptResponse["_meta"]>,
|
|
) =>
|
|
agent.client
|
|
.sessionUpdate({
|
|
sessionId: targetSessionId,
|
|
update: {
|
|
sessionUpdate: "state_update",
|
|
state: "idle",
|
|
stopReason,
|
|
...(_meta === undefined ? {} : { _meta }),
|
|
},
|
|
})
|
|
.pipe(Effect.as(_meta === undefined ? {} : { _meta }));
|
|
|
|
yield* agent.handleInitialize((request) =>
|
|
Effect.gen(function* () {
|
|
if (floodStderr) {
|
|
yield* Effect.promise(
|
|
() =>
|
|
new Promise<void>((resolve) => {
|
|
process.stderr.write("stderr".repeat(350_000), () => resolve());
|
|
}),
|
|
);
|
|
}
|
|
parameterizedModelPicker = request.capabilities?._meta?.parameterizedModelPicker === true;
|
|
return {
|
|
protocolVersion: 2,
|
|
info: { name: "t3-acp-mock-agent", version: "0.0.0" },
|
|
_meta: { modelState: modelState() },
|
|
capabilities: {
|
|
session: {
|
|
...(supportsSessionLifecycle ? { fork: {}, additionalDirectories: {} } : {}),
|
|
...(supportsV2Management ? { delete: {} } : {}),
|
|
...(supportsAcpMcp ? { mcp: { acp: {} } } : {}),
|
|
},
|
|
...(supportsV2Management ? { providers: {} } : {}),
|
|
},
|
|
...(initializeAuthMethodId
|
|
? {
|
|
authMethods: [
|
|
{
|
|
type: "agent" as const,
|
|
methodId: initializeAuthMethodId,
|
|
name: "Mock agent authentication",
|
|
},
|
|
],
|
|
}
|
|
: {}),
|
|
};
|
|
}),
|
|
);
|
|
|
|
// Mirrors the real Antigravity agent: the API key method reads
|
|
// GEMINI_API_KEY from the process environment and rejects when it is missing.
|
|
yield* agent.handleAuthenticate((request) =>
|
|
Effect.gen(function* () {
|
|
if (antigravityProfile) {
|
|
if (
|
|
request.methodId !== "oauth-personal" &&
|
|
!(request.methodId === "gemini-api-key" && process.env.GEMINI_API_KEY)
|
|
) {
|
|
return yield* AcpError.AcpRequestError.invalidParams(
|
|
`Mock Antigravity rejected auth method ${request.methodId}.`,
|
|
);
|
|
}
|
|
} else if (advertisedAuthMethodId && request.methodId !== advertisedAuthMethodId) {
|
|
return yield* AcpError.AcpRequestError.invalidParams(
|
|
`Unknown mock authentication method: ${request.methodId}`,
|
|
);
|
|
}
|
|
authenticated = true;
|
|
return {};
|
|
}),
|
|
);
|
|
if (antigravityProfile) {
|
|
yield* agent.handleLogout(() => Effect.succeed({}));
|
|
}
|
|
|
|
yield* agent.handleLogout(() =>
|
|
Effect.sync(() => {
|
|
authenticated = false;
|
|
return {};
|
|
}),
|
|
);
|
|
|
|
const requireAuthentication = Effect.fn("acpMockAgent.requireAuthentication")(function* () {
|
|
if (!authenticated) {
|
|
return yield* AcpError.AcpRequestError.authRequired();
|
|
}
|
|
});
|
|
|
|
yield* agent.handleCreateSession(() =>
|
|
Effect.gen(function* () {
|
|
yield* requireAuthentication();
|
|
if (antigravityProfile) {
|
|
yield* publishAntigravityCommands(sessionId);
|
|
}
|
|
if (commandAdvertisementDelayMs >= 0) {
|
|
yield* Effect.sleep(`${commandAdvertisementDelayMs} millis`).pipe(
|
|
Effect.andThen(
|
|
agent.client.sessionUpdate({
|
|
sessionId,
|
|
update: {
|
|
sessionUpdate: "available_commands_update",
|
|
availableCommands: [
|
|
{
|
|
name: "review",
|
|
description: "Review the current changes",
|
|
input: { type: "text", hint: "focus" },
|
|
},
|
|
{
|
|
name: "$workspace-skill",
|
|
description: "Run the workspace skill",
|
|
input: null,
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
),
|
|
Effect.forkDetach,
|
|
);
|
|
}
|
|
return {
|
|
sessionId,
|
|
configOptions: configOptions(),
|
|
};
|
|
}),
|
|
);
|
|
|
|
yield* agent.handleResumeSession((request) =>
|
|
Effect.gen(function* () {
|
|
yield* requireAuthentication();
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: request.sessionId,
|
|
update: {
|
|
sessionUpdate: "user_message_chunk",
|
|
content: { type: "text", text: "native-resume-started" },
|
|
},
|
|
});
|
|
if (waitForResumeRelease) {
|
|
yield* Deferred.await(resumeRelease);
|
|
}
|
|
if (antigravityProfile) {
|
|
yield* publishAntigravityCommands(request.sessionId);
|
|
}
|
|
return {
|
|
modes: modeState(),
|
|
models: modelState(),
|
|
configOptions: configOptions(),
|
|
_meta: { nativeResume: true },
|
|
};
|
|
}),
|
|
);
|
|
|
|
const emitLoadReplayNotifications = (requestedSessionId: string) => {
|
|
writeJsonRpcNotification("session/update", {
|
|
_meta: { isReplay: true },
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId: "replay-tool-1",
|
|
title: "Replay tool",
|
|
kind: "search",
|
|
status: "completed",
|
|
},
|
|
});
|
|
writeJsonRpcNotification("session/update", {
|
|
_meta: { isReplay: true },
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "agent_message_chunk",
|
|
messageId: "mock-agent-message",
|
|
content: { type: "text", text: "replayed assistant text" },
|
|
},
|
|
});
|
|
};
|
|
|
|
yield* agent.handleLoadSession((request) =>
|
|
Effect.gen(function* () {
|
|
const requestedSessionId = String(request.sessionId ?? sessionId);
|
|
if (failLoadSession) {
|
|
return yield* AcpError.AcpRequestError.internalError("Mock load session failure");
|
|
}
|
|
if (failLoadSessionAfterConfigReplay) {
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "config_option_update",
|
|
configOptions: [
|
|
{
|
|
configId: "candidate-only",
|
|
name: "Candidate only",
|
|
type: "boolean",
|
|
currentValue: true,
|
|
},
|
|
],
|
|
},
|
|
});
|
|
return yield* AcpError.AcpRequestError.internalError(
|
|
"Mock load session failure after config replay",
|
|
);
|
|
}
|
|
if (hangLoadSessionAfterReplay || delayLoadSessionAfterReplay) {
|
|
emitLoadReplayNotifications(requestedSessionId);
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "user_message_chunk",
|
|
messageId: "mock-user-message",
|
|
content: { type: "text", text: "replay-tail" },
|
|
},
|
|
});
|
|
yield* Effect.sleep(loadSessionDelayMs);
|
|
return {
|
|
configOptions: configOptions(),
|
|
};
|
|
}
|
|
if (emitLoadReplay) {
|
|
emitLoadReplayNotifications(requestedSessionId);
|
|
}
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "user_message_chunk",
|
|
messageId: "mock-user-message",
|
|
content: { type: "text", text: "replay" },
|
|
},
|
|
});
|
|
return {
|
|
configOptions: configOptions(),
|
|
};
|
|
}),
|
|
);
|
|
|
|
if (!omitSessionListHandler) {
|
|
yield* agent.handleListSessions((request) =>
|
|
Effect.gen(function* () {
|
|
yield* requireAuthentication();
|
|
return {
|
|
sessions: [
|
|
{
|
|
sessionId,
|
|
cwd: request.cwd ?? process.cwd(),
|
|
title: "Mock session",
|
|
updatedAt: "1970-01-01T00:00:00.000Z",
|
|
},
|
|
],
|
|
};
|
|
}),
|
|
);
|
|
}
|
|
|
|
yield* agent.handleForkSession((request) =>
|
|
Effect.gen(function* () {
|
|
yield* requireAuthentication();
|
|
return {
|
|
sessionId: `${request.sessionId}-fork`,
|
|
configOptions: configOptions(),
|
|
};
|
|
}),
|
|
);
|
|
|
|
yield* agent.handleCloseSession(() =>
|
|
Effect.gen(function* () {
|
|
yield* requireAuthentication();
|
|
return {};
|
|
}),
|
|
);
|
|
|
|
yield* agent.handleDeleteSession(() =>
|
|
Effect.gen(function* () {
|
|
yield* requireAuthentication();
|
|
return {};
|
|
}),
|
|
);
|
|
|
|
yield* agent.handleListProviders(() =>
|
|
Effect.gen(function* () {
|
|
yield* requireAuthentication();
|
|
return {
|
|
providers: [
|
|
{
|
|
providerId: "mock-provider",
|
|
supported: ["openai", "anthropic"],
|
|
required: false,
|
|
current: configuredProvider,
|
|
},
|
|
],
|
|
};
|
|
}),
|
|
);
|
|
|
|
yield* agent.handleSetProvider((request) =>
|
|
Effect.gen(function* () {
|
|
yield* requireAuthentication();
|
|
configuredProvider = { apiType: request.apiType, baseUrl: request.baseUrl };
|
|
return {};
|
|
}),
|
|
);
|
|
|
|
yield* agent.handleDisableProvider(() =>
|
|
Effect.gen(function* () {
|
|
yield* requireAuthentication();
|
|
configuredProvider = null;
|
|
return {};
|
|
}),
|
|
);
|
|
|
|
yield* agent.handleSetSessionConfigOption((request) =>
|
|
Effect.gen(function* () {
|
|
if (exitOnSetConfigOption) {
|
|
return yield* Effect.sync(() => {
|
|
process.exit(7);
|
|
});
|
|
}
|
|
if (failSetConfigOption) {
|
|
return yield* AcpError.AcpRequestError.invalidParams(
|
|
"Mock invalid params for session/set_config_option",
|
|
{
|
|
method: "session/set_config_option",
|
|
params: request,
|
|
},
|
|
);
|
|
}
|
|
if (request.configId === "mode" && typeof request.value === "string") {
|
|
currentModeId = request.value;
|
|
}
|
|
if (request.configId === "model" && typeof request.value === "string") {
|
|
currentModelId = request.value;
|
|
}
|
|
if (request.configId === "reasoning" && typeof request.value === "string") {
|
|
currentReasoning = request.value;
|
|
}
|
|
if (request.configId === "context" && typeof request.value === "string") {
|
|
currentContext = request.value;
|
|
}
|
|
if (request.configId === "fast") {
|
|
currentFast = request.value === true || request.value === "true";
|
|
}
|
|
return {
|
|
configOptions: configOptions(),
|
|
};
|
|
}),
|
|
);
|
|
|
|
yield* agent.handleCancel(({ sessionId }) =>
|
|
Effect.gen(function* () {
|
|
const cancelledSessionId = String(sessionId ?? "mock-session-1");
|
|
cancelledSessions.add(cancelledSessionId);
|
|
if (exitOnCancel) {
|
|
return yield* Effect.sync(() => process.exit(0));
|
|
}
|
|
if (completeFirstPromptOnCancel) {
|
|
yield* Deferred.succeed(nativeCancelRequested, undefined);
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: cancelledSessionId,
|
|
update: {
|
|
sessionUpdate: "agent_thought_chunk",
|
|
content: { type: "text", text: "native-cancel-received" },
|
|
},
|
|
});
|
|
}
|
|
if (emitLateUpdateAfterCancel) {
|
|
yield* Effect.sleep("50 millis");
|
|
yield* Effect.sync(() => {
|
|
writeJsonRpcNotification("session/update", {
|
|
sessionId: cancelledSessionId,
|
|
update: {
|
|
sessionUpdate: "agent_message_chunk",
|
|
messageId: "mock-agent-message",
|
|
content: { type: "text", text: "late after cancel" },
|
|
},
|
|
});
|
|
});
|
|
}
|
|
if (emitTaskBackgroundedAfterCancel) {
|
|
// Grok cancel-as-detach: the foreground command is re-run as a
|
|
// background task that later completes on its own.
|
|
yield* Effect.sync(() => {
|
|
writeJsonRpcNotification("_x.ai/task_backgrounded", {
|
|
sessionId: cancelledSessionId,
|
|
update: {
|
|
sessionUpdate: "task_backgrounded",
|
|
tool_call_id: "task-bg-1",
|
|
task_id: "task-bg-1",
|
|
command: "sleep 30",
|
|
},
|
|
});
|
|
});
|
|
yield* Effect.sleep("1200 millis")
|
|
.pipe(
|
|
Effect.andThen(
|
|
Effect.sync(() => {
|
|
writeJsonRpcNotification("_x.ai/task_completed", {
|
|
sessionId: cancelledSessionId,
|
|
update: {
|
|
sessionUpdate: "task_completed",
|
|
task_snapshot: { task_id: "task-bg-1", command: "sleep 30" },
|
|
},
|
|
});
|
|
}),
|
|
),
|
|
)
|
|
.pipe(Effect.forkDetach);
|
|
}
|
|
}),
|
|
);
|
|
|
|
yield* agent.handlePrompt((request) =>
|
|
Effect.gen(function* () {
|
|
const requestedSessionId = String(request.sessionId ?? sessionId);
|
|
beginAcpMockPrompt(cancelledSessions, requestedSessionId);
|
|
promptCount += 1;
|
|
if (
|
|
process.env.T3_ACP_CRASH_PROMPT === "1" &&
|
|
request.prompt.some((part) => part.type === "text" && part.text === "crash now")
|
|
) {
|
|
return yield* Effect.sync(() => process.exit(23));
|
|
}
|
|
|
|
if (clientFsProbePath !== undefined && clientFsProbeLogPath !== undefined) {
|
|
const probes = [
|
|
[
|
|
"fs/write_text_file",
|
|
{ sessionId: requestedSessionId, path: clientFsProbePath, content: "probe" },
|
|
],
|
|
["fs/read_text_file", { sessionId: requestedSessionId, path: clientFsProbePath }],
|
|
] as const;
|
|
for (const [method, params] of probes) {
|
|
const outcome = yield* agent.raw.request(method, params).pipe(
|
|
Effect.map((result) => ({ method, result })),
|
|
Effect.catch((error) =>
|
|
Effect.succeed({
|
|
method,
|
|
errorCode: error._tag === "AcpRequestError" ? error.code : error._tag,
|
|
}),
|
|
),
|
|
);
|
|
NodeFS.appendFileSync(clientFsProbeLogPath, `${encodeJson(outcome)}\n`, "utf8");
|
|
}
|
|
}
|
|
|
|
if (vibeRetryOutcome !== undefined) {
|
|
if (vibeRetryOutcome === "recovered") {
|
|
yield* Effect.sync(() =>
|
|
writeJsonRpcNotification("session/update", {
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "tool_call",
|
|
toolCallId: "tool-before-retry",
|
|
title: "Read file",
|
|
kind: "read",
|
|
status: "in_progress",
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
for (const [index, noticeSessionId] of [
|
|
"unrelated-session",
|
|
requestedSessionId,
|
|
requestedSessionId,
|
|
].entries()) {
|
|
// Progress from an earlier tool must not end the retry.
|
|
if (index === 2 && vibeRetryOutcome === "recovered") {
|
|
yield* Effect.sync(() =>
|
|
writeJsonRpcNotification("session/update", {
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId: "tool-before-retry",
|
|
status: "in_progress",
|
|
rawOutput: { progress: "still reading" },
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
yield* Effect.sync(() =>
|
|
writeJsonRpcNotification("_session/retrying", {
|
|
sessionId: noticeSessionId,
|
|
category: "rate_limited",
|
|
detail: "Rate limit reached. Retrying. api_key=private-key",
|
|
}),
|
|
);
|
|
}
|
|
if (vibeRetryOutcome === "failed") {
|
|
return yield* new AcpError.AcpRequestError({
|
|
code: -31001,
|
|
errorMessage: "Rate limit exceeded for mistral (model: mistral-vibe-cli-latest).",
|
|
});
|
|
}
|
|
if (vibeRetryOutcome === "completed") {
|
|
return yield* finishPrompt(requestedSessionId, "end_turn");
|
|
}
|
|
if (vibeRetryOutcome === "cancelled") {
|
|
return yield* finishPrompt(requestedSessionId, "cancelled");
|
|
}
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "agent_message_chunk",
|
|
content: { type: "text", text: "Recovered answer" },
|
|
},
|
|
});
|
|
return yield* finishPrompt(requestedSessionId, "end_turn");
|
|
}
|
|
|
|
if (emitV2Fidelity) {
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: `${requestedSessionId}-child`,
|
|
update: {
|
|
sessionUpdate: "terminal_update",
|
|
terminalId: "standalone-terminal",
|
|
command: "printf child",
|
|
output: { data: Buffer.from("child").toString("base64") },
|
|
},
|
|
});
|
|
const updates: ReadonlyArray<AcpSchema.SessionUpdate> = [
|
|
{
|
|
sessionUpdate: "user_message_chunk",
|
|
messageId: "user-1",
|
|
content: { type: "text", text: "stale user text" },
|
|
},
|
|
{ sessionUpdate: "user_message", messageId: "user-1", content: null },
|
|
{
|
|
sessionUpdate: "agent_thought_chunk",
|
|
messageId: "thought-1",
|
|
content: { type: "text", text: "stale thought" },
|
|
},
|
|
{
|
|
sessionUpdate: "agent_message_chunk",
|
|
messageId: "assistant-1",
|
|
content: { type: "text", text: "interleaved answer" },
|
|
},
|
|
{
|
|
sessionUpdate: "agent_message",
|
|
messageId: "assistant-1",
|
|
content: [{ type: "text", text: "authoritative answer" }],
|
|
},
|
|
{ sessionUpdate: "agent_message", messageId: "assistant-1" },
|
|
{
|
|
sessionUpdate: "agent_thought",
|
|
messageId: "thought-1",
|
|
content: [{ type: "text", text: "final thought" }],
|
|
},
|
|
{
|
|
sessionUpdate: "plan_update",
|
|
plan: { type: "markdown", planId: "plan-a", content: "# Plan A" },
|
|
},
|
|
{
|
|
sessionUpdate: "plan_update",
|
|
plan: {
|
|
type: "items",
|
|
planId: "plan-b",
|
|
entries: [{ content: "Ship B", priority: "high", status: "in_progress" }],
|
|
},
|
|
},
|
|
{ sessionUpdate: "plan_removed", planId: "plan-a" },
|
|
{
|
|
sessionUpdate: "terminal_update",
|
|
terminalId: "standalone-terminal",
|
|
command: "printf proof",
|
|
cwd: process.cwd(),
|
|
},
|
|
{
|
|
sessionUpdate: "terminal_output_chunk",
|
|
terminalId: "standalone-terminal",
|
|
data: Buffer.from("proof").toString("base64"),
|
|
},
|
|
{
|
|
sessionUpdate: "terminal_update",
|
|
terminalId: "standalone-terminal",
|
|
exitStatus: { exitCode: 0 },
|
|
},
|
|
{ sessionUpdate: "state_update", state: "requires_action" },
|
|
{ sessionUpdate: "state_update", state: "running" },
|
|
{
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId: "structured-diff",
|
|
title: "Edit files",
|
|
kind: "edit",
|
|
status: "completed",
|
|
content: [
|
|
{
|
|
type: "diff",
|
|
changes: [
|
|
{
|
|
operation: "move",
|
|
oldPath: "/workspace/old.ts",
|
|
path: "/workspace/new.ts",
|
|
fileType: "text",
|
|
mimeType: "text/typescript",
|
|
},
|
|
],
|
|
patch: {
|
|
format: "git_patch",
|
|
text: "diff --git a/old.ts b/new.ts\nrename from old.ts\nrename to new.ts\n",
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId: "structured-read",
|
|
title: "Read `src/env.ts`",
|
|
kind: "read",
|
|
status: "completed",
|
|
rawInput: { path: "src/env.ts" },
|
|
locations: [{ path: "src/env.ts" }],
|
|
},
|
|
{
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId: "structured-search",
|
|
title: "Grep",
|
|
kind: "search",
|
|
status: "completed",
|
|
rawInput: { query: "TODO", path: "apps/web" },
|
|
},
|
|
// Grok backend searches: the query only arrives in the completed rawOutput.
|
|
{
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId: "grok-x-search",
|
|
title: "X search:",
|
|
kind: "search",
|
|
status: "in_progress",
|
|
rawInput: { variant: "XSearch", backend: true },
|
|
},
|
|
{
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId: "grok-x-search",
|
|
title: "X search:",
|
|
status: "completed",
|
|
rawOutput: {
|
|
call_id: "xs_call-1",
|
|
input: '{"query":"conversation_id:42","limit":"10","mode":"Latest"}',
|
|
name: "x_keyword_search",
|
|
id: "grok-x-search",
|
|
},
|
|
},
|
|
{
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId: "grok-web-search",
|
|
title: "Web search:",
|
|
kind: "search",
|
|
status: "completed",
|
|
rawInput: { variant: "WebSearch", backend: true },
|
|
rawOutput: {
|
|
action: {
|
|
type: "search",
|
|
query: "t3 code",
|
|
sources: [
|
|
{ type: "url", url: "https://t3.codes" },
|
|
{ type: "url", url: "https://t3.codes" },
|
|
{ type: "url", url: "https://github.com/pingdotgg/t3code" },
|
|
],
|
|
},
|
|
id: "grok-web-search",
|
|
status: "completed",
|
|
},
|
|
},
|
|
{
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId: "grok-web-fetch",
|
|
title: "Fetch: https://t3.codes",
|
|
kind: "fetch",
|
|
status: "completed",
|
|
rawInput: { variant: "WebFetch", url: "https://t3.codes" },
|
|
rawOutput: {
|
|
type: "WebFetch",
|
|
Content: { url: "https://t3.codes", content: "T3 Code page" },
|
|
},
|
|
content: [{ type: "content", content: { type: "text", text: "T3 Code page" } }],
|
|
},
|
|
{
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId: "antigravity-shell",
|
|
title: "run_command",
|
|
kind: "execute",
|
|
status: "completed",
|
|
rawInput: { command: "cat probe.txt" },
|
|
rawOutput: { commandLine: "cat probe.txt", exitCode: 0, combinedOutput: "after\n" },
|
|
},
|
|
{
|
|
sessionUpdate: "compaction_update",
|
|
compactionId: "compact-1",
|
|
status: "in_progress",
|
|
},
|
|
{
|
|
sessionUpdate: "compaction_summary_chunk",
|
|
compactionId: "compact-1",
|
|
content: { type: "text", text: "Retained decisions." },
|
|
},
|
|
{
|
|
sessionUpdate: "compaction_update",
|
|
compactionId: "compact-1",
|
|
status: "completed",
|
|
summary: [{ type: "text", text: "Retained decisions." }],
|
|
},
|
|
];
|
|
for (const update of updates) {
|
|
yield* agent.client.sessionUpdate({ sessionId: requestedSessionId, update });
|
|
}
|
|
return yield* finishPrompt(requestedSessionId, "end_turn");
|
|
}
|
|
|
|
if (residualCallbackTriggerPath !== undefined) {
|
|
yield* Effect.gen(function* () {
|
|
while (!(yield* Effect.sync(() => NodeFS.existsSync(residualCallbackTriggerPath)))) {
|
|
yield* Effect.sleep("20 millis");
|
|
}
|
|
yield* Effect.sync(() => {
|
|
writeJsonRpcNotification("session/update", {
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "agent_message_chunk",
|
|
messageId: "mock-agent-message",
|
|
content: { type: "text", text: "residual assistant callback" },
|
|
},
|
|
});
|
|
writeJsonRpcNotification("session/update", {
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId: "residual-tool-call",
|
|
title: "Residual tool callback",
|
|
kind: "other",
|
|
status: "pending",
|
|
rawInput: {},
|
|
},
|
|
});
|
|
writeJsonRpcNotification("session/update", {
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "plan_update",
|
|
plan: {
|
|
type: "items",
|
|
planId: "mock-plan",
|
|
entries: [
|
|
{ content: "Residual plan callback", priority: "high", status: "pending" },
|
|
],
|
|
},
|
|
},
|
|
});
|
|
});
|
|
yield* agent.client
|
|
.requestPermission({
|
|
sessionId: requestedSessionId,
|
|
title: "Residual permission callback",
|
|
subject: {
|
|
type: "tool_call",
|
|
toolCall: {
|
|
toolCallId: "residual-permission",
|
|
title: "Residual permission callback",
|
|
},
|
|
},
|
|
options: [{ optionId: "allow", name: "Allow", kind: "allow_once" }],
|
|
})
|
|
.pipe(
|
|
Effect.exit,
|
|
Effect.tap(() => Effect.sync(() => logResidualCallbackResponse("permission"))),
|
|
Effect.ignore,
|
|
Effect.forkDetach,
|
|
);
|
|
yield* agent.client
|
|
.elicit({
|
|
sessionId: requestedSessionId,
|
|
message: "Residual elicitation callback",
|
|
mode: "form",
|
|
requestedSchema: {
|
|
type: "object",
|
|
properties: {
|
|
approved: { type: "boolean", title: "Approved" },
|
|
},
|
|
},
|
|
})
|
|
.pipe(
|
|
Effect.exit,
|
|
Effect.tap(() => Effect.sync(() => logResidualCallbackResponse("elicitation"))),
|
|
Effect.ignore,
|
|
Effect.forkDetach,
|
|
);
|
|
if (exitAfterResidualCallbacks) {
|
|
yield* Effect.sleep("100 millis");
|
|
return yield* Effect.sync(() => process.exit(0));
|
|
}
|
|
}).pipe(Effect.forkDetach);
|
|
}
|
|
if (completeFirstPromptOnCancel && promptCount === 1) {
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId: "native-cancel-tool",
|
|
title: "Long command",
|
|
kind: "execute",
|
|
status: "in_progress",
|
|
},
|
|
});
|
|
yield* Deferred.await(nativeCancelRequested);
|
|
yield* Deferred.await(nativeCancelRelease);
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId: "native-cancel-tool",
|
|
status: "failed",
|
|
content: [{ type: "content", content: { type: "text", text: "Cancelled." } }],
|
|
},
|
|
});
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "agent_message_chunk",
|
|
content: { type: "text", text: "Request cancelled." },
|
|
},
|
|
});
|
|
return yield* finishPrompt(requestedSessionId, "cancelled", { nativeCancel: true });
|
|
}
|
|
|
|
if (Number.isFinite(promptDelayMs) && promptDelayMs > 0) {
|
|
yield* Effect.sleep(`${promptDelayMs} millis`);
|
|
}
|
|
|
|
if (failPrompt) {
|
|
return yield* AcpError.AcpRequestError.internalError("Mock prompt failure");
|
|
}
|
|
|
|
if (emitStaleXAiPromptCompleteBeforeSecondHang && promptCount === 1) {
|
|
return yield* finishPrompt(requestedSessionId, "end_turn", {
|
|
promptId: "mock-stale-xai-prompt-1",
|
|
requestId: "mock-stale-xai-prompt-1",
|
|
});
|
|
}
|
|
|
|
if (emitStaleXAiPromptCompleteBeforeSecondHang && promptCount === 2) {
|
|
const currentPromptId = promptIdFromRequestMeta(request) ?? "mock-current-xai-prompt-2";
|
|
writeJsonRpcNotification("_x.ai/session/prompt_complete", {
|
|
sessionId: requestedSessionId,
|
|
promptId: "mock-stale-xai-prompt-1",
|
|
stopReason: "end_turn",
|
|
agentResult: null,
|
|
});
|
|
|
|
writeJsonRpcNotification("_x.ai/session/prompt_complete", {
|
|
sessionId: requestedSessionId,
|
|
promptId: currentPromptId,
|
|
stopReason: "end_turn",
|
|
agentResult: null,
|
|
});
|
|
|
|
return yield* Effect.never;
|
|
}
|
|
|
|
if (emitOverlappingXAiPromptCompleteOutOfOrder && promptCount === 1) {
|
|
overlappingFirstPromptId = promptIdFromRequestMeta(request);
|
|
return yield* Effect.never;
|
|
}
|
|
|
|
if (emitOverlappingXAiPromptCompleteOutOfOrder && promptCount === 2) {
|
|
const secondPromptId = promptIdFromRequestMeta(request);
|
|
if (overlappingFirstPromptId !== undefined && secondPromptId !== undefined) {
|
|
writeJsonRpcNotification("_x.ai/session/prompt_complete", {
|
|
sessionId: requestedSessionId,
|
|
promptId: secondPromptId,
|
|
stopReason: "end_turn",
|
|
agentResult: null,
|
|
});
|
|
writeJsonRpcNotification("_x.ai/session/prompt_complete", {
|
|
sessionId: requestedSessionId,
|
|
promptId: overlappingFirstPromptId,
|
|
stopReason: "end_turn",
|
|
agentResult: null,
|
|
});
|
|
}
|
|
return yield* Effect.never;
|
|
}
|
|
|
|
if (
|
|
hangPromptForever ||
|
|
(hangFirstPromptForever && promptCount === 1) ||
|
|
(emitEmptySuccessfulBashThenHang && promptCount === 2)
|
|
) {
|
|
return yield* Effect.never;
|
|
}
|
|
|
|
if (
|
|
emitRunningCommandThenHang ||
|
|
(emitRunningCommandThenHangOnFirstPrompt && promptCount === 1)
|
|
) {
|
|
const toolCallId = "tool-call-running-1";
|
|
if (runningCommandPidPath !== undefined) {
|
|
const command = runningCommandIgnoresTerm
|
|
? 'trap "" TERM; bash -c \'trap "" TERM; while :; do sleep 1; done\' & child=$!; printf "%s %s\n" "$$" "$child" > "$1"; wait "$child"'
|
|
: 'sleep 120 & child=$!; printf "%s %s\n" "$$" "$child" > "$1"; wait "$child"';
|
|
if (runningCommandSeparateSession) {
|
|
const launcher = [
|
|
'const { spawn } = require("node:child_process");',
|
|
"const child = spawn(process.argv[1], process.argv.slice(2), { stdio: 'ignore' });",
|
|
"child.once('exit', (code, signal) => process.exitCode = code ?? (signal ? 1 : 0));",
|
|
].join(" ");
|
|
const detachedCommand = runningCommandIgnoresTerm
|
|
? 'trap "" TERM; bash -c \'trap "" TERM; while :; do sleep 1; done\' & child=$!; printf "%s %s %s\n" "$PPID" "$$" "$child" > "$1"; wait "$child"'
|
|
: 'sleep 120 & child=$!; printf "%s %s %s\n" "$PPID" "$$" "$child" > "$1"; wait "$child"';
|
|
// Nested bash publishes "$PPID $$ $child" once it starts. Do not
|
|
// write the launcher PID alone here: that races with bash and can
|
|
// clobber the triple that interrupt tests wait for.
|
|
const detachedLauncher = NodeChildProcess.spawn(
|
|
process.execPath,
|
|
["-e", launcher, "bash", "-c", detachedCommand, "bash", runningCommandPidPath],
|
|
{ detached: true, stdio: "ignore" },
|
|
);
|
|
detachedLauncher.unref();
|
|
} else {
|
|
NodeChildProcess.spawn("bash", ["-c", command, "bash", runningCommandPidPath], {
|
|
stdio: "ignore",
|
|
});
|
|
}
|
|
}
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId,
|
|
title: "Terminal",
|
|
kind: "execute",
|
|
status: "pending",
|
|
rawInput: {
|
|
command: ["sleep", "120"],
|
|
},
|
|
},
|
|
});
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId,
|
|
title: "Terminal",
|
|
kind: "execute",
|
|
status: "in_progress",
|
|
rawInput: {
|
|
command: ["sleep", "120"],
|
|
},
|
|
// Grok-like mid-stream Bash re-report: exit_code 0 while still running.
|
|
rawOutput: { type: "Bash", exit_code: 0 },
|
|
},
|
|
});
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId: "tool-call-output-1",
|
|
title: "get_command_or_subagent_output",
|
|
kind: "other",
|
|
status: "pending",
|
|
rawInput: { task_id: "task-running-1" },
|
|
},
|
|
});
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId: "tool-call-output-1",
|
|
status: "in_progress",
|
|
rawOutput: { task_id: "task-running-1", status: "running" },
|
|
},
|
|
});
|
|
if (exitAfterRunningCommandLaunch) {
|
|
yield* Effect.sleep("100 millis");
|
|
return yield* Effect.sync(() => process.exit(0));
|
|
}
|
|
// Stay open until session/cancel so interrupt tests can observe a running tool.
|
|
while (!cancelledSessions.has(requestedSessionId)) {
|
|
yield* Effect.sleep("25 millis");
|
|
}
|
|
cancelledSessions.delete(requestedSessionId);
|
|
return yield* finishPrompt(requestedSessionId, "cancelled");
|
|
}
|
|
|
|
if (emitXAiRateLimitThenHang) {
|
|
writeJsonRpcNotification("_x.ai/session/prompt_complete", {
|
|
sessionId: requestedSessionId,
|
|
promptId: promptIdFromRequestMeta(request) ?? "mock-xai-rate-limit-prompt-1",
|
|
stopReason: "rate_limit",
|
|
agentResult: null,
|
|
});
|
|
return yield* Effect.never;
|
|
}
|
|
|
|
if (emitContentThenHang) {
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "agent_message_chunk",
|
|
content: { type: "text", text: "partial before stall" },
|
|
},
|
|
});
|
|
return yield* Effect.never;
|
|
}
|
|
|
|
if (emitPlanThenHang) {
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "plan",
|
|
entries: [
|
|
{
|
|
content: "Wait for more ACP progress",
|
|
priority: "high",
|
|
status: "in_progress",
|
|
},
|
|
],
|
|
},
|
|
});
|
|
return yield* Effect.never;
|
|
}
|
|
|
|
if (emitActiveToolThenHang) {
|
|
const toolCallId = "tool-call-long-running-1";
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId,
|
|
title: "Long-running tool",
|
|
kind: "execute",
|
|
status: "pending",
|
|
rawInput: { command: ["long-running-tool"] },
|
|
},
|
|
});
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId,
|
|
status: "in_progress",
|
|
},
|
|
});
|
|
return yield* Effect.never;
|
|
}
|
|
if (emitEmptySuccessfulBash || (emitEmptySuccessfulBashThenHang && promptCount === 1)) {
|
|
const update = {
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId: "tool-call-empty-success-1",
|
|
title: "Terminal",
|
|
kind: "execute",
|
|
status: "completed",
|
|
rawInput: { command: "true" },
|
|
rawOutput: { type: "Bash", exit_code: 0 },
|
|
},
|
|
} as const;
|
|
yield* agent.client.sessionUpdate(update);
|
|
yield* Effect.sleep("25 millis");
|
|
yield* agent.client.sessionUpdate(update);
|
|
return yield* finishPrompt(requestedSessionId, "end_turn");
|
|
}
|
|
|
|
if (emitXAiPromptCompleteThenHang) {
|
|
writeJsonRpcNotification("session/update", {
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "agent_message_chunk",
|
|
messageId: "mock-agent-message",
|
|
content: { type: "text", text: "hello from" },
|
|
},
|
|
});
|
|
|
|
if (emitForeignSessionUpdates) {
|
|
writeJsonRpcNotification("session/update", {
|
|
sessionId: "mock-child-session-1",
|
|
update: {
|
|
sessionUpdate: "agent_message_chunk",
|
|
messageId: "mock-agent-message",
|
|
content: { type: "text", text: "child before completion" },
|
|
},
|
|
});
|
|
}
|
|
|
|
writeJsonRpcNotification("_x.ai/session/prompt_complete", {
|
|
sessionId: requestedSessionId,
|
|
promptId: promptIdFromRequestMeta(request) ?? "mock-xai-prompt-1",
|
|
...(omitXAiPromptCompleteStopReason ? {} : { stopReason: "end_turn" }),
|
|
agentResult: null,
|
|
});
|
|
|
|
if (emitForeignSessionUpdates) {
|
|
writeJsonRpcNotification("session/update", {
|
|
sessionId: "mock-child-session-1",
|
|
update: {
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId: "child-tool-call-1",
|
|
title: "Child-only tool",
|
|
kind: "other",
|
|
status: "pending",
|
|
rawInput: {},
|
|
},
|
|
});
|
|
writeJsonRpcNotification("session/update", {
|
|
sessionId: "mock-child-session-1",
|
|
update: {
|
|
sessionUpdate: "agent_message_chunk",
|
|
messageId: "mock-agent-message",
|
|
content: { type: "text", text: "child after completion" },
|
|
},
|
|
});
|
|
}
|
|
|
|
for (const text of [" ", "mo", "ck"]) {
|
|
writeJsonRpcNotification("session/update", {
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "agent_message_chunk",
|
|
messageId: "mock-agent-message",
|
|
content: { type: "text", text },
|
|
},
|
|
});
|
|
}
|
|
|
|
return yield* Effect.never;
|
|
}
|
|
|
|
if (emitGrokMonitorPostTurnPoll) {
|
|
const monitorCallId = "call-monitor-1";
|
|
const pollCallId = "call-monitor-poll-1";
|
|
const taskId = "01a05f41-5107-7550-821e-79e8d1cd7687";
|
|
const description = "Watch count-sheet Typst unit until done";
|
|
writeJsonRpcNotification("session/update", {
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "tool_call",
|
|
toolCallId: monitorCallId,
|
|
title: "monitor",
|
|
kind: "other",
|
|
status: "pending",
|
|
rawInput: { description },
|
|
_meta: {
|
|
"x.ai/tool": { version: 1, name: "monitor", kind: "task", namespace: "grok_build" },
|
|
},
|
|
},
|
|
});
|
|
writeJsonRpcNotification("session/update", {
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId: monitorCallId,
|
|
status: "completed",
|
|
rawInput: { description },
|
|
rawOutput: {
|
|
type: "Monitor",
|
|
taskId,
|
|
timeoutMs: 36_000_000,
|
|
},
|
|
},
|
|
});
|
|
writeJsonRpcNotification("_x.ai/session/prompt_complete", {
|
|
sessionId: requestedSessionId,
|
|
promptId: promptIdFromRequestMeta(request) ?? "mock-xai-prompt-1",
|
|
stopReason: "end_turn",
|
|
agentResult: null,
|
|
});
|
|
yield* Effect.sleep("120 millis");
|
|
writeJsonRpcNotification("session/update", {
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "tool_call",
|
|
toolCallId: pollCallId,
|
|
title: "get_command_or_subagent_output",
|
|
kind: "other",
|
|
status: "completed",
|
|
rawInput: { variant: "TaskOutput", task_ids: [taskId], timeout_ms: 0 },
|
|
rawOutput: {
|
|
type: "TaskOutput",
|
|
Result: {
|
|
task_id: taskId,
|
|
command: `[monitor] ${description}`,
|
|
status: "completed",
|
|
exit_code: 0,
|
|
output: "Monitor finished.",
|
|
},
|
|
},
|
|
},
|
|
});
|
|
return yield* Effect.never;
|
|
}
|
|
|
|
if (emitGrokBackgroundTaskStarted) {
|
|
const toolCallId = "call-fb9d0000-0000-0000-0000-000000000026";
|
|
const command = "sleep 40; echo done-a";
|
|
writeJsonRpcNotification("session/update", {
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "tool_call",
|
|
toolCallId,
|
|
title: "run_terminal_command",
|
|
kind: "execute",
|
|
status: "in_progress",
|
|
rawInput: { command },
|
|
},
|
|
});
|
|
writeJsonRpcNotification("session/update", {
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId,
|
|
status: "completed",
|
|
rawOutput: {
|
|
type: "BackgroundTaskStarted",
|
|
task_id: toolCallId,
|
|
task_type: "bash",
|
|
status: "running",
|
|
command,
|
|
},
|
|
},
|
|
});
|
|
writeJsonRpcNotification("_x.ai/session/prompt_complete", {
|
|
sessionId: requestedSessionId,
|
|
promptId: promptIdFromRequestMeta(request) ?? "mock-xai-prompt-1",
|
|
stopReason: "end_turn",
|
|
agentResult: null,
|
|
});
|
|
return yield* Effect.never;
|
|
}
|
|
|
|
if (emitBackgroundToolDuringAnswer) {
|
|
// A command backgrounded earlier reports progress and then finishes
|
|
// while the next answer is still streaming.
|
|
const toolCallId = "background-1";
|
|
const say = (text: string) =>
|
|
agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text } },
|
|
});
|
|
const progress = (status: "in_progress" | "completed", stdout: string) =>
|
|
agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId,
|
|
status,
|
|
rawOutput: { stdout },
|
|
},
|
|
});
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId,
|
|
title: "Terminal",
|
|
kind: "execute",
|
|
status: "in_progress",
|
|
rawInput: { command: "sleep 3 && echo done" },
|
|
},
|
|
});
|
|
yield* say("| a | b |\n|---|---|\n| 1 ");
|
|
yield* progress("in_progress", ".");
|
|
yield* say("| x |\n");
|
|
yield* progress("completed", "done");
|
|
yield* say("| 2 | y |\n");
|
|
// Agents can repeat a terminal update after the call finished.
|
|
yield* progress("completed", "done");
|
|
yield* say("| 3 | z |");
|
|
return yield* finishPrompt(requestedSessionId, "end_turn");
|
|
}
|
|
|
|
if (emitInterleavedAssistantToolCalls) {
|
|
const toolCallId = "tool-call-1";
|
|
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "agent_message_chunk",
|
|
messageId: "mock-agent-message",
|
|
content: { type: "text", text: "before tool" },
|
|
},
|
|
});
|
|
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId,
|
|
title: "Terminal",
|
|
kind: "execute",
|
|
status: "pending",
|
|
rawInput: {
|
|
command: ["echo", "hello"],
|
|
},
|
|
},
|
|
});
|
|
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId,
|
|
status: "completed",
|
|
rawOutput: {
|
|
exitCode: 0,
|
|
stdout: "hello",
|
|
stderr: "",
|
|
},
|
|
},
|
|
});
|
|
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "agent_message_chunk",
|
|
messageId: "mock-agent-message",
|
|
content: { type: "text", text: "after tool" },
|
|
},
|
|
});
|
|
|
|
return yield* finishPrompt(requestedSessionId, "end_turn");
|
|
}
|
|
|
|
if (emitElicitation || emitMcpToolApprovalElicitation) {
|
|
yield* agent.client.elicit({
|
|
sessionId: requestedSessionId,
|
|
message: "Approve this request?",
|
|
mode: "form",
|
|
requestedSchema: {
|
|
type: "object",
|
|
properties: {
|
|
approved: { type: "boolean", title: "Approved" },
|
|
},
|
|
},
|
|
...(emitMcpToolApprovalElicitation
|
|
? { _meta: { codex_approval_kind: "mcp_tool_call" } }
|
|
: {}),
|
|
});
|
|
return yield* finishPrompt(requestedSessionId, "end_turn");
|
|
}
|
|
|
|
if (emitUrlElicitation) {
|
|
yield* agent.client.elicit({
|
|
sessionId: requestedSessionId,
|
|
message: "Open authentication page",
|
|
mode: "url",
|
|
url: "https://example.com/auth",
|
|
elicitationId: "url-elicitation-1",
|
|
});
|
|
return yield* finishPrompt(requestedSessionId, "end_turn");
|
|
}
|
|
|
|
if (emitToolCalls) {
|
|
const toolCallId = "tool-call-1";
|
|
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId,
|
|
title: "Terminal",
|
|
kind: "execute",
|
|
status: "pending",
|
|
rawInput: {
|
|
command: ["cat", "server/package.json"],
|
|
},
|
|
},
|
|
});
|
|
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId,
|
|
status: "in_progress",
|
|
},
|
|
});
|
|
|
|
const permissionOptions: Array<AcpSchema.PermissionOption> = [
|
|
{ optionId: permissionOptionIds.allowOnce, name: "Allow once", kind: "allow_once" },
|
|
...(omitAllowAlways
|
|
? []
|
|
: [
|
|
{
|
|
optionId: permissionOptionIds.allowAlways,
|
|
name: "Allow always",
|
|
kind: "allow_always" as const,
|
|
},
|
|
]),
|
|
{ optionId: permissionOptionIds.rejectOnce, name: "Reject", kind: "reject_once" },
|
|
];
|
|
|
|
let cancelled = cancelledSessions.delete(requestedSessionId);
|
|
for (let index = 0; index < permissionRequestCount; index++) {
|
|
const command =
|
|
index > 0
|
|
? (process.env.T3_ACP_SECOND_PERMISSION_COMMAND ?? "cat server/package.json")
|
|
: "cat server/package.json";
|
|
const permission = yield* agent.client.requestPermission({
|
|
sessionId: requestedSessionId,
|
|
title: process.env.T3_ACP_PERMISSION_TITLE ?? `\`${command}\``,
|
|
subject: {
|
|
type: "tool_call",
|
|
toolCall: {
|
|
toolCallId: index === 0 ? toolCallId : `${toolCallId}-${index + 1}`,
|
|
title: process.env.T3_ACP_PERMISSION_TITLE ?? `\`${command}\``,
|
|
kind: "execute",
|
|
status: "pending",
|
|
rawInput: {
|
|
variant: "Bash",
|
|
command,
|
|
description: index === 0 ? "Read package metadata" : "Read it again",
|
|
},
|
|
content: [
|
|
{
|
|
type: "content",
|
|
content: {
|
|
type: "text",
|
|
text: `Not in allowlist: ${command}`,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
options: permissionOptions,
|
|
});
|
|
cancelled =
|
|
cancelled ||
|
|
cancelledSessions.delete(requestedSessionId) ||
|
|
permission.outcome.outcome === "cancelled";
|
|
if (cancelled) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (hangAfterPermission) {
|
|
return yield* Effect.never;
|
|
}
|
|
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId,
|
|
title: "Terminal",
|
|
kind: "execute",
|
|
status: "completed",
|
|
rawOutput: {
|
|
exitCode: 0,
|
|
stdout: '{ "name": "t3" }',
|
|
stderr: "",
|
|
},
|
|
},
|
|
});
|
|
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "agent_message_chunk",
|
|
messageId: "mock-agent-message",
|
|
content: { type: "text", text: "hello from mock" },
|
|
},
|
|
});
|
|
|
|
return yield* finishPrompt(requestedSessionId, cancelled ? "cancelled" : "end_turn");
|
|
}
|
|
|
|
if (emitGenericToolPlaceholders) {
|
|
const toolCallId = "tool-call-generic-1";
|
|
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId,
|
|
title: "Read File",
|
|
kind: "read",
|
|
status: "pending",
|
|
rawInput: {},
|
|
},
|
|
});
|
|
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId,
|
|
status: "in_progress",
|
|
},
|
|
});
|
|
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId,
|
|
status: "completed",
|
|
rawOutput: {
|
|
content: "package.json\n",
|
|
},
|
|
},
|
|
});
|
|
|
|
return yield* finishPrompt(requestedSessionId, "end_turn");
|
|
}
|
|
|
|
// In-turn monitor + TaskOutput hydrate, then a late post-finalize
|
|
// duplicate terminal TaskOutput for the same task. Exercises the
|
|
// already-handled short-circuit: must not pin hasPendingBackgroundWork.
|
|
if (emitInTurnTaskOutputThenLateDuplicate) {
|
|
const monitorToolCallId = "tool-call-monitor-1";
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId: monitorToolCallId,
|
|
title: "Monitor: mock background task",
|
|
kind: "execute",
|
|
status: "pending",
|
|
rawInput: {},
|
|
},
|
|
});
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId: monitorToolCallId,
|
|
status: "in_progress",
|
|
},
|
|
});
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId: "tool-call-fetch-1",
|
|
title: "get_command_or_subagent_output",
|
|
kind: "other",
|
|
status: "pending",
|
|
rawInput: { task_id: "task-monitor-1" },
|
|
},
|
|
});
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId: "tool-call-fetch-1",
|
|
status: "completed",
|
|
rawOutput: { output: "MONITOR_LISTING_TOKEN" },
|
|
},
|
|
});
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "agent_message_chunk",
|
|
messageId: "mock-agent-message",
|
|
content: { type: "text", text: "Monitor listing ready in-turn." },
|
|
},
|
|
});
|
|
// After deferred finalize (~2s) clears activeTurn, re-emit a terminal
|
|
// TaskOutput for the same task so bufferPostSettleWake sees
|
|
// alreadyHandledToolUpdate with a non-empty wake path.
|
|
yield* Effect.gen(function* () {
|
|
yield* Effect.sleep("2500 millis");
|
|
yield* Effect.sync(() => {
|
|
writeJsonRpcNotification("session/update", {
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId: "tool-call-fetch-1",
|
|
title: "get_command_or_subagent_output",
|
|
kind: "other",
|
|
status: "completed",
|
|
rawOutput: { output: "MONITOR_LISTING_TOKEN_LATE" },
|
|
},
|
|
});
|
|
});
|
|
}).pipe(Effect.forkDetach);
|
|
return yield* finishPrompt(requestedSessionId, "end_turn");
|
|
}
|
|
|
|
if (emitPostSettleMonitorFlow) {
|
|
const monitorToolCallId = "tool-call-monitor-1";
|
|
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId: monitorToolCallId,
|
|
title: "Monitor: mock background task",
|
|
kind: "execute",
|
|
status: "pending",
|
|
rawInput: {},
|
|
},
|
|
});
|
|
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId: monitorToolCallId,
|
|
status: "in_progress",
|
|
},
|
|
});
|
|
|
|
// After the prompt settles, replay the CLI-injected monitor-event
|
|
// turn: end notice, TaskOutput hydration, then (once the trigger
|
|
// file exists) the report chunk. Detached fiber on the real clock;
|
|
// it outlives the prompt handler.
|
|
yield* Effect.gen(function* () {
|
|
yield* Effect.sleep("150 millis");
|
|
yield* Effect.sync(() => {
|
|
writeJsonRpcNotification("session/update", {
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "user_message_chunk",
|
|
messageId: "mock-user-message",
|
|
content: {
|
|
type: "text",
|
|
text: 'Monitor "task-monitor-1" ended: [monitor ended: exit 0]',
|
|
},
|
|
},
|
|
});
|
|
writeJsonRpcNotification("session/update", {
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId: "tool-call-fetch-1",
|
|
title: "get_command_or_subagent_output",
|
|
kind: "other",
|
|
status: "pending",
|
|
rawInput: { task_id: "task-monitor-1" },
|
|
},
|
|
});
|
|
writeJsonRpcNotification("session/update", {
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId: "tool-call-fetch-1",
|
|
status: "completed",
|
|
rawOutput: { output: "MONITOR_LISTING_TOKEN" },
|
|
},
|
|
});
|
|
});
|
|
if (injectedReportTriggerPath === undefined) return;
|
|
while (!(yield* Effect.sync(() => NodeFS.existsSync(injectedReportTriggerPath)))) {
|
|
yield* Effect.sleep("20 millis");
|
|
}
|
|
yield* Effect.sync(() => {
|
|
writeJsonRpcNotification("session/update", {
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "agent_message_chunk",
|
|
messageId: "mock-agent-message",
|
|
content: { type: "text", text: "Monitor finished. MONITOR_REPORT_TOKEN" },
|
|
},
|
|
});
|
|
});
|
|
}).pipe(Effect.forkDetach);
|
|
|
|
return yield* finishPrompt(requestedSessionId, "end_turn");
|
|
}
|
|
|
|
if (emitAskQuestion) {
|
|
yield* agent.client.extRequest("cursor/ask_question", {
|
|
toolCallId: "ask-question-tool-call-1",
|
|
title: "Question",
|
|
questions: [
|
|
{
|
|
id: "scope",
|
|
prompt: "Which scope?",
|
|
options: [
|
|
{ id: "workspace", label: "Workspace" },
|
|
{ id: "session", label: "Session" },
|
|
],
|
|
},
|
|
],
|
|
});
|
|
|
|
return yield* finishPrompt(requestedSessionId, "end_turn");
|
|
}
|
|
|
|
if (emitXAiAskUserQuestion || emitXAiAskUserQuestionThenHang) {
|
|
const result = yield* agent.client.extRequest("_x.ai/ask_user_question", {
|
|
method: "x.ai/ask_user_question",
|
|
params: {
|
|
sessionId: requestedSessionId,
|
|
toolCallId: "ask-user-question-tool-call-1",
|
|
questions: [
|
|
{
|
|
question: "Which scope should Grok use?",
|
|
multiSelect: null,
|
|
options: [
|
|
{ label: "Workspace", description: "Use the current workspace" },
|
|
{ label: "Session", description: "Only use this session" },
|
|
],
|
|
},
|
|
],
|
|
mode: "default",
|
|
},
|
|
});
|
|
if (typeof result !== "object" || result === null || !("outcome" in result)) {
|
|
throw new Error("Expected _x.ai/ask_user_question response outcome.");
|
|
}
|
|
if (result.outcome === "cancelled") {
|
|
return yield* finishPrompt(requestedSessionId, "end_turn");
|
|
}
|
|
if (
|
|
result.outcome !== "accepted" ||
|
|
!("answers" in result) ||
|
|
typeof result.answers !== "object" ||
|
|
result.answers === null
|
|
) {
|
|
throw new Error("Expected accepted _x.ai/ask_user_question response answers.");
|
|
}
|
|
|
|
if (emitXAiAskUserQuestionThenHang) {
|
|
return yield* Effect.never;
|
|
}
|
|
|
|
return yield* finishPrompt(requestedSessionId, "end_turn");
|
|
}
|
|
|
|
if (emitXAiPlanMdWrite) {
|
|
// Match Grok's real session layout so isGrokPlanMarkdownPath accepts it.
|
|
const planRoot = process.env.T3_ACP_PLAN_ROOT ?? "/tmp/mock-home/.grok";
|
|
const planPath = `${planRoot}/sessions/${requestedSessionId}/plan.md`;
|
|
const planBody = "# Mock plan\n\n- Write the feature\n- Add a test\n- Ship it\n";
|
|
// enter_plan_mode first so the adapter arms planModeActive.
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId: "enter-plan-mode-1",
|
|
title: "enter_plan_mode",
|
|
kind: "other",
|
|
status: "completed",
|
|
rawInput: { variant: "EnterPlanMode" },
|
|
},
|
|
});
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId: "plan-md-write-1",
|
|
title: "write",
|
|
kind: "edit",
|
|
status: "pending",
|
|
rawInput: { file_path: planPath, content: planBody },
|
|
},
|
|
});
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId: "plan-md-write-1",
|
|
kind: "edit",
|
|
status: "completed",
|
|
title: `Write \`${planPath}\``,
|
|
rawInput: { file_path: planPath, content: planBody },
|
|
content: [
|
|
{
|
|
type: "diff",
|
|
path: planPath,
|
|
oldText: "",
|
|
newText: planBody,
|
|
},
|
|
],
|
|
},
|
|
});
|
|
return yield* finishPrompt(requestedSessionId, "end_turn");
|
|
}
|
|
|
|
if (emitXAiExitPlanMode) {
|
|
const result = yield* agent.client.extRequest("_x.ai/exit_plan_mode", {
|
|
method: "x.ai/exit_plan_mode",
|
|
params: {
|
|
sessionId: requestedSessionId,
|
|
toolCallId: "exit-plan-mode-tool-call-1",
|
|
planContent: "# Exit plan\n\n- Step one\n- Step two\n",
|
|
},
|
|
});
|
|
if (typeof result !== "object" || result === null || !("outcome" in result)) {
|
|
throw new Error("Expected _x.ai/exit_plan_mode response outcome.");
|
|
}
|
|
if (
|
|
result.outcome !== "abandoned" &&
|
|
result.outcome !== "approved" &&
|
|
result.outcome !== "request_changes"
|
|
) {
|
|
throw new Error(
|
|
`Expected exit_plan_mode outcome abandoned|approved|request_changes, got ${String(result.outcome)}`,
|
|
);
|
|
}
|
|
return yield* finishPrompt(requestedSessionId, "end_turn");
|
|
}
|
|
|
|
if (emitForeignSessionUpdates) {
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "agent_message_chunk",
|
|
messageId: "mock-agent-message",
|
|
content: { type: "text", text: "root before child" },
|
|
},
|
|
});
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: "mock-child-session-1",
|
|
update: {
|
|
sessionUpdate: "agent_message_chunk",
|
|
messageId: "mock-agent-message",
|
|
content: { type: "text", text: "child content" },
|
|
},
|
|
});
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: "mock-child-session-1",
|
|
update: {
|
|
sessionUpdate: "tool_call_update",
|
|
toolCallId: "child-tool-call-1",
|
|
title: "Child-only tool",
|
|
kind: "other",
|
|
status: "pending",
|
|
rawInput: {},
|
|
},
|
|
});
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "agent_message_chunk",
|
|
messageId: "mock-agent-message",
|
|
content: { type: "text", text: " root after child" },
|
|
},
|
|
});
|
|
return yield* finishPrompt(requestedSessionId, "end_turn");
|
|
}
|
|
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "plan_update",
|
|
plan: {
|
|
type: "items",
|
|
planId: "mock-plan",
|
|
entries: [
|
|
{
|
|
content: "Inspect mock ACP state",
|
|
priority: "high",
|
|
status: "completed",
|
|
},
|
|
{
|
|
content: "Implement the requested change",
|
|
priority: "high",
|
|
status: "in_progress",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
});
|
|
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "agent_message_chunk",
|
|
messageId: "mock-agent-message",
|
|
content: { type: "text", text: promptResponseText ?? "hello from mock" },
|
|
},
|
|
});
|
|
|
|
return yield* finishPrompt(requestedSessionId, "end_turn");
|
|
}),
|
|
);
|
|
|
|
yield* agent.handleUnknownExtRequest((method, params) => {
|
|
if (method === "_test/environment") {
|
|
return Effect.succeed({
|
|
inherited: process.env.T3_ACP_RUNTIME_AMBIENT === "sentinel",
|
|
explicit: process.env.T3_ACP_RUNTIME_EXPLICIT === "kept",
|
|
});
|
|
}
|
|
if (method === "_test/release-resume") {
|
|
return Deferred.succeed(resumeRelease, undefined).pipe(Effect.as({}));
|
|
}
|
|
if (method === "_test/finish-cancel") {
|
|
return Deferred.succeed(nativeCancelRelease, undefined).pipe(Effect.as({}));
|
|
}
|
|
if (method === "_test/startup-metadata") {
|
|
return Effect.gen(function* () {
|
|
for (const [metadataSessionId, commandName, modeId] of [
|
|
[sessionId, "plan", "code"],
|
|
["child-session", "foreign-command", "ask"],
|
|
] as const) {
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: metadataSessionId,
|
|
update: {
|
|
sessionUpdate: "available_commands_update",
|
|
availableCommands: [{ name: commandName, description: "Native command" }],
|
|
},
|
|
});
|
|
currentModeId = modeId;
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: metadataSessionId,
|
|
update: {
|
|
sessionUpdate: "config_option_update",
|
|
configOptions: configOptions().map((option) =>
|
|
option.type === "select" && option.category === "model"
|
|
? {
|
|
...option,
|
|
currentValue: metadataSessionId === sessionId ? "gpt-5.4" : "default",
|
|
}
|
|
: option,
|
|
),
|
|
},
|
|
});
|
|
yield* agent.client.sessionUpdate({
|
|
sessionId: metadataSessionId,
|
|
update: {
|
|
sessionUpdate: "agent_message_chunk",
|
|
content: { type: "text", text: "Startup transcript must not replay." },
|
|
},
|
|
});
|
|
}
|
|
return {};
|
|
});
|
|
}
|
|
if (method === "cursor/list_available_models") {
|
|
return Effect.succeed({
|
|
models: availableModels(),
|
|
});
|
|
}
|
|
|
|
if (method !== "session/mode/set") {
|
|
return Effect.fail(AcpError.AcpRequestError.methodNotFound(method));
|
|
}
|
|
|
|
const nextModeId =
|
|
typeof params === "object" &&
|
|
params !== null &&
|
|
"modeId" in params &&
|
|
typeof params.modeId === "string"
|
|
? params.modeId
|
|
: typeof params === "object" &&
|
|
params !== null &&
|
|
"mode" in params &&
|
|
typeof params.mode === "string"
|
|
? params.mode
|
|
: undefined;
|
|
const requestedSessionId =
|
|
typeof params === "object" &&
|
|
params !== null &&
|
|
"sessionId" in params &&
|
|
typeof params.sessionId === "string"
|
|
? params.sessionId
|
|
: sessionId;
|
|
|
|
if (typeof nextModeId === "string" && nextModeId.trim()) {
|
|
currentModeId = nextModeId.trim();
|
|
return agent.client
|
|
.sessionUpdate({
|
|
sessionId: requestedSessionId,
|
|
update: {
|
|
sessionUpdate: "current_mode_update",
|
|
currentModeId,
|
|
},
|
|
})
|
|
.pipe(Effect.as({}));
|
|
}
|
|
|
|
return Effect.succeed({});
|
|
});
|
|
|
|
yield* agent.handleUnknownExtNotification((method) =>
|
|
method === "_test/exit" ? Effect.sync(() => process.exit(19)) : Effect.void,
|
|
);
|
|
|
|
return yield* Effect.never;
|
|
}).pipe(
|
|
Effect.provide(
|
|
EffectAcpAgent.layerStdio(
|
|
requestLogPath
|
|
? {
|
|
logIncoming: true,
|
|
logger: (event) => {
|
|
if (event.direction !== "incoming" || event.stage !== "raw") {
|
|
return Effect.void;
|
|
}
|
|
if (typeof event.payload !== "string") {
|
|
return Effect.void;
|
|
}
|
|
const payload = event.payload;
|
|
return Effect.sync(() => {
|
|
NodeFS.appendFileSync(
|
|
requestLogPath,
|
|
payload.endsWith("\n") ? payload : `${payload}\n`,
|
|
"utf8",
|
|
);
|
|
});
|
|
},
|
|
}
|
|
: {},
|
|
),
|
|
),
|
|
Effect.scoped,
|
|
Effect.provide(NodeServices.layer),
|
|
);
|
|
|
|
NodeRuntime.runMain(program);
|