Skip to content
Draft
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
71 changes: 71 additions & 0 deletions apps/server/src/provider/Layers/GrokAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1264,4 +1264,75 @@ it.layer(grokAdapterTestLayer)("GrokAdapterLive", (it) => {
// hang until the suite timeout instead of failing here.
}).pipe(TestClock.withLive),
);

it.effect("does not call session/set_model while a prior Grok prompt is in flight", () =>
Effect.gen(function* () {
const threadId = ThreadId.make("grok-steer-skips-set-model");
const tempDir = yield* Effect.promise(() =>
NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "grok-steer-set-model-")),
);
const requestLogPath = NodePath.join(tempDir, "requests.ndjson");
const wrapperPath = yield* Effect.promise(() =>
makeMockGrokWrapper({
T3_ACP_PROMPT_DELAY_MS: "400",
T3_ACP_REQUEST_LOG_PATH: requestLogPath,
}),
);
const adapter = yield* makeTestAdapter(wrapperPath);
const instanceId = ProviderInstanceId.make("grok");
const startSelection = {
instanceId,
model: "grok-build",
options: [{ id: "reasoningEffort", value: "high" }],
};

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

const firstSendTurnFiber = yield* adapter
.sendTurn({
threadId,
input: "first prompt",
attachments: [],
modelSelection: startSelection,
})
.pipe(Effect.forkChild);
yield* waitForFileContent(requestLogPath, 80, '"method":"session/prompt"');

const runningSessions = yield* adapter.listSessions();
const runningSession = runningSessions.find((session) => session.threadId === threadId);
const steered = yield* adapter.sendTurn({
threadId,
input: "steer with a different effort",
attachments: [],
modelSelection: {
instanceId,
model: "grok-build",
options: [{ id: "reasoningEffort", value: "low" }],
},
});
const firstTurn = yield* Fiber.join(firstSendTurnFiber);

assert.equal(String(steered.turnId), String(firstTurn.turnId));
assert.equal(String(steered.turnId), String(runningSession?.activeTurnId));

const requests = yield* Effect.promise(() => readJsonLines(requestLogPath));
const methods = requests.flatMap((entry) =>
typeof entry.method === "string" ? [entry.method] : [],
);
const firstPromptIndex = methods.indexOf("session/prompt");
assert.isAtLeast(firstPromptIndex, 0);
assert.isFalse(
methods.slice(firstPromptIndex + 1).includes("session/set_model"),
`set_model after in-flight prompt: ${methods.join(", ")}`,
);

yield* adapter.stopSession(threadId);
}).pipe(TestClock.withLive),
);
});
43 changes: 35 additions & 8 deletions apps/server/src/provider/Layers/GrokAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import {
currentGrokModelIdFromSessionSetup,
makeGrokAcpRuntime,
resolveGrokAcpBaseModelId,
resolveGrokReasoningEffortSelection,
} from "../acp/GrokAcpSupport.ts";
import {
extractXAiAskUserQuestions,
Expand Down Expand Up @@ -117,6 +118,8 @@ interface GrokSessionContext {
* continues it, and only the last remaining prompt settles the turn. */
promptsInFlight: number;
currentModelId: string | undefined;
/** Last effort successfully applied via `session/set_model` `_meta`. */
currentReasoningEffort: string | undefined;
stopped: boolean;
}

Expand Down Expand Up @@ -738,6 +741,9 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte
const requestedStartModelId = grokModelSelection?.model
? resolveGrokAcpBaseModelId(grokModelSelection.model)
: undefined;
const requestedStartReasoningEffort = resolveGrokReasoningEffortSelection(
grokModelSelection?.options,
);
const boundModelId = yield* applyGrokAcpModelSelection({
runtime: acp,
currentModelId: currentGrokModelIdFromSessionSetup(started.sessionSetupResult),
Expand Down Expand Up @@ -779,6 +785,7 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte
interruptedTurnIds: new Set(),
promptsInFlight: 0,
currentModelId: boundModelId,
currentReasoningEffort: requestedStartReasoningEffort,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium Layers/GrokAdapter.ts:788

A requested reasoning effort is cached as applied even when the initial selection has no boundModelId, so a later selection with that model and the same effort omits _meta.reasoningEffort and never applies it. Initialize currentReasoningEffort only when the start model selection was actually applied.

-            currentReasoningEffort: requestedStartReasoningEffort,
+            currentReasoningEffort: boundModelId ? requestedStartReasoningEffort : undefined,
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/provider/Layers/GrokAdapter.ts around line 788:

A requested reasoning effort is cached as applied even when the initial selection has no `boundModelId`, so a later selection with that model and the same effort omits `_meta.reasoningEffort` and never applies it. Initialize `currentReasoningEffort` only when the start model selection was actually applied.

stopped: false,
};

Expand Down Expand Up @@ -949,14 +956,24 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte
const requestedTurnModelId = turnModelSelection?.model
? resolveGrokAcpBaseModelId(turnModelSelection.model)
: undefined;
const currentModelId = yield* applyGrokAcpModelSelection({
runtime: ctx.acp,
currentModelId: ctx.currentModelId,
requestedModelId: requestedTurnModelId,
selections: turnModelSelection?.options,
mapError: (cause) =>
mapAcpToAdapterError(PROVIDER, input.threadId, "session/set_model", cause),
});
const requestedTurnReasoningEffort = resolveGrokReasoningEffortSelection(
turnModelSelection?.options,
);
const previousModelId = ctx.currentModelId;
// Steers must not call session/set_model: prep holds the thread
// lock while the prior prompt is still in flight outside it.
const currentModelId =
steeringTurnId === undefined
? yield* applyGrokAcpModelSelection({
runtime: ctx.acp,
currentModelId: ctx.currentModelId,
requestedModelId: requestedTurnModelId,
currentReasoningEffort: ctx.currentReasoningEffort,
selections: turnModelSelection?.options,
mapError: (cause) =>
mapAcpToAdapterError(PROVIDER, input.threadId, "session/set_model", cause),
})
: ctx.currentModelId;

const text = input.input?.trim();
const imagePromptParts = yield* Effect.forEach(
Expand Down Expand Up @@ -1006,6 +1023,16 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte
}

ctx.currentModelId = currentModelId;
if (steeringTurnId === undefined) {
if (requestedTurnReasoningEffort !== undefined) {
ctx.currentReasoningEffort = requestedTurnReasoningEffort;
} else if (
requestedTurnModelId !== undefined &&
requestedTurnModelId !== previousModelId
) {
ctx.currentReasoningEffort = undefined;
}
}
const displayModel = currentModelId
? resolveGrokAcpBaseModelId(currentModelId)
: undefined;
Expand Down
37 changes: 37 additions & 0 deletions apps/server/src/provider/acp/GrokAcpSupport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,43 @@ describe("applyGrokAcpModelSelection", () => {
}),
);

it.effect("skips set_model when the composer restates the already-applied effort", () =>
Effect.gen(function* () {
const { runtime, modelCalls } = makeRecordingRuntime();
const result = yield* applyGrokAcpModelSelection({
runtime,
currentModelId: "model-a",
requestedModelId: "model-a",
currentReasoningEffort: "effort-c",
selections: [{ id: "reasoningEffort", value: "effort-c" }],
mapError: (cause) => cause.message,
});
expect(modelCalls).toEqual([]);
expect(result).toBe("model-a");
}),
);

it.effect("calls set_model when only the reasoning effort changes", () =>
Effect.gen(function* () {
const { runtime, modelCalls } = makeRecordingRuntime();
const result = yield* applyGrokAcpModelSelection({
runtime,
currentModelId: "model-a",
requestedModelId: "model-a",
currentReasoningEffort: "effort-c",
selections: [{ id: "reasoningEffort", value: "effort-d" }],
mapError: (cause) => cause.message,
});
expect(modelCalls).toEqual([
{
modelId: "model-a",
options: { _meta: { reasoningEffort: "effort-d" } },
},
]);
expect(result).toBe("model-a");
}),
);

it.effect("applies model switch and effort together", () =>
Effect.gen(function* () {
const { runtime, modelCalls } = makeRecordingRuntime();
Expand Down
12 changes: 8 additions & 4 deletions apps/server/src/provider/acp/GrokAcpSupport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,21 +109,25 @@ export function resolveGrokReasoningEffortSelection(
/**
* Apply model and/or reasoning effort via Grok ACP `session/set_model`.
* Effort is sent as `_meta.reasoningEffort` (Grok private extension).
* Calls set_model when the model changes or when an effort selection is present
* (effort can change without a model id change).
* Calls set_model only when the model id or the applied effort actually
* changes. A present-but-unchanged `reasoningEffort` selection is a no-op —
* the composer always includes that option once Grok capabilities are
* discovered, and `session/set_model` is unsafe while a prompt is in flight.
*/
export function applyGrokAcpModelSelection<E>(input: {
readonly runtime: Pick<AcpSessionRuntime.AcpSessionRuntime["Service"], "setSessionModel">;
readonly currentModelId: string | undefined;
readonly requestedModelId: string | undefined;
readonly selections?: ReadonlyArray<ProviderOptionSelection> | null;
readonly currentReasoningEffort?: string | undefined;
readonly selections?: ReadonlyArray<ProviderOptionSelection> | null | undefined;
readonly mapError: (cause: EffectAcpErrors.AcpError) => E;
}): Effect.Effect<string | undefined, E> {
const targetModelId = input.requestedModelId ?? input.currentModelId;
const reasoningEffort = resolveGrokReasoningEffortSelection(input.selections);
const shouldSwitchModel =
input.requestedModelId !== undefined && input.requestedModelId !== input.currentModelId;
const shouldApplyEffort = reasoningEffort !== undefined;
const shouldApplyEffort =
reasoningEffort !== undefined && reasoningEffort !== input.currentReasoningEffort;

if (!targetModelId || (!shouldSwitchModel && !shouldApplyEffort)) {
return Effect.succeed(input.currentModelId);
Expand Down
Loading