feat(lib): implement A-4 M0~M1 BaseAgentAdapter pattern and resolve_home contract
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# lib_py.agents package initialization — BaseAgentAdapter and static registry
|
||||
@@ -0,0 +1,35 @@
|
||||
# __main__.py — CLI bridge for lib_py.agents facts and resolution (N4)
|
||||
|
||||
import sys, json
|
||||
from lib_py.agents.registry import get_adapter, own_key, agent_of_row
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python -m lib_py.agents <facts|resolve> [args...]", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
cmd = sys.argv[1]
|
||||
if cmd == 'facts':
|
||||
agent_name = sys.argv[2] if len(sys.argv) > 2 else ''
|
||||
adapter = get_adapter(agent_name)
|
||||
if not adapter:
|
||||
print(f"ERROR: Unknown agent {agent_name!r}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
# Emit shell-eval friendly facts
|
||||
print(f"AGENT_NAME={adapter.name}")
|
||||
print(f"OWN_KEY={adapter.own_key}")
|
||||
|
||||
elif cmd == 'resolve':
|
||||
name = sys.argv[2] if len(sys.argv) > 2 else ''
|
||||
resolved = agent_of_row({}, session_name=name)
|
||||
if resolved:
|
||||
print(resolved)
|
||||
sys.exit(0)
|
||||
sys.exit(1)
|
||||
|
||||
else:
|
||||
print(f"ERROR: Unknown CLI command {cmd!r}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1 @@
|
||||
# Adapters package init
|
||||
@@ -0,0 +1,12 @@
|
||||
# agy.py — Antigravity (agy) agent adapter
|
||||
|
||||
from lib_py.agents.base import BaseAgentAdapter
|
||||
|
||||
class AgyAgentAdapter(BaseAgentAdapter):
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return 'agy'
|
||||
|
||||
@property
|
||||
def own_key(self) -> str:
|
||||
return 'agy_conversation_id_own'
|
||||
@@ -0,0 +1,12 @@
|
||||
# claude.py — Claude Code agent adapter
|
||||
|
||||
from lib_py.agents.base import BaseAgentAdapter
|
||||
|
||||
class ClaudeAgentAdapter(BaseAgentAdapter):
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return 'claude'
|
||||
|
||||
@property
|
||||
def own_key(self) -> str:
|
||||
return 'claude_session_id_own'
|
||||
@@ -0,0 +1,12 @@
|
||||
# cline.py — Cline agent adapter
|
||||
|
||||
from lib_py.agents.base import BaseAgentAdapter
|
||||
|
||||
class ClineAgentAdapter(BaseAgentAdapter):
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return 'cline'
|
||||
|
||||
@property
|
||||
def own_key(self) -> str:
|
||||
return 'cline_conversation_id_own'
|
||||
@@ -0,0 +1,12 @@
|
||||
# hermes.py — Hermes agent adapter
|
||||
|
||||
from lib_py.agents.base import BaseAgentAdapter
|
||||
|
||||
class HermesAgentAdapter(BaseAgentAdapter):
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return 'hermes'
|
||||
|
||||
@property
|
||||
def own_key(self) -> str:
|
||||
return 'hermes_conversation_id_own'
|
||||
@@ -0,0 +1,41 @@
|
||||
# base.py — BaseAgentAdapter abstract base class, SpawnSpec, and DiscoveryContext
|
||||
|
||||
import os, json, sqlite3
|
||||
from typing import Optional, Dict, Any, List
|
||||
from lib_py.paths import resolve_home
|
||||
from lib_py.verify_session import verify_session_uuid, workspace_key
|
||||
|
||||
class SpawnSpec:
|
||||
def __init__(self, agent_name: str, session_name: str, command: str, env: Optional[Dict[str, str]] = None):
|
||||
self.agent_name = agent_name
|
||||
self.session_name = session_name
|
||||
self.command = command
|
||||
self.env = env or {}
|
||||
|
||||
class DiscoveryContext:
|
||||
def __init__(self, workspace: str, agent_name: str, home_dir: Optional[str] = None):
|
||||
self.workspace = workspace
|
||||
self.agent_name = agent_name
|
||||
self.home_dir = resolve_home(home_dir)
|
||||
|
||||
class BaseAgentAdapter:
|
||||
@property
|
||||
def name(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def own_key(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
def derive_session_name(self, slug: str, role: str = "creator") -> str:
|
||||
r = role.lower()
|
||||
return f"{slug}-{r}-{self.name}"
|
||||
|
||||
def matches_session_name(self, session_name: str) -> bool:
|
||||
for r in ('creator', 'planner', 'reviewer'):
|
||||
if session_name.endswith(f"-{r}-{self.name}"):
|
||||
return True
|
||||
return session_name.endswith(f"-{self.name}")
|
||||
|
||||
def verify_session(self, ws: str, uuid: str, row: Optional[Dict[str, Any]] = None, mode: str = "discover") -> bool:
|
||||
return verify_session_uuid(ws, self.name, uuid, row=row, mode=mode)
|
||||
@@ -0,0 +1,55 @@
|
||||
# registry.py — Static agent adapter registry and resolution functions (N4 & N5)
|
||||
|
||||
from typing import Optional, Dict, Any
|
||||
from lib_py.agents.base import BaseAgentAdapter
|
||||
from lib_py.agents.adapters.claude import ClaudeAgentAdapter
|
||||
from lib_py.agents.adapters.agy import AgyAgentAdapter
|
||||
from lib_py.agents.adapters.hermes import HermesAgentAdapter
|
||||
from lib_py.agents.adapters.cline import ClineAgentAdapter
|
||||
|
||||
_ADAPTERS: Dict[str, BaseAgentAdapter] = {
|
||||
'claude': ClaudeAgentAdapter(),
|
||||
'agy': AgyAgentAdapter(),
|
||||
'hermes': HermesAgentAdapter(),
|
||||
'cline': ClineAgentAdapter(),
|
||||
}
|
||||
|
||||
def get_adapter(agent_name: str) -> Optional[BaseAgentAdapter]:
|
||||
if not agent_name:
|
||||
return None
|
||||
return _ADAPTERS.get(agent_name.lower())
|
||||
|
||||
def own_key(agent_name: str) -> Optional[str]:
|
||||
adapter = get_adapter(agent_name)
|
||||
return adapter.own_key if adapter else None
|
||||
|
||||
def agent_of_row(row: Dict[str, Any], session_name: str = "", match_cmd: bool = True) -> Optional[str]:
|
||||
"""
|
||||
Resolves agent for a given registry row or session name using strict priority order (N5):
|
||||
1. Explicit 'agent' field in row dictionary
|
||||
2. Session name suffix matching (*-{creator,planner,reviewer}-<agent> or *-<agent>)
|
||||
3. pane.cmd exact match (if match_cmd is True, e.g. for non-adoption lookup)
|
||||
4. None (resolution failure)
|
||||
"""
|
||||
if isinstance(row, dict):
|
||||
explicit = row.get('agent')
|
||||
if explicit and str(explicit).lower() in _ADAPTERS:
|
||||
return str(explicit).lower()
|
||||
if not session_name:
|
||||
session_name = row.get('name', '')
|
||||
|
||||
name = session_name or (row.get('name', '') if isinstance(row, dict) else '')
|
||||
if name:
|
||||
for agent_name, adapter in _ADAPTERS.items():
|
||||
if adapter.matches_session_name(name):
|
||||
return agent_name
|
||||
|
||||
if match_cmd and isinstance(row, dict):
|
||||
pane = row.get('pane') or {}
|
||||
cmd = pane.get('cmd', '') if isinstance(pane, dict) else ''
|
||||
if cmd:
|
||||
for agent_name in _ADAPTERS:
|
||||
if cmd == agent_name or cmd.endswith(f"/{agent_name}"):
|
||||
return agent_name
|
||||
|
||||
return None
|
||||
Reference in New Issue
Block a user