Merge branch 'staging' into fix/chat-user-timestamp-from-activity
This commit is contained in:
commit
fa592bbead
1
.github/workflows/secret-scan.yml
vendored
1
.github/workflows/secret-scan.yml
vendored
@ -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
|
||||
|
||||
@ -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
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
154
workspace/tests/test_runtime_capabilities.py
Normal file
154
workspace/tests/test_runtime_capabilities.py
Normal 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
|
||||
Loading…
Reference in New Issue
Block a user