pyacri 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. pyacri-0.4.0/.github/ISSUE_TEMPLATE/bug_report.md +22 -0
  2. pyacri-0.4.0/.github/ISSUE_TEMPLATE/design_challenge.md +17 -0
  3. pyacri-0.4.0/.github/ISSUE_TEMPLATE/proposal.md +13 -0
  4. pyacri-0.4.0/.github/PULL_REQUEST_TEMPLATE.md +14 -0
  5. pyacri-0.4.0/.github/workflows/ci.yml +72 -0
  6. pyacri-0.4.0/.github/workflows/publish.yml +38 -0
  7. pyacri-0.4.0/.gitignore +41 -0
  8. pyacri-0.4.0/CODE_OF_CONDUCT.md +39 -0
  9. pyacri-0.4.0/CONTRIBUTING.md +59 -0
  10. pyacri-0.4.0/LICENSE +21 -0
  11. pyacri-0.4.0/PKG-INFO +296 -0
  12. pyacri-0.4.0/README.md +259 -0
  13. pyacri-0.4.0/SECURITY.md +34 -0
  14. pyacri-0.4.0/acri/__init__.py +73 -0
  15. pyacri-0.4.0/acri/_synonyms.py +31 -0
  16. pyacri-0.4.0/acri/_template.py +27 -0
  17. pyacri-0.4.0/acri/_text.py +34 -0
  18. pyacri-0.4.0/acri/adapters.py +50 -0
  19. pyacri-0.4.0/acri/cli.py +97 -0
  20. pyacri-0.4.0/acri/compass.py +70 -0
  21. pyacri-0.4.0/acri/config.py +87 -0
  22. pyacri-0.4.0/acri/corpus.py +57 -0
  23. pyacri-0.4.0/acri/credentials.py +25 -0
  24. pyacri-0.4.0/acri/daemon.py +75 -0
  25. pyacri-0.4.0/acri/escape_hatch.py +38 -0
  26. pyacri-0.4.0/acri/gate.py +42 -0
  27. pyacri-0.4.0/acri/ledger.py +59 -0
  28. pyacri-0.4.0/acri/mcp_connect.py +45 -0
  29. pyacri-0.4.0/acri/port.py +69 -0
  30. pyacri-0.4.0/acri/press.py +64 -0
  31. pyacri-0.4.0/acri/py.typed +0 -0
  32. pyacri-0.4.0/acri/router.py +21 -0
  33. pyacri-0.4.0/acri/sandbox.py +43 -0
  34. pyacri-0.4.0/acri/schemas.py +34 -0
  35. pyacri-0.4.0/acri/server.py +87 -0
  36. pyacri-0.4.0/acri/studio.py +70 -0
  37. pyacri-0.4.0/acri/studio_data.py +38 -0
  38. pyacri-0.4.0/acri/studio_page.html +112 -0
  39. pyacri-0.4.0/assay/accuracy.py +75 -0
  40. pyacri-0.4.0/assay/clients.py +32 -0
  41. pyacri-0.4.0/assay/diagnose.py +63 -0
  42. pyacri-0.4.0/assay/fixtures.json +177 -0
  43. pyacri-0.4.0/assay/fixtures.py +41 -0
  44. pyacri-0.4.0/assay/fixtures_500.json +1 -0
  45. pyacri-0.4.0/assay/latency.py +43 -0
  46. pyacri-0.4.0/assay/mcp_live.py +63 -0
  47. pyacri-0.4.0/assay/recall.py +49 -0
  48. pyacri-0.4.0/assay/report.py +18 -0
  49. pyacri-0.4.0/assay/scale.py +64 -0
  50. pyacri-0.4.0/docs/architecture.md +289 -0
  51. pyacri-0.4.0/docs/assets/benchmark-results.svg +59 -0
  52. pyacri-0.4.0/docs/assets/resolution-flow.svg +59 -0
  53. pyacri-0.4.0/docs/decisions.md +535 -0
  54. pyacri-0.4.0/examples/live_demo.py +60 -0
  55. pyacri-0.4.0/pyproject.toml +53 -0
  56. pyacri-0.4.0/rust/.gitignore +2 -0
  57. pyacri-0.4.0/rust/Cargo.toml +8 -0
  58. pyacri-0.4.0/rust/README.md +33 -0
  59. pyacri-0.4.0/rust/src/bm25.rs +28 -0
  60. pyacri-0.4.0/rust/src/compass.rs +45 -0
  61. pyacri-0.4.0/rust/src/corpus.rs +72 -0
  62. pyacri-0.4.0/rust/src/lib.rs +19 -0
  63. pyacri-0.4.0/rust/src/synonyms.rs +43 -0
  64. pyacri-0.4.0/rust/src/text.rs +47 -0
  65. pyacri-0.4.0/rust/tests/resolve.rs +61 -0
  66. pyacri-0.4.0/tests/test_assay_smoke.py +51 -0
  67. pyacri-0.4.0/tests/test_cli.py +32 -0
  68. pyacri-0.4.0/tests/test_compass.py +37 -0
  69. pyacri-0.4.0/tests/test_config.py +95 -0
  70. pyacri-0.4.0/tests/test_corpus.py +40 -0
  71. pyacri-0.4.0/tests/test_daemon.py +59 -0
  72. pyacri-0.4.0/tests/test_escape_hatch.py +25 -0
  73. pyacri-0.4.0/tests/test_gate.py +34 -0
  74. pyacri-0.4.0/tests/test_integration.py +89 -0
  75. pyacri-0.4.0/tests/test_ports.py +96 -0
  76. pyacri-0.4.0/tests/test_press.py +35 -0
  77. pyacri-0.4.0/tests/test_router.py +58 -0
  78. pyacri-0.4.0/tests/test_sandbox.py +34 -0
  79. pyacri-0.4.0/tests/test_schemas.py +46 -0
  80. pyacri-0.4.0/tests/test_server.py +55 -0
  81. pyacri-0.4.0/tests/test_studio.py +83 -0
  82. pyacri-0.4.0/tests/test_studio_data.py +55 -0
  83. pyacri-0.4.0/typescript/.gitignore +2 -0
  84. pyacri-0.4.0/typescript/README.md +42 -0
  85. pyacri-0.4.0/typescript/package-lock.json +47 -0
  86. pyacri-0.4.0/typescript/package.json +15 -0
  87. pyacri-0.4.0/typescript/src/compass.ts +52 -0
  88. pyacri-0.4.0/typescript/src/corpus.ts +37 -0
  89. pyacri-0.4.0/typescript/src/synonyms.ts +27 -0
  90. pyacri-0.4.0/typescript/src/text.ts +28 -0
  91. pyacri-0.4.0/typescript/test/compass.test.ts +45 -0
  92. pyacri-0.4.0/typescript/tsconfig.json +11 -0
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: Bug report
3
+ about: acri resolved the wrong tools, or misbehaved
4
+ labels: bug
5
+ ---
6
+
7
+ **What happened**
8
+
9
+ **What you expected**
10
+
11
+ **Steps to reproduce**
12
+ 1.
13
+
14
+ **Environment**
15
+ - acri version:
16
+ - Provider + model (gemini / openai-compatible / anthropic / local):
17
+ - Number of registered tools:
18
+ - OS + Python/Node version:
19
+
20
+ **`ledger` output for the failing turn**
21
+
22
+ <!-- The trace is usually the whole answer. Redact arguments if they are sensitive. -->
@@ -0,0 +1,17 @@
1
+ ---
2
+ name: Design challenge
3
+ about: Argue that part of the architecture is wrong
4
+ labels: design
5
+ ---
6
+
7
+ **Which part** (corpus / compass / port / gate / press / ledger / assay, or a claim in docs/architecture.md)
8
+
9
+ **What the design currently says**
10
+
11
+ **Why it is wrong**
12
+
13
+ <!-- Evidence beats intuition: a paper, a provider doc, an assay run, or arithmetic. -->
14
+
15
+ **What it should be instead**
16
+
17
+ **What this would cost** (what breaks, what has to be rewritten, who is affected)
@@ -0,0 +1,13 @@
1
+ ---
2
+ name: Proposal
3
+ about: Propose a new port adapter, corpus ingester, or assay benchmark
4
+ labels: proposal
5
+ ---
6
+
7
+ **Surface** (port adapter / corpus ingester / assay benchmark / compass ranking)
8
+
9
+ **What it adds** — one paragraph. What can a user do afterwards that they cannot do now?
10
+
11
+ **Why it is the minimal version** — what did you deliberately leave out?
12
+
13
+ **How it is proven** — which `assay` run shows it works, or what run would need to exist?
@@ -0,0 +1,14 @@
1
+ ## What
2
+
3
+ ## Why this is the minimal version
4
+
5
+ What did you deliberately *not* build?
6
+
7
+ ## Checklist
8
+ - [ ] No performance number without a reproducible `assay/` script and a committed run
9
+ - [ ] Nothing rewrites a prompt prefix (or the arithmetic showing it wins is included)
10
+ - [ ] `gate` stays advisory — no path suppresses a tool call the model wanted
11
+ - [ ] Every resolve decision still reaches `ledger`
12
+ - [ ] No new dependency that a few lines could replace
13
+ - [ ] No daemon, gateway, or port introduced
14
+ - [ ] Code files ≤ 250 words (350 hard cap); docs exempt
@@ -0,0 +1,72 @@
1
+ name: ci
2
+ on:
3
+ push: { branches: [main] }
4
+ pull_request:
5
+
6
+ jobs:
7
+ test:
8
+ name: pytest
9
+ runs-on: ubuntu-latest
10
+ strategy:
11
+ matrix:
12
+ python-version: ["3.10", "3.12"]
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: actions/setup-python@v5
16
+ with: { python-version: "${{ matrix.python-version }}" }
17
+ - run: pip install -e ".[dev]"
18
+ - run: pytest -q
19
+
20
+ claims:
21
+ name: no unreceipted claims
22
+ runs-on: ubuntu-latest
23
+ steps:
24
+ - uses: actions/checkout@v4
25
+ - uses: actions/setup-python@v5
26
+ with: { python-version: "3.12" }
27
+
28
+ - name: Required community files exist
29
+ run: |
30
+ for f in README.md LICENSE CONTRIBUTING.md CODE_OF_CONDUCT.md SECURITY.md docs/architecture.md; do
31
+ test -f "$f" || { echo "missing: $f"; exit 1; }
32
+ done
33
+
34
+ - name: Every performance claim carries its receipt
35
+ run: |
36
+ python - <<'PY'
37
+ import pathlib, re, sys
38
+ # A performance claim is a percentage or an "Nx better" multiplier.
39
+ claim = re.compile(r'\d+(?:\.\d+)?\s*%'
40
+ r'|\d+(?:\.\d+)?\s*[x×]\s*(?:faster|cheaper|smaller|less|lower|'
41
+ r'reduction|improvement|better|speedup)', re.I)
42
+ # A receipt is a citation, an assay run, or an explicit hedge.
43
+ receipt = re.compile(r'https?://|assay/|estimate|projected|hypothetical|illustrative|'
44
+ r'worked example|for example|e\.g\.', re.I)
45
+ bad = []
46
+ for pattern in ('*.md', '*.svg', '*.html'):
47
+ for p in pathlib.Path('.').rglob(pattern):
48
+ if '.github' in p.parts: continue
49
+ for n, line in enumerate(p.read_text(encoding='utf-8').splitlines(), 1):
50
+ if claim.search(line) and not receipt.search(line):
51
+ bad.append(f"{p}:{n}: {line.strip()[:100]}")
52
+ if bad:
53
+ print("Performance claims without a citation, an assay run, or an explicit hedge.")
54
+ print("See the claims policy in README.md.\n")
55
+ print("\n".join(bad))
56
+ sys.exit(1)
57
+ print("ok - no unreceipted claims")
58
+ PY
59
+
60
+ - name: Relative links resolve
61
+ run: |
62
+ python - <<'PY'
63
+ import pathlib, re, sys
64
+ bad = []
65
+ for p in pathlib.Path('.').rglob('*.md'):
66
+ for target in re.findall(r'\]\(([^)#?:]+)\)', p.read_text(encoding='utf-8')):
67
+ t = target.strip()
68
+ if t.startswith(('http', '#', 'mailto:', '../../')): continue
69
+ if not (p.parent / t).exists(): bad.append(f"{p} -> {t}")
70
+ if bad: sys.exit("broken relative links:\n" + "\n".join(bad))
71
+ print("ok - links resolve")
72
+ PY
@@ -0,0 +1,38 @@
1
+ name: publish
2
+
3
+ # Triggered only by publishing a GitHub Release -- pushing a tag alone does
4
+ # NOT run this. A release is a separate, deliberate action so there's always
5
+ # a human decision point between "tag exists" and "PyPI gets a new version",
6
+ # which cannot be undone once uploaded (only yanked, never overwritten).
7
+ #
8
+ # Needs, on the PyPI side, a trusted publisher registered for this repo
9
+ # (Publishing -> Trusted Publishers on the PyPI project page): owner
10
+ # INERATE, repo acri, workflow publish.yml, environment pypi. That's a
11
+ # one-time setup step on pypi.org itself -- nothing here can do it, and
12
+ # nothing here runs until it's done and a Release is published.
13
+ on:
14
+ release:
15
+ types: [published]
16
+
17
+ jobs:
18
+ build:
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+ - uses: actions/setup-python@v5
23
+ with: { python-version: "3.12" }
24
+ - run: pip install build
25
+ - run: python -m build
26
+ - uses: actions/upload-artifact@v4
27
+ with: { name: dist, path: dist/ }
28
+
29
+ publish:
30
+ needs: build
31
+ runs-on: ubuntu-latest
32
+ environment: pypi
33
+ permissions:
34
+ id-token: write # OIDC trusted publishing -- no stored token/secret
35
+ steps:
36
+ - uses: actions/download-artifact@v4
37
+ with: { name: dist, path: dist/ }
38
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,41 @@
1
+ # environments & secrets
2
+ .env
3
+ .env.*
4
+ !.env.example
5
+
6
+ # python
7
+ __pycache__/
8
+ *.pyc
9
+ .venv/
10
+ dist/
11
+ *.egg-info/
12
+
13
+ # rust
14
+ target/
15
+ rust/target/
16
+ **/*.rs.bk
17
+ **/.rustc_info.json
18
+ **/.cargo-artifact-lock
19
+ **/.cargo-build-lock
20
+ **/.cargo-lock
21
+ **/CACHEDIR.TAG
22
+
23
+ # node & typescript
24
+ node_modules/
25
+ typescript/node_modules/
26
+ typescript/dist/
27
+ *.tsbuildinfo
28
+
29
+ # local indexes & traces (never committed)
30
+ *.acri-index
31
+ ledger/*.jsonl
32
+ .acri/
33
+ assay/results/
34
+
35
+ # junk
36
+ .DS_Store
37
+ Thumbs.db
38
+
39
+ # private working notes — never published
40
+ CLAUDE.md
41
+ .private/
@@ -0,0 +1,39 @@
1
+ # Code of Conduct
2
+
3
+ ## Our pledge
4
+
5
+ We as members, contributors, and maintainers pledge to make participation in acri a
6
+ harassment-free experience for everyone, regardless of age, body size, disability,
7
+ ethnicity, gender identity and expression, level of experience, education, socio-economic
8
+ status, nationality, personal appearance, race, religion, or sexual identity and
9
+ orientation.
10
+
11
+ ## Our standards
12
+
13
+ **Do:** be kind and patient (many contributors are not native English speakers, and many
14
+ are learning systems engineering as they go — that is welcome here), give constructive
15
+ review comments, accept constructive criticism gracefully, focus on what is best for the
16
+ community.
17
+
18
+ **Don't:** use sexualized language or imagery, troll, insult, make personal or political
19
+ attacks, harass publicly or privately, publish others' private information, or behave in
20
+ ways that would be considered inappropriate in a professional setting.
21
+
22
+ ## Technical disagreement is not misconduct
23
+
24
+ Arguing that a design is wrong — bluntly, with evidence — is encouraged and is how this
25
+ project improves. Attacking the person who proposed it is not. The line is whether you
26
+ are addressing the argument or the author.
27
+
28
+ ## Enforcement
29
+
30
+ Maintainers are responsible for clarifying standards and will take appropriate, fair
31
+ corrective action in response to unacceptable behavior — from a warning to a permanent
32
+ ban depending on severity and repetition.
33
+
34
+ Report violations by opening a confidential issue or contacting the maintainers. All
35
+ reports will be reviewed promptly and kept confidential.
36
+
37
+ ## Attribution
38
+
39
+ Adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.1.
@@ -0,0 +1,59 @@
1
+ # Contributing to acri
2
+
3
+ acri is a small, sharp library that refuses to grow into a framework. Contributions are
4
+ held to that standard — the constraint is the product.
5
+
6
+ ## Ground rules
7
+
8
+ 1. **Laziest thing that works.** No abstraction with one implementation. No dependency
9
+ for what twenty lines do. Standard library first, then an already-installed dep, then
10
+ new code. The shortest working diff wins.
11
+ 2. **No unreproducible numbers.** No performance claim enters this repository without a
12
+ script in `assay/` that regenerates it and a committed run. This rule has no
13
+ exceptions, and it applies to READMEs, docs, issues, and commit messages.
14
+ 3. **The resolver may be wrong; it may never be silently wrong.** Every decision acri
15
+ makes is recorded in `ledger`. A tool that was considered and skipped is as important
16
+ to log as one that was chosen.
17
+ 4. **`gate` is advisory, never authoritative.** A false "no tool needed" makes a model
18
+ answer from memory — confident, fluent, wrong, with no error and no retry. Nothing in
19
+ acri may suppress a tool call the model actually wanted to make.
20
+ 5. **Append-only by default.** Anything that rewrites a prompt prefix invalidates the
21
+ provider's cache and costs the user money. If a change rewrites history, it must show
22
+ the arithmetic proving it wins.
23
+ 6. **No daemon, no gateway, no port.** acri is an import. A change that requires the user
24
+ to run a process is out of scope by definition.
25
+ 7. **Code files stay small** — 250 words, 350 hard cap. Docs are exempt.
26
+
27
+ ## What to contribute
28
+
29
+ | Surface | What it looks like |
30
+ |---------|--------------------|
31
+ | **`port` adapters** | A new provider in ~50 lines: take tools + query, return the request payload that provider expects. Highest-value contribution. |
32
+ | **`corpus` ingesters** | Teach acri to read a new tool source (OpenAPI, a registry format, a framework's tool objects). |
33
+ | **`assay` benchmarks** | A reproducible scenario with a committed run. Benchmarks that *disprove* something are more valuable than ones that confirm it. |
34
+ | **`compass` retrieval** | Better ranking. Must come with an `assay` run showing it beats the current default. |
35
+ | **Docs** | Especially corrections. The architecture doc names its own weak points; sharpening them is welcome. |
36
+
37
+ ## Workflow
38
+
39
+ 1. Open an issue first for anything non-trivial — a template is provided. Small fixes can
40
+ go straight to a PR.
41
+ 2. Fork, branch (`feat/<slug>` or `fix/<slug>`).
42
+ 3. Run the self-checks (see `.github/workflows/ci.yml` for what CI runs).
43
+ 4. PR describing what it does, **why it is the minimal version**, and what you
44
+ deliberately did not build.
45
+ 5. One maintainer review plus green CI merges it.
46
+
47
+ ## Commit style
48
+
49
+ Conventional commits (`feat:`, `fix:`, `docs:`, `chore:`). Subjects under 72 characters.
50
+
51
+ ## Disagreeing with the design
52
+
53
+ The architecture doc is an argument, not a decree. If you think a layer is wrong, open an
54
+ issue titled `design:` and make the case. Several parts of acri exist specifically because
55
+ an earlier version of the design was wrong and someone said so.
56
+
57
+ ## Questions
58
+
59
+ Open a discussion or an issue. Be kind — see [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md).
pyacri-0.4.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Piyush Sharma (ScienHAC) and acri contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
pyacri-0.4.0/PKG-INFO ADDED
@@ -0,0 +1,296 @@
1
+ Metadata-Version: 2.5
2
+ Name: pyacri
3
+ Version: 0.4.0
4
+ Summary: A client-side capability resolver: pick the right few tools before the request is sent.
5
+ Project-URL: Homepage, https://github.com/INERATE/acri
6
+ Project-URL: Repository, https://github.com/INERATE/acri
7
+ Project-URL: Issues, https://github.com/INERATE/acri/issues
8
+ Author: Piyush Sharma
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Classifier: Development Status :: 2 - Pre-Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Typing :: Typed
17
+ Requires-Python: >=3.10
18
+ Provides-Extra: assay
19
+ Requires-Dist: google-genai>=1; extra == 'assay'
20
+ Requires-Dist: mcp>=1; extra == 'assay'
21
+ Requires-Dist: openai>=1; extra == 'assay'
22
+ Provides-Extra: dev
23
+ Requires-Dist: mcp>=1; extra == 'dev'
24
+ Requires-Dist: pytest>=8; extra == 'dev'
25
+ Requires-Dist: pyyaml>=6; extra == 'dev'
26
+ Provides-Extra: release
27
+ Requires-Dist: build>=1; extra == 'release'
28
+ Requires-Dist: twine>=5; extra == 'release'
29
+ Provides-Extra: server
30
+ Requires-Dist: mcp>=1; extra == 'server'
31
+ Requires-Dist: pyyaml>=6; extra == 'server'
32
+ Provides-Extra: studio
33
+ Requires-Dist: pyyaml>=6; extra == 'studio'
34
+ Provides-Extra: yaml
35
+ Requires-Dist: pyyaml>=6; extra == 'yaml'
36
+ Description-Content-Type: text/markdown
37
+
38
+ # acri
39
+
40
+ **A**gent **C**apability **R**esolution **I**nterface
41
+
42
+ > DNS resolves a name to an address.
43
+ > **acri resolves an intent to the right tools.**
44
+
45
+ You don't hand a browser all 300 million domains. You shouldn't hand a model all 300 tools.
46
+
47
+ ---
48
+
49
+ ## The problem
50
+
51
+ Tool selection degrades as your toolset grows. Anthropic's own documentation puts the
52
+ cliff at **30–50 tools**: past that, the model starts picking the wrong one.
53
+
54
+ The fix is to stop sending every schema on every request, and send only the few that
55
+ matter for the current turn. Anthropic ships this as
56
+ [tool search](https://docs.claude.com/en/docs/agents-and-tools/tool-use/tool-search-tool).
57
+ OpenAI ships an equivalent.
58
+
59
+ **Gemini does not. Ollama does not. vLLM does not. Your 8B local model does not.**
60
+
61
+ acri is that layer, for everyone else — one import, no gateway, no daemon, no framework
62
+ to adopt.
63
+
64
+ ## What acri is
65
+
66
+ A client-side, provider-agnostic capability resolver. It sits between your code and the
67
+ LLM API, and decides which tools the model gets to see this turn.
68
+
69
+ ## What acri is not
70
+
71
+ - **Not an MCP replacement.** MCP defines how tools connect. acri decides which of them
72
+ the model sees. Your existing MCP servers work unchanged.
73
+ - **Not a framework.** No orchestration, no graph, no agent loop. It is a function you
74
+ call. Use it inside LangGraph, inside the raw SDK, or inside nothing.
75
+ - **Not a proxy or gateway.** No process to run, no port to open, no network hop added.
76
+ - **Not a model.** Bring your own key. acri never calls an LLM you didn't ask for.
77
+
78
+ ## The system
79
+
80
+ ```
81
+ acri
82
+ ├── corpus the capability index — MCP servers, OpenAPI, plain functions,
83
+ │ indexed into one searchable body
84
+ ├── compass the resolver — intent in, the right k tools out ← the core
85
+ ├── router the tier picker — cheap/strong model, chosen once, before generating
86
+ ├── port provider adapters — gemini · openai-compatible (OpenAI, vLLM, Ollama, ...)
87
+ ├── config acri.yaml — declares capabilities and limits, never control flow
88
+ ├── daemon the OpenAI-shaped request handler — acri.run(), nothing more
89
+ ├── server `acri up` — binds it: stdlib http.server, SSE, localhost by default
90
+ ├── gate the necessity check — does this turn need a tool at all? (advisory)
91
+ ├── press the compactor — big payloads to short digests + a handle
92
+ ├── sandbox CPU/memory/network/volume limits on stdio MCP servers, via docker
93
+ ├── ledger the decision trace — what was chosen, skipped, and what it cost
94
+ ├── studio `acri studio` — read-only dashboard over acri.yaml + the ledger, own process/port
95
+ └── assay the proving ground — the only place a benchmark number may come from
96
+
97
+ examples/ end-to-end scripts against a real MCP server — not part of the installed package
98
+ rust/ minimal Rust port of corpus + compass, v0.1 scope only
99
+ typescript/ minimal TypeScript port of corpus + compass, v0.1 scope only
100
+ ```
101
+
102
+ ## Status
103
+
104
+ **Pre-alpha — API not yet stable (v0.x, see `docs/decisions.md`). `corpus` + `compass` +
105
+ `port` + `ledger` + `assay` + an exact-match cache + a pre-generation router all exist,
106
+ are tested, and back every claim below with a script or a test file. `daemon` (`acri up`)
107
+ exists too, ahead of its own gate — see the v1.0 roadmap row for what that means.**
108
+
109
+ Not yet published to PyPI — [`.github/workflows/publish.yml`](.github/workflows/publish.yml) exists but
110
+ hasn't been triggered (needs a trusted publisher registered on PyPI's side, then a GitHub Release). The
111
+ distribution name will be `pyacri`, not `acri` — PyPI blocks names within edit-distance-1 of an existing
112
+ package (`acris`, `acr`, and `acre` already exist, all unrelated); `import acri` and the `acri` command
113
+ are unaffected, since PyPI's distribution name and the Python import name are separate settings. Until
114
+ published, install from a clone:
115
+
116
+ ```bash
117
+ pip install -e ".[dev]"
118
+ ```
119
+
120
+ ```python
121
+ import acri
122
+
123
+ tools = acri.from_callables([get_weather, get_stock_price, merge_pull_request])
124
+ corpus = acri.index(tools) # build once, reuse across the whole task
125
+
126
+ result = acri.run("what's the weather in Tokyo?", corpus, my_openai_client)
127
+ print(result.tool_calls) # [{"name": "get_weather", "arguments": '{"city": "Tokyo"}'}]
128
+ ```
129
+
130
+ `acri.run()` resolves, calls the provider, and — if you pass `ledger=acri.Ledger()` — records
131
+ the trace. Prefer to drive the pieces yourself? `acri.resolve(query, corpus, k=5)` returns the
132
+ ranked tools; `acri.gemini` / `acri.openai_compatible` take it from there.
133
+
134
+ Read [`docs/architecture.md`](docs/architecture.md) for the full design, the prior art it
135
+ builds on, and the claims it explicitly refuses to make. Read
136
+ [`docs/decisions.md`](docs/decisions.md) for every capability that was proposed and cut,
137
+ with the evidence that decided it.
138
+
139
+ ### First numbers
140
+
141
+ Measured on a synthetic 100-tool corpus spanning 20 domains (github, postgres, stripe,
142
+ aws_ec2, jira, zendesk, ...) with realistic cross-domain confusability, and 50 hand-written
143
+ queries phrased the way someone would actually type them, not as paraphrases of the tool
144
+ descriptions. Reproduce: `pip install -e ".[dev]"` then `python -m assay.recall`. Fixtures
145
+ and script: [`assay/fixtures.json`](assay/fixtures.json), [`assay/recall.py`](assay/recall.py).
146
+
147
+ | k | recall@k | tools shown instead of 100 |
148
+ |---|----------|-----------------------------|
149
+ | 1 | 74% — [`assay/recall.py`](assay/recall.py) | 1 |
150
+ | 3 | 90% — [`assay/recall.py`](assay/recall.py) | 3 |
151
+ | 5 | 100% — [`assay/recall.py`](assay/recall.py) | 5 |
152
+ | 10 | 100% — [`assay/recall.py`](assay/recall.py) | 10 |
153
+
154
+ Both adversarial queries (no correct tool exists in the corpus) correctly resolve to
155
+ nothing, at every k. The k=5 row moved from 86% to 100% ([`assay/recall.py`](assay/recall.py))
156
+ after a small query-side synonym table was added to `compass.py` —
157
+ [`_ALIASES`](acri/compass.py), 10 entries, each traced to
158
+ a real miss, not guessed (`pr` → `pull`, `request`; `rain` → `weather`; `text` → `sms`,
159
+ `message`; and so on). It expands the query only, never the indexed tool text, so a tool's
160
+ own description can't start quietly matching a synonym it never claimed.
161
+
162
+ Recall says the right tool is *available* to the model in a much smaller set — not that
163
+ the model picks it. That's a separate, live-model question:
164
+ [`assay/accuracy.py`](assay/accuracy.py), naive (all 100 tools) vs. acri (top 5), same 50
165
+ queries, real `gemini-2.5-flash` calls, no mocking.
166
+
167
+ **Corrected twice, both times in public.** The first published number here claimed acri roughly doubles accuracy: naive ~24%, acri ~53% (commit history on [`assay/accuracy.py`](assay/accuracy.py)). That was a broken benchmark: every one
168
+ of the 100 fixture tools had an *empty* parameter schema, so a well-behaved model correctly
169
+ declined to fabricate a ticket ID or a SQL string it was never given, and that correct
170
+ caution was scored as a tool-selection failure. Fixing the schemas and the obviously
171
+ under-specified queries produced a second number: naive 72%, acri 74% ([`assay/accuracy.py`](assay/accuracy.py)), that this README called modest and honest. It
172
+ was honest, but still not fully clean: a live diagnostic pass
173
+ (`assay/diagnose.py`) surfaced 9 more queries that satisfied their tool's *declared*
174
+ `required` fields but still left a genuinely necessary value unstated — "bump it up a size"
175
+ with no target size, "point \[domain\] at the new server" with no address, an opportunity
176
+ referenced by company name instead of ID. Full list and reasoning:
177
+ [`docs/decisions.md`](docs/decisions.md). Fixed the same way as the first round — supply the
178
+ missing value, or mark the field `required` if the action is meaningless without it — never
179
+ by rewording toward the answer. Third run, current:
180
+
181
+ | Arm | accuracy | median latency |
182
+ |---|---|---|
183
+ | naive (100 tools) | 84% — [`assay/accuracy.py`](assay/accuracy.py) | 1792 ms |
184
+ | acri (top 5) | 92% — [`assay/accuracy.py`](assay/accuracy.py) | 1596 ms |
185
+
186
+ **An 8-point gap this time, not noise** — for scale, naive's own score moved 2 points
187
+ between the previous two runs with zero code changes on its side, so 2 points is this
188
+ benchmark's rough noise floor at n=50; 8 is well outside it. `assay/diagnose.py` confirms
189
+ `resolver_miss=0` — every remaining acri miss is the model's, not compass's — and all four
190
+ share one shape: the model declines to act (`picked: None`, not a wrong tool) on a
191
+ write/mutating request — refund a charge, launch a server, send an email, book a meeting —
192
+ even once every *required* field is present. That reads as the model being conservative
193
+ about consequential actions specifically, not a resolver or benchmark defect, and it is left
194
+ unpatched on purpose: supplying more certainty than a real caller would have crosses from
195
+ fixing the benchmark into fixing the test until it passes. This is still a single run at each
196
+ stage; repeat runs would firm up the interval and are a natural next step, not yet taken.
197
+ Reproduce: `GEMINI_API_KEY=... python -m assay.accuracy --provider gemini`, or via
198
+ [Vertex AI / ADC](assay/clients.py):
199
+ `GOOGLE_APPLICATION_CREDENTIALS=... GOOGLE_CLOUD_PROJECT=... python -m assay.accuracy --provider vertex`.
200
+
201
+ What this means for the project's claims: **recall — the context-window reduction — remains
202
+ the number that survived every round unaffected** (BM25 never reads a tool's parameters, so
203
+ none of the query-completeness fixes could have moved it; only the synonym layer did, on
204
+ purpose). The accuracy claim is now both real and no longer small, but it took three
205
+ corrected rounds to get a benchmark that measures tool selection instead of measuring
206
+ whether the fixtures gave the model enough to work with — a fact worth remembering before
207
+ trusting the next number this project publishes, including from this project's own authors.
208
+
209
+ **Also verified against a real MCP server, not a mock:**
210
+ [`assay/mcp_live.py`](assay/mcp_live.py) spawns the official
211
+ `@modelcontextprotocol/server-filesystem` over stdio, ingests its live `tools/list` response
212
+ through `acri.adapters.from_mcp_tools()`, resolves a query against the real 14-tool corpus,
213
+ and executes the top-ranked tool through an actual `tools/call`. `list_directory` was the
214
+ top match for "what files are in this directory" (score 1.000 of 14 candidates), and its
215
+ live result contained a marker file planted before the run specifically to prove the pipeline
216
+ is real. Run: `python -m assay.mcp_live <a-directory> "<query>"` (needs Node.js on `PATH`).
217
+
218
+ `compass.resolve()` itself, over 1,040 calls against the same 100-tool corpus: p50 0.040ms,
219
+ p95 0.087ms ([`assay/latency.py`](assay/latency.py)). Not headlined on purpose — see
220
+ [`docs/decisions.md`](docs/decisions.md) for why resolver latency is beside the point
221
+ against a network call measured in hundreds of milliseconds.
222
+
223
+ ### Does it hold up at scale?
224
+
225
+ [`assay/scale.py`](assay/scale.py) reruns recall@k and latency against
226
+ [`assay/fixtures_500.json`](assay/fixtures_500.json) — the same 100 tools and 52 gold queries
227
+ above, plus ~400 more tools across ~40 new domains (gitlab and bitbucket alongside github,
228
+ discord and telegram alongside slack, and so on), so corpus size is the only thing that
229
+ changed. It isn't: recall@5 drops from 100% to 92%, recall@1 from 74% to 60% — [`assay/scale.py`](assay/scale.py).
230
+ Latency stays fast — p50 0.179ms, p95 0.285ms at 504 tools, still nothing against a network
231
+ call. The context-window case gets stronger, not weaker, at this scale: 5 of 504 tools shown is a 99% reduction — [`assay/scale.py`](assay/scale.py), the same
232
+ arithmetic the 100-tool table above already uses. Recall genuinely degrades, though, and
233
+ that's the metric that matters more than either latency or the reduction percentage.
234
+
235
+ The honest reason, checked query by query rather than assumed: BM25 has no semantic
236
+ understanding, so once a real lexical competitor exists it can win. "Is PR 42 on the
237
+ acme/webapp repo merged" now out-scores `github_get_pull_request` with `bitbucket`'s own
238
+ pull-request tools, because neither tool's description names its own platform and the query
239
+ doesn't say which host it means — a gap the original 100-tool corpus never had to close, since
240
+ no second git host existed to be confused with. Not patched: rewording the query toward the
241
+ answer or adding an alias here would be tuning the benchmark, not fixing the resolver — see
242
+ `docs/decisions.md`'s "Corrected twice, both times in public" for why that line gets held.
243
+ Reproduce: `python -m assay.scale`.
244
+
245
+ ### Diagrams
246
+
247
+ ![acri resolution flow: corpus.index once, compass.resolve per query, top 5 of 100 tools sent to port](docs/assets/resolution-flow.svg)
248
+
249
+ ![acri benchmark results: recall@k and naive vs acri tool-selection accuracy, both linked to their assay scripts](docs/assets/benchmark-results.svg)
250
+
251
+ Both are static SVGs generated from the numbers above, nothing more — no simulator, no live
252
+ trace, no feature these diagrams show that isn't already shipped and linked to the script
253
+ that measured it. If a future version adds `studio` (the real trace visualizer — see
254
+ [`docs/decisions.md`](docs/decisions.md)), it replaces these; until then, these are it.
255
+
256
+ ### Roadmap
257
+
258
+ | Version | Scope | Gate to ship |
259
+ |---------|-------|--------------|
260
+ | **v0.1** | `corpus` + `compass` + `port` + minimal `ledger` | **Shipped.** `pytest` green, no native deps. |
261
+ | **v0.2** | `assay` | **Shipped.** Recall, latency, and a live accuracy result, all above — the accuracy number was corrected twice after the benchmark itself was found to be flawed, both times in public. |
262
+ | **v0.3** | Pre-generation router, exact-match cache | **Shipped.** Cache: `acri.run(..., cache={})` skips a repeated (provider, model, query, offered tools) call — [`acri/port.py`](acri/port.py)'s `cached_call`. Router: `acri.run(..., cheap_model=...)` routes one call to a cheaper tier, once, before generating — [`acri/router.py`](acri/router.py). Eligibility (is this call stateless and prefix-free?) is the caller's judgment, not acri's — see `docs/architecture.md` §4.4. Tests: [`tests/test_ports.py`](tests/test_ports.py), [`tests/test_router.py`](tests/test_router.py), [`tests/test_integration.py`](tests/test_integration.py). |
263
+ | **later** | `gate`, `press` | **Shipped, ahead of their gate** ("only if `ledger` data proves they are needed" — no real ledger data exists yet; built at the maintainer's explicit request, same override pattern as the daemon). `gate` ([`acri/gate.py`](acri/gate.py)): a threshold on the raw BM25 score `compass` already computes — `Resolved.score` is always 1.0 for the winner by construction, so `raw_top_score()` exposes the un-normalized number instead. No default threshold ships; picking one needs the same ledger data the gate condition itself was waiting for, so it stays caller-supplied. `press` ([`acri/press.py`](acri/press.py)): large tool results become a digest plus a handle to the untruncated original — `recover()` gets it back, which is the answer to the "digest drops an identifier" risk `docs/architecture.md` names. TOON-style header-once encoding for uniform tabular results; not the `toon-format` PyPI package, whose `encode()` raises `NotImplementedError` as of 0.1.0 — checked directly before writing this instead. Tests: [`tests/test_gate.py`](tests/test_gate.py), [`tests/test_press.py`](tests/test_press.py). |
264
+ | **v1.1** | `sandbox` — CPU/memory/network/volume limits on stdio MCP servers; `find_more_tools`, the escape hatch `docs/architecture.md` §4.1 always documented | **Shipped.** [`acri/sandbox.py`](acri/sandbox.py) wraps an `mcp:` entry's command in `docker run -i --rm` with resource limits and, via `acri.yaml`'s `sandbox.volumes:` (host path → container path), `-v` mounts — so a sandboxed filesystem/git server can see a real project folder; append `:ro` to a container path yourself for read-only, no separate flag. Calls the container engine, never reimplements namespaces/cgroups, per `docs/decisions.md`. Docker Desktop's daemon wasn't running in the environment this was built in, so the command-construction is tested ([`tests/test_sandbox.py`](tests/test_sandbox.py)) but an actual `docker run` proxying a real MCP session is not — stated plainly, not glossed over. `find_more_tools` ([`acri/escape_hatch.py`](acri/escape_hatch.py)): `acri.run()` always appends it to the tools actually sent to the provider — not to `compass.resolve()`'s own output, so the recall@k numbers above are unaffected, and not to the ledger's `offered`, which stays the real resolution candidates only ([`tests/test_integration.py`](tests/test_integration.py)). Calling it re-searches the full corpus; the caller executes it and appends the result like any other tool call, per §4.1's "append, never rewrite" rule — acri does not auto-execute it. Tests: [`tests/test_sandbox.py`](tests/test_sandbox.py), [`tests/test_config.py`](tests/test_config.py), [`tests/test_escape_hatch.py`](tests/test_escape_hatch.py). Live, end-to-end demo against a real MCP server: [`examples/live_demo.py`](examples/live_demo.py). |
265
+ | **v1.0** | `daemon` — long-lived process, OpenAI-compatible HTTP endpoint | **Shipped, ahead of its own gate.** `docs/decisions.md`: "the daemon is built after the library has users who want it, not before" — not met (no PyPI release yet), and built anyway at the maintainer's explicit request; that override is deliberate, not an oversight. `acri up` ([`acri/server.py`](acri/server.py), stdlib `http.server` only) connects to `acri.yaml`'s `mcp:` entries once at startup ([`acri/mcp_connect.py`](acri/mcp_connect.py)), then serves `/v1/chat/completions` over SSE via [`acri/daemon.py`](acri/daemon.py)'s handler — the same `acri.run()` the library calls, not a reimplementation. Binds `127.0.0.1` by default; conversation content is opt-in (`--log-conversations`), off by default via `RedactingLedger`. Verified against a real MCP server and a real Gemini call, not just fakes — which surfaced and fixed a real bug: Gemini's function-calling schema rejects the `$schema` key real MCP servers commonly add, invisible to every synthetic fixture in this repo (`acri/schemas.py`). Tests: [`tests/test_config.py`](tests/test_config.py), [`tests/test_cli.py`](tests/test_cli.py), [`tests/test_daemon.py`](tests/test_daemon.py), [`tests/test_server.py`](tests/test_server.py), [`tests/test_schemas.py`](tests/test_schemas.py). Wire-level SSE only — one blocking `acri.run()` call chunked out, not a true streaming upstream call. |
266
+ | **v1.2** | `studio`, minimal Rust and TypeScript ports, PyPI publish workflow | **Shipped, ahead of gate, each stated plainly.** `studio` ([`acri/studio.py`](acri/studio.py)): decisions.md already had a full design for this (`docs/decisions.md`, "`studio` — the trace visualizer") — built to that spec, not invented from scratch; two honest simplifications against it recorded in [`docs/architecture.md`](docs/architecture.md) §4.5. Own process, own port (`acri studio`, default 8099), reads only `acri.yaml` and `.acri/ledger.jsonl`, never connects to an MCP server or a model — a separate `pip install pyacri[studio]` extra, per decisions.md's own naming (PyPI's distribution name; `import acri` and the `acri` command are unaffected). Two views: the static mesh (servers/models/tools ever seen) and the live trace (recent ledger entries, polled every 2s). `rust/` and `typescript/`: v0.1-scope-only ports of `corpus` + `compass` (BM25 resolve()) — nothing else. Rust's gate ("only if `assay` shows Python ranking is a measurable share of a turn") has been measured and answered no (`assay/scale.py`: p50 0.179ms against network calls running hundreds of ms+); TypeScript's gate ("after the Python package has users, never in parallel") is unmet on its own terms, no PyPI release exists yet. Both verified against `tests/test_compass.py`'s exact cases: Rust 9/9 (`cargo test`), TypeScript 5/5 (`npm test`). [`.github/workflows/publish.yml`](.github/workflows/publish.yml): OIDC trusted publishing, triggered only by a published GitHub Release, not a tag push — needs a one-time trusted-publisher registration on PyPI's side before it can run at all. Tests: [`tests/test_studio.py`](tests/test_studio.py), [`tests/test_studio_data.py`](tests/test_studio_data.py). |
267
+
268
+ ## The claims policy
269
+
270
+ This project makes no performance claim it cannot reproduce.
271
+
272
+ No number appears in this repository — README, docs, paper, or commit message — unless a
273
+ script in `assay/` regenerates it from a public benchmark, and the run is committed
274
+ alongside it. Estimates are labelled as estimates. Projections are labelled as
275
+ projections.
276
+
277
+ If you find a number here without its receipt, that is a bug. Please
278
+ [open an issue](../../issues/new).
279
+
280
+ ## Prior art
281
+
282
+ acri stands on published work and does not pretend otherwise:
283
+
284
+ - [RAG-MCP](https://arxiv.org/abs/2505.03275) — retrieval over tool schemas to cut prompt bloat
285
+ - [When2Call](https://arxiv.org/abs/2504.18851) — when (not) to call tools
286
+ - [Anthropic tool search](https://docs.claude.com/en/docs/agents-and-tools/tool-use/tool-search-tool) and [context editing](https://docs.claude.com/en/docs/build-with-claude/context-editing) — the same idea, shipped provider-side
287
+
288
+ acri's contribution is **placement**: the same capability, client-side and
289
+ provider-agnostic, for the models that don't have it natively.
290
+
291
+ ## License
292
+
293
+ MIT — see [LICENSE](LICENSE).
294
+
295
+ Built by [Piyush Sharma](https://github.com/ScienHAC) under [INERATE](https://github.com/INERATE),
296
+ alongside [Atelier](https://github.com/INERATE/atelier).