From c62f79f9d7e8a4df7af5199b07e3546f976fea89 Mon Sep 17 00:00:00 2001 From: RockChinQ Date: Wed, 4 Nov 2026 01:13:39 +0800 Subject: [PATCH] fix(agent): pass execution context when resolving model context window (#2858) RunnerContextBuilder called ModelManager.get_model_by_uuid(model_uuid) without the required tenant context. The TypeError was swallowed at debug level, so runtime.metadata.model_context_window_tokens was always missing. Thread the run's ExecutionContext through build_context_from_event. Co-authored-by: Claude Opus 5.5 Co-authored-by: multica-agent --- .../pkg/agent/runner/context_builder.py | 11 +++++++--- src/langbot/pkg/agent/runner/orchestrator.py | 1 + .../agent/test_context_validation.py | 21 ++++++++++++++++--- .../agent/test_legacy_identity_boundary.py | 4 ++-- 4 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/langbot/pkg/agent/runner/context_builder.py b/src/langbot/pkg/agent/runner/context_builder.py index 4637ba221..8489bb4c0 100644 --- a/src/langbot/pkg/agent/runner/context_builder.py +++ b/src/langbot/pkg/agent/runner/context_builder.py @@ -6,6 +6,7 @@ import uuid import time import typing +from ...api.http.context import ExecutionContext from ...core import app from .descriptor import RunnerDescriptor from .persistent_state_store import get_persistent_state_store @@ -191,7 +192,9 @@ class RunnerContextBuilder: return bool({'invoke', 'stream', 'count_tokens'} & {str(operation) for operation in operations}) return model_resource.get('model_type') != 'rerank' - async def _build_model_context_window_tokens(self, resources: AgentResources) -> int | None: + async def _build_model_context_window_tokens( + self, execution_context: ExecutionContext, resources: AgentResources + ) -> int | None: model_mgr = getattr(self.ap, 'model_mgr', None) if model_mgr is None: return None @@ -205,7 +208,7 @@ class RunnerContextBuilder: continue try: - model = await model_mgr.get_model_by_uuid(model_uuid) + model = await model_mgr.get_model_by_uuid(execution_context, model_uuid) except Exception as exc: logger = getattr(self.ap, 'logger', None) if logger is not None: @@ -220,6 +223,7 @@ class RunnerContextBuilder: async def build_context_from_event( self, + execution_context: ExecutionContext, event: AgentEventEnvelope, binding: AgentBinding, descriptor: RunnerDescriptor, @@ -231,6 +235,7 @@ class RunnerContextBuilder: Does NOT inline full history by default. Args: + execution_context: Trusted tenant context used to resolve models event: Event envelope binding: Agent binding descriptor: Runner descriptor @@ -360,7 +365,7 @@ class RunnerContextBuilder: persistent_state_store = get_persistent_state_store(self.ap.persistence_mgr.get_db_engine()) state: AgentRunState = await persistent_state_store.build_snapshot_from_event(event, binding, descriptor) - model_context_window_tokens = await self._build_model_context_window_tokens(resources) + model_context_window_tokens = await self._build_model_context_window_tokens(execution_context, resources) # Build runtime context runtime: AgentRuntimeContext = { diff --git a/src/langbot/pkg/agent/runner/orchestrator.py b/src/langbot/pkg/agent/runner/orchestrator.py index 6889de784..2b2aacff6 100644 --- a/src/langbot/pkg/agent/runner/orchestrator.py +++ b/src/langbot/pkg/agent/runner/orchestrator.py @@ -171,6 +171,7 @@ class AgentRunOrchestrator: pass context = await self.context_builder.build_context_from_event( + execution_context=execution_context, event=execution_event, binding=binding, descriptor=descriptor, diff --git a/tests/unit_tests/agent/test_context_validation.py b/tests/unit_tests/agent/test_context_validation.py index 438a85eed..14812581b 100644 --- a/tests/unit_tests/agent/test_context_validation.py +++ b/tests/unit_tests/agent/test_context_validation.py @@ -22,9 +22,17 @@ from langbot.pkg.agent.runner.context_builder import ( ) from langbot.pkg.agent.runner.descriptor import RunnerDescriptor from langbot.pkg.agent.runner.host_models import AgentEventEnvelope, AgentBinding, BindingScope +from langbot.pkg.api.http.context import ExecutionContext from langbot.pkg.core import app +EXECUTION_CONTEXT = ExecutionContext( + instance_uuid='test-instance', + workspace_uuid='test-workspace', + placement_generation=1, +) + + class TestContextValidation: """Test that context builder output validates against SDK RunnerContext.""" @@ -131,6 +139,7 @@ class TestContextValidation: # Build context context_dict = await builder.build_context_from_event( + execution_context=EXECUTION_CONTEXT, event=event, binding=binding, descriptor=descriptor, @@ -199,6 +208,7 @@ class TestContextValidation: ) mock_get_store.return_value = mock_store context_dict = await builder.build_context_from_event( + execution_context=EXECUTION_CONTEXT, event=event, binding=self._make_binding(), descriptor=self._make_descriptor(), @@ -253,6 +263,7 @@ class TestContextValidation: mock_get_store.return_value = mock_store context_dict = await builder.build_context_from_event( + execution_context=EXECUTION_CONTEXT, event=event, binding=binding, descriptor=descriptor, @@ -260,7 +271,7 @@ class TestContextValidation: ) assert context_dict['runtime']['metadata']['model_context_window_tokens'] == 128000 - mock_app.model_mgr.get_model_by_uuid.assert_awaited_once_with('llm-model') + mock_app.model_mgr.get_model_by_uuid.assert_awaited_once_with(EXECUTION_CONTEXT, 'llm-model') @pytest.mark.asyncio async def test_model_context_window_uses_primary_llm_only(self): @@ -289,8 +300,8 @@ class TestContextValidation: }, ] - assert await builder._build_model_context_window_tokens(resources) is None - mock_app.model_mgr.get_model_by_uuid.assert_awaited_once_with('primary-model') + assert await builder._build_model_context_window_tokens(EXECUTION_CONTEXT, resources) is None + mock_app.model_mgr.get_model_by_uuid.assert_awaited_once_with(EXECUTION_CONTEXT, 'primary-model') @pytest.mark.asyncio async def test_build_context_preserves_subject_data_for_non_message_events(self): @@ -338,6 +349,7 @@ class TestContextValidation: mock_get_store.return_value = mock_store context_dict = await builder.build_context_from_event( + execution_context=EXECUTION_CONTEXT, event=event, binding=binding, descriptor=descriptor, @@ -378,6 +390,7 @@ class TestContextValidation: mock_get_store.return_value = mock_store context_dict = await builder.build_context_from_event( + execution_context=EXECUTION_CONTEXT, event=event, binding=binding, descriptor=descriptor, @@ -421,6 +434,7 @@ class TestContextValidation: mock_get_store.return_value = mock_store context_dict = await builder.build_context_from_event( + execution_context=EXECUTION_CONTEXT, event=event, binding=binding, descriptor=descriptor, @@ -459,6 +473,7 @@ class TestContextValidation: mock_get_store.return_value = mock_store context_dict = await builder.build_context_from_event( + execution_context=EXECUTION_CONTEXT, event=event, binding=binding, descriptor=descriptor, diff --git a/tests/unit_tests/agent/test_legacy_identity_boundary.py b/tests/unit_tests/agent/test_legacy_identity_boundary.py index ea8997acb..53c369829 100644 --- a/tests/unit_tests/agent/test_legacy_identity_boundary.py +++ b/tests/unit_tests/agent/test_legacy_identity_boundary.py @@ -14,7 +14,7 @@ from langbot.pkg.agent.runner.query_entry_adapter import QueryEntryAdapter from langbot.pkg.agent.runner.context_builder import RunnerContextBuilder from langbot.pkg.agent.runner.host_models import AgentEventEnvelope from tests.unit_tests.agent import test_event_first_protocol as event_fixtures -from tests.unit_tests.agent.test_context_validation import TestContextValidation as Helpers +from tests.unit_tests.agent.test_context_validation import EXECUTION_CONTEXT, TestContextValidation as Helpers from tests.utils.legacy_plugin_contract import load_identity_helper, verified_plugin_root @@ -35,7 +35,7 @@ async def build(event, runner, source='legacy-session'): with patch('langbot.pkg.agent.runner.context_builder.get_persistent_state_store') as store: store.return_value.build_snapshot_from_event = AsyncMock(return_value={}) context = await builder.build_context_from_event( - event, binding, helper._make_descriptor(), helper._make_resources() + EXECUTION_CONTEXT, event, binding, helper._make_descriptor(), helper._make_resources() ) return context, binding