Skip to content

ACE-045: per-process model cache + single SQL parse + shared guard indices - #103

Merged
ashwin-agami merged 3 commits into
mainfrom
ACE-045-model-cache-single-parse
Jul 11, 2026
Merged

ashwin-agami merged 3 commits into
mainfrom
ACE-045-model-cache-single-parse

Conversation

@ashwin-agami

Copy link
Copy Markdown
Contributor

Summary

Behaviour-preserving perf hardening (audit finding P2, feature F12-runtime-scalability). The semantic model is parsed/indexed far more than needed per query. Two independent wins, same answers:

  • Slice 1 — GuardContext (both paths). The _model_safety guard battery re-parsed the SQL and rebuilt each model index per guard. A GuardContext now parses once + builds each index once, threaded through the guards.
  • Slice 2 — get_cached_org (long-lived server). The parent loaded the whole model 2–3× per query and re-loaded it every query. It's now cached per process, keyed (org_id, datasource, model_version), served warm across queries + users.

Changes

  • semantic_model/runtime.pyGuardContext + build_guard_context; optional ctx= on the 6 guards + _preflight_select/_check_aggregation_semantics. ctx=None keeps the standalone (cli.py) path byte-identical.
  • execute_sql.py — build the context once in _model_safety, thread it; rebuild after an auto-rewrite (SQL changes).
  • tools.pyget_cached_org + _current_org_ctx/_current_org_id; route _resolve_units/_resolve_receipt/tool_get_datasource_schema. Bypass the cache when model_version is None (never serve a stale model).
  • mcp_http.py — set _current_org_ctx per request in handle_mcp from the resolved org (mirrors _actor_ctx). Tenant-safe by construction: the cache key is org-scoped via the OrgResolver seam — N=1 today, per-request under a multi-tenant resolver, no fork, no F14 dependency.
  • tests/test_guard_context.py, test_org_cache.py, conftest.py (isolates the module-global cache).
  • Synced the bundled lib/ mirror.

Test plan / acceptance

  • Slice 1: parse=1 + each-index-built=1 counter; verdict-parity with/without ctx across 6 SQL cases; unparseable→allow.
  • Slice 2: 1 load/query + 0 on repeat; reload after a version bump; two orgs on the same datasource get separate models (no cross-tenant); default org_id → local.
  • Full gate green: 1413+ tests, ruff + format + gitleaks + lib-drift all pass (uv run dev.py check).

Out of scope (per spec)

Cross-query caching on the subprocess/file path (needs ACE-028); the multi-tenant resolver (agami-hosted F5) — this only consumes the OrgResolver port.

Spec: ACE-045

…once

Thread an optional GuardContext (one parsed sqlglot tree + the 4 model indices) through the
_model_safety guard battery, so the SQL is parsed once and each index built once instead of
6x parse + per-guard index rebuild (audit P2). Backward-compatible: ctx=None keeps the
standalone callers (cli.py) byte-identical; rebuild ctx after an auto-rewrite (SQL changes).
Synced the bundled lib mirror (execute_sql.py). Tests: parse=1/index-build=1 counters,
verdict-parity with/without ctx, unparseable degrade-to-allow. Full gate green (1413 tests).

Spec: ACE-045
Cache the Organization in the long-lived parent, keyed (org_id, datasource, model_version),
so the 2-3 loads/query collapse to 1 and it serves warm across queries + users. org_id comes
from the OrgResolver seam via a _current_org_ctx ContextVar the HTTP server sets per request
(mcp_http handle_mcp), falling back to AGAMI_ORG_ID/'local' (stdio/single-tenant) -> tenant-safe
by construction, no F14 dependency, no downstream fork. Route _resolve_units/_resolve_receipt/
tool_get_datasource_schema through it. Bypass the cache when model_version is None (never serve
a stale model). conftest isolates the module-global cache between tests. Full gate green.

Spec: ACE-045
Copilot AI review requested due to automatic review settings July 11, 2026 08:26

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements ACE-045 performance hardening by (1) introducing a per-invocation GuardContext to avoid repeated SQL parsing and repeated semantic-model index builds across the safety-guard battery, and (2) adding a per-process semantic-model cache (get_cached_org) scoped by (org_id, datasource, model_version) so long-lived servers reuse warm models across queries while remaining tenant-safe via a request-scoped ContextVar.

Changes:

  • Add GuardContext / build_guard_context and thread ctx= through guard functions to reuse a single parsed SQL tree and shared indices.
  • Add per-process org/model caching in tools.py, plus request-scoped org-id propagation in the HTTP MCP transport to keep caching org-scoped.
  • Add tests covering guard-context reuse/parity and org-cache warm hits, version invalidation, and org scoping.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/test_org_cache.py Adds Slice 2 tests validating per-process org/model caching behavior and org scoping.
tests/test_guard_context.py Adds Slice 1 tests ensuring single-parse/single-index-build and parity with/without ctx.
tests/conftest.py Adds a global autouse fixture to isolate module-global org cache state between tests.
packages/agami-core/src/semantic_model/runtime.py Introduces GuardContext and updates guards to optionally reuse parsed SQL + indices.
packages/agami-core/src/execute_sql.py Builds guard context once and threads it through the safety battery; refreshes after rewrite.
plugins/agami/lib/execute_sql.py Syncs the same guard-context threading change into the bundled lib/ mirror.
packages/agami-core/src/tools.py Adds (org, datasource, version) model caching and uses it in tool paths that previously reloaded models.
packages/agami-core/src/mcp_http.py Sets _current_org_ctx per request so the per-process model cache remains tenant-scoped.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/agami-core/src/execute_sql.py
Comment thread plugins/agami/lib/execute_sql.py
Comment thread packages/agami-core/src/semantic_model/runtime.py Outdated
Comment thread tests/test_org_cache.py Outdated
…lglot, unify pre_flight parity, drop dup cache fixture

- build_guard_context returns None when sqlglot unavailable (guards short-circuit to allow before touching ctx, so index-building was wasted work)
- pre_flight_check non-ctx path routes through _parse_sql so ctx/non-ctx are byte-identical for unparseable SQL (was: 'unparseable; skipped' vs 'no SELECT; skipped')
- remove per-file _isolate_cache fixture; conftest._reset_org_cache already covers it
@ashwin-agami

Copy link
Copy Markdown
Contributor Author

Thanks @copilot — all three addressed:

  • build_guard_context builds indices even when sqlglot is unavailable — good catch; it now returns `None` early when `not _HAVE_SQLGLOT`, since every guard short-circuits to allow before it touches the context. Fixed centrally in `build_guard_context` so both call sites (initial + post-rewrite rebuild) benefit.
  • pre_flight_check parity for unparseable SQL — real divergence in the byte-identical `.as_dict()` invariant this spec claims: with `ctx`, `ctx.tree` is `None` → "no SELECT; skipped", but the non-ctx path raised → "unparseable; skipped (…)". Unified by routing the non-ctx branch through the same `_parse_sql` helper, so both paths are now identical.
  • duplicate autouse cache fixture — removed the per-file `_isolate_cache`; `conftest._reset_org_cache` already covers it (and dropped the now-unused `pytest` import).

Gate green (1417 passed). Pushed.

@ashwin-agami
ashwin-agami merged commit 7cdbd36 into main Jul 11, 2026
6 of 7 checks passed
@ashwin-agami
ashwin-agami deleted the ACE-045-model-cache-single-parse branch July 11, 2026 09:58
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 11, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants