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/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}")
|
interagent/validator.py
ADDED
|
@@ -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
|