Merge branch 'staging' into fix/chat-user-timestamp-from-activity

This commit is contained in:
hongmingwang-moleculeai 2026-04-26 22:38:14 -07:00 committed by GitHub
commit fa592bbead
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 247 additions and 0 deletions

View File

@ -89,6 +89,7 @@ jobs:
'sk-ant-[A-Za-z0-9_-]{40,}' # Anthropic API key
'sk-proj-[A-Za-z0-9_-]{40,}' # OpenAI project key
'sk-svcacct-[A-Za-z0-9_-]{40,}' # OpenAI service-account key
'sk-cp-[A-Za-z0-9_-]{60,}' # MiniMax API key (F1088 vector — caught only after the fact)
'xox[baprs]-[A-Za-z0-9-]{20,}' # Slack tokens
'AKIA[0-9A-Z]{16}' # AWS access key ID
'ASIA[0-9A-Z]{16}' # AWS STS temp access key ID

View File

@ -35,6 +35,83 @@ class AdapterConfig:
heartbeat: Any = None # HeartbeatLoop instance
@dataclass(frozen=True)
class RuntimeCapabilities:
"""Adapter-declared ownership of cross-cutting platform capabilities.
The platform provides FALLBACK implementations of heartbeat, cron,
durable session, etc. When a runtime SDK provides one of these
natively (e.g. claude-code's streaming session model, hermes-agent's
sidecar lifecycle), the adapter sets the corresponding flag to True.
The platform reads these flags and skips its fallback for that
capability the adapter is responsible instead.
Observability is NEVER skipped: A2A protocol, activity_logs, and the
broadcaster always run regardless of who owns the capability. These
flags only switch WHO IMPLEMENTS the behavior, not whether the
platform sees it.
All defaults are False so introducing this dataclass is a no-op:
every existing adapter inherits BaseAdapter.capabilities() which
returns RuntimeCapabilities() with everything off, matching today's
"platform does it all" behavior. Each capability gets a platform-
side consumer in a follow-up PR; this class is the foundation.
See project memory `project_runtime_native_pluggable.md` for the
architecture principle these flags encode.
"""
# Heartbeat — adapter sends its own keep-alive signal to the platform's
# broadcaster instead of relying on workspace/heartbeat.py's 30s loop.
# Set True when the SDK already maintains a long-lived session that
# produces natural progress events (e.g. claude-code streaming).
provides_native_heartbeat: bool = False
# Cron / schedule — adapter handles scheduled triggers internally
# (Temporal workflows, Durable Functions, sidecar daemons). Platform
# scheduler skips polling workspace_schedules for this workspace,
# avoiding double-fire on restart.
provides_native_scheduler: bool = False
# Durable session — adapter persists in-flight session state across
# restarts and exposes it via pre_stop_state/restore_state. When True,
# the platform's a2a_queue does not need to enqueue mid-session
# requests; the adapter handles QUEUED-state on its own.
provides_native_session: bool = False
# Status lifecycle — adapter reports its own ready/degraded/failed
# state (e.g. via heartbeat metadata). Platform respects the adapter
# report instead of inferring status from heartbeat error rate.
provides_native_status_mgmt: bool = False
# Retry — adapter handles transient errors (rate limits, 5xx) with
# its own backoff. Platform stops re-dispatching A2A requests that
# the adapter explicitly marked as "retrying internally".
provides_native_retry: bool = False
# Activity log decoration — adapter contributes runtime-specific
# fields (model, token_count, latency breakdown) into activity_log
# rows alongside the platform-defined columns.
provides_activity_decoration: bool = False
# Channel dispatch — adapter sends to external channels (Slack,
# Lark, etc.) directly instead of routing through platform channels
# manager. Used when the SDK has built-in channel integrations.
provides_channel_dispatch: bool = False
def to_dict(self) -> dict[str, bool]:
"""Serializable shape for the heartbeat payload + /capabilities
endpoint. Plain dict avoids leaking dataclass internals to Go."""
return {
"heartbeat": self.provides_native_heartbeat,
"scheduler": self.provides_native_scheduler,
"session": self.provides_native_session,
"status_mgmt": self.provides_native_status_mgmt,
"retry": self.provides_native_retry,
"activity_decoration": self.provides_activity_decoration,
"channel_dispatch": self.provides_channel_dispatch,
}
class BaseAdapter(ABC):
"""Interface every agent infrastructure adapter must implement.
@ -72,6 +149,21 @@ class BaseAdapter(ABC):
Override in subclasses for adapter-specific settings."""
return {}
def capabilities(self) -> "RuntimeCapabilities":
"""Declare which cross-cutting capabilities this adapter owns
natively vs delegates to platform fallback.
Default returns RuntimeCapabilities() every flag False, meaning
the platform owns everything (today's behavior). Adapters override
to declare native ownership; e.g. claude-code's adapter returns
RuntimeCapabilities(provides_native_heartbeat=True,
provides_native_session=True).
Subsequent platform-side consumers (idle-timeout override,
scheduler skip, etc.) read this and route accordingly. See
project memory `project_runtime_native_pluggable.md`."""
return RuntimeCapabilities()
# ------------------------------------------------------------------
# Plugin install hooks
# ------------------------------------------------------------------

