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.
- interagent/__init__.py +23 -0
- interagent/cli.py +982 -0
- interagent/constants.py +49 -0
- interagent/locking.py +147 -0
- interagent/messaging.py +183 -0
- interagent/session.py +129 -0
- interagent/task.py +204 -0
- interagent/templates/__init__.py +35 -0
- interagent/templates/review_request.md +68 -0
- interagent/templates/task_delegation.md +69 -0
- interagent/templates/update_prompt.md +70 -0
- interagent/utils.py +90 -0
- interagent/validator.py +156 -0
- interagent/watchdog.py +140 -0
- interagent_framework-0.1.0.dist-info/METADATA +588 -0
- interagent_framework-0.1.0.dist-info/RECORD +20 -0
- interagent_framework-0.1.0.dist-info/WHEEL +5 -0
- interagent_framework-0.1.0.dist-info/entry_points.txt +4 -0
- interagent_framework-0.1.0.dist-info/licenses/LICENSE +21 -0
- interagent_framework-0.1.0.dist-info/top_level.txt +1 -0
interagent/constants.py
ADDED
|
@@ -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
|
interagent/messaging.py
ADDED
|
@@ -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
|
+
}
|