| author | |
| committer | |
| log | 19a67ef5211996a737d9f9fe608ab2c80f8a6714 |
| tree | 9b03145054a05a8b594f7f43f6df1a36dd949e24 |
| parent | a0ea721c47f0b1ac923fbff1043d2994500ecf51 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>3 files changed, 50 insertions(+), 0 deletions(-)
apps/server/src/provider/acp/AcpJsonRpcConnection.test.ts+37| ... | ... | @@ -1752,4 +1752,41 @@ describe("AcpSessionRuntime", () => { |
| 1752 | 1752 | Effect.ensuring(Effect.sync(() => NodeFS.rmSync(tempDir, { recursive: true, force: true }))), |
| 1753 | 1753 | ); |
| 1754 | 1754 | }); |
| 1755 | ||
| 1756 | it.effect("sends configured custom models the agent does not advertise", () => { | |
| 1757 | const tempDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "acp-runtime-")); | |
| 1758 | const requestLogPath = NodePath.join(tempDir, "requests.ndjson"); | |
| 1759 | return Effect.gen(function* () { | |
| 1760 | const runtime = yield* AcpSessionRuntime.AcpSessionRuntime; | |
| 1761 | yield* runtime.start(); | |
| 1762 | ||
| 1763 | yield* runtime.setModel("cline-free/custom-model"); | |
| 1764 | const error = yield* runtime.setModel("unconfigured/model").pipe(Effect.flip); | |
| 1765 | expect(error._tag).toBe("AcpRequestError"); | |
| 1766 | ||
| 1767 | const sentModels = NodeFS.readFileSync(requestLogPath, "utf8") | |
| 1768 | .trim() | |
| 1769 | .split("\n") | |
| 1770 | .filter((line) => line.length > 0) | |
| 1771 | .map((line) => JSON.parse(line) as { method?: string; params?: { value?: unknown } }) | |
| 1772 | .filter((message) => message.method === "session/set_config_option") | |
| 1773 | .map((message) => message.params?.value); | |
| 1774 | expect(sentModels).toEqual(["cline-free/custom-model"]); | |
| 1775 | }).pipe( | |
| 1776 | Effect.provide( | |
| 1777 | AcpSessionRuntime.layer({ | |
| 1778 | ...mockRuntimeOptions, | |
| 1779 | spawn: { | |
| 1780 | command: mockAgentCommand, | |
| 1781 | args: mockAgentArgs, | |
| 1782 | env: { T3_ACP_REQUEST_LOG_PATH: requestLogPath }, | |
| 1783 | }, | |
| 1784 | customModels: ["cline-free/custom-model"], | |
| 1785 | }), | |
| 1786 | ), | |
| 1787 | Effect.scoped, | |
| 1788 | Effect.provide(NodeServices.layer), | |
| 1789 | Effect.ensuring(Effect.sync(() => NodeFS.rmSync(tempDir, { recursive: true, force: true }))), | |
| 1790 | ); | |
| 1791 | }); | |
| 1755 | 1792 | }); |
packages/provider-acp-registry/src/server/adapter.ts+4| ... | ... | @@ -9,6 +9,7 @@ import { |
| 9 | 9 | makeCopilotSubagentRouting, |
| 10 | 10 | } from "./copilotAcp.ts"; |
| 11 | 11 | import { defaultInstanceIdForDriver, ProviderDriverKind } from "@t3tools/contracts"; |
| 12 | import { readCustomModelEntries } from "@t3tools/shared/model"; | |
| 12 | 13 | import { AcpRegistrySettings } from "../settings.ts"; |
| 13 | 14 | import * as HostProcess from "@t3tools/shared/HostProcess"; |
| 14 | 15 | import { resolveSelfInvocation, type SelfInvocation } from "@t3tools/shared/nodeRuntime"; |
| ... | ... | @@ -168,6 +169,9 @@ function makeAcpRegistryRuntime( |
| 168 | 169 | env: { ...resolved.spawn.env, ...processEnvironment }, |
| 169 | 170 | }, |
| 170 | 171 | ...(options.settings.authMethodId ? { authMethodId: options.settings.authMethodId } : {}), |
| 172 | customModels: readCustomModelEntries(options.settings.customModels).map( | |
| 173 | (entry) => entry.slug, | |
| 174 | ), | |
| 171 | 175 | }), |
| 172 | 176 | ); |
| 173 | 177 | return yield* Effect.service(AcpSessionRuntime.AcpSessionRuntime).pipe( |
packages/provider-acp/src/server/AcpSessionRuntime.ts+9| ... | ... | @@ -155,6 +155,12 @@ export interface AcpSessionRuntimeOptions { |
| 155 | 155 | readonly onTermination?: (error: EffectAcpErrors.AcpError) => Effect.Effect<void>; |
| 156 | 156 | readonly onOutgoingResponseFailure?: EffectAcpClient.AcpClientOptions["onOutgoingResponseFailure"]; |
| 157 | 157 | readonly onOutgoingResponse?: EffectAcpClient.AcpClientOptions["onOutgoingResponse"]; |
| 158 | /** | |
| 159 | * Model ids the user configured for this instance. The model config option | |
| 160 | * accepts them even when the agent does not advertise them, and the agent | |
| 161 | * validates them instead. | |
| 162 | */ | |
| 163 | readonly customModels?: ReadonlyArray<string>; | |
| 158 | 164 | } |
| 159 | 165 | |
| 160 | 166 | export interface AcpSessionRequestLogEvent { |
| ... | ... | @@ -1977,6 +1983,9 @@ export const make = ( |
| 1977 | 1983 | if (allowedValues.includes(value)) { |
| 1978 | 1984 | return; |
| 1979 | 1985 | } |
| 1986 | if (configOption.category === "model" && options.customModels?.includes(value)) { | |
| 1987 | return; | |
| 1988 | } | |
| 1980 | 1989 | return yield* new EffectAcpErrors.AcpRequestError({ |
| 1981 | 1990 | code: -32602, |
| 1982 | 1991 | errorMessage: `Invalid value ${formatConfigOptionValue(value)} for session config option "${configOption.id}": expected one of ${allowedValues.join(", ")}`, |