View File

@ -0,0 +1,154 @@
"""Tests for RuntimeCapabilities + BaseAdapter.capabilities() — the
foundation primitive for the native+pluggable runtime principle (task
#117). The dataclass + default method are intentionally a no-op
addition; these tests pin that contract so a future change can't
accidentally flip a default and silently move ownership.
"""
from dataclasses import is_dataclass
import pytest
from adapter_base import BaseAdapter, RuntimeCapabilities
class _MinimalAdapter(BaseAdapter):
"""Concrete subclass with only the abstract members satisfied —
every other behavior should fall through to BaseAdapter defaults
so we can assert what those defaults are."""
@staticmethod
def name() -> str:
return "test-minimal"
@staticmethod
def display_name() -> str:
return "Test Minimal"
@staticmethod
def description() -> str:
return "Minimal adapter for capability default tests"
async def setup(self, config) -> None:
return None
async def create_executor(self, config): # pragma: no cover
raise NotImplementedError
class _NativeHeartbeatAdapter(_MinimalAdapter):
"""Models a runtime that owns heartbeat natively — declares it via
capabilities() override. Used to verify the override mechanism
works without touching defaults."""
def capabilities(self) -> RuntimeCapabilities:
return RuntimeCapabilities(provides_native_heartbeat=True)
class TestRuntimeCapabilitiesDataclass:
"""The dataclass surface itself."""
def test_is_a_dataclass(self):
assert is_dataclass(RuntimeCapabilities)
def test_is_frozen(self):
# Immutability matters: capabilities are declared at class-load
# time and read by the platform on every heartbeat. A mutable
# value would let a runtime change capabilities mid-flight,
# creating impossible-to-debug state where the platform's idea
# of who-owns-heartbeat drifts from the adapter's actual code.
c = RuntimeCapabilities()
with pytest.raises((AttributeError, Exception)):
c.provides_native_heartbeat = True # type: ignore[misc]
def test_all_defaults_false(self):
# Every flag MUST default to False — that's what makes adding
# the dataclass a no-op for existing adapters. If any default
# flips to True, every adapter that didn't override capabilities
# silently switches who-owns-that-capability and the platform
# stops providing the fallback. Catastrophic for langgraph /
# crewai / deepagents which have no native impl.
c = RuntimeCapabilities()
assert c.provides_native_heartbeat is False
assert c.provides_native_scheduler is False
assert c.provides_native_session is False
assert c.provides_native_status_mgmt is False
assert c.provides_native_retry is False
assert c.provides_activity_decoration is False
assert c.provides_channel_dispatch is False
def test_to_dict_keys_are_stable_wire_names(self):
# The Go side reads these by string key from the heartbeat
# payload. If Python renames a field (provides_native_heartbeat
# → has_native_heartbeat) the dict's wire name should NOT change
# — pin the JSON keys here so a refactor on the Python side
# doesn't silently break the Go consumer.
c = RuntimeCapabilities()
assert set(c.to_dict().keys()) == {
"heartbeat",
"scheduler",
"session",
"status_mgmt",
"retry",
"activity_decoration",
"channel_dispatch",
}
def test_to_dict_values_match_flags(self):
c = RuntimeCapabilities(
provides_native_heartbeat=True,
provides_native_session=True,
)
d = c.to_dict()
assert d["heartbeat"] is True
assert d["session"] is True
# Untouched flags stay False — we don't want a "True for one
# capability flips siblings via dataclass inheritance" surprise.
assert d["scheduler"] is False
assert d["status_mgmt"] is False
class TestBaseAdapterCapabilitiesDefault:
"""The BaseAdapter.capabilities() default — the contract every
existing adapter inherits without changes."""
def test_default_returns_all_false(self):
# The whole point of landing this primitive as a separate PR
# is that it's behavior-preserving for everyone. If this test
# fails, every adapter in the project has just had its
# capability declarations silently changed.
a = _MinimalAdapter()
caps = a.capabilities()
assert caps == RuntimeCapabilities()
assert caps.to_dict() == {
"heartbeat": False,
"scheduler": False,
"session": False,
"status_mgmt": False,
"retry": False,
"activity_decoration": False,
"channel_dispatch": False,
}
def test_default_returns_RuntimeCapabilities_instance(self):
a = _MinimalAdapter()
assert isinstance(a.capabilities(), RuntimeCapabilities)
def test_subclass_can_override_capabilities(self):
# Without this working, the entire native+pluggable principle
# is unimplementable. Pin it with a fixture that flips one flag.
a = _NativeHeartbeatAdapter()
caps = a.capabilities()
assert caps.provides_native_heartbeat is True
# Sibling flags untouched — overriding one doesn't accidentally
# move ownership of the others.
assert caps.provides_native_scheduler is False
assert caps.provides_native_session is False
def test_override_does_not_affect_default_for_other_subclasses(self):
# Method-level dispatch, not class-attribute mutation. A
# subclass declaring native_heartbeat must NOT change what
# _MinimalAdapter (a sibling) reports.
minimal = _MinimalAdapter().capabilities()
native = _NativeHeartbeatAdapter().capabilities()
assert minimal.provides_native_heartbeat is False
assert native.provides_native_heartbeat is True