interagent-framework 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,49 @@
1
+ """Constants for the InterAgent framework."""
2
+
3
+ from pathlib import Path
4
+
5
+ # Directory structure
6
+ INTERAGENT_DIR = Path(".interagent")
7
+ AGENTS_DIR = INTERAGENT_DIR / "agents"
8
+ TASKS_DIR = INTERAGENT_DIR / "tasks"
9
+ MESSAGES_DIR = INTERAGENT_DIR / "messages"
10
+ SHARED_DIR = INTERAGENT_DIR / "shared"
11
+
12
+ # Task directories
13
+ TASKS_ACTIVE_DIR = TASKS_DIR / "active"
14
+ TASKS_COMPLETED_DIR = TASKS_DIR / "completed"
15
+
16
+ # Message directories
17
+ MESSAGES_PENDING_DIR = MESSAGES_DIR / "pending"
18
+ MESSAGES_ARCHIVE_DIR = MESSAGES_DIR / "archive"
19
+
20
+ # File paths
21
+ SESSION_FILE = INTERAGENT_DIR / "session.json"
22
+ AGENTS_FILE = INTERAGENT_DIR / "agents.json"
23
+
24
+ # Valid agents
25
+ VALID_AGENTS = ["claude", "kimi"]
26
+
27
+ # Valid roles
28
+ VALID_ROLES = ["principal", "delegate", "reviewer", "collaborator"]
29
+
30
+ # Valid modes
31
+ VALID_MODES = ["hierarchical", "peer", "review"]
32
+
33
+ # Task statuses
34
+ TASK_STATUSES = [
35
+ "pending",
36
+ "assigned",
37
+ "in_progress",
38
+ "completed",
39
+ "under_review",
40
+ "revision_needed",
41
+ "approved",
42
+ "rejected",
43
+ ]
44
+
45
+ # Message types
46
+ MESSAGE_TYPES = ["message", "delegation", "review", "discussion"]
47
+
48
+ # Priorities
49
+ PRIORITIES = ["low", "medium", "high", "critical"]
interagent/locking.py ADDED
@@ -0,0 +1,147 @@
1
+ """File locking mechanism for preventing race conditions."""
2
+
3
+ import time
4
+ from pathlib import Path
5
+ from typing import Optional
6
+ from contextlib import contextmanager
7
+
8
+ from .constants import INTERAGENT_DIR
9
+
10
+ LOCK_DIR = INTERAGENT_DIR / ".locks"
11
+ DEFAULT_TIMEOUT = 30 # seconds
12
+ DEFAULT_RETRY_DELAY = 0.1 # seconds
13
+
14
+
15
+ class LockError(Exception):
16
+ """Raised when lock cannot be acquired."""
17
+ pass
18
+
19
+
20
+ def acquire_lock(lock_name: str, timeout: float = DEFAULT_TIMEOUT) -> bool:
21
+ """Acquire a lock by creating a lock file.
22
+
23
+ Args:
24
+ lock_name: Name of the lock (e.g., task_id)
25
+ timeout: Maximum time to wait for lock
26
+
27
+ Returns:
28
+ True if lock acquired, False otherwise
29
+ """
30
+ LOCK_DIR.mkdir(parents=True, exist_ok=True)
31
+ lock_file = LOCK_DIR / f"{lock_name}.lock"
32
+
33
+ start_time = time.time()
34
+
35
+ while time.time() - start_time < timeout:
36
+ try:
37
+ # Try to create lock file exclusively
38
+ with open(lock_file, "x") as f:
39
+ f.write(str(time.time()))
40
+ return True
41
+ except FileExistsError:
42
+ # Lock exists, check if stale
43
+ try:
44
+ with open(lock_file, "r") as f:
45
+ lock_time = float(f.read().strip())
46
+
47
+ # If lock is older than 5 minutes, it's stale
48
+ if time.time() - lock_time > 300:
49
+ lock_file.unlink()
50
+ continue
51
+
52
+ except (ValueError, IOError):
53
+ pass
54
+
55
+ # Wait and retry
56
+ time.sleep(DEFAULT_RETRY_DELAY)
57
+
58
+ return False
59
+
60
+
61
+ def release_lock(lock_name: str) -> bool:
62
+ """Release a lock by removing the lock file.
63
+
64
+ Args:
65
+ lock_name: Name of the lock
66
+
67
+ Returns:
68
+ True if lock was released, False if it didn't exist
69
+ """
70
+ lock_file = LOCK_DIR / f"{lock_name}.lock"
71
+
72
+ try:
73
+ if lock_file.exists():
74
+ lock_file.unlink()
75
+ return True
76
+ except IOError:
77
+ pass
78
+
79
+ return False
80
+
81
+
82
+ @contextmanager
83
+ def lock(lock_name: str, timeout: float = DEFAULT_TIMEOUT):
84
+ """Context manager for acquiring and releasing locks.
85
+
86
+ Usage:
87
+ with lock("task-123"):
88
+ # Do work on task-123
89
+ pass
90
+ """
91
+ if not acquire_lock(lock_name, timeout):
92
+ raise LockError(f"Could not acquire lock: {lock_name}")
93
+
94
+ try:
95
+ yield
96
+ finally:
97
+ release_lock(lock_name)
98
+
99
+
100
+ def is_locked(lock_name: str) -> bool:
101
+ """Check if a resource is currently locked.
102
+
103
+ Args:
104
+ lock_name: Name of the lock
105
+
106
+ Returns:
107
+ True if locked, False otherwise
108
+ """
109
+ lock_file = LOCK_DIR / f"{lock_name}.lock"
110
+
111
+ if not lock_file.exists():
112
+ return False
113
+
114
+ # Check if lock is stale (read-only check, no side effects)
115
+ try:
116
+ with open(lock_file, "r") as f:
117
+ lock_time = float(f.read().strip())
118
+
119
+ # If lock is older than 5 minutes, treat as not locked
120
+ # (acquire_lock() will clean it up when needed)
121
+ if time.time() - lock_time > 300:
122
+ return False
123
+
124
+ except (ValueError, IOError):
125
+ pass
126
+
127
+ return True
128
+
129
+
130
+ def wait_for_unlock(lock_name: str, timeout: float = DEFAULT_TIMEOUT) -> bool:
131
+ """Wait for a lock to be released.
132
+
133
+ Args:
134
+ lock_name: Name of the lock
135
+ timeout: Maximum time to wait
136
+
137
+ Returns:
138
+ True if unlocked (or was never locked), False if timeout
139
+ """
140
+ start_time = time.time()
141
+
142
+ while time.time() - start_time < timeout:
143
+ if not is_locked(lock_name):
144
+ return True
145
+ time.sleep(DEFAULT_RETRY_DELAY)
146
+
147
+ return False
@@ -0,0 +1,183 @@
1
+ """Messaging system for InterAgent."""
2
+
3
+ from typing import Any, Dict, List, Optional
4
+
5
+ from .constants import MESSAGES_PENDING_DIR, MESSAGES_ARCHIVE_DIR, MESSAGE_TYPES
6
+ from .utils import load_json, save_json, generate_id, now_iso
7
+
8
+
9
+ class Message:
10
+ """Represents a message between agents."""
11
+
12
+ def __init__(self, data: Dict[str, Any]):
13
+ """Initialize message with data."""
14
+ self._data = data
15
+
16
+ @property
17
+ def id(self) -> str:
18
+ """Get message ID."""
19
+ return self._data.get("id", "unknown")
20
+
21
+ @property
22
+ def sender(self) -> str:
23
+ """Get sender agent."""
24
+ return self._data.get("from", "unknown")
25
+
26
+ @property
27
+ def recipient(self) -> str:
28
+ """Get recipient agent."""
29
+ return self._data.get("to", "unknown")
30
+
31
+ @property
32
+ def subject(self) -> str:
33
+ """Get message subject."""
34
+ return self._data.get("subject", "")
35
+
36
+ @property
37
+ def content(self) -> str:
38
+ """Get message content."""
39
+ return self._data.get("content", "")
40
+
41
+ @property
42
+ def message_type(self) -> str:
43
+ """Get message type."""
44
+ return self._data.get("type", "message")
45
+
46
+ @property
47
+ def timestamp(self) -> str:
48
+ """Get timestamp."""
49
+ return self._data.get("timestamp", "")
50
+
51
+ @property
52
+ def is_read(self) -> bool:
53
+ """Check if message is read."""
54
+ return self._data.get("read", False)
55
+
56
+ def to_dict(self) -> Dict[str, Any]:
57
+ """Convert to dictionary."""
58
+ return self._data
59
+
60
+ def to_markdown(self) -> str:
61
+ """Convert to markdown format."""
62
+ lines = [
63
+ f"## Message from {self.sender.upper()}",
64
+ "",
65
+ f"**To:** {self.recipient}",
66
+ f"**Subject:** {self.subject or '(no subject)'}",
67
+ f"**Time:** {self.timestamp}",
68
+ f"**Type:** {self.message_type}",
69
+ "",
70
+ self.content,
71
+ "",
72
+ ]
73
+ return "\n".join(lines)
74
+
75
+ @classmethod
76
+ def load(cls, message_id: str, pending: bool = True) -> Optional["Message"]:
77
+ """Load message by ID."""
78
+ if pending:
79
+ filepath = MESSAGES_PENDING_DIR / f"{message_id}.json"
80
+ else:
81
+ filepath = MESSAGES_ARCHIVE_DIR / f"{message_id}.json"
82
+
83
+ data = load_json(filepath)
84
+ if data:
85
+ return cls(data)
86
+ return None
87
+
88
+ def save(self, pending: bool = True) -> bool:
89
+ """Save message to file."""
90
+ if pending:
91
+ filepath = MESSAGES_PENDING_DIR / f"{self.id}.json"
92
+ else:
93
+ filepath = MESSAGES_ARCHIVE_DIR / f"{self.id}.json"
94
+ return save_json(filepath, self._data)
95
+
96
+ def mark_read(self) -> bool:
97
+ """Mark message as read and move to archive."""
98
+ pending_path = MESSAGES_PENDING_DIR / f"{self.id}.json"
99
+ archive_path = MESSAGES_ARCHIVE_DIR / f"{self.id}.json"
100
+
101
+ self._data["read"] = True
102
+ self._data["read_at"] = now_iso()
103
+
104
+ save_json(archive_path, self._data)
105
+
106
+ if pending_path.exists():
107
+ pending_path.unlink()
108
+ return True
109
+ return False
110
+
111
+ @classmethod
112
+ def create(
113
+ cls,
114
+ sender: str,
115
+ recipient: str,
116
+ content: str,
117
+ subject: str = "",
118
+ message_type: str = "message",
119
+ task_id: Optional[str] = None,
120
+ ) -> "Message":
121
+ """Create a new message."""
122
+ if message_type not in MESSAGE_TYPES:
123
+ message_type = "message"
124
+
125
+ data = {
126
+ "id": generate_id("msg"),
127
+ "from": sender,
128
+ "to": recipient,
129
+ "subject": subject,
130
+ "content": content,
131
+ "type": message_type,
132
+ "timestamp": now_iso(),
133
+ "read": False,
134
+ "task_id": task_id,
135
+ }
136
+
137
+ return cls(data)
138
+
139
+
140
+ class MessageBus:
141
+ """Manages message routing and storage."""
142
+
143
+ @staticmethod
144
+ def send(message: Message) -> bool:
145
+ """Send a message (save to pending)."""
146
+ return message.save(pending=True)
147
+
148
+ @staticmethod
149
+ def get_inbox(agent: str) -> List[Message]:
150
+ """Get all pending messages for an agent."""
151
+ messages = []
152
+ for filepath in MESSAGES_PENDING_DIR.glob("*.json"):
153
+ data = load_json(filepath)
154
+ if data and data.get("to") == agent:
155
+ messages.append(Message(data))
156
+ return sorted(messages, key=lambda m: m.timestamp)
157
+
158
+ @staticmethod
159
+ def get_outbox(agent: str) -> List[Message]:
160
+ """Get all sent messages from an agent."""
161
+ messages = []
162
+
163
+ # Check pending
164
+ for filepath in MESSAGES_PENDING_DIR.glob("*.json"):
165
+ data = load_json(filepath)
166
+ if data and data.get("from") == agent:
167
+ messages.append(Message(data))
168
+
169
+ # Check archive
170
+ for filepath in MESSAGES_ARCHIVE_DIR.glob("*.json"):
171
+ data = load_json(filepath)
172
+ if data and data.get("from") == agent:
173
+ messages.append(Message(data))
174
+
175
+ return sorted(messages, key=lambda m: m.timestamp)
176
+
177
+ @staticmethod
178
+ def mark_read(message_id: str) -> bool:
179
+ """Mark a message as read."""
180
+ message = Message.load(message_id, pending=True)
181
+ if message:
182
+ return message.mark_read()
183
+ return False
interagent/session.py ADDED
@@ -0,0 +1,129 @@
1
+ """Session management for InterAgent."""
2
+
3
+ from typing import Any, Dict, List, Optional
4
+
5
+ from .constants import SESSION_FILE, VALID_AGENTS, VALID_MODES
6
+ from .utils import load_json, save_json, generate_id, now_iso
7
+
8
+
9
+ class Session:
10
+ """Manages an inter-agent collaboration session."""
11
+
12
+ def __init__(self, data: Optional[Dict[str, Any]] = None):
13
+ """Initialize session with data."""
14
+ self._data = data or {}
15
+
16
+ @property
17
+ def id(self) -> str:
18
+ """Get session ID."""
19
+ return self._data.get("id", "unknown")
20
+
21
+ @property
22
+ def name(self) -> str:
23
+ """Get session name."""
24
+ return self._data.get("name", "Unnamed Session")
25
+
26
+ @property
27
+ def mode(self) -> str:
28
+ """Get collaboration mode."""
29
+ return self._data.get("mode", "hierarchical")
30
+
31
+ @property
32
+ def principal(self) -> str:
33
+ """Get principal agent."""
34
+ return self._data.get("principal", "claude")
35
+
36
+ @property
37
+ def agents(self) -> Dict[str, Dict[str, Any]]:
38
+ """Get agent configurations."""
39
+ return self._data.get("agents", {})
40
+
41
+ def get_agent_role(self, agent: str) -> str:
42
+ """Get role for an agent."""
43
+ return self.agents.get(agent, {}).get("role", "delegate")
44
+
45
+ def to_dict(self) -> Dict[str, Any]:
46
+ """Convert to dictionary."""
47
+ return self._data
48
+
49
+ @classmethod
50
+ def load(cls) -> Optional["Session"]:
51
+ """Load session from file."""
52
+ data = load_json(SESSION_FILE)
53
+ if data:
54
+ return cls(data)
55
+ return None
56
+
57
+ def save(self) -> bool:
58
+ """Save session to file."""
59
+ return save_json(SESSION_FILE, self._data)
60
+
61
+ @classmethod
62
+ def create(
63
+ cls,
64
+ name: str,
65
+ principal: str = "claude",
66
+ mode: str = "hierarchical",
67
+ ) -> "Session":
68
+ """Create a new session."""
69
+ if principal not in VALID_AGENTS:
70
+ raise ValueError(f"Invalid principal: {principal}")
71
+ if mode not in VALID_MODES:
72
+ raise ValueError(f"Invalid mode: {mode}")
73
+
74
+ agents = {}
75
+ for agent in VALID_AGENTS:
76
+ agents[agent] = {
77
+ "role": "principal" if agent == principal else "delegate",
78
+ "since": now_iso(),
79
+ }
80
+
81
+ data = {
82
+ "id": generate_id("session"),
83
+ "name": name,
84
+ "created": now_iso(),
85
+ "updated": now_iso(),
86
+ "mode": mode,
87
+ "principal": principal,
88
+ "agents": agents,
89
+ "active_tasks": [],
90
+ "completed_tasks": [],
91
+ "discussions": [],
92
+ }
93
+
94
+ return cls(data)
95
+
96
+ def update(self, **kwargs) -> None:
97
+ """Update session fields."""
98
+ self._data.update(kwargs)
99
+ self._data["updated"] = now_iso()
100
+
101
+ def add_task(self, task_id: str) -> None:
102
+ """Add task to active tasks."""
103
+ tasks = self._data.get("active_tasks", [])
104
+ if task_id not in tasks:
105
+ tasks.append(task_id)
106
+ self._data["active_tasks"] = tasks
107
+
108
+ def complete_task(self, task_id: str) -> None:
109
+ """Move task from active to completed."""
110
+ active = self._data.get("active_tasks", [])
111
+ completed = self._data.get("completed_tasks", [])
112
+
113
+ if task_id in active:
114
+ active.remove(task_id)
115
+ completed.append(task_id)
116
+ self._data["active_tasks"] = active
117
+ self._data["completed_tasks"] = completed
118
+
119
+ def get_summary(self) -> Dict[str, Any]:
120
+ """Get session summary."""
121
+ return {
122
+ "id": self.id,
123
+ "name": self.name,
124
+ "mode": self.mode,
125
+ "principal": self.principal,
126
+ "agents": self.agents,
127
+ "active_tasks_count": len(self._data.get("active_tasks", [])),
128
+ "completed_tasks_count": len(self._data.get("completed_tasks", [])),
129
+ }