fix(agent-runner): package context for plugin execution

This commit is contained in:
huanghuoguoguo
2026-05-21 13:56:17 +08:00
parent 26923c66c0
commit 094b87e578
10 changed files with 393 additions and 47 deletions

View File

@@ -12,6 +12,7 @@ from .errors import (
)
from .registry import AgentRunnerRegistry
from .context_builder import AgentRunContextBuilder
from .context_packager import AgentContextPackager
from .resource_builder import AgentResourceBuilder
from .result_normalizer import AgentResultNormalizer
from .orchestrator import AgentRunOrchestrator
@@ -37,6 +38,7 @@ __all__ = [
'RunnerExecutionError',
'AgentRunnerRegistry',
'AgentRunContextBuilder',
'AgentContextPackager',
'AgentResourceBuilder',
'AgentResultNormalizer',
'AgentRunOrchestrator',

View File

@@ -1,4 +1,4 @@
"""Agent run context builder for converting Query to AgentRunContext."""
"""Agent run context builder for provisioning AgentRunContext envelopes."""
from __future__ import annotations
import uuid
@@ -11,6 +11,7 @@ from langbot_plugin.api.entities.builtin.platform import message as platform_mes
from ...core import app
from .descriptor import AgentRunnerDescriptor
from .config_migration import ConfigMigration
from .context_packager import AgentContextPackager
from .state_store import get_state_store
from . import events as runner_events
@@ -136,13 +137,13 @@ class AgentRunContextPayload(typing.TypedDict):
class AgentRunContextBuilder:
"""Builder for converting Query to AgentRunContext.
"""Builder for provisioning AgentRunContext from a Pipeline Query.
Responsibilities:
- Generate new run_id (UUID, not query id)
- Set trigger type to 'message.received' for pipeline
- Build conversation context from session
- Convert messages to SDK format
- Package and convert messages to SDK format
- Build input from user_message and message_chain
- Build params from query.variables with filtering
- Build state snapshot from state_store
@@ -165,6 +166,7 @@ class AgentRunContextBuilder:
def __init__(self, ap: app.Application):
self.ap = ap
self.context_packager = AgentContextPackager()
async def build_context(
self,
@@ -172,7 +174,7 @@ class AgentRunContextBuilder:
descriptor: AgentRunnerDescriptor,
resources: AgentResources,
) -> AgentRunContextPayload:
"""Build AgentRunContext from Query.
"""Build AgentRunContext envelope from Query.
Args:
query: Pipeline query
@@ -205,19 +207,6 @@ class AgentRunContextBuilder:
'pipeline_uuid': query.pipeline_uuid,
}
# Build input
input: AgentInput = self._build_input(query)
# Build messages
messages = self._build_messages(query)
# Build params from query.variables with filtering
params = self._build_params(query)
# Build state snapshot from state_store
state_store = get_state_store()
state: AgentRunState = state_store.build_snapshot(query, descriptor)
# Get runner binding config from ai.runner_config[runner_id]
# This is Pipeline's configuration for this specific runner binding,
# passed through AgentRunContext.config to the runner
@@ -226,6 +215,20 @@ class AgentRunContextBuilder:
descriptor.id,
)
# Build input
input: AgentInput = self._build_input(query)
# Build bounded working context window for the runner.
packaged_context = self.context_packager.package_messages(query, runner_config)
messages = self._build_messages(packaged_context.messages)
# Build params from query.variables with filtering
params = self._build_params(query)
# Build state snapshot from state_store
state_store = get_state_store()
state: AgentRunState = state_store.build_snapshot(query, descriptor)
streaming_supported = await self._is_stream_output_supported(query)
remove_think = query.pipeline_config.get('output', {}).get('misc', {}).get('remove-think', False)
@@ -241,6 +244,10 @@ class AgentRunContextBuilder:
'pipeline_name': query.variables.get('_monitoring_pipeline_name', 'Unknown'),
'streaming_supported': streaming_supported,
'remove_think': remove_think,
'context_packaging': {
'policy': packaged_context.policy,
'history': packaged_context.history,
},
},
}
@@ -526,13 +533,12 @@ class AgentRunContextBuilder:
return prompt_messages
def _build_messages(self, query: pipeline_query.Query) -> list[dict[str, typing.Any]]:
"""Build messages list from query."""
def _build_messages(self, source_messages: list[typing.Any]) -> list[dict[str, typing.Any]]:
"""Build messages list from packaged source messages."""
messages: list[dict[str, typing.Any]] = []
if query.messages:
for msg in query.messages:
messages.append(msg.model_dump(mode='json'))
for msg in source_messages:
messages.append(msg.model_dump(mode='json'))
return messages

