History

Trim namespace metadata cost

Rhys Sullivan committed 7 months ago

Commit 1f82a221 parent aa4c8194 files · +223 −28Browse files

Unified · Split

4 files
  1. .changeset/five-plums-wave.md +5 −0
  2. packages/hosts/mcp/src/index.ts +39 −25
  3. packages/platform/sdk/src/runtime/execution/scope/source-catalog.test.ts +127 −0
  4. packages/platform/sdk/src/runtime/execution/scope/source-catalog.ts +52 −3
.changeset/five-plums-wave.md+5−0
@@ -0,0 +1,5 @@
1---
2"executor": patch
3---
4
5Avoid generating source tool type previews when building namespace metadata, and cap MCP execute help examples to five tools per source.
packages/hosts/mcp/src/index.ts+39−25
@@ -3,13 +3,12 @@ import type { · 2 unmodified lines
3} from "@executor/platform-sdk/schema";3} from "@executor/platform-sdk/schema";
4import {4import {
5 EXECUTOR_SOURCES_ADD_HELP_LINES,5 EXECUTOR_SOURCES_ADD_HELP_LINES,
6 RuntimeExecutionResolverService,6 RuntimeSourceCatalogStoreService,
7 createExecution,7 createExecution,
8 getExecution,8 getExecution,
9 resumeExecution,9 resumeExecution,
10 type ExecutorRuntime,10 type ExecutorRuntime,
11} from "@executor/platform-sdk/runtime";11} from "@executor/platform-sdk/runtime";
12import { ExecutionIdSchema } from "@executor/platform-sdk/schema";
13import * as Effect from "effect/Effect";12import * as Effect from "effect/Effect";
14import * as Cause from "effect/Cause";13import * as Cause from "effect/Cause";
15import * as Exit from "effect/Exit";14import * as Exit from "effect/Exit";
@@ -121,19 +120,33 @@ const interactionModeForServer = (server: McpServer): "live_form" | "detach" => · 105 unmodified lines
121 supportsManagedElicitation(server) ? "live_form" : "detach";120 supportsManagedElicitation(server) ? "live_form" : "detach";
122 121
123type CatalogLike = {122type CatalogLike = {
124 listNamespaces: (input: { limit: number }) => Effect.Effect<123 projected: {
125 ReadonlyArray<{ namespace: string; displayName?: string }>,124 toolDescriptors: Record<string, { toolPath: readonly string[] }>;
126 unknown125 };
127 >;126 source: {
127 name: string;
128 enabled: boolean;
129 status: string;
130 };
128};131};
129 132
130const buildExecuteWorkflowText = (namespaces: readonly string[] = []): string =>133const executeDescriptionToolsPerSource = 5;
134
135const buildExecuteWorkflowText = (
136 sourceToolExamples: ReadonlyArray<{
137 sourceName: string;
138 toolPaths: readonly string[];
139 }> = [],
140): string =>
131 [141 [
132 "Execute TypeScript in sandbox; call tools via discovery workflow.",142 "Execute TypeScript in sandbox; call tools via discovery workflow.",
133 ...(namespaces.length > 0143 ...(sourceToolExamples.length > 0
134 ? [144 ? [
135 "Available namespaces:",145 "Available source tool examples:",
136 ...namespaces.map((namespace) => `- ${namespace}`),146 ...sourceToolExamples.flatMap((source) => [
147 `${source.sourceName}:`,
148 ...source.toolPaths.map((toolPath) => `- ${toolPath}`),
149 ]),
137 ]150 ]
138 : []),151 : []),
139 "Workflow:",152 "Workflow:",
@@ -152,28 +165,29 @@ const loadExecuteDescription = (runtime: ExecutorRuntime): Promise<string> => · 12 unmodified lines
152 runControlPlane(165 runControlPlane(
153 runtime,166 runtime,
154 Effect.gen(function* () {167 Effect.gen(function* () {
155 const resolveExecutionEnvironment = yield* RuntimeExecutionResolverService;168 const sourceCatalogStore = yield* RuntimeSourceCatalogStoreService;
156 const environment = yield* resolveExecutionEnvironment({169 const catalogs = yield* sourceCatalogStore.loadWorkspaceSourceCatalogs({
157 scopeId: runtime.localInstallation.scopeId,170 scopeId: runtime.localInstallation.scopeId,
158 actorScopeId: runtime.localInstallation.actorScopeId,171 actorScopeId: runtime.localInstallation.actorScopeId,
159 executionId: ExecutionIdSchema.make("exec_mcp_help"),
160 });172 });
161 173
162 const catalog = environment.catalog as CatalogLike | undefined;174 const sourceToolExamples = (catalogs as ReadonlyArray<CatalogLike>)
163 if (!catalog) {175 .filter((catalog) => catalog.source.enabled && catalog.source.status === "connected")
176 .map((catalog) => ({
177 sourceName: catalog.source.name,
178 toolPaths: Object.values(catalog.projected.toolDescriptors)
179 .map((descriptor) => descriptor.toolPath.join("."))
180 .filter((toolPath) => toolPath.length > 0)
181 .sort((left, right) => left.localeCompare(right))
182 .slice(0, executeDescriptionToolsPerSource),
183 }))
184 .filter((catalog) => catalog.toolPaths.length > 0);
185
186 if (sourceToolExamples.length === 0) {
164 return defaultExecuteDescription;187 return defaultExecuteDescription;
165 }188 }
166 189
167 const namespaces = yield* catalog.listNamespaces({ limit: 200 }).pipe(190 return buildExecuteWorkflowText(sourceToolExamples);
168 Effect.map((items) =>
169 items.length > 0
170 ? items.map((item) => item.displayName ?? item.namespace)
171 : ["none discovered yet"],
172 ),
173 Effect.catchAll(() => Effect.succeed(["none discovered yet"])),
174 );
175
176 return buildExecuteWorkflowText(namespaces);
177 }).pipe(Effect.catchAll(() => Effect.succeed(defaultExecuteDescription))),191 }).pipe(Effect.catchAll(() => Effect.succeed(defaultExecuteDescription))),
178 );192 );
179 193
packages/platform/sdk/src/runtime/execution/scope/source-catalog.test.ts+127−0
@@ -0,0 +1,127 @@
1import {
2 describe,
3 expect,
4 it,
5} from "@effect/vitest";
6import * as Effect from "effect/Effect";
7
8import {
9 createScopeSourceCatalog,
10} from "./source-catalog";
11
12const makeLoadedCatalog = (input: {
13 sourceId: string;
14 toolPaths: ReadonlyArray<string>;
15 enabled?: boolean;
16 status?: "connected" | "error";
17}) => ({
18 source: {
19 id: input.sourceId,
20 scopeId: "ws_test",
21 name: input.sourceId,
22 kind: "openapi",
23 endpoint: `https://example.test/${input.sourceId}`,
24 status: input.status ?? "connected",
25 enabled: input.enabled ?? true,
26 namespace: null,
27 bindingVersion: 1,
28 binding: {},
29 importAuthPolicy: "reuse_runtime",
30 importAuth: {
31 kind: "none",
32 },
33 auth: {
34 kind: "none",
35 },
36 sourceHash: null,
37 lastError: null,
38 createdAt: 0,
39 updatedAt: 0,
40 },
41 projected: {
42 toolDescriptors: Object.fromEntries(
43 input.toolPaths.map((path, index) => [
44 `cap_${index}`,
45 {
46 toolPath: path.split("."),
47 },
48 ]),
49 ),
50 },
51}) as any;
52
53const noopScopeConfigStore = {
54 load: () => Effect.die("unexpected config load"),
55 writeProject: () => Effect.void,
56 resolveRelativePath: (path: string) => Effect.succeed(path),
57} as any;
58
59const noopScopeStateStore = {
60 load: () => Effect.die("unexpected scope state load"),
61 write: () => Effect.void,
62} as any;
63
64const noopSourceArtifactStore = {
65 build: () => Effect.die("unexpected source artifact build"),
66 read: () => Effect.die("unexpected source artifact read"),
67 write: () => Effect.void,
68 remove: () => Effect.void,
69} as any;
70
71describe("scope source catalog", () => {
72 it.effect("lists namespaces from projected tool paths without loading the tool index", () =>
73 Effect.gen(function* () {
74 const catalog = createScopeSourceCatalog({
75 scopeId: "ws_test" as any,
76 actorScopeId: "acc_test" as any,
77 sourceCatalogStore: {
78 loadWorkspaceSourceCatalogs: () => Effect.succeed([
79 makeLoadedCatalog({
80 sourceId: "github",
81 toolPaths: [
82 "github.repos.get",
83 "github.repos.list",
84 "github.issues.get",
85 ],
86 }),
87 makeLoadedCatalog({
88 sourceId: "linear",
89 toolPaths: [
90 "linear.issue.create",
91 ],
92 }),
93 makeLoadedCatalog({
94 sourceId: "disabled",
95 toolPaths: [
96 "disabled.admin.delete",
97 ],
98 enabled: false,
99 }),
100 ]),
101 loadWorkspaceSourceCatalogToolIndex: () => Effect.die("unexpected tool index load"),
102 loadWorkspaceSourceCatalogToolByPath: () => Effect.die("unexpected tool lookup"),
103 } as any,
104 scopeConfigStore: noopScopeConfigStore,
105 scopeStateStore: noopScopeStateStore,
106 sourceArtifactStore: noopSourceArtifactStore,
107 runtimeLocalScope: null,
108 });
109
110 const namespaces = yield* catalog.listNamespaces({ limit: 10 });
111
112 expect(namespaces).toEqual([
113 {
114 namespace: "github.issues",
115 toolCount: 1,
116 },
117 {
118 namespace: "github.repos",
119 toolCount: 2,
120 },
121 {
122 namespace: "linear.issue",
123 toolCount: 1,
124 },
125 ]);
126 }));
127});
packages/platform/sdk/src/runtime/execution/scope/source-catalog.ts+52−3
@@ -1,6 +1,7 @@
1import {1import {
2 createToolCatalogFromEntries,2 createToolCatalogFromEntries,
3 type ToolCatalog,3 type ToolCatalog,
4 type ToolNamespace,
4} from "@executor/codemode-core";5} from "@executor/codemode-core";
5import type {6import type {
6 ScopeId,7 ScopeId,
@@ -80,6 +81,49 @@ const hasSubstringMatch = (value: string, queryToken: string): boolean => { · 73 unmodified lines
80const queryTokenWeight = (token: string): number =>81const queryTokenWeight = (token: string): number =>
81 LOW_SIGNAL_QUERY_TOKENS.has(token) ? 0.25 : 1;82 LOW_SIGNAL_QUERY_TOKENS.has(token) ? 0.25 : 1;
82 83
84const namespaceFromPath = (path: string): string => {
85 const [first, second] = path.split(".");
86 return second ? `${first}.${second}` : first;
87};
88
89const sortNamespaces = (namespaces: Iterable<ToolNamespace>): ToolNamespace[] =>
90 [...namespaces].sort((left, right) => left.namespace.localeCompare(right.namespace));
91
92const loadWorkspaceCatalogNamespaces = (input: {
93 scopeId: Source["scopeId"];
94 actorScopeId: ScopeId;
95 sourceCatalogStore: Effect.Effect.Success<typeof RuntimeSourceCatalogStoreService>;
96}): Effect.Effect<
97 readonly ToolNamespace[],
98 Error,
99 ScopeStorageServices
100> =>
101 Effect.gen(function* () {
102 const catalogs = yield* input.sourceCatalogStore.loadWorkspaceSourceCatalogs({
103 scopeId: input.scopeId,
104 actorScopeId: input.actorScopeId,
105 });
106
107 const namespaces = new Map<string, ToolNamespace>();
108
109 for (const catalog of catalogs) {
110 if (!catalog.source.enabled || catalog.source.status !== "connected") {
111 continue;
112 }
113
114 for (const descriptor of Object.values(catalog.projected.toolDescriptors)) {
115 const namespace = namespaceFromPath(descriptor.toolPath.join("."));
116 const existing = namespaces.get(namespace);
117 namespaces.set(namespace, {
118 namespace,
119 toolCount: (existing?.toolCount ?? 0) + 1,
120 });
121 }
122 }
123
124 return sortNamespaces(namespaces.values());
125 });
126
83export const loadWorkspaceCatalogTools = (input: {127export const loadWorkspaceCatalogTools = (input: {
84 scopeId: Source["scopeId"];128 scopeId: Source["scopeId"];
85 actorScopeId: ScopeId;129 actorScopeId: ScopeId;
@@ -291,9 +335,14 @@ export const createScopeSourceCatalog = (input: { · 205 unmodified lines
291 return {335 return {
292 listNamespaces: ({ limit }) =>336 listNamespaces: ({ limit }) =>
293 provideRuntimeLocalScope(337 provideRuntimeLocalScope(
294 Effect.flatMap(createSharedCatalog(false), (catalog) =>338 provideWorkspaceStorage(Effect.map(
295 catalog.listNamespaces({ limit }),339 loadWorkspaceCatalogNamespaces({
296 ),340 scopeId: input.scopeId,
341 actorScopeId: input.actorScopeId,
342 sourceCatalogStore: input.sourceCatalogStore,
343 }),
344 (namespaces) => namespaces.slice(0, limit),
345 )),
297 input.runtimeLocalScope,346 input.runtimeLocalScope,
298 ),347 ),
299 348