Skip to content

fix(specialists): add metadata-bearing VFS enumeration#61

Merged
khaliqgant merged 2 commits intomainfrom
feat/librarian-vfs-query
Apr 25, 2026
Merged

fix(specialists): add metadata-bearing VFS enumeration#61
khaliqgant merged 2 commits intomainfrom
feat/librarian-vfs-query

Conversation

@khaliqgant
Copy link
Copy Markdown
Member

@khaliqgant khaliqgant commented Apr 25, 2026

Summary

  • Add optional LibrarianVfs.enumerate for providers with indexed, property-bearing enumeration backends.
  • Route filtered librarian enumerations through enumerate when available, while keeping existing list/search behavior for providers that do not implement it.
  • Add a narrowly gated apiFallback retry when VFS returns entries but post-filtering removes all of them because the entries lack structured properties.
  • Add shared librarian-engine coverage for enumerate selection, list/search compatibility, zero-entry fallback, post-filter-empty fallback, and enumerate error handling.

Production failure

In production, sage's github_specialist({ capability: 'enumerate', query: 'open PRs in AgentWorkforce/cloud', filters: { state: 'open' } }) could return empty findings and eventually surface as a generic Slack failure. The root cause is that the cloud VFS provider enumerates through RelayFile's tree endpoint. TreeEntry includes propertyCount but not actual properties, while the librarian engine filters entries by structured fields such as entry.properties.state. That meant list returned entries, every entry failed the state:open post-filter, the normal zero-entry apiFallback gate did not fire, and the specialist exhausted its internal harness.

Design

This implements the senior-reviewed design by adding an optional metadata-bearing enumerate method to LibrarianVfs. Providers with an indexed/property-aware backend can implement it and return entries with properties populated. The engine still defensively post-filters all returned entries, so implementations must not silently drop unsupported filters.

The engine now prefers vfs.enumerate only when filters are active. Unfiltered requests continue through the existing list/search path, and providers without enumerate retain the previous list/search behavior. The apiFallback interface is unchanged.

Breaking change

LibrarianSource no longer has the broad 'vfs' variant. It now reports more specific VFS sources: 'vfs-list', 'vfs-enumerate', and 'vfs-search', plus the existing 'apiFallback' and 'mixed'. Runtime JSON consumers still receive a string, but TypeScript consumers that exhaustively handle the union need to update their cases.

Cloud follow-up

The cloud-side relayfile-vfs-provider.ts should plug into this by implementing enumerate({ roots, filters, limit }) with a property-bearing backend, likely via RelayFile's file query endpoint instead of the tree endpoint. It should preserve the documented filter semantics: OR within each filter key, AND across keys, and return entries that the specialists engine can re-filter defensively.

Validation

  • npm install
  • npm test --workspace @agent-assistant/specialists
  • npx tsc -p packages/specialists/tsconfig.json --noEmit

Open in Devin Review

Add an optional librarian VFS enumerate hook for providers that can return property-bearing entries, split librarian source diagnostics by VFS path, and retry apiFallback when structured filters remove all VFS results.

Co-Authored-By: Codex <noreply@openai.com>
devin-ai-integration[bot]

This comment was marked as resolved.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d5f0f633d5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if (hasFilters && options.vfs.enumerate) {
try {
const enumerated = await options.vfs.enumerate({
roots: adapter.listRoots(types, filters),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use default entity types when enumerate has no type filter

When a filtered query has no explicit/inferred type, requestedTypes is empty, but this call now passes that empty array to adapter.listRoots. In current adapters this can collapse the search space to nothing (e.g., Linear’s listRoots(types) is types.map(...)), so enumerate runs with roots: [] and valid queries like state:open team:ENG return no results even when matching entries exist. The prior list/search path used adapter.entityTypes in this case, so this is a behavior regression.

Useful? React with 👍 / 👎.

Comment on lines +145 to +148
entries = enumerated.map((entry) => ({
entry,
enumerationType: adapter.inferEntityType(entry),
}));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reapply requested-type filtering to enumerate results

Enumerated entries are now accepted directly without passing through toEnumerationEntry, so requested type constraints are no longer enforced by the engine. This matters for adapters where type is not in filterKeys (e.g., Linear), because matchesRequestedFilters won’t filter by type there; if an enumerate backend returns mixed entity kinds, type-scoped requests can include wrong entities. The list/search path previously enforced this via toEnumerationEntry.

Useful? React with 👍 / 👎.

…leakage, duplicate apiFallback

Three bugs surfaced by automated review (codex P1, codex P2, devin P1):

1. Empty `requestedTypes` collapsed `enumerate` roots to []
   When the query supplied no `type:` filter (e.g. `state:open team:ENG`),
   `requestedTypes` returned []. The new enumerate branch passed that
   empty array straight into `adapter.listRoots(types, filters)`. Every
   current adapter implements `listRoots` as `types.map(...)`, so the
   result was `roots: []` and the property-bearing backend was queried
   with no roots — guaranteed empty result for any non-type-scoped
   filtered query. Mirrors the existing list-path's
   `adapter.entityTypes` default by computing
   `effectiveTypes = types.length > 0 ? types : adapter.entityTypes`
   once and using it for both the roots arg and the type-constraint
   enforcement below.

2. Type-constraint enforcement bypassed for enumerate path
   Enumerated entries were accepted directly via `entries.map(entry =>
   ({entry, enumerationType: inferEntityType(entry)}))`, skipping
   `toEnumerationEntry`. For adapters where `type` is NOT in
   `filterKeys` (Linear), `matchesRequestedFilters` doesn't filter by
   type — so an `enumerate` backend returning mixed entity kinds could
   leak the wrong type past a `type:`-scoped query. Route enumerated
   entries through `toEnumerationEntry(adapter, entry, effectiveTypes)`
   like the list path already does. Same fix applied to the
   apiFallback paths for consistency.

3. Duplicate apiFallback call when first attempt's entries fail filter
   The post-filter-empty safety net at the bottom of the handler called
   `loadFallbackEntries` with identical `instruction`, `text`,
   `filters`, `types` even when the zero-entry gate above had already
   tried apiFallback and gotten back entries that all failed the
   post-filter. Track `apiFallbackAttempted` and short-circuit the
   safety net when the fallback was already invoked for this turn.

Three regression tests added — one per issue. Coverage now 10 cases on
`librarian-engine.test.ts`. Full `@agent-assistant/specialists` suite
27/27 passing; typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@khaliqgant khaliqgant merged commit b67605e into main Apr 25, 2026
1 check passed
@khaliqgant khaliqgant deleted the feat/librarian-vfs-query branch April 25, 2026 22:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant