virtual-context 0.2.7__tar.gz → 0.2.8__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 (222) hide show
  1. {virtual_context-0.2.7 → virtual_context-0.2.8}/PKG-INFO +1 -1
  2. {virtual_context-0.2.7 → virtual_context-0.2.8}/pyproject.toml +1 -1
  3. virtual_context-0.2.8/tests/test_fill_pass.py +274 -0
  4. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/__init__.py +1 -1
  5. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/assembler.py +70 -33
  6. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/retriever.py +12 -5
  7. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/formats.py +77 -29
  8. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/message_filter.py +192 -0
  9. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/metrics.py +25 -1
  10. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/server.py +69 -4
  11. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/types.py +6 -0
  12. {virtual_context-0.2.7 → virtual_context-0.2.8}/.gitignore +0 -0
  13. {virtual_context-0.2.7 → virtual_context-0.2.8}/LICENSE +0 -0
  14. {virtual_context-0.2.7 → virtual_context-0.2.8}/README.md +0 -0
  15. {virtual_context-0.2.7 → virtual_context-0.2.8}/assets/dashboard.png +0 -0
  16. {virtual_context-0.2.7 → virtual_context-0.2.8}/assets/hero.png +0 -0
  17. {virtual_context-0.2.7 → virtual_context-0.2.8}/models.yaml +0 -0
  18. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/REGRESSION_MAP.md +0 -0
  19. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/conftest.py +0 -0
  20. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/docker-compose.test.yml +0 -0
  21. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/haiku/__init__.py +0 -0
  22. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/haiku/conftest.py +0 -0
  23. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/haiku/test_compaction.py +0 -0
  24. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/haiku/test_retrieval.py +0 -0
  25. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/haiku/test_tagging.py +0 -0
  26. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/ollama/__init__.py +0 -0
  27. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/ollama/conftest.py +0 -0
  28. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/ollama/test_compactor.py +0 -0
  29. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/ollama/test_pipeline.py +0 -0
  30. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/ollama/test_provider.py +0 -0
  31. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/ollama/test_tag_generator.py +0 -0
  32. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/proxy/__init__.py +0 -0
  33. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/proxy/test_dashboard_cors.py +0 -0
  34. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/proxy/test_metrics.py +0 -0
  35. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_assembler.py +0 -0
  36. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_backend_integration.py +0 -0
  37. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_budget_enforcement.py +0 -0
  38. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_cli_init.py +0 -0
  39. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_compaction_commit_prune.py +0 -0
  40. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_compactor.py +0 -0
  41. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_compactor_concurrent.py +0 -0
  42. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_composite_store.py +0 -0
  43. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_config.py +0 -0
  44. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_context_bleed.py +0 -0
  45. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_conversation_identity.py +0 -0
  46. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_conversation_lifecycle.py +0 -0
  47. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_conversation_scoping.py +0 -0
  48. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_embedding_tag_generator.py +0 -0
  49. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_empty_turn_skip.py +0 -0
  50. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_engine_integration.py +0 -0
  51. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_engine_lookback.py +0 -0
  52. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_engine_state.py +0 -0
  53. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_fact_enrichment.py +0 -0
  54. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_fact_graph_integration.py +0 -0
  55. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_fact_link_checker.py +0 -0
  56. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_fact_link_query.py +0 -0
  57. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_fact_link_types.py +0 -0
  58. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_fact_links_sqlite.py +0 -0
  59. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_find_quote.py +0 -0
  60. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_format_agnostic.py +0 -0
  61. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_headless.py +0 -0
  62. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_history_filter.py +0 -0
  63. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_idf_retrieval.py +0 -0
  64. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_ingest_index_integrity.py +0 -0
  65. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_longmemeval_auth.py +0 -0
  66. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_mcp_server.py +0 -0
  67. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_media.py +0 -0
  68. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_message_filter.py +0 -0
  69. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_metrics_persistence.py +0 -0
  70. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_model_catalog.py +0 -0
  71. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_model_limits.py +0 -0
  72. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_monitor.py +0 -0
  73. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_multi_instance.py +0 -0
  74. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_noop_fact_link_store.py +0 -0
  75. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_openrouter_provider.py +0 -0
  76. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_paging.py +0 -0
  77. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_passthrough_filter.py +0 -0
  78. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_presets.py +0 -0
  79. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_prev_context_leak.py +0 -0
  80. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_provider_adapters.py +0 -0
  81. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_proxy.py +0 -0
  82. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_proxy_dashboard.py +0 -0
  83. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_proxy_formats.py +0 -0
  84. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_proxy_message_filter.py +0 -0
  85. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_proxy_session.py +0 -0
  86. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_proxy_streaming.py +0 -0
  87. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_raw_content.py +0 -0
  88. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_recall_all.py +0 -0
  89. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_registry_lifecycle.py +0 -0
  90. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_request_captures_persistence.py +0 -0
  91. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_retriever.py +0 -0
  92. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_rrf_scoring.py +0 -0
  93. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_segmenter.py +0 -0
  94. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_semantic_search.py +0 -0
  95. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_sender_identity.py +0 -0
  96. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_session_cache.py +0 -0
  97. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_session_date.py +0 -0
  98. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_storage_protocols.py +0 -0
  99. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_store_sqlite.py +0 -0
  100. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_stub_turn_handling.py +0 -0
  101. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_supersession.py +0 -0
  102. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_supersession_migration.py +0 -0
  103. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_tag_canonicalizer.py +0 -0
  104. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_tag_consolidator.py +0 -0
  105. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_tag_generator.py +0 -0
  106. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_tag_splitter.py +0 -0
  107. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_telemetry.py +0 -0
  108. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_telemetry_integration.py +0 -0
  109. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_tool_loop.py +0 -0
  110. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_tool_output_interceptor.py +0 -0
  111. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_tool_result_filter.py +0 -0
  112. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_tool_tags.py +0 -0
  113. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_tui.py +0 -0
  114. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_turn_grouping.py +0 -0
  115. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_turn_tag_index.py +0 -0
  116. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_unified_budget.py +0 -0
  117. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_upstream_trim.py +0 -0
  118. {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_verb_expansion.py +0 -0
  119. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual-context.yaml.example +0 -0
  120. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/cli/__init__.py +0 -0
  121. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/cli/main.py +0 -0
  122. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/config.py +0 -0
  123. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/conversation_identity.py +0 -0
  124. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/__init__.py +0 -0
  125. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/compaction_pipeline.py +0 -0
  126. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/compactor.py +0 -0
  127. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/composite_store.py +0 -0
  128. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/conversation_store.py +0 -0
  129. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/embedding_provider.py +0 -0
  130. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/embedding_tag_generator.py +0 -0
  131. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/engine_utils.py +0 -0
  132. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/fact_query.py +0 -0
  133. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/fts_preprocessor.py +0 -0
  134. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/hint_builder.py +0 -0
  135. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/llm_utils.py +0 -0
  136. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/math_utils.py +0 -0
  137. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/model_catalog.py +0 -0
  138. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/monitor.py +0 -0
  139. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/paging_manager.py +0 -0
  140. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/protocols.py +0 -0
  141. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/provider_adapters.py +0 -0
  142. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/quote_search.py +0 -0
  143. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/retrieval_assembler.py +0 -0
  144. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/retrieval_scoring.py +0 -0
  145. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/search_engine.py +0 -0
  146. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/segmenter.py +0 -0
  147. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/semantic_search.py +0 -0
  148. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/store.py +0 -0
  149. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/tag_canonicalizer.py +0 -0
  150. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/tag_consolidator.py +0 -0
  151. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/tag_generator.py +0 -0
  152. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/tag_scoring.py +0 -0
  153. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/tag_splitter.py +0 -0
  154. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/tagging_pipeline.py +0 -0
  155. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/telemetry.py +0 -0
  156. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/temporal_resolver.py +0 -0
  157. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/tool_loop.py +0 -0
  158. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/tool_query.py +0 -0
  159. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/turn_tag_index.py +0 -0
  160. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/data/anthropic-tokenizer/tokenizer.json +0 -0
  161. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/engine.py +0 -0
  162. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/ingest/__init__.py +0 -0
  163. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/ingest/curator.py +0 -0
  164. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/ingest/date_resolver.py +0 -0
  165. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/ingest/parsers.py +0 -0
  166. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/ingest/supersession.py +0 -0
  167. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/mcp/__init__.py +0 -0
  168. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/mcp/server.py +0 -0
  169. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/model_limits.py +0 -0
  170. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/openclaw/virtual-context.mjs +0 -0
  171. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/patterns.py +0 -0
  172. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/presets/__init__.py +0 -0
  173. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/presets/agentic.py +0 -0
  174. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/presets/base.py +0 -0
  175. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/presets/coding.py +0 -0
  176. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/providers/__init__.py +0 -0
  177. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/providers/anthropic.py +0 -0
  178. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/providers/base.py +0 -0
  179. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/providers/generic_openai.py +0 -0
  180. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/providers/ollama_native.py +0 -0
  181. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/__init__.py +0 -0
  182. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/_envelope.py +0 -0
  183. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/dashboard.html +0 -0
  184. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/dashboard.py +0 -0
  185. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/handlers.py +0 -0
  186. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/helpers.py +0 -0
  187. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/media.py +0 -0
  188. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/multi.py +0 -0
  189. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/registry.py +0 -0
  190. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/session_cache.py +0 -0
  191. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/state.py +0 -0
  192. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/static/android-chrome-192x192.png +0 -0
  193. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/static/android-chrome-512x512.png +0 -0
  194. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/static/apple-touch-icon.png +0 -0
  195. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/static/favicon-16x16.png +0 -0
  196. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/static/favicon-32x32.png +0 -0
  197. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/static/favicon.ico +0 -0
  198. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/static/site.webmanifest +0 -0
  199. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/tool_output_interceptor.py +0 -0
  200. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/storage/__init__.py +0 -0
  201. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/storage/falkordb.py +0 -0
  202. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/storage/filesystem.py +0 -0
  203. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/storage/helpers.py +0 -0
  204. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/storage/neo4j.py +0 -0
  205. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/storage/noop_fact_link_store.py +0 -0
  206. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/storage/postgres.py +0 -0
  207. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/storage/sqlite.py +0 -0
  208. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/token_counter.py +0 -0
  209. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/tui/__init__.py +0 -0
  210. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/tui/app.py +0 -0
  211. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/tui/chat.tcss +0 -0
  212. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/tui/chat_provider.py +0 -0
  213. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/tui/headless.py +0 -0
  214. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/tui/modals/__init__.py +0 -0
  215. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/tui/modals/turn_inspector.py +0 -0
  216. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/tui/state.py +0 -0
  217. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/tui/widgets/__init__.py +0 -0
  218. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/tui/widgets/budget_bar.py +0 -0
  219. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/tui/widgets/chat_view.py +0 -0
  220. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/tui/widgets/input_box.py +0 -0
  221. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/tui/widgets/tag_panel.py +0 -0
  222. {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/tui/widgets/turn_list.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: virtual-context
3
- Version: 0.2.7
3
+ Version: 0.2.8
4
4
  Summary: OS-style virtual memory for LLM session context management
5
5
  Project-URL: Homepage, https://virtual-context.com
6
6
  Project-URL: Repository, https://github.com/virtual-context/virtual-context
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "virtual-context"
7
- version = "0.2.7"
7
+ version = "0.2.8"
8
8
  description = "OS-style virtual memory for LLM session context management"
9
9
  readme = "README.md"
10
10
  license = "AGPL-3.0-or-later"
@@ -0,0 +1,274 @@
1
+ """Tests for the fill pass pipeline stage."""
2
+
3
+ import copy
4
+ import json
5
+ import re
6
+ from unittest.mock import MagicMock
7
+ from datetime import datetime, timezone
8
+
9
+ from virtual_context.proxy.formats import detect_format
10
+ from virtual_context.types import (
11
+ RetrievalResult, StoredSummary, AssembledContext, TagSummary, SegmentMetadata,
12
+ )
13
+
14
+
15
+ def test_retrieval_result_has_overflow_field():
16
+ rr = RetrievalResult()
17
+ assert hasattr(rr, "overflow_summaries")
18
+ assert rr.overflow_summaries == []
19
+
20
+
21
+ def test_retrieval_result_overflow_accepts_summaries():
22
+ s = StoredSummary(ref="seg_001", primary_tag="cooking", summary="test", summary_tokens=50)
23
+ rr = RetrievalResult(overflow_summaries=[s])
24
+ assert len(rr.overflow_summaries) == 1
25
+ assert rr.overflow_summaries[0].ref == "seg_001"
26
+
27
+
28
+ def test_assembled_context_has_retrieval_result():
29
+ ac = AssembledContext()
30
+ assert ac.retrieval_result is None
31
+
32
+
33
+ def test_assembled_context_has_presented_tags():
34
+ ac = AssembledContext()
35
+ assert isinstance(ac.presented_tags, set)
36
+ assert len(ac.presented_tags) == 0
37
+
38
+
39
+ def test_assembled_context_presented_tags_populated():
40
+ ac = AssembledContext(presented_tags={"cooking", "baking", "recipes"})
41
+ assert "cooking" in ac.presented_tags
42
+ assert len(ac.presented_tags) == 3
43
+
44
+
45
+ def test_format_tag_section_standalone():
46
+ """Standalone format_tag_section produces the same XML format as the assembler."""
47
+ from virtual_context.core.assembler import format_tag_section
48
+ from virtual_context.types import StoredSummary, SegmentMetadata
49
+ from datetime import datetime, timezone
50
+
51
+ s1 = StoredSummary(
52
+ ref="s1", primary_tag="cooking", summary="Italian cooking techniques",
53
+ summary_tokens=100, tags=["cooking", "italian"],
54
+ metadata=SegmentMetadata(),
55
+ start_timestamp=datetime.now(timezone.utc),
56
+ )
57
+
58
+ result = format_tag_section("cooking", [s1])
59
+ assert '<virtual-context tags="cooking, italian"' in result
60
+ assert "[1/1]" in result
61
+ assert "Italian cooking techniques" in result
62
+ assert "</virtual-context>" in result
63
+
64
+
65
+ def _make_summary(ref: str, tag: str, tokens: int) -> StoredSummary:
66
+ return StoredSummary(
67
+ ref=ref, primary_tag=tag, tags=[tag],
68
+ summary=f"Summary for {tag}", summary_tokens=tokens,
69
+ metadata=SegmentMetadata(),
70
+ start_timestamp=datetime.now(timezone.utc),
71
+ )
72
+
73
+
74
+ def _make_anthropic_body(user_texts: list[str]) -> dict:
75
+ messages = []
76
+ for text in user_texts:
77
+ messages.append({"role": "user", "content": text})
78
+ messages.append({"role": "assistant", "content": [{"type": "text", "text": f"Reply to: {text}"}]})
79
+ messages.append({"role": "user", "content": "current question"})
80
+ return {"system": "You are helpful.", "messages": messages, "model": "claude-opus-4-6"}
81
+
82
+
83
+ def test_fill_pass_no_op_when_at_target():
84
+ from virtual_context.proxy.message_filter import fill_pass
85
+ body = _make_anthropic_body(["hello", "world"])
86
+ fmt = detect_format(body)
87
+ result, summaries, turns = fill_pass(
88
+ body=body, fmt=fmt, outbound_tokens=90000, target_tokens=90000,
89
+ assembled=None, pre_filter_body=body,
90
+ store=MagicMock(), conversation_id="test",
91
+ )
92
+ assert summaries == 0
93
+ assert turns == 0
94
+
95
+
96
+ def test_fill_pass_no_op_when_over_target():
97
+ from virtual_context.proxy.message_filter import fill_pass
98
+ body = _make_anthropic_body(["hello"])
99
+ fmt = detect_format(body)
100
+ result, summaries, turns = fill_pass(
101
+ body=body, fmt=fmt, outbound_tokens=100000, target_tokens=90000,
102
+ assembled=None, pre_filter_body=body,
103
+ store=MagicMock(), conversation_id="test",
104
+ )
105
+ assert summaries == 0
106
+ assert turns == 0
107
+
108
+
109
+ def test_fill_pass_adds_breadth_summaries():
110
+ from virtual_context.proxy.message_filter import fill_pass
111
+
112
+ body = _make_anthropic_body(["hello"])
113
+ fmt = detect_format(body)
114
+
115
+ ts1 = TagSummary(tag="cooking", summary="Italian cooking", summary_tokens=50,
116
+ source_segment_refs=["seg_a"], updated_at=datetime.now(timezone.utc))
117
+ ts2 = TagSummary(tag="baking", summary="Bread baking", summary_tokens=50,
118
+ source_segment_refs=["seg_b"], updated_at=datetime.now(timezone.utc))
119
+ mock_store = MagicMock()
120
+ mock_store.get_all_tag_summaries.return_value = [ts1, ts2]
121
+
122
+ assembled = AssembledContext(
123
+ presented_segment_refs=set(),
124
+ presented_tags=set(),
125
+ tag_sections={},
126
+ retrieval_result=RetrievalResult(),
127
+ )
128
+
129
+ result, summaries_added, turns_added = fill_pass(
130
+ body=body, fmt=fmt, outbound_tokens=70000, target_tokens=90000,
131
+ assembled=assembled, pre_filter_body=copy.deepcopy(body),
132
+ store=mock_store, conversation_id="test",
133
+ summary_ratio=1.0,
134
+ )
135
+ assert summaries_added >= 1
136
+ mock_store.get_all_tag_summaries.assert_called_once_with(conversation_id="test")
137
+
138
+
139
+ def test_presented_tags_from_segments_and_full_sections():
140
+ """presented_tags must include secondary tags from SEGMENTS/FULL sections."""
141
+ tag_sections = {
142
+ "cooking": (
143
+ '<virtual-context tags="cooking, italian" segments="2">\n'
144
+ "[1/2]\nPasta techniques\n\n---\n\n[2/2]\nRisotto methods\n"
145
+ "</virtual-context>"
146
+ ),
147
+ "baking": (
148
+ '<virtual-context tags="baking, bread, sourdough" segments="1">\n'
149
+ "[1/1]\nSourdough starter maintenance\n"
150
+ "</virtual-context>"
151
+ ),
152
+ }
153
+
154
+ _vc_tags_re = re.compile(r'<virtual-context\s+tags="([^"]*)"')
155
+ presented_tags: set[str] = set()
156
+ for section_text in tag_sections.values():
157
+ for m in _vc_tags_re.finditer(section_text):
158
+ for t in m.group(1).split(", "):
159
+ t = t.strip()
160
+ if t:
161
+ presented_tags.add(t)
162
+ # Also include section keys
163
+ presented_tags.update(tag_sections.keys())
164
+
165
+ assert "cooking" in presented_tags
166
+ assert "baking" in presented_tags
167
+ assert "italian" in presented_tags
168
+ assert "bread" in presented_tags
169
+ assert "sourdough" in presented_tags
170
+ assert "grilling" not in presented_tags
171
+
172
+
173
+ def test_presented_tags_fallback_to_section_key():
174
+ """When tags attr is empty, section key should still be covered."""
175
+ tag_sections = {
176
+ "misc": '<virtual-context tags="" segments="1">\n[1/1]\nRandom\n</virtual-context>',
177
+ }
178
+
179
+ _vc_tags_re = re.compile(r'<virtual-context\s+tags="([^"]*)"')
180
+ presented_tags: set[str] = set()
181
+ for section_text in tag_sections.values():
182
+ for m in _vc_tags_re.finditer(section_text):
183
+ for t in m.group(1).split(", "):
184
+ t = t.strip()
185
+ if t:
186
+ presented_tags.add(t)
187
+ presented_tags.update(tag_sections.keys())
188
+
189
+ # "misc" comes from keys(), not from regex (empty attr)
190
+ assert "misc" in presented_tags
191
+
192
+
193
+ def test_fill_pass_accounting_summary_and_turns():
194
+ """After fill pass, counts must match actual content added."""
195
+ from virtual_context.proxy.message_filter import fill_pass
196
+
197
+ body = _make_anthropic_body(["hello", "world"])
198
+ pre_filter = copy.deepcopy(body)
199
+ extra_msgs = [
200
+ {"role": "user", "content": "older message 1"},
201
+ {"role": "assistant", "content": [{"type": "text", "text": "older reply 1"}]},
202
+ {"role": "user", "content": "older message 2"},
203
+ {"role": "assistant", "content": [{"type": "text", "text": "older reply 2"}]},
204
+ ]
205
+ pre_filter["messages"] = extra_msgs + pre_filter["messages"]
206
+
207
+ fmt = detect_format(body)
208
+
209
+ ts1 = TagSummary(tag="history", summary="Historical events discussed",
210
+ summary_tokens=30, source_segment_refs=["seg_h1"],
211
+ updated_at=datetime.now(timezone.utc))
212
+ mock_store = MagicMock()
213
+ mock_store.get_all_tag_summaries.return_value = [ts1]
214
+
215
+ assembled = AssembledContext(
216
+ presented_segment_refs=set(),
217
+ presented_tags=set(),
218
+ tag_sections={},
219
+ retrieval_result=RetrievalResult(),
220
+ )
221
+
222
+ result_body, summaries_added, turns_added = fill_pass(
223
+ body=body, fmt=fmt,
224
+ outbound_tokens=70000, target_tokens=90000,
225
+ assembled=assembled, pre_filter_body=pre_filter,
226
+ store=mock_store, conversation_id="test-conv",
227
+ summary_ratio=0.5,
228
+ )
229
+
230
+ assert summaries_added >= 1
231
+ result_json = json.dumps(result_body)
232
+ assert "Historical events discussed" in result_json
233
+
234
+
235
+ def test_fill_pass_sanitizes_restored_turns():
236
+ """Restored turns must have thinking blocks stripped and media replaced."""
237
+ from virtual_context.proxy.message_filter import _sanitize_restored_turn
238
+
239
+ messages = [
240
+ {"role": "user", "content": [
241
+ {"type": "text", "text": "show me the image"},
242
+ {"type": "image", "source": {"type": "base64", "data": "abc123"}},
243
+ ]},
244
+ {"role": "assistant", "content": [
245
+ {"type": "thinking", "thinking": "let me think..."},
246
+ {"type": "text", "text": "here is my answer"},
247
+ ]},
248
+ ]
249
+
250
+ sanitized = _sanitize_restored_turn(messages)
251
+
252
+ user_content = sanitized[0]["content"]
253
+ assert any(b.get("text") == "[image removed from restored turn]" for b in user_content)
254
+ assert not any(b.get("type") == "image" for b in user_content)
255
+
256
+ asst_content = sanitized[1]["content"]
257
+ assert not any(b.get("type") == "thinking" for b in asst_content)
258
+ assert any(b.get("text") == "here is my answer" for b in asst_content)
259
+
260
+
261
+ def test_fill_pass_no_restore_tool_references():
262
+ """Media placeholders must NOT reference vc_restore_tool."""
263
+ from virtual_context.proxy.message_filter import _sanitize_restored_turn
264
+
265
+ messages = [
266
+ {"role": "user", "content": [
267
+ {"type": "input_image", "image_url": "data:image/png;base64,abc123"},
268
+ ]},
269
+ ]
270
+
271
+ sanitized = _sanitize_restored_turn(messages)
272
+ result_json = json.dumps(sanitized)
273
+ assert "vc_restore_tool" not in result_json
274
+ assert "image removed from restored turn" in result_json
@@ -14,7 +14,7 @@ from .types import (
14
14
  VirtualContextConfig,
15
15
  )
16
16
 
17
- __version__ = "0.2.7"
17
+ __version__ = "0.2.8"
18
18
 
19
19
  __all__ = [
20
20
  "VirtualContextEngine",
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import fnmatch
6
6
  import logging
7
+ import re
7
8
  from datetime import datetime, timezone
8
9
  from pathlib import Path
9
10
  from typing import Callable
@@ -24,6 +25,60 @@ from ..types import (
24
25
  )
25
26
 
26
27
 
28
+ def format_tag_section(
29
+ tag: str,
30
+ summaries: list["StoredSummary"],
31
+ store: "ContextStore | None" = None,
32
+ conversation_id: str = "",
33
+ ) -> str:
34
+ """Render a tag section in the canonical <virtual-context> format.
35
+
36
+ Shared by the assembler and the fill pass. Tool-hint enrichment is
37
+ optional (gracefully degrades if store is None).
38
+ """
39
+ if not summaries:
40
+ return ""
41
+
42
+ summaries = sorted(summaries, key=lambda s: s.start_timestamp)
43
+
44
+ all_tags = sorted({t for s in summaries for t in s.tags})
45
+ tags_attr = ", ".join(all_tags) if all_tags else tag
46
+
47
+ total = len(summaries)
48
+ summary_texts: list[str] = []
49
+ for idx, s in enumerate(summaries, 1):
50
+ prefix = f"[{idx}/{total}]"
51
+ session = s.metadata.session_date
52
+ if session:
53
+ prefix += f" [{session}]"
54
+ text = f"{prefix}\n{s.summary}"
55
+ tool_tags = [t for t in s.tags if t.startswith("tool_")]
56
+ if tool_tags:
57
+ text += f'\n[tool output truncated — vc_expand_topic("{tool_tags[0]}") for full result]'
58
+ # Optional tool hint enrichment
59
+ if store and conversation_id and s.ref:
60
+ get_refs = getattr(store, "get_tool_outputs_for_segment", None)
61
+ get_names = getattr(store, "get_tool_names_for_segment", None)
62
+ if callable(get_refs) and callable(get_names):
63
+ try:
64
+ refs = get_refs(conversation_id, s.ref)
65
+ if refs:
66
+ names = get_names(conversation_id, s.ref)
67
+ names_str = ", ".join(names) if names else "tools"
68
+ text += f"\n[Tools: {names_str} -- {len(refs)} outputs restorable via vc_restore_tool]"
69
+ except Exception:
70
+ pass
71
+ summary_texts.append(text)
72
+
73
+ body = "\n\n---\n\n".join(summary_texts)
74
+
75
+ return (
76
+ f'<virtual-context tags="{tags_attr}" segments="{len(summaries)}">\n'
77
+ f"{body}\n"
78
+ f"</virtual-context>"
79
+ )
80
+
81
+
27
82
  class ContextAssembler:
28
83
  """Assemble enriched context within token budget.
29
84
 
@@ -275,6 +330,18 @@ class ContextAssembler:
275
330
 
276
331
  total_tokens = core_tokens + tag_tokens + facts_tokens_actual + conv_tokens
277
332
 
333
+ # Compute presented_tags from rendered <virtual-context tags="..."> headers
334
+ _vc_tags_re = re.compile(r'<virtual-context\s+tags="([^"]*)"')
335
+ _presented_tags: set[str] = set()
336
+ for _section_text in tag_sections.values():
337
+ for _m in _vc_tags_re.finditer(_section_text):
338
+ for _t in _m.group(1).split(", "):
339
+ _t = _t.strip()
340
+ if _t:
341
+ _presented_tags.add(_t)
342
+ # Also include section keys as baseline for edge cases
343
+ _presented_tags.update(tag_sections.keys())
344
+
278
345
  return AssembledContext(
279
346
  core_context=core,
280
347
  tag_sections=tag_sections,
@@ -291,6 +358,8 @@ class ContextAssembler:
291
358
  prepend_text=prepend_text,
292
359
  presented_segment_refs=presented_refs,
293
360
  selected_facts=selected_facts,
361
+ retrieval_result=retrieval_result,
362
+ presented_tags=_presented_tags,
294
363
  )
295
364
 
296
365
  def _format_facts(self, facts: list[Fact], max_tokens: int) -> str:
@@ -313,39 +382,7 @@ class ContextAssembler:
313
382
  return "<facts>\n" + "\n".join(lines) + "\n</facts>"
314
383
 
315
384
  def _format_tag_section(self, tag: str, summaries: list[StoredSummary]) -> str:
316
- if not summaries:
317
- return ""
318
-
319
- # Sort chronologically so reader sees old → new progression
320
- summaries = sorted(summaries, key=lambda s: s.start_timestamp)
321
-
322
- all_tags = sorted({t for s in summaries for t in s.tags})
323
- tags_attr = ", ".join(all_tags) if all_tags else tag
324
-
325
- # Prefix each summary with sequence number and optional session date
326
- total = len(summaries)
327
- summary_texts: list[str] = []
328
- for idx, s in enumerate(summaries, 1):
329
- prefix = f"[{idx}/{total}]"
330
- session = s.metadata.session_date
331
- if session:
332
- prefix += f" [{session}]"
333
- text = f"{prefix}\n{s.summary}"
334
- # Expansion hint for tool segments
335
- tool_tags = [t for t in s.tags if t.startswith("tool_")]
336
- if tool_tags:
337
- text += f'\n[tool output truncated — vc_expand_topic("{tool_tags[0]}") for full result]'
338
- # Tool-enriched segment summary: append tool hint at assembly time
339
- text = self._maybe_append_tool_hint(text, s.ref)
340
- summary_texts.append(text)
341
-
342
- body = "\n\n---\n\n".join(summary_texts)
343
-
344
- return (
345
- f'<virtual-context tags="{tags_attr}" segments="{len(summaries)}">\n'
346
- f"{body}\n"
347
- f"</virtual-context>"
348
- )
385
+ return format_tag_section(tag, summaries, store=self._store, conversation_id=self._conversation_id)
349
386
 
350
387
  def _format_segments_section(self, tag: str, segments: list[StoredSegment]) -> str:
351
388
  if not segments:
@@ -120,6 +120,7 @@ class ContextRetriever:
120
120
  """
121
121
  start_time = time.monotonic()
122
122
  active_tags = set(current_active_tags or [])
123
+ overflow: list[StoredSummary] = []
123
124
 
124
125
  # Tag the inbound message — scope vocabulary to current conversation
125
126
  store_tags = [ts.tag for ts in self.store.get_all_tags(
@@ -207,6 +208,7 @@ class ContextRetriever:
207
208
  summaries=floor_summaries,
208
209
  total_tokens=floor_tokens,
209
210
  facts=self._fetch_all_facts(),
211
+ overflow_summaries=overflow,
210
212
  retrieval_metadata={
211
213
  "elapsed_ms": round(elapsed * 1000, 1),
212
214
  "tags_from_message": tag_result.tags,
@@ -226,6 +228,7 @@ class ContextRetriever:
226
228
  summaries=[],
227
229
  total_tokens=0,
228
230
  facts=self._fetch_all_facts(),
231
+ overflow_summaries=overflow,
229
232
  retrieval_metadata={
230
233
  "elapsed_ms": round(elapsed * 1000, 1),
231
234
  "tags_from_message": tag_result.tags,
@@ -293,15 +296,18 @@ class ContextRetriever:
293
296
  selected: list[StoredSummary] = []
294
297
  selected_refs: set[str] = set()
295
298
  total_tokens = 0
299
+ _overflow_cap = 50
296
300
  for summary in all_summaries:
297
301
  if summary.ref in selected_refs:
298
302
  continue
299
303
  if total_tokens + summary.summary_tokens > token_budget:
300
- logger.info(
301
- "Retriever: '%s' SKIP (budget exhausted: need %dt, have %dt remaining of %dt)",
302
- summary.primary_tag, summary.summary_tokens, token_budget - total_tokens, token_budget,
303
- )
304
- break
304
+ if len(overflow) < _overflow_cap:
305
+ overflow.append(summary)
306
+ logger.debug(
307
+ "Retriever: '%s' OVERFLOW (%dt, %d candidates)",
308
+ summary.primary_tag, summary.summary_tokens, len(overflow),
309
+ )
310
+ continue
305
311
  selected.append(summary)
306
312
  selected_refs.add(summary.ref)
307
313
  total_tokens += summary.summary_tokens
@@ -401,6 +407,7 @@ class ContextRetriever:
401
407
  full_detail=full_detail,
402
408
  total_tokens=total_tokens,
403
409
  facts=facts,
410
+ overflow_summaries=overflow,
404
411
  retrieval_metadata=retrieval_metadata,
405
412
  retrieval_scores=retrieval_scores,
406
413
  query_embedding=query_embedding,
@@ -818,13 +818,32 @@ class AnthropicFormat(PayloadFormat):
818
818
  return body
819
819
  body = copy.deepcopy(body)
820
820
  context_block = f"<system-reminder>\n{prepend_text}\n</system-reminder>"
821
- existing = body.get("system", "")
822
- if isinstance(existing, list):
823
- body["system"] = existing + [{"type": "text", "text": context_block}]
821
+ # Inject into the last user message that does NOT contain tool_result
822
+ # blocks. Prepending text before tool_result breaks Anthropic's
823
+ # "tool_result must immediately follow tool_use" constraint.
824
+ # This keeps the system prompt stable and cacheable.
825
+ messages = body.get("messages", [])
826
+ for i in range(len(messages) - 1, -1, -1):
827
+ msg = messages[i]
828
+ if msg.get("role") != "user":
829
+ continue
830
+ content = msg.get("content", "")
831
+ # Skip tool_result-bearing user messages
832
+ if isinstance(content, list) and any(
833
+ isinstance(b, dict) and b.get("type") == "tool_result"
834
+ for b in content
835
+ ):
836
+ continue
837
+ if isinstance(content, str):
838
+ messages[i] = dict(msg)
839
+ messages[i]["content"] = f"{context_block}\n\n{content}"
840
+ elif isinstance(content, list):
841
+ messages[i] = dict(msg)
842
+ messages[i]["content"] = [{"type": "text", "text": context_block}] + list(content)
843
+ break
824
844
  else:
825
- # Append VC block AFTER existing system prompt so the stable prefix
826
- # remains cacheable by Anthropic prompt caching.
827
- body["system"] = f"{existing}\n\n{context_block}" if existing else context_block
845
+ messages.append({"role": "user", "content": context_block})
846
+ body["messages"] = messages
828
847
  return body
829
848
 
830
849
  def extract_conversation_id(self, body: dict) -> str | None:
@@ -1185,17 +1204,23 @@ class OpenAIFormat(PayloadFormat):
1185
1204
  return body
1186
1205
  body = copy.deepcopy(body)
1187
1206
  context_block = f"<system-reminder>\n{prepend_text}\n</system-reminder>"
1207
+ # Inject into the LAST user message (current turn) so the system
1208
+ # message remains stable and cacheable by OpenAI prefix caching.
1188
1209
  messages = body.get("messages", [])
1189
- if messages and messages[0].get("role") == "system":
1190
- existing = messages[0].get("content", "")
1191
- messages[0] = dict(messages[0])
1192
- # Append VC block AFTER existing system prompt so the stable prefix
1193
- # remains cacheable by OpenAI prefix caching.
1194
- messages[0]["content"] = (
1195
- f"{existing}\n\n{context_block}" if existing else context_block
1196
- )
1210
+ for i in range(len(messages) - 1, -1, -1):
1211
+ msg = messages[i]
1212
+ if msg.get("role") != "user":
1213
+ continue
1214
+ content = msg.get("content", "")
1215
+ if isinstance(content, str):
1216
+ messages[i] = dict(msg)
1217
+ messages[i]["content"] = f"{context_block}\n\n{content}"
1218
+ elif isinstance(content, list):
1219
+ messages[i] = dict(msg)
1220
+ messages[i]["content"] = [{"type": "text", "text": context_block}] + list(content)
1221
+ break
1197
1222
  else:
1198
- messages.insert(0, {"role": "system", "content": context_block})
1223
+ messages.append({"role": "user", "content": context_block})
1199
1224
  body["messages"] = messages
1200
1225
  return body
1201
1226
 
@@ -1535,14 +1560,21 @@ class GeminiFormat(PayloadFormat):
1535
1560
  return body
1536
1561
  body = copy.deepcopy(body)
1537
1562
  context_block = f"<system-reminder>\n{prepend_text}\n</system-reminder>"
1538
-
1539
- # Gemini uses system_instruction.parts[] for system prompt
1540
- si = body.get("system_instruction", {})
1541
- existing_parts = si.get("parts", []) if isinstance(si, dict) else []
1542
- # Append VC block AFTER existing system instruction so the stable prefix
1543
- # remains cacheable by Gemini context caching.
1544
- new_parts = list(existing_parts) + [{"text": context_block}]
1545
- body["system_instruction"] = {"parts": new_parts}
1563
+ # Inject into the LAST user message (current turn) so the
1564
+ # system instruction remains stable and cacheable by Gemini context caching.
1565
+ contents = body.get("contents", [])
1566
+ for i in range(len(contents) - 1, -1, -1):
1567
+ msg = contents[i]
1568
+ if msg.get("role") != "user":
1569
+ continue
1570
+ parts = msg.get("parts", [])
1571
+ contents[i] = dict(msg)
1572
+ contents[i]["parts"] = [{"text": context_block}] + list(parts)
1573
+ break
1574
+ else:
1575
+ # No user message — prepend as user message
1576
+ contents.insert(0, {"role": "user", "parts": [{"text": context_block}]})
1577
+ body["contents"] = contents
1546
1578
  return body
1547
1579
 
1548
1580
  # -- Conversation markers --
@@ -2082,12 +2114,28 @@ class OpenAIResponsesFormat(PayloadFormat):
2082
2114
  return body
2083
2115
  body = copy.deepcopy(body)
2084
2116
  context_block = f"<system-reminder>\n{prepend_text}\n</system-reminder>"
2085
- existing = body.get("instructions", "")
2086
- # Append VC block AFTER existing instructions so the stable prefix
2087
- # remains cacheable by OpenAI prefix caching.
2088
- body["instructions"] = (
2089
- f"{existing}\n\n{context_block}" if existing else context_block
2090
- )
2117
+ # Inject into the LAST user item in input (current turn) so the
2118
+ # instructions remain stable and cacheable by OpenAI prefix caching.
2119
+ items = body.get("input", [])
2120
+ if isinstance(items, list):
2121
+ for i in range(len(items) - 1, -1, -1):
2122
+ item = items[i]
2123
+ if not isinstance(item, dict) or item.get("role") != "user":
2124
+ continue
2125
+ content = item.get("content", "")
2126
+ if isinstance(content, str):
2127
+ items[i] = dict(item)
2128
+ items[i]["content"] = f"{context_block}\n\n{content}"
2129
+ elif isinstance(content, list):
2130
+ items[i] = dict(item)
2131
+ items[i]["content"] = [{"type": "input_text", "text": context_block}] + list(content)
2132
+ break
2133
+ else:
2134
+ items.append({"role": "user", "content": context_block})
2135
+ body["input"] = items
2136
+ elif isinstance(items, str):
2137
+ # input is a plain string — prepend context
2138
+ body["input"] = f"{context_block}\n\n{items}"
2091
2139
  return body
2092
2140
 
2093
2141
  # -- Conversation markers --