Extending AgenticAPI¶
Step-by-step guides for adding new components to the framework.
Adding a New Policy¶
Policies evaluate generated code before sandbox execution. They are pure synchronous functions with no I/O.
- Create
src/agenticapi/harness/policy/my_policy.py:
from agenticapi.harness.policy.base import Policy, PolicyResult
class MyPolicy(Policy):
"""Description of what this policy checks."""
my_threshold: int = 100 # Pydantic fields for configuration
def evaluate(
self,
*,
code: str,
intent_action: str = "",
intent_domain: str = "",
**kwargs: object,
) -> PolicyResult:
violations = []
warnings = []
if len(code) > self.my_threshold:
violations.append(f"Code exceeds threshold of {self.my_threshold}")
return PolicyResult(
allowed=len(violations) == 0,
violations=violations,
warnings=warnings,
policy_name=type(self).__name__,
)
- Export from
harness/policy/__init__.pyandharness/__init__.py - Add tests in
tests/unit/harness/test_my_policy.py - Optionally export from
src/agenticapi/__init__.pyfor public API
Adding a New Tool¶
Tools provide agents with access to external systems. They implement the Tool protocol.
- Create
src/agenticapi/runtime/tools/my_tool.py:
from typing import Any
from agenticapi.runtime.tools.base import Tool, ToolDefinition, ToolCapability
class MyTool:
def __init__(self, *, name: str = "my_tool", description: str = "...") -> None:
self._definition = ToolDefinition(
name=name,
description=description,
capabilities=[ToolCapability.READ],
parameters_schema={
"type": "object",
"properties": {
"query": {"type": "string", "description": "The query to execute"},
},
},
)
@property
def definition(self) -> ToolDefinition:
return self._definition
async def invoke(self, **kwargs: Any) -> Any:
query = kwargs.get("query", "")
# ... tool logic ...
return result
- Export from
runtime/tools/__init__.py - Add tests in
tests/unit/runtime/test_my_tool.py - Reference:
database.py,cache.py,http_client.py,queue.py
Adding a New LLM Backend¶
LLM backends implement the LLMBackend protocol for code generation and intent parsing.
- Create
src/agenticapi/runtime/llm/my_backend.py:
from agenticapi.runtime.llm.base import LLMBackend, LLMPrompt, LLMResponse, LLMChunk, LLMUsage
from agenticapi.exceptions import CodeGenerationError
class MyBackend:
def __init__(
self,
*,
model: str = "my-model-v1",
api_key: str | None = None,
max_tokens: int = 4096,
timeout: float = 120.0,
) -> None:
import os
resolved_key = api_key or os.environ.get("MY_API_KEY")
if not resolved_key:
raise ValueError("API key required via parameter or MY_API_KEY env var")
self._model = model
self._client = MySDK(api_key=resolved_key, timeout=timeout)
@property
def model_name(self) -> str:
return self._model
async def generate(self, prompt: LLMPrompt) -> LLMResponse:
try:
result = await self._client.generate(...)
return LLMResponse(
content=result.text,
usage=LLMUsage(input_tokens=result.input, output_tokens=result.output),
model=self._model,
)
except Exception as exc:
raise CodeGenerationError(f"MyBackend failed: {exc}") from exc
async def generate_stream(self, prompt: LLMPrompt) -> AsyncIterator[LLMChunk]:
try:
async for chunk in self._client.stream(...):
yield LLMChunk(content=chunk.text, is_final=False)
yield LLMChunk(content="", is_final=True)
except Exception as exc:
raise CodeGenerationError(f"MyBackend streaming failed: {exc}") from exc
- Export from
runtime/llm/__init__.py - Add tests in
tests/unit/runtime/test_my_backend.py(mock the SDK client) - Reference:
anthropic.py,openai.py,gemini.py
Adding a New Example¶
- Create
examples/NN_my_example/app.py(no__init__.pyneeded) - Include a comprehensive docstring with:
- What features are demonstrated
- Prerequisites (API keys, pip installs)
- Run command (
uvicornandagenticapi dev) - curl commands for every endpoint
- Key patterns:
- Use
TYPE_CHECKINGforAgentContextimport - Use broad
IntentScopewildcards (*.read,*.analyze) — LLMs classify domains unpredictably - Pass
tools=toolstoAgenticApp()when using tools with LLM - Guard LLM creation:
llm = Backend() if os.environ.get("KEY") else None - Add E2E tests in
tests/e2e/test_examples.py - Update
examples/README.md
Adding Authentication¶
- Choose a scheme:
APIKeyHeader,APIKeyQuery,HTTPBearer, orHTTPBasic - Write a verify function:
async (AuthCredentials) -> AuthUser | None - Create
Authenticator(scheme=..., verify=...) - Attach per-endpoint (
auth=) or app-wide (AgenticApp(auth=)) - Access user in handler via
context.auth_user - Reference:
examples/09_auth_agent/app.py
Adding File Upload/Download¶
File Upload (multipart)¶
- Add
UploadedFilestype annotation to handler parameter - Client sends
multipart/form-datawithintentfield and file fields - Files are injected as
dict[str, UploadFile]
from agenticapi import UploadedFiles
@app.agent_endpoint(name="documents")
async def handle(intent, context, files: UploadedFiles):
pdf = files["document"]
return {"filename": pdf.filename, "size": pdf.size, "type": pdf.content_type}
curl -X POST http://localhost:8000/agent/documents \
-F 'intent=Analyze this document' \
-F 'document=@report.pdf'
File Download¶
- Return
FileResultfrom handler instead of a dict - Framework converts to the appropriate HTTP response (bytes, file path, or streaming)
from agenticapi import FileResult
@app.agent_endpoint(name="export")
async def export_csv(intent, context):
return FileResult(
content=b"name,value\nalice,42",
media_type="text/csv",
filename="export.csv",
)
Reference: examples/10_file_handling/app.py
Adding Custom Response Types¶
Handlers can return non-JSON responses using result wrapper types:
from agenticapi import HTMLResult, PlainTextResult, FileResult
# HTML page
@app.agent_endpoint(name="dashboard")
async def dashboard(intent, context):
return HTMLResult(content="<h1>Dashboard</h1><p>Welcome!</p>")
# Plain text
@app.agent_endpoint(name="status")
async def status(intent, context):
return PlainTextResult(content="OK")
# File download
@app.agent_endpoint(name="export")
async def export(intent, context):
return FileResult(content=b"csv,data", media_type="text/csv", filename="export.csv")
You can also return any Starlette Response subclass directly (HTMLResponse, StreamingResponse, etc.) for full control over headers and status codes.
Reference: examples/11_html_responses/app.py
Building HTMX Apps¶
AgenticAPI provides built-in HTMX support for building interactive server-rendered UIs with agent endpoints.
HtmxHeaders (request detection)¶
Add HtmxHeaders as a handler parameter — it's auto-injected with parsed HTMX request headers:
from agenticapi import HtmxHeaders, HTMLResult
@app.agent_endpoint(name="items")
async def items(intent, context, htmx: HtmxHeaders):
if htmx.is_htmx:
# Return just the HTML fragment for HTMX partial update
return HTMLResult(content="<li>New item</li>")
# Return full page for initial load
return HTMLResult(content="<html>...</html>")
HtmxHeaders attributes: is_htmx, boosted, target, trigger, trigger_name, current_url, prompt
htmx_response_headers (response control)¶
Use htmx_response_headers() to build HTMX response headers for client-side control:
from agenticapi import htmx_response_headers, HTMLResult
from starlette.responses import HTMLResponse
@app.agent_endpoint(name="add")
async def add_item(intent, context, htmx: HtmxHeaders):
# ... create item ...
headers = htmx_response_headers(trigger="itemAdded", retarget="#item-list", reswap="beforeend")
return HTMLResponse(content="<li>Added!</li>", headers=headers)
Supported headers: trigger, trigger_after_settle, trigger_after_swap, redirect, refresh, retarget, reswap, push_url, replace_url
Reference: examples/12_htmx/app.py
Using Dependency Injection¶
FastAPI-style Depends() for handler parameters:
from agenticapi import Depends
async def get_db():
async with engine.connect() as conn:
yield conn # cleanup runs after the handler returns
@app.agent_endpoint(name="orders")
async def orders(intent, context, db = Depends(get_db)):
return {"rows": await db.fetch_all("SELECT * FROM orders")}
- Write a plain callable provider (sync or async, may
yieldfor cleanup) - Annotate the handler parameter with
= Depends(provider) - The framework scans the signature at registration time and resolves the dependency graph per request
- Nested dependencies are supported (providers can themselves declare
Depends()) - Each request gets its own resolution — same request re-uses cached resolutions
See dependencies.md for the full design.
Declaring Tools via @tool¶
Instead of writing a Tool protocol class, decorate a typed function:
from agenticapi import tool
@tool(description="Look up a user by ID")
async def get_user(user_id: int) -> dict:
"""Return the user record for the given ID."""
return {"id": user_id, "name": "Alice"}
registry.register(get_user)
- Type hints on params generate
parameters_schemaautomatically - The docstring first line is used as the description if not given explicitly
- Both sync and async functions work
- Reference:
src/agenticapi/runtime/tools/decorator.py
Adding Cost Budget Enforcement¶
Use BudgetPolicy alongside other policies in the harness:
from agenticapi import BudgetPolicy, PricingRegistry, HarnessEngine
budget = BudgetPolicy(
pricing=PricingRegistry.default(),
max_per_request_usd=0.50,
max_per_session_usd=5.00,
max_per_user_per_day_usd=50.00,
)
harness = HarnessEngine(policies=[code_policy, data_policy, budget])
BudgetPolicy is currently an explicit integration pattern around LLM calls, not a fully automatic stock-harness feature. Exceeded budgets raise BudgetExceeded -> HTTP 402. See budgets.md.
Adding Observability¶
from agenticapi.observability import configure_tracing, configure_metrics
configure_tracing(service_name="my-agent")
configure_metrics(service_name="my-agent")
Both are no-ops unless opentelemetry-api is installed. See observability.md for spans, metrics, and semantic conventions.
Exposing Endpoints via MCP¶
- Install:
pip install agentharnessapi[mcp] - Mark endpoints:
@app.agent_endpoint(name="x", enable_mcp=True) - Mount:
expose_as_mcp(app) - Only
enable_mcp=Trueendpoints become MCP tools - Test:
npx @modelcontextprotocol/inspector http://localhost:8000/mcp - Reference:
examples/08_mcp_agent/app.py
Creating a New Extension Package¶
Extensions are independently-installable packages under extensions/<name>/ that wrap third-party libraries without bloating the core dependency graph.
High-level steps:
- Create
extensions/<pkg-name>/with its ownpyproject.toml,src/<pkg>/__init__.py,tests/conftest.py, andREADME.md - Depend on
agenticapi>=0.1.0and pin the wrapped library (>=X.Y,<X.Y+1) - Use lazy imports for the wrapped library (see
_imports.pypattern inagenticapi-claude-agent-sdk) - Make all errors inherit from
agenticapi.AgenticAPIError - Stub the wrapped library in
tests/conftest.pyso tests run offline - Ship
py.typedin the package directory (PEP 561) - Document the public API in
README.mdand reference the extension fromextensions.md
Full specification: see extensions.md.
Reference implementation: extensions/agenticapi-claude-agent-sdk/