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/task.py ADDED
@@ -0,0 +1,204 @@
1
+ """Task management for InterAgent."""
2
+
3
+ import re
4
+ from enum import Enum
5
+ from typing import Any, Dict, List, Optional
6
+ from pathlib import Path
7
+
8
+ from .constants import TASKS_ACTIVE_DIR, TASKS_COMPLETED_DIR, TASK_STATUSES, PRIORITIES
9
+ from .utils import load_json, save_json, generate_id, now_iso
10
+
11
+
12
+ class TaskStatus(Enum):
13
+ """Task status enumeration."""
14
+ PENDING = "pending"
15
+ ASSIGNED = "assigned"
16
+ IN_PROGRESS = "in_progress"
17
+ COMPLETED = "completed"
18
+ UNDER_REVIEW = "under_review"
19
+ REVISION_NEEDED = "revision_needed"
20
+ APPROVED = "approved"
21
+ REJECTED = "rejected"
22
+
23
+
24
+ class Task:
25
+ """Represents a task in the system."""
26
+
27
+ def __init__(self, data: Dict[str, Any]):
28
+ """Initialize task with data."""
29
+ self._data = data
30
+
31
+ @property
32
+ def id(self) -> str:
33
+ """Get task ID."""
34
+ return self._data.get("id", "unknown")
35
+
36
+ @property
37
+ def title(self) -> str:
38
+ """Get task title."""
39
+ return self._data.get("title", "Untitled")
40
+
41
+ @property
42
+ def status(self) -> str:
43
+ """Get task status."""
44
+ return self._data.get("status", "pending")
45
+
46
+ @property
47
+ def assignee(self) -> Optional[str]:
48
+ """Get task assignee."""
49
+ return self._data.get("assignee")
50
+
51
+ @property
52
+ def assigner(self) -> Optional[str]:
53
+ """Get task assigner."""
54
+ return self._data.get("assigner")
55
+
56
+ @property
57
+ def priority(self) -> str:
58
+ """Get task priority."""
59
+ return self._data.get("priority", "medium")
60
+
61
+ def to_dict(self) -> Dict[str, Any]:
62
+ """Convert to dictionary."""
63
+ return self._data
64
+
65
+ def to_markdown(self) -> str:
66
+ """Convert to markdown format."""
67
+ lines = [
68
+ f"# Task: {self.title}",
69
+ "",
70
+ f"**ID:** {self.id}",
71
+ f"**Status:** {self.status}",
72
+ f"**Priority:** {self.priority}",
73
+ f"**Assignee:** {self.assignee or 'Unassigned'}",
74
+ f"**Assigner:** {self.assigner or 'Unknown'}",
75
+ "",
76
+ "## Description",
77
+ self._data.get("description", "_No description_"),
78
+ "",
79
+ ]
80
+
81
+ if self._data.get("requirements"):
82
+ lines.extend(["## Requirements", ""])
83
+ for req in self._data["requirements"]:
84
+ lines.append(f"- [ ] {req}")
85
+ lines.append("")
86
+
87
+ if self._data.get("acceptance_criteria"):
88
+ lines.extend(["## Acceptance Criteria", ""])
89
+ for crit in self._data["acceptance_criteria"]:
90
+ lines.append(f"- [ ] {crit}")
91
+ lines.append("")
92
+
93
+ if self._data.get("deliverables"):
94
+ lines.extend(["## Deliverables", ""])
95
+ for d in self._data["deliverables"]:
96
+ lines.append(f"- {d}")
97
+ lines.append("")
98
+
99
+ return "\n".join(lines)
100
+
101
+ @classmethod
102
+ def load(cls, task_id: str) -> Optional["Task"]:
103
+ """Load task by ID."""
104
+ # Validate task_id format to prevent path traversal
105
+ if not re.match(r'^[a-zA-Z0-9_-]+$', task_id):
106
+ return None
107
+
108
+ # Try active first
109
+ filepath = TASKS_ACTIVE_DIR / f"{task_id}.json"
110
+ data = load_json(filepath)
111
+
112
+ if not data:
113
+ # Try completed
114
+ filepath = TASKS_COMPLETED_DIR / f"{task_id}.json"
115
+ data = load_json(filepath)
116
+
117
+ if data:
118
+ return cls(data)
119
+ return None
120
+
121
+ def save(self) -> bool:
122
+ """Save task to file."""
123
+ filepath = TASKS_ACTIVE_DIR / f"{self.id}.json"
124
+ return save_json(filepath, self._data)
125
+
126
+ def update(self, **kwargs) -> None:
127
+ """Update task fields."""
128
+ self._data.update(kwargs)
129
+ self._data["updated"] = now_iso()
130
+
131
+ def move_to_completed(self) -> bool:
132
+ """Move task from active to completed."""
133
+ active_path = TASKS_ACTIVE_DIR / f"{self.id}.json"
134
+ completed_path = TASKS_COMPLETED_DIR / f"{self.id}.json"
135
+
136
+ if active_path.exists():
137
+ save_json(completed_path, self._data)
138
+ active_path.unlink()
139
+ return True
140
+ return False
141
+
142
+ @classmethod
143
+ def create(
144
+ cls,
145
+ title: str,
146
+ description: str = "",
147
+ assignee: Optional[str] = None,
148
+ assigner: Optional[str] = None,
149
+ priority: str = "medium",
150
+ requirements: Optional[List[str]] = None,
151
+ acceptance_criteria: Optional[List[str]] = None,
152
+ ) -> "Task":
153
+ """Create a new task."""
154
+ if priority not in PRIORITIES:
155
+ priority = "medium"
156
+
157
+ data = {
158
+ "id": generate_id("task"),
159
+ "title": title,
160
+ "description": description,
161
+ "status": "pending",
162
+ "priority": priority,
163
+ "assignee": assignee,
164
+ "assigner": assigner,
165
+ "created": now_iso(),
166
+ "updated": now_iso(),
167
+ "requirements": requirements or [],
168
+ "acceptance_criteria": acceptance_criteria or [],
169
+ "deliverables": [],
170
+ "notes": [],
171
+ }
172
+
173
+ return cls(data)
174
+
175
+ @classmethod
176
+ def list_all(
177
+ cls,
178
+ status: Optional[str] = None,
179
+ assignee: Optional[str] = None,
180
+ active_only: bool = False,
181
+ ) -> List["Task"]:
182
+ """List all tasks matching criteria."""
183
+ tasks = []
184
+
185
+ # Load active tasks
186
+ for filepath in TASKS_ACTIVE_DIR.glob("*.json"):
187
+ data = load_json(filepath)
188
+ if data:
189
+ tasks.append(cls(data))
190
+
191
+ # Load completed if not active_only
192
+ if not active_only:
193
+ for filepath in TASKS_COMPLETED_DIR.glob("*.json"):
194
+ data = load_json(filepath)
195
+ if data:
196
+ tasks.append(cls(data))
197
+
198
+ # Filter
199
+ if status:
200
+ tasks = [t for t in tasks if t.status == status]
201
+ if assignee:
202
+ tasks = [t for t in tasks if t.assignee == assignee]
203
+
204
+ return tasks
@@ -0,0 +1,35 @@
1
+ """Templates for InterAgent.
2
+
3
+ This module contains markdown templates for common collaboration scenarios.
4
+ """
5
+
6
+ from pathlib import Path
7
+
8
+ TEMPLATES_DIR = Path(__file__).parent
9
+
10
+
11
+ def get_template(name: str) -> str:
12
+ """Get a template by name.
13
+
14
+ Args:
15
+ name: Template name (e.g., 'task_delegation', 'review_request')
16
+
17
+ Returns:
18
+ Template content as string
19
+ """
20
+ template_file = TEMPLATES_DIR / f"{name}.md"
21
+ if template_file.exists():
22
+ return template_file.read_text()
23
+ raise FileNotFoundError(f"Template not found: {name}")
24
+
25
+
26
+ def list_templates() -> list:
27
+ """List available templates.
28
+
29
+ Returns:
30
+ List of template names
31
+ """
32
+ return [f.stem for f in TEMPLATES_DIR.glob("*.md")]
33
+
34
+
35
+ __all__ = ["get_template", "list_templates", "TEMPLATES_DIR"]
@@ -0,0 +1,68 @@
1
+ # Review Request
2
+
3
+ **Task:** {{ task_title }}
4
+ **Task ID:** {{ task_id }}
5
+ **Submitted By:** {{ author }}
6
+ **Date:** {{ date }}
7
+ **Requested Reviewer:** {{ reviewer }}
8
+
9
+ ---
10
+
11
+ ## Summary
12
+
13
+ {{ summary }}
14
+
15
+ ## Changes
16
+
17
+ {% for change in changes %}
18
+ - **{{ change.file }}**: {{ change.description }}
19
+ {% endfor %}
20
+
21
+ ## Testing
22
+
23
+ {% if tests %}
24
+ - [x] Tests written and passing
25
+ - Coverage: {{ coverage }}%
26
+ {% else %}
27
+ - [ ] Tests pending
28
+ {% endif %}
29
+
30
+ ## Specific Areas for Review
31
+
32
+ Please focus on:
33
+
34
+ {% for area in focus_areas %}
35
+ - [ ] {{ area }}
36
+ {% endfor %}
37
+
38
+ ---
39
+
40
+ ## Review Response Template
41
+
42
+ **Reviewer:** {{ reviewer }}
43
+ **Date:** {{ review_date }}
44
+
45
+ ### Overall Assessment
46
+ - [ ] **APPROVED** - Ready to merge
47
+ - [ ] **NEEDS_REVISION** - Changes required
48
+ - [ ] **DISCUSSION_NEEDED** - Need to discuss
49
+
50
+ ### Feedback
51
+
52
+ **What's Good:**
53
+ {% for good in feedback_good %}
54
+ - {{ good }}
55
+ {% endfor %}
56
+
57
+ **Suggestions:**
58
+ {% for suggestion in feedback_suggestions %}
59
+ - {{ suggestion }}
60
+ {% endfor %}
61
+
62
+ **Required Changes (if any):**
63
+ {% for change in required_changes %}
64
+ - [ ] {{ change }}
65
+ {% endfor %}
66
+
67
+ ### Next Steps
68
+ {{ next_steps }}
@@ -0,0 +1,69 @@
1
+ # Task Delegation Template
2
+
3
+ **From:** {{ sender }} ({{ sender_role }})
4
+ **To:** {{ recipient }} ({{ recipient_role }})
5
+ **Date:** {{ date }}
6
+ **Task ID:** {{ task_id }}
7
+
8
+ ---
9
+
10
+ ## Task: {{ title }}
11
+
12
+ ### Description
13
+ {{ description }}
14
+
15
+ ### Requirements
16
+ {% for req in requirements %}
17
+ - [ ] {{ req }}
18
+ {% endfor %}
19
+
20
+ ### Acceptance Criteria
21
+ {% for criteria in acceptance_criteria %}
22
+ - [ ] {{ criteria }}
23
+ {% endfor %}
24
+
25
+ ### Priority
26
+ {{ priority }}
27
+
28
+ ### Context
29
+ {{ context }}
30
+
31
+ ### Expected Deliverables
32
+ {% for deliverable in deliverables %}
33
+ - [ ] {{ deliverable }}
34
+ {% endfor %}
35
+
36
+ ---
37
+
38
+ ## Expected Response
39
+
40
+ Please respond with one of:
41
+
42
+ - ✅ **ACCEPT** - I can complete this task
43
+ - ❌ **REJECT** - I cannot complete this task (explain why)
44
+ - ❓ **CLARIFY** - I need more information
45
+
46
+ Once accepted, please provide:
47
+ 1. Your approach/plan
48
+ 2. Estimated time to complete
49
+ 3. Any dependencies or blockers
50
+
51
+ ---
52
+
53
+ ## Communication
54
+
55
+ Use the following to update status:
56
+
57
+ ```bash
58
+ # Start work
59
+ interagent task update {{ task_id }} --status in_progress
60
+
61
+ # Add note
62
+ interagent task update {{ task_id }} --note "Making progress..."
63
+
64
+ # Complete
65
+ interagent task update {{ task_id }} --status completed
66
+
67
+ # Request review
68
+ interagent msg send --to {{ sender }} --subject "Review Request" --message "Task complete!"
69
+ ```
@@ -0,0 +1,70 @@
1
+ # Template Update Task
2
+
3
+ **Date:** {date}
4
+ **Assigned to:** {agent}
5
+ **Focus:** {focus}
6
+
7
+ ## Your Task
8
+
9
+ You are responsible for keeping the project kickoff template current with the
10
+ latest AI coding best practices and tool capabilities.
11
+
12
+ ## Steps
13
+
14
+ ### 1. Research (search the web)
15
+
16
+ Search for the following topics - focus on {year}:
17
+
18
+ - "Claude Code sub-agents best practices {year}"
19
+ - "Kimi Code agents capabilities {year}"
20
+ - "AI coding workflow multi-agent patterns {year}"
21
+ - "Claude Code hooks slash commands new features {year}"
22
+ - Recent Anthropic developer blog posts about Claude Code
23
+ - Recent Moonshot AI / Kimi developer docs about Kimi Code agents
24
+
25
+ ### 2. Review the Current Template
26
+
27
+ Read the file at: `{template_path}`
28
+
29
+ Pay attention to:
30
+ - Any commands that reference specific versions or flags
31
+ - Steps that describe Claude Code or Kimi Code behavior
32
+ - The cross-agent sub-agent prompting section
33
+ - The InterAgent CLI commands mentioned
34
+
35
+ ### 3. Identify Improvements
36
+
37
+ Look for:
38
+ - New sub-agent or tool capabilities in Claude Code or Kimi Code
39
+ - Outdated commands, flags, or workflows
40
+ - Better multi-agent collaboration patterns
41
+ - Improved prompt structures
42
+ - New `interagent` CLI commands that should be documented
43
+ - New cross-agent prompting techniques discovered since the last update
44
+
45
+ ### 4. Update the Template
46
+
47
+ Edit the file at `{template_path}` with your improvements.
48
+ Rules:
49
+ - Make targeted, minimal changes - do not restructure working sections
50
+ - Update version years (e.g. "2025/2026") if appropriate
51
+ - Add new capabilities where relevant
52
+ - Remove references to features that no longer exist
53
+
54
+ ### 5. Write Change Summary
55
+
56
+ Create (or overwrite) `TEMPLATE_UPDATE.md` in the same directory as the
57
+ template with:
58
+ - Date of update
59
+ - List of changes made and the reason for each
60
+ - Sources and links you referenced
61
+ - Any open questions or areas that need the user's decision
62
+
63
+ ## Focus Area for This Run
64
+
65
+ {focus}
66
+
67
+ ## Expected Output
68
+
69
+ 1. Updated `{template_path}`
70
+ 2. New `TEMPLATE_UPDATE.md` in the same directory as the template
interagent/utils.py ADDED
@@ -0,0 +1,90 @@
1
+ """Utility functions for InterAgent."""
2
+
3
+ import json
4
+ import uuid
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+ from typing import Any, Dict, Optional
8
+
9
+ from .constants import (
10
+ INTERAGENT_DIR,
11
+ AGENTS_DIR,
12
+ TASKS_ACTIVE_DIR,
13
+ TASKS_COMPLETED_DIR,
14
+ MESSAGES_PENDING_DIR,
15
+ MESSAGES_ARCHIVE_DIR,
16
+ SHARED_DIR,
17
+ )
18
+
19
+
20
+ def ensure_dirs() -> None:
21
+ """Ensure all required directories exist."""
22
+ for d in [
23
+ INTERAGENT_DIR,
24
+ AGENTS_DIR,
25
+ TASKS_ACTIVE_DIR,
26
+ TASKS_COMPLETED_DIR,
27
+ MESSAGES_PENDING_DIR,
28
+ MESSAGES_ARCHIVE_DIR,
29
+ SHARED_DIR,
30
+ ]:
31
+ d.mkdir(parents=True, exist_ok=True)
32
+
33
+
34
+ def generate_id(prefix: str = "id") -> str:
35
+ """Generate a unique ID with prefix."""
36
+ return f"{prefix}-{str(uuid.uuid4())[:6]}"
37
+
38
+
39
+ def now_iso() -> str:
40
+ """Get current timestamp in ISO format."""
41
+ return datetime.now().isoformat()
42
+
43
+
44
+ def load_json(filepath: Path) -> Optional[Dict[str, Any]]:
45
+ """Load JSON from file."""
46
+ if not filepath.exists():
47
+ return None
48
+ try:
49
+ with open(filepath, "r", encoding="utf-8") as f:
50
+ return json.load(f)
51
+ except (json.JSONDecodeError, IOError):
52
+ return None
53
+
54
+
55
+ def save_json(filepath: Path, data: Dict[str, Any]) -> bool:
56
+ """Save data to JSON file."""
57
+ try:
58
+ filepath.parent.mkdir(parents=True, exist_ok=True)
59
+ with open(filepath, "w", encoding="utf-8") as f:
60
+ json.dump(data, f, indent=2)
61
+ return True
62
+ except IOError:
63
+ return False
64
+
65
+
66
+ def list_json_files(directory: Path) -> list:
67
+ """List all JSON files in directory."""
68
+ if not directory.exists():
69
+ return []
70
+ return sorted(directory.glob("*.json"))
71
+
72
+
73
+ def print_success(message: str) -> None:
74
+ """Print success message."""
75
+ print(f"[OK] {message}")
76
+
77
+
78
+ def print_warning(message: str) -> None:
79
+ """Print warning message."""
80
+ print(f"[WARN] {message}")
81
+
82
+
83
+ def print_error(message: str) -> None:
84
+ """Print error message."""
85
+ print(f"[ERR] {message}")
86
+
87
+
88
+ def print_info(message: str) -> None:
89
+ """Print info message."""
90
+ print(f"[INFO] {message}")
@@ -0,0 +1,156 @@
1
+ """JSON schema validation for InterAgent."""
2
+
3
+ from typing import Any, Dict, List, Tuple
4
+ from .constants import TASK_STATUSES, MESSAGE_TYPES, PRIORITIES, VALID_AGENTS
5
+
6
+
7
+ class ValidationError(Exception):
8
+ """Raised when validation fails."""
9
+ pass
10
+
11
+
12
+ def validate_task(data: Dict[str, Any]) -> Tuple[bool, List[str]]:
13
+ """Validate task data.
14
+
15
+ Returns:
16
+ (is_valid, list_of_errors)
17
+ """
18
+ errors = []
19
+
20
+ # Required fields
21
+ required = ["id", "title", "status", "created"]
22
+ for field in required:
23
+ if field not in data:
24
+ errors.append(f"Missing required field: {field}")
25
+
26
+ # Validate status
27
+ if "status" in data and data["status"] not in TASK_STATUSES:
28
+ errors.append(f"Invalid status: {data['status']}")
29
+
30
+ # Validate priority
31
+ if "priority" in data and data["priority"] not in PRIORITIES:
32
+ errors.append(f"Invalid priority: {data['priority']}")
33
+
34
+ # Validate assignee/assigner
35
+ if "assignee" in data and data["assignee"]:
36
+ if data["assignee"] not in VALID_AGENTS:
37
+ errors.append(f"Invalid assignee: {data['assignee']}")
38
+
39
+ if "assigner" in data and data["assigner"]:
40
+ if data["assigner"] not in VALID_AGENTS:
41
+ errors.append(f"Invalid assigner: {data['assigner']}")
42
+
43
+ # Validate types
44
+ if "title" in data and not isinstance(data["title"], str):
45
+ errors.append("Title must be a string")
46
+
47
+ if "description" in data and not isinstance(data["description"], str):
48
+ errors.append("Description must be a string")
49
+
50
+ if "requirements" in data and not isinstance(data["requirements"], list):
51
+ errors.append("Requirements must be a list")
52
+
53
+ return len(errors) == 0, errors
54
+
55
+
56
+ def validate_message(data: Dict[str, Any]) -> Tuple[bool, List[str]]:
57
+ """Validate message data.
58
+
59
+ Returns:
60
+ (is_valid, list_of_errors)
61
+ """
62
+ errors = []
63
+
64
+ # Required fields
65
+ required = ["id", "from", "to", "content", "timestamp"]
66
+ for field in required:
67
+ if field not in data:
68
+ errors.append(f"Missing required field: {field}")
69
+
70
+ # Validate sender/recipient
71
+ if "from" in data and data["from"] not in VALID_AGENTS:
72
+ errors.append(f"Invalid sender: {data['from']}")
73
+
74
+ if "to" in data and data["to"] not in VALID_AGENTS:
75
+ errors.append(f"Invalid recipient: {data['to']}")
76
+
77
+ # Validate type
78
+ if "type" in data and data["type"] not in MESSAGE_TYPES:
79
+ errors.append(f"Invalid message type: {data['type']}")
80
+
81
+ # Validate types
82
+ if "content" in data and not isinstance(data["content"], str):
83
+ errors.append("Content must be a string")
84
+
85
+ if "subject" in data and not isinstance(data["subject"], str):
86
+ errors.append("Subject must be a string")
87
+
88
+ return len(errors) == 0, errors
89
+
90
+
91
+ def validate_session(data: Dict[str, Any]) -> Tuple[bool, List[str]]:
92
+ """Validate session data."""
93
+ errors = []
94
+
95
+ required = ["id", "name", "created", "mode", "principal"]
96
+ for field in required:
97
+ if field not in data:
98
+ errors.append(f"Missing required field: {field}")
99
+
100
+ if "mode" in data and data["mode"] not in ["hierarchical", "peer", "review"]:
101
+ errors.append(f"Invalid mode: {data['mode']}")
102
+
103
+ if "principal" in data and data["principal"] not in VALID_AGENTS:
104
+ errors.append(f"Invalid principal: {data['principal']}")
105
+
106
+ return len(errors) == 0, errors
107
+
108
+
109
+ def sanitize_string(value: Any, max_length: int = 1000) -> str:
110
+ """Sanitize a string value."""
111
+ if not isinstance(value, str):
112
+ return str(value)[:max_length]
113
+ return value[:max_length]
114
+
115
+
116
+ def sanitize_task_data(data: Dict[str, Any]) -> Dict[str, Any]:
117
+ """Sanitize task data before saving."""
118
+ sanitized = {}
119
+
120
+ # Copy allowed fields with sanitization
121
+ if "id" in data:
122
+ sanitized["id"] = sanitize_string(data["id"], 50)
123
+ if "title" in data:
124
+ sanitized["title"] = sanitize_string(data["title"], 200)
125
+ if "description" in data:
126
+ sanitized["description"] = sanitize_string(data["description"], 5000)
127
+ if "status" in data and data["status"] in TASK_STATUSES:
128
+ sanitized["status"] = data["status"]
129
+ if "priority" in data and data["priority"] in PRIORITIES:
130
+ sanitized["priority"] = data["priority"]
131
+ if "assignee" in data and data["assignee"] in VALID_AGENTS:
132
+ sanitized["assignee"] = data["assignee"]
133
+ if "assigner" in data and data["assigner"] in VALID_AGENTS:
134
+ sanitized["assigner"] = data["assigner"]
135
+
136
+ # Lists
137
+ if "requirements" in data and isinstance(data["requirements"], list):
138
+ sanitized["requirements"] = [
139
+ sanitize_string(r, 500) for r in data["requirements"]
140
+ ]
141
+ if "acceptance_criteria" in data and isinstance(data["acceptance_criteria"], list):
142
+ sanitized["acceptance_criteria"] = [
143
+ sanitize_string(c, 500) for c in data["acceptance_criteria"]
144
+ ]
145
+ if "deliverables" in data and isinstance(data["deliverables"], list):
146
+ sanitized["deliverables"] = [
147
+ sanitize_string(d, 500) for d in data["deliverables"]
148
+ ]
149
+
150
+ # Timestamps
151
+ if "created" in data:
152
+ sanitized["created"] = data["created"]
153
+ if "updated" in data:
154
+ sanitized["updated"] = data["updated"]
155
+
156
+ return sanitized