{"id":"5e3832a0-b8fc-4c2b-bffa-e2cf4ff96f60","slug":"agentic-base","kind":"mcp","name":"agentic-base","summary":"Run provenance, comparison validity, and the shared primitives for agentic work on research infrastructure. A base layer, not an agent framework.","owner":"saradamian","repoUrl":"https://github.com/saradamian/agentic-base","license":"EUPL-1.2","licenseTier":"weak-copyleft","trust":"community","stars":0,"match":null,"fewStars":true,"format":"mcp","createdAt":"2026-09-12T04:37:41.000Z","addedAt":"2026-10-21T06:46:58.027Z","logoUrl":"https://avatars.githubusercontent.com/u/26249073?v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["memory-knowledge"],"tags":["agentic-ai","agents","fastapi","hpc","llm-evaluation","opentelemetry","provenance","pydantic"],"runtime":"python","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-23T00:33:03.792Z","sample":false,"url":"https://conduits.dev/tools/agentic-base","language":"Python","transports":["stdio"],"install":{"pypi":"surf-agentic-base"},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"# surf-agentic-base\n\n[![ci](https://github.com/saradamian/agentic-base/actions/workflows/ci.yml/badge.svg)](https://github.com/saradamian/agentic-base/actions/workflows/ci.yml)\n[![licence: EUPL-1.2](https://img.shields.io/badge/licence-EUPL--1.2-blue.svg)](https://github.com/saradamian/agentic-base/blob/main/LICENSE)\n[![python](https://img.shields.io/badge/python-3.10%20to%203.14-blue.svg)](https://github.com/saradamian/agentic-base/blob/main/pyproject.toml)\n[![cite](https://img.shields.io/badge/cite-CITATION.cff-green.svg)](https://github.com/saradamian/agentic-base/blob/main/CITATION.cff)\n\nThe record of what an AI agent did, kept so that the people who have to answer for it can read\nit later. One run is one record. It says what the model received, who the run acted for, what\nclass of data it touched and on which tier, who approved which action, whether the person was told\nthey were dealing with an AI, and who scored the outcome. Agents write it. The person it worked\nfor, an auditor, an incident responder and the person comparing two versions read it.\n\nThis repository is that contract and the pieces around it: the record type with the rules a\nwriter must meet, a service that keeps records per tenant under audit, exports in the provenance\nstandards, and the small library a tool server needs. Two checks on claims about agents come out\nof the record, and they also run on logs you already have.\n\n## The contract\n\nA run record carries these fields, in groups:\n\n| group | fields |\n|---|---|\n| where it came from | `tenant`, `code_revision`, `component_versions`, `model`, `endpoint`, `precision`, `sampling`, `system_prompt`, `messages` |\n| what joins it to the rest | `trace_id` (its spans), `conversation_id` (the other runs of one exchange) |\n| for whom, on what | `principal`, `classification`, `isolation_tier`, `disclosure`, `content_marking`, `redaction` |\n| oversight | `approvals`, each saying who said yes to which action and when |\n| the outcome | `status`, `finish_reasons`, `resolved`, `label_source` (who scored it), `instrument`, `degraded` |\n| what it cost | `prompt_tokens` (of which `cache_read_tokens`, `cache_creation_tokens`), `completion_tokens`, `joules`, `num_steps`, `total_tool_calls`, `elapsed_ms` |\n| for a comparison | `item`, `arm`, `arm_fingerprint`, `failure_kind` |\n\nOnly `tenant` and `code_revision` are required, so a writer that does not know the rest yet still\nwrites. The service refuses three things at the write path: an outcome with no scorer, personal or\nhealth data on the shared tier, and a transcript that redaction could not process. Every create,\nlabel and approval joins a per-tenant hash chain, verified on request. A person can be erased with\nthe chain still verifying.\n\nThe type is `agentic_base.domain.outcomes.RunRecordCreate`. The rules over it are functions on a\nstructural protocol, so a consumer with its own record type gets them without adopting the\nstorage. The library half runs on Python 3.10 with four dependencies and no database. The words\n(principal, scorer, citable, exclusion channel) are defined in\n[Concepts](https://github.com/saradamian/agentic-base/blob/main/docs/concepts.md).\n\n## Record a run\n\n```python\nfrom agentic_base.client import RunRecorder\nfrom agentic_base.domain.outcomes import DataClass, IsolationTier\n\nrecorder = RunRecorder(\"http://localhost:8080\", tenant=\"example-team\", code_revision=\"7c1e0d2\", token=TOKEN)\nwith recorder.run(\n    item=\"mr-103\",\n    arm=\"reviewer-2026.09\",\n    model=\"some-model\",\n    principal=\"urn:example:alice\",\n    classification=DataClass.INTERNAL,\n    isolation_tier=IsolationTier.VIRTUALISED,\n) as run:\n    run.messages = [{\"role\": \"user\", \"content\": \"Review mr-103.\"}, {\"role\": \"assistant\", \"content\": \"...\"}]\n```\n\nThe record is written when the block exits, also on an exception. `examples/service_agent.py` is\nthe whole story for a review agent: three runs, how each person was told it was an AI, a\nmaintainer's approval, and three verdicts of different standing.\n[Running the service](https://github.com/saradamian/agentic-base/blob/main/docs/service.md) has\nthe install, the tokens, and what each example prints.\n\n## Read it back\n\n- `GET /runs/integrity` verifies the tenant's chain and names a changed, cut or missing entry.\n- `GET /runs/{run_id}/provenance?format=...` returns the run as W3C PROV, an OpenLineage event\n  or a Process Run Crate, so someone else's tooling reads it without learning ours.\n- `agentic-base-mlflow` exports a tenant into MLflow. `agentic-base-mcp` serves the database\n  read-only to a chat client.\n- The spans the service emits carry the OpenTelemetry GenAI and OpenInference vocabulary, so\n  Phoenix, Langfuse and Jaeger label them without translation\n  ([Observability](https://github.com/saradamian/agentic-base/blob/main/docs/OBSERVABILITY.md)).\n\n## Build a tool server on it\n\nA server that gives an agent tools needs four things this library has: a way to declare a tool,\na result type, a fetch that refuses internal addresses on every hop, and a record of each call.\nThey are in the library half, which installs on Python 3.10 with four dependencies and no\ndatabase.\n\n```python\nimport hashlib\n\nimport httpx\n\nfrom agentic_base.recording import observer_from_environment\nfrom agentic_base.security.netsec import URLSafetyError, open_checked\nfrom agentic_base.tools.decorator import tool\nfrom agentic_base.tools.types import ToolResult\n\n\n@tool(description=\"The sha256 of the file at a URL.\")\ndef checksum(url: str) -> ToolResult:\n    try:\n        with httpx.Client(follow_redirects=False) as client, open_checked(url, client=client) as response:\n            digest = hashlib.sha256(response.read()).hexdigest()\n    except URLSafetyError as exc:\n        return ToolResult.fail(f\"URL rejected: {exc}\")\n    return ToolResult.ok(digest)\n\n\nobserver = observer_from_environment(server=\"example\")\nobserver.record(\"checksum\", {\"url\": \"https://example.org/x\"}, \"...\", True, 12.0)\n```\n\n- `tool` turns the function into a `Tool` with a name, a description and typed parameters; the\n  MCP SDK and an in-process registry both read that one object.\n- `open_checked` validates the URL and every redirect target, connects to the address it\n  validated, and yields the response before the body is read. `safe_fetch_text` does the same for\n  a text.\n- `observer_from_environment` returns a recorder that appends one JSON line per call, failed\n  calls included. By default the lines go to one file a month in\n  `~/.local/state/agentic-base/calls` (under `$XDG_STATE_HOME` when that is set), readable by\n  you only, for six months. `AP_CALL_LOG` names another file, and `AP_CALL_LOG=off` records nothing. A host with\n  its own journal implements `CallObserver` instead.\n- `ObservingMiddleware` puts that observer in front of every tool call of a server built on the\n  MCP SDK: `MCPServer(name, middleware=[ObservingMiddleware(observer_from_environment(server=name))])`.\n  It imports nothing from the SDK, so it adds no dependency to the library half. Each line then\n  also says which client and request the call came from, its trace and span ids, the caller's\n  `_meta` keys and, for a failed call, the error the client got. `call_context` and `call_outcome`\n  build those fields for a relay in front of a server that is not built on this library, and\n  `call_log_schema()` is the JSON Schema a line satisfies. An agent that sends its run id\n  as `_meta[\"agentic_base.run_id\"]` gets its calls joined to its run record.\n\nWhat is deliberately not here: the server loop itself. The MCP SDK has it, and a server of a few\ntools is about a hundred lines on the SDK's low-level `Server`.\n\n### See what agents did with it\n\n`agentic-base calls` reads the call log back. This is a short session of an MCP client with the\nEasyBuild server, which is built on this library:\n\n```console\n$ agentic-base calls\n7 calls to 1 server, at 2026-10-09 03:13 UTC, in ~/.local/state/agentic-base/calls\n\nserver     tool                 calls  failed  median ms  p95 ms\neasybuild  compute_checksum         2       1         56     106\neasybuild  pypi_info                2       1        109     174\neasybuild  github_release_info      1       0        226     226\neasybuild  list_toolchains          1       0          0       0\neasybuild  search_easyconfigs       1       0         58      58\n\nclients: mcp 0.1.0 (7)\n\nagentic-base calls --failures shows each failed call with its arguments.\n\n$ agentic-base calls --failures\n2026-10-09 03:13  easybuild  pypi_info  {\"package\": \"tqdm-but-misspelt-xyz\"}\n  PyPI has no package 'tqdm-but-misspelt-xyz'\n2026-10-09 03:13  easybuild  compute_checksum  {\"url\": \"http://169.254.169.254/latest/meta-data\"}\n  URL rejected: disallowed address: 169.254.169.254\n```\n\nThe second failure is the checked fetch refusing a cloud metadata address. Each failed call\nshows what the agent asked for and the error it got back. `--since 7d` and `--server` narrow the\nlog down, and `--json` gives the same summary to a script.\n\n## Check a comparison\n\nBecause the record names the scorer and the reason a run has no verdict, two checks follow from\nit: did the comparison lose runs unevenly, and who scored each outcome. They also read logs you\nalready have, with no server, database or token:\n\n```bash\npip install surf-agentic-base\npython -m agentic_base.demo --jsonl > results.jsonl   # forty example runs; or use a log of your own\nagentic-base check results.jsonl --arm config\n```\n\n```text\nbaseline: assessed 20; excluded 1 (1 timeout); analysed 19\nwith-planner: assessed 20; excluded 6 (6 timeout); analysed 14\nnot sound: timeout: 30.0% (with-planner) vs 5.0% (baseline), 95% interval +0.8 to +47.3 pp\noutcomes: 27 name a citable scorer, 4 diagnostic, 2 name none\n```\n\nThe exit code gates a CI job: **0** sound, **1** not sound, **2** when the input cannot answer\neither way. [Check a comparison](https://github.com/saradamian/agentic-base/blob/main/docs/checks.md)\nhas the input formats, the CI snippet, and the same check from Python over a record type of your\nown.\n\n## What you use it for\n\n| you want to | use |\n|---|---|\n| keep an account of what your agent did, for whom, with whose approval | the service, through `agentic_base.client` |\n| hand a run to someone else's tooling | `agentic_base.provenance`: W3C PROV, a Process Run Crate, an OpenLineage event |\n| see runs in a tracker you already have | `agentic-base-mlflow` |\n| ask about runs from a chat client | `agentic-base-mcp`, read-only |\n| remove personal data before it is stored | `agentic_base.redaction` |\n| declare a tool a model can call | `agentic_base.tools` |\n| pre-check a URL an agent wants to fetch, or stream a file from it | `agentic_base.security.netsec` |\n| record the calls a served tool receives | `agentic_base.recording` |\n| see what agents did with your tool server | `agentic-base calls` |\n| know which outcomes may be cited | `agentic_base.domain.outcomes` |\n| check whether version B really beats version A | `agentic-base check`, or `agentic_base.domain.validity` |\n\n`netsec` validates a URL and pins the address it resolved to. It bounds accidental damage; it is\nnot a network boundary, and address forms or paths it does not know about get through. Where an\nagent is untrusted, enforce egress in the network as well, with an egress proxy or a network\npolicy that allows only the destinations you intend.\n\n## What it is not\n\nNot an agent framework, a tracing backend, a metrics store, an experiment tracker, a workflow\nengine or an inference server. Each exists and is better than one we would write.\n\nNot an enforcement layer either. The writer states the principal, the data class, the isolation\ntier and the approvals, and the record keeps what it was told. Whether the cluster, the network\nand the approval were real is for the caller to check at its own boundary. The service refuses\nonly the three things listed under the contract.\n[The reuse ledger](https://github.com/saradamian/agentic-base/blob/main/docs/architecture/reuse-ledger.md) gives a verdict per concern (adopt,\nbridge or build), and a test fails when the code does by hand what the ledger says it adopted.\n\n## Installing\n\n```bash\npip install surf-agentic-base                # the library half: four dependencies, Python 3.10+\npip install 'surf-agentic-base[provenance]'  # plus the three provenance-standard libraries\npip install 'surf-agentic-base[service]'     # the service: FastAPI, storage, tracing, MCP\npip install 'surf-agentic-base[mlflow]'      # plus the MLflow export\n```\n\nThe import name is `agentic_base`. The distribution is named `surf-agentic-base` because\n`agentic-base` on PyPI belongs to an unrelated project.\n\n## Documentation\n\n- [Concepts](https://github.com/saradamian/agentic-base/blob/main/docs/concepts.md): the fifteen terms, each pointing at its code\n- [Running the service](https://github.com/saradamian/agentic-base/blob/main/docs/service.md): worked examples, tokens, MCP, export\n- [Check a comparison](https://github.com/saradamian/agentic-base/blob/main/docs/checks.md): the two checks, over the record or over logs you already have\n- [Decisions](https://github.com/saradamian/agentic-base/blob/main/docs/decisions.md): the choices that are cheap now and expensive to reverse\n- [Compliance evidence](https://github.com/saradamian/agentic-base/blob/main/docs/architecture/compliance.md): what the record produces for the AI Act, NIS2, the Cyber Resilience Act and the Data Act, and what is missing\n- [Redaction](https://github.com/saradamian/agentic-base/blob/main/docs/architecture/redaction.md) and [incident response](https://github.com/saradamian/agentic-base/blob/main/docs/architecture/incident-response.md): for whoever operates the service\n- [Reuse ledger](https://github.com/saradamian/agentic-base/blob/main/docs/architecture/reuse-ledger.md): adopt, bridge or build, with the reason\n- [Archive](https://github.com/saradamian/agentic-base/blob/main/archive/README.md): the design vision and other superseded pages, kept for the record\n\n## Contributing and releases\n\n`CONTRIBUTING.md` is the short form for someone about to open a pull request, and\n`docs/ENGINEERING.md` is why the rules are the rules. Changes land on `main` through pull requests\nthat the gate has passed on both ends of the supported Python range. SURF's own deployment is\nkept in a separate repository built from this one plus an overlay\n(`docs/architecture/deployment-overlay.md`).\n\nSemantic versioning, with the version taken from the git tag; while the major version is `0`, a\nminor bump may change the public interface. Releases are signed `vX.Y.Z` tags, published to PyPI\nby trusted publishing with build provenance and an SBOM attested on the distribution files.\n\nCite the repository using `CITATION.cff`. Licensed under the European Union Public Licence v. 1.2\n(`LICENSE`).\n"}