mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-22 08:25:21 +08:00
feat: add verified manual pipeline migration for plugin runners
This commit is contained in:
@@ -249,6 +249,20 @@ class RunnerContextBuilder:
|
||||
'timestamp': event.event_time or int(time.time()),
|
||||
}
|
||||
|
||||
# A legacy Tbox resume must use the persisted original bot, not a
|
||||
# newly reconstructed callback envelope. Ordinary sender mode is unchanged.
|
||||
bot_id = event.bot_id
|
||||
if event.event_type == 'interaction.submitted' and binding.runner_config.get('user-id-source') == 'legacy-bot':
|
||||
identity = event.legacy_identity
|
||||
bot_id = (
|
||||
identity.bot_id
|
||||
if identity is not None
|
||||
and identity.workspace_id == event.workspace_id
|
||||
and identity.bot_id == event.bot_id
|
||||
and identity.pipeline_id == (binding.processor_id or binding.agent_id)
|
||||
else None
|
||||
)
|
||||
|
||||
# Build conversation context from event
|
||||
conversation: ConversationContext | None = None
|
||||
if event.conversation_id:
|
||||
@@ -256,13 +270,39 @@ class RunnerContextBuilder:
|
||||
'session_id': None,
|
||||
'conversation_id': event.conversation_id,
|
||||
'thread_id': event.thread_id,
|
||||
'launcher_type': None, # Will be filled from actor/subject if needed
|
||||
'launcher_type': None,
|
||||
'launcher_id': None,
|
||||
'sender_id': event.actor.actor_id if event.actor else None,
|
||||
'bot_id': event.bot_id,
|
||||
'bot_id': bot_id,
|
||||
'workspace_id': event.workspace_id,
|
||||
}
|
||||
|
||||
identity = event.legacy_identity
|
||||
if (
|
||||
binding.runner_config.get('user-id-source') == 'legacy-session'
|
||||
and identity is not None
|
||||
and identity.workspace_id == event.workspace_id
|
||||
and identity.bot_id == event.bot_id
|
||||
and identity.pipeline_id == (binding.processor_id or binding.agent_id)
|
||||
and binding.processor_type == 'pipeline'
|
||||
):
|
||||
# Coze used Query itself; Dify/n8n used Query.session, which may
|
||||
# deliberately differ (e.g. group per-member session isolation).
|
||||
if binding.runner_id == 'plugin:langbot-team/CozeAgent/default':
|
||||
launcher_type = identity.query_launcher_type
|
||||
launcher_id = identity.query_launcher_id
|
||||
elif binding.runner_id in {
|
||||
'plugin:langbot-team/DifyAgent/default',
|
||||
'plugin:langbot-team/N8nAgent/default',
|
||||
}:
|
||||
launcher_type = identity.session_launcher_type
|
||||
launcher_id = identity.session_launcher_id
|
||||
else:
|
||||
launcher_type = launcher_id = None
|
||||
if launcher_type in ('group', 'person') and launcher_id:
|
||||
conversation['launcher_type'] = launcher_type
|
||||
conversation['launcher_id'] = launcher_id
|
||||
|
||||
# Build event context (Protocol v1 event-first)
|
||||
event_context = {
|
||||
'event_id': event.event_id,
|
||||
@@ -322,7 +362,7 @@ class RunnerContextBuilder:
|
||||
'trace_id': run_id,
|
||||
'deadline_at': self._build_deadline_from_binding(binding),
|
||||
'metadata': {
|
||||
'bot_id': event.bot_id,
|
||||
'bot_id': bot_id,
|
||||
'workspace_id': event.workspace_id,
|
||||
'streaming_supported': event.delivery.supports_streaming,
|
||||
'model_context_window_tokens': model_context_window_tokens,
|
||||
|
||||
@@ -17,6 +17,18 @@ from langbot_plugin.api.entities.builtin.runner.input import AgentInput
|
||||
from langbot_plugin.api.entities.builtin.runner.delivery import DeliveryContext
|
||||
|
||||
|
||||
class LegacyRunnerIdentity(pydantic.BaseModel):
|
||||
"""Host-captured native Query identity; never populated from event data/params."""
|
||||
|
||||
workspace_id: str | None = None
|
||||
bot_id: str | None = None
|
||||
pipeline_id: str | None = None
|
||||
query_launcher_type: str | None = None
|
||||
query_launcher_id: str | None = None
|
||||
session_launcher_type: str | None = None
|
||||
session_launcher_id: str | None = None
|
||||
|
||||
|
||||
class AgentEventEnvelope(pydantic.BaseModel):
|
||||
"""Event envelope for LangBot Host event gateway.
|
||||
|
||||
@@ -69,6 +81,9 @@ class AgentEventEnvelope(pydantic.BaseModel):
|
||||
data: dict[str, typing.Any] = pydantic.Field(default_factory=dict)
|
||||
"""Small structured event payload. Large payloads should be referenced via raw_ref."""
|
||||
|
||||
legacy_identity: LegacyRunnerIdentity | None = None
|
||||
"""Host-only provenance, separate from untrusted event payload and adapter params."""
|
||||
|
||||
|
||||
# Binding scope types
|
||||
class BindingScope(pydantic.BaseModel):
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
import copy
|
||||
import json
|
||||
import time
|
||||
import typing
|
||||
@@ -14,7 +15,7 @@ from langbot_plugin.api.entities.builtin.platform import message as platform_mes
|
||||
|
||||
from .descriptor import RunnerDescriptor
|
||||
from .errors import RunnerProtocolError
|
||||
from .host_models import AgentBinding, AgentEventEnvelope
|
||||
from .host_models import AgentBinding, AgentEventEnvelope, LegacyRunnerIdentity
|
||||
from .interaction_store import InteractionStore
|
||||
|
||||
|
||||
@@ -152,7 +153,12 @@ class InteractionManager:
|
||||
runner_id=descriptor.id,
|
||||
processor_type=binding.processor_type,
|
||||
processor_id=processor_id,
|
||||
request=request_data,
|
||||
request={
|
||||
**request_data,
|
||||
'_host_legacy_identity': event.legacy_identity.model_dump(mode='json')
|
||||
if event.legacy_identity is not None
|
||||
else None,
|
||||
},
|
||||
delivery_target=reply_target,
|
||||
replaces_interaction_id=(update_target or {}).get('interaction_id'),
|
||||
bot_id=event.bot_id,
|
||||
@@ -161,6 +167,7 @@ class InteractionManager:
|
||||
thread_id=event.thread_id,
|
||||
actor_id=event.actor.actor_id if event.actor else None,
|
||||
expires_at=expires_at,
|
||||
**self._pipeline_admission(binding, descriptor, adapter_context),
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -204,6 +211,50 @@ class InteractionManager:
|
||||
diagnostics.set_outcome('waiting', reason_code='waiting')
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _pipeline_admission(binding, descriptor, adapter_context):
|
||||
if binding.processor_type != 'pipeline':
|
||||
return {}
|
||||
query = (adapter_context or {}).get('_query')
|
||||
expected = copy.deepcopy((adapter_context or {}).get('_pipeline_expected_config'))
|
||||
session = getattr(query, 'session', None)
|
||||
conversation = (adapter_context or {}).get('_pipeline_conversation')
|
||||
|
||||
def authority():
|
||||
from .config_resolver import RunnerConfigResolver
|
||||
|
||||
return (
|
||||
query is not None
|
||||
and session is not None
|
||||
and conversation is not None
|
||||
and isinstance(expected, dict)
|
||||
and session.using_conversation is conversation
|
||||
and getattr(query, 'pipeline_uuid', None) == binding.processor_id
|
||||
and RunnerConfigResolver.resolve_runner_id(expected) == descriptor.id == binding.runner_id
|
||||
and RunnerConfigResolver.resolve_runner_config(expected, descriptor.id) == binding.runner_config
|
||||
)
|
||||
|
||||
return {'expected_config': expected, 'authority_check': authority}
|
||||
|
||||
async def restore_legacy_identity(self, event: AgentEventEnvelope, binding: AgentBinding) -> None:
|
||||
"""Resume only from the scoped, consumed Host record, never callback fields."""
|
||||
event.legacy_identity = None
|
||||
submission = getattr(event.input, 'interaction', None)
|
||||
if submission is None:
|
||||
return
|
||||
if hasattr(submission, 'model_dump'):
|
||||
submission = submission.model_dump(mode='json')
|
||||
record = await self.store.find_resume_request(event=event, binding=binding, submission=submission)
|
||||
if record is None:
|
||||
return
|
||||
identity = record['request'].get('_host_legacy_identity')
|
||||
if not isinstance(identity, dict):
|
||||
return
|
||||
try:
|
||||
event.legacy_identity = LegacyRunnerIdentity.model_validate(identity)
|
||||
except pydantic.ValidationError:
|
||||
return
|
||||
|
||||
@diagnostics.observe('interaction', 'interaction.acknowledge', source='platform', stage='ack')
|
||||
async def acknowledge_submission(self, record: dict[str, typing.Any], adapter: typing.Any) -> None:
|
||||
"""Best-effort transition of submitted controls into a read-only state."""
|
||||
|
||||
@@ -14,6 +14,8 @@ from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from ...entity.persistence.agent_interaction import AgentInteraction
|
||||
from ...persistence.tenant_uow import TenantUnitOfWork
|
||||
from ...persistence.pipeline_admission import lock_pipeline_admission
|
||||
|
||||
|
||||
UTC = datetime.timezone.utc
|
||||
@@ -49,6 +51,12 @@ def _utc_now() -> datetime.datetime:
|
||||
return datetime.datetime.now(UTC)
|
||||
|
||||
|
||||
def _db_datetime(value):
|
||||
# Existing schema is TIMESTAMP WITHOUT TIME ZONE; persist UTC naive on
|
||||
# both backends and normalize to aware UTC only for application comparisons.
|
||||
return _as_utc(value).replace(tzinfo=None) if value is not None else None
|
||||
|
||||
|
||||
def _as_utc(value: datetime.datetime | None) -> datetime.datetime | None:
|
||||
if value is None:
|
||||
return None
|
||||
@@ -113,6 +121,8 @@ class InteractionStore:
|
||||
thread_id: str | None = None,
|
||||
actor_id: str | None = None,
|
||||
expires_at: int | float | None = None,
|
||||
expected_config: dict | None = None,
|
||||
authority_check: typing.Callable[[], bool] | None = None,
|
||||
) -> tuple[dict[str, typing.Any], str]:
|
||||
"""Persist a request and return its record plus one-time callback token."""
|
||||
if not interaction_id or not run_id or not binding_id or not runner_id or not processor_id:
|
||||
@@ -145,11 +155,32 @@ class InteractionStore:
|
||||
delivery_target_json=delivery_target_json,
|
||||
replaces_interaction_id=replaces_interaction_id,
|
||||
callback_token_hash=_token_hash(callback_token),
|
||||
expires_at=expires_at_dt,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
expires_at=_db_datetime(expires_at_dt),
|
||||
created_at=_db_datetime(now),
|
||||
updated_at=_db_datetime(now),
|
||||
)
|
||||
|
||||
if processor_type == 'pipeline':
|
||||
# Require Host-captured configuration and live conversation authority;
|
||||
# runner output must never supply either of these admission inputs.
|
||||
if not workspace_id or not isinstance(expected_config, dict) or authority_check is None:
|
||||
raise InteractionScopeError('Pipeline interaction authority unavailable')
|
||||
try:
|
||||
async with TenantUnitOfWork(self.engine, workspace_id) as uow:
|
||||
current = await lock_pipeline_admission(uow.session, workspace_id, processor_id)
|
||||
if current != expected_config or authority_check() is not True:
|
||||
raise InteractionScopeError('Pipeline interaction authority changed')
|
||||
existing = await self._get_by_run_interaction(uow.session, run_id, interaction_id)
|
||||
if existing is not None:
|
||||
raise DuplicateInteractionError('Interaction already exists')
|
||||
uow.session.add(row)
|
||||
await uow.session.flush()
|
||||
return self._to_dict(row), callback_token
|
||||
except ValueError as exc:
|
||||
raise InteractionScopeError('Pipeline interaction authority unavailable') from exc
|
||||
except IntegrityError as exc:
|
||||
raise DuplicateInteractionError('Interaction already exists') from exc
|
||||
|
||||
async with self._session_factory() as session:
|
||||
existing = await self._get_by_run_interaction(session, run_id, interaction_id)
|
||||
if existing is not None:
|
||||
@@ -182,7 +213,7 @@ class InteractionStore:
|
||||
AgentInteraction.interaction_id == interaction_id,
|
||||
AgentInteraction.status.in_(['pending', 'submitted']),
|
||||
)
|
||||
.values(delivery_result_json=payload, updated_at=_utc_now())
|
||||
.values(delivery_result_json=payload, updated_at=_db_datetime(_utc_now()))
|
||||
)
|
||||
await session.commit()
|
||||
return result.rowcount == 1
|
||||
@@ -228,6 +259,38 @@ class InteractionStore:
|
||||
row = result.scalar_one_or_none()
|
||||
return self._to_dict(row) if row is not None else None
|
||||
|
||||
async def find_resume_request(self, *, event, binding, submission) -> dict[str, typing.Any] | None:
|
||||
"""Resolve persisted provenance with exact tenant/processor/callback scope.
|
||||
|
||||
Ambiguous identities fail closed instead of picking the newest run.
|
||||
"""
|
||||
if not isinstance(submission, dict) or not submission.get('interaction_id'):
|
||||
return None
|
||||
target = event.delivery.reply_target or {}
|
||||
target_type, target_id = target.get('target_type'), target.get('target_id')
|
||||
conversation_id = f'{target_type}_{target_id}' if target_type and target_id else event.conversation_id
|
||||
conditions = [
|
||||
AgentInteraction.interaction_id == submission['interaction_id'],
|
||||
AgentInteraction.status == 'submitted',
|
||||
AgentInteraction.binding_id == binding.binding_id,
|
||||
AgentInteraction.runner_id == binding.runner_id,
|
||||
AgentInteraction.processor_type == binding.processor_type,
|
||||
AgentInteraction.processor_id == (binding.processor_id or binding.agent_id),
|
||||
]
|
||||
for column, value in (
|
||||
(AgentInteraction.workspace_id, event.workspace_id),
|
||||
(AgentInteraction.bot_id, event.bot_id),
|
||||
(AgentInteraction.conversation_id, conversation_id),
|
||||
(AgentInteraction.thread_id, event.thread_id),
|
||||
(AgentInteraction.actor_id, event.actor.actor_id if event.actor else None),
|
||||
):
|
||||
conditions.append(column.is_(None) if value is None else column == value)
|
||||
async with self._session_factory() as session:
|
||||
result = await session.execute(sqlalchemy.select(AgentInteraction).where(*conditions))
|
||||
records = [self._to_dict(row) for row in result.scalars()]
|
||||
matches = [record for record in records if record['submission'] == submission]
|
||||
return matches[0] if len(matches) == 1 else None
|
||||
|
||||
async def get_request(self, run_id: str, interaction_id: str) -> dict[str, typing.Any] | None:
|
||||
"""Return a request by the runner-visible identity."""
|
||||
async with self._session_factory() as session:
|
||||
@@ -283,7 +346,7 @@ class InteractionStore:
|
||||
if expires_at is not None and expires_at <= now:
|
||||
row.status = 'expired'
|
||||
row.status_reason = 'interaction expired before submission'
|
||||
row.updated_at = now
|
||||
row.updated_at = _db_datetime(now)
|
||||
await session.commit()
|
||||
raise InteractionExpiredError('Interaction has expired')
|
||||
|
||||
@@ -297,9 +360,9 @@ class InteractionStore:
|
||||
)
|
||||
.values(
|
||||
status='submitted',
|
||||
submitted_at=submission_time,
|
||||
submitted_at=_db_datetime(submission_time),
|
||||
submission_json=_json_dumps(submission),
|
||||
updated_at=now,
|
||||
updated_at=_db_datetime(now),
|
||||
)
|
||||
)
|
||||
if update_result.rowcount != 1:
|
||||
@@ -323,7 +386,7 @@ class InteractionStore:
|
||||
AgentInteraction.interaction_id == interaction_id,
|
||||
AgentInteraction.status == 'pending',
|
||||
)
|
||||
.values(status='delivery_failed', status_reason=reason, updated_at=now)
|
||||
.values(status='delivery_failed', status_reason=reason, updated_at=_db_datetime(now))
|
||||
)
|
||||
await session.commit()
|
||||
return result.rowcount == 1
|
||||
@@ -337,12 +400,12 @@ class InteractionStore:
|
||||
.where(
|
||||
AgentInteraction.status == 'pending',
|
||||
AgentInteraction.expires_at.is_not(None),
|
||||
AgentInteraction.expires_at <= cutoff,
|
||||
AgentInteraction.expires_at <= _db_datetime(cutoff),
|
||||
)
|
||||
.values(
|
||||
status='expired',
|
||||
status_reason='interaction expired',
|
||||
updated_at=cutoff,
|
||||
updated_at=_db_datetime(cutoff),
|
||||
)
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
"""Host-owned, run-scoped reasoning policy for schema-declared model selectors.
|
||||
|
||||
This policy is not an SDK resource or a provider kwarg. Only the Host binding
|
||||
assembler supplies it; model actions consult it after resource authorization.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import typing
|
||||
|
||||
from ...provider.modelmgr.reasoning import normalize_reasoning_config, validate_reasoning_config
|
||||
from .config_schema import NONE_SENTINELS, iter_schema_items
|
||||
from .descriptor import RunnerDescriptor
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from ...provider.modelmgr.requester import RuntimeLLMModel
|
||||
|
||||
|
||||
ModelReasoningOverrides = dict[str, dict[str, str]]
|
||||
|
||||
|
||||
def extract_model_reasoning_overrides(
|
||||
descriptor: RunnerDescriptor,
|
||||
runner_config: dict[str, typing.Any],
|
||||
resources: typing.Mapping[str, typing.Any],
|
||||
) -> ModelReasoningOverrides:
|
||||
"""Normalize explicit UUID-to-level mappings, intersecting selection and grants.
|
||||
|
||||
An absent entry preserves persisted model defaults. Explicit provider_default
|
||||
overrides them using the native requester's semantics. Descriptor defaults and
|
||||
undeclared config fields cannot silently introduce reasoning overrides.
|
||||
"""
|
||||
authorized = {model.get('model_id') for model in resources.get('models', [])}
|
||||
overrides: ModelReasoningOverrides = {}
|
||||
for item in iter_schema_items(descriptor, {'model-fallback-selector'}):
|
||||
field_name = item.get('name')
|
||||
if not isinstance(field_name, str):
|
||||
continue
|
||||
selection = runner_config.get(field_name)
|
||||
if not isinstance(selection, dict) or 'reasoning' not in selection:
|
||||
continue
|
||||
configured = selection['reasoning']
|
||||
if not isinstance(configured, dict):
|
||||
raise ValueError('Invalid runner model reasoning configuration')
|
||||
candidates = [selection.get('primary')]
|
||||
fallbacks = selection.get('fallbacks')
|
||||
if isinstance(fallbacks, list):
|
||||
candidates.extend(fallbacks)
|
||||
selected = {value for value in candidates if isinstance(value, str) and value not in NONE_SENTINELS}
|
||||
for model_id, level in configured.items():
|
||||
# Validate with Core, but do not echo arbitrary config/secret values.
|
||||
try:
|
||||
if not isinstance(model_id, str) or not isinstance(level, str):
|
||||
raise ValueError
|
||||
config = normalize_reasoning_config({'level': level})
|
||||
except (TypeError, ValueError):
|
||||
raise ValueError('Invalid runner model reasoning configuration') from None
|
||||
if model_id not in selected or model_id not in authorized:
|
||||
continue
|
||||
if model_id in overrides and overrides[model_id] != config:
|
||||
raise ValueError('Conflicting runner model reasoning overrides')
|
||||
overrides[model_id] = config
|
||||
return overrides
|
||||
|
||||
|
||||
def model_with_reasoning_override(
|
||||
model: RuntimeLLMModel,
|
||||
model_id: str,
|
||||
session: typing.Mapping[str, typing.Any] | None,
|
||||
) -> RuntimeLLMModel:
|
||||
"""Clone only the runtime wrapper, after the caller has authorized the model.
|
||||
|
||||
Do not mutate shared model entities or providers. Requesters retain ownership
|
||||
of ability/capability validation and provider-specific argument translation.
|
||||
"""
|
||||
if session is None:
|
||||
return model
|
||||
overrides = session.get('authorization', {}).get('model_reasoning_overrides', {})
|
||||
if model_id not in overrides:
|
||||
return model
|
||||
config = validate_reasoning_config(overrides[model_id], model.model_entity.abilities, model.model_entity.extra_args)
|
||||
scoped_model = copy.copy(model)
|
||||
scoped_model.reasoning_config_override = copy.deepcopy(config)
|
||||
return scoped_model
|
||||
@@ -29,6 +29,7 @@ from .execution_context import (
|
||||
)
|
||||
from .host_models import AgentBinding, AgentEventEnvelope
|
||||
from .invoker import RunnerInvoker
|
||||
from .model_reasoning import extract_model_reasoning_overrides
|
||||
from .interaction_manager import InteractionManager
|
||||
from .query_bridge import QueryRunBridge
|
||||
from .registry import RunnerRegistry
|
||||
@@ -121,6 +122,12 @@ class AgentRunOrchestrator:
|
||||
project_mcp_resource_config(execution_query, binding.runner_config)
|
||||
object.__setattr__(execution_query, '_execution_context', execution_context)
|
||||
|
||||
if event.event_type == 'interaction.submitted' and binding.runner_config.get('user-id-source') in (
|
||||
'legacy-session',
|
||||
'legacy-bot',
|
||||
):
|
||||
await self.interaction_manager.restore_legacy_identity(event, binding)
|
||||
|
||||
execution_event = event
|
||||
resource_addition = await build_mcp_resource_context_addition(self.ap, execution_query)
|
||||
if resource_addition:
|
||||
@@ -133,6 +140,7 @@ class AgentRunOrchestrator:
|
||||
binding=binding,
|
||||
descriptor=descriptor,
|
||||
)
|
||||
model_reasoning_overrides = extract_model_reasoning_overrides(descriptor, binding.runner_config, resources)
|
||||
|
||||
context = await self.context_builder.build_context_from_event(
|
||||
event=execution_event,
|
||||
@@ -192,6 +200,7 @@ class AgentRunOrchestrator:
|
||||
'state_scopes': list(binding.state_policy.state_scopes),
|
||||
},
|
||||
'state_context': state_context,
|
||||
'model_reasoning_overrides': model_reasoning_overrides,
|
||||
}
|
||||
|
||||
seen_sequences: set[int] = set()
|
||||
@@ -228,6 +237,7 @@ class AgentRunOrchestrator:
|
||||
execution_query=execution_query,
|
||||
platform_context=freeze_platform_context(event),
|
||||
reply_streams=reply_streams,
|
||||
model_reasoning_overrides=model_reasoning_overrides,
|
||||
)
|
||||
|
||||
event_log_id = await self.journal.write_event_log(
|
||||
@@ -412,6 +422,10 @@ class AgentRunOrchestrator:
|
||||
plan = self.query_bridge.build_plan(query)
|
||||
adapter_context = dict(plan.adapter_context)
|
||||
adapter_context['_query'] = query
|
||||
import copy
|
||||
|
||||
adapter_context['_pipeline_expected_config'] = copy.deepcopy(query.pipeline_config)
|
||||
adapter_context['_pipeline_conversation'] = getattr(getattr(query, 'session', None), 'using_conversation', None)
|
||||
adapter_context['_execution_context'] = get_query_execution_context(query)
|
||||
|
||||
# Inbound files and subsequent runner tools must share one Host scope.
|
||||
|
||||
@@ -25,6 +25,7 @@ from langbot_plugin.api.entities.builtin.runner.delivery import DeliveryContext
|
||||
from .host_models import (
|
||||
AgentConfig,
|
||||
AgentEventEnvelope,
|
||||
LegacyRunnerIdentity,
|
||||
StatePolicy,
|
||||
DeliveryPolicy,
|
||||
)
|
||||
@@ -100,6 +101,24 @@ class QueryEntryAdapter:
|
||||
delivery=delivery,
|
||||
raw_ref=raw_ref,
|
||||
data=event.data,
|
||||
legacy_identity=cls._build_legacy_identity(query, execution_context.workspace_uuid),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _build_legacy_identity(query, workspace_id: str) -> LegacyRunnerIdentity:
|
||||
def exact(value):
|
||||
value = getattr(value, 'value', value)
|
||||
return value if isinstance(value, str) and value else None
|
||||
|
||||
session = getattr(query, 'session', None)
|
||||
return LegacyRunnerIdentity(
|
||||
workspace_id=workspace_id,
|
||||
bot_id=exact(getattr(query, 'bot_uuid', None)),
|
||||
pipeline_id=exact(getattr(query, 'pipeline_uuid', None)),
|
||||
query_launcher_type=exact(getattr(query, 'launcher_type', None)),
|
||||
query_launcher_id=exact(getattr(query, 'launcher_id', None)),
|
||||
session_launcher_type=exact(getattr(session, 'launcher_type', None)),
|
||||
session_launcher_id=exact(getattr(session, 'launcher_id', None)),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -13,6 +13,7 @@ import threading
|
||||
from langbot_plugin.api.entities.builtin.pipeline import query as pipeline_query
|
||||
|
||||
from .context_builder import AgentResources
|
||||
from .model_reasoning import ModelReasoningOverrides
|
||||
from ...provider.tools.toolmgr import ToolSourceRef
|
||||
|
||||
|
||||
@@ -52,6 +53,7 @@ class RunAuthorizationSnapshot(typing.TypedDict):
|
||||
platform_context: dict[str, typing.Any]
|
||||
authorized_ids: dict[str, set[str]]
|
||||
authorized_operations: dict[str, dict[str, set[str]]]
|
||||
model_reasoning_overrides: ModelReasoningOverrides
|
||||
|
||||
|
||||
SteeringQueueItem = dict[str, typing.Any]
|
||||
@@ -119,6 +121,7 @@ class AgentRunSessionRegistry:
|
||||
execution_query: pipeline_query.Query | None = None,
|
||||
platform_context: dict[str, typing.Any] | None = None,
|
||||
reply_streams: typing.Any = None,
|
||||
model_reasoning_overrides: ModelReasoningOverrides | None = None,
|
||||
) -> None:
|
||||
"""Register a new agent run session.
|
||||
|
||||
@@ -164,6 +167,7 @@ class AgentRunSessionRegistry:
|
||||
'platform_context': copy.deepcopy(platform_context or {}),
|
||||
'authorized_ids': self._build_authorized_ids(resources_snapshot),
|
||||
'authorized_operations': self._build_authorized_operations(resources_snapshot),
|
||||
'model_reasoning_overrides': copy.deepcopy(model_reasoning_overrides or {}),
|
||||
}
|
||||
|
||||
session: AgentRunSession = {
|
||||
|
||||
Reference in New Issue
Block a user