Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions apps/server/scripts/acp-mock-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ const emitInterleavedAssistantToolCalls =
const emitGenericToolPlaceholders = process.env.T3_ACP_EMIT_GENERIC_TOOL_PLACEHOLDERS === "1";
const emitAskQuestion = process.env.T3_ACP_EMIT_ASK_QUESTION === "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 xAiExitPlanFile = process.env.T3_ACP_XAI_EXIT_PLAN_FILE;
const xAiExitPlanRepeat = process.env.T3_ACP_XAI_EXIT_PLAN_REPEAT === "1";
const emitXAiPromptCompleteThenHang = process.env.T3_ACP_EMIT_XAI_PROMPT_COMPLETE_THEN_HANG === "1";
const emitForeignSessionUpdates = process.env.T3_ACP_EMIT_FOREIGN_SESSION_UPDATES === "1";
const hangPromptForever = process.env.T3_ACP_HANG_PROMPT_FOREVER === "1";
Expand Down Expand Up @@ -773,6 +776,44 @@ const program = Effect.gen(function* () {
return { stopReason: "end_turn" };
}

if (emitXAiExitPlanMode) {
if (xAiExitPlanFile) {
// Mimic Grok reporting the plan-file path on plan-mode entry, then
// racing its own plan write: exit_plan_mode carries no planContent.
yield* agent.client.sessionUpdate({
sessionId: requestedSessionId,
update: {
sessionUpdate: "tool_call_update",
toolCallId: "enter-plan-mode-tool-call-1",
status: "completed",
rawOutput: {
type: "EnterPlanMode",
Entered: {
message: "You have entered plan mode.",
plan_file_path: xAiExitPlanFile,
},
},
},
});
}
// Optionally re-present within the same prompt, mimicking the model
// revising the plan right after receiving the revise feedback.
const presentations = xAiExitPlanRepeat ? 2 : 1;
for (let index = 0; index < presentations; index += 1) {
const result = yield* agent.client.extRequest("_x.ai/exit_plan_mode", {
sessionId: requestedSessionId,
toolCallId: `exit-plan-mode-tool-call-${index + 1}`,
...(xAiExitPlanFile
? { planContent: null }
: { planContent: `# Plan v${index + 1}\n\n- Add the endpoint\n- Add the test` }),
});
if (typeof result !== "object" || result === null || !("outcome" in result)) {
throw new Error("Expected _x.ai/exit_plan_mode response outcome.");
}
}
return { stopReason: "end_turn" };
}

if (emitXAiAskUserQuestion) {
const result = yield* agent.client.extRequest("_x.ai/ask_user_question", {
method: "x.ai/ask_user_question",
Expand Down
252 changes: 251 additions & 1 deletion apps/server/src/provider/Layers/GrokAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ import {
} from "@t3tools/contracts";

import { ServerConfig } from "../../config.ts";
import { grokPromptSettlementBelongsToContext, makeGrokAdapter } from "./GrokAdapter.ts";
import {
grokPromptSettlementBelongsToContext,
makeGrokAdapter,
shouldAutoApproveGrokPlan,
} from "./GrokAdapter.ts";
const decodeGrokSettings = Schema.decodeSync(GrokSettings);

const __dirname = NodePath.dirname(NodeURL.fileURLToPath(import.meta.url));
Expand Down Expand Up @@ -122,6 +126,49 @@ it("requires a settlement to match the live Grok turn", () => {
);
});

it("approves a captured Grok plan only after a fresh non-plan user prompt", () => {
// No capture yet: never approve.
assert.isFalse(
shouldAutoApproveGrokPlan({
planCapture: undefined,
activeInteractionMode: "default",
promptSerial: 3,
}),
);
// Same prompt re-presentation (no new user input): capture again.
assert.isFalse(
shouldAutoApproveGrokPlan({
planCapture: { promptSerial: 3 },
activeInteractionMode: "default",
promptSerial: 3,
}),
);
// Plan-mode refinement prompt: capture the revised plan, do not approve.
assert.isFalse(
shouldAutoApproveGrokPlan({
planCapture: { promptSerial: 3 },
activeInteractionMode: "plan",
promptSerial: 4,
}),
);
// Fresh default-mode prompt after capture — a new turn or an implement
// click steered into the still-running capturing turn: approve.
assert.isTrue(
shouldAutoApproveGrokPlan({
planCapture: { promptSerial: 3 },
activeInteractionMode: "default",
promptSerial: 4,
}),
);
assert.isTrue(
shouldAutoApproveGrokPlan({
planCapture: { promptSerial: 3 },
activeInteractionMode: undefined,
promptSerial: 4,
}),
);
});

it.layer(grokAdapterTestLayer)("GrokAdapterLive", (it) => {
it.effect("starts a session and maps mock ACP prompt flow to runtime events", () =>
Effect.gen(function* () {
Expand Down Expand Up @@ -1159,6 +1206,209 @@ it.layer(grokAdapterTestLayer)("GrokAdapterLive", (it) => {
}),
);

it.effect("captures Grok plan approvals and approves them on implementation turns", () =>
Effect.gen(function* () {
const threadId = ThreadId.make("grok-xai-exit-plan-mode");
const tempDir = yield* Effect.promise(() =>
NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "grok-acp-plan-")),
);
const requestLogPath = NodePath.join(tempDir, "requests.ndjson");
const wrapperPath = yield* Effect.promise(() =>
makeMockGrokWrapper({
T3_ACP_EMIT_XAI_EXIT_PLAN_MODE: "1",
T3_ACP_REQUEST_LOG_PATH: requestLogPath,
}),
);
const adapter = yield* makeTestAdapter(wrapperPath);
const proposedPlans: Array<
Extract<ProviderRuntimeEvent, { type: "turn.proposed.completed" }>
> = [];
const eventsFiber = yield* Stream.runForEach(adapter.streamEvents, (event) => {
if (String(event.threadId) !== String(threadId)) {
return Effect.void;
}
if (event.type === "turn.proposed.completed") {
proposedPlans.push(event);
}
return Effect.void;
}).pipe(Effect.forkChild);

yield* adapter.startSession({
threadId,
provider: ProviderDriverKind.make("grok"),
cwd: process.cwd(),
runtimeMode: "full-access",
});

// First presentation (agent-initiated planning in a default-mode
// thread): the plan is captured and Grok is told to stop and wait.
yield* adapter.sendTurn({
threadId,
input: "make a plan",
attachments: [],
interactionMode: "default",
});
assert.equal(proposedPlans.length, 1);
assert.equal(
proposedPlans[0]?.payload.planMarkdown,
"# Plan v1\n\n- Add the endpoint\n- Add the test",
);
assert.equal(proposedPlans[0]?.raw?.method, "_x.ai/exit_plan_mode");

// Refinement turn (thread in plan mode): the revised plan is captured
// again instead of being approved.
yield* adapter.sendTurn({
threadId,
input: "add a rollout step",
attachments: [],
interactionMode: "plan",
});
assert.equal(proposedPlans.length, 2);

// Implementation turn: the pending plan approval is granted so Grok can
// exit plan mode and build.
yield* adapter.sendTurn({
threadId,
input: "implement the plan",
attachments: [],
interactionMode: "default",
});
assert.equal(proposedPlans.length, 2);

const requests = yield* Effect.promise(() => readJsonLines(requestLogPath));
const planResponses = requests.flatMap((entry) =>
!("method" in entry) &&
typeof entry.result === "object" &&
entry.result !== null &&
"outcome" in entry.result &&
typeof entry.result.outcome === "string"
? [entry.result.outcome]
: [],
);
assert.deepEqual(planResponses, ["rejected", "rejected", "approved"]);

yield* Fiber.interrupt(eventsFiber);
yield* adapter.stopSession(threadId);
}).pipe(TestClock.withLive),
);

it.effect("captures a plan re-presented within the capturing turn instead of approving it", () =>
Effect.gen(function* () {
const threadId = ThreadId.make("grok-xai-exit-plan-same-turn");
const tempDir = yield* Effect.promise(() =>
NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "grok-acp-plan-repeat-")),
);
const requestLogPath = NodePath.join(tempDir, "requests.ndjson");
const wrapperPath = yield* Effect.promise(() =>
makeMockGrokWrapper({
T3_ACP_EMIT_XAI_EXIT_PLAN_MODE: "1",
T3_ACP_XAI_EXIT_PLAN_REPEAT: "1",
T3_ACP_REQUEST_LOG_PATH: requestLogPath,
}),
);
const adapter = yield* makeTestAdapter(wrapperPath);
const proposedPlans: Array<
Extract<ProviderRuntimeEvent, { type: "turn.proposed.completed" }>
> = [];
const eventsFiber = yield* Stream.runForEach(adapter.streamEvents, (event) => {
if (
String(event.threadId) === String(threadId) &&
event.type === "turn.proposed.completed"
) {
proposedPlans.push(event);
}
return Effect.void;
}).pipe(Effect.forkChild);

yield* adapter.startSession({
threadId,
provider: ProviderDriverKind.make("grok"),
cwd: process.cwd(),
runtimeMode: "full-access",
});
// Both presentations happen inside this single default-mode turn, so
// neither may be auto-approved on the user's behalf.
yield* adapter.sendTurn({
threadId,
input: "make a plan",
attachments: [],
interactionMode: "default",
});

assert.equal(proposedPlans.length, 2);
assert.equal(
proposedPlans[1]?.payload.planMarkdown,
"# Plan v2\n\n- Add the endpoint\n- Add the test",
);

const requests = yield* Effect.promise(() => readJsonLines(requestLogPath));
const planResponses = requests.flatMap((entry) =>
!("method" in entry) &&
typeof entry.result === "object" &&
entry.result !== null &&
"outcome" in entry.result &&
typeof entry.result.outcome === "string"
? [entry.result.outcome]
: [],
);
assert.deepEqual(planResponses, ["rejected", "rejected"]);

yield* Fiber.interrupt(eventsFiber);
yield* adapter.stopSession(threadId);
}).pipe(TestClock.withLive),
);

it.effect("recovers the Grok plan from the plan file when planContent is empty", () =>
Effect.gen(function* () {
const threadId = ThreadId.make("grok-xai-exit-plan-empty-content");
const tempDir = yield* Effect.promise(() =>
NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "grok-acp-plan-file-")),
);
const planFilePath = NodePath.join(tempDir, "plan.md");
yield* Effect.promise(() =>
NodeFSP.writeFile(planFilePath, "# Recovered plan\n\n- Read from disk\n", "utf8"),
);
const wrapperPath = yield* Effect.promise(() =>
makeMockGrokWrapper({
T3_ACP_EMIT_XAI_EXIT_PLAN_MODE: "1",
T3_ACP_XAI_EXIT_PLAN_FILE: planFilePath,
}),
);
const adapter = yield* makeTestAdapter(wrapperPath);
const proposedPlans: Array<
Extract<ProviderRuntimeEvent, { type: "turn.proposed.completed" }>
> = [];
const eventsFiber = yield* Stream.runForEach(adapter.streamEvents, (event) => {
if (
String(event.threadId) === String(threadId) &&
event.type === "turn.proposed.completed"
) {
proposedPlans.push(event);
}
return Effect.void;
}).pipe(Effect.forkChild);

yield* adapter.startSession({
threadId,
provider: ProviderDriverKind.make("grok"),
cwd: process.cwd(),
runtimeMode: "full-access",
});
yield* adapter.sendTurn({
threadId,
input: "make a plan",
attachments: [],
interactionMode: "default",
});

assert.equal(proposedPlans.length, 1);
assert.equal(proposedPlans[0]?.payload.planMarkdown, "# Recovered plan\n\n- Read from disk");

yield* Fiber.interrupt(eventsFiber);
yield* adapter.stopSession(threadId);
}).pipe(TestClock.withLive),
);

it.effect("continues streaming events when native notification logging fails", () =>
Effect.gen(function* () {
const threadId = ThreadId.make("grok-native-log-failure");
Expand Down
Loading
Loading