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
13 authToolFailure,13 authToolFailure,
14 isUserActionableError,14 isUserActionableError,
15 isToolResult,15 isToolResult,
16 IntegrationSlug,
16 ToolResult,17 ToolResult,
17 ToolAddress,18 ToolAddress,
18 parseToolAddress,19 parseToolAddress,
@@ -665,13 +666,17 @@ export const searchTools = Effect.fn("executor.tools.search")(function* ( · 646 unmodified lines
665 });666 });
666 667
667 const emptyQuery = normalizeSearchText(query).length === 0;668 const emptyQuery = normalizeSearchText(query).length === 0;
668 const hasNamespace =669 // The exact integration slug an empty-query enumeration reads under; null
669 options?.namespace !== undefined && normalizeSearchText(options.namespace).length > 0;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;
670 675
671 // An empty query with no namespace stays empty: it carries neither a676 // An empty query with no namespace stays empty: it carries neither a
672 // ranking signal nor a scope, and listing the whole workspace "by default"677 // ranking signal nor a scope, and listing the whole workspace "by default"
673 // is exactly the arbitrary dump the ranked search refuses to be.678 // is exactly the arbitrary dump the ranked search refuses to be.
674 if (emptyQuery && !hasNamespace) {679 if (emptyQuery && scope === null) {
675 return {680 return {
676 items: [],681 items: [],
677 total: 0,682 total: 0,
@@ -680,15 +685,26 @@ export const searchTools = Effect.fn("executor.tools.search")(function* ( · 2 unmodified lines
680 } satisfies PagedResult<ToolDiscoveryResult>;685 } satisfies PagedResult<ToolDiscoveryResult>;
681 }686 }
682 687
683 const all = yield* executor.tools.list({ includeAnnotations: false }).pipe(688 // Enumeration's scope is an exact integration slug (see below), so it is the
684 Effect.mapError(689 // read's filter, not a post-read predicate: pushing it down lets the list
685 (cause) =>690 // narrow its own catalog refresh to that one integration instead of
686 new ExecutionToolError({691 // re-listing every stale connection in the workspace. Ranked search is
687 message: "Failed to list tools for search",692 // deliberately NOT scoped here — `matchesNamespace` is token-prefix, so a
688 cause,693 // scoped read would drop the prefix-sibling integrations it means to match.
689 }),694 const all = yield* executor.tools
690 ),695 .list({
691 );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 );
692 const searchable = all.map(toSearchableTool);708 const searchable = all.map(toSearchableTool);
693 709
694 // An empty query WITH a namespace is enumeration, not search: there is no710 // 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
698 // sweep in prefix-sibling integrations (namespace "google" matching714 // sweep in prefix-sibling integrations (namespace "google" matching
699 // google_gmail and google_sheets), which would silently break the census715 // google_gmail and google_sheets), which would silently break the census
700 // guarantee: `total` here must reconcile against716 // 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.
702 const ranked: readonly ToolDiscoveryResult[] = emptyQuery720 const ranked: readonly ToolDiscoveryResult[] = emptyQuery
703 ? searchable721 ? searchable
704 .filter((tool) => tool.integration === options?.namespace?.trim())
705 .sort((left, right) => left.path.localeCompare(right.path))722 .sort((left, right) => left.path.localeCompare(right.path))
706 .map((tool) => ({723 .map((tool) => ({
707 path: tool.path,724 path: tool.path,