feat(context): doc catalog — frontmatter/H1 descriptor as read-on-demand index
Docs were indexed as up to 40 h1-h3 headings per .md (a TOC flood) and force-fed or excluded wholesale by a binary docs gate. Replace with a compact, always-on catalog the agent reads on demand. - RepoMapIndexer: .md now yields ONE descriptor (frontmatter description/summary/title -> first # H1 -> first prose line), not a heading dump. Replay-safe (still List<String> symbols). - SessionOrchestrator: always-on '## Docs available (file_read to open)' catalog — top-N docs by recency as 'path — descriptor', hard-capped at DOCS_CATALOG_MAX. One line each, so it stays present without the poisoning the docs gate suppressed. Source layout still excludes doc content for non-doc prompts.
This commit is contained in:
+48
-3
@@ -197,6 +197,11 @@ private const val REFERENCE_EXISTS_CODE = "REFERENCE_EXISTS"
|
||||
private const val OUTPUT_SUMMARY_LIMIT = 500
|
||||
private const val REPO_MAP_INJECT_TOP_K = 30
|
||||
private const val REPO_MAP_FILES_PER_DIR = 8
|
||||
// Read-on-demand doc catalog: the top-N docs (by repo-map recency score) surfaced as
|
||||
// `path — descriptor` so a stage learns which docs exist and can file_read one when relevant,
|
||||
// instead of docs being force-fed (or excluded outright). One line each, hard-capped, so it can
|
||||
// stay always-on without re-poisoning context (2026-07-07 doc-injection rework).
|
||||
private const val DOCS_CATALOG_MAX = 20
|
||||
|
||||
// A stage prompt must explicitly ask about documentation for .md/docs paths to enter the repo
|
||||
// layout listing; otherwise docs only reach context through a real retrieval hit.
|
||||
@@ -1698,9 +1703,12 @@ abstract class SessionOrchestrator(
|
||||
val recorded = eventStore.read(sessionId)
|
||||
.mapNotNull { it.payload as? RepoKnowledgeRetrievedEvent }
|
||||
.lastOrNull { it.stageId == stageId }
|
||||
if (recorded != null) return repoEntriesOrMapFloor(sessionId, recorded.hits, stagePrompt)
|
||||
if (recorded != null) {
|
||||
return repoEntriesOrMapFloor(sessionId, recorded.hits, stagePrompt) + buildDocsCatalogEntry(sessionId)
|
||||
}
|
||||
|
||||
val retriever = repoKnowledgeRetriever ?: return buildRepoMapEntries(sessionId, stagePrompt)
|
||||
val retriever = repoKnowledgeRetriever
|
||||
?: return buildRepoMapEntries(sessionId, stagePrompt) + buildDocsCatalogEntry(sessionId)
|
||||
val hits = runCatching { retriever.retrieve(sessionId, stagePrompt, REPO_MAP_INJECT_TOP_K) }
|
||||
.getOrElse { e ->
|
||||
if (e is CancellationException) throw e
|
||||
@@ -1710,7 +1718,44 @@ abstract class SessionOrchestrator(
|
||||
// Record the raw retrieval for audit (a degenerate all-zero result is itself the signal that
|
||||
// the embedder is noop / the index is empty), but only useful hits ever reach the agent.
|
||||
if (hits.isNotEmpty()) emit(sessionId, RepoKnowledgeRetrievedEvent(sessionId, stageId, stagePrompt, hits))
|
||||
return repoEntriesOrMapFloor(sessionId, hits, stagePrompt)
|
||||
return repoEntriesOrMapFloor(sessionId, hits, stagePrompt) + buildDocsCatalogEntry(sessionId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Always-on, capped catalog of the docs that exist, as `path — descriptor` lines (the descriptor
|
||||
* is the doc's frontmatter/H1/first-line summary recorded on [RepoMapEntry.symbols]). This is the
|
||||
* read-on-demand doc index: one line per doc lets a stage decide whether to file_read it, so docs
|
||||
* stay discoverable without the heading-flood the docs gate was added to suppress. Distinct from
|
||||
* [buildRepoMapEntries]/[repoEntriesOrMapFloor], which still exclude doc *content* from the source
|
||||
* layout for non-doc prompts. Reads the recorded map (replay-safe — never re-scans the FS).
|
||||
*/
|
||||
private suspend fun buildDocsCatalogEntry(sessionId: SessionId): List<ContextEntry> {
|
||||
val map = eventStore.read(sessionId)
|
||||
.mapNotNull { it.payload as? RepoMapComputedEvent }
|
||||
.lastOrNull() ?: return emptyList()
|
||||
val docs = map.entries
|
||||
.filter { isDocPath(it.path) }
|
||||
.sortedByDescending { it.score }
|
||||
.take(DOCS_CATALOG_MAX)
|
||||
if (docs.isEmpty()) return emptyList()
|
||||
val content = buildString {
|
||||
appendLine("## Docs available (file_read to open — do not assume contents)")
|
||||
docs.forEach { entry ->
|
||||
val descriptor = entry.symbols.firstOrNull()?.takeIf { it.isNotBlank() }
|
||||
appendLine(if (descriptor != null) "- ${entry.path} — $descriptor" else "- ${entry.path}")
|
||||
}
|
||||
}.trimEnd()
|
||||
return listOf(
|
||||
ContextEntry(
|
||||
id = ContextEntryId(UUID.randomUUID().toString()),
|
||||
layer = ContextLayer.L3,
|
||||
content = content,
|
||||
sourceType = "docsCatalog",
|
||||
sourceId = "docs-catalog",
|
||||
tokenEstimate = estimateTokens(content),
|
||||
role = EntryRole.SYSTEM,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user