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.
- {virtual_context-0.2.7 → virtual_context-0.2.8}/PKG-INFO +1 -1
- {virtual_context-0.2.7 → virtual_context-0.2.8}/pyproject.toml +1 -1
- virtual_context-0.2.8/tests/test_fill_pass.py +274 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/__init__.py +1 -1
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/assembler.py +70 -33
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/retriever.py +12 -5
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/formats.py +77 -29
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/message_filter.py +192 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/metrics.py +25 -1
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/server.py +69 -4
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/types.py +6 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/.gitignore +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/LICENSE +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/README.md +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/assets/dashboard.png +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/assets/hero.png +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/models.yaml +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/REGRESSION_MAP.md +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/conftest.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/docker-compose.test.yml +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/haiku/__init__.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/haiku/conftest.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/haiku/test_compaction.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/haiku/test_retrieval.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/haiku/test_tagging.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/ollama/__init__.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/ollama/conftest.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/ollama/test_compactor.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/ollama/test_pipeline.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/ollama/test_provider.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/ollama/test_tag_generator.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/proxy/__init__.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/proxy/test_dashboard_cors.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/proxy/test_metrics.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_assembler.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_backend_integration.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_budget_enforcement.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_cli_init.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_compaction_commit_prune.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_compactor.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_compactor_concurrent.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_composite_store.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_config.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_context_bleed.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_conversation_identity.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_conversation_lifecycle.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_conversation_scoping.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_embedding_tag_generator.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_empty_turn_skip.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_engine_integration.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_engine_lookback.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_engine_state.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_fact_enrichment.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_fact_graph_integration.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_fact_link_checker.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_fact_link_query.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_fact_link_types.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_fact_links_sqlite.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_find_quote.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_format_agnostic.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_headless.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_history_filter.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_idf_retrieval.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_ingest_index_integrity.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_longmemeval_auth.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_mcp_server.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_media.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_message_filter.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_metrics_persistence.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_model_catalog.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_model_limits.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_monitor.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_multi_instance.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_noop_fact_link_store.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_openrouter_provider.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_paging.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_passthrough_filter.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_presets.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_prev_context_leak.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_provider_adapters.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_proxy.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_proxy_dashboard.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_proxy_formats.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_proxy_message_filter.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_proxy_session.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_proxy_streaming.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_raw_content.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_recall_all.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_registry_lifecycle.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_request_captures_persistence.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_retriever.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_rrf_scoring.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_segmenter.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_semantic_search.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_sender_identity.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_session_cache.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_session_date.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_storage_protocols.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_store_sqlite.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_stub_turn_handling.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_supersession.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_supersession_migration.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_tag_canonicalizer.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_tag_consolidator.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_tag_generator.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_tag_splitter.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_telemetry.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_telemetry_integration.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_tool_loop.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_tool_output_interceptor.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_tool_result_filter.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_tool_tags.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_tui.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_turn_grouping.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_turn_tag_index.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_unified_budget.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_upstream_trim.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/tests/test_verb_expansion.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual-context.yaml.example +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/cli/__init__.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/cli/main.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/config.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/conversation_identity.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/__init__.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/compaction_pipeline.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/compactor.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/composite_store.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/conversation_store.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/embedding_provider.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/embedding_tag_generator.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/engine_utils.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/fact_query.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/fts_preprocessor.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/hint_builder.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/llm_utils.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/math_utils.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/model_catalog.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/monitor.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/paging_manager.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/protocols.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/provider_adapters.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/quote_search.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/retrieval_assembler.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/retrieval_scoring.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/search_engine.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/segmenter.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/semantic_search.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/store.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/tag_canonicalizer.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/tag_consolidator.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/tag_generator.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/tag_scoring.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/tag_splitter.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/tagging_pipeline.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/telemetry.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/temporal_resolver.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/tool_loop.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/tool_query.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/core/turn_tag_index.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/data/anthropic-tokenizer/tokenizer.json +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/engine.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/ingest/__init__.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/ingest/curator.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/ingest/date_resolver.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/ingest/parsers.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/ingest/supersession.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/mcp/__init__.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/mcp/server.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/model_limits.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/openclaw/virtual-context.mjs +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/patterns.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/presets/__init__.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/presets/agentic.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/presets/base.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/presets/coding.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/providers/__init__.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/providers/anthropic.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/providers/base.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/providers/generic_openai.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/providers/ollama_native.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/__init__.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/_envelope.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/dashboard.html +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/dashboard.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/handlers.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/helpers.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/media.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/multi.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/registry.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/session_cache.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/state.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/static/android-chrome-192x192.png +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/static/android-chrome-512x512.png +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/static/apple-touch-icon.png +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/static/favicon-16x16.png +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/static/favicon-32x32.png +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/static/favicon.ico +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/static/site.webmanifest +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/proxy/tool_output_interceptor.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/storage/__init__.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/storage/falkordb.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/storage/filesystem.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/storage/helpers.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/storage/neo4j.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/storage/noop_fact_link_store.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/storage/postgres.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/storage/sqlite.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/token_counter.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/tui/__init__.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/tui/app.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/tui/chat.tcss +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/tui/chat_provider.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/tui/headless.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/tui/modals/__init__.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/tui/modals/turn_inspector.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/tui/state.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/tui/widgets/__init__.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/tui/widgets/budget_bar.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/tui/widgets/chat_view.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/tui/widgets/input_box.py +0 -0
- {virtual_context-0.2.7 → virtual_context-0.2.8}/virtual_context/tui/widgets/tag_panel.py +0 -0
- {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.
|
|
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
|
|
@@ -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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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
|
-
|
|
826
|
-
|
|
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
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
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.
|
|
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
|
-
#
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
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
|
-
|
|
2086
|
-
#
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
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 --
|