π«π· Version franΓ§aise
See also: Memory system Β· YAML reference Β· Tool inventory Β· Back to index
RaggableTree
Multi-language semantic graph of the source code: Tree-sitter parsing, embedding enrichment, vector storage, and exposure to agents through 15 tools. Replaces raw file reading with a structured index offering five stratified node levels (L0 monorepo β L1 package β L2 module β L3 symbol β L4 statement) plus the edge layer between them.
Overview
An agent that only has file_read and directory_read must read entire files, guess their relations, and overload its LLM context. The RaggableTree precomputes these elements:
- Structure: each file is decomposed into classes, methods, functions, with stable Fully-Qualified Names (
pkg::Module::Class::method). - Relations: imports, calls, inheritance, and implementations are resolved from the AST and stored as graph edges.
- Semantics: signatures, docstrings, code snippets and, optionally, LLM summaries are composed into
EmbeddingTextand then vectorized. - Evolution: an incremental reindexing engine updates the index after each commit or watcher event.
The exposed tools (codebase_map, symbol_detail, flow_trace, impact_analysis, etc.) give the agent structural queries instead of grep over plain text.
Quick start
using Orkeon.Analysis.Abstractions.DTOs.Tools;
using Orkeon.Analysis.Adapters;
using Orkeon.Analysis.Core;
using Orkeon.Domain.FileSystem;
// fileSystem: IFileSystemService (VFS) with the project mounted at /src
var builder = new RaggableTreeBuilder(new TypeScriptAdapter(), fileSystem);
var result = await builder.BuildAsync(
"/src",
new IndexCodebaseRequest { RootPath = "/src" },
CancellationToken.None);
Console.WriteLine($"{result.Tree.Nodes.Count} nodes, {result.Tree.Edges.Count} edges, indexId={result.IndexId}");
For DI usage:
services.AddRaggableTree(new RaggableTreeOptions
{
Languages = ["typescript", "python"],
Embedding = new EmbeddingOptions { Provider = EmbeddingProviderKind.OpenAI, ApiKey = apiKey },
});
Configuration
The only configuration surface the shipped hosts bind is the RaggableTree
appsettings section (read key-by-key by the runner host β there is no crew-YAML
raggableTree: key):
{
"RaggableTree": {
"Enabled": true, // false disables the feature entirely (runner opt-out)
"Exclude": ["node_modules", "dist", ".git", "bin", "obj"],
"RootAlias": "", // shortens every FQN when set
"EnrichWithLlm": false, // true generates SemanticSummary via the summarizer
"IncludeStatements": false, // true indexes L4 (per-statement)
"IndexMode": "frozen", // assigned but consumed by nothing β see the historical note below
"Embedding": {
"Provider": "LocalSmartComponents", // the default β or None | OpenAI | Ollama (Onnx is a reserved no-op arm)
"Model": "", // empty β the provider's own default
"ApiKey": null,
"BaseUrl": null,
"Dimensions": null,
"MaxTextChars": null
}
}
}
Languages are never configured: they are auto-detected from the codebase and
scoped per index_codebase call. Everything else on RaggableTreeOptions
(Summarizer, ValidateCitations β default true, gating the citation
validators) is reachable from C# via AddRaggableTree(options); the
VectorStore/Cache option groups exist on the record but are consumed by
nothing β the store is InMemoryRaggableStore, adapted onto an ambient
IVectorStoreProvider when one is registered.
All fields are immutable record types in Orkeon.Analysis.DependencyInjection.RaggableTreeOptions.
Six-phase pipeline
- Discovery (
IFileSystemDiscoverer) β recursive traversal, package detection via markers (package.json,*.csproj,pyproject.toml,go.mod,Cargo.toml), exclusion via patterns and.gitignore. - Parse + Extract (
UniversalSemanticMapper+ILanguageAdapter) β Tree-sitter parse β application of the adapter's queries β L2 (module) and L3 (symbol) nodes withFqn,Signature,SourceSnippet,Sha256. - Dependency resolution (
DependencyGraphBuilder+IReferenceResolver) β resolution of imports, calls, inheritance, implementations into edges (EdgeKind.Imports,Calls,Extends,Implements). Unresolved FQNs becomeUnresolvedRef. - Fingerprinting (
IFrameworkFingerprinter) β application of per-decorator/annotation rules (NestJS, Angular, ASP.NET, Flask, FastAPI): placing tags such ashttp-endpoint,guard,serviceon the relevant nodes. - Enrichment β composition of the
EmbeddingText(IEmbeddingTextComposer), optionally an LLM summary (INodeSummarizer), then batch embedding (IEmbeddingProvider). - Persistence β
IVectorStoreProvider.IndexAsyncfor the embeddings; the tree itself stays in the in-memory store (theRaggableTreeCacheJSON serializer exists but is wired by no shipped host).
Incremental reindexing (IncrementalReindexEngine) restarts from phase 2 for the changed files only, reuses the nodes of unchanged files, and runs phases 4-6 only on the newNodes.
Tool catalogue
| Tool | Usage | Typical request |
|---|---|---|
index_codebase |
Builds the initial index | { "root_path": "/src" } |
incremental_reindex |
Updates the index after file changes | { "changed_files": ["src/a.ts"] } |
index_status |
Lists every indexed virtual root (date, node/edge counts) | {} |
is_path_indexed |
Checks whether a path is covered by an indexed root | { "virtual_path": "/src/app/main.ts" } |
codebase_map |
Overview: packages, file/symbol counts | { "level": "L2_Module" } |
package_summary |
Summary of a package (modules, dependencies, key symbols) | { "fqn": "app::core" } |
symbol_detail |
Detail of a symbol (signature, doc, callers, callees) | { "fqn": "app::UserService::create" } |
symbol_source |
Source code excerpt (signature, body, full span) | { "fqn": "...", "mode": "SignatureAndBody" } |
codebase_search |
Semantic search by embedding | { "query": "session expiration", "top_k": 10 } |
dependency_graph |
Level-scoped dependency graph (L1/L2/L3), Mermaid/DOT rendering | { "scope": "L2_Module", "edge_kinds": ["Imports", "Calls"] } |
sub_graph |
BFS expansion around seeds | { "seeds": ["pkg::X"], "depth": 3 } |
flow_trace |
Call paths between two FQNs | { "from": "A", "to": "B", "max_paths": 5 } |
impact_analysis |
Transitive impacts of a change | { "target": "...", "direction": "Backward" } |
complexity_report |
Top-N by metric (Cyclomatic, LoC, Callers...) | { "metric": "Cyclomatic", "top_n": 20 } |
statement_query |
L4 structural queries (if, try, return...) | { "parent_fqns": ["..."], "kinds": ["TryCatch"] } |
The 15 tools are registered via AddRaggableTreeTools (Orkeon.Tools.Analysis.DependencyInjection.RaggableToolsExtensions).
Agent strategies
The framework exposes ICodebaseContextProvider, which produces a compact summary of the codebase (packages, top complexity, top coupling, detected patterns) suitable for an agent's system prompt. Three formats: markdown (default, ~300 tokens), compact (~150 tokens), json (~400 tokens). No framework component calls it automatically β it is registered by AddRaggableTree and the host resolves it and injects the summary where it wishes.
Hybrid search
codebase_search (and IRaggableStore.SemanticSearchAsync) is hybrid since the
freshness work: an embedding cosine ranking and a BM25 lexical ranking are fused by
Reciprocal Rank Fusion (SemanticQuery.Mode: Hybrid default / Vector / Lexical).
The lexical half uses a code-aware tokenizer (CodeTokenizer): identifiers are split
on camelCase / snake_case / digit boundaries and indexed both as sub-tokens and whole
(getUserById β get user by id getuserbyid), so exact-identifier queries keep their
strong signal while concept queries gain recall. Each SearchHit carries MatchOrigin
(hybrid/vector/bm25); hybrid scores are RRF rank aggregates (not similarities β
never compare them across origins). With no embedder wired, Hybrid degrades to
Lexical instead of returning empty; explicit Vector keeps the historical contract.
Freshness (edit β search coordination)
The index stays truthful about an actively edited workspace through a mark-dirty + lazy-reindex design:
- Write hook β
FileWriteTooltakes an optionalIIndexInvalidation(registered byAddRaggableTree, implemented by the store): every successful write marks its path dirty. O(1), synchronous, no-op outside indexed roots. - Lazy pass β the read tools (
codebase_search,symbol_source,flow_trace,codebase_map) callIndexFreshnessService.EnsureFreshAsyncbefore answering: the dirty set βͺ the git working-tree changes (catchesshell_commandedits) is reindexed incrementally β grouped, single-flight, debounced (a clean git probe is trusted for 2 s).codebase_searchresponses reportrefreshed_files;index_statusreportsdirty_count/dirty_paths, so "stale" is observable. - Store safety β
InMemoryRaggableStoreholds aReaderWriterLockSlim: searches enumerate safely DURING an incremental reindex (previously a concurrent search could throw on the mutated dictionaries). The refresh publishesRaggableTreeUpdatedon theIRaggableTreeEventBus. - Failure barrier β a failed refresh serves the current (stale) index and keeps the dirty debt for the next attempt: stale results beat a dead search.
Historical note: an earlier version of this document described three watcher-driven
synchronization modes (frozen/live/breakOnChange via RaggableTreeIndexMode).
Those modes were never consumed by any code β the enum existed, nothing read it. The
freshness design above replaces that fiction; ICodebaseWatcher remains available for
hosts that want push-based invalidation on top of the lazy pass.
Extensibility
Adding a language
Implement ILanguageAdapter (Orkeon.Analysis.Abstractions.Interfaces): provide LanguageName, FileExtensions, the seven Tree-sitter queries (DeclarationQuery, ImportQuery, CallQuery, InheritanceQuery, DocCommentQuery, DecoratorQuery, StatementQuery), the MapNodeKind mapping, and the ExtractSignature / ResolveImportPath extractors. Register the adapter via DI (services.AddSingleton<ILanguageAdapter, MyAdapter>()); AddRaggableTree discovers it automatically.
Adding a fingerprinter
Create an IReadOnlyList<FingerprintRule> listing the decorators/annotations to match together with their tags, then build a FrameworkFingerprinter(framework, rules). See Orkeon.Analysis.Fingerprinters.NestJsRules as a reference (15 rules).
Adding an embedding provider
Implement IEmbeddingProvider.EmbedBatchAsync(texts, ct) β IReadOnlyList<ReadOnlyMemory<float>>. Three reference implementations: OpenAIEmbeddingProvider, OllamaEmbeddingProvider, and LocalEmbeddingProvider (see the section below). The default interface method EmbedAsync(nodes, model, ct) (on IEmbeddingProvider, Orkeon.Analysis.Abstractions.Interfaces) fills node.Embedding from the composed EmbeddingText.
Embedding providers
Five members exist on EmbeddingProviderKind:
| Provider | Network | API key | Dims | Notes |
|---|---|---|---|---|
None |
n/a | n/a | n/a | Pipeline without embeddings (phases 5-6 disabled) |
Onnx |
no | no | β | Reserved β currently a no-op registration arm |
OpenAI |
required | required | 1536 (text-embedding-3-small) |
Maximum quality, MTEB ~62.3 |
Ollama |
local (HTTP) | no | model-dependent | Local Ollama / llama.cpp daemon required |
LocalSmartComponents |
no | no | 384 (BGE-micro-v2) | In-process, ONNX CPU, cold start ~200 ms |
LocalSmartComponents local provider
Implemented by Orkeon.Tools.Embeddings.Local (opt-in package, upstream package SmartComponents.LocalEmbeddings v0.1.0-preview10148, BGE-micro-v2 model). Suited to dev CLI contexts, offline CI indexing, and demos without an API key.
Quality / cost trade-off (see spec Β§10):
- 384 dimensions vs 1536 (OpenAI) β less fine-grained semantic search on out-of-domain factual Q&A.
- MTEB score ~58.5 vs ~62.3 β acceptable for code indexing (signatures, FQNs, and docstrings are structured signals that are less ambiguous than long-form text).
- API cost β 0 (model embedded in the NuGet, ~22 MB).
- No network dependency, no key to manage.
- Cold start ~150-300 ms (loading the ONNX model into memory); ~5-15 ms / text on a modern x86_64 CPU.
- RAM footprint ~200 MB once the model is loaded.
Minimal configuration (appsettings.json, see spec Β§7.2)
{
"RaggableTree": {
"Embedding": {
"Provider": "LocalSmartComponents"
// Dimensions, BaseUrl, ApiKey, Model: all optional and ignored.
// Defaults: 384 dims (BGE-micro-v2), CPU, in-process.
}
}
}
On the code side, the DI registration is:
services.AddOrkeonLocalEmbeddings(); // Orkeon.Tools.Embeddings.Local package
services.AddRaggableTree(new RaggableTreeOptions
{
Embedding = new EmbeddingOptions
{
Provider = EmbeddingProviderKind.LocalSmartComponents,
},
});
Extended configuration with a VFS ModelPath (see spec Β§7.3)
To point to an ONNX model other than the embedded BGE-micro-v2, declare a read-only VFS mount and pass a virtual path in Local.ModelPath. Physical paths (C:/..., /var/...) are rejected by IFileSystemService at startup β see vfs-compliance.md.
{
"Orkeon": {
"FileSystem": {
"Mounts": [
"C:\\data\\embedding-models:/models:ro" // <physical>:<virtual>:<rights>
]
}
},
"RaggableTree": {
"Embedding": {
"Provider": "LocalSmartComponents",
"Dimensions": 384,
"MaxTextChars": 2000,
"Local": {
"ModelPath": "/models/my-custom.onnx", // virtual path β resolved by IFileSystemService
"MaxConcurrency": 8
}
}
}
}
The licenses of the model and of the upstream package are tracked in THIRD-PARTY-NOTICES.md. A minimal console example lives in examples/local-embeddings/.
Adding a vector store
Implement IVectorStoreProvider (IndexAsync, SearchAsync, DeleteAsync). See the six existing memory providers (Redis, SQLite, ChromaDB, Pinecone, LanceDB, InMemory) for the VectorDocument/VectorMetadata mapping patterns.
V1 limitations
- No support for C++ macros / fully resolved templates, nor for Ruby/Elixir metaprogramming.
- The import resolver is heuristic for dynamic languages (Python, TypeScript): re-exports and monkey patches can produce
UnresolvedRef. - Embedding and summarizer require an external provider (API keys or a local Ollama endpoint) to be enabled; the pipeline works without them (phases 5-6 disabled).
- The watcher relies on
System.IO.FileSystemWatcherβ on Linux/WSL, rename events may arrive decomposed (observed as Deleted then Created). - The JSON cache is not versioned: an adapter update may invalidate existing caches, which are then rebuilt on demand.
See also: RaggableTree ADR Β· Tool inventory Β· Back to index