View File

@@ -0,0 +1,79 @@
"""Agent context packaging helpers."""
from __future__ import annotations
import dataclasses
import typing
from langbot_plugin.api.entities.builtin.pipeline import query as pipeline_query
DEFAULT_LEGACY_MAX_ROUND = 10
@dataclasses.dataclass(frozen=True)
class ContextPackagingResult:
"""Packaged working context for one AgentRunner run."""
messages: list[typing.Any]
policy: dict[str, typing.Any]
history: dict[str, typing.Any]
def get_legacy_max_round(runner_config: dict[str, typing.Any]) -> typing.Any:
"""Return the configured legacy max-round value.
Keep the existing config semantics intact: callers are expected to pass the
already-resolved runner binding config, and invalid values fail the same way
the old truncator failed when comparing them with an integer round count.
"""
return runner_config.get('max-round', DEFAULT_LEGACY_MAX_ROUND)
def select_legacy_max_round_messages(
messages: list[typing.Any] | None,
max_round: typing.Any,
) -> list[typing.Any]:
"""Select the same message window as the legacy round truncator."""
if not messages:
return []
temp_messages: list[typing.Any] = []
current_round = 0
for msg in messages[::-1]:
if current_round < max_round:
temp_messages.append(msg)
if getattr(msg, 'role', None) == 'user':
current_round += 1
else:
break
return temp_messages[::-1]
class AgentContextPackager:
"""Build the bounded working context for AgentRunner execution."""
def package_messages(
self,
query: pipeline_query.Query,
runner_config: dict[str, typing.Any],
) -> ContextPackagingResult:
"""Package query messages using the current legacy max-round policy."""
source_messages = query.messages or []
max_round = get_legacy_max_round(runner_config)
packaged_messages = select_legacy_max_round_messages(source_messages, max_round)
return ContextPackagingResult(
messages=packaged_messages,
policy={
'mode': 'legacy_max_round',
'max_round': max_round,
},
history={
'source': 'query.messages',
'source_total_count': len(source_messages),
'delivered_count': len(packaged_messages),
'messages_complete': len(packaged_messages) == len(source_messages),
},
)

View File

@@ -31,7 +31,7 @@ class AgentRunOrchestrator:
Responsibilities:
- Resolve runner ID from pipeline config (new or old format)
- Get runner descriptor from registry
- Build AgentRunContext from Query
- Provision AgentRunContext envelope from Query
- Build AgentResources with permission filtering
- Invoke plugin runtime RUN_AGENT action
- Normalize AgentRunResult to Pipeline messages

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
from .. import stage, entities
from . import truncator
from ...utils import importutil
from ...agent.runner.config_migration import ConfigMigration
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
from . import truncators
@@ -30,6 +31,9 @@ class ConversationMessageTruncator(stage.PipelineStage):
async def process(self, query: pipeline_query.Query, stage_inst_name: str) -> entities.StageProcessResult:
"""处理"""
if ConfigMigration.resolve_runner_id(query.pipeline_config):
return entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query)
query = await self.trun.truncate(query)
return entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query)

View File

@@ -3,6 +3,10 @@ from __future__ import annotations
from .. import truncator
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
from ....agent.runner.config_migration import ConfigMigration
from ....agent.runner.context_packager import (
get_legacy_max_round,
select_legacy_max_round_messages,
)
@truncator.truncator_class('round')
@@ -11,25 +15,15 @@ class RoundTruncator(truncator.Truncator):
async def truncate(self, query: pipeline_query.Query) -> pipeline_query.Query:
"""截断"""
# max-round remains a pipeline-side trimming knob until token-budget
# based compaction replaces this stage.
runner_id = ConfigMigration.resolve_runner_id(query.pipeline_config)
runner_config = ConfigMigration.resolve_runner_config(query.pipeline_config, runner_id) if runner_id else {}
max_round = runner_config.get('max-round', 10)
if runner_id:
runner_config = ConfigMigration.resolve_runner_config(query.pipeline_config, runner_id)
else:
runner_config = query.pipeline_config.get('msg-truncate', {}).get('round', {})
temp_messages = []
current_round = 0
# Traverse from back to front
for msg in query.messages[::-1]:
if current_round < max_round:
temp_messages.append(msg)
if msg.role == 'user':
current_round += 1
else:
break
query.messages = temp_messages[::-1]
query.messages = select_legacy_max_round_messages(
query.messages,
get_legacy_max_round(runner_config),
)
return query