perf(execution): drop executor.tools.list from buildExecuteDescription in favor of sources list
Unified · Split
| @@ -227,11 +227,11 @@ export class McpSessionDO extends DurableObject { · 226 unmodified lines | ||
| 227 | 227 | sessionMeta.organizationId, |
| 228 | 228 | sessionMeta.organizationName, |
| 229 | 229 | ); |
| 230 | − // Build the description here so the two postgres queries it runs | |
| 231 | − // (`executor.sources.list` + `executor.tools.list`) land as | |
| 232 | − // children of `McpSessionDO.createRuntime`. host-mcp would | |
| 233 | − // otherwise call `Effect.runPromise(engine.getDescription)` at | |
| 234 | − // its async MCP-SDK boundary and orphan those sub-spans. | |
| 230 | + // Build the description here so the postgres query it runs | |
| 231 | + // (`executor.sources.list`) lands as a child of | |
| 232 | + // `McpSessionDO.createRuntime`. host-mcp would otherwise call | |
| 233 | + // `Effect.runPromise(engine.getDescription)` at its async | |
| 234 | + // MCP-SDK boundary and orphan the sub-span. | |
| 235 | 235 | const description = yield* buildExecuteDescription(executor); |
| 236 | 236 | const mcpServer = yield* Effect.promise(() => |
| 237 | 237 | createExecutorMcpServer({ engine, description }), |
| @@ -0,0 +1,109 @@ | ||
| 1 | +import { describe, expect, it } from "@effect/vitest"; | |
| 2 | +import { Effect } from "effect"; | |
| 3 | + | |
| 4 | +import { createExecutor, definePlugin, makeTestConfig } from "@executor/sdk"; | |
| 5 | + | |
| 6 | +import { buildExecuteDescription } from "./description"; | |
| 7 | + | |
| 8 | +const EmptyInputSchema = { | |
| 9 | + type: "object", | |
| 10 | + properties: {}, | |
| 11 | + additionalProperties: false, | |
| 12 | +} as const; | |
| 13 | + | |
| 14 | +// Two plugins registering static sources whose ids are distinct from their | |
| 15 | +// pluginIds/names. If `buildExecuteDescription` ever renders the wrong field | |
| 16 | +// (e.g. pluginId, an internal UUID, or the source name), these assertions | |
| 17 | +// fail — which is the class of bug a hand-rolled fake `Executor` would miss. | |
| 18 | +const githubPlugin = definePlugin(() => ({ | |
| 19 | + id: "github-plugin" as const, | |
| 20 | + storage: () => ({}), | |
| 21 | + staticSources: () => [ | |
| 22 | + { | |
| 23 | + id: "github", | |
| 24 | + kind: "in-memory", | |
| 25 | + name: "GitHub", | |
| 26 | + tools: [ | |
| 27 | + { | |
| 28 | + name: "noop", | |
| 29 | + description: "noop", | |
| 30 | + inputSchema: EmptyInputSchema, | |
| 31 | + handler: () => Effect.succeed(null), | |
| 32 | + }, | |
| 33 | + ], | |
| 34 | + }, | |
| 35 | + ], | |
| 36 | +})); | |
| 37 | + | |
| 38 | +const slackPlugin = definePlugin(() => ({ | |
| 39 | + id: "slack-plugin" as const, | |
| 40 | + storage: () => ({}), | |
| 41 | + staticSources: () => [ | |
| 42 | + { | |
| 43 | + id: "slack", | |
| 44 | + kind: "in-memory", | |
| 45 | + name: "Slack Workspace", | |
| 46 | + tools: [ | |
| 47 | + { | |
| 48 | + name: "noop", | |
| 49 | + description: "noop", | |
| 50 | + inputSchema: EmptyInputSchema, | |
| 51 | + handler: () => Effect.succeed(null), | |
| 52 | + }, | |
| 53 | + ], | |
| 54 | + }, | |
| 55 | + ], | |
| 56 | +})); | |
| 57 | + | |
| 58 | +describe("buildExecuteDescription", () => { | |
| 59 | + it.effect( | |
| 60 | + "renders real source ids as namespaces (sorted) through the real executor flow", | |
| 61 | + () => | |
| 62 | + Effect.gen(function* () { | |
| 63 | + // Intentionally register in non-alphabetical order — the formatter | |
| 64 | + // is expected to sort by source id. | |
| 65 | + const executor = yield* createExecutor( | |
| 66 | + makeTestConfig({ plugins: [slackPlugin(), githubPlugin()] as const }), | |
| 67 | + ); | |
| 68 | + | |
| 69 | + const description = yield* buildExecuteDescription(executor); | |
| 70 | + | |
| 71 | + // Stable anchor from the workflow preamble. | |
| 72 | + expect(description).toContain( | |
| 73 | + "Execute TypeScript in a sandboxed runtime", | |
| 74 | + ); | |
| 75 | + // The namespaces section header. | |
| 76 | + expect(description).toContain("## Available namespaces"); | |
| 77 | + // Each source renders with its ACTUAL id (not pluginId / name / UUID). | |
| 78 | + expect(description).toContain("`github` — GitHub"); | |
| 79 | + expect(description).toContain("`slack` — Slack Workspace"); | |
| 80 | + // And the plugin ids must NOT leak into the namespace list. | |
| 81 | + expect(description).not.toContain("`github-plugin`"); | |
| 82 | + expect(description).not.toContain("`slack-plugin`"); | |
| 83 | + | |
| 84 | + // Sort order: `github` before `slack`. | |
| 85 | + const githubIdx = description.indexOf("`github`"); | |
| 86 | + const slackIdx = description.indexOf("`slack`"); | |
| 87 | + expect(githubIdx).toBeGreaterThan(-1); | |
| 88 | + expect(slackIdx).toBeGreaterThan(-1); | |
| 89 | + expect(githubIdx).toBeLessThan(slackIdx); | |
| 90 | + }), | |
| 91 | + ); | |
| 92 | + | |
| 93 | + it.effect( | |
| 94 | + "omits the Available namespaces section when no plugins register sources", | |
| 95 | + () => | |
| 96 | + Effect.gen(function* () { | |
| 97 | + const executor = yield* createExecutor( | |
| 98 | + makeTestConfig({ plugins: [] as const }), | |
| 99 | + ); | |
| 100 | + | |
| 101 | + const description = yield* buildExecuteDescription(executor); | |
| 102 | + | |
| 103 | + expect(description).toContain( | |
| 104 | + "Execute TypeScript in a sandboxed runtime", | |
| 105 | + ); | |
| 106 | + expect(description).not.toContain("## Available namespaces"); | |
| 107 | + }), | |
| 108 | + ); | |
| 109 | +}); | |
| @@ -1,5 +1,5 @@ | ||
| 1 | 1 | import { Effect } from "effect"; |
| 2 | −import type { Executor, Tool, Source } from "@executor/sdk"; | |
| 2 | +import type { Executor, Source } from "@executor/sdk"; | |
| 3 | 3 | |
| 4 | 4 | /** |
| 5 | 5 | * Builds a tool description dynamically. |
| @@ -13,17 +13,11 @@ export const buildExecuteDescription = (executor: Executor): Effect.Effect<strin · 7 unmodified lines | ||
| 13 | 13 | const sources: readonly Source[] = yield* executor.sources |
| 14 | 14 | .list() |
| 15 | 15 | .pipe(Effect.orDie, Effect.withSpan("executor.sources.list")); |
| 16 | − const tools: readonly Tool[] = yield* executor.tools | |
| 17 | − .list() | |
| 18 | − .pipe(Effect.orDie, Effect.withSpan("executor.tools.list")); | |
| 19 | − | |
| 20 | − const namespaces = new Set<string>(); | |
| 21 | − for (const tool of tools) namespaces.add(tool.sourceId); | |
| 22 | 16 | |
| 23 | − return formatDescription([...namespaces], sources); | |
| 17 | + return formatDescription(sources); | |
| 24 | 18 | }).pipe(Effect.withSpan("buildExecuteDescription")); |
| 25 | 19 | |
| 26 | −const formatDescription = (namespaces: readonly string[], sources: readonly Source[]): string => { | |
| 20 | +const formatDescription = (sources: readonly Source[]): string => { | |
| 27 | 21 | const lines: string[] = [ |
| 28 | 22 | "Execute TypeScript in a sandboxed runtime with access to configured API tools.", |
| 29 | 23 | "", |
| @@ -50,15 +44,14 @@ const formatDescription = (namespaces: readonly string[], sources: readonly Sour · 20 unmodified lines | ||
| 50 | 44 | "- If execution pauses for interaction, resume it with the returned `resumePayload`.", |
| 51 | 45 | ]; |
| 52 | 46 | |
| 53 | − if (namespaces.length > 0) { | |
| 47 | + if (sources.length > 0) { | |
| 54 | 48 | lines.push(""); |
| 55 | 49 | lines.push("## Available namespaces"); |
| 56 | 50 | lines.push(""); |
| 57 | − const sorted = [...namespaces].sort(); | |
| 58 | − for (const ns of sorted) { | |
| 59 | − const source = sources.find((s) => s.id === ns); | |
| 60 | − const label = source?.name ?? ns; | |
| 61 | − lines.push(`- \`${ns}\`${label !== ns ? ` — ${label}` : ""}`); | |
| 51 | + const sorted = [...sources].sort((a, b) => a.id.localeCompare(b.id)); | |
| 52 | + for (const source of sorted) { | |
| 53 | + const label = source.name; | |
| 54 | + lines.push(`- \`${source.id}\`${label !== source.id ? ` — ${label}` : ""}`); | |
| 62 | 55 | } |
| 63 | 56 | } |
| 64 | 57 | |