Files
tracer-cloud--opensre/core/agent_harness/session/integration_resolution.py
T
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

199 lines
7.7 KiB
Python

"""Per-session integration state, cache helpers, and turn-time resolution.
Owns everything session-scoped for integration discovery: configured service
names, the resolved-config cache, GitHub repo scope, background warm tasks, and
``resolve_and_cache_integrations`` for the turn engine.
``SessionCore`` composes :class:`IntegrationState` as ``session.integrations`` and
re-exposes public fields via properties for API stability. Port-level fetch/classify
logic lives in :mod:`platform.harness_ports` (wired at startup from
``integrations/harness_adapters``).
"""
from __future__ import annotations
import threading
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any
from platform.harness_ports import (
IntegrationResolutionResult,
resolve_integrations,
)
if TYPE_CHECKING:
from core.agent_harness.ports import SessionStore
__all__ = [
"IntegrationResolutionResult",
"IntegrationState",
"has_only_underscore_prefixed_keys",
"has_resolved_integrations",
"merge_resolved_integrations",
"resolve_and_cache_integrations",
"resolve_integrations",
]
# ---------------------------------------------------------------------------
# Cache helpers (shared by IntegrationState and resolve_and_cache_integrations)
# ---------------------------------------------------------------------------
def has_resolved_integrations(cache: dict[str, Any] | None) -> bool:
"""Return True when the cache holds at least one integration config."""
if not cache:
return False
return any(not str(key).startswith("_") for key in cache)
def has_only_underscore_prefixed_keys(cache: dict[str, Any] | None) -> bool:
"""True when every cache key starts with ``_`` (book-keeping only, no configs)."""
if not cache:
return False
return all(str(key).startswith("_") for key in cache)
def merge_resolved_integrations(
base: dict[str, Any] | None,
updates: dict[str, Any],
) -> dict[str, Any]:
"""Merge integration configs while preserving gateway/runtime metadata keys."""
merged = dict(base or {})
merged.update(updates)
return merged
def _has_usable_cache(cache: dict[str, Any] | None) -> bool:
"""True when a cache holds resolved configs and need not be re-resolved."""
return cache is not None and (
has_resolved_integrations(cache) or not has_only_underscore_prefixed_keys(cache)
)
def resolve_and_cache_integrations(session: SessionStore) -> dict[str, Any]:
"""Resolve a session's integration configs, using and updating its cache."""
cached = session.resolved_integrations_cache
if _has_usable_cache(cached):
return dict(cached or {})
resolved = resolve_integrations()
if resolved:
session.resolved_integrations_cache = merge_resolved_integrations(cached, resolved)
return dict(session.resolved_integrations_cache or {})
# ---------------------------------------------------------------------------
# Session integration facet
# ---------------------------------------------------------------------------
@dataclass
class IntegrationState:
"""A session's integration-resolution state and the logic that warms it."""
configured: tuple[str, ...] = ()
"""Session-scoped configured integration names for planning-time capability checks."""
configured_known: bool = False
"""Whether ``configured`` reflects known state (vs default unknown)."""
resolved_cache: dict[str, Any] | None = None
"""Resolved integration configs (env/store) shared across turns.
Populated silently at REPL boot and again after integration mutations so the
conversational assistant and investigations can call registered tools without
waiting for the first user message to trigger a visible "Loading integrations"
pass. Cleared by :meth:`refresh` when integrations change."""
github_repo_scope: tuple[str, str] | None = None
"""Sticky owner/repo inferred from chat, env, or git remote for GitHub tools."""
_warm_lock: threading.Lock = field(default_factory=threading.Lock, repr=False, compare=False)
_warm_generation: int = field(default=0, repr=False, compare=False)
_warm_task: Any = field(default=None, repr=False, compare=False)
def hydrate(self) -> None:
"""Load configured integration names (env + local store) — metadata only.
Run at REPL boot and again whenever an integration is added or removed so
capability checks and the tool-gathering pass reflect the current store
state instead of a stale boot-time snapshot. Must not resolve keyring-backed
secrets; full configs are resolved on demand via :meth:`warm`/:meth:`get`.
"""
try:
from platform.harness_ports import configured_integration_services
self.configured = tuple(sorted(configured_integration_services()))
self.configured_known = True
except Exception:
# Best-effort: keep whatever state we already had (default unknown).
pass
def warm(self, *, generation: int | None = None) -> None:
"""Resolve full integration configs once, without progress UI.
Empty resolves are not cached so a later turn can retry if boot-time
resolution raced store/env hydration. Failures leave the cache unset.
"""
cached = self.resolved_cache
if cached is not None and not has_only_underscore_prefixed_keys(cached):
return
if generation is None:
with self._warm_lock:
generation = self._warm_generation
try:
resolved = resolve_integrations()
except Exception:
# Best-effort warmup: leave cache unset so later turns can retry.
return
self._store(resolved, generation=generation)
def _store(self, resolved: dict[str, Any], *, generation: int) -> None:
if not resolved:
return
with self._warm_lock:
if generation != self._warm_generation:
return
if self.resolved_cache is not None and not has_only_underscore_prefixed_keys(
self.resolved_cache
):
return
self.resolved_cache = merge_resolved_integrations(self.resolved_cache, resolved)
def get(self) -> IntegrationResolutionResult:
"""Return the session's integration configs as a typed snapshot (cache-aware).
An explicit empty cache is treated as known state; metadata-only caches
trigger one quiet warmup, merged through the same generation guard as startup.
"""
cached = self.resolved_cache
if _has_usable_cache(cached):
return IntegrationResolutionResult(resolved_integrations=dict(cached or {}))
self.warm()
return IntegrationResolutionResult(resolved_integrations=dict(self.resolved_cache or {}))
def refresh(self) -> None:
"""Re-resolve after the local store changes: drop cache, re-hydrate, re-warm."""
self._cancel_warm(drop_cache=True)
self.hydrate()
self.warm()
def reset(self) -> None:
"""Reset all resolution state for /new (cancels any in-flight warm task)."""
self._cancel_warm(drop_cache=True)
self.configured = ()
self.configured_known = False
def release(self) -> None:
"""Cancel the in-flight warm task for teardown (keeps cached data)."""
self._cancel_warm(drop_cache=False)
def _cancel_warm(self, *, drop_cache: bool) -> None:
with self._warm_lock:
self._warm_generation += 1
pending = self._warm_task
self._warm_task = None
if drop_cache:
self.resolved_cache = None
self.github_repo_scope = None
if pending is not None and not pending.done():
pending.cancel()