Skip to content
Open
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
10 changes: 9 additions & 1 deletion apps/server/scripts/acp-mock-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ 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 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 permissionOptionIds = {
allowOnce: process.env.T3_ACP_ALLOW_ONCE_OPTION_ID ?? "allow-once",
Expand Down Expand Up @@ -279,7 +281,13 @@ function modeState(): AcpSchema.SessionModeState {
}

const grokAcpModels: ReadonlyArray<AcpSchema.ModelInfo> = [
{ modelId: "grok-build", name: "Grok Build" },
{
modelId: "grok-build",
name: "Grok Build",
...(initialGrokReasoningEffort
? { _meta: { reasoningEffort: initialGrokReasoningEffort } }
: {}),
},
{ modelId: "grok-mock-alt", name: "Grok Mock Alt" },
];

Expand Down
134 changes: 131 additions & 3 deletions apps/server/src/provider/Layers/GrokAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,109 @@ it.layer(grokAdapterTestLayer)("GrokAdapterLive", (it) => {
}),
);

it.effect("does not carry reasoning effort across a start-session model switch", () =>
Effect.gen(function* () {
const threadId = ThreadId.make("grok-start-model-effort-reset");
const tempDir = yield* Effect.promise(() =>
NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "grok-start-model-effort-reset-")),
);
const requestLogPath = NodePath.join(tempDir, "requests.ndjson");
const wrapperPath = yield* Effect.promise(() =>
makeMockGrokWrapper({
T3_ACP_REQUEST_LOG_PATH: requestLogPath,
T3_ACP_INITIAL_GROK_REASONING_EFFORT: "high",
}),
);
const adapter = yield* makeTestAdapter(wrapperPath);

yield* adapter.startSession({
threadId,
provider: ProviderDriverKind.make("grok"),
cwd: process.cwd(),
runtimeMode: "full-access",
modelSelection: {
instanceId: ProviderInstanceId.make("grok"),
model: "grok-mock-alt",
},
});
yield* adapter.sendTurn({
threadId,
input: "select the new model effort",
attachments: [],
modelSelection: {
instanceId: ProviderInstanceId.make("grok"),
model: "grok-mock-alt",
options: [{ id: "reasoningEffort", value: "high" }],
},
});

const requests = yield* Effect.promise(() => readJsonLines(requestLogPath));
const setModelRequests = requests.filter((entry) => entry.method === "session/set_model");
assert.lengthOf(setModelRequests, 2);
const [startSelection, turnSelection] = setModelRequests;
assert.nestedPropertyVal(startSelection, "params.modelId", "grok-mock-alt");
assert.notNestedProperty(startSelection, "params._meta.reasoningEffort");
assert.nestedPropertyVal(turnSelection, "params.modelId", "grok-mock-alt");
assert.nestedPropertyVal(turnSelection, "params._meta.reasoningEffort", "high");

yield* adapter.stopSession(threadId);
}),
);

it.effect("clears and reapplies reasoning effort on the same model", () =>
Effect.gen(function* () {
const threadId = ThreadId.make("grok-same-model-effort-clear");
const tempDir = yield* Effect.promise(() =>
NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "grok-same-model-effort-clear-")),
);
const requestLogPath = NodePath.join(tempDir, "requests.ndjson");
const wrapperPath = yield* Effect.promise(() =>
makeMockGrokWrapper({ T3_ACP_REQUEST_LOG_PATH: requestLogPath }),
);
const adapter = yield* makeTestAdapter(wrapperPath);
const selection = (effort?: string) => ({
instanceId: ProviderInstanceId.make("grok"),
model: "grok-build",
...(effort ? { options: [{ id: "reasoningEffort", value: effort }] } : {}),
});

yield* adapter.startSession({
threadId,
provider: ProviderDriverKind.make("grok"),
cwd: process.cwd(),
runtimeMode: "full-access",
modelSelection: selection(),
});
yield* adapter.sendTurn({
threadId,
input: "use high effort",
attachments: [],
modelSelection: selection("high"),
});
yield* adapter.sendTurn({
threadId,
input: "use the model default",
attachments: [],
modelSelection: selection(),
});
yield* adapter.sendTurn({
threadId,
input: "use high effort again",
attachments: [],
modelSelection: selection("high"),
});

const requests = yield* Effect.promise(() => readJsonLines(requestLogPath));
const setModelRequests = requests.filter((entry) => entry.method === "session/set_model");
assert.lengthOf(setModelRequests, 3);
assert.nestedPropertyVal(setModelRequests[0], "params._meta.reasoningEffort", "high");
assert.notNestedProperty(setModelRequests[1], "params._meta");
assert.nestedPropertyVal(setModelRequests[2], "params._meta.reasoningEffort", "high");

yield* adapter.stopSession(threadId);
}),
);

