acm-context-management 0.2.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 (68) hide show
  1. acm_context_management-0.2.0/.gitignore +10 -0
  2. acm_context_management-0.2.0/PACKAGING.md +139 -0
  3. acm_context_management-0.2.0/PKG-INFO +111 -0
  4. acm_context_management-0.2.0/README.md +78 -0
  5. acm_context_management-0.2.0/acm_engine/__init__.py +124 -0
  6. acm_context_management-0.2.0/acm_engine/_vendor/cache_layout.py +218 -0
  7. acm_context_management-0.2.0/acm_engine/_vendor/context_editing.py +813 -0
  8. acm_context_management-0.2.0/acm_engine/_vendor/context_profiles.py +560 -0
  9. acm_context_management-0.2.0/acm_engine/_vendor/relevance.py +688 -0
  10. acm_context_management-0.2.0/acm_engine/_vendor/relevance_encoder.py +222 -0
  11. acm_context_management-0.2.0/acm_engine/_vendor/visual_tool/__init__.py +5 -0
  12. acm_context_management-0.2.0/acm_engine/_vendor/visual_tool/indexer.py +121 -0
  13. acm_context_management-0.2.0/acm_engine/_vendor/visual_tool/rasterizer.py +235 -0
  14. acm_context_management-0.2.0/acm_gateway/__init__.py +5 -0
  15. acm_context_management-0.2.0/acm_gateway/__main__.py +21 -0
  16. acm_context_management-0.2.0/acm_gateway/app.py +1231 -0
  17. acm_context_management-0.2.0/acm_gateway/config.py +170 -0
  18. acm_context_management-0.2.0/acm_gateway/context_windows.py +203 -0
  19. acm_context_management-0.2.0/acm_gateway/droplist.py +478 -0
  20. acm_context_management-0.2.0/acm_gateway/events.py +89 -0
  21. acm_context_management-0.2.0/acm_gateway/pipeline.py +173 -0
  22. acm_context_management-0.2.0/acm_gateway/providers_store.py +190 -0
  23. acm_context_management-0.2.0/acm_gateway/translate.py +111 -0
  24. acm_context_management-0.2.0/acm_gateway/translate_anthropic.py +295 -0
  25. acm_context_management-0.2.0/acm_gateway/upstream.py +287 -0
  26. acm_context_management-0.2.0/acm_gateway/visualizer.py +120 -0
  27. acm_context_management-0.2.0/acm_mcp/__init__.py +5 -0
  28. acm_context_management-0.2.0/acm_mcp/jit.py +106 -0
  29. acm_context_management-0.2.0/acm_mcp/memory_store.py +55 -0
  30. acm_context_management-0.2.0/acm_mcp/server.py +271 -0
  31. acm_context_management-0.2.0/adapters/claude-code/README.md +87 -0
  32. acm_context_management-0.2.0/adapters/cursor/README.md +41 -0
  33. acm_context_management-0.2.0/adapters/vscode/README.md +103 -0
  34. acm_context_management-0.2.0/adapters/vscode/node_modules/@esbuild/darwin-arm64/README.md +3 -0
  35. acm_context_management-0.2.0/adapters/vscode/node_modules/@types/debug/README.md +69 -0
  36. acm_context_management-0.2.0/adapters/vscode/node_modules/@types/estree/README.md +15 -0
  37. acm_context_management-0.2.0/adapters/vscode/node_modules/@types/estree-jsx/README.md +15 -0
  38. acm_context_management-0.2.0/adapters/vscode/node_modules/@types/hast/README.md +15 -0
  39. acm_context_management-0.2.0/adapters/vscode/node_modules/@types/mdast/README.md +15 -0
  40. acm_context_management-0.2.0/adapters/vscode/node_modules/@types/ms/README.md +82 -0
  41. acm_context_management-0.2.0/adapters/vscode/node_modules/@types/node/README.md +15 -0
  42. acm_context_management-0.2.0/adapters/vscode/node_modules/@types/prop-types/README.md +15 -0
  43. acm_context_management-0.2.0/adapters/vscode/node_modules/@types/react/README.md +15 -0
  44. acm_context_management-0.2.0/adapters/vscode/node_modules/@types/react-dom/README.md +16 -0
  45. acm_context_management-0.2.0/adapters/vscode/node_modules/@types/unist/README.md +15 -0
  46. acm_context_management-0.2.0/adapters/vscode/node_modules/@types/vscode/README.md +15 -0
  47. acm_context_management-0.2.0/adapters/vscode/node_modules/@ungap/structured-clone/README.md +95 -0
  48. acm_context_management-0.2.0/adapters/vscode/node_modules/csstype/README.md +291 -0
  49. acm_context_management-0.2.0/adapters/vscode/node_modules/debug/README.md +481 -0
  50. acm_context_management-0.2.0/adapters/vscode/node_modules/esbuild/README.md +3 -0
  51. acm_context_management-0.2.0/adapters/vscode/node_modules/extend/README.md +81 -0
  52. acm_context_management-0.2.0/adapters/vscode/node_modules/inline-style-parser/README.md +229 -0
  53. acm_context_management-0.2.0/adapters/vscode/node_modules/js-tokens/README.md +240 -0
  54. acm_context_management-0.2.0/adapters/vscode/node_modules/loose-envify/README.md +45 -0
  55. acm_context_management-0.2.0/adapters/vscode/node_modules/parse-entities/node_modules/@types/unist/README.md +122 -0
  56. acm_context_management-0.2.0/adapters/vscode/node_modules/react/README.md +37 -0
  57. acm_context_management-0.2.0/adapters/vscode/node_modules/react-dom/README.md +60 -0
  58. acm_context_management-0.2.0/adapters/vscode/node_modules/scheduler/README.md +9 -0
  59. acm_context_management-0.2.0/adapters/vscode/node_modules/style-to-js/README.md +271 -0
  60. acm_context_management-0.2.0/adapters/vscode/node_modules/style-to-object/README.md +188 -0
  61. acm_context_management-0.2.0/adapters/vscode/node_modules/typescript/README.md +50 -0
  62. acm_context_management-0.2.0/adapters/vscode/node_modules/undici-types/README.md +6 -0
  63. acm_context_management-0.2.0/config/acm.config.example.json +39 -0
  64. acm_context_management-0.2.0/pyproject.toml +71 -0
  65. acm_context_management-0.2.0/scripts/e2e_relevance.py +145 -0
  66. acm_context_management-0.2.0/scripts/smoke.sh +75 -0
  67. acm_context_management-0.2.0/scripts/sync_engine.py +74 -0
  68. acm_context_management-0.2.0/scripts/wire_test.py +138 -0
