| author | |
| committer | |
| log | ec20db4a5a27de45f69f64ca05d17ec1bd96644d |
| tree | 52fbdad1a49c2b416272e4b8be39854c038d9136 |
| parent | 745c225f92401ba0950770fa7c3ec67a7067dbeb |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Bil0000 <62337003+Bil0000@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>19 files changed, 958 insertions(+), 107 deletions(-)
apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts+86-1| ... | ... | @@ -1008,9 +1008,12 @@ describe("AcpAdapterV2", () => { |
| 1008 | 1008 | ); |
| 1009 | 1009 | type Runtime = AcpSessionRuntime.AcpSessionRuntime["Service"]; |
| 1010 | 1010 | let handler: Parameters<Runtime["handleSessionUpdate"]>[0] | undefined; |
| 1011 | let createTerminal: Parameters<Runtime["handleCreateTerminal"]>[0] | undefined; | |
| 1011 | 1012 | const instanceId = ProviderInstanceId.make("devin-replay"); |
| 1012 | 1013 | const adapter = makeAcpAdapterV2({ |
| 1013 | 1014 | instanceId, |
| 1015 | // Production Devin runs commands through client terminals. | |
| 1016 | clientTerminals: { childProcessSpawner, shellCommands: true }, | |
| 1014 | 1017 | crypto: yield* Crypto.Crypto, |
| 1015 | 1018 | fileSystem: yield* FileSystem.FileSystem, |
| 1016 | 1019 | idAllocator, |
| ... | ... | @@ -1031,9 +1034,18 @@ describe("AcpAdapterV2", () => { |
| 1031 | 1034 | Effect.sync(() => { |
| 1032 | 1035 | handler = next; |
| 1033 | 1036 | }).pipe(Effect.andThen(runtime.handleSessionUpdate(next))), |
| 1037 | handleCreateTerminal: (next) => | |
| 1038 | Effect.sync(() => { | |
| 1039 | createTerminal = next; | |
| 1040 | }).pipe(Effect.andThen(runtime.handleCreateTerminal(next))), | |
| 1034 | 1041 | prompt: () => |
| 1035 | 1042 | Effect.gen(function* () { |
| 1036 | 1043 | assert.isDefined(handler); |
| 1044 | assert.isDefined(createTerminal); | |
| 1045 | const fallbackTerminal = yield* createTerminal( | |
| 1046 | { sessionId: "mock-session-1", command: "true acp-mcp-call task_status {}" }, | |
| 1047 | { requestId: "child-mcp-terminal", method: "terminal/create" }, | |
| 1048 | ); | |
| 1037 | 1049 | // Production thread 54aeb6d7 split after "(command". Metadata shapes |
| 1038 | 1050 | // below were captured from live Devin sessions showy-mile/fragrant-chamomile. |
| 1039 | 1051 | const updates = [ |
| ... | ... | @@ -1075,6 +1087,48 @@ describe("AcpAdapterV2", () => { |
| 1075 | 1087 | "cognition.ai/subagent_context": { parentAgentId: "child-a" }, |
| 1076 | 1088 | }, |
| 1077 | 1089 | }, |
| 1090 | { | |
| 1091 | sessionUpdate: "tool_call", | |
| 1092 | toolCallId: "child-weather", | |
| 1093 | title: "Check weather", | |
| 1094 | status: "completed", | |
| 1095 | rawInput: { server: "weather", tool: "get_weather", city: "Berlin" }, | |
| 1096 | rawOutput: { | |
| 1097 | result: { | |
| 1098 | _meta: { | |
| 1099 | source: { name: "Weather", logoUrl: "https://example.com/weather.png" }, | |
| 1100 | }, | |
| 1101 | content: [{ type: "text", text: "Sunny" }], | |
| 1102 | }, | |
| 1103 | }, | |
| 1104 | _meta: { | |
| 1105 | is_mcp_tool_call: true, | |
| 1106 | "cognition.ai/subagent_context": { parentAgentId: "child-a" }, | |
| 1107 | }, | |
| 1108 | }, | |
| 1109 | { | |
| 1110 | sessionUpdate: "tool_call", | |
| 1111 | toolCallId: "child-mcp-fallback", | |
| 1112 | title: "Ran command", | |
| 1113 | kind: "execute", | |
| 1114 | status: "completed", | |
| 1115 | content: [{ type: "terminal", terminalId: fallbackTerminal.terminalId }], | |
| 1116 | _meta: { "cognition.ai/subagent_context": { parentAgentId: "child-a" } }, | |
| 1117 | }, | |
| 1118 | { | |
| 1119 | sessionUpdate: "tool_call", | |
| 1120 | toolCallId: "parent-weather", | |
| 1121 | title: "Check weather", | |
| 1122 | status: "completed", | |
| 1123 | rawInput: { server: "weather", tool: "get_weather", city: "Berlin" }, | |
| 1124 | rawOutput: { | |
| 1125 | _meta: { | |
| 1126 | source: { name: "Weather", logoUrl: "https://example.com/weather.png" }, | |
| 1127 | }, | |
| 1128 | content: [{ type: "text", text: "Sunny" }], | |
| 1129 | }, | |
| 1130 | _meta: { is_mcp_tool_call: true }, | |
| 1131 | }, | |
| 1078 | 1132 | { |
| 1079 | 1133 | sessionUpdate: "agent_message_chunk", |
| 1080 | 1134 | content: { type: "text", text: " → decider → event)." }, |
| ... | ... | @@ -1192,6 +1246,37 @@ describe("AcpAdapterV2", () => { |
| 1192 | 1246 | ); |
| 1193 | 1247 | assert.deepEqual([...childMessages.values()], ["Checking the code.", "ONE"]); |
| 1194 | 1248 | assert.equal(task?.prompt, "Run pwd, then reply ONE."); |
| 1249 | // Terminal-fallback MCP calls in a child session keep their T3 identity. | |
| 1250 | assert.isTrue( | |
| 1251 | items.some( | |
| 1252 | (item) => | |
| 1253 | item.threadId === task?.childThreadId && | |
| 1254 | item.type === "dynamic_tool" && | |
| 1255 | item.toolName === "t3-code.task_status", | |
| 1256 | ), | |
| 1257 | ); | |
| 1258 | const childMcp = items.find( | |
| 1259 | (item) => | |
| 1260 | item.threadId === task?.childThreadId && | |
| 1261 | item.type === "dynamic_tool" && | |
| 1262 | item.toolName === "weather.get_weather", | |
| 1263 | ); | |
| 1264 | const parentMcp = items.find( | |
| 1265 | (item) => | |
| 1266 | item.threadId === threadId && | |
| 1267 | item.type === "dynamic_tool" && | |
| 1268 | item.toolName === "weather.get_weather", | |
| 1269 | ); | |
| 1270 | for (const item of [parentMcp, childMcp]) { | |
| 1271 | assert.equal(item?.title, "get weather"); | |
| 1272 | assert.deepEqual(item?.toolSource, { | |
| 1273 | key: "mcp:weather", | |
| 1274 | name: "Weather", | |
| 1275 | kind: "integration", | |
| 1276 | icon: { _tag: "themed-logo", logoUrl: "https://example.com/weather.png" }, | |
| 1277 | }); | |
| 1278 | assert.deepEqual(item?.toolIcon, item?.toolSource?.icon); | |
| 1279 | } | |
| 1195 | 1280 | assert.isTrue( |
| 1196 | 1281 | items.some( |
| 1197 | 1282 | (item) => |
| ... | ... | @@ -1216,7 +1301,7 @@ describe("AcpAdapterV2", () => { |
| 1216 | 1301 | const parentTools = items.filter( |
| 1217 | 1302 | (item) => item.threadId === threadId && item.type === "dynamic_tool", |
| 1218 | 1303 | ); |
| 1219 | assert.equal(parentTools.length, 1); | |
| 1304 | assert.equal(parentTools.length, 2); | |
| 1220 | 1305 | assert.equal(parentTools[0]?.title, "Parent tool finished"); |
| 1221 | 1306 | }).pipe(Effect.provide(testLayer), Effect.scoped), |
| 1222 | 1307 | ); |
apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts+56-15| ... | ... | @@ -63,6 +63,7 @@ import { |
| 63 | 63 | makeAcpMcpOverAcpBridge, |
| 64 | 64 | type AcpMcpOverAcpBridge, |
| 65 | 65 | } from "../../mcp/AcpMcpOverAcpBridge.ts"; |
| 66 | import { mcpToolPresentation } from "../../provider/McpToolPresentation.ts"; | |
| 66 | 67 | import * as McpProviderSession from "../../mcp/McpProviderSession.ts"; |
| 67 | 68 | import { |
| 68 | 69 | applyAcpAgentTerminalUpdate, |
| ... | ... | @@ -1632,6 +1633,21 @@ export function makeAcpAdapterV2( |
| 1632 | 1633 | embeddedTerminalsByToolCallId.delete(oldest); |
| 1633 | 1634 | } |
| 1634 | 1635 | }; |
| 1636 | // Command lines of the terminals embedded in a tool call, so MCP calls | |
| 1637 | // made through the acp-mcp-call terminal fallback keep their identity. | |
| 1638 | const embeddedTerminalCommands = ( | |
| 1639 | sessionId: string, | |
| 1640 | toolCallId: string, | |
| 1641 | ): ReadonlyArray<string> => | |
| 1642 | ( | |
| 1643 | embeddedTerminalsByToolCallId.get(sessionScopedId(sessionId, toolCallId)) | |
| 1644 | ?.terminalIds ?? [] | |
| 1645 | ).flatMap((terminalId) => { | |
| 1646 | const command = | |
| 1647 | clientTerminals?.readCommandLine(terminalId) ?? | |
| 1648 | agentTerminalsById.get(sessionScopedId(sessionId, terminalId))?.command; | |
| 1649 | return command === undefined ? [] : [command]; | |
| 1650 | }); | |
| 1635 | 1651 | // Client terminals (Devin) run with the T3 server's privileges, so they |
| 1636 | 1652 | // are policy-checked against the active turn policy; a command the user |
| 1637 | 1653 | // already approved satisfies an "ask" disposition. |
| ... | ... | @@ -3267,17 +3283,10 @@ export function makeAcpAdapterV2( |
| 3267 | 3283 | // agent-specific shape and project the same branded dynamic_tool |
| 3268 | 3284 | // item native providers produce (e.g. the T3 orchestration tools). |
| 3269 | 3285 | const mcpIdentity = extractMcpToolCallIdentity(toolCall, { |
| 3270 | embeddedTerminalCommands: ( | |
| 3271 | embeddedTerminalsByToolCallId.get( | |
| 3272 | sessionScopedId(context.nativeThreadId, toolCall.toolCallId), | |
| 3273 | )?.terminalIds ?? [] | |
| 3274 | ).flatMap((terminalId) => { | |
| 3275 | const command = | |
| 3276 | clientTerminals?.readCommandLine(terminalId) ?? | |
| 3277 | agentTerminalsById.get(sessionScopedId(context.nativeThreadId, terminalId)) | |
| 3278 | ?.command; | |
| 3279 | return command === undefined ? [] : [command]; | |
| 3280 | }), | |
| 3286 | embeddedTerminalCommands: embeddedTerminalCommands( | |
| 3287 | context.nativeThreadId, | |
| 3288 | toolCall.toolCallId, | |
| 3289 | ), | |
| 3281 | 3290 | }); |
| 3282 | 3291 | let turnItem: OrchestrationV2TurnItem; |
| 3283 | 3292 | if (toolCall.toolCallId.startsWith("acp-compaction:")) { |
| ... | ... | @@ -3302,9 +3311,14 @@ export function makeAcpAdapterV2( |
| 3302 | 3311 | } else if (mcpIdentity !== undefined) { |
| 3303 | 3312 | turnItem = { |
| 3304 | 3313 | ...base, |
| 3305 | // Identity lives in toolName, like native Codex MCP items; the | |
| 3306 | // agent's own title (e.g. "Ran command") would shadow it. | |
| 3307 | 3314 | title: null, |
| 3315 | ...mcpToolPresentation({ | |
| 3316 | serverName: mcpIdentity.server, | |
| 3317 | toolName: mcpIdentity.tool, | |
| 3318 | source: unknownRecord( | |
| 3319 | (unknownRecord(rawOutputRecord?.result) ?? rawOutputRecord)?._meta, | |
| 3320 | )?.source, | |
| 3321 | }), | |
| 3308 | 3322 | type: "dynamic_tool", |
| 3309 | 3323 | toolName: `${mcpIdentity.server}.${mcpIdentity.tool}`, |
| 3310 | 3324 | input: |
| ... | ... | @@ -4328,7 +4342,8 @@ export function makeAcpAdapterV2( |
| 4328 | 4342 | return; |
| 4329 | 4343 | } |
| 4330 | 4344 | if (context.finalized) return; |
| 4331 | if (notification.sessionId !== (yield* Ref.get(activeSessionId))) { | |
| 4345 | const rootSessionId = yield* Ref.get(activeSessionId); | |
| 4346 | if (notification.sessionId !== rootSessionId) { | |
| 4332 | 4347 | // Finalize may have completed during the activeSessionId yield. |
| 4333 | 4348 | if (context.finalized) return; |
| 4334 | 4349 | if (flavor.extractSubagentUpdate === undefined) return; |
| ... | ... | @@ -4356,6 +4371,17 @@ export function makeAcpAdapterV2( |
| 4356 | 4371 | const key = `${nativeTaskId}:tool:${toolCall.toolCallId}`; |
| 4357 | 4372 | const merged = mergeToolCallState(context.tools.get(key), toolCall); |
| 4358 | 4373 | context.tools.set(key, merged); |
| 4374 | // Terminals are remembered under the raw session id: the child's | |
| 4375 | // own session, or the root one when the flavor routes child | |
| 4376 | // updates out of it (Devin). | |
| 4377 | const mcpIdentity = extractMcpToolCallIdentity(merged, { | |
| 4378 | embeddedTerminalCommands: [ | |
| 4379 | ...embeddedTerminalCommands(notification.sessionId, toolCall.toolCallId), | |
| 4380 | ...(rootSessionId === null | |
| 4381 | ? [] | |
| 4382 | : embeddedTerminalCommands(rootSessionId, toolCall.toolCallId)), | |
| 4383 | ], | |
| 4384 | }); | |
| 4359 | 4385 | const now = yield* DateTime.now; |
| 4360 | 4386 | const status = toolStatus(merged.status); |
| 4361 | 4387 | const startedAt = context.toolStartedAt.get(key) ?? now; |
| ... | ... | @@ -4380,7 +4406,22 @@ export function makeAcpAdapterV2( |
| 4380 | 4406 | completedAt: completedAtForStatus(status, now), |
| 4381 | 4407 | updatedAt: now, |
| 4382 | 4408 | type: "dynamic_tool", |
| 4383 | toolName: merged.title ?? merged.kind ?? "Tool", | |
| 4409 | ...(mcpIdentity === undefined | |
| 4410 | ? {} | |
| 4411 | : mcpToolPresentation({ | |
| 4412 | serverName: mcpIdentity.server, | |
| 4413 | toolName: mcpIdentity.tool, | |
| 4414 | source: unknownRecord( | |
| 4415 | ( | |
| 4416 | unknownRecord(unknownRecord(merged.data.rawOutput)?.result) ?? | |
| 4417 | unknownRecord(merged.data.rawOutput) | |
| 4418 | )?._meta, | |
| 4419 | )?.source, | |
| 4420 | })), | |
| 4421 | toolName: | |
| 4422 | mcpIdentity === undefined | |
| 4423 | ? (merged.title ?? merged.kind ?? "Tool") | |
| 4424 | : `${mcpIdentity.server}.${mcpIdentity.tool}`, | |
| 4384 | 4425 | input: merged.data.rawInput ?? null, |
| 4385 | 4426 | output: merged.data.rawOutput ?? merged.data.content ?? null, |
| 4386 | 4427 | }, |
apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts+105| ... | ... | @@ -2195,6 +2195,111 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 2195 | 2195 | }); |
| 2196 | 2196 | const makeWakeHarness = makeWakeHarnessWithOptions(); |
| 2197 | 2197 | |
| 2198 | it.effect.each([ | |
| 2199 | { isError: false, title: "Check weather" }, | |
| 2200 | { isError: true, title: "Check weather" }, | |
| 2201 | { isError: false, title: undefined }, | |
| 2202 | ])("keeps late MCP display metadata with %j", ({ isError, title }) => | |
| 2203 | Effect.gen(function* () { | |
| 2204 | const harness = yield* makeWakeHarness; | |
| 2205 | yield* harness.runtime.startTurn( | |
| 2206 | makeClaudeTestTurnInput({ | |
| 2207 | threadId: harness.threadId, | |
| 2208 | providerThread: harness.providerThread, | |
| 2209 | now: yield* DateTime.now, | |
| 2210 | attemptId: RunAttemptId.make("mcp-presentation"), | |
| 2211 | text: "Check the weather", | |
| 2212 | attachments: [], | |
| 2213 | }), | |
| 2214 | ); | |
| 2215 | const toolName = "mcp__weather__get_weather"; | |
| 2216 | const id = "weather-call"; | |
| 2217 | yield* Effect.promise(() => | |
| 2218 | harness.getOpenedOptions()!.canUseTool!( | |
| 2219 | toolName, | |
| 2220 | { city: "Berlin" }, | |
| 2221 | { | |
| 2222 | signal: new AbortController().signal, | |
| 2223 | toolUseID: id, | |
| 2224 | requestId: "weather-request", | |
| 2225 | }, | |
| 2226 | ), | |
| 2227 | ); | |
| 2228 | yield* harness.offerAndWait( | |
| 2229 | claudeSdkFrame({ | |
| 2230 | type: "assistant", | |
| 2231 | uuid: "weather-assistant", | |
| 2232 | session_id: WAKE_NATIVE_SESSION, | |
| 2233 | parent_tool_use_id: null, | |
| 2234 | message: { | |
| 2235 | id: "weather-message", | |
| 2236 | type: "message", | |
| 2237 | role: "assistant", | |
| 2238 | model: "claude-sonnet-4-6", | |
| 2239 | content: [{ type: "tool_use", id, name: toolName, input: { city: "Berlin" } }], | |
| 2240 | }, | |
| 2241 | tool_use_meta: [ | |
| 2242 | { | |
| 2243 | id, | |
| 2244 | display_name: title, | |
| 2245 | server_display_name: "Weather", | |
| 2246 | icon_url: "https://example.com/weather.png", | |
| 2247 | }, | |
| 2248 | ], | |
| 2249 | }), | |
| 2250 | ); | |
| 2251 | yield* harness.offerAndWait( | |
| 2252 | claudeSdkFrame({ | |
| 2253 | type: "user", | |
| 2254 | uuid: "weather-result", | |
| 2255 | session_id: WAKE_NATIVE_SESSION, | |
| 2256 | parent_tool_use_id: null, | |
| 2257 | message: { | |
| 2258 | role: "user", | |
| 2259 | content: [ | |
| 2260 | { | |
| 2261 | type: "tool_result", | |
| 2262 | tool_use_id: id, | |
| 2263 | content: "Weather result", | |
| 2264 | is_error: isError, | |
| 2265 | }, | |
| 2266 | ], | |
| 2267 | }, | |
| 2268 | }), | |
| 2269 | ); | |
| 2270 | yield* Queue.offer( | |
| 2271 | harness.sdkMessages, | |
| 2272 | makeResultFrame({ uuid: "weather-terminal", result: "Weather checked" }), | |
| 2273 | ); | |
| 2274 | yield* Queue.take(harness.terminalReceipts); | |
| 2275 | const items = harness.events.flatMap((event) => | |
| 2276 | event.type === "turn_item.updated" && event.turnItem.type === "dynamic_tool" | |
| 2277 | ? [event.turnItem] | |
| 2278 | : [], | |
| 2279 | ); | |
| 2280 | assert.deepEqual( | |
| 2281 | items.map((item) => item.status), | |
| 2282 | ["running", "running", isError ? "failed" : "completed"], | |
| 2283 | ); | |
| 2284 | assert.isNull(items[0]?.title); | |
| 2285 | for (const item of items.slice(1)) { | |
| 2286 | assert.equal(item.title, title ?? "get weather"); | |
| 2287 | assert.deepEqual(item.toolIcon, { | |
| 2288 | _tag: "themed-logo", | |
| 2289 | logoUrl: "https://example.com/weather.png", | |
| 2290 | }); | |
| 2291 | assert.deepEqual(item.toolSource, { | |
| 2292 | key: "mcp:weather", | |
| 2293 | name: "Weather", | |
| 2294 | kind: "integration", | |
| 2295 | icon: { _tag: "themed-logo", logoUrl: "https://example.com/weather.png" }, | |
| 2296 | }); | |
| 2297 | assert.deepEqual(item.input, { city: "Berlin" }); | |
| 2298 | } | |
| 2299 | assert.equal(new Set(items.map((item) => item.id)).size, 1); | |
| 2300 | }).pipe(Effect.scoped, Effect.provide(Layer.merge(IdAllocator.layer, NodeServices.layer))), | |
| 2301 | ); | |
| 2302 | ||
| 2198 | 2303 | it.effect( |
| 2199 | 2304 | "reuses a background shell's query for omitted and explicit Normal, but blocks Fast", |
| 2200 | 2305 | () => |
apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts+27-49| ... | ... | @@ -67,7 +67,6 @@ import { |
| 67 | 67 | type ProviderUserInputAnswers, |
| 68 | 68 | type ProviderThreadId, |
| 69 | 69 | type ThreadId, |
| 70 | type ToolActivitySource, | |
| 71 | 70 | } from "@t3tools/contracts"; |
| 72 | 71 | |
| 73 | 72 | import * as Cause from "effect/Cause"; |
| ... | ... | @@ -116,6 +115,7 @@ import type { ServerProviderShape } from "../../provider/Services/ServerProvider |
| 116 | 115 | import { mergeProviderInstanceEnvironment } from "../../provider/ProviderInstanceEnvironment.ts"; |
| 117 | 116 | import { T3_CODE_ORCHESTRATION_INSTRUCTIONS } from "../../provider/T3OrchestrationInstructions.ts"; |
| 118 | 117 | import { buildRuntimeInstructions } from "../../provider/RuntimeInstructions.ts"; |
| 118 | import { mcpToolPresentation, normalizeMcpText } from "../../provider/McpToolPresentation.ts"; | |
| 119 | 119 | import * as McpProviderSession from "../../mcp/McpProviderSession.ts"; |
| 120 | 120 | import * as IdAllocator from "../IdAllocator.ts"; |
| 121 | 121 | import { makeProviderFailure, makeProviderRetryTurnItem } from "../ProviderFailure.ts"; |
| ... | ... | @@ -2089,62 +2089,33 @@ function claudeToolUseBlocksFromAssistantMessage( |
| 2089 | 2089 | return message.message.content.filter(isClaudeToolUseContentBlock); |
| 2090 | 2090 | } |
| 2091 | 2091 | |
| 2092 | interface ClaudeToolPresentation { | |
| 2093 | readonly title: string; | |
| 2094 | readonly toolSource?: ToolActivitySource; | |
| 2095 | } | |
| 2096 | ||
| 2097 | function boundedMetaText(value: unknown, maxLength: number): string | undefined { | |
| 2098 | if (typeof value !== "string") return undefined; | |
| 2099 | const text = value.trim().replace(/\s+/gu, " "); | |
| 2100 | return text.length > 0 && text.length <= maxLength ? text : undefined; | |
| 2101 | } | |
| 2102 | ||
| 2103 | function metaHttpUrl(value: unknown): string | undefined { | |
| 2104 | if (typeof value !== "string" || value.length > 4096) return undefined; | |
| 2105 | try { | |
| 2106 | const url = new URL(value); | |
| 2107 | return (url.protocol === "https:" || url.protocol === "http:") && url.href.length <= 4096 | |
| 2108 | ? url.href | |
| 2109 | : undefined; | |
| 2110 | } catch { | |
| 2111 | return undefined; | |
| 2112 | } | |
| 2113 | } | |
| 2092 | type ClaudeToolPresentation = ReturnType<typeof mcpToolPresentation>; | |
| 2114 | 2093 | |
| 2115 | /** | |
| 2116 | * Claude Code's own label for each tool use (the MCP tool's title, or its | |
| 2117 | * humanized name) plus the server's name and icon. The CLI sends these as | |
| 2118 | * `tool_use_meta` on assistant frames; the SDK types do not declare it yet. | |
| 2119 | */ | |
| 2120 | 2094 | function claudeToolPresentationsFromAssistantMessage( |
| 2121 | 2095 | message: SDKMessage, |
| 2122 | 2096 | ): ReadonlyMap<string, ClaudeToolPresentation> { |
| 2123 | 2097 | const presentations = new Map<string, ClaudeToolPresentation>(); |
| 2124 | 2098 | const meta = message.type === "assistant" ? Reflect.get(message, "tool_use_meta") : undefined; |
| 2125 | 2099 | if (!Array.isArray(meta)) return presentations; |
| 2100 | const toolNames = new Map( | |
| 2101 | claudeToolUseBlocksFromAssistantMessage(message).map((tool) => [ | |
| 2102 | tool.id, | |
| 2103 | tool.name.replace(/^mcp__claude_ai_/u, "mcp__"), | |
| 2104 | ]), | |
| 2105 | ); | |
| 2126 | 2106 | for (const entry of meta) { |
| 2127 | 2107 | if (typeof entry !== "object" || entry === null) continue; |
| 2128 | const id = boundedMetaText(Reflect.get(entry, "id"), 512); | |
| 2129 | const title = boundedMetaText(Reflect.get(entry, "display_name"), 160); | |
| 2130 | if (id === undefined || title === undefined) continue; | |
| 2131 | const serverName = boundedMetaText(Reflect.get(entry, "server_display_name"), 160); | |
| 2132 | const iconUrl = metaHttpUrl(Reflect.get(entry, "icon_url")); | |
| 2133 | presentations.set(id, { | |
| 2134 | title, | |
| 2135 | ...(serverName === undefined | |
| 2136 | ? {} | |
| 2137 | : { | |
| 2138 | toolSource: { | |
| 2139 | key: `mcp:${serverName.toLowerCase()}`, | |
| 2140 | name: serverName, | |
| 2141 | kind: "integration", | |
| 2142 | ...(iconUrl === undefined | |
| 2143 | ? {} | |
| 2144 | : { icon: { _tag: "themed-logo", logoUrl: iconUrl } as const }), | |
| 2145 | }, | |
| 2146 | }), | |
| 2147 | }); | |
| 2108 | const id = normalizeMcpText(Reflect.get(entry, "id"), 512); | |
| 2109 | if (id === undefined) continue; | |
| 2110 | presentations.set( | |
| 2111 | id, | |
| 2112 | mcpToolPresentation({ | |
| 2113 | toolName: toolNames.get(id), | |
| 2114 | title: Reflect.get(entry, "display_name"), | |
| 2115 | serverDisplayName: Reflect.get(entry, "server_display_name"), | |
| 2116 | iconUrl: Reflect.get(entry, "icon_url"), | |
| 2117 | }), | |
| 2118 | ); | |
| 2148 | 2119 | } |
| 2149 | 2120 | return presentations; |
| 2150 | 2121 | } |
| ... | ... | @@ -3878,6 +3849,9 @@ export function makeClaudeAdapterV2( |
| 3878 | 3849 | : { |
| 3879 | 3850 | ...itemBase, |
| 3880 | 3851 | type: "dynamic_tool", |
| 3852 | ...(input.presentation?.toolIcon === undefined | |
| 3853 | ? {} | |
| 3854 | : { toolIcon: input.presentation.toolIcon }), | |
| 3881 | 3855 | ...(input.presentation?.toolSource === undefined |
| 3882 | 3856 | ? {} |
| 3883 | 3857 | : { toolSource: input.presentation.toolSource }), |
| ... | ... | @@ -6053,7 +6027,11 @@ export function makeClaudeAdapterV2( |
| 6053 | 6027 | toolName: toolUse.name, |
| 6054 | 6028 | toolInput: nativeToolInput, |
| 6055 | 6029 | parentToolUseId: parentToolUseIdFromSdkMessage(message), |
| 6056 | presentation: toolPresentations.get(toolUse.id), | |
| 6030 | presentation: | |
| 6031 | toolPresentations.get(toolUse.id) ?? | |
| 6032 | mcpToolPresentation({ | |
| 6033 | toolName: toolUse.name.replace(/^mcp__claude_ai_/u, "mcp__"), | |
| 6034 | }), | |
| 6057 | 6035 | }); |
| 6058 | 6036 | const heldPlan = heldProposedPlansByToolUseId.get(toolUse.id); |
| 6059 | 6037 | if (heldPlan !== undefined) { |
apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts+50-2| ... | ... | @@ -823,14 +823,62 @@ describe("CodexAdapterV2 dynamic tool projection", () => { |
| 823 | 823 | ); |
| 824 | 824 | assert.equal( |
| 825 | 825 | CodexAdapterV2.projectCodexDynamicToolItem({ ...call, arguments: { title: " " } }).title, |
| 826 | undefined, | |
| 826 | "js", | |
| 827 | 827 | ); |
| 828 | 828 | assert.equal( |
| 829 | 829 | CodexAdapterV2.projectCodexDynamicToolItem({ ...call, server: "github" }).title, |
| 830 | undefined, | |
| 830 | "js", | |
| 831 | 831 | ); |
| 832 | 832 | }); |
| 833 | 833 | |
| 834 | it.each(["inProgress", "completed", "failed"] as const)( | |
| 835 | "presents ordinary MCP calls when %s", | |
| 836 | (status) => { | |
| 837 | const projection = CodexAdapterV2.projectCodexDynamicToolItem({ | |
| 838 | type: "mcpToolCall", | |
| 839 | id: "weather-call", | |
| 840 | server: "weather", | |
| 841 | tool: "get_weather", | |
| 842 | status, | |
| 843 | arguments: { city: "Berlin" }, | |
| 844 | }); | |
| 845 | assert.equal(projection.title, "get weather"); | |
| 846 | assert.deepEqual(projection.toolSource, { | |
| 847 | key: "mcp:weather", | |
| 848 | name: "weather", | |
| 849 | kind: "integration", | |
| 850 | }); | |
| 851 | assert.deepEqual(projection.input, { city: "Berlin" }); | |
| 852 | }, | |
| 853 | ); | |
| 854 | ||
| 855 | it("uses Codex connector names without reading a display title from arguments", () => { | |
| 856 | const projection = CodexAdapterV2.projectCodexDynamicToolItem({ | |
| 857 | type: "mcpToolCall", | |
| 858 | id: "connector-call", | |
| 859 | server: "_apps", | |
| 860 | tool: "connector_get_weather", | |
| 861 | status: "completed", | |
| 862 | arguments: { title: "Argument, not display metadata" }, | |
| 863 | appContext: { | |
| 864 | connectorId: "weather-app", | |
| 865 | appName: "Weather", | |
| 866 | actionName: "Check weather", | |
| 867 | }, | |
| 868 | result: { | |
| 869 | content: [], | |
| 870 | _meta: { source: { logoUrl: "https://example.com/weather.png" } }, | |
| 871 | }, | |
| 872 | }); | |
| 873 | assert.equal(projection.title, "Check weather"); | |
| 874 | assert.equal(projection.toolSource?.name, "Weather"); | |
| 875 | assert.deepEqual(projection.toolIcon, { | |
| 876 | _tag: "themed-logo", | |
| 877 | logoUrl: "https://example.com/weather.png", | |
| 878 | }); | |
| 879 | assert.deepEqual(projection.toolSource?.icon, projection.toolIcon); | |
| 880 | }); | |
| 881 | ||
| 834 | 882 | it("preserves native browser and app icons alongside MCP tool output", () => { |
| 835 | 883 | const browser = CodexAdapterV2.projectCodexDynamicToolItem({ |
| 836 | 884 | type: "mcpToolCall", |
apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts+3-2| ... | ... | @@ -489,9 +489,10 @@ export function projectCodexDynamicToolItem( |
| 489 | 489 | item.type === "mcpToolCall" |
| 490 | 490 | ? `${item.server}.${item.tool}` |
| 491 | 491 | : [trimText(item.namespace), item.tool].filter(Boolean).join("."); |
| 492 | const title = dynamicToolTitle(toolName, item.arguments); | |
| 492 | const presentation = item.type === "mcpToolCall" ? mcpToolPresentation(item) : {}; | |
| 493 | const title = dynamicToolTitle(toolName, item.arguments) ?? presentation.title; | |
| 493 | 494 | const projection: CodexDynamicToolProjection = { |
| 494 | ...(item.type === "mcpToolCall" ? mcpToolPresentation(item) : {}), | |
| 495 | ...presentation, | |
| 495 | 496 | toolName, |
| 496 | 497 | ...(title ? { title } : {}), |
| 497 | 498 | input: item.arguments, |
apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.test.ts+40| ... | ... | @@ -359,6 +359,22 @@ describe("CursorAdapterV2", () => { |
| 359 | 359 | numFiles: 0, |
| 360 | 360 | }; |
| 361 | 361 | const updates: ReadonlyArray<InteractionUpdate> = [ |
| 362 | ...(["tool-call-started", "tool-call-completed"] as const).map((type) => ({ | |
| 363 | type, | |
| 364 | modelCallId: "native-model-call", | |
| 365 | callId: "mcp-weather", | |
| 366 | toolCall: { | |
| 367 | type: "mcp" as const, | |
| 368 | args: { | |
| 369 | providerIdentifier: "weather", | |
| 370 | toolName: "get_weather", | |
| 371 | args: { city: "Berlin" }, | |
| 372 | }, | |
| 373 | ...(type === "tool-call-completed" | |
| 374 | ? { result: { status: "success" as const, value: { content: [], isError: false } } } | |
| 375 | : {}), | |
| 376 | }, | |
| 377 | })), | |
| 362 | 378 | { |
| 363 | 379 | type: "tool-call-completed", |
| 364 | 380 | modelCallId: "native-model-call", |
| ... | ... | @@ -625,6 +641,30 @@ describe("CursorAdapterV2", () => { |
| 625 | 641 | Stream.takeUntil((event) => event.type === "turn.terminal"), |
| 626 | 642 | Stream.runCollect, |
| 627 | 643 | ); |
| 644 | const mcpItems = events.flatMap((event) => | |
| 645 | event.type === "turn_item.updated" && | |
| 646 | event.turnItem.type === "dynamic_tool" && | |
| 647 | event.turnItem.toolName === "mcp__weather__get_weather" | |
| 648 | ? [event.turnItem] | |
| 649 | : [], | |
| 650 | ); | |
| 651 | assert.deepEqual( | |
| 652 | mcpItems.map((item) => item.status), | |
| 653 | ["running", "completed"], | |
| 654 | ); | |
| 655 | for (const item of mcpItems) { | |
| 656 | assert.equal(item.title, "get weather"); | |
| 657 | assert.deepEqual(item.toolSource, { | |
| 658 | key: "mcp:weather", | |
| 659 | name: "weather", | |
| 660 | kind: "integration", | |
| 661 | }); | |
| 662 | assert.deepEqual(item.input, { | |
| 663 | providerIdentifier: "weather", | |
| 664 | toolName: "get_weather", | |
| 665 | args: { city: "Berlin" }, | |
| 666 | }); | |
| 667 | } | |
| 628 | 668 | const fileSearchItems = events.flatMap((event) => |
| 629 | 669 | event.type === "turn_item.updated" && |
| 630 | 670 | event.turnItem.type === "file_search" && |
apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.ts+7| ... | ... | @@ -44,6 +44,7 @@ import * as Stream from "effect/Stream"; |
| 44 | 44 | |
| 45 | 45 | import { resolveAttachmentPath } from "../../attachmentStore.ts"; |
| 46 | 46 | import * as ServerConfig from "../../config.ts"; |
| 47 | import { mcpToolPresentation } from "../../provider/McpToolPresentation.ts"; | |
| 47 | 48 | import * as McpProviderSession from "../../mcp/McpProviderSession.ts"; |
| 48 | 49 | import { CursorTransportFailure } from "../../provider/acp/CursorTransportFailure.ts"; |
| 49 | 50 | import { cursorSdkModelSelection } from "../../provider/cursorSdkModel.ts"; |
| ... | ... | @@ -1282,6 +1283,12 @@ export function makeCursorAdapterV2( |
| 1282 | 1283 | turnItem = { |
| 1283 | 1284 | ...base, |
| 1284 | 1285 | type: "dynamic_tool", |
| 1286 | ...(toolCall.type === "mcp" | |
| 1287 | ? mcpToolPresentation({ | |
| 1288 | serverName: toolCall.args.providerIdentifier, | |
| 1289 | toolName: toolCall.args.toolName, | |
| 1290 | }) | |
| 1291 | : {}), | |
| 1285 | 1292 | toolName: cursorToolName(toolCall), |
| 1286 | 1293 | input: toolCall.args, |
| 1287 | 1294 | ...(cursorToolOutput(toolCall) === undefined |
apps/server/src/orchestration-v2/Adapters/OpenCodeAdapterV2.test.ts+244| ... | ... | @@ -902,6 +902,250 @@ describe("OpenCodeAdapterV2", () => { |
| 902 | 902 | }).pipe(Effect.provide(IdAllocator.layer), Effect.scoped), |
| 903 | 903 | ); |
| 904 | 904 | |
| 905 | it.effect("presents OpenCode MCP calls without treating remote tools as local edits", () => | |
| 906 | Effect.gen(function* () { | |
| 907 | const nativeEvents = asyncEventStream(); | |
| 908 | const nativeSessionId = "native-opencode-mcp"; | |
| 909 | let statusReads = 0; | |
| 910 | const harness = yield* makeOpenCodeRuntimeHarness("mcp-presentation", nativeSessionId, { | |
| 911 | event: { | |
| 912 | subscribe: async (_input: unknown, options: { signal?: AbortSignal }) => { | |
| 913 | options.signal?.addEventListener("abort", () => nativeEvents.close(), { once: true }); | |
| 914 | return { stream: nativeEvents.stream }; | |
| 915 | }, | |
| 916 | }, | |
| 917 | session: { | |
| 918 | create: async () => ({ data: { id: nativeSessionId, time: { created: 1, updated: 1 } } }), | |
| 919 | promptAsync: async () => ({ data: true }), | |
| 920 | }, | |
| 921 | mcp: { | |
| 922 | status: async () => { | |
| 923 | statusReads++; | |
| 924 | return { | |
| 925 | data: { | |
| 926 | "my.server_with_underscores": { status: "connected" }, | |
| 927 | ambiguous: { status: "connected" }, | |
| 928 | ambiguous_server: { status: "connected" }, | |
| 929 | code: { status: "connected" }, | |
| 930 | apply: { status: "connected" }, | |
| 931 | }, | |
| 932 | }; | |
| 933 | }, | |
| 934 | }, | |
| 935 | }); | |
| 936 | yield* harness.startTurn(); | |
| 937 | const received = yield* harness.runtime.events.pipe( | |
| 938 | Stream.takeUntil( | |
| 939 | (event) => event.type === "turn_item.updated" && event.turnItem.type === "compaction", | |
| 940 | ), | |
| 941 | Stream.runCollect, | |
| 942 | Effect.forkScoped, | |
| 943 | ); | |
| 944 | for (const status of ["running", "completed", "error"] as const) { | |
| 945 | yield* Effect.promise(() => | |
| 946 | nativeEvents.push({ | |
| 947 | type: "message.part.updated", | |
| 948 | properties: { | |
| 949 | sessionID: nativeSessionId, | |
| 950 | part: { | |
| 951 | id: "mcp-part", | |
| 952 | sessionID: nativeSessionId, | |
| 953 | messageID: "mcp-assistant", | |
| 954 | type: "tool", | |
| 955 | callID: "mcp-call", | |
| 956 | tool: "my_server_with_underscores_edit_document", | |
| 957 | state: { | |
| 958 | status, | |
| 959 | input: { document: "remote-doc" }, | |
| 960 | title: "Edit remote document", | |
| 961 | metadata: {}, | |
| 962 | output: "saved", | |
| 963 | error: "failed", | |
| 964 | time: { start: 1, end: 2 }, | |
| 965 | }, | |
| 966 | }, | |
| 967 | }, | |
| 968 | }), | |
| 969 | ); | |
| 970 | } | |
| 971 | yield* Effect.promise(() => | |
| 972 | nativeEvents.push({ | |
| 973 | type: "message.part.updated", | |
| 974 | properties: { | |
| 975 | sessionID: nativeSessionId, | |
| 976 | part: { | |
| 977 | id: "ambiguous-part", | |
| 978 | sessionID: nativeSessionId, | |
| 979 | messageID: "mcp-assistant", | |
| 980 | type: "tool", | |
| 981 | callID: "ambiguous-call", | |
| 982 | tool: "ambiguous_server_edit_document", | |
| 983 | state: { | |
| 984 | status: "completed", | |
| 985 | input: {}, | |
| 986 | title: "Ambiguous tool", | |
| 987 | metadata: {}, | |
| 988 | output: "", | |
| 989 | time: { start: 1, end: 2 }, | |
| 990 | }, | |
| 991 | }, | |
| 992 | }, | |
| 993 | }), | |
| 994 | ); | |
| 995 | for (const tool of ["code_search", "apply_patch"]) { | |
| 996 | yield* Effect.promise(() => | |
| 997 | nativeEvents.push({ | |
| 998 | type: "message.part.updated", | |
| 999 | properties: { | |
| 1000 | sessionID: nativeSessionId, | |
| 1001 | part: { | |
| 1002 | id: `native-${tool}`, | |
| 1003 | sessionID: nativeSessionId, | |
| 1004 | messageID: "mcp-assistant", | |
| 1005 | type: "tool", | |
| 1006 | callID: `native-${tool}`, | |
| 1007 | tool, | |
| 1008 | state: { | |
| 1009 | status: "completed", | |
| 1010 | input: { query: "needle", filePath: "local.ts" }, | |
| 1011 | title: `Native ${tool}`, | |
| 1012 | metadata: {}, | |
| 1013 | output: "done", | |
| 1014 | time: { start: 1, end: 2 }, | |
| 1015 | }, | |
| 1016 | }, | |
| 1017 | }, | |
| 1018 | }), | |
| 1019 | ); | |
| 1020 | } | |
| 1021 | yield* Effect.promise(() => | |
| 1022 | nativeEvents.push({ | |
| 1023 | type: "session.compacted", | |
| 1024 | properties: { sessionID: nativeSessionId }, | |
| 1025 | }), | |
| 1026 | ); | |
| 1027 | const allItems = (yield* Fiber.join(received)).flatMap((event) => | |
| 1028 | event.type === "turn_item.updated" ? [event.turnItem] : [], | |
| 1029 | ); | |
| 1030 | assert.equal( | |
| 1031 | allItems.find((item) => item.title === "Native code_search")?.type, | |
| 1032 | "web_search", | |
| 1033 | ); | |
| 1034 | assert.equal( | |
| 1035 | allItems.find((item) => item.title === "Native apply_patch")?.type, | |
| 1036 | "file_change", | |
| 1037 | ); | |
| 1038 | const items = allItems.filter((item) => item.type === "dynamic_tool"); | |
| 1039 | assert.deepEqual( | |
| 1040 | items.slice(0, 3).map((item) => item.status), | |
| 1041 | ["running", "completed", "failed"], | |
| 1042 | ); | |
| 1043 | assert.deepEqual( | |
| 1044 | items.slice(0, 3).map((item) => item.title), | |
| 1045 | ["Edit remote document", "Edit remote document", "edit document"], | |
| 1046 | ); | |
| 1047 | for (const item of items.slice(0, 3)) { | |
| 1048 | assert.deepEqual(item.toolSource, { | |
| 1049 | key: "mcp:my.server_with_underscores", | |
| 1050 | name: "my.server with underscores", | |
| 1051 | kind: "integration", | |
| 1052 | }); | |
| 1053 | assert.deepEqual(item.input, { document: "remote-doc" }); | |
| 1054 | } | |
| 1055 | assert.equal(items[3]?.toolName, "ambiguous_server_edit_document"); | |
| 1056 | assert.isUndefined(items[3]?.toolSource); | |
| 1057 | assert.equal(statusReads, 1); | |
| 1058 | }).pipe(Effect.provide(IdAllocator.layer), Effect.scoped), | |
| 1059 | ); | |
| 1060 | ||
| 1061 | it.effect.each(["failure", "timeout"] as const)("retries MCP status after %s", (failure) => | |
| 1062 | Effect.gen(function* () { | |
| 1063 | const nativeEvents = asyncEventStream(); | |
| 1064 | const called = promiseGate<void>(); | |
| 1065 | const sessionId = `mcp-status-${failure}`; | |
| 1066 | let statusReads = 0; | |
| 1067 | let signal: AbortSignal | undefined; | |
| 1068 | const harness = yield* makeOpenCodeRuntimeHarness(sessionId, sessionId, { | |
| 1069 | event: { subscribe: async () => ({ stream: nativeEvents.stream }) }, | |
| 1070 | session: { | |
| 1071 | create: async () => ({ data: { id: sessionId, time: { created: 1, updated: 1 } } }), | |
| 1072 | promptAsync: async () => ({ data: true }), | |
| 1073 | }, | |
| 1074 | mcp: { | |
| 1075 | status: async (_input: unknown, options: { signal?: AbortSignal }) => { | |
| 1076 | statusReads++; | |
| 1077 | if (statusReads === 1) { | |
| 1078 | signal = options?.signal; | |
| 1079 | called.resolve(); | |
| 1080 | if (failure === "timeout") return new Promise(() => {}); | |
| 1081 | throw new Error("MCP status unavailable"); | |
| 1082 | } | |
| 1083 | return { data: { weather: { status: "connected" } } }; | |
| 1084 | }, | |
| 1085 | }, | |
| 1086 | }); | |
| 1087 | yield* harness.startTurn(); | |
| 1088 | const received = yield* harness.runtime.events.pipe( | |
| 1089 | Stream.takeUntil( | |
| 1090 | (event) => event.type === "turn_item.updated" && event.turnItem.type === "compaction", | |
| 1091 | ), | |
| 1092 | Stream.runCollect, | |
| 1093 | Effect.forkScoped, | |
| 1094 | ); | |
| 1095 | for (const status of ["running", "completed"] as const) { | |
| 1096 | const emitted = yield* Effect.promise(() => | |
| 1097 | nativeEvents.push({ | |
| 1098 | type: "message.part.updated", | |
| 1099 | properties: { | |
| 1100 | sessionID: sessionId, | |
| 1101 | part: { | |
| 1102 | id: "weather-part", | |
| 1103 | sessionID: sessionId, | |
| 1104 | messageID: "weather-message", | |
| 1105 | type: "tool", | |
| 1106 | callID: "weather-call", | |
| 1107 | tool: "weather_edit_document", | |
| 1108 | state: { | |
| 1109 | status, | |
| 1110 | input: { document: "remote-doc" }, | |
| 1111 | title: "Edit remote document", | |
| 1112 | metadata: {}, | |
| 1113 | output: "saved", | |
| 1114 | time: { start: 1, end: 2 }, | |
| 1115 | }, | |
| 1116 | }, | |
| 1117 | }, | |
| 1118 | }), | |
| 1119 | ).pipe(Effect.forkScoped); | |
| 1120 | if (status === "running" && failure === "timeout") { | |
| 1121 | yield* Effect.promise(() => called.promise); | |
| 1122 | yield* TestClock.adjust("1 second"); | |
| 1123 | } | |
| 1124 | yield* Fiber.join(emitted); | |
| 1125 | } | |
| 1126 | yield* Effect.promise(() => | |
| 1127 | nativeEvents.push({ type: "session.compacted", properties: { sessionID: sessionId } }), | |
| 1128 | ); | |
| 1129 | const completed = (yield* Fiber.join(received)).find( | |
| 1130 | (event) => | |
| 1131 | event.type === "turn_item.updated" && | |
| 1132 | event.turnItem.type === "dynamic_tool" && | |
| 1133 | event.turnItem.status === "completed", | |
| 1134 | ); | |
| 1135 | assert.equal(statusReads, 2); | |
| 1136 | assert.equal( | |
| 1137 | completed?.type === "turn_item.updated" && completed.turnItem.title, | |
| 1138 | "Edit remote document", | |
| 1139 | ); | |
| 1140 | assert.deepEqual(completed?.type === "turn_item.updated" && completed.turnItem.toolSource, { | |
| 1141 | key: "mcp:weather", | |
| 1142 | name: "weather", | |
| 1143 | kind: "integration", | |
| 1144 | }); | |
| 1145 | if (failure === "timeout") assert.isTrue(signal?.aborted); | |
| 1146 | }).pipe(Effect.provide(IdAllocator.layer), Effect.scoped), | |
| 1147 | ); | |
| 1148 | ||
| 905 | 1149 | it.effect("admits a native command on its user receipt before generation completes", () => |
| 906 | 1150 | Effect.gen(function* () { |
| 907 | 1151 | const nativeEvents = asyncEventStream(); |
apps/server/src/orchestration-v2/Adapters/OpenCodeAdapterV2.testkit.ts+1| ... | ... | @@ -383,6 +383,7 @@ function makeReplayClient(controller: OpenCodeReplayController): OpencodeClient |
| 383 | 383 | reply: (input: unknown) => request("question.reply", input), |
| 384 | 384 | }, |
| 385 | 385 | mcp: { |
| 386 | status: () => request("mcp.status", {}), | |
| 386 | 387 | add: (input: unknown) => request("mcp.add", input), |
| 387 | 388 | }, |
| 388 | 389 | } as unknown as OpencodeClient; |
apps/server/src/orchestration-v2/Adapters/OpenCodeAdapterV2.ts+44-6| ... | ... | @@ -56,6 +56,7 @@ import * as Stream from "effect/Stream"; |
| 56 | 56 | |
| 57 | 57 | import { resolveAttachmentPath } from "../../attachmentStore.ts"; |
| 58 | 58 | import * as ServerConfig from "../../config.ts"; |
| 59 | import { mcpToolPresentation } from "../../provider/McpToolPresentation.ts"; | |
| 59 | 60 | import * as McpProviderSession from "../../mcp/McpProviderSession.ts"; |
| 60 | 61 | import type { EventNdjsonLogger } from "../../provider/Layers/EventNdjsonLogger.ts"; |
| 61 | 62 | import * as ProviderEventLoggers from "../../provider/Layers/ProviderEventLoggers.ts"; |
| ... | ... | @@ -286,6 +287,7 @@ interface ActiveOpenCodeTurn { |
| 286 | 287 | readonly parts: Map<string, Exclude<OpenCodePart, ToolPart>>; |
| 287 | 288 | readonly partIdsByMessage: Map<string, Set<string>>; |
| 288 | 289 | readonly toolNamesByCallId: Map<string, string>; |
| 290 | mcpServerNames?: ReadonlyArray<string>; | |
| 289 | 291 | readonly providerTurn: OrchestrationV2ProviderTurn; |
| 290 | 292 | nextItemOrdinal: number; |
| 291 | 293 | nativeUserMessageId: string | null; |
| ... | ... | @@ -1593,12 +1595,48 @@ export function makeOpenCodeAdapterV2( |
| 1593 | 1595 | | "completedAt" |
| 1594 | 1596 | | "updatedAt" |
| 1595 | 1597 | >; |
| 1596 | const turnItem = openCodeToolTurnItem(base, { | |
| 1597 | name: part.tool, | |
| 1598 | input: toolInput(part), | |
| 1599 | output: toolOutput(part), | |
| 1600 | completedMetadata: part.state.status === "completed" ? part.state.metadata : undefined, | |
| 1601 | }); | |
| 1598 | const input = toolInput(part); | |
| 1599 | const output = toolOutput(part); | |
| 1600 | const isNativeTool = part.tool === "code_search" || part.tool === "apply_patch"; | |
| 1601 | if (!isNativeTool && part.tool.includes("_") && turn.mcpServerNames === undefined) { | |
| 1602 | const serverNames = yield* OpenCodeRuntime.runOpenCodeSdk("mcp.status", (signal) => | |
| 1603 | client.mcp.status(undefined, { signal, throwOnError: true }), | |
| 1604 | ).pipe( | |
| 1605 | Effect.timeout("1 second"), | |
| 1606 | Effect.map((response) => Object.keys(response.data ?? {})), | |
| 1607 | Effect.catch(() => Effect.succeed(undefined)), | |
| 1608 | ); | |
| 1609 | if (serverNames !== undefined) turn.mcpServerNames = serverNames; | |
| 1610 | } | |
| 1611 | const matchingServers = turn.mcpServerNames?.filter( | |
| 1612 | (name) => | |
| 1613 | !isNativeTool && part.tool.startsWith(`${name.replace(/[^a-zA-Z0-9_-]/g, "_")}_`), | |
| 1614 | ); | |
| 1615 | const serverName = matchingServers?.length === 1 ? matchingServers[0] : undefined; | |
| 1616 | const presentation = | |
| 1617 | serverName === undefined | |
| 1618 | ? {} | |
| 1619 | : mcpToolPresentation({ | |
| 1620 | serverName, | |
| 1621 | toolName: part.tool.slice(serverName.replace(/[^a-zA-Z0-9_-]/g, "_").length + 1), | |
| 1622 | title: toolTitle(part) === part.tool ? undefined : toolTitle(part), | |
| 1623 | }); | |
| 1624 | const turnItem: OrchestrationV2TurnItem = matchingServers?.length | |
| 1625 | ? { | |
| 1626 | ...base, | |
| 1627 | type: "dynamic_tool", | |
| 1628 | ...presentation, | |
| 1629 | toolName: part.tool, | |
| 1630 | input, | |
| 1631 | ...(output === undefined ? {} : { output }), | |
| 1632 | } | |
| 1633 | : openCodeToolTurnItem(base, { | |
| 1634 | name: part.tool, | |
| 1635 | input, | |
| 1636 | output, | |
| 1637 | completedMetadata: | |
| 1638 | part.state.status === "completed" ? part.state.metadata : undefined, | |
| 1639 | }); | |
| 1602 | 1640 | yield* emitProviderEvent({ |
| 1603 | 1641 | type: "node.updated", |
| 1604 | 1642 | driver: OPENCODE_PROVIDER, |
apps/server/src/orchestration-v2/Adapters/PiAdapterV2.test.ts+41| ... | ... | @@ -1063,6 +1063,47 @@ describe("PiAdapterV2", () => { |
| 1063 | 1063 | }).pipe(Effect.scoped, Effect.provide(testLayer)), |
| 1064 | 1064 | ); |
| 1065 | 1065 | |
| 1066 | it.effect("presents explicitly namespaced MCP extension tools", () => | |
| 1067 | Effect.gen(function* () { | |
| 1068 | const fake = yield* makeFakePi; | |
| 1069 | const { runtime, takeEvent } = yield* openRuntime(fake); | |
| 1070 | const providerThread = yield* runtime.ensureThread({ | |
| 1071 | threadId: THREAD_ID, | |
| 1072 | modelSelection: modelSelection("default"), | |
| 1073 | runtimePolicy, | |
| 1074 | }); | |
| 1075 | yield* startTurn(runtime, providerThread); | |
| 1076 | yield* fake.takeRequest("prompt"); | |
| 1077 | yield* fake.emit({ type: "agent_start" }); | |
| 1078 | for (const type of ["tool_execution_start", "tool_execution_end"]) { | |
| 1079 | yield* fake.emit({ | |
| 1080 | type, | |
| 1081 | toolCallId: "weather-call", | |
| 1082 | toolName: "mcp__weather__get_weather", | |
| 1083 | args: { city: "Berlin" }, | |
| 1084 | result: { content: [{ type: "text", text: "Sunny" }] }, | |
| 1085 | isError: false, | |
| 1086 | }); | |
| 1087 | const event = yield* takeEvent( | |
| 1088 | (event) => event.type === "turn_item.updated" && event.turnItem.type === "dynamic_tool", | |
| 1089 | ); | |
| 1090 | if (event.type !== "turn_item.updated" || event.turnItem.type !== "dynamic_tool") | |
| 1091 | return yield* Effect.die("Expected an MCP tool item"); | |
| 1092 | assert.equal(event.turnItem.title, "get weather"); | |
| 1093 | assert.equal( | |
| 1094 | event.turnItem.status, | |
| 1095 | type === "tool_execution_start" ? "running" : "completed", | |
| 1096 | ); | |
| 1097 | assert.deepEqual(event.turnItem.toolSource, { | |
| 1098 | key: "mcp:weather", | |
| 1099 | name: "weather", | |
| 1100 | kind: "integration", | |
| 1101 | }); | |
| 1102 | assert.deepEqual(event.turnItem.input, { city: "Berlin" }); | |
| 1103 | } | |
| 1104 | }).pipe(Effect.scoped, Effect.provide(testLayer)), | |
| 1105 | ); | |
| 1106 | ||
| 1066 | 1107 | it.effect("observes official subagent results without inventing child threads", () => |
| 1067 | 1108 | Effect.gen(function* () { |
| 1068 | 1109 | const fake = yield* makeFakePi; |
apps/server/src/orchestration-v2/Adapters/PiAdapterV2.ts+2| ... | ... | @@ -61,6 +61,7 @@ import { ChildProcessSpawner } from "effect/unstable/process"; |
| 61 | 61 | |
| 62 | 62 | import { resolveAttachmentPath } from "../../attachmentStore.ts"; |
| 63 | 63 | import * as ServerConfig from "../../config.ts"; |
| 64 | import { mcpToolPresentation } from "../../provider/McpToolPresentation.ts"; | |
| 64 | 65 | import * as McpProviderSession from "../../mcp/McpProviderSession.ts"; |
| 65 | 66 | import { |
| 66 | 67 | expandPiSkillReference, |
| ... | ... | @@ -1016,6 +1017,7 @@ export function makePiAdapterV2( |
| 1016 | 1017 | ...shared, |
| 1017 | 1018 | title: toolName, |
| 1018 | 1019 | type: "dynamic_tool", |
| 1020 | ...mcpToolPresentation({ toolName }), | |
| 1019 | 1021 | toolName, |
| 1020 | 1022 | input: args ?? {}, |
| 1021 | 1023 | ...(outputText.length > 0 ? { output: outputText } : {}), |
apps/server/src/orchestration-v2/testkit/fixtures/claude_mcp_tool_presentation/output.ts+7-2| ... | ... | @@ -35,6 +35,7 @@ export function assertClaudeMcpToolPresentationOutput( |
| 35 | 35 | |
| 36 | 36 | assert.equal(scrape?.status, "completed"); |
| 37 | 37 | assert.equal(scrape?.title, "Firecrawl scrape"); |
| 38 | assert.deepEqual(scrape?.toolIcon, scrape?.toolSource?.icon); | |
| 38 | 39 | assert.deepEqual(scrape?.toolSource, { |
| 39 | 40 | key: "mcp:firecrawl", |
| 40 | 41 | name: "Firecrawl", |
| ... | ... | @@ -46,6 +47,10 @@ export function assertClaudeMcpToolPresentationOutput( |
| 46 | 47 | }); |
| 47 | 48 | |
| 48 | 49 | assert.equal(map?.status, "completed"); |
| 49 | assert.isNull(map?.title); | |
| 50 | assert.notProperty(map, "toolSource"); | |
| 50 | assert.equal(map?.title, "firecrawl map"); | |
| 51 | assert.deepEqual(map?.toolSource, { | |
| 52 | key: "mcp:firecrawl", | |
| 53 | name: "Firecrawl", | |
| 54 | kind: "integration", | |
| 55 | }); | |
| 51 | 56 | } |
apps/server/src/provider/CodexToolPresentation.ts+16-20| ... | ... | @@ -6,25 +6,18 @@ import type { |
| 6 | 6 | } from "@t3tools/contracts"; |
| 7 | 7 | import type * as EffectCodexSchema from "effect-codex-app-server/schema"; |
| 8 | 8 | |
| 9 | import { | |
| 10 | mcpToolPresentation as integrationToolPresentation, | |
| 11 | normalizeMcpHttpUrl as normalizedHttpUrl, | |
| 12 | normalizeMcpText as normalizedDisplayName, | |
| 13 | } from "./McpToolPresentation.ts"; | |
| 14 | ||
| 9 | 15 | type CodexLifecycleItem = EffectCodexSchema.V2ItemCompletedNotification["item"]; |
| 10 | 16 | |
| 11 | 17 | function asUnknownRecord(value: unknown): Record<string, unknown> | undefined { |
| 12 | 18 | return value && typeof value === "object" ? (value as Record<string, unknown>) : undefined; |
| 13 | 19 | } |
| 14 | 20 | |
| 15 | function normalizedHttpUrl(value: unknown): string | undefined { | |
| 16 | if (typeof value !== "string" || value.length > 4096) return undefined; | |
| 17 | try { | |
| 18 | const url = new URL(value); | |
| 19 | const href = url.href; | |
| 20 | return (url.protocol === "http:" || url.protocol === "https:") && href.length <= 4096 | |
| 21 | ? href | |
| 22 | : undefined; | |
| 23 | } catch { | |
| 24 | return undefined; | |
| 25 | } | |
| 26 | } | |
| 27 | ||
| 28 | 21 | function normalizedImageUrl(value: unknown): string | undefined { |
| 29 | 22 | if (typeof value !== "string" || value.length > 4096) return undefined; |
| 30 | 23 | try { |
| ... | ... | @@ -45,12 +38,6 @@ function normalizedAppId(value: unknown): string | undefined { |
| 45 | 38 | : undefined; |
| 46 | 39 | } |
| 47 | 40 | |
| 48 | function normalizedDisplayName(value: unknown): string | undefined { | |
| 49 | if (typeof value !== "string") return undefined; | |
| 50 | const displayName = value.trim().replace(/\s+/gu, " "); | |
| 51 | return displayName && displayName.length <= 160 ? displayName : undefined; | |
| 52 | } | |
| 53 | ||
| 54 | 41 | function normalizedSourceKeyPart(value: string): string { |
| 55 | 42 | return value.trim().toLowerCase(); |
| 56 | 43 | } |
| ... | ... | @@ -130,6 +117,7 @@ function themedLogoIcon( |
| 130 | 117 | } |
| 131 | 118 | |
| 132 | 119 | export interface McpToolPresentation { |
| 120 | readonly title?: string; | |
| 133 | 121 | readonly toolSurface?: "browser" | "computer"; |
| 134 | 122 | readonly toolIcon?: ToolActivityIcon; |
| 135 | 123 | readonly toolSource?: ToolActivitySource; |
| ... | ... | @@ -227,5 +215,13 @@ export function mcpToolPresentation( |
| 227 | 215 | }; |
| 228 | 216 | } |
| 229 | 217 | |
| 230 | return {}; | |
| 218 | return integrationToolPresentation({ | |
| 219 | serverName: appContext?.connectorId ?? item.server, | |
| 220 | toolName: item.tool, | |
| 221 | title: appContext?.actionName, | |
| 222 | serverDisplayName: appContext?.appName, | |
| 223 | source: sourceMetadata, | |
| 224 | iconUrl: sourceLogo?._tag === "themed-logo" ? sourceLogo.logoUrl : undefined, | |
| 225 | iconUrlDark: sourceLogo?._tag === "themed-logo" ? sourceLogo.logoUrlDark : undefined, | |
| 226 | }); | |
| 231 | 227 | } |
apps/server/src/provider/McpToolPresentation.test.ts created+103| ... | ... | @@ -0,0 +1,103 @@ |
| 1 | import { describe, expect, it } from "@effect/vitest"; | |
| 2 | import { mcpToolPresentation } from "./McpToolPresentation.ts"; | |
| 3 | ||
| 4 | describe("mcpToolPresentation", () => { | |
| 5 | it("uses supplied names and logos while retaining the server identity", () => { | |
| 6 | expect( | |
| 7 | mcpToolPresentation({ | |
| 8 | serverName: "firecrawl-local", | |
| 9 | toolName: "firecrawl_scrape", | |
| 10 | title: " Scrape\n page ", | |
| 11 | serverDisplayName: "Firecrawl", | |
| 12 | iconUrl: "https://example.com/icon.png", | |
| 13 | iconUrlDark: "https://example.com/dark.png", | |
| 14 | }), | |
| 15 | ).toEqual({ | |
| 16 | title: "Scrape page", | |
| 17 | toolIcon: { | |
| 18 | _tag: "themed-logo", | |
| 19 | logoUrl: "https://example.com/icon.png", | |
| 20 | logoUrlDark: "https://example.com/dark.png", | |
| 21 | }, | |
| 22 | toolSource: { | |
| 23 | key: "mcp:firecrawl-local", | |
| 24 | name: "Firecrawl", | |
| 25 | kind: "integration", | |
| 26 | icon: { | |
| 27 | _tag: "themed-logo", | |
| 28 | logoUrl: "https://example.com/icon.png", | |
| 29 | logoUrlDark: "https://example.com/dark.png", | |
| 30 | }, | |
| 31 | }, | |
| 32 | }); | |
| 33 | }); | |
| 34 | ||
| 35 | it("uses MCP result source metadata for the tool and its integration", () => { | |
| 36 | const presentation = mcpToolPresentation({ | |
| 37 | serverName: "firecrawl-local", | |
| 38 | toolName: "firecrawl_scrape", | |
| 39 | source: { | |
| 40 | name: "Firecrawl", | |
| 41 | logoUrl: "https://example.com/firecrawl.png", | |
| 42 | logoUrlDark: "https://example.com/firecrawl-dark.png", | |
| 43 | }, | |
| 44 | }); | |
| 45 | expect(presentation.toolSource?.name).toBe("Firecrawl"); | |
| 46 | expect(presentation.toolIcon).toEqual({ | |
| 47 | _tag: "themed-logo", | |
| 48 | logoUrl: "https://example.com/firecrawl.png", | |
| 49 | logoUrlDark: "https://example.com/firecrawl-dark.png", | |
| 50 | }); | |
| 51 | expect(presentation.toolSource?.icon).toEqual(presentation.toolIcon); | |
| 52 | }); | |
| 53 | ||
| 54 | it("uses readable fallback names without a guessed logo", () => { | |
| 55 | expect(mcpToolPresentation({ toolName: "mcp__my_server__get_weather" })).toEqual({ | |
| 56 | title: "get weather", | |
| 57 | toolSource: { key: "mcp:my_server", name: "my server", kind: "integration" }, | |
| 58 | }); | |
| 59 | expect(mcpToolPresentation({ toolName: "Read" })).toEqual({}); | |
| 60 | expect(mcpToolPresentation({ serverName: "t3-code", toolName: "delegate_task" })).toEqual({}); | |
| 61 | }); | |
| 62 | ||
| 63 | it.each([ | |
| 64 | "javascript:alert(1)", | |
| 65 | "file:///tmp/logo.png", | |
| 66 | "not a URL", | |
| 67 | "https://example.com/" + "x".repeat(4096), | |
| 68 | ])("ignores an invalid logo %s", (iconUrl) => { | |
| 69 | expect( | |
| 70 | mcpToolPresentation({ | |
| 71 | serverName: "weather", | |
| 72 | toolName: "get_weather", | |
| 73 | iconUrl, | |
| 74 | title: "x".repeat(161), | |
| 75 | }), | |
| 76 | ).toEqual({ | |
| 77 | title: "get weather", | |
| 78 | toolSource: { key: "mcp:weather", name: "weather", kind: "integration" }, | |
| 79 | }); | |
| 80 | }); | |
| 81 | ||
| 82 | it("rejects malformed and overlong names", () => { | |
| 83 | expect(mcpToolPresentation({ serverName: {}, toolName: 1, title: [] })).toEqual({}); | |
| 84 | expect(mcpToolPresentation({ serverName: "x".repeat(161), toolName: "get_weather" })).toEqual( | |
| 85 | {}, | |
| 86 | ); | |
| 87 | expect(mcpToolPresentation({ title: "Tool title" })).toEqual({ title: "Tool title" }); | |
| 88 | }); | |
| 89 | ||
| 90 | it.each([ | |
| 91 | null, | |
| 92 | "logo", | |
| 93 | { logoUrl: "file:///tmp/logo.png" }, | |
| 94 | { logoUrlDark: "https://example.com/dark.png" }, | |
| 95 | ])("ignores source metadata without a valid logo: %j", (source) => { | |
| 96 | expect(mcpToolPresentation({ serverName: "weather", toolName: "get_weather", source })).toEqual( | |
| 97 | { | |
| 98 | title: "get weather", | |
| 99 | toolSource: { key: "mcp:weather", name: "weather", kind: "integration" }, | |
| 100 | }, | |
| 101 | ); | |
| 102 | }); | |
| 103 | }); |
apps/server/src/provider/McpToolPresentation.ts created+70| ... | ... | @@ -0,0 +1,70 @@ |
| 1 | import type { ToolActivityIcon, ToolActivitySource } from "@t3tools/contracts"; | |
| 2 | import { resolveT3McpToolDefinition } from "@t3tools/shared/t3McpToolPresentation"; | |
| 3 | ||
| 4 | export function normalizeMcpText(value: unknown, maxLength = 160): string | undefined { | |
| 5 | if (typeof value !== "string") return undefined; | |
| 6 | const text = value.trim().replace(/\s+/gu, " "); | |
| 7 | return text.length > 0 && text.length <= maxLength ? text : undefined; | |
| 8 | } | |
| 9 | ||
| 10 | export function normalizeMcpHttpUrl(value: unknown): string | undefined { | |
| 11 | if (typeof value !== "string" || value.length > 4096) return undefined; | |
| 12 | try { | |
| 13 | const url = new URL(value); | |
| 14 | return (url.protocol === "https:" || url.protocol === "http:") && url.href.length <= 4096 | |
| 15 | ? url.href | |
| 16 | : undefined; | |
| 17 | } catch { | |
| 18 | return undefined; | |
| 19 | } | |
| 20 | } | |
| 21 | ||
| 22 | export function mcpToolPresentation(input: { | |
| 23 | readonly toolName?: unknown; | |
| 24 | readonly serverName?: unknown; | |
| 25 | readonly title?: unknown; | |
| 26 | readonly serverDisplayName?: unknown; | |
| 27 | readonly iconUrl?: unknown; | |
| 28 | readonly iconUrlDark?: unknown; | |
| 29 | readonly source?: unknown; | |
| 30 | }): { | |
| 31 | readonly title?: string; | |
| 32 | readonly toolIcon?: ToolActivityIcon; | |
| 33 | readonly toolSource?: ToolActivitySource; | |
| 34 | } { | |
| 35 | const source = | |
| 36 | typeof input.source === "object" && input.source !== null ? input.source : undefined; | |
| 37 | const qualified = | |
| 38 | typeof input.toolName === "string" ? /^mcp__(.+?)__(.+)$/i.exec(input.toolName) : null; | |
| 39 | const server = normalizeMcpText(input.serverName ?? qualified?.[1] ?? input.serverDisplayName); | |
| 40 | const tool = normalizeMcpText(qualified?.[2] ?? input.toolName); | |
| 41 | if (server && tool && resolveT3McpToolDefinition(`${server}.${tool}`)) return {}; | |
| 42 | const title = | |
| 43 | normalizeMcpText(input.title) ?? | |
| 44 | (server && tool ? normalizeMcpText(tool.replace(/[_-]+/gu, " ")) : undefined); | |
| 45 | if (!server) return title ? { title } : {}; | |
| 46 | const name = | |
| 47 | normalizeMcpText(input.serverDisplayName) ?? | |
| 48 | normalizeMcpText(source && Reflect.get(source, "name")) ?? | |
| 49 | normalizeMcpText(server.replace(/[_-]+/gu, " ")) ?? | |
| 50 | server; | |
| 51 | const logoUrl = | |
| 52 | normalizeMcpHttpUrl(input.iconUrl) ?? | |
| 53 | normalizeMcpHttpUrl(source && Reflect.get(source, "logoUrl")); | |
| 54 | const logoUrlDark = | |
| 55 | normalizeMcpHttpUrl(input.iconUrlDark) ?? | |
| 56 | normalizeMcpHttpUrl(source && Reflect.get(source, "logoUrlDark")); | |
| 57 | const icon = logoUrl | |
| 58 | ? { _tag: "themed-logo" as const, logoUrl, ...(logoUrlDark ? { logoUrlDark } : {}) } | |
| 59 | : undefined; | |
| 60 | return { | |
| 61 | ...(title ? { title } : {}), | |
| 62 | ...(icon ? { toolIcon: icon } : {}), | |
| 63 | toolSource: { | |
| 64 | key: `mcp:${server.toLowerCase()}`, | |
| 65 | name, | |
| 66 | kind: "integration", | |
| 67 | ...(icon ? { icon } : {}), | |
| 68 | }, | |
| 69 | }; | |
| 70 | } |
apps/server/src/provider/acp/AcpRuntimeModel.test.ts+42-4| ... | ... | @@ -1539,9 +1539,44 @@ describe("extractMcpToolCallIdentity", () => { |
| 1539 | 1539 | }); |
| 1540 | 1540 | }); |
| 1541 | 1541 | |
| 1542 | it("does not brand tools whose meta asserts a foreign server", () => { | |
| 1543 | // The foreign assertion vetoes every loose source, including a title | |
| 1544 | // that would otherwise match a T3 convention. | |
| 1542 | it.each([ | |
| 1543 | { _meta: { claudeCode: { toolName: "mcp__weather__get_weather" } } }, | |
| 1544 | { _meta: { toolName: "mcp::weather::get_weather", serverId: "weather" } }, | |
| 1545 | ])("recovers external MCP identity from provider metadata", (metadata) => { | |
| 1546 | const toolCall = toolCallFromUpdate({ | |
| 1547 | sessionUpdate: "tool_call", | |
| 1548 | toolCallId: "weather-call", | |
| 1549 | title: "Checking the forecast", | |
| 1550 | status: "in_progress", | |
| 1551 | ...metadata, | |
| 1552 | }); | |
| 1553 | expect(extractMcpToolCallIdentity(toolCall)).toEqual({ | |
| 1554 | server: "weather", | |
| 1555 | tool: "get_weather", | |
| 1556 | }); | |
| 1557 | }); | |
| 1558 | ||
| 1559 | it.each([ | |
| 1560 | { kind: "execute", toolName: "developer__shell", extensionName: "developer" }, | |
| 1561 | { kind: "edit", toolName: "developer__edit", extensionName: "developer" }, | |
| 1562 | { kind: "other", toolName: "weather__get_weather", extensionName: "weather" }, | |
| 1563 | ] as const)( | |
| 1564 | "leaves goose $toolName unclassified so built-ins keep their projection", | |
| 1565 | ({ kind, toolName, extensionName }) => { | |
| 1566 | // goose tags built-in extensions exactly like user MCP servers. | |
| 1567 | const toolCall = toolCallFromUpdate({ | |
| 1568 | sessionUpdate: "tool_call", | |
| 1569 | toolCallId: "goose-builtin", | |
| 1570 | kind, | |
| 1571 | title: "developer: shell", | |
| 1572 | status: "in_progress", | |
| 1573 | _meta: { goose: { toolCall: { toolName, extensionName } } }, | |
| 1574 | }); | |
| 1575 | expect(extractMcpToolCallIdentity(toolCall)).toBeUndefined(); | |
| 1576 | }, | |
| 1577 | ); | |
| 1578 | ||
| 1579 | it("uses the asserted server instead of a misleading T3 title", () => { | |
| 1545 | 1580 | const toolCall = toolCallFromUpdate({ |
| 1546 | 1581 | sessionUpdate: "tool_call", |
| 1547 | 1582 | toolCallId: "foreign-1", |
| ... | ... | @@ -1551,7 +1586,10 @@ describe("extractMcpToolCallIdentity", () => { |
| 1551 | 1586 | _meta: { toolName: "delegate_task", serverId: "other-orchestrator" }, |
| 1552 | 1587 | }); |
| 1553 | 1588 | |
| 1554 | expect(extractMcpToolCallIdentity(toolCall)).toBeUndefined(); | |
| 1589 | expect(extractMcpToolCallIdentity(toolCall)).toEqual({ | |
| 1590 | server: "other-orchestrator", | |
| 1591 | tool: "delegate_task", | |
| 1592 | }); | |
| 1555 | 1593 | }); |
| 1556 | 1594 | |
| 1557 | 1595 | it("does not brand path-like or unknown-tool titles", () => { |
apps/server/src/provider/acp/AcpRuntimeModel.ts+14-6| ... | ... | @@ -1134,9 +1134,7 @@ export function extractMcpToolCallIdentity( |
| 1134 | 1134 | // (goose, qwen, claude-acp) or only through the namespaced function name |
| 1135 | 1135 | // in the title. The verbatim wire title survives merges even when a later |
| 1136 | 1136 | // titleless or LLM-enriched update replaces the presentation title, so |
| 1137 | // match those rather than the summarized state title. Name-derived matches | |
| 1138 | // are gated on the known T3 tool inventory so path-like titles (for | |
| 1139 | // example "t3-code/README.md") never brand. | |
| 1137 | // match those rather than the summarized state title. | |
| 1140 | 1138 | const claudeCode = isRecord(meta?.claudeCode) ? meta.claudeCode : undefined; |
| 1141 | 1139 | const gooseToolCall = isRecord(meta?.goose) |
| 1142 | 1140 | ? isRecord(meta.goose.toolCall) |
| ... | ... | @@ -1160,15 +1158,22 @@ export function extractMcpToolCallIdentity( |
| 1160 | 1158 | } |
| 1161 | 1159 | } |
| 1162 | 1160 | } |
| 1163 | // A present-but-foreign origin assertion marks the whole call as another | |
| 1164 | // server's MCP call, so no loose name matching (meta or title) may brand it. | |
| 1165 | 1161 | const gooseExtension = |
| 1166 | 1162 | typeof gooseToolCall?.extensionName === "string" ? gooseToolCall.extensionName.trim() : ""; |
| 1167 | 1163 | const assertsForeignOrigin = |
| 1168 | 1164 | (metaServerId.length > 0 && !/^t3[-_ ]?code$/i.test(metaServerId)) || |
| 1169 | 1165 | (gooseExtension.length > 0 && !/^t3[-_ ]?code$/i.test(gooseExtension)); |
| 1166 | // A foreign origin never brands as T3. qwen's serverId marks a real MCP | |
| 1167 | // server, but goose reports its built-in extensions (developer__shell, | |
| 1168 | // edits) the same way as user MCP servers, so goose stays unclassified and | |
| 1169 | // keeps its command and file-change projections. | |
| 1170 | 1170 | if (assertsForeignOrigin) { |
| 1171 | return undefined; | |
| 1171 | if (metaServerId.length === 0 || metaToolName.length === 0) return undefined; | |
| 1172 | const prefix = [`mcp__${metaServerId}__`, `mcp::${metaServerId}::`].find((prefix) => | |
| 1173 | metaToolName.startsWith(prefix), | |
| 1174 | ); | |
| 1175 | const tool = prefix === undefined ? metaToolName : metaToolName.slice(prefix.length); | |
| 1176 | return tool ? { server: metaServerId, tool } : undefined; | |
| 1172 | 1177 | } |
| 1173 | 1178 | const candidates = [ |
| 1174 | 1179 | meta?.toolName, |
| ... | ... | @@ -1178,6 +1183,9 @@ export function extractMcpToolCallIdentity( |
| 1178 | 1183 | ].filter((value): value is string => typeof value === "string"); |
| 1179 | 1184 | for (const candidate of candidates) { |
| 1180 | 1185 | const trimmed = candidate.trim(); |
| 1186 | const qualified = /^mcp__(.+?)__(.+)$/i.exec(trimmed); | |
| 1187 | if (qualified?.[1] && qualified[2] && !/^t3[-_ ]?code$/i.test(qualified[1])) | |
| 1188 | return { server: qualified[1], tool: qualified[2] }; | |
| 1181 | 1189 | const match = |
| 1182 | 1190 | T3_MCP_TITLE_CALL.exec(trimmed) ?? |
| 1183 | 1191 | T3_MCP_TITLE_SUFFIX_CALL.exec(trimmed) ?? |