PydanticAI lead agent orchestration with read-only explorer subagent and boundary-enforced runtime tools
Lerim’s agent runtime orchestrates all memory operations through a PydanticAI lead agent with strict write boundaries and read-only subagent delegation.
Read file contents with line numbers and boundary enforcement:
# src/lerim/runtime/agent.py:244-259@agent.tooldef read( ctx: RunContext[RuntimeToolContext], file_path: str, offset: int = 1, limit: int = 2000,) -> str: """Read a file and return numbered lines. If file_path is a directory, list its entries instead. Use offset/limit to paginate large files.""" return read_file_tool( context=ctx.deps, file_path=file_path, offset=offset, limit=limit, )
See runtime/tools.py:184-216 for implementation with read boundary checks.
Write structured memory records (Python builds frontmatter):
# src/lerim/runtime/agent.py:326-346@agent.tooldef write_memory( ctx: RunContext[RuntimeToolContext], primitive: str, title: str, body: str, confidence: float = 0.8, tags: list[str] | None = None, kind: str | None = None,) -> dict[str, Any]: """Write a structured memory record. Python builds the markdown — pass fields directly. primitive must be 'decision' or 'learning'. kind is required for learnings (insight/procedure/friction/pitfall/preference).""" return write_memory_tool( context=ctx.deps, primitive=primitive, title=title, body=body, confidence=confidence, tags=tags, kind=kind, )
The LLM never writes YAML frontmatter directly. It passes structured fields to write_memory, which builds the markdown file in Python. This prevents formatting errors and injection attacks.
The lead agent delegates evidence gathering to a read-only explorer subagent:
# src/lerim/runtime/agent.py:293-308@agent.toolasync def explore( ctx: RunContext[RuntimeToolContext], query: str,) -> dict[str, Any]: """Delegate read-only evidence gathering to explorer subagent. Async so PydanticAI can run multiple explore calls in parallel when the LLM emits them in the same tool-call turn (max 4). """ result = await get_explorer_agent().run( query, deps=ctx.deps, usage=ctx.usage ) return result.output.model_dump()
The explorer is a separate PydanticAI agent with only read tools:
# src/lerim/runtime/subagents.py:21-44def _build_explorer(model=None) -> Agent[RuntimeToolContext, ExplorerEnvelope]: """Build read-only explorer subagent with glob/read/grep tools.""" agent = Agent[RuntimeToolContext, ExplorerEnvelope]( model=model or build_orchestration_model("explorer"), output_type=ExplorerEnvelope, deps_type=RuntimeToolContext, name="lerim-explorer", instructions="""You are a read-only explorer for Lerim memories and workspace artifacts.Memory layout (base_path defaults to the memory root passed by the lead agent):- decisions/*.md — architecture/design decisions- learnings/*.md — insights, procedures, facts- summaries/YYYYMMDD/HHMMSS/*.md — session summariesEach .md file has YAML frontmatter (id, title, confidence, tags, kind, created) then a markdown body.Search strategy:1. Use grep to find memories by keyword, title, or tag.2. Use glob to list files when you need to scan a directory.3. Use read to get the full content of specific files found by grep/glob.Return structured evidence with file paths.""", retries=1, )
Explorer tools (read-only):
glob — Find files by pattern
read — Read file contents
grep — Search file contents by regex
Why a separate explorer agent?
The explorer subagent serves two purposes:
Tool isolation: The lead agent cannot accidentally call write operations during evidence gathering
Parallel search: PydanticAI runs multiple async explore calls in parallel when the LLM emits them in the same turn (up to 4 concurrent searches)
This design makes searches faster while maintaining strict boundaries.
The lead agent follows a deterministic decision policy during sync:
Extract candidates: Run extract_pipeline to get raw candidates
Search existing: Delegate to explore to find similar existing memories
Compare: Evaluate semantic overlap and confidence scores
Decide: For each candidate:
add — No existing match found
update — Existing memory found with lower confidence or incomplete information
no-op — Existing memory is already high-quality
Write: Call write_memory for add decisions, edit for update, skip for no-op
This policy is encoded in the lead agent’s prompt, not in code.
The decision policy uses the explorer’s structured search results to make deterministic choices. This reduces hallucination compared to asking the LLM to “decide if similar memories exist.”