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+
5+Avoid 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
33 } from "@executor/platform-sdk/schema";
44 import {
55 EXECUTOR_SOURCES_ADD_HELP_LINES,
6− RuntimeExecutionResolverService,
6+ RuntimeSourceCatalogStoreService,
77 createExecution,
88 getExecution,
99 resumeExecution,
1010 type ExecutorRuntime,
1111 } from "@executor/platform-sdk/runtime";
12−import { ExecutionIdSchema } from "@executor/platform-sdk/schema";
1312 import * as Effect from "effect/Effect";
1413 import * as Cause from "effect/Cause";
1514 import * as Exit from "effect/Exit";
@@ -121,19 +120,33 @@ const interactionModeForServer = (server: McpServer): "live_form" | "detach" => · 105 unmodified lines
121120 supportsManagedElicitation(server) ? "live_form" : "detach";
122121
123122 type CatalogLike = {
124− listNamespaces: (input: { limit: number }) => Effect.Effect<
125− ReadonlyArray<{ namespace: string; displayName?: string }>,
126− unknown
127− >;
123+ projected: {
124+ toolDescriptors: Record<string, { toolPath: readonly string[] }>;
125+ };
126+ source: {
127+ name: string;
128+ enabled: boolean;
129+ status: string;
130+ };
128131 };
129132
130−const buildExecuteWorkflowText = (namespaces: readonly string[] = []): string =>
133+const executeDescriptionToolsPerSource = 5;
134+
135+const buildExecuteWorkflowText = (
136+ sourceToolExamples: ReadonlyArray<{
137+ sourceName: string;
138+ toolPaths: readonly string[];
139+ }> = [],
140+): string =>
131141 [
132142 "Execute TypeScript in sandbox; call tools via discovery workflow.",
133− ...(namespaces.length > 0
143+ ...(sourceToolExamples.length > 0
134144 ? [
135− "Available namespaces:",
136− ...namespaces.map((namespace) => `- ${namespace}`),
145+ "Available source tool examples:",
146+ ...sourceToolExamples.flatMap((source) => [
147+ `${source.sourceName}:`,
148+ ...source.toolPaths.map((toolPath) => `- ${toolPath}`),
149+ ]),
137150 ]
138151 : []),
139152 "Workflow:",
@@ -152,28 +165,29 @@ const loadExecuteDescription = (runtime: ExecutorRuntime): Promise<string> => · 12 unmodified lines
152165 runControlPlane(
153166 runtime,
154167 Effect.gen(function* () {
155− const resolveExecutionEnvironment = yield* RuntimeExecutionResolverService;
156− const environment = yield* resolveExecutionEnvironment({
168+ const sourceCatalogStore = yield* RuntimeSourceCatalogStoreService;
169+ const catalogs = yield* sourceCatalogStore.loadWorkspaceSourceCatalogs({
157170 scopeId: runtime.localInstallation.scopeId,
158171 actorScopeId: runtime.localInstallation.actorScopeId,
159− executionId: ExecutionIdSchema.make("exec_mcp_help"),
160172 });
161173
162− const catalog = environment.catalog as CatalogLike | undefined;
163− if (!catalog) {
174+ const sourceToolExamples = (catalogs as ReadonlyArray<CatalogLike>)
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) {
164187 return defaultExecuteDescription;
165188 }
166189
167− const namespaces = yield* catalog.listNamespaces({ limit: 200 }).pipe(
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);
190+ return buildExecuteWorkflowText(sourceToolExamples);
177191 }).pipe(Effect.catchAll(() => Effect.succeed(defaultExecuteDescription))),
178192 );
179193
packages/platform/sdk/src/runtime/execution/scope/source-catalog.test.ts+127−0
@@ -0,0 +1,127 @@
1+import {
2+ describe,
3+ expect,
4+ it,
5+} from "@effect/vitest";
6+import * as Effect from "effect/Effect";
7+
8+import {
9+ createScopeSourceCatalog,
10+} from "./source-catalog";
11+
12+const 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+
53+const noopScopeConfigStore = {
54+ load: () => Effect.die("unexpected config load"),
55+ writeProject: () => Effect.void,
56+ resolveRelativePath: (path: string) => Effect.succeed(path),
57+} as any;
58+
59+const noopScopeStateStore = {
60+ load: () => Effect.die("unexpected scope state load"),
61+ write: () => Effect.void,
62+} as any;
63+
64+const 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+
71+describe("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 @@
11 import {
22 createToolCatalogFromEntries,
33 type ToolCatalog,
4+ type ToolNamespace,
45 } from "@executor/codemode-core";
56 import type {
67 ScopeId,
@@ -80,6 +81,49 @@ const hasSubstringMatch = (value: string, queryToken: string): boolean => { · 73 unmodified lines
8081 const queryTokenWeight = (token: string): number =>
8182 LOW_SIGNAL_QUERY_TOKENS.has(token) ? 0.25 : 1;
8283
84+const namespaceFromPath = (path: string): string => {
85+ const [first, second] = path.split(".");
86+ return second ? `${first}.${second}` : first;
87+};
88+
89+const sortNamespaces = (namespaces: Iterable<ToolNamespace>): ToolNamespace[] =>
90+ [...namespaces].sort((left, right) => left.namespace.localeCompare(right.namespace));
91+
92+const 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+
83127 export const loadWorkspaceCatalogTools = (input: {
84128 scopeId: Source["scopeId"];
85129 actorScopeId: ScopeId;
@@ -291,9 +335,14 @@ export const createScopeSourceCatalog = (input: { · 205 unmodified lines
291335 return {
292336 listNamespaces: ({ limit }) =>
293337 provideRuntimeLocalScope(
294− Effect.flatMap(createSharedCatalog(false), (catalog) =>
295− catalog.listNamespaces({ limit }),
296− ),
338+ provideWorkspaceStorage(Effect.map(
339+ loadWorkspaceCatalogNamespaces({
340+ scopeId: input.scopeId,
341+ actorScopeId: input.actorScopeId,
342+ sourceCatalogStore: input.sourceCatalogStore,
343+ }),
344+ (namespaces) => namespaces.slice(0, limit),
345+ )),
297346 input.runtimeLocalScope,
298347 ),
299348