Skip to content
Merged
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
131 changes: 131 additions & 0 deletions apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, describe, it } from "@effect/vitest";
import {
CheckpointId,
GrokSettings,
EnvironmentId,
MessageId,
type ModelSelection,
Expand Down Expand Up @@ -88,6 +89,10 @@ import {
type AcpAdapterV2SubagentUpdate,
} from "./AcpAdapterV2.ts";

import { makeGrokAdapterV2 } from "./GrokAdapterV2.ts";

const DEFAULT_GROK_SETTINGS = Schema.decodeSync(GrokSettings)({});

const serverConfigLayer = ServerConfig.layerTest(process.cwd(), {
prefix: "t3-acp-v2-adapter-",
}).pipe(Layer.provide(NodeServices.layer));
Expand Down Expand Up @@ -2689,6 +2694,132 @@ describe("AcpAdapterV2", () => {
}).pipe(Effect.provide(testLayer)),
);

for (const model of ["grok-build", "composer-2"]) {
it.effect(`Grok configures the native session for ${model}`, () =>
Effect.gen(function* () {
const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const fileSystem = yield* FileSystem.FileSystem;
const idAllocator = yield* IdAllocatorV2;
const path = yield* Path.Path;
const serverConfig = yield* ServerConfig;
const mockAgentPath = yield* path.fromFileUrl(
new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),
);
const protocolEvents = yield* Queue.unbounded<EffectAcpProtocol.AcpProtocolLogEvent>();
const instanceId = ProviderInstanceId.make("grok-test");
const adapter = makeGrokAdapterV2({
instanceId,
settings: DEFAULT_GROK_SETTINGS,
environment: {},
hostPlatform: yield* HostProcessPlatform,
childProcessSpawner,
crypto: yield* Crypto.Crypto,
fileSystem,
idAllocator,
serverConfig,
makeRuntime: makeMockRuntime({ childProcessSpawner, mockAgentPath, protocolEvents }),
});
yield* adapter.openSession({
threadId: ThreadId.make(`grok-model-${model}`),
providerSessionId: ProviderSessionId.make(`grok-model-${model}`),
modelSelection: { instanceId, model },
runtimePolicy: ProviderAdapterV2RuntimePolicy.make({
runtimeMode: "full-access",
interactionMode: "default",
cwd: process.cwd(),
}),
});
const requests = (yield* Queue.takeAll(protocolEvents)).map(rawProtocolRequest);
assert.equal(
requests.filter(
(request) =>
request?.method === "session/set_config_option" &&
typeof request.params === "object" &&
request.params !== null &&
"configId" in request.params &&
request.params.configId === "model",
).length,
model === "grok-build" ? 0 : 1,
);
}).pipe(Effect.provide(testLayer), Effect.scoped),
);
}

it.live("Grok reapplies an explicit return to the session's setup-time model", () =>
Effect.gen(function* () {
const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const fileSystem = yield* FileSystem.FileSystem;
const idAllocator = yield* IdAllocatorV2;
const path = yield* Path.Path;
const serverConfig = yield* ServerConfig;
const mockAgentPath = yield* path.fromFileUrl(
new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),
);
const protocolEvents = yield* Queue.unbounded<EffectAcpProtocol.AcpProtocolLogEvent>();
const instanceId = ProviderInstanceId.make("grok-test-switch-back");
const adapter = makeGrokAdapterV2({
instanceId,
settings: DEFAULT_GROK_SETTINGS,
environment: {},
hostPlatform: yield* HostProcessPlatform,
childProcessSpawner,
crypto: yield* Crypto.Crypto,
fileSystem,
idAllocator,
serverConfig,
makeRuntime: makeMockRuntime({ childProcessSpawner, mockAgentPath, protocolEvents }),
});
const threadId = ThreadId.make("grok-model-switch-back");
const runtimePolicy = ProviderAdapterV2RuntimePolicy.make({
runtimeMode: "full-access",
interactionMode: "default",
cwd: process.cwd(),
});
const runtime = yield* adapter.openSession({
threadId,
providerSessionId: ProviderSessionId.make("grok-model-switch-back"),
modelSelection: { instanceId, model: "composer-2" },
runtimePolicy,
});
const providerThread = yield* runtime.ensureThread({
threadId,
modelSelection: { instanceId, model: "composer-2" },
runtimePolicy,
});
// The mock session starts on default. Switching away and explicitly
// back must send the model configuration change; stale metadata can make
// the return trip a silent no-op that left the session on the alt model.
for (const model of ["default", "composer-2"]) {
yield* runtime.startTurn(
makeTurnInput({
threadId,
providerThread,
instanceId,
runtimePolicy,
now: yield* DateTime.now,
modelSelection: { instanceId, model },
}),
);
yield* runtime.events.pipe(
Stream.filter((event) => event.type === "turn.terminal"),
Stream.runHead,
);
}
const requests = (yield* Queue.takeAll(protocolEvents)).map(rawProtocolRequest);
assert.equal(
requests.filter(
(request) =>
request?.method === "session/set_config_option" &&
typeof request.params === "object" &&
request.params !== null &&
"configId" in request.params &&
request.params.configId === "model",
).length,
3,
);
}).pipe(Effect.provide(testLayer), Effect.scoped),
);

