1import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
2import * as NodeServices from "@effect/platform-node/NodeServices";
3import { expect, it } from "@effect/vitest";
4import { ProviderInstanceId } from "@t3tools/contracts";
5import { McpProviderSessions } from "@t3tools/provider-core/server/McpProviderSessions";
6import * as Effect from "effect/Effect";
7import * as Layer from "effect/Layer";
8import { McpServer } from "effect/ai";
9
10import { AssetSigner } from "../assets/AssetSigner.ts";
11import { ServerSecretStore } from "../auth/ServerSecretStore.ts";
12import { ServerConfig } from "../config.ts";
13import { DeviceService } from "../device/DeviceService.ts";
14import { ServerEnvironment } from "../environment/ServerEnvironment.ts";
15import { GitWorkflowService } from "../git/GitWorkflowService.ts";
16import * as McpHttpServer from "../mcp/McpHttpServer.ts";
17import { PreviewAutomationBroker } from "../mcp/PreviewAutomationBroker.ts";
18import {
19 CLAUDE_PROVIDER,
20 makeClaudeQueryOptions,
21} from "../orchestration-v2/Adapters/ClaudeAdapterV2.ts";
22import { OrchestratorV2 } from "../orchestration-v2/Orchestrator.ts";
23import { ProviderAdapterRegistryV2 } from "../orchestration-v2/ProviderAdapterRegistry.ts";
24import { ThreadLaunchService } from "../orchestration-v2/ThreadLaunchService.ts";
25import { ThreadManagementService } from "../orchestration-v2/ThreadManagementService.ts";
26import { ThreadSearch } from "../orchestration-v2/ThreadSearch.ts";
27import { PeerForwarding } from "../peer/PeerForwarding.ts";
28import { PeerLinkRequests } from "../peer/PeerLinkRequests.ts";
29import { PeerLinks } from "../peer/PeerLinks.ts";
30import { RemoteDelegation } from "../peer/RemoteDelegation.ts";
31import { PreviewManager } from "../preview/Manager.ts";
32import { PreviewBrowser } from "../preview/PreviewBrowser.ts";
33import { ManagedProjectFolders } from "../project/ManagedProjectFolders.ts";
34import { ProjectCloneTracker } from "../project/ProjectCloneTracker.ts";
35import { ProjectService } from "../project/ProjectService.ts";
36import { ProjectSetupScriptRunner } from "../project/ProjectSetupScriptRunner.ts";
37import { PullRequestReviewGuideService } from "../pullRequest/PullRequestReviewGuideService.ts";
38import { PullRequestService } from "../pullRequest/PullRequestService.ts";
39import { ScheduledTaskService } from "../scheduledTasks/ScheduledTaskService.ts";
40import { SecretRequests } from "../secrets/SecretRequests.ts";
41import { ServerSettingsService } from "../serverSettings.ts";
42import { SourceControlRepositoryService } from "../sourceControl/SourceControlRepositoryService.ts";
43import { GitVcsDriver } from "../vcs/GitVcsDriver.ts";
44import { VcsStatusBroadcaster } from "../vcs/VcsStatusBroadcaster.ts";
45import { buildCodexAdditionalContext } from "./CodexDeveloperInstructions.ts";
46import { DocumentText } from "./DocumentText.ts";
47import { ProviderRegistry } from "./ProviderRegistry.ts";
48import budget from "./agentContextBudget.json" with { type: "json" };
49
50// Registration only stores tool definitions, so the handlers' services are never called.
51const toolHandlerServices = Layer.mergeAll(
52 Layer.mock(AssetSigner)({}),
53 Layer.mock(DeviceService)({}),
54 Layer.mock(DocumentText)({}),
55 Layer.mock(GitVcsDriver)({}),
56 Layer.mock(GitWorkflowService)({}),
57 Layer.mock(ManagedProjectFolders)({ namedProjectsRoot: "" }),
58 Layer.mock(McpProviderSessions)({}),
59 Layer.mock(OrchestratorV2)({}),
60 Layer.mock(PeerForwarding)({}),
61 Layer.mock(PeerLinkRequests)({}),
62 Layer.mock(PeerLinks)({}),
63 Layer.mock(PreviewAutomationBroker)({}),
64 Layer.mock(PreviewBrowser)({}),
65 Layer.mock(PreviewManager)({}),
66 Layer.mock(ProjectCloneTracker)({}),
67 Layer.mock(ProjectService)({}),
68 Layer.mock(ProjectSetupScriptRunner)({}),
69 Layer.mock(ProviderAdapterRegistryV2)({}),
70 Layer.mock(ProviderRegistry)({}),
71 Layer.mock(PullRequestReviewGuideService)({}),
72 Layer.mock(PullRequestService)({}),
73 Layer.mock(RemoteDelegation)({}),
74 Layer.mock(ScheduledTaskService)({}),
75 Layer.mock(SecretRequests)({}),
76 ServerConfig.layerTest(process.cwd(), { prefix: "t3-agent-context-budget-" }),
77 Layer.mock(ServerEnvironment)({}),
78 Layer.mock(ServerSecretStore)({}),
79 Layer.mock(ServerSettingsService)({}),
80 Layer.mock(SourceControlRepositoryService)({}),
81 Layer.mock(ThreadLaunchService)({}),
82 Layer.mock(ThreadManagementService)({}),
83 Layer.mock(ThreadSearch)({}),
84 Layer.mock(VcsStatusBroadcaster)({}),
85).pipe(Layer.provideMerge(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer)));
86
87const utf8Bytes = (text: string) => Buffer.byteLength(text, "utf8");
88
89const claudeSystemAppend = () => {
90 const { systemPrompt } = makeClaudeQueryOptions({
91 modelSelection: {
92 instanceId: ProviderInstanceId.make(CLAUDE_PROVIDER),
93 model: "claude-opus-5-5",
94 },
95 nativeThreadId: "context-budget",
96 resume: false,
97 cwd: null,
98 mcpServers: { "t3-code": { type: "http", url: "http://127.0.0.1/mcp" } },
99 });
100 return typeof systemPrompt === "object" && "append" in systemPrompt ? systemPrompt.append : "";
101};
102
103const codexAdditionalContext = () =>
104 Object.values(
105 buildCodexAdditionalContext(
106 { model: "gpt-6.1-sol", modelName: "GPT-6.1 Sol", reasoningEffort: "high" },
107 { browser: true, device: true },
108 ),
109 )
110 .map((entry) => entry.value)
111 .join("");
112
113const layerAdvertisedTools = McpHttpServer.layerTools.pipe(
114 Layer.provideMerge(McpHttpServer.layerInProcessMcpServer),
115 Layer.provide(toolHandlerServices),
116);
117
118// Claude Code loads these tools up front instead of behind ToolSearch, outside this budget.
119it.effect("only request_secret asks Claude Code to load it up front", () =>
120 Effect.gen(function* () {
121 const mcp = yield* McpServer.McpServer;
122 const alwaysLoad = mcp.tools
123 .filter(({ tool }) => tool._meta?.["anthropic/alwaysLoad"] !== undefined)
124 .map(({ tool }) => [tool.name, tool._meta]);
125 expect(alwaysLoad).toEqual([["request_secret", { "anthropic/alwaysLoad": true }]]);
126 }).pipe(Effect.provide(layerAdvertisedTools)),
127);
128
129// CI rejects a PR that raises any value above main's agentContextBudget.json, so growth
130// needs the agent-context-increase label. Lower the values when the text shrinks.
131it.effect("agent context matches the committed budget", () =>
132 Effect.gen(function* () {
133 const mcp = yield* McpServer.McpServer;
134 const measured = {
135 claudeSystemAppendBytes: utf8Bytes(claudeSystemAppend()),
136 codexAdditionalContextBytes: utf8Bytes(codexAdditionalContext()),
137 // The model sees these fields. outputSchema and annotations are protocol metadata.
138 mcpToolListJsonBytes: utf8Bytes(
139 JSON.stringify(
140 mcp.tools.map(({ tool }) => ({
141 name: tool.name,
142 description: tool.description,
143 inputSchema: tool.inputSchema,
144 })),
145 ),
146 ),
147 };
148
149 expect(
150 measured,
151 "Set agentContextBudget.json to the measured values. Before you cut text to fit, run the evals that cover it (evals/README.md).",
152 ).toEqual(budget);
153 }).pipe(Effect.provide(layerAdvertisedTools)),
154);