@@ -0,0 +1,10 @@
1
+ # Python build artifacts
2
+ dist/
3
+ build/
4
+ *.egg-info/
5
+ __pycache__/
6
+ *.pyc
7
+ .venv/
8
+
9
+ # Local config copy (user edits; example is shipped)
10
+ config/acm.config.json
@@ -0,0 +1,139 @@
1
+ # Packaging & publishing
2
+
3
+ The extension ships as **one PyPI distribution** — `acm-context-management` —
4
+ exposing two console scripts: `acm-gateway` and `acm-mcp`. It is self-contained:
5
+ the context-management engine is **vendored** into `acm_engine/_vendor/`, so an
6
+ end user never needs the website's `../backend`.
7
+
8
+ ## Before you build: sync the vendored engine
9
+
10
+ The vendored copy is committed, but refresh it whenever the engine changes in
11
+ `backend/`:
12
+
13
+ ```bash
14
+ cd extension
15
+ python scripts/sync_engine.py # copies the 3 modules into acm_engine/_vendor/
16
+ git add acm_engine/_vendor && git commit -m "sync vendored engine"
17
+ ```
18
+
19
+ ## Build
20
+
21
+ ```bash
22
+ cd extension
23
+ uv build # -> dist/acm_context_management-0.2.0-py3-none-any.whl + .tar.gz
24
+ ```
25
+
26
+ Confirm the wheel is self-contained (contains the vendored engine **and** the
27
+ `visual_tool/` subpackage — without it the visual method breaks once installed):
28
+
29
+ ```bash
30
+ python -m zipfile -l dist/acm_context_management-0.2.0-py3-none-any.whl | grep _vendor
31
+ # acm_engine/_vendor/{context_profiles,context_editing,cache_layout,relevance,
32
+ # relevance_encoder}.py + acm_engine/_vendor/visual_tool/{rasterizer,indexer,__init__}.py
33
+ ```
34
+
35
+ Smoke-test a clean install (run it from a directory **other** than `extension/`
36
+ so it doesn't pick up the source tree):
37
+
38
+ ```bash
39
+ uv venv /tmp/acmwheel --python 3.11
40
+ uv pip install --python /tmp/acmwheel/bin/python dist/acm_context_management-0.2.0-py3-none-any.whl
41
+ cd /tmp && /tmp/acmwheel/bin/python -c "from acm_gateway.app import app; \
42
+ from visual_tool.rasterizer import render_2col_pages; print('ok', len(render_2col_pages('x'*9000)), 'pages')"
43
+ ```
44
+
45
+ ## Publish to PyPI
46
+
47
+ ```bash
48
+ uv publish # uses UV_PUBLISH_TOKEN or ~/.pypirc
49
+ # first, test against TestPyPI:
50
+ uv publish --publish-url https://test.pypi.org/legacy/
51
+ ```
52
+
53
+ ## How end users run it
54
+
55
+ ```bash
56
+ # zero-install, ephemeral (uvx resolves the package, runs the script):
57
+ uvx --from acm-context-management acm-gateway
58
+ uvx --from acm-context-management acm-mcp
59
+
60
+ # or install once:
61
+ uv tool install acm-context-management
62
+ acm-gateway # http://127.0.0.1:8807
63
+ acm-mcp # stdio MCP server
64
+ ```
65
+
66
+ Required env for the gateway upstream:
67
+
68
+ ```bash
69
+ export ACM_UPSTREAM_BASE_URL=https://openrouter.ai/api/v1 # OpenAI-compatible
70
+ export ACM_UPSTREAM_API_KEY=sk-or-v1-...
71
+ # for the Anthropic /v1/messages surface (Claude Code):
72
+ export ACM_ANTHROPIC_API_KEY=sk-ant-...
73
+ ```
74
+
75
+ ## Publish the VS Code extension (Open VSX + MS Marketplace)
76
+
77
+ The webview UI ships as a `.vsix`. **Open VSX is the priority** — it's where
78
+ Cursor / Windsurf / Antigravity (VS Code forks) install extensions; the MS
79
+ Marketplace covers stock VS Code.
80
+
81
+ ```bash
82
+ cd extension/adapters/vscode
83
+ npm install # first time only
84
+ npm run package # -> acm-context-management-0.2.0.vsix (runs vsce, bundles UI)
85
+ ```
86
+
87
+ One-time setup before the first publish:
88
+
89
+ - **Publisher id** — `package.json` uses `"publisher": "acm"`. Create that
90
+ publisher (or change the field to one you own) on **both** registries.
91
+ - **Icon** — add a 128×128+ **PNG** at `media/icon.png` and set
92
+ `"icon": "media/icon.png"` in `package.json`. The marketplaces show a
93
+ placeholder without it (packaging still succeeds; this is listing quality).
94
+
95
+ Publish:
96
+
97
+ ```bash
98
+ # Open VSX (Cursor / Windsurf / Antigravity / VSCodium)
99
+ # token: https://open-vsx.org -> account -> Access Tokens
100
+ npx ovsx publish acm-context-management-0.2.0.vsix -p "$OVSX_TOKEN"
101
+
102
+ # MS Marketplace (stock VS Code)
103
+ # PAT: https://dev.azure.com -> Marketplace (manage) publisher
104
+ npx @vscode/vsce publish --no-dependencies -p "$VSCE_TOKEN"
105
+ ```
106
+
107
+ Local install for testing without publishing:
108
+
109
+ ```bash
110
+ code --install-extension acm-context-management-0.2.0.vsix # VS Code
111
+ cursor --install-extension acm-context-management-0.2.0.vsix # Cursor
112
+ ```
113
+
114
+ ## Docker (optional, for a hosted gateway)
115
+
116
+ ```dockerfile
117
+ # extension/Dockerfile (TODO(acm): add this file)
118
+ FROM python:3.12-slim
119
+ RUN pip install acm-context-management
120
+ EXPOSE 8807
121
+ ENV ACM_HOST=0.0.0.0
122
+ CMD ["acm-gateway"]
123
+ ```
124
+
125
+ Publish to GHCR: `docker build -t ghcr.io/<you>/acm-gateway . && docker push …`.
126
+
127
+ ## Versioning
128
+
129
+ Bump `version` in `pyproject.toml` (single source). Tag releases
130
+ `acm-ext-vX.Y.Z`. The VSCode extension has its own version in
131
+ `adapters/vscode/package.json` and its own publish flow (see that folder's
132
+ README — `vsce publish` + `ovsx publish`).
133
+
134
+ ## What is NOT on PyPI
135
+
136
+ The IDE adapters (`adapters/claude-code`, `adapters/cursor`,
137
+ `adapters/vscode`) are config/glue distributed via the repo and the VS
138
+ Marketplace / Open VSX — not pip. Only the gateway + MCP server are Python
139
+ distributions.
@@ -0,0 +1,111 @@
1
+ Metadata-Version: 2.4
2
+ Name: acm-context-management
3
+ Version: 0.2.0
4
+ Summary: Context-management gateway + MCP server that brings the ACM techniques (trimming, summarisation, image-recall, memory) into any IDE
5
+ Project-URL: Homepage, https://github.com/HarishanA21/agentic_context_management
6
+ Project-URL: Repository, https://github.com/HarishanA21/agentic_context_management
7
+ Author: ACM
8
+ License: MIT
9
+ Keywords: claude-code,context-management,cursor,gateway,ides,llm,mcp,vscode
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
+ Classifier: Topic :: Software Development :: Libraries
18
+ Requires-Python: >=3.11
19
+ Requires-Dist: fastapi>=0.110
20
+ Requires-Dist: httpx>=0.27
21
+ Requires-Dist: langchain-core>=0.3
22
+ Requires-Dist: langchain>=0.3
23
+ Requires-Dist: mcp>=1.2
24
+ Requires-Dist: pillow>=10
25
+ Requires-Dist: pydantic>=2
26
+ Requires-Dist: python-dotenv>=1.0
27
+ Requires-Dist: uvicorn[standard]>=0.29
28
+ Provides-Extra: dev
29
+ Requires-Dist: build; extra == 'dev'
30
+ Requires-Dist: pytest>=8; extra == 'dev'
31
+ Requires-Dist: twine; extra == 'dev'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # ACM Extension — context management for any IDE
35
+
36
+ This folder is **self-contained**. It turns the context-management engine that
37
+ already powers the website (in [`../backend`](../backend)) into something usable
38
+ from VSCode, Cursor, Antigravity, Claude Code, and Windsurf — **without changing
39
+ a single line of the website's code.**
40
+
41
+ It reaches the IDEs through three surfaces (see
42
+ [`../EXTENSION_PLAN.md`](../EXTENSION_PLAN.md) for the why):
43
+
44
+ | Folder | Surface | What it is |
45
+ |---|---|---|
46
+ | `acm_gateway/` | **Gateway** | A local OpenAI/Anthropic-compatible proxy. The IDE talks to it as if it were the AI; it applies every technique on the wire, then forwards to the real provider. Full feature parity. |
47
+ | `acm_mcp/` | **MCP** | An MCP server exposing `remember` / `recall` / `compact` / `set_profile` tools. Works in *every* MCP-capable IDE. |
48
+ | `adapters/` | **Hooks + config** | Per-IDE glue: Claude Code hooks, Cursor `mcp.json`, VSCode notes. |
49
+ | `acm_engine/` | — | Thin bridge that imports the techniques from `../backend` so there is **one** source of truth. |
50
+
51
+ ## How it reuses the website's engine
52
+
53
+ `acm_engine/` puts `../backend` on `sys.path` and re-exports the pure technique
54
+ functions (`trim_tool_results`, `summarise_old_messages`,
55
+ `sliding_window_trim`, `evict_stale_images`, `annotate_cache_breakpoints`) plus
56
+ the `Profile` schema. Nothing in `../backend` is edited. The website and the
57
+ extension share the same code; fix a bug once, both benefit.
58
+
59
+ ## Quick start (gateway)
60
+
61
+ ```bash
62
+ cd extension
63
+ uv venv --python 3.11
64
+ source .venv/bin/activate
65
+ uv pip install -e .
66
+
67
+ # point it at any OpenAI-compatible upstream (OpenRouter shown)
68
+ export ACM_UPSTREAM_BASE_URL=https://openrouter.ai/api/v1
69
+ export ACM_UPSTREAM_API_KEY=sk-or-v1-...
70
+
71
+ acm-gateway # serves http://127.0.0.1:8807
72
+ ```
73
+
74
+ Then tell the IDE the AI lives at `http://127.0.0.1:8807/v1` (Cursor: custom
75
+ OpenAI base URL; Claude Code: `ANTHROPIC_BASE_URL`).
76
+
77
+ ## Config
78
+
79
+ A single [`config/acm.config.example.json`](config/acm.config.example.json)
80
+ holds the active profile (which techniques are on). Copy it to
81
+ `config/acm.config.json` and edit, or set `ACM_CONFIG` to its path. The schema
82
+ is the website's `Profile` model — same presets (`minimal`, `long_chat`,
83
+ `power_research`, `cheap_long`, `visual_recall`).
84
+
85
+ ## Status
86
+
87
+ - **Gateway** — runnable. OpenAI (`/v1/chat/completions`) **and** Anthropic
88
+ (`/v1/messages`) surfaces; full technique pipeline (trim / summarise / sliding
89
+ window / image-evict / cache breakpoints / **visual method** — rasterise big
90
+ tool outputs to images); **manual message removal** (cascade-safe drop-list,
91
+ persisted to `~/.acm/dropped.json`); **multi-provider routing** (OpenAI /
92
+ OpenRouter / Google / Azure on the OpenAI surface, Anthropic native; creds in
93
+ `~/.acm/providers.json`; route by `x-acm-provider` header, `slug/model`
94
+ prefix, or default — Bedrock via OpenRouter since it needs AWS SigV4); control
95
+ plane (`/status`, `/profile`, `/memory`, `/compact`, `/conversations`,
96
+ `/messages*`, `/providers*`).
97
+ - **MCP server** — runnable: memory (`remember`/`recall`), `compact`,
98
+ `set_profile`/`status`, manual removal (`list_messages`/`drop_message`/
99
+ `restore_message`), providers (`list_providers`/`add_provider`/
100
+ `set_default_provider`), **sub-agent** (`spawn_subagent`), and **JIT retrieval**
101
+ (`find_files`/`read_slice`/`grep_files`).
102
+ - **Claude Code** — gateway + hooks (trim + memory) + MCP. Done.
103
+ - **Cursor** — gateway + hooks (capture + **compact-on-stop**) + MCP. Done.
104
+ - **VSCode** — full extension (Language Model Tools + `@acm` chat participant +
105
+ webview settings) talking to the gateway; MCP drop-in for Antigravity /
106
+ Windsurf. TypeScript is written but not yet `tsc`-compiled here (needs
107
+ `npm install` with `@types/vscode`).
108
+
109
+ Remaining work is marked with `# TODO(acm)` / `TODO(acm)`: orphan-`tool_use`
110
+ pruning on the Anthropic path, React components in the VSCode webview, and
111
+ auto-starting the gateway from the extension.
@@ -0,0 +1,78 @@
1
+ # ACM Extension — context management for any IDE
2
+
3
+ This folder is **self-contained**. It turns the context-management engine that
4
+ already powers the website (in [`../backend`](../backend)) into something usable
5
+ from VSCode, Cursor, Antigravity, Claude Code, and Windsurf — **without changing
6
+ a single line of the website's code.**
7
+
8
+ It reaches the IDEs through three surfaces (see
9
+ [`../EXTENSION_PLAN.md`](../EXTENSION_PLAN.md) for the why):
10
+
11
+ | Folder | Surface | What it is |
12
+ |---|---|---|
13
+ | `acm_gateway/` | **Gateway** | A local OpenAI/Anthropic-compatible proxy. The IDE talks to it as if it were the AI; it applies every technique on the wire, then forwards to the real provider. Full feature parity. |
14
+ | `acm_mcp/` | **MCP** | An MCP server exposing `remember` / `recall` / `compact` / `set_profile` tools. Works in *every* MCP-capable IDE. |
15
+ | `adapters/` | **Hooks + config** | Per-IDE glue: Claude Code hooks, Cursor `mcp.json`, VSCode notes. |
16
+ | `acm_engine/` | — | Thin bridge that imports the techniques from `../backend` so there is **one** source of truth. |
17
+
18
+ ## How it reuses the website's engine
19
+
20
+ `acm_engine/` puts `../backend` on `sys.path` and re-exports the pure technique
21
+ functions (`trim_tool_results`, `summarise_old_messages`,
22
+ `sliding_window_trim`, `evict_stale_images`, `annotate_cache_breakpoints`) plus
23
+ the `Profile` schema. Nothing in `../backend` is edited. The website and the
24
+ extension share the same code; fix a bug once, both benefit.
25
+
26
+ ## Quick start (gateway)
27
+
28
+ ```bash
29
+ cd extension
30
+ uv venv --python 3.11
31
+ source .venv/bin/activate
32
+ uv pip install -e .
33
+
34
+ # point it at any OpenAI-compatible upstream (OpenRouter shown)
35
+ export ACM_UPSTREAM_BASE_URL=https://openrouter.ai/api/v1
36
+ export ACM_UPSTREAM_API_KEY=sk-or-v1-...
37
+
38
+ acm-gateway # serves http://127.0.0.1:8807
39
+ ```
40
+
41
+ Then tell the IDE the AI lives at `http://127.0.0.1:8807/v1` (Cursor: custom
42
+ OpenAI base URL; Claude Code: `ANTHROPIC_BASE_URL`).
43
+
44
+ ## Config
45
+
46
+ A single [`config/acm.config.example.json`](config/acm.config.example.json)
47
+ holds the active profile (which techniques are on). Copy it to
48
+ `config/acm.config.json` and edit, or set `ACM_CONFIG` to its path. The schema
49
+ is the website's `Profile` model — same presets (`minimal`, `long_chat`,
50
+ `power_research`, `cheap_long`, `visual_recall`).
51
+
52
+ ## Status
53
+
54
+ - **Gateway** — runnable. OpenAI (`/v1/chat/completions`) **and** Anthropic
55
+ (`/v1/messages`) surfaces; full technique pipeline (trim / summarise / sliding
56
+ window / image-evict / cache breakpoints / **visual method** — rasterise big
57
+ tool outputs to images); **manual message removal** (cascade-safe drop-list,
58
+ persisted to `~/.acm/dropped.json`); **multi-provider routing** (OpenAI /
59
+ OpenRouter / Google / Azure on the OpenAI surface, Anthropic native; creds in
60
+ `~/.acm/providers.json`; route by `x-acm-provider` header, `slug/model`
61
+ prefix, or default — Bedrock via OpenRouter since it needs AWS SigV4); control
62
+ plane (`/status`, `/profile`, `/memory`, `/compact`, `/conversations`,
63
+ `/messages*`, `/providers*`).
64
+ - **MCP server** — runnable: memory (`remember`/`recall`), `compact`,
65
+ `set_profile`/`status`, manual removal (`list_messages`/`drop_message`/
66
+ `restore_message`), providers (`list_providers`/`add_provider`/
67
+ `set_default_provider`), **sub-agent** (`spawn_subagent`), and **JIT retrieval**
68
+ (`find_files`/`read_slice`/`grep_files`).
69
+ - **Claude Code** — gateway + hooks (trim + memory) + MCP. Done.
70
+ - **Cursor** — gateway + hooks (capture + **compact-on-stop**) + MCP. Done.
71
+ - **VSCode** — full extension (Language Model Tools + `@acm` chat participant +
72
+ webview settings) talking to the gateway; MCP drop-in for Antigravity /
73
+ Windsurf. TypeScript is written but not yet `tsc`-compiled here (needs
74
+ `npm install` with `@types/vscode`).
75
+
76
+ Remaining work is marked with `# TODO(acm)` / `TODO(acm)`: orphan-`tool_use`
77
+ pruning on the Anthropic path, React components in the VSCode webview, and
78
+ auto-starting the gateway from the extension.
@@ -0,0 +1,124 @@
1
+ """Bridge to the website's context-management engine.
2
+
3
+ This is the *one* place the extension reaches into ``../backend``. It puts the
4
+ backend directory on ``sys.path`` and re-exports the **pure** technique
5
+ functions plus the ``Profile`` schema, so the gateway and the website run the
6
+ exact same code. Nothing in ``../backend`` is edited.
7
+
8
+ Why only the pure functions? ``context_editing.apply_context_edits`` is
9
+ LangGraph-specific — it calls ``agent.get_state`` / ``agent.update_state``. The
10
+ gateway has no LangGraph agent; it has a plain list of messages off the wire.
11
+ So we import the lower-level building blocks (which operate on a
12
+ ``List[BaseMessage]``) and the gateway's own ``pipeline.py`` orchestrates them
13
+ in the same fixed order ``apply_context_edits`` uses.
14
+
15
+ If an import here ever fails, it's almost always because ``../backend`` moved or
16
+ a dependency in ``backend/requirements.txt`` isn't installed in this venv.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import sys
22
+ from pathlib import Path
23
+
24
+ # Two ways to find the engine, in priority order:
25
+ # 1. The **vendored** copy shipped inside the wheel (acm_engine/_vendor/) —
26
+ # this is what an end user who `pip install`ed the package gets. Refresh it
27
+ # from backend with `python scripts/sync_engine.py`.
28
+ # 2. The repo's ../backend tree — used during local dev so a checkout runs the
29
+ # live engine without a sync step.
30
+ # Whichever wins is put on sys.path so the engine's flat imports
31
+ # (`import context_profiles`, `from context_editing import ...`) resolve.
32
+ _VENDOR = Path(__file__).resolve().parent / "_vendor"
33
+ _BACKEND = Path(__file__).resolve().parents[2] / "backend"
34
+
35
+ if (_VENDOR / "context_profiles.py").is_file():
36
+ _ENGINE_DIR = _VENDOR
37
+ elif (_BACKEND / "context_profiles.py").is_file():
38
+ _ENGINE_DIR = _BACKEND
39
+ else: # pragma: no cover - defensive
40
+ raise ImportError(
41
+ "acm_engine found neither a vendored engine (acm_engine/_vendor/) nor "
42
+ f"the repo backend at {_BACKEND}. Run `python scripts/sync_engine.py`."
43
+ )
44
+
45
+ _engine_str = str(_ENGINE_DIR)
46
+ if _engine_str not in sys.path:
47
+ sys.path.insert(0, _engine_str)
48
+
49
+ # Re-export the schema (pure pydantic, zero app coupling).
50
+ from context_profiles import ( # noqa: E402
51
+ BUILTIN_PRESETS,
52
+ DEFAULT_PRESET_NAME,
53
+ PRESET_SUMMARY,
54
+ Profile,
55
+ parse_profile,
56
+ )
57
+
58
+ # Re-export the pure, message-list-level techniques. None of these touch the
59
+ # database, FastAPI, or a LangGraph agent — they take a list of messages and
60
+ # return rewrites.
61
+ from context_editing import ( # noqa: E402
62
+ _DEFAULT_SUMMARY_SYSTEM as DEFAULT_SUMMARY_SYSTEM,
63
+ evict_stale_images,
64
+ sliding_window_trim,
65
+ summarise_old_messages,
66
+ trim_tool_results,
67
+ )
68
+ from cache_layout import ( # noqa: E402
69
+ annotate_cache_breakpoints,
70
+ read_cache_tokens,
71
+ )
72
+
73
+ # Task-aware relevance pruning (suggest-only; judge engine ships now, encoder
74
+ # seam ready for Phase 2). Pure, message-list-level like the rest.
75
+ from relevance import ( # noqa: E402
76
+ Episode,
77
+ JudgeSuggester,
78
+ Suggestion,
79
+ active_task,
80
+ build_audit_rows,
81
+ load_audits,
82
+ load_feedback,
83
+ record_audit,
84
+ record_feedback,
85
+ segment_into_episodes,
86
+ suggest_removals,
87
+ )
88
+
89
+ # Local encoder engine (no LLM call). Heavy ML libs load lazily, so importing
90
+ # the class here stays cheap even on a minimal install.
91
+ from relevance_encoder import EncoderSuggester # noqa: E402
92
+
93
+ # Where the engine was actually loaded from (vendored copy or repo backend).
94
+ ENGINE_DIR = _ENGINE_DIR
95
+ BACKEND_DIR = _ENGINE_DIR # back-compat alias
96
+
97
+ __all__ = [
98
+ "ENGINE_DIR",
99
+ "BACKEND_DIR",
100
+ "BUILTIN_PRESETS",
101
+ "DEFAULT_PRESET_NAME",
102
+ "PRESET_SUMMARY",
103
+ "Profile",
104
+ "parse_profile",
105
+ "DEFAULT_SUMMARY_SYSTEM",
106
+ "trim_tool_results",
107
+ "summarise_old_messages",
108
+ "sliding_window_trim",
109
+ "evict_stale_images",
110
+ "annotate_cache_breakpoints",
111
+ "read_cache_tokens",
112
+ "Episode",
113
+ "Suggestion",
114
+ "JudgeSuggester",
115
+ "segment_into_episodes",
116
+ "active_task",
117
+ "suggest_removals",
118
+ "record_feedback",
119
+ "load_feedback",
120
+ "build_audit_rows",
121
+ "record_audit",
122
+ "load_audits",
123
+ "EncoderSuggester",
124
+ ]
@@ -0,0 +1,218 @@
1
+ """Provider-agnostic image-recall middleware (cache + eviction at call time).
2
+
3
+ This is the *common* solution for the image-recall techniques — it works for
4
+ every model the app can route to, because it leans on the one thing all
5
+ caching backends share: **a stable, append-only prefix**.
6
+
7
+ * OpenAI / Azure (and OpenAI models via OpenRouter): caching is automatic.
8
+ Keeping the prefix stable is all that's needed; the cache_control hints
9
+ we add are simply ignored.
10
+ * Anthropic / Bedrock-Claude / Gemini (incl. via OpenRouter): caching is
11
+ explicit. OpenRouter forwards the ``cache_control`` markers we put on
12
+ content blocks straight through to the upstream provider.
13
+
14
+ The middleware does both halves of "visual recall", gated by the profile's
15
+ ``image_recall.mode``:
16
+
17
+ * eviction (``evict`` / ``cache_evict``) — inside ``wrap_model_call`` we
18
+ drop the pixels of all but the most-recent K image tool-results from the
19
+ request the model sees, replacing them with their REFERENCES + a digest.
20
+ Doing it here (not just between turns) means it also fires inside a single
21
+ agent tool-loop — which is what the single-shot demo needs.
22
+ * caching (``cache`` / ``cache_evict``) — mark the stable prefix with an
23
+ ephemeral breakpoint so re-reading the frozen history is cheap/fast.
24
+ Caching never changes the model's output.
25
+
26
+ Gotchas baked in:
27
+ * Breakpoints go on a *text* block, never an image/tool block (Anthropic
28
+ rejects ``cache_control`` there — langchain issue #34920).
29
+ * String content is widened to a one-element list so we can hang a marker.
30
+ * Everything is defensive: an image-recall hiccup must never break a turn.
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import logging
36
+ from typing import Any, Dict, List, Optional
37
+
38
+ from langchain.agents.middleware import AgentMiddleware
39
+ from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage
40
+
41
+ log = logging.getLogger("image_recall")
42
+
43
+
44
+ # ─── cache-token reader (used by demo + chat to surface cache details) ───
45
+
46
+
47
+ def read_cache_tokens(msg: BaseMessage) -> Dict[str, int]:
48
+ """Pull cache hit/write counts off an AIMessage, provider-agnostic.
49
+
50
+ Returns ``{cache_read, cache_write}``. Anthropic/Gemini expose these via
51
+ ``usage_metadata.input_token_details`` (cache_read / cache_creation);
52
+ OpenAI exposes a ``cache_read`` only. Falls back to ``response_metadata``
53
+ when the normalised field is absent. All-zero when nothing cached.
54
+ """
55
+ out = {"cache_read": 0, "cache_write": 0}
56
+ um = getattr(msg, "usage_metadata", None) or {}
57
+ details = um.get("input_token_details") or {}
58
+ if isinstance(details, dict):
59
+ out["cache_read"] = int(details.get("cache_read") or 0)
60
+ out["cache_write"] = int(
61
+ details.get("cache_creation") or details.get("cache_write") or 0
62
+ )
63
+ if out["cache_read"] == 0 and out["cache_write"] == 0:
64
+ # OpenAI-style raw usage on response_metadata.
65
+ rm = getattr(msg, "response_metadata", None) or {}
66
+ usage = rm.get("usage") or rm.get("token_usage") or {}
67
+ ptd = (usage.get("prompt_tokens_details") or {}) if isinstance(usage, dict) else {}
68
+ if isinstance(ptd, dict):
69
+ out["cache_read"] = int(ptd.get("cached_tokens") or 0)
70
+ return out
71
+
72
+
73
+ # ─── cache breakpoint placement ──────────────────────────────────────────
74
+
75
+
76
+ def _last_stable_index(messages: List[BaseMessage]) -> Optional[int]:
77
+ """Index of the last message in the *settled* prefix (just before the
78
+ final HumanMessage). The newest turn stays dynamic / reprocessed fresh."""
79
+ if len(messages) < 2:
80
+ return None
81
+ last_human = None
82
+ for i in range(len(messages) - 1, -1, -1):
83
+ if isinstance(messages[i], HumanMessage):
84
+ last_human = i
85
+ break
86
+ boundary = (last_human - 1) if last_human is not None else (len(messages) - 1)
87
+ return boundary if boundary >= 0 else None
88
+
89
+
90
+ def _mark_block(msg: BaseMessage, ttl: str) -> bool:
91
+ """Attach an ephemeral cache_control marker to a *text* block of ``msg``.
92
+ Skips image / non-text blocks. Returns True if a marker was placed."""
93
+ cc: Dict[str, Any] = {"type": "ephemeral"}
94
+ if ttl == "1h":
95
+ cc["ttl"] = "1h"
96
+ content = getattr(msg, "content", None)
97
+ if isinstance(content, str):
98
+ if not content.strip():
99
+ return False
100
+ msg.content = [{"type": "text", "text": content, "cache_control": cc}]
101
+ return True
102
+ if isinstance(content, list):
103
+ for block in reversed(content):
104
+ if isinstance(block, dict) and block.get("type") == "text":
105
+ block["cache_control"] = cc
106
+ return True
107
+ return False
108
+
109
+
110
+ def annotate_cache_breakpoints(
111
+ messages: List[BaseMessage],
112
+ *,
113
+ system: Optional[BaseMessage] = None,
114
+ ttl: str = "5m",
115
+ ) -> int:
116
+ """Place ephemeral breakpoints on the stable prefix (system + last settled
117
+ message). Mutates in place; returns how many breakpoints were set."""
118
+ placed = 0
119
+ if isinstance(system, SystemMessage) and _mark_block(system, ttl):
120
+ placed += 1
121
+ idx = _last_stable_index(messages)
122
+ if idx is not None:
123
+ target = messages[idx]
124
+ if target.__class__.__name__ == "ToolMessage":
125
+ # Don't mark a bare tool block; walk back to an AI/System text block.
126
+ for j in range(idx, -1, -1):
127
+ if isinstance(messages[j], (AIMessage, SystemMessage)):
128
+ target = messages[j]
129
+ break
130
+ if _mark_block(target, ttl):
131
+ placed += 1
132
+ return placed
133
+
134
+
135
+ # ─── the middleware ──────────────────────────────────────────────────────
136
+
137
+
138
+ class ImageRecallMiddleware(AgentMiddleware):
139
+ """Applies the selected image-recall technique on every model call.
140
+
141
+ ``mode`` ∈ {off, cache, evict, cache_evict}. Provider-agnostic: caching is
142
+ a no-op effect for auto-caching backends and active for explicit ones.
143
+ Attach only when ``mode != off``.
144
+ """
145
+
146
+ def __init__(
147
+ self,
148
+ *,
149
+ mode: str = "off",
150
+ keep_recent_images: int = 3,
151
+ ttl: str = "5m",
152
+ ) -> None:
153
+ super().__init__()
154
+ self.mode = mode if mode in {"off", "cache", "evict", "cache_evict"} else "off"
155
+ self.keep_recent_images = max(1, int(keep_recent_images or 3))
156
+ self.ttl = ttl if ttl in {"5m", "1h"} else "5m"
157
+
158
+ @property
159
+ def caching_enabled(self) -> bool:
160
+ return self.mode in {"cache", "cache_evict"}
161
+
162
+ @property
163
+ def eviction_enabled(self) -> bool:
164
+ return self.mode in {"evict", "cache_evict"}
165
+
166
+ def _apply(self, request) -> None:
167
+ """Mutate request.messages in place: evict stale images, then mark
168
+ cache breakpoints. Order matters — eviction first so the breakpoint
169
+ lands on the de-imaged prefix."""
170
+ msgs = getattr(request, "messages", None)
171
+ if not msgs:
172
+ return
173
+ if self.eviction_enabled:
174
+ # Lazy import avoids a context_editing <-> cache_layout cycle.
175
+ from context_editing import evict_stale_images
176
+
177
+ replacements, info = evict_stale_images(
178
+ msgs, keep_recent_images=self.keep_recent_images
179
+ )
180
+ if replacements:
181
+ by_id = {getattr(r, "id", None): r for r in replacements}
182
+ request.messages = [
183
+ by_id.get(getattr(m, "id", None), m) for m in msgs
184
+ ]
185
+ msgs = request.messages
186
+ log.info(
187
+ "[image_recall] evicted %d stale image(s), kept %d as pixels, freed ~%d tokens",
188
+ info["evicted"], info["kept_recent"], info["freed_tokens"],
189
+ )
190
+ if self.caching_enabled:
191
+ placed = annotate_cache_breakpoints(
192
+ msgs,
193
+ system=getattr(request, "system_message", None),
194
+ ttl=self.ttl,
195
+ )
196
+ if placed:
197
+ log.info(
198
+ "[image_recall] placed %d cache breakpoint(s) (ttl=%s) on the stable prefix",
199
+ placed, self.ttl,
200
+ )
201
+
202
+ def wrap_model_call(self, request, handler): # type: ignore[override]
203
+ try:
204
+ self._apply(request)
205
+ except Exception as e: # never break the turn over image-recall
206
+ log.warning("[image_recall] apply failed: %r", e)
207
+ return handler(request)
208
+
209
+ async def awrap_model_call(self, request, handler): # type: ignore[override]
210
+ try:
211
+ self._apply(request)
212
+ except Exception as e:
213
+ log.warning("[image_recall] apply failed: %r", e)
214
+ return await handler(request)
215
+
216
+
217
+ # Back-compat alias (earlier name during development).
218
+ CachePrefixMiddleware = ImageRecallMiddleware