Architecture: agentic-tools¶
Package name:
agentic-tools| Version: 0.1.0 | Python: 3.11+
Executive summary¶
agentic-tools is the shared utility layer for the tafreeman/agentic-runtime-platform monorepo. It
provides a multi-provider LLM client abstraction, a disk-backed SHA-256
response cache, model probing and provider discovery, a model bakeoff runner, a benchmark and
LLM-as-judge evaluation framework, a structured error taxonomy, and script bootstrap helpers.
The package is defined by the repo-root pyproject.toml (project name agentic-tools), which is
also the uv workspace root; there is no tools/pyproject.toml. Members agentic-workflows-v2 and
agentic-v2-eval declare agentic-tools as a workspace dependency.
The central design choice is a static facade + provider adapters + shared disk cache pattern.
Callers never instantiate a client object; they call LLMClient.generate_text(model_name, prompt)
as a static method. The method dispatches to one of several routing backends (local ONNX and Windows AI are distinct local backends) based on a name prefix
in the model string. A 24-hour, SHA-256-keyed disk cache sits in front of all provider calls when
enabled.
Dependency diagram¶
graph TD
subgraph workspace["agentic-tools (tools/, defined by the repo-root pyproject.toml)"]
core["tools/core\nconfig · errors · cache\nresponse_cache · prompt_db"]
llm["tools/llm\nLLMClient · provider_adapters\nModelProbe · ModelInventory\nbakeoff · rank_models"]
bench["tools/agents/benchmarks\nBenchmarkRunner · llm_evaluator\nevaluation_pipeline · registry"]
core --> llm
core --> bench
llm --> bench
end
awv2["agentic-workflows-v2\n(runtime + server)"]
eval["agentic-v2-eval\n(evaluation framework)"]
workspace --> awv2
workspace --> eval
awv2 -.->|"langchain/model_builders (legacy local: builder)\nimports LLMClient"| llm
awv2 -.->|"server/datasets reads\nbenchmark datasets + loader"| bench
eval -.->|"adapters/llm_client wraps LLMClient"| llm
eval -.->|"datasets bridges the\nbenchmark registry"| bench
Technology stack¶
| Component | Technology | Notes |
|---|---|---|
| Language | Python 3.11+ | from __future__ import annotations throughout |
| Build backend | hatchling | pyproject.toml as single config source |
| Workspace manager | uv | Workspace root is the repo-root pyproject.toml; members: agentic-workflows-v2, agentic-v2-eval |
| HTTP (sync) | urllib.request stdlib |
Ollama, Azure Foundry, Azure OpenAI adapters |
| HTTP (async) | aiohttp |
Available for async provider backends |
| Data validation | Pydantic v2 | model_dump() / model_validate() — not .dict() / .parse_obj() |
| OpenAI SDK | openai >=3.27.0,<4 |
OpenAI and Azure OpenAI providers |
| Anthropic SDK | anthropic >=1.13.0,<2 |
Claude provider |
| Numeric | numpy >=2.4.6,<3 |
Upper bound for semver safety |
| Optional: Google | google-generativeai |
Gemini provider |
| Optional: ONNX | onnxruntime-genai |
Local ONNX model inference |
| Optional: Windows AI | .NET bridge + WinRT | Phi Silica NPU (Copilot+ PC) |
| GitHub Models | gh CLI subprocess |
Retired upstream 2026-07-30; the gh: adapter remains but calls now fail (ADR-016) |
| LangChain | langchain-core >=1.6.9,<2 |
Thin adapter in langchain_adapter.py |
The ranges above mirror the repo-root pyproject.toml; that file and uv.lock are the source of truth.
Package structure¶
pyproject.toml # Repo root: package definition (agentic-tools) + uv workspace root
tools/
├── __init__.py
├── llm/ # LLM client and provider layer
│ ├── llm_client.py # LLMClient static facade — primary entry point
│ ├── provider_adapters.py # All provider adapter functions
│ ├── langchain_adapter.py # LangChain compatibility shim
│ ├── local_model.py # ONNX model wrapper (onnxruntime-genai)
│ ├── local_models.py # LOCAL_MODELS catalog (key → aigallery path)
│ ├── local_model_cli.py # CLI for local model operations
│ ├── local_model_discovery.py # Auto-detect from ~/.cache/aigallery
│ ├── model_probe.py # ModelProbe class (slim facade + public API)
│ ├── model_registry.py # Curated model registry constants
│ ├── check_provider_limits.py # Provider rate-limit inspection CLI
│ ├── list_gemini.py # Gemini model listing helper
│ ├── probe_config.py # ProbeResult dataclass, constants, cache I/O, with_retry
│ ├── probe_providers.py # Per-provider probe dispatch
│ ├── probe_providers_cloud.py # Cloud probe logic (GitHub, OpenAI, Gemini, Claude, Azure)
│ ├── probe_providers_local.py # Local probe logic (ONNX, Ollama, LM Studio, AITK)
│ ├── probe_discovery.py # discover_all_models() — multi-provider orchestration
│ ├── probe_discovery_providers.py # Underlying per-provider discovery helpers
│ ├── model_inventory.py # build_inventory() — passive + active capability scan
│ ├── model_locks.py # PID-based lock files for concurrent ONNX loading
│ ├── model_bakeoff.py # Multi-model bakeoff runner
│ ├── bakeoff_tasks.py # TaskSpec dataclass + TASKS list
│ ├── bakeoff_reporting.py # _write_reports(), _recommend_alignment()
│ ├── rank_models.py # Score-based model ranking from probe + limits data
│ ├── run_local_concurrency.py # Local concurrency stress harness
│ ├── windows_ai.py # WindowsAIModel — .NET bridge to Phi Silica
│ └── windows_ai_bridge/ # C# .NET bridge project (PhiSilicaBridge.csproj)
├── core/ # Shared utilities
│ ├── config.py # ModelConfig, PathConfig, Config dataclasses
│ ├── errors.py # ErrorCode StrEnum + classify_error()
│ ├── cache.py # Function-based cache API (delegates to ResponseCache)
│ ├── response_cache.py # ResponseCache class: SHA-256, 24h TTL, disk-backed
│ ├── prompt_db.py # JSON-file-backed prompt/rubric/evaluation store
│ ├── _encoding.py # Windows console encoding fix (UTF-8)
│ ├── tool_init.py # ToolInit bootstrap dataclass
│ ├── local_media.py # Local media file handling
│ └── model_availability.py # Runtime model availability helpers
├── agents/
│ ├── repo_analyzer/ # Repository analysis agent helpers
│ └── benchmarks/ # Benchmark infrastructure
│ ├── registry.py # BenchmarkRegistry + PRESET_CONFIGS
│ ├── datasets.py # BENCHMARK_DEFINITIONS per benchmark
│ ├── loader.py # load_benchmark() + clear_cache()
│ ├── runner.py # run_benchmark() + interactive CLI
│ ├── runner_commands.py # cmd_* CLI handler functions
│ ├── runner_ui.py # colorize, print_table, interactive_mode
│ ├── llm_evaluator.py # LLM-as-judge: build_evaluation_prompt, evaluate_with_llm
│ ├── evaluator_models.py # EvaluationResult, DimensionScore, SCORE_RUBRIC
│ ├── evaluator_reporting.py # summarize_batch_results, save_evaluation_report
│ ├── evaluation_pipeline.py # evaluate_task_output_llm, get_gold_standard_for_task
│ └── workflow_pipeline.py # extract_workflow_data, save_workflow_phases_md
└── tests/ # Key-free test suite
├── conftest.py
├── test_benchmark_pipeline.py
├── test_config.py
├── test_errors.py
├── test_evaluation_pipeline.py
├── test_llm_client.py
├── test_llm_evaluator.py
├── test_local_model_parsing.py
├── test_model_inventory.py
├── test_model_probe.py
├── test_model_registry_constants.py
├── test_probe_discovery_cloud.py
├── test_repo_analyzer.py
├── test_tool_init.py
└── test_workflow_pipeline.py
LLM client¶
Core interface¶
from tools.llm.llm_client import LLMClient
# Synchronous, static — no instance required
response = LLMClient.generate_text(
model_name="gh:gpt-4o-mini",
prompt="Explain Kahn's algorithm.",
system_instruction="You are a concise technical writer.", # optional
temperature=0.7, # default
max_tokens=4096, # default
)
LLMClient is a pure static-method facade. No instance state exists. Thread safety is delegated
to the ResponseCache layer, which uses a threading.Lock on all read/write operations.
Call flow¶
Caller
└─ LLMClient.generate_text(model_name, prompt, ...)
├─ ResponseCache.get(sha256_key) ← 24h disk cache
│ └─ hit → return cached string
└─ miss → provider_adapters.dispatch(prefix)
├─ local:* → call_local() (onnxruntime-genai)
├─ ollama:* → call_ollama() (urllib.request REST)
├─ windows-ai:* → call_windows_ai() (.NET bridge → Phi Silica)
├─ azure-foundry:* → call_azure_foundry() (urllib.request REST)
├─ azure-openai:* → call_azure_openai() (openai.AzureOpenAI SDK)
├─ gh:* → call_github_models() (gh CLI subprocess)
├─ openai:* → call_openai() (openai.OpenAI SDK)
├─ gemini:* → call_gemini() (google.generativeai SDK)
└─ claude:* → call_claude() (anthropic.Anthropic SDK)
└─ ResponseCache.set(sha256_key, response)
Provider routing table¶
| Prefix | Provider | Transport | Required Env Var(s) |
|---|---|---|---|
local:* |
Local ONNX | onnxruntime-genai in-process |
none; models in ~/.cache/aigallery |
ollama:* |
Ollama REST | http://localhost:11434 (default) |
OLLAMA_HOST (optional override) |
windows-ai:* |
Windows Copilot Runtime / Phi Silica | .NET bridge subprocess | Win 11 Copilot+ PC + dotnet |
azure-foundry:* |
Azure AI Foundry | urllib.request REST |
AZURE_FOUNDRY_API_KEY, AZURE_FOUNDRY_ENDPOINT_1 |
azure-openai:* |
Azure OpenAI Service | openai.AzureOpenAI SDK |
AZURE_OPENAI_ENDPOINT_0, AZURE_OPENAI_API_KEY_0 |
gh:* |
GitHub Models | gh CLI subprocess |
GITHUB_TOKEN |
openai:* |
OpenAI (hosted) | openai.OpenAI SDK |
OPENAI_API_KEY |
gemini:* |
Google Gemini | google.generativeai SDK |
GEMINI_API_KEY or GOOGLE_API_KEY |
claude:* |
Anthropic Claude | anthropic.Anthropic SDK |
ANTHROPIC_API_KEY or CLAUDE_API_KEY |
bare gpt* |
OpenAI (inferred) | openai.OpenAI SDK |
OPENAI_API_KEY |
bare gemini* |
Google Gemini (inferred) | google.generativeai |
GEMINI_API_KEY |
bare claude* |
Anthropic (inferred) | anthropic.Anthropic |
ANTHROPIC_API_KEY |
Remote provider gate¶
Remote providers (OpenAI, Anthropic, Gemini, Azure variants) are disabled by default. This prevents accidental API spend during local development and CI runs.
# Blocked unless PROMPTEVAL_ALLOW_REMOTE=1
LLMClient.generate_text("gpt-4o", "Hello") # raises RuntimeError
# Exempt from this gate; GitHub still makes a remote, token-backed call
LLMClient.generate_text("local:phi4mini", "Hello") # OK
LLMClient.generate_text("gh:gpt-4o-mini", "Hello") # OK
LLMClient.generate_text("ollama:llama3", "Hello") # OK
LLMClient.generate_text("windows-ai:phi-silica", "Hi") # OK
Set PROMPTEVAL_ALLOW_REMOTE=1 to enable the remote providers (OpenAI, Anthropic, Gemini, and the Azure variants).
GitHub Models is exempt from this application gate, but it still sends a
network request and its availability, quota, and billing depend on the current
account.
Response cache¶
| Property | Value |
|---|---|
| Algorithm | SHA-256 of model, system prompt and prompt content; temperature and max_tokens are folded into the prompt content by the caller |
| TTL | 24 hours (configurable) |
| Storage | ~/.cache/prompts-eval/responses/<first-2-hex>/<sha256_hex>.json plus cache_index.json |
| Max size | MAX_CACHE_SIZE_MB = 500 is defined but not enforced; entries expire by TTL only (cleanup_expired()) |
| Thread safety | threading.Lock on all read/write operations |
| Enable | PROMPTS_CACHE_ENABLED=1 (default) |
| Disable | PROMPTS_CACHE_ENABLED=0 |
The cache is transparent to callers. When PROMPTS_CACHE_ENABLED is on, generate_text() consults
it first and populates it after every successful response; the variable is re-read on each call.
Known defect
llm_client.py imports the cache as top-level modules (from cache import ...) and
core/cache.py does from response_cache import .... Those names resolve only when
tools/core is itself on sys.path. In a normal editable install the import raises
ImportError, which generate_text() catches and treats as "caching unavailable", so the
cache is silently skipped even though PROMPTS_CACHE_ENABLED defaults to on.
Model probing subsystem¶
ModelProbe provides runtime availability checking before evaluation runs. Rather than attempting
an inference that may fail or incur cost, the probe sends a minimal test request and caches the
result with TTL-stratified expiry.
ModelProbe
├─ check_model(model, force_probe=False) → ProbeResult
├─ filter_runnable(models) → list[str]
├─ get_probe_report(models) → dict
├─ clear_cache(model=None)
└─ get_cache_summary() → dict
discover_all_models(verbose=False) → dict # multi-provider scan
build_inventory(active_probes=False) → dict # passive + active capability inventory
TTL strategy: - Successful probe: 1 hour cache - Permanent error (403, model unavailable): 24 hours cache - Transient error (rate limit, timeout): 5 minutes cache
Model bakeoff¶
model_bakeoff.py runs a repeatable capability comparison across discovered models and recommends
values for DEEP_RESEARCH_SMALL_MODEL and DEEP_RESEARCH_HEAVY_MODEL.
Scoring combines:
- Task completion score (0–100, per-task weighted checks)
- Latency penalty (min(elapsed_s, 120) × 0.15)
Output: JSON + Markdown reports written to reports/model-bakeoff by default (--out-dir overrides it).
Benchmark infrastructure¶
tools/agents/benchmarks/ provides an LLM-as-judge evaluation framework usable independently of
the bakeoff. The primary entry point is evaluate_with_llm() in llm_evaluator.py, which scores
output on five weighted dimensions (Completeness, Correctness, Quality, Specificity, Alignment)
on a 0.0–10.0 scale.
Consumers define their own acceptance thresholds. The shared benchmark package does not impose a global coverage or source-quality gate.
Error taxonomy¶
tools.core.errors defines a StrEnum with ten values, two membership sets, and a heuristic
classifier.
| Code | Meaning | Retryable |
|---|---|---|
SUCCESS |
No error | — |
UNAVAILABLE_MODEL |
Model not found or not loaded | No |
PERMISSION_DENIED |
Access denied (403/401) or remote gate | No |
RATE_LIMITED |
Provider rate limit exceeded (429) | Yes |
TIMEOUT |
Request exceeded timeout | Yes |
PARSE_ERROR |
Could not parse LLM response JSON | Yes |
FILE_NOT_FOUND |
Required file path missing | No |
INVALID_INPUT |
Input failed validation | No |
NETWORK_ERROR |
DNS/connection/network failure | Yes |
INTERNAL_ERROR |
Unexpected error | No |
from tools.core.errors import classify_error, ErrorCode
code, should_retry = classify_error("Rate limit exceeded")
# → (ErrorCode.RATE_LIMITED, True)
Core configuration¶
from tools.core.config import default_config
# Loaded at import time from environment
default_config.models.generator_model # GEN_MODEL env var, default "gpt-4o-mini"
default_config.models.reviewer_model # REV_MODEL env var
default_config.models.refiner_model # REF_MODEL env var
default_config.models.generator_temp # 0.7
default_config.models.reviewer_temp # 0.0
default_config.models.refiner_temp # 0.5
Dependency installation¶
# Core (no optional providers) — run from the repo root, which owns the package
pip install -e .
# With Google Gemini support
pip install google-generativeai
# With local ONNX model inference
pip install onnxruntime-genai
# With Windows AI (Phi Silica) — Windows only
pip install winrt-runtime
# Dev dependencies (test, lint, typecheck)
pip install -e ".[dev]"
Environment variables reference¶
| Variable | Provider / Feature | Notes |
|---|---|---|
OPENAI_API_KEY |
OpenAI | Required for openai:* and bare gpt* |
ANTHROPIC_API_KEY |
Anthropic Claude | Required for claude:* |
GEMINI_API_KEY / GOOGLE_API_KEY |
Google Gemini | Required for gemini:* |
GITHUB_TOKEN / GH_TOKEN |
GitHub Models | Required for gh:* |
AZURE_OPENAI_ENDPOINT_0 |
Azure OpenAI | Slot-indexed; _0 through _n for failover |
AZURE_OPENAI_API_KEY_0 |
Azure OpenAI | Paired with endpoint slot |
AZURE_OPENAI_API_VERSION |
Azure OpenAI | Default in this package: 2024-02-15-preview (the runtime's own backend defaults to 2024-10-21) |
AZURE_FOUNDRY_API_KEY |
Azure Foundry | Single key for all Foundry endpoints |
AZURE_FOUNDRY_ENDPOINT_1 |
Azure Foundry | Model slot 1 (phi4mini, phi4) |
AZURE_FOUNDRY_ENDPOINT_2 |
Azure Foundry | Model slot 2 (mistral) |
OLLAMA_HOST |
Ollama | Default: http://localhost:11434 |
OLLAMA_TIMEOUT_SECONDS |
Ollama | Default: 180 |
LOCAL_MODEL_PATH |
Local ONNX | Override; auto-detected from ~/.cache/aigallery |
PROMPTS_CACHE_ENABLED |
Response cache | 1 (default) / 0 to disable |
PROMPTEVAL_ALLOW_REMOTE |
Remote gate | 1 to enable OpenAI/Claude/Gemini/Azure |
GEN_MODEL / REV_MODEL / REF_MODEL |
Config | Override default pipeline model assignments |
PHI_SILICA_LAF_FEATURE_ID |
Windows AI | Limited Access Feature token (if required) |
PHI_SILICA_LAF_TOKEN |
Windows AI | LAF unlock token |
Testing¶
All tests in tools/tests/ are fully mocked — no live LLM calls are made. The suite covers
LLMClient, ModelProbe, ModelInventory, llm_evaluator, evaluation_pipeline, and config.
# From repo root
python -m pytest tools/tests/ -v
# With coverage
python -m pytest tools/tests/ -q --cov=tools --cov-report=term-missing
Markers: slow, integration. Skip with pytest -m 'not integration'.
See tools/tests/test_llm_client.py for representative patterns using unittest.mock.patch.