History

Warm source index at startup

Rhys Sullivan committed 7 months ago

Commit 211e5c21 parent ee439d36 files · +342 −44Browse files

Unified · Split

6 files
  1. .changeset/cold-lions-listen.md +5 −0
  2. packages/platform/sdk/src/runtime/catalog/source/runtime.ts +150 −32
  3. packages/platform/sdk/src/runtime/execution/scope/source-catalog.test.ts +80 −0
  4. packages/platform/sdk/src/runtime/execution/scope/source-catalog.ts +41 −12
  5. packages/platform/sdk/src/runtime/index.test.ts +46 −0
  6. packages/platform/sdk/src/runtime/index.ts +20 −0
.changeset/cold-lions-listen.md+5−0
@@ -0,0 +1,5 @@
1+---
2+"executor": patch
3+---
4+
5+Prewarm the lean workspace source index at startup and reuse it across discovery requests so the first tool search no longer pays the full catalog hydration cost.
packages/platform/sdk/src/runtime/catalog/source/runtime.ts+150−32
@@ -8,6 +8,7 @@ import type { · 7 unmodified lines
88 StoredSourceRecord,
99 StoredSourceCatalogRevisionRecord,
1010 } from "#schema";
11+import * as Cache from "effect/Cache";
1112 import * as Context from "effect/Context";
1213 import * as Effect from "effect/Effect";
1314 import * as Layer from "effect/Layer";
@@ -133,6 +134,22 @@ const catalogNamespaceFromPath = (path: string): string => { · 119 unmodified lines
133134
134135 const descriptorPath = (descriptor: CatalogToolDescriptor): string => descriptor.path;
135136
137+export const loadedSourceCatalogToolIndexEntryFromLoadedTool = (
138+ tool: LoadedSourceCatalogTool,
139+): LoadedSourceCatalogToolIndexEntry => ({
140+ path: tool.path,
141+ searchNamespace: tool.searchNamespace,
142+ searchText: tool.searchText,
143+ source: tool.source,
144+ sourceRecord: tool.sourceRecord,
145+ capabilityId: tool.capabilityId,
146+ executableId: tool.executableId,
147+ capability: tool.capability,
148+ executable: tool.executable,
149+ descriptor: tool.descriptor,
150+ projectedCatalog: tool.projectedCatalog,
151+});
152+
136153 const optionalJsonString = (value: unknown): string | null => {
137154 if (value === undefined || value === null) {
138155 return null;
@@ -683,7 +700,6 @@ type RuntimeSourceCatalogStoreShape = { · 544 unmodified lines
683700 loadWorkspaceSourceCatalogToolIndex: (input: {
684701 scopeId: ScopeId;
685702 actorScopeId?: ScopeId | null;
686− includeSchemas: boolean;
687703 }) => Effect.Effect<readonly LoadedSourceCatalogToolIndexEntry[], Error, never>;
688704 loadWorkspaceSourceCatalogToolByPath: (input: {
689705 scopeId: ScopeId;
@@ -710,6 +726,23 @@ type SourceCatalogRuntimeServices = · 20 unmodified lines
710726 | RuntimeSourceStoreService
711727 | SourceArtifactStore;
712728
729+type WorkspaceSourceCatalogCacheKey = {
730+ scopeId: ScopeId;
731+ actorScopeId: ScopeId | null;
732+ sourceFingerprint: string;
733+};
734+
735+const normalizeActorScopeId = (
736+ actorScopeId?: ScopeId | null,
737+): ScopeId | null => actorScopeId ?? null;
738+
739+const workspaceSourceFingerprint = (sources: readonly Source[]): string =>
740+ JSON.stringify(
741+ [...sources].sort((left, right) =>
742+ String(left.id).localeCompare(String(right.id))
743+ ),
744+ );
745+
713746 const ensureRuntimeCatalogWorkspace = (
714747 deps: RuntimeSourceCatalogStoreDeps,
715748 scopeId: ScopeId,
@@ -731,19 +764,45 @@ const buildSnapshotFromArtifact = (input: { · 15 unmodified lines
731764 return decodeCatalogSnapshotV1(input.artifact.snapshot);
732765 };
733766
734−const loadWorkspaceSourceCatalogsWithDeps = (deps: RuntimeSourceCatalogStoreDeps, input: {
735− scopeId: ScopeId;
736− actorScopeId?: ScopeId | null;
737−}): Effect.Effect<readonly LoadedSourceCatalog[], Error, never> =>
767+const loadWorkspaceSourcesWithDeps = (
768+ deps: RuntimeSourceCatalogStoreDeps,
769+ input: {
770+ scopeId: ScopeId;
771+ actorScopeId?: ScopeId | null;
772+ },
773+): Effect.Effect<readonly Source[], Error, never> =>
738774 Effect.gen(function* () {
739775 yield* ensureRuntimeCatalogWorkspace(deps, input.scopeId);
740− const sources = yield* deps.sourceStore.loadSourcesInScope(
776+
777+ return yield* deps.sourceStore.loadSourcesInScope(
741778 input.scopeId,
742779 {
743780 actorScopeId: input.actorScopeId,
744781 },
745782 );
783+ });
746784
785+const loadWorkspaceSourceCatalogCacheKeyWithDeps = (
786+ deps: RuntimeSourceCatalogStoreDeps,
787+ input: {
788+ scopeId: ScopeId;
789+ actorScopeId?: ScopeId | null;
790+ },
791+): Effect.Effect<WorkspaceSourceCatalogCacheKey, Error, never> =>
792+ Effect.map(
793+ loadWorkspaceSourcesWithDeps(deps, input),
794+ (sources) => ({
795+ scopeId: input.scopeId,
796+ actorScopeId: normalizeActorScopeId(input.actorScopeId),
797+ sourceFingerprint: workspaceSourceFingerprint(sources),
798+ }),
799+ );
800+
801+const loadLoadedSourceCatalogsFromSourcesWithDeps = (
802+ deps: RuntimeSourceCatalogStoreDeps,
803+ sources: readonly Source[],
804+): Effect.Effect<readonly LoadedSourceCatalog[], Error, never> =>
805+ Effect.gen(function* () {
747806 const localCatalogs = yield* Effect.forEach(sources, (source) =>
748807 Effect.gen(function* () {
749808 const artifact = yield* deps.sourceArtifactStore.read({
@@ -781,6 +840,15 @@ const loadWorkspaceSourceCatalogsWithDeps = (deps: RuntimeSourceCatalogStoreDeps · 31 unmodified lines
781840 return localCatalogs.filter((catalogEntry): catalogEntry is LoadedSourceCatalog => catalogEntry !== null);
782841 });
783842
843+const loadWorkspaceSourceCatalogsWithDeps = (deps: RuntimeSourceCatalogStoreDeps, input: {
844+ scopeId: ScopeId;
845+ actorScopeId?: ScopeId | null;
846+}): Effect.Effect<readonly LoadedSourceCatalog[], Error, never> =>
847+ Effect.gen(function* () {
848+ const sources = yield* loadWorkspaceSourcesWithDeps(deps, input);
849+ return yield* loadLoadedSourceCatalogsFromSourcesWithDeps(deps, sources);
850+ });
851+
784852 const loadSourceWithCatalogWithDeps = (deps: RuntimeSourceCatalogStoreDeps, input: {
785853 scopeId: ScopeId;
786854 sourceId: Source["id"];
@@ -1107,7 +1175,6 @@ export const expandCatalogToolByPath = (input: { · 320 unmodified lines
11071175 export const loadWorkspaceSourceCatalogToolIndex = (input: {
11081176 scopeId: ScopeId;
11091177 actorScopeId?: ScopeId | null;
1110− includeSchemas: boolean;
11111178 }): Effect.Effect<
11121179 readonly LoadedSourceCatalogToolIndexEntry[],
11131180 Error,
@@ -1120,21 +1187,10 @@ export const loadWorkspaceSourceCatalogToolIndex = (input: { · 6 unmodified lines
11201187 });
11211188 const tools = yield* expandCatalogTools({
11221189 catalogs,
1123− includeSchemas: input.includeSchemas,
1190+ includeSchemas: false,
1191+ includeTypePreviews: false,
11241192 });
1125− return tools.map((tool) => ({
1126− path: tool.path,
1127− searchNamespace: tool.searchNamespace,
1128− searchText: tool.searchText,
1129− source: tool.source,
1130− sourceRecord: tool.sourceRecord,
1131− capabilityId: tool.capabilityId,
1132− executableId: tool.executableId,
1133− capability: tool.capability,
1134− executable: tool.executable,
1135− descriptor: tool.descriptor,
1136− projectedCatalog: tool.projectedCatalog,
1137− }));
1193+ return tools.map(loadedSourceCatalogToolIndexEntryFromLoadedTool);
11381194 });
11391195
11401196 export const loadWorkspaceSourceCatalogToolByPath = (input: {
@@ -1187,23 +1243,85 @@ export const RuntimeSourceCatalogStoreLive = Layer.effect( · 46 unmodified lines
11871243 sourceArtifactStore,
11881244 };
11891245
1246+ const workspaceCatalogCache = yield* Cache.make<
1247+ WorkspaceSourceCatalogCacheKey,
1248+ readonly LoadedSourceCatalog[],
1249+ Error
1250+ >({
1251+ capacity: 32,
1252+ timeToLive: "10 minutes",
1253+ lookup: (key) =>
1254+ loadWorkspaceSourceCatalogsWithDeps(
1255+ deps,
1256+ {
1257+ scopeId: key.scopeId,
1258+ actorScopeId: key.actorScopeId,
1259+ },
1260+ ),
1261+ });
1262+
1263+ const leanWorkspaceToolIndexCache = yield* Cache.make<
1264+ WorkspaceSourceCatalogCacheKey,
1265+ readonly LoadedSourceCatalogToolIndexEntry[],
1266+ Error
1267+ >({
1268+ capacity: 32,
1269+ timeToLive: "10 minutes",
1270+ lookup: (key) =>
1271+ Effect.gen(function* () {
1272+ const catalogs = yield* workspaceCatalogCache.get(key);
1273+ const tools = yield* expandCatalogTools({
1274+ catalogs,
1275+ includeSchemas: false,
1276+ includeTypePreviews: false,
1277+ });
1278+
1279+ return tools.map(loadedSourceCatalogToolIndexEntryFromLoadedTool);
1280+ }),
1281+ });
1282+
1283+ const loadCachedWorkspaceCatalogs = (input: {
1284+ scopeId: ScopeId;
1285+ actorScopeId?: ScopeId | null;
1286+ }) =>
1287+ Effect.flatMap(
1288+ loadWorkspaceSourceCatalogCacheKeyWithDeps(deps, input),
1289+ (key) => workspaceCatalogCache.get(key),
1290+ );
1291+
1292+ const loadLeanWorkspaceToolIndex = (input: {
1293+ scopeId: ScopeId;
1294+ actorScopeId?: ScopeId | null;
1295+ }) =>
1296+ Effect.flatMap(
1297+ loadWorkspaceSourceCatalogCacheKeyWithDeps(deps, input),
1298+ (key) => leanWorkspaceToolIndexCache.get(key),
1299+ );
1300+
11901301 return RuntimeSourceCatalogStoreService.of({
11911302 loadWorkspaceSourceCatalogs: (input) =>
1192− loadWorkspaceSourceCatalogsWithDeps(deps, input),
1303+ loadCachedWorkspaceCatalogs(input),
11931304 loadSourceWithCatalog: (input) =>
11941305 loadSourceWithCatalogWithDeps(deps, input),
11951306 loadWorkspaceSourceCatalogToolIndex: (input) =>
1196− loadWorkspaceSourceCatalogToolIndex(input).pipe(
1197− Effect.provideService(RuntimeLocalScopeService, runtimeLocalScope),
1198− Effect.provideService(RuntimeSourceStoreService, sourceStore),
1199− Effect.provideService(SourceArtifactStore, sourceArtifactStore),
1200− ),
1307+ loadLeanWorkspaceToolIndex({
1308+ scopeId: input.scopeId,
1309+ actorScopeId: input.actorScopeId,
1310+ }),
12011311 loadWorkspaceSourceCatalogToolByPath: (input) =>
1202− loadWorkspaceSourceCatalogToolByPath(input).pipe(
1203− Effect.provideService(RuntimeLocalScopeService, runtimeLocalScope),
1204− Effect.provideService(RuntimeSourceStoreService, sourceStore),
1205− Effect.provideService(SourceArtifactStore, sourceArtifactStore),
1206− ),
1312+ input.includeSchemas
1313+ ? loadWorkspaceSourceCatalogToolByPath(input).pipe(
1314+ Effect.provideService(RuntimeLocalScopeService, runtimeLocalScope),
1315+ Effect.provideService(RuntimeSourceStoreService, sourceStore),
1316+ Effect.provideService(SourceArtifactStore, sourceArtifactStore),
1317+ )
1318+ : Effect.map(
1319+ loadLeanWorkspaceToolIndex({
1320+ scopeId: input.scopeId,
1321+ actorScopeId: input.actorScopeId,
1322+ }),
1323+ (tools) => tools.find((tool) => tool.path === input.path) ?? null,
1324+ ),
12071325 });
12081326 }),
12091327 );
packages/platform/sdk/src/runtime/execution/scope/source-catalog.test.ts+80−0
@@ -124,4 +124,84 @@ describe("scope source catalog", () => { · 123 unmodified lines
124124 },
125125 ]);
126126 }));
127+
128+ it.effect("reuses a lean shared catalog for repeated no-schema search and lookup calls", () =>
129+ Effect.gen(function* () {
130+ const loadWorkspaceSourceCatalogToolIndexCalls: Array<undefined> = [];
131+
132+ const catalog = createScopeSourceCatalog({
133+ scopeId: "ws_test" as any,
134+ actorScopeId: "acc_test" as any,
135+ sourceCatalogStore: {
136+ loadWorkspaceSourceCatalogs: () => Effect.die("unexpected catalog load"),
137+ loadWorkspaceSourceCatalogToolIndex: () => {
138+ loadWorkspaceSourceCatalogToolIndexCalls.push(undefined);
139+
140+ return Effect.succeed([{
141+ path: "github.issues.list",
142+ searchNamespace: "github.issues",
143+ searchText: "github issues list repository issues",
144+ source: {
145+ enabled: true,
146+ status: "connected",
147+ },
148+ sourceRecord: {},
149+ capabilityId: "cap_1",
150+ executableId: "exec_1",
151+ capability: {
152+ surface: {
153+ title: "List Repository Issues",
154+ summary: "List issues for a repository",
155+ },
156+ },
157+ executable: {
158+ display: {
159+ leaf: "list",
160+ operationId: "listRepositoryIssues",
161+ pathTemplate: "/repos/{owner}/{repo}/issues",
162+ },
163+ },
164+ descriptor: {
165+ path: "github.issues.list",
166+ sourceKey: "github",
167+ description: "List issues for a repository",
168+ interaction: "auto",
169+ },
170+ projectedCatalog: {},
171+ }] as any);
172+ },
173+ loadWorkspaceSourceCatalogToolByPath: () => Effect.die("unexpected tool lookup"),
174+ } as any,
175+ scopeConfigStore: noopScopeConfigStore,
176+ scopeStateStore: noopScopeStateStore,
177+ sourceArtifactStore: noopSourceArtifactStore,
178+ runtimeLocalScope: null,
179+ });
180+
181+ const hits = yield* catalog.searchTools({
182+ query: "GitHub list repository issues",
183+ limit: 20,
184+ });
185+ const first = yield* catalog.getToolByPath({
186+ path: "github.issues.list" as any,
187+ includeSchemas: false,
188+ });
189+ const second = yield* catalog.getToolByPath({
190+ path: "github.issues.list" as any,
191+ includeSchemas: false,
192+ });
193+ const listed = yield* catalog.listTools({
194+ query: "GitHub list repository issues",
195+ limit: 20,
196+ includeSchemas: false,
197+ });
198+
199+ expect(hits).toHaveLength(1);
200+ expect(hits[0]?.path).toBe("github.issues.list");
201+ expect(hits[0]?.score).toBeGreaterThan(0);
202+ expect(first?.contract?.inputTypePreview).toBeUndefined();
203+ expect(second?.contract?.outputTypePreview).toBeUndefined();
204+ expect(listed).toHaveLength(1);
205+ expect(loadWorkspaceSourceCatalogToolIndexCalls).toHaveLength(1);
206+ }));
127207 });
packages/platform/sdk/src/runtime/execution/scope/source-catalog.ts+41−12
@@ -13,6 +13,8 @@ import { · 12 unmodified lines
1313 RuntimeSourceCatalogStoreService,
1414 type LoadedSourceCatalogToolIndexEntry,
1515 catalogToolCatalogEntry,
16+ expandCatalogTools,
17+ loadedSourceCatalogToolIndexEntryFromLoadedTool,
1618 } from "../../catalog/source/runtime";
1719 import type {
1820 RuntimeLocalScopeState,
@@ -134,17 +136,34 @@ export const loadWorkspaceCatalogTools = (input: { · 115 unmodified lines
134136 Error,
135137 ScopeStorageServices
136138 > =>
137− Effect.map(
138− input.sourceCatalogStore.loadWorkspaceSourceCatalogToolIndex({
139− scopeId: input.scopeId,
140− actorScopeId: input.actorScopeId,
141− includeSchemas: input.includeSchemas,
142− }),
143− (tools) =>
144− tools.filter(
145− (tool) => tool.source.enabled && tool.source.status === "connected",
146− ),
147− );
139+ input.includeSchemas
140+ ? Effect.gen(function* () {
141+ const catalogs = yield* input.sourceCatalogStore.loadWorkspaceSourceCatalogs({
142+ scopeId: input.scopeId,
143+ actorScopeId: input.actorScopeId,
144+ });
145+ const tools = yield* expandCatalogTools({
146+ catalogs,
147+ includeSchemas: true,
148+ includeTypePreviews: true,
149+ });
150+
151+ return tools
152+ .map(loadedSourceCatalogToolIndexEntryFromLoadedTool)
153+ .filter(
154+ (tool) => tool.source.enabled && tool.source.status === "connected",
155+ );
156+ })
157+ : Effect.map(
158+ input.sourceCatalogStore.loadWorkspaceSourceCatalogToolIndex({
159+ scopeId: input.scopeId,
160+ actorScopeId: input.actorScopeId,
161+ }),
162+ (tools) =>
163+ tools.filter(
164+ (tool) => tool.source.enabled && tool.source.status === "connected",
165+ ),
166+ );
148167
149168 export const loadWorkspaceCatalogToolByPath = (input: {
150169 scopeId: Source["scopeId"];
@@ -313,7 +332,7 @@ export const createScopeSourceCatalog = (input: { · 162 unmodified lines
313332 const provideWorkspaceStorage = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
314333 effect.pipe(Effect.provide(scopeStorageLayer));
315334
316− const createSharedCatalog = (includeSchemas: boolean): Effect.Effect<ToolCatalog, Error, never> =>
335+ const buildSharedCatalog = (includeSchemas: boolean) =>
317336 provideWorkspaceStorage(Effect.gen(function* () {
318337 const catalogTools = yield* loadWorkspaceCatalogTools({
319338 scopeId: input.scopeId,
@@ -332,6 +351,16 @@ export const createScopeSourceCatalog = (input: { · 12 unmodified lines
332351 });
333352 }));
334353
354+ const leanSharedCatalog = Effect.runSync(
355+ Effect.cached(buildSharedCatalog(false)),
356+ );
357+ const schemaSharedCatalog = Effect.runSync(
358+ Effect.cached(buildSharedCatalog(true)),
359+ );
360+
361+ const createSharedCatalog = (includeSchemas: boolean): Effect.Effect<ToolCatalog, Error, never> =>
362+ includeSchemas ? schemaSharedCatalog : leanSharedCatalog;
363+
335364 return {
336365 listNamespaces: ({ limit }) =>
337366 provideRuntimeLocalScope(
packages/platform/sdk/src/runtime/index.test.ts+46−0
@@ -0,0 +1,46 @@
1+import {
2+ describe,
3+ expect,
4+ it,
5+} from "@effect/vitest";
6+import * as Effect from "effect/Effect";
7+
8+import {
9+ RuntimeSourceCatalogStoreService,
10+} from "./catalog/source/runtime";
11+import {
12+ prewarmWorkspaceSourceCatalogToolIndex,
13+} from "./index";
14+
15+describe("runtime startup prewarm", () => {
16+ it.effect("warms the workspace source tool index for the active installation", () =>
17+ Effect.gen(function* () {
18+ const calls: Array<{
19+ scopeId: string;
20+ actorScopeId: string | null | undefined;
21+ }> = [];
22+
23+ yield* prewarmWorkspaceSourceCatalogToolIndex({
24+ scopeId: "ws_test" as any,
25+ actorScopeId: "acc_test" as any,
26+ }).pipe(
27+ Effect.provideService(
28+ RuntimeSourceCatalogStoreService,
29+ RuntimeSourceCatalogStoreService.of({
30+ loadWorkspaceSourceCatalogs: () => Effect.die("unexpected catalog load"),
31+ loadSourceWithCatalog: () => Effect.die("unexpected source load"),
32+ loadWorkspaceSourceCatalogToolIndex: (input) => {
33+ calls.push(input as any);
34+ return Effect.succeed([]);
35+ },
36+ loadWorkspaceSourceCatalogToolByPath: () => Effect.die("unexpected tool lookup"),
37+ }),
38+ ),
39+ );
40+
41+ expect(calls).toEqual([{
42+ scopeId: "ws_test",
43+ actorScopeId: "acc_test",
44+ }]);
45+ }));
46+});
packages/platform/sdk/src/runtime/index.ts+20−0
@@ -17,6 +17,7 @@ import { · 16 unmodified lines
1717 RuntimeSourceAuthMaterialLive,
1818 } from "./auth/source-auth-material";
1919 import {
20+ RuntimeSourceCatalogStoreService,
2021 RuntimeSourceCatalogStoreLive,
2122 } from "./catalog/source/runtime";
2223 import {
@@ -223,6 +224,18 @@ export type RuntimeAuthStorageServices = { · 200 unmodified lines
223224 sourceSessions: ExecutorStateStoreShape["sourceAuthSessions"];
224225 };
225226
227+export const prewarmWorkspaceSourceCatalogToolIndex = (input: {
228+ scopeId: LocalInstallation["scopeId"];
229+ actorScopeId: LocalInstallation["actorScopeId"];
230+}) =>
231+ Effect.gen(function* () {
232+ const sourceCatalogStore = yield* RuntimeSourceCatalogStoreService;
233+ yield* sourceCatalogStore.loadWorkspaceSourceCatalogToolIndex({
234+ scopeId: input.scopeId,
235+ actorScopeId: input.actorScopeId,
236+ });
237+ });
238+
226239 export type RuntimeSecretsStorageServices =
227240 & ExecutorStateStoreShape["secretMaterials"]
228241 & RuntimeSecretMaterialServices;
@@ -513,6 +526,13 @@ export const createExecutorRuntimeFromServices = (input: { · 284 unmodified lines
513526 Effect.provide(managedRuntime),
514527 Effect.catchAll(() => Effect.void),
515528 );
529+ yield* prewarmWorkspaceSourceCatalogToolIndex({
530+ scopeId: localInstallation.scopeId,
531+ actorScopeId: localInstallation.actorScopeId,
532+ }).pipe(
533+ Effect.provide(managedRuntime),
534+ Effect.catchAll(() => Effect.void),
535+ );
516536
517537 return {
518538 storage: input.services.storage,