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
22 changes: 22 additions & 0 deletions apps/server/scripts/acp-mock-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ 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 advertiseEmptySelectOption = process.env.T3_ACP_EMPTY_SELECT_OPTION === "1";
const promptResponseText = process.env.T3_ACP_PROMPT_RESPONSE_TEXT;
const initialGrokReasoningEffort =
process.env.T3_ACP_INITIAL_GROK_REASONING_EFFORT?.trim() || undefined;
Expand Down Expand Up @@ -129,6 +130,7 @@ let parameterizedModelPicker = false;
let currentReasoning = "medium";
let currentContext = "272k";
let currentFast = false;
let currentProfile = "";
let authenticated = !requiresAuthentication;
let promptCount = 0;
let overlappingFirstPromptId: string | undefined;
Expand Down Expand Up @@ -314,6 +316,23 @@ function configOptions(): ReadonlyArray<AcpSchema.SessionConfigOption> {
}

return [
...(advertiseEmptySelectOption
? [
{
configId: "profile",
name: "Profile",
type: "select" as const,
currentValue: currentProfile,
options: [
{ value: "", name: "Provider default" },
{ value: "custom", name: "Custom" },
{ value: "__acp_default__", name: "Literal value" },
{ value: "__acp_default____acp_default__", name: "Repeated prefix" },
{ value: "__acp_default__".padEnd(256, "_"), name: "Longest legacy value" },
],
},
]
: []),
{
configId: "mode",
name: "Mode",
Expand Down Expand Up @@ -790,6 +809,9 @@ const program = Effect.gen(function* () {
if (request.configId === "model" && typeof request.value === "string") {
currentModelId = request.value;
}
if (request.configId === "profile" && typeof request.value === "string") {
currentProfile = request.value;
}
if (request.configId === "reasoning" && typeof request.value === "string") {
currentReasoning = request.value;
}
Expand Down
109 changes: 109 additions & 0 deletions apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ import type * as EffectAcpSchema from "effect-acp/compat";
import * as ServerConfig from "../../config.ts";
import * as McpProviderSession from "@t3tools/provider-core/server/mcpSession";
import * as AcpSessionRuntime from "../../provider/acp/AcpSessionRuntime.ts";
import {
acpProviderOptionDescriptors,
encodeAcpOptionValue,
} from "../../provider/acp/AcpSessionConfig.ts";
import {
extractXAiAcpSubagentEndNotice,
extractXAiAcpSubagentUpdate,
Expand Down Expand Up @@ -3424,6 +3428,111 @@ describe("AcpAdapterV2", () => {
}).pipe(Effect.provide(layerTest), Effect.scoped),
);

it.live("decodes ACP sentinels without changing persisted legacy values", () =>
Effect.gen(function* () {
const path = yield* Path.Path;
const protocolEvents = yield* Queue.unbounded<EffectAcpProtocol.AcpProtocolLogEvent>();
const instanceId = ProviderInstanceId.make("acp-empty-option-test");
let currentLabel: string | undefined;
const adapter = makeAcpAdapterV2({
crypto: yield* Crypto.Crypto,
instanceId,
flavor: {
driver: ACP_TEST_DRIVER,
capabilities: AcpProviderCapabilitiesV2,
makeRuntime: makeMockRuntime({
childProcessSpawner: yield* ChildProcessSpawner.ChildProcessSpawner,
mockAgentPath: yield* path.fromFileUrl(
new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),
),
environment: { T3_ACP_EMPTY_SELECT_OPTION: "1" },
protocolEvents,
}),
onSessionConfigurationUpdate: (configOptions, modeState) =>
Effect.sync(() => {
const descriptor = acpProviderOptionDescriptors({
configOptions,
modeState,
}).find((option) => option.id === "profile");
if (descriptor?.type === "select") {
currentLabel = descriptor.options.find(
(choice) => choice.id === descriptor.currentValue,
)?.label;
}
}),
},
fileSystem: yield* FileSystem.FileSystem,
idAllocator: yield* IdAllocator.IdAllocatorV2,
serverConfig: yield* ServerConfig.ServerConfig,
selfInvocation: yield* resolveSelfInvocation(),
});
const threadId = ThreadId.make("thread-acp-empty-option");
const runtimePolicy = ProviderAdapterV2RuntimePolicy.make({
runtimeMode: "full-access",
interactionMode: "default",
cwd: process.cwd(),
});
const modelSelection = {
instanceId,
model: "default",
options: [{ id: "profile", value: "__acp_default__" }],
} satisfies ModelSelection;
const runtime = yield* adapter.openSession({
threadId,
providerSessionId: ProviderSessionId.make("provider-session-acp-empty-option"),
modelSelection,
runtimePolicy,
});
const providerThread = yield* runtime.ensureThread({
threadId,
modelSelection,
runtimePolicy,
});
assert.equal(currentLabel, "Literal value");

const requestedValues = [
{ value: "", label: "Provider default" },
{ value: "custom", label: "Custom" },
{ value: "__acp_default__", label: "Literal value" },
{ value: "__acp_default____acp_default__", label: "Repeated prefix" },
{ value: "__acp_default__".padEnd(256, "_"), label: "Longest legacy value" },
{ value: "", label: "Provider default" },
];
for (const [index, { value, label }] of requestedValues.entries()) {
yield* runtime.startTurn(
makeTurnInput({
threadId,
providerThread,
instanceId,
runtimePolicy,
now: yield* DateTime.now,
ordinal: index + 1,
modelSelection: {
instanceId,
model: "default",
options: [{ id: "profile", value: encodeAcpOptionValue(value) }],
},
}),
);
yield* runtime.events.pipe(
Stream.filter((event) => event.type === "turn.terminal"),
Stream.runHead,
);
assert.equal(currentLabel, label);
}
const requests = (yield* Queue.takeAll(protocolEvents)).filter(
(event) =>
event.direction === "outgoing" &&
rawProtocolMethod(event) === "session/set_config_option" &&
rawProtocolRequestParam(event, "configId") === "profile",
);
assert.deepEqual(
requests.map((event) => rawProtocolRequestParam(event, "value")),
["__acp_default__", ...requestedValues.map(({ value }) => value)],
);
}).pipe(Effect.provide(layerTest), Effect.scoped),
);

it.effect("reconfigures a loaded ACP session from its own active setup metadata", () =>
Effect.gen(function* () {
const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
Expand Down
31 changes: 20 additions & 11 deletions apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,10 @@ import {
resolveEmbeddedTerminalContent,
type AcpClientTerminals,
} from "../../provider/acp/AcpClientTerminals.ts";
import { ACP_SESSION_MODE_OPTION_ID } from "../../provider/acp/AcpSessionConfig.ts";
import {
ACP_SESSION_MODE_OPTION_ID,
encodeAcpOptionValue,
} from "../../provider/acp/AcpSessionConfig.ts";
import * as AcpSessionRuntime from "../../provider/acp/AcpSessionRuntime.ts";
import {
t3AcpPromptWithInstructions,
Expand Down Expand Up @@ -6358,6 +6361,7 @@ export function makeAcpAdapterV2(
// agent-side set rejection to a warning; the agent's default
// applies for that option.
const option = configOptions.find((candidate) => candidate.id === selection.id);
let value = selection.value;
if (
option !== undefined &&
option.type === "select" &&
Expand All @@ -6366,14 +6370,18 @@ export function makeAcpAdapterV2(
const advertisedValues = option.options.flatMap((entry) =>
"value" in entry ? [entry.value] : entry.options.map((choice) => choice.value),
);
if (!advertisedValues.includes(selection.value)) continue;
const advertisedValue = advertisedValues.find(
(candidate) => encodeAcpOptionValue(candidate) === selection.value,
);
if (advertisedValue === undefined) continue;
value = advertisedValue;
}
yield* runtime.setConfigOption(selection.id, selection.value).pipe(
yield* runtime.setConfigOption(selection.id, value).pipe(
Effect.catchTags({
AcpRequestError: (error) =>
Effect.logWarning("ACP session rejected a configuration option value", {
optionId: selection.id,
value: selection.value,
value,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
detail: error.message,
}),
}),
Expand All @@ -6387,13 +6395,14 @@ export function makeAcpAdapterV2(
// The synthetic mode selection is skipped rather than failed when the
// agent no longer advertises it: mode sets are volatile across agent
// versions and a stale persisted mode should not block the turn.
if (
modeSelection !== undefined &&
typeof modeSelection.value === "string" &&
modeState?.availableModes.some((mode) => mode.id === modeSelection.value) === true &&
modeState.currentModeId !== modeSelection.value
) {
yield* runtime.setMode(modeSelection.value);
const selectedMode =
typeof modeSelection?.value === "string"
? modeState?.availableModes.find(
(mode) => encodeAcpOptionValue(mode.id) === modeSelection.value,
)
: undefined;
if (selectedMode !== undefined && modeState?.currentModeId !== selectedMode.id) {
yield* runtime.setMode(selectedMode.id);
}
const effectiveModeState = yield* runtime.getModeState;
const effectiveConfigOptions = yield* runtime.getConfigOptions;
Expand Down
99 changes: 93 additions & 6 deletions apps/server/src/provider/acp/AcpSessionConfig.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,49 @@
import { describe, expect, it } from "@effect/vitest";
import { ModelCapabilities } from "@t3tools/contracts";
import * as Schema from "effect/Schema";

import { ACP_SESSION_MODE_OPTION_ID, acpProviderOptionDescriptors } from "./AcpSessionConfig.ts";

const decodeCapabilities = Schema.decodeUnknownSync(ModelCapabilities);
const emptyValue = "__acp_default__".padEnd(257, "_");

describe("acpProviderOptionDescriptors", () => {
it("represents empty agent choices with a nonempty sentinel through the contract", () => {
const descriptors = acpProviderOptionDescriptors({
configOptions: [
{
id: "agent",
name: "Selection",
type: "select",
currentValue: "",
options: [
{ value: "", name: "Provider default" },
{ value: "custom", name: "Custom" },
],
},
],
modeState: undefined,
});
expect(
decodeCapabilities({
optionDescriptors: descriptors,
}),
).toEqual({
optionDescriptors: [
{
id: "agent",
label: "Selection",
type: "select",
currentValue: emptyValue,
options: [
{ id: emptyValue, label: "Provider default" },
{ id: "custom", label: "Custom" },
],
},
],
});
});

it("maps non-model select options and excludes model and collaboration categories", () => {
const descriptors = acpProviderOptionDescriptors({
configOptions: [
Expand Down Expand Up @@ -79,7 +120,7 @@ describe("acpProviderOptionDescriptors", () => {
]);
});

it("flattens grouped select choices and drops empty or duplicate values", () => {
it("flattens grouped select choices and drops duplicate and blank values", () => {
const descriptors = acpProviderOptionDescriptors({
configOptions: [
{
Expand Down Expand Up @@ -151,6 +192,43 @@ describe("acpProviderOptionDescriptors", () => {
]);
});

it.each([
{ currentModeId: "", currentValue: emptyValue },
{ currentModeId: "__acp_default__", currentValue: "__acp_default__" },
{
currentModeId: "__acp_default__".padEnd(256, "_"),
currentValue: "__acp_default__".padEnd(256, "_"),
},
])("keeps empty and legacy session modes distinct for $currentModeId", (selection) => {
const descriptors = acpProviderOptionDescriptors({
configOptions: [],
modeState: {
currentModeId: selection.currentModeId,
availableModes: [
{ id: "", name: "Provider default" },
{ id: "__acp_default__", name: "Literal value" },
{ id: "__acp_default__".padEnd(256, "_"), name: "Longest legacy value" },
{ id: emptyValue, name: "Too long" },
],
},
});

expect(decodeCapabilities({ optionDescriptors: descriptors }).optionDescriptors).toEqual([
{
id: ACP_SESSION_MODE_OPTION_ID,
label: "Mode",
description: "Session mode advertised by the ACP agent.",
type: "select",
currentValue: selection.currentValue,
options: [
{ id: emptyValue, label: "Provider default" },
{ id: "__acp_default__", label: "Literal value" },
{ id: "__acp_default__".padEnd(256, "_"), label: "Longest legacy value" },
],
},
]);
});

it("suppresses the synthetic mode descriptor when modes duplicate a config option", () => {
const descriptors = acpProviderOptionDescriptors({
configOptions: [
Expand Down Expand Up @@ -207,8 +285,9 @@ describe("acpProviderOptionDescriptors", () => {
]);
});

it("preserves canonical opaque option values and omits values the wire would mutate", () => {
const exactValue = "value:with:opaque-markers";
it("preserves legacy sentinel-like values and rejects invalid choices", () => {
const exactValue = "__acp_default__";
const longestLegacyValue = exactValue.padEnd(256, "_");
const descriptors = acpProviderOptionDescriptors({
configOptions: [
{
Expand All @@ -218,8 +297,11 @@ describe("acpProviderOptionDescriptors", () => {
currentValue: exactValue,
options: [
{ value: exactValue, name: "Exact" },
{ value: " value with spaces ", name: "Would be trimmed" },
{ value: "x".repeat(257), name: "Too long" },
{ value: `${exactValue}${exactValue}`, name: "Repeated prefix" },
{ value: longestLegacyValue, name: "Longest legacy value" },
{ value: " value with spaces ", name: "Padded" },
{ value: "", name: " " },
{ value: emptyValue, name: "Too long" },
],
},
],
Expand All @@ -229,7 +311,12 @@ describe("acpProviderOptionDescriptors", () => {
expect(descriptors[0]).toMatchObject({
id: "opaque",
currentValue: exactValue,
options: [{ id: exactValue, label: "Exact" }],
options: [
{ id: exactValue, label: "Exact" },
{ id: `${exactValue}${exactValue}`, label: "Repeated prefix" },
{ id: longestLegacyValue, label: "Longest legacy value" },
{ id: emptyValue, label: "Default" },
],
});
});

Expand Down
Loading
Loading