Every AI coding session starts the same way. The assistant lists directories, opens files, greps for function names, reads the README again, and slowly rebuilds a picture of the project it already understood yesterday. On a small repo that costs a minute. On a large one it eats a real share of the context window before any work starts, and the picture it builds is still incomplete.
I call this the re-discovery tax, and I built AIMFP (AI Modular Functional Procedural) to stop paying it.
AIMFP is an MCP server (pip install aimfp, or a Claude Code plugin) that gives the AI a persistent, structured database of the project: files, functions, types, call interactions, modules, tasks, decisions and notes. The AI keeps that database up to date as it works and reads from it in the next session, so it doesn't have to re-read the source tree.
This post covers what AIMFP is, then spends most of its time on the part I think is most useful to other people building agent tooling: how it manages context.
What AIMFP is, briefly
- An MCP server with about 315 tools and 131 directives (step-by-step workflows), all stored in SQLite.
-
Four databases:
-
aimfp_core.dbis read-only and ships in the wheel. It holds directives, helper definitions, and the next-step guidance attached to each tool. -
project.dbholds the project's state: files, functions, interactions, types, themes, flows, modules, completion paths, milestones, tasks and notes. -
user_preferences.dbholds per-directive preferences and project settings. -
user_directives.dbis optional. It's used for automation projects, where the AI generates code from rules you write in YAML/JSON.
-
- An opinionated coding style: functional-procedural code with pure functions, immutable data, no OOP, and side effects isolated at the edges. This is deliberate. Pure functions with explicit inputs and outputs are much easier to index, and an accurate index is what makes the rest of the system work.
- A modularity map. Every piece of work is placed on the map before it's written:
theme (stable project area)
└─ flow (one behaviour, ≥ 1 theme)
└─ files (declare their flows when reserved)
└─ module (reusable domain code at a directory seam)
Completion paths move themes forward, milestones build flows, and tasks carry flow IDs. Feature files are thin orchestrators that compose module functions.
-
Local only. No network calls and no telemetry. All state lives in
.aimfp-project/inside your repo.
AIMFP also tracks its own development. Right now its own project.db holds 131 files, 1,303 functions, 2,920 call interactions, 16 modules, 20 flows, 9 themes and 74 completed tasks. Most of the design decisions below came from running into problems while using it on itself.
The core rule: the database is the index, source files are the last resort
AIMFP's system prompt says this directly: use DB tools before reading source files.
Instead of:
ls -R src/ → cat 12 files → grep "def calculate" → cat 3 more files
the AI calls:
get_function_by_name("calculate_total")
→ file, purpose, parameters (typed), returns, notes attached to it
get_interactions_by_function(fn_id)
→ what it calls, what calls it
Each answer is a short structured record instead of a file. The AI still opens source files when it has to edit them, but it opens them knowing what it's looking for.
The rule is the easy part. The harder part was keeping all of this from flooding the context window anyway. A database of 1,300 functions can bury the AI just as thoroughly as a source tree if every call returns everything. The rest of this post covers how AIMFP avoids that.
Context management, layer by layer
1. One call to start a session, with only the sections that need attention
The first action of every session is a single call:
aimfp_run(is_new_session=true)
It returns a session bundle: project purpose and goals, the active path, milestone and task, the flows and themes of that work, infrastructure, recently changed files (each tagged with its flows, themes and module), metadata for recent notes, recent history, user settings, watchdog status, and the reference material the AI needs to work.
The important part is what the bundle doesn't include:
-
Sections with nothing to report are dropped. A backup appears only if one was actually taken. A migration notice appears only if a migration is pending. A pure helper,
drop_empty, removes everyNoneand empty container before the payload goes out. -
The full directive catalogue isn't bundled. There are 131 directives and 66 FP-style rules. The AI gets their names in its reference material and loads a directive's steps with
get_directive_by_name(name)only when it's about to run it. - Coding-specific reference material is included only once there's code to write, meaning once a milestone exists. Before that, the next step is project discovery, and the core reference already covers it.
Here's a trimmed version of the bundle AIMFP returned for its own repo today:
{
"position": {
"path": { "id": 1, "name": "AIMFP Ongoing Development" },
"milestone": {
"id": 2, "name": "Features", "status": "pending",
"flows": [
{ "id": 1, "name": "Session Lifecycle Flow", "themes": ["Helper / MCP Tools"] },
{ "id": 19, "name": "Tracking Audit Flow", "themes": ["Database Operations", "Helper / MCP Tools"] }
]
}
},
"recent_files": [
{ "path": "src/aimfp/helpers/project/tasks.py",
"flows": [{ "id": 4, "name": "Project Tracking Flow" }],
"themes": ["Database Operations", "Helper / MCP Tools"],
"module": "helpers_project" }
],
"history": {
"last_completed_task": { "id": 75, "name": "bump-version.py covers server.json (MCP Registry) versions" }
},
"watchdog": { "status": "ok", "reminders": [] },
"deferred_notes": [{ "id": 103, "reference_table": "files", "reference_id": 96 }]
}
From this one payload the AI knows where the work is, what was done last, what's deferred, and which parts of the map are involved, without opening a single file.
2. Status in tiers, sized to how often each one is called
aimfp_status has three tiers, and the tier is chosen by call frequency, not by how much information exists:
| Tier | When it's used | What it returns |
|---|---|---|
quick (default) |
Inside work loops, "where am I?" | Path, milestone, focused task with item names, its flows and themes, other open work, structure gap count |
summary |
After lost or compacted context |
quick + purpose and goals, infrastructure, recent files with their map annotations, recent notes, history, active branches, core reference |
detailed |
Rarely |
summary + the full work tree |
Focused-work descriptions are capped at 300 characters in summary, and quick omits them entirely. The quick tier is the one that gets called many times per session, so it's the one kept smallest.
Compaction is handled explicitly. When the client compacts the conversation, the AI has lost details but the database hasn't. aimfp_status(type='summary') is the documented recovery call, and the Claude Code plugin's SessionStart hook matches startup|resume|compact so the AI is pointed back at AIMFP straight after a compaction.
3. Checkpoint calls that return nothing when nothing is wrong
During work the AI calls aimfp_run(is_new_session=false) at checkpoints. This call only returns watchdog alerts. If there are none, the data object is empty ({}).
That's an intentional design choice. A checkpoint that always returns a status block teaches the model to skim it. A checkpoint that's usually empty gets read when something does come back.
4. Instructions arrive just in time, not all at once
This is the part I'd most recommend to anyone building MCP tooling.
Most agent setups put every rule in the system prompt and hope the model remembers the relevant one at the right moment. AIMFP keeps the system prompt short. It sets the non-negotiable rules and the session-start call, and the detail lives in two places:
Directives in the database. A directive like project_task_decomposition or project_milestone_complete is a stored workflow with a main sequence, conditional branches and a fallback. The AI loads it by name right before running it. When the workflow is done, its steps can fall out of context without anything being lost, because they're still in the database.
Return statements on tool results. Every tool result can include return_statements, which are short next-step instructions defined per tool in aimfp_core.db. There are about 320 of them across the tool definitions. They're gated on the payload, so a statement is sent only when the key it refers to is present. For example, the bundle above included deferred_notes, so the result also included:
"data.deferred_notes lists deferred work (metadata only). Read the ones relevant to the
current work with get_notes_comprehensive(note_id=X); resolved ones → update_note(note_type='completed')."
If there had been no deferred notes, the AI wouldn't have seen that instruction at all. When aimfp_run bundles sections owned by other tools, it attaches those tools' gated return statements too, and removes duplicates.
So the guidance the AI receives depends on the state of the project, not on everything it might ever need to know. Users can also attach their own return statements to any tool (set_custom_return_statement) if they want a specific behaviour every time that tool is called.
5. Short reads by default, full detail on request
Large reads are short by default, and the AI has to ask for full detail:
- Milestone getters,
get_all_flowsandget_tasks_by_milestonereturndescription: nullunless the call passesdetail_level='full'. They can also return task counts per status from a single grouped query, so getting an overview never requires writing SQL. -
audit_tracking, which compares the database against the actual code, writes its full findings to a JSON file on disk and returns counts, a few samples per category (sample_limit, default 5), and the report path. The fix tool,apply_audit_fixes, reads that report from disk, so the full list of findings never has to pass through the context window. -
search_notesreturns capped previews. The AI reads a specific note in full only when it looks relevant. - Single-entity lookups (a file, function, type, flow or module) include references to attached notes rather than the note bodies.
- Function parameters always come back in one normalized shape, a parsed list of
{name, type, required, default, description, kind}. Records written by different sessions in different formats don't make the AI spend tokens working out which format it's looking at.
6. Show the part of the map the work touches
It would be easy to include the whole theme/flow/module map in every status call. AIMFP doesn't. From the status module's docstring:
Structure (themes, flows, modules) is attached to what the payload already shows. It is never listed whole: a full map every call is a list the AI has to sort for relevance itself, which buries the part that matters now.
So the active milestone comes with its flows, a recent file comes with its flows, themes and module, and the full map is a separate call (get_all_flows, get_structure_health) for when the AI actually needs it.
Structural gaps get the same treatment: flows without a theme, files without a flow, files outside their module, oversized flows. The watchdog turns them into one summary reminder with counts per gap type, not one reminder per gap.
7. A watchdog that keeps the index honest
None of this is useful if the database is wrong. A stale index is worse than none, because the AI trusts it.
When a session starts, aimfp_run launches a detached filesystem watchdog (the one runtime dependency is watchdog). It compares the database with what's on disk and records reminders for:
- new files that aren't tracked
- functions in the code that are missing from the database, and the reverse
- deleted files
- stale timestamps
- audit drift and structure gaps
Before the first payload is built, the session-start call runs a synchronous reconciliation pass, so the first answer doesn't race the watcher. Checkpoint calls also link edited tracked files to whichever task is in progress. Edits made without a tracking call still appear in that task's context and in its completion audit.
8. Writes are what make reads trustworthy
All of the above saves tokens on reads only if the writes happen. AIMFP's write-side discipline is the file coding loop:
reserve file (with flow_ids) → search_modules for overlap → reserve functions + types
→ write the code → finalize file → assign to module
→ finalize functions + types (purpose, params, returns) → add interactions → link types to functions
AIMFP enforces this loop in a few ways:
- Finalize gates. Finalizing rejects a function with no purpose, parameters or return value, and warns when the recorded parameters don't match the actual signature.
- A completion gate. Marking a task complete audits the files linked to it against the code. If the drift is serious enough to block completion, the refusal includes the exact tool calls that fix it.
-
Plugin hooks. A
PostToolUsehook onWrite|Editreminds the AI to track source files, and aStophook checks thataimfp_endran when you said "wrap up".
The tracking work costs tokens. A continuation checkpoint is cheap (around 2k tokens), batch helpers like reserve_files and finalize_functions cut down on round-trips, and all analytics are opt-in. The trade is to spend a little on every write so the next session doesn't spend a lot re-reading. Past a handful of files, that trade pays for itself many times over.
What this looks like in practice
The user starts a session with "continue where we left off". The AI:
- Calls
aimfp_run(is_new_session=true)and reads the bundle. - Knows the active milestone, the focused task and its open items, which flows are involved, and that note #103 is deferred work on file #96.
- Calls
search_noteswith the task's keywords to check for past decisions. - Uses
get_function_by_nameandget_interactions_by_functionto find exactly what to change. - Opens only the files it's about to edit.
- Writes the code, runs the tracking loop, marks items done, and completes the task through the audit gate.
- If the conversation gets compacted halfway through, calls
aimfp_status(type='summary')and carries on.
At no point does it list the source tree.
Lessons for anyone building agent tooling
Some of these apply whether or not you ever use AIMFP:
- Pick payload size by call frequency. The call made fifty times a session should be the smallest one.
- Leave out empty sections. Every empty key is something the model has to read and ignore.
- Send instructions with the state they're about. Attaching guidance to tool results, gated on payload content, works better than a longer system prompt.
- Keep full reports on disk and return pointers. Return counts, samples and a path, and let the fix tool read the file.
- Show the relevant slice of the map, not the whole map.
- Make "nothing to report" an actual empty result.
- Plan for compaction. Have one documented call that rebuilds working context from durable state.
- An index has to be enforced to stay accurate. Without gates and a watcher, it drifts out of date.
Caveats
- FP only. AIMFP refuses OOP codebases. It's opinionated on purpose, since pure functions are what keep the index reliable.
- Linux only. There's no platform-specific code, so other operating systems are untested, not just unsupported.
-
About 315 tools. Claude Code asks for approval on each MCP tool the first time it's used.
get_claude_permissionsproduces an allowlist so that happens only once. - It depends on the AI following the process. Gates, hooks and return statements make that much more likely, but the system prompt is still what holds it together.
- License: PolyForm Noncommercial 1.0.0 for every version after 1.66.1. Personal, research and educational use is free, and commercial use needs a license. Versions up to 1.66.1 remain MIT.
Try it
Claude Code (recommended): you need uv installed.
/plugin marketplace add aryanduntley/aimfp
/plugin install aimfp@aimfp
Then ask: "Add the AIMFP system prompt", followed by "Initialize AIMFP for my project".
Other MCP clients:
pip install aimfp
# register: python3 -m aimfp (stdio transport)
python3 -m aimfp --system-prompt # paste into your client's instructions
- GitHub: github.com/aryanduntley/aimfp
- PyPI: pypi.org/project/aimfp
I'd like to hear how other people are handling context in their agent setups, especially what you do after compaction. Let me know in the comments.
Top comments (0)