Agent Skills¶
CodeNib's retrieval, reranking, and graph-expansion capabilities are packaged as
skills — self-contained units of metadata + execution logic that an LLM agent can
select and invoke. Skills live under codenib/agent/skills/.
How skills are defined¶
A skill is a package directory containing:
config.yaml— metadata (skill_id,skill_type, inputs/outputs,cost,index_requirements, default params)skill.md— agent-readable description of when to use the skillexecutor.py— acreate_executor(context) -> Callablefactory returning the execution function.contextis the typed op-context dataclass for the skill's type (RetrieveContext/RerankContext/TransformContext/ExpandContext);customcomposer skills (codenib_context,bm25_search,crossencoder_rerank,repository_search) instead receive a typedComposerContextsbundle (codenib/agent/skills/context.py).
SkillLoader.load_all(skills_dir, contexts) scans the directory, parses each
config.yaml, reads skill.md, imports the executor, and registers a SkillMetadata
in the registry. Skills can also be declared in code with the @skill decorator
(codenib/agent/skills/registry.py), which captures the same metadata and registers
the decorated function.
Registry API¶
SkillRegistry is a singleton catalogue of SkillMetadata:
from codenib.agent.skills.registry import SkillRegistry
registry = SkillRegistry()
registry.list_skills() # -> {skill_id: SkillMetadata}
registry.get("bm25_search") # -> SkillMetadata | None
registry.has("llm_rerank") # -> bool
Exposing skills to an agent¶
registry_to_tools(registry, allow=..., exclude=...)
(codenib/agent/tool_schema.py) converts registered skills into LLM tool schemas,
applying an optional allow set first, then an exclude set.
AgentRunner (codenib/agent/runner.py) wraps this. It accepts allow_skills and
exclude_skills, and — when given a manifest — runs a ResourceGuard preflight that
automatically excludes skills whose required indexes are unavailable and surfaces
warnings in the system prompt:
from codenib.agent.runner import AgentRunner
from codenib.agent.skills.registry import SkillRegistry
runner = AgentRunner(
model="gpt-4o",
registry=SkillRegistry(),
allow_skills={"bm25_search", "find_callers", "llm_rerank"},
)
result = runner.run("How does authentication work in this repo?")
Running the agent¶
query() is the one-call entry point. Pass exactly one index source in
CodeNibAgentOptions: repo_path builds the indexes inline, manifest loads
a compiled repo_manifest.json, and contexts reuses objects you already
constructed.
from codenib.agent.runner import CodeNibAgentOptions, query
result = query(
"Where is request retry behavior implemented?",
options=CodeNibAgentOptions(
manifest="/path/to/repo_manifest.json",
allowed_skills=["bm25_search", "find_callers"],
max_turns=10,
),
)
By default the agent pulls context through tools. The first prompt carries the task, the tool descriptions, and any missing-index warnings, but no retrieved code. These options change how context reaches the model:
| Option | Where | Effect |
|---|---|---|
allowed_skills / excluded_skills (allow_skills / exclude_skills on AgentRunner), compile_table |
both | Choose the skill set, directly or by a per-query scenario table |
max_turns, max_context_tokens |
both | Bound the loop and the retained chat history |
enable_lsp_route_context and the lsp_route_* options |
both | Add static LSP route anchors to the first prompt; the agent must still read source before answering |
sandbox |
both | Replace read, grep, glob, and bash with container-backed tools; requires a revision-matched manifest. See Isolated agent execution |
default_tool_ids, include_default_tools |
AgentRunner |
Narrow or withhold the four default tools |
first_turn_tool_choice, force_first_turn_only |
AgentRunner |
Force tool calls until the first successful read, or on turn 0 only |
compact_after_read, compact_keep_reads |
AgentRunner |
After the first successful read, shorten earlier bulky tool outputs in the history to references |
force_final_answer, review_final_answer |
AgentRunner |
Add a tool-free final-answer turn, or one evidence-review pass, for question-answering callers |
Context methods compares
these mechanisms with MCP's explore_context and with the evaluation
harness's pre-loaded context arms.
Default tools vs skills¶
The always-on filesystem primitives — read / grep / glob / bash — are a
separate type from skills. They are ToolSpec objects (codenib/agent/tools/spec.py)
in a ToolRegistry the runner holds alongside the SkillRegistry, registered via
ensure_default_tools_registered. They carry no index_requirements, no operator,
and no retrieval skill_type, and are never narrowed by allow_skills /
exclude_skills (use the runner's default_tool_ids to restrict which defaults are
exposed, or include_default_tools=False to withhold them).
Available skills¶
| Skill | Type | Description |
|---|---|---|
bm25_search |
custom | Fast keyword retrieval (TF-IDF/BM25) over symbol chunks; names_only=true returns compact NAME tags (no bodies), with unified_name relabeling when a graph is present. |
embedding_search |
retrieval | Semantic vector retrieval that matches code by meaning, not literal tokens. |
hybrid_search |
aggregate | Fuses multiple retrievers (e.g. BM25 + embedding) via weighted score normalization. |
codenib_context |
custom | One-call GraphRAG composer: search seeds + deterministic call-graph expansion into a compact set (graph step toggle: CODENIB_COMPOSER_NO_GRAPH=1). |
repository_search |
custom | Repository-wide source retrieval with lexical search and optional semantic reciprocal-rank fusion; excludes supporting material by default. |
find_callers |
expand | Incoming call-graph edges of a symbol (who calls X). |
find_callees |
expand | Outgoing call-graph edges of a symbol (what X calls). |
trace |
expand | Shortest call path between two symbols. |
lsp_route |
expand | Symbol-name semantic navigation over the static symbol graph: ranks endpoint/bridge/provider/type route anchors for a query, optionally with one-hop neighbors. |
lsp_definition |
expand | LSP-compatible go-to-definition for the identifier at an exact source position (repo-relative file_path, 1-based line, 0-based character), served by the runtime's selected provider: an injected live LSP provider, otherwise SCIP occurrences persisted with the graph, otherwise symbol-graph heuristics. |
lsp_references |
expand | LSP-compatible find-references at an exact source position, with optional include_declaration; same position contract and provider selection as lsp_definition. |
llm_rerank |
rerank | High-precision LLM-judged reranking to refine top results. |
crossencoder_rerank |
custom | Pairwise reranking that scores each query/candidate pair with a cross-encoder; requires a CrossEncoderContext in the runner's skill contexts. |
code_to_query |
transform | Packs retrieved code nodes into a reusable follow-up search query. |
Navigation skills and the LSP provider¶
The definition, reference, and route skills all use the same LSP-shaped
navigation provider, injected into ExpandContext. Since 0.2.2, a context bound
to an index, including a C/C++-only build, answers them from the verified
persisted symbol graph. Native clangd fact-query postings are benchmarked
separately, but their project-local .idx files change in place and carry no
source-selection receipt or allowed-file proof yet, so the agent runtime does
not use them. Portable artifacts and mixed-language contexts take the same
persisted route. Each result keeps its backend, fallback, capability, and
snapshot metadata, so an agent trace can explain the route without changing the
location shape the agent sees.
Line-numbering at the agent boundary¶
Retrieval and graph representations used by skills are 0-based — BM25
documents, FAISS metadata, symbol-graph anchors, and tree-sitter CodeChunk
objects count from 0. The agent boundary is 1-based outward: line numbers
shown to, and accepted back from, the LLM are 1-based. Dataset
CodeLocation values are also 1-based; _chunk_to_code_block in
dataset/gt_locate.py performs that separate output conversion.
All conversion lives in one module, codenib/agent/boundary.py, with one
site per direction:
- Output —
AgentRunner._serialize_resultruns every line-bearing result throughto_agent_repr(+1) before it reaches the LLM, so a result's structuredstart_line/end_lineagree with its renderedcontentgutter (whichwrap_code_snippetalready renders 1-based). - Input — a skill input declared
is_line_number: truein itsconfig.yamlis passed throughfrom_agent_repr(-1) by the runner before the executor sees it, so executors keep working in 0-based internals.
The native LSP skills follow the same contract: lsp_definition /
lsp_references declare their line input is_line_number: true (1-based
in, 0-based internally), while character stays 0-based end to end,
matching the native LSP wire convention.
Authoring a skill that accepts line numbers¶
Mark the input in config.yaml; the runner handles the conversion:
inputs:
- name: ranges
type: List[List[int]]
is_line_number: true
description: 1-based [start, end] line ranges (converted to 0-based for you)
Do not add ad-hoc +1/-1 in executors or rendering paths — route through
the boundary helpers so the offset lives in exactly one place.