Skip to content
Source
Sign inSign up

backgroundWait.ts

154 lines7.7 KB

1// @effect-diagnostics nodeBuiltinImport:off
2/**
3 * T3's real `t3_background_wait` handler and ThreadManagementService over a Claude session.
4 * Only the adapter is the eval's: like Claude's, it matches task IDs against the session's
5 * running background work, which the case lists in `world.backgroundTasks`.
6 */
7import {
8 EnvironmentId,
9 type OrchestrationV2ProviderThread,
10 type OrchestrationV2ThreadProjection,
11 ProviderDriverKind,
12 ProviderInstanceId,
13 ProviderSessionId,
14 ProviderThreadId,
15 ThreadId,
16} from "@t3tools/contracts";
17import type * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter";
18import * as Context from "effect/Context";
19import * as DateTime from "effect/DateTime";
20import * as Effect from "effect/Effect";
21import * as Layer from "effect/Layer";
22import * as References from "effect/References";
23// oxlint-disable-next-line t3code/no-raw-mcp-registration -- the eval calls the tool McpHttpServer registered and registers none.
24import { McpSchema, McpServer } from "effect/ai";
25​
26import * as McpHttpServer from "../../apps/server/src/mcp/McpHttpServer.ts";
27import * as McpInvocationContext from "../../apps/server/src/mcp/McpInvocationContext.ts";
28import { liveThreadShell } from "../../apps/server/src/mcp/McpToolAccess.testkit.ts";
29import * as OrchestratorMcpService from "../../apps/server/src/mcp/OrchestratorMcpService.ts";
30import * as ThreadMetadataMcpService from "../../apps/server/src/mcp/ThreadMetadataMcpService.ts";
31import * as OrchestratorHandlers from "../../apps/server/src/mcp/toolkits/orchestrator/handlers.ts";
32import { OrchestratorToolkit } from "../../apps/server/src/mcp/toolkits/orchestrator/tools.ts";
33import * as CommandReceiptStore from "../../apps/server/src/orchestration-v2/CommandReceiptStore.ts";
34import * as Orchestrator from "../../apps/server/src/orchestration-v2/Orchestrator.ts";
35import * as ProviderSessionManager from "../../apps/server/src/orchestration-v2/ProviderSessionManager.ts";
36import * as ThreadManagementService from "../../apps/server/src/orchestration-v2/ThreadManagementService.ts";
37import * as PeerForwarding from "../../apps/server/src/peer/PeerForwarding.ts";
38import * as PeerLinkRequests from "../../apps/server/src/peer/PeerLinkRequests.ts";
39import * as PeerLinks from "../../apps/server/src/peer/PeerLinks.ts";
40import * as RemoteDelegation from "../../apps/server/src/peer/RemoteDelegation.ts";
41import * as SqlitePersistence from "../../apps/server/src/persistence/Sqlite.ts";
42import * as ServerSettings from "../../apps/server/src/serverSettings.ts";
43​
44const threadId = ThreadId.make("mcp-tool-use-eval");
45const providerThreadId = ProviderThreadId.make("provider-thread:mcp-tool-use-eval");
46const providerSessionId = ProviderSessionId.make("provider-session:mcp-tool-use-eval");
47const providerInstanceId = ProviderInstanceId.make("claudeAgent");
48const driver = ProviderDriverKind.make("claudeAgent");
49​
50/** Calls T3's `t3_background_wait` with `running` as the session's running background task IDs. */
51export const makeBackgroundWait = Effect.fn("evaluateMcpToolUse.makeBackgroundWait")(function* (
52 running: ReadonlyArray<string>,
53) {
54 const now = yield* DateTime.now;
55 const providerThread = {
56 id: providerThreadId,
57 driver,
58 providerInstanceId,
59 providerSessionId,
60 appThreadId: threadId,
61 nativeThreadRef: { driver, nativeId: "native-thread", strength: "strong" },
62 pendingBackgroundTasks: running.map((taskId) => ({ taskId, kind: "command" })),
63 createdAt: now,
64 updatedAt: now,
65 } as unknown as OrchestrationV2ProviderThread;
66 const runtime = {
67 driver,
68 providerSessionId,
69 awaitBackgroundTasks: (input: { readonly taskIds: ReadonlyArray<string> }) =>
70 Effect.succeed({ matched: [...new Set(input.taskIds)].filter((id) => running.includes(id)) }),
71 } as unknown as ProviderAdapter.ProviderAdapterV2SessionRuntime;
72 const real = yield* Layer.build(
73 ThreadManagementService.layer.pipe(
74 Layer.provide(
75 Layer.mergeAll(
76 Layer.mock(Orchestrator.OrchestratorV2)({
77 getThreadRecords: () =>
78 Effect.succeed({
79 thread: { id: threadId, deletedAt: null, activeProviderThreadId: providerThreadId },
80 providerThreads: [providerThread],
81 } as unknown as OrchestrationV2ThreadProjection),
82 }),
83 Layer.mock(ProviderSessionManager.ProviderSessionManagerV2)({
84 get: () => Effect.succeedSome(runtime),
85 }),
86 ServerSettings.layerTest(),
87 CommandReceiptStore.layer.pipe(Layer.provide(SqlitePersistence.layerMemory)),
88 ),
89 ),
90 ),
91 // The receipt store's in-memory migrations log every step.
92 ).pipe(Effect.provideService(References.MinimumLogLevel, "Error"));
93 const service = Context.get(real, ThreadManagementService.ThreadManagementService);
94 // The caller's thread is live on the Claude instance, mid-turn.
95 const threads = Layer.mock(ThreadManagementService.ThreadManagementService)({
96 ...service,
97 getThreadShell: () => Effect.succeed({ ...liveThreadShell(threadId), providerInstanceId }),
98 });
99 const context = yield* Layer.build(
100 McpHttpServer.toolkitRegistration(OrchestratorToolkit, OrchestratorHandlers.layer).pipe(
101 Layer.provideMerge(McpHttpServer.layerInProcessMcpServer),
102 Layer.provide(Layer.mock(OrchestratorMcpService.OrchestratorMcpService)({})),
103 Layer.provide(Layer.mock(ThreadMetadataMcpService.ThreadMetadataMcpService)({})),
104 Layer.provide(threads),
105 Layer.provide(Layer.mock(PeerForwarding.PeerForwarding)({})),
106 Layer.provide(Layer.mock(PeerLinkRequests.PeerLinkRequests)({})),
107 Layer.provide(Layer.mock(PeerLinks.PeerLinks)({})),
108 Layer.provide(Layer.mock(RemoteDelegation.RemoteDelegation)({})),
109 ),
110 );
111 type Entry = Parameters<McpServer.McpServer["Service"]["addTool"]>[0];
112 // The registry keeps each tool's handler; its public type lists only definitions.
113 const tools = Context.get(context, McpServer.McpServer).tools as ReadonlyArray<Entry>;
114 const entry = tools.find((tool) => tool.tool.name === "t3_background_wait");
115 if (entry === undefined) return yield* Effect.die(new Error("T3 has no t3_background_wait."));
116 const scope: McpInvocationContext.McpThreadInvocationScope = {
117 environmentId: EnvironmentId.make("00000000-0000-4000-8000-0000000000e0"),
118 requestNamespace: "mcp-tool-use-eval",
119 thread: { threadId, providerSessionId: "mcp-tool-use-eval", providerInstanceId },
120 client: undefined,
121 capabilities: new Set<McpInvocationContext.McpCapability>(["orchestration"]),
122 issuedAt: 0,
123 };
124 const request = McpSchema.McpRequestContext.of({
125 clientId: 0,
126 protocolVersion: "2025-06-18",
127 clientCapabilities: {},
128 });
129 return Effect.fn("evaluateMcpToolUse.backgroundWait")(function* (input: unknown) {
130 const result = yield* entry.handle(input).pipe(
131 Effect.provideService(McpInvocationContext.McpInvocationContext, scope),
132 Effect.provideService(McpSchema.McpRequestContext, request),
133 // An MCP client shows a protocol error, such as bad parameters, as the tool's error.
134 Effect.catch((error) =>
135 Effect.succeed(
136 new McpSchema.CallToolResult({
137 isError: true,
138 content: [{ type: "text", text: error.message }],
139 }),
140 ),
141 ),
142 );
143 if (!("content" in result))
144 return yield* Effect.die(new Error("Background wait asked for input."));
145 const text = result.content
146 .flatMap((part) => (part.type === "text" ? [part.text] : []))
147 .join("\n");
148 if (text === "Tool execution failed due to an internal server error.") {
149 return yield* Effect.die(new Error("t3_background_wait hit a defect in T3 or the eval."));
150 }
151 return { text, isError: result.isError === true };
152 });
153});
154export type BackgroundWait = Effect.Success<ReturnType<typeof makeBackgroundWait>>;