it.effect("skips requested options that the active ACP session does not expose", () =>
Effect.gen(function* () {
const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
Expand Down
30 changes: 29 additions & 1 deletion apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5768,8 +5768,13 @@ export function makeAcpAdapterV2(options: AcpAdapterV2Options): ProviderAdapterV
runtimePolicy: ProviderAdapterV2RuntimePolicy,
) {
const requestedModel = flavor.resolveModelId?.(modelSelection) ?? modelSelection.model;
let appliedModel: string | undefined;
if (flavor.applyModelSelection !== undefined) {
yield* flavor.applyModelSelection({ runtime, startResult, modelSelection });
appliedModel = yield* flavor.applyModelSelection({
runtime,
startResult,
modelSelection,
});
} else if (
requestedModel.length > 0 &&
requestedModel !== "auto" &&
Expand All @@ -5783,6 +5788,29 @@ export function makeAcpAdapterV2(options: AcpAdapterV2Options): ProviderAdapterV
yield* runtime.setModel(requestedModel);
}
}
// Same-runtime switches compare against this stored setup, so keep
// its model metadata in sync with what the session now runs on;
// otherwise switching A -> B -> A would see the stale setup-time A
// and skip the final switch.
if (appliedModel !== undefined) {
const applied = appliedModel;
yield* Ref.update(activeSessionSetup, (setup) => {
if (setup === null) {
return setup;
}
const models = setup.sessionSetupResult.models;
if (models == null || models.currentModelId === applied) {
return setup;
}
return {
...setup,
sessionSetupResult: {
...setup.sessionSetupResult,
models: { ...models, currentModelId: applied },
},
};
});
}
const optionSelections = modelSelection.options ?? [];
const configOptions = yield* runtime.getConfigOptions;
const availableConfigIds = new Set(configOptions.map((option) => option.id));
Expand Down
20 changes: 20 additions & 0 deletions apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import type * as EffectAcpErrors from "effect-acp/errors";
import { ServerConfig } from "../../config.ts";
import { makeAcpNativeLoggerFactory } from "../../provider/acp/AcpNativeLogging.ts";
import {
applyGrokAcpModelSelection,
currentGrokModelIdFromSessionSetup,
makeGrokAcpRuntime,
resolveGrokAcpBaseModelId,
} from "../../provider/acp/GrokAcpSupport.ts";
Expand Down Expand Up @@ -223,6 +225,24 @@ export function makeGrokAcpAdapterFlavor(options: GrokAdapterV2Options): AcpAdap
supportsImagePrompts: true,
supportsCompaction: true,
resolveModelId: (selection) => resolveGrokAcpBaseModelId(selection.model),
applyModelSelection: ({ runtime, startResult, modelSelection }) =>
Effect.gen(function* () {
const legacy = startResult.initializeResult.protocolVersion === 1;
const options = legacy ? [] : yield* runtime.getConfigOptions;
const configuredModel = options.find((option) => option.category === "model")?.currentValue;
return yield* applyGrokAcpModelSelection({
runtime: legacy
? runtime
: { setSessionModel: (model) => runtime.setModel(model).pipe(Effect.as({})) },
currentModelId: legacy
? currentGrokModelIdFromSessionSetup(startResult.sessionSetupResult)
: typeof configuredModel === "string"
? configuredModel
: undefined,
requestedModelId: resolveGrokAcpBaseModelId(modelSelection.model),
mapError: (cause) => cause,
});
}),
makeRuntime:
options.makeRuntime ??
((input) =>
Expand Down
Loading