feat(lib): implement A-4 M0~M1 BaseAgentAdapter pattern and resolve_home contract

This commit is contained in:
2026-08-14 08:44:36 +09:00
parent e10db8966e
commit caffd04f36
20 changed files with 743 additions and 15 deletions
+1
View File
@@ -0,0 +1 @@
# lib_py.agents package initialization — BaseAgentAdapter and static registry
+35
View File
@@ -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'
+41
View File
@@ -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)
+55
View File
@@ -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