History

Push the search namespace into the tools read

Empty-query enumeration filtered tools by exact integration slug after listing everything. Pass it as the read filter instead, so the read refreshes only that integration. Ranked search stays unscoped: its namespace match is token-prefix.

Rhys Sullivan committed 3 months ago

Commit 92116ff1 parent 071b9701 file · +31 −14Browse files

Unified · Split

packages/core/execution/src/tool-invoker.ts+31−14
@@ -13,6 +13,7 @@ import { · 12 unmodified lines
1313 authToolFailure,
1414 isUserActionableError,
1515 isToolResult,
16+ IntegrationSlug,
1617 ToolResult,
1718 ToolAddress,
1819 parseToolAddress,
@@ -665,13 +666,17 @@ export const searchTools = Effect.fn("executor.tools.search")(function* ( · 646 unmodified lines
665666 });
666667
667668 const emptyQuery = normalizeSearchText(query).length === 0;
668− const hasNamespace =
669− options?.namespace !== undefined && normalizeSearchText(options.namespace).length > 0;
669+ // The exact integration slug an empty-query enumeration reads under; null
670+ // when the caller named no usable namespace.
671+ const scope =
672+ options?.namespace !== undefined && normalizeSearchText(options.namespace).length > 0
673+ ? IntegrationSlug.make(options.namespace.trim())
674+ : null;
670675
671676 // An empty query with no namespace stays empty: it carries neither a
672677 // ranking signal nor a scope, and listing the whole workspace "by default"
673678 // is exactly the arbitrary dump the ranked search refuses to be.
674− if (emptyQuery && !hasNamespace) {
679+ if (emptyQuery && scope === null) {
675680 return {
676681 items: [],
677682 total: 0,
@@ -680,15 +685,26 @@ export const searchTools = Effect.fn("executor.tools.search")(function* ( · 2 unmodified lines
680685 } satisfies PagedResult<ToolDiscoveryResult>;
681686 }
682687
683− const all = yield* executor.tools.list({ includeAnnotations: false }).pipe(
684− Effect.mapError(
685− (cause) =>
686− new ExecutionToolError({
687− message: "Failed to list tools for search",
688− cause,
689− }),
690− ),
691− );
688+ // Enumeration's scope is an exact integration slug (see below), so it is the
689+ // read's filter, not a post-read predicate: pushing it down lets the list
690+ // narrow its own catalog refresh to that one integration instead of
691+ // re-listing every stale connection in the workspace. Ranked search is
692+ // deliberately NOT scoped here — `matchesNamespace` is token-prefix, so a
693+ // scoped read would drop the prefix-sibling integrations it means to match.
694+ const all = yield* executor.tools
695+ .list({
696+ includeAnnotations: false,
697+ ...(emptyQuery && scope !== null ? { integration: scope } : {}),
698+ })
699+ .pipe(
700+ Effect.mapError(
701+ (cause) =>
702+ new ExecutionToolError({
703+ message: "Failed to list tools for search",
704+ cause,
705+ }),
706+ ),
707+ );
692708 const searchable = all.map(toSearchableTool);
693709
694710 // An empty query WITH a namespace is enumeration, not search: there is no
@@ -698,10 +714,11 @@ export const searchTools = Effect.fn("executor.tools.search")(function* ( · 3 unmodified lines
698714 // sweep in prefix-sibling integrations (namespace "google" matching
699715 // google_gmail and google_sheets), which would silently break the census
700716 // guarantee: `total` here must reconcile against
701− // `executor.integrations.list`'s per-integration toolCount.
717+ // `executor.integrations.list`'s per-integration toolCount. That exact match
718+ // is now the `integration` filter on the read above, so `searchable` is
719+ // already the namespace's catalog and re-filtering it here would be a no-op.
702720 const ranked: readonly ToolDiscoveryResult[] = emptyQuery
703721 ? searchable
704− .filter((tool) => tool.integration === options?.namespace?.trim())
705722 .sort((left, right) => left.path.localeCompare(right.path))
706723 .map((tool) => ({
707724 path: tool.path,