it.effect("closes the ACP child process when a session stops", () =>
Effect.gen(function* () {
const threadId = ThreadId.make("grok-stop-session-close");
Expand Down Expand Up @@ -273,7 +376,13 @@ it.layer(grokAdapterTestLayer)("GrokAdapterLive", (it) => {
it.effect("restores ready without completing an unstarted turn when preparation fails", () =>
Effect.gen(function* () {
const threadId = ThreadId.make("grok-preparation-failure-while-connecting");
const wrapperPath = yield* Effect.promise(() => makeMockGrokWrapper());
const tempDir = yield* Effect.promise(() =>
NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "grok-preparation-failure-")),
);
const requestLogPath = NodePath.join(tempDir, "requests.ndjson");
const wrapperPath = yield* Effect.promise(() =>
makeMockGrokWrapper({ T3_ACP_REQUEST_LOG_PATH: requestLogPath }),
);
const adapter = yield* makeTestAdapter(wrapperPath);

const runtimeEvents: ProviderRuntimeEvent[] = [];
Expand All @@ -295,6 +404,11 @@ it.layer(grokAdapterTestLayer)("GrokAdapterLive", (it) => {
adapter.sendTurn({
threadId,
input: "prepare invalid attachment",
modelSelection: {
instanceId: ProviderInstanceId.make("grok"),
model: "grok-build",
options: [{ id: "reasoningEffort", value: "high" }],
},
attachments: [
{
type: "image",
Expand All @@ -321,6 +435,8 @@ it.layer(grokAdapterTestLayer)("GrokAdapterLive", (it) => {
assert.isUndefined(turnCompletedEvent);
assert.equal(readySession?.status, "ready");
assert.isUndefined(readySession?.activeTurnId);
const requests = yield* Effect.promise(() => readJsonLines(requestLogPath));
assert.isFalse(requests.some((entry) => entry.method === "session/set_model"));

yield* Fiber.interrupt(runtimeEventsFiber);
yield* adapter.stopSession(threadId);
Expand Down Expand Up @@ -1018,8 +1134,13 @@ it.layer(grokAdapterTestLayer)("GrokAdapterLive", (it) => {
it.effect("rejects sendTurn with empty input and no attachments", () =>
Effect.gen(function* () {
const threadId = ThreadId.make("grok-empty-turn");

const wrapperPath = yield* Effect.promise(() => makeMockGrokWrapper());
const tempDir = yield* Effect.promise(() =>
NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "grok-empty-turn-")),
);
const requestLogPath = NodePath.join(tempDir, "requests.ndjson");
const wrapperPath = yield* Effect.promise(() =>
makeMockGrokWrapper({ T3_ACP_REQUEST_LOG_PATH: requestLogPath }),
);
const adapter = yield* makeTestAdapter(wrapperPath);

yield* adapter.startSession({
Expand All @@ -1035,10 +1156,17 @@ it.layer(grokAdapterTestLayer)("GrokAdapterLive", (it) => {
threadId,
input: " ",
attachments: [],
modelSelection: {
instanceId: ProviderInstanceId.make("grok"),
model: "grok-build",
options: [{ id: "reasoningEffort", value: "high" }],
},
}),
);

assert.equal(error._tag, "ProviderAdapterValidationError");
const requests = yield* Effect.promise(() => readJsonLines(requestLogPath));
assert.isFalse(requests.some((entry) => entry.method === "session/set_model"));

yield* adapter.stopSession(threadId);
}),
Expand Down
48 changes: 39 additions & 9 deletions apps/server/src/provider/Layers/GrokAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
type ThreadId,
TurnId,
} from "@t3tools/contracts";
import { getModelSelectionStringOptionValue } from "@t3tools/shared/model";
import * as Crypto from "effect/Crypto";
import * as DateTime from "effect/DateTime";
import * as Deferred from "effect/Deferred";
Expand Down Expand Up @@ -56,7 +57,9 @@ import { makeAcpNativeLoggerFactory } from "../acp/AcpNativeLogging.ts";
import {
applyGrokAcpModelSelection,
currentGrokModelIdFromSessionSetup,
currentGrokReasoningEffortFromSessionSetup,
makeGrokAcpRuntime,
normalizeGrokReasoningEffort,
resolveGrokAcpBaseModelId,
} from "../acp/GrokAcpSupport.ts";
import {
Expand Down Expand Up @@ -117,6 +120,7 @@ interface GrokSessionContext {
* continues it, and only the last remaining prompt settles the turn. */
promptsInFlight: number;
currentModelId: string | undefined;
currentReasoningEffort: string | undefined;
stopped: boolean;
}

Expand Down Expand Up @@ -738,10 +742,22 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte
const requestedStartModelId = grokModelSelection?.model
? resolveGrokAcpBaseModelId(grokModelSelection.model)
: undefined;
const currentStartModelId = currentGrokModelIdFromSessionSetup(
started.sessionSetupResult,
);
const currentStartReasoningEffort = currentGrokReasoningEffortFromSessionSetup(
started.sessionSetupResult,
);
const requestedStartReasoningEffort = getModelSelectionStringOptionValue(
grokModelSelection,
"reasoningEffort",
);
const boundModelId = yield* applyGrokAcpModelSelection({
runtime: acp,
currentModelId: currentGrokModelIdFromSessionSetup(started.sessionSetupResult),
currentModelId: currentStartModelId,
currentReasoningEffort: currentStartReasoningEffort,
requestedModelId: requestedStartModelId,
requestedReasoningEffort: requestedStartReasoningEffort,
mapError: (cause) =>
mapAcpToAdapterError(PROVIDER, input.threadId, "session/set_model", cause),
});
Expand Down Expand Up @@ -778,6 +794,10 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte
interruptedTurnIds: new Set(),
promptsInFlight: 0,
currentModelId: boundModelId,
currentReasoningEffort:
requestedStartModelId !== undefined
? normalizeGrokReasoningEffort(requestedStartReasoningEffort)
: currentStartReasoningEffort,
stopped: false,
};

Expand Down Expand Up @@ -942,14 +962,10 @@ 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,
mapError: (cause) =>
mapAcpToAdapterError(PROVIDER, input.threadId, "session/set_model", cause),
});

const requestedTurnReasoningEffort = getModelSelectionStringOptionValue(
turnModelSelection,
"reasoningEffort",
);
const text = input.input?.trim();
const imagePromptParts = yield* Effect.forEach(
input.attachments ?? [],
Expand Down Expand Up @@ -997,7 +1013,21 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte
});
}

const currentModelId = yield* applyGrokAcpModelSelection({
runtime: ctx.acp,
currentModelId: ctx.currentModelId,
currentReasoningEffort: ctx.currentReasoningEffort,
requestedModelId: requestedTurnModelId,
requestedReasoningEffort: requestedTurnReasoningEffort,
mapError: (cause) =>
mapAcpToAdapterError(PROVIDER, input.threadId, "session/set_model", cause),
});
ctx.currentModelId = currentModelId;
if (requestedTurnModelId !== undefined) {
ctx.currentReasoningEffort = normalizeGrokReasoningEffort(
requestedTurnReasoningEffort,
);
}
const displayModel = currentModelId
? resolveGrokAcpBaseModelId(currentModelId)
: undefined;
Expand Down
Loading
Loading