maestro-agents 0.1.0__tar.gz
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.
- maestro_agents-0.1.0/.gitignore +46 -0
- maestro_agents-0.1.0/PKG-INFO +11 -0
- maestro_agents-0.1.0/pyproject.toml +24 -0
- maestro_agents-0.1.0/src/maestro_agents/__init__.py +8 -0
- maestro_agents-0.1.0/src/maestro_agents/animation.py +79 -0
- maestro_agents-0.1.0/src/maestro_agents/backend.py +33 -0
- maestro_agents-0.1.0/src/maestro_agents/base.py +168 -0
- maestro_agents-0.1.0/src/maestro_agents/contract_validator.py +94 -0
- maestro_agents-0.1.0/src/maestro_agents/deps.py +17 -0
- maestro_agents-0.1.0/src/maestro_agents/frontend.py +33 -0
- maestro_agents-0.1.0/src/maestro_agents/permissions.py +24 -0
- maestro_agents-0.1.0/src/maestro_agents/planning.py +40 -0
- maestro_agents-0.1.0/src/maestro_agents/py.typed +0 -0
- maestro_agents-0.1.0/src/maestro_agents/review.py +31 -0
- maestro_agents-0.1.0/src/maestro_agents/test_agent.py +39 -0
- maestro_agents-0.1.0/src/maestro_agents/tools.py +266 -0
- maestro_agents-0.1.0/tests/__init__.py +0 -0
- maestro_agents-0.1.0/tests/conftest.py +16 -0
- maestro_agents-0.1.0/tests/test_agent_base.py +63 -0
- maestro_agents-0.1.0/tests/test_backend_agent.py +23 -0
- maestro_agents-0.1.0/tests/test_contract_validator.py +49 -0
- maestro_agents-0.1.0/tests/test_planning_agent.py +23 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.egg-info/
|
|
6
|
+
*.egg
|
|
7
|
+
dist/
|
|
8
|
+
build/
|
|
9
|
+
|
|
10
|
+
# Virtual environments
|
|
11
|
+
.venv/
|
|
12
|
+
|
|
13
|
+
# Testing
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
htmlcov/
|
|
16
|
+
.coverage
|
|
17
|
+
coverage.xml
|
|
18
|
+
|
|
19
|
+
# Type checking
|
|
20
|
+
.mypy_cache/
|
|
21
|
+
.pyright/
|
|
22
|
+
|
|
23
|
+
# Ruff
|
|
24
|
+
.ruff_cache/
|
|
25
|
+
|
|
26
|
+
# IDE
|
|
27
|
+
.idea/
|
|
28
|
+
*.swp
|
|
29
|
+
*.swo
|
|
30
|
+
*~
|
|
31
|
+
|
|
32
|
+
# OS
|
|
33
|
+
.DS_Store
|
|
34
|
+
Thumbs.db
|
|
35
|
+
|
|
36
|
+
# uv
|
|
37
|
+
uv.lock
|
|
38
|
+
|
|
39
|
+
# Project
|
|
40
|
+
*.db
|
|
41
|
+
*.sqlite3
|
|
42
|
+
.maestro/
|
|
43
|
+
src/api/
|
|
44
|
+
|
|
45
|
+
# env
|
|
46
|
+
.env
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: maestro-agents
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Role-specific LLM agents with SSOT integration
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Requires-Dist: litellm>=1.50
|
|
7
|
+
Requires-Dist: maestro-harness
|
|
8
|
+
Requires-Dist: maestro-ssot
|
|
9
|
+
Requires-Dist: pydantic-ai>=0.5
|
|
10
|
+
Requires-Dist: pydantic>=2.10
|
|
11
|
+
Requires-Dist: rich>=13.0
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "maestro-agents"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Role-specific LLM agents with SSOT integration"
|
|
5
|
+
requires-python = ">=3.12"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"pydantic>=2.10",
|
|
8
|
+
"pydantic-ai>=0.5",
|
|
9
|
+
"litellm>=1.50",
|
|
10
|
+
"rich>=13.0",
|
|
11
|
+
"maestro-ssot",
|
|
12
|
+
"maestro-harness",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[tool.uv.sources]
|
|
16
|
+
maestro-ssot = { workspace = true }
|
|
17
|
+
maestro-harness = { workspace = true }
|
|
18
|
+
|
|
19
|
+
[build-system]
|
|
20
|
+
requires = ["hatchling"]
|
|
21
|
+
build-backend = "hatchling.build"
|
|
22
|
+
|
|
23
|
+
[tool.hatch.build.targets.wheel]
|
|
24
|
+
packages = ["src/maestro_agents"]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Rich-based thinking animation and typewriter output for agent execution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.live import Live
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ThinkingAnimation:
|
|
14
|
+
"""Display a thinking spinner during LLM calls, then typewriter output.
|
|
15
|
+
|
|
16
|
+
Example::
|
|
17
|
+
anim = ThinkingAnimation("planner-1 planning")
|
|
18
|
+
anim.start()
|
|
19
|
+
# ... await LLM call ...
|
|
20
|
+
anim.stop()
|
|
21
|
+
anim.typewrite(result_text, prefix="[cyan]planner-1[/] ")
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, label: str = "thinking", console: Console | None = None) -> None:
|
|
25
|
+
self.label = label
|
|
26
|
+
self._console = console
|
|
27
|
+
self._live: Live | None = None
|
|
28
|
+
|
|
29
|
+
def _get_console(self) -> Console:
|
|
30
|
+
if self._console is not None:
|
|
31
|
+
return self._console
|
|
32
|
+
from rich.console import Console
|
|
33
|
+
|
|
34
|
+
return Console()
|
|
35
|
+
|
|
36
|
+
def start(self) -> None:
|
|
37
|
+
"""Start the thinking spinner."""
|
|
38
|
+
from rich.live import Live
|
|
39
|
+
from rich.spinner import Spinner
|
|
40
|
+
|
|
41
|
+
spinner = Spinner("dots", text=f"[dim]{self.label}[/]")
|
|
42
|
+
self._live = Live(
|
|
43
|
+
spinner, refresh_per_second=8, transient=True, console=self._get_console()
|
|
44
|
+
)
|
|
45
|
+
self._live.start()
|
|
46
|
+
|
|
47
|
+
def stop(self) -> None:
|
|
48
|
+
"""Stop the spinner."""
|
|
49
|
+
if self._live is not None:
|
|
50
|
+
self._live.stop()
|
|
51
|
+
self._live = None
|
|
52
|
+
|
|
53
|
+
def typewrite(
|
|
54
|
+
self,
|
|
55
|
+
text: str,
|
|
56
|
+
prefix: str = "",
|
|
57
|
+
delay: float = 0.008,
|
|
58
|
+
max_chars: int = 2000,
|
|
59
|
+
) -> None:
|
|
60
|
+
"""Print text with a typewriter effect.
|
|
61
|
+
|
|
62
|
+
For long text (>max_chars), speeds up to avoid excessive wait time.
|
|
63
|
+
"""
|
|
64
|
+
console = self._get_console()
|
|
65
|
+
text = text.strip()
|
|
66
|
+
if not text:
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
# Speed up for very long text
|
|
70
|
+
if len(text) > max_chars:
|
|
71
|
+
delay = max(0.001, delay * max_chars / len(text))
|
|
72
|
+
|
|
73
|
+
if prefix:
|
|
74
|
+
console.print(prefix, end="")
|
|
75
|
+
|
|
76
|
+
for char in text:
|
|
77
|
+
console.print(char, end="")
|
|
78
|
+
time.sleep(delay)
|
|
79
|
+
console.print() # newline
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""BackendAgent: implements backend code consuming contracts from SSOT."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from maestro_agents.base import MaestroAgent
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BackendAgent(MaestroAgent):
|
|
9
|
+
"""Implements backend modules by reading contracts and writing code.
|
|
10
|
+
|
|
11
|
+
Uses SSOT tools to:
|
|
12
|
+
1. Claim a requirement assigned to the backend
|
|
13
|
+
2. Read the relevant contracts
|
|
14
|
+
3. Write implementation code via the harness sandbox
|
|
15
|
+
4. Update requirement status to DONE when complete
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
role = "backend"
|
|
19
|
+
|
|
20
|
+
def system_prompt(self) -> str:
|
|
21
|
+
return (
|
|
22
|
+
"You are the Backend Agent in the MAESTRO multi-agent system. "
|
|
23
|
+
"Your job is to implement backend code based on requirements and "
|
|
24
|
+
"API contracts from the SSOT. "
|
|
25
|
+
"Read the assigned requirement and its contracts, then write "
|
|
26
|
+
"clean, well-structured Python code to actual files. "
|
|
27
|
+
"You have file_read, file_write, and shell_exec tools to interact "
|
|
28
|
+
"with the codebase. Use file_write to create/modify source files, "
|
|
29
|
+
"shell_exec to run tests and verify your implementation. "
|
|
30
|
+
"Do NOT use 'cd' in shell commands — the sandbox cwd is already "
|
|
31
|
+
"the project root. Use the SSOT tools to read contracts and requirements, "
|
|
32
|
+
"and update the requirement status when done."
|
|
33
|
+
)
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""MaestroAgent base class wrapping PydanticAI with SSOT + Harness injection."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
from pydantic_ai import Agent
|
|
10
|
+
from pydantic_ai.settings import ModelSettings
|
|
11
|
+
|
|
12
|
+
from maestro_harness.harness import Harness
|
|
13
|
+
from maestro_ssot.hub import SSOTHub
|
|
14
|
+
|
|
15
|
+
from .animation import ThinkingAnimation
|
|
16
|
+
from .deps import AgentDeps
|
|
17
|
+
from .permissions import AgentPermissions
|
|
18
|
+
from .tools import register_tools
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from pydantic_ai import AgentEventStream
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AgentResult(BaseModel):
|
|
25
|
+
"""Result of a single agent execution."""
|
|
26
|
+
|
|
27
|
+
output: str
|
|
28
|
+
success: bool = True
|
|
29
|
+
error: str | None = None
|
|
30
|
+
input_tokens: int = 0
|
|
31
|
+
output_tokens: int = 0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class MaestroAgent:
|
|
35
|
+
"""Base class for all MAESTRO role agents.
|
|
36
|
+
|
|
37
|
+
Wraps a PydanticAI Agent with SSOT and Harness dependencies.
|
|
38
|
+
Subclasses override ``system_prompt`` and ``role`` to specialize behavior.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
role: str = "generic"
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
agent_id: str,
|
|
46
|
+
hub: SSOTHub,
|
|
47
|
+
harness: Harness,
|
|
48
|
+
model: str | object = "test",
|
|
49
|
+
permissions: AgentPermissions | None = None,
|
|
50
|
+
thinking: bool | None = None,
|
|
51
|
+
) -> None:
|
|
52
|
+
self.agent_id = agent_id
|
|
53
|
+
self.hub = hub
|
|
54
|
+
self.harness = harness
|
|
55
|
+
|
|
56
|
+
deps = AgentDeps(
|
|
57
|
+
hub=hub,
|
|
58
|
+
harness=harness,
|
|
59
|
+
agent_id=agent_id,
|
|
60
|
+
role=self.role,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
model_settings: ModelSettings | None = None
|
|
64
|
+
if thinking is not None:
|
|
65
|
+
model_settings = ModelSettings(thinking=thinking)
|
|
66
|
+
|
|
67
|
+
prompt = self.system_prompt() + "\n\n" + self._policy_context()
|
|
68
|
+
self._agent = Agent(
|
|
69
|
+
model, # type: ignore[arg-type]
|
|
70
|
+
deps_type=AgentDeps,
|
|
71
|
+
output_type=str,
|
|
72
|
+
system_prompt=prompt,
|
|
73
|
+
model_settings=model_settings,
|
|
74
|
+
)
|
|
75
|
+
register_tools(self._agent, permissions)
|
|
76
|
+
self._deps = deps
|
|
77
|
+
|
|
78
|
+
def _policy_context(self) -> str:
|
|
79
|
+
"""Return a formatted description of the agent's Harness policy."""
|
|
80
|
+
lines: list[str] = []
|
|
81
|
+
policy = self.harness.get_agent_policy(self.agent_id)
|
|
82
|
+
if policy and policy.filesystem and policy.filesystem.allow:
|
|
83
|
+
lines.append(f"Allowed file paths: {', '.join(policy.filesystem.allow)}")
|
|
84
|
+
if policy and policy.filesystem and policy.filesystem.deny:
|
|
85
|
+
lines.append(f"Denied file paths: {', '.join(policy.filesystem.deny)}")
|
|
86
|
+
if policy and policy.commands and policy.commands.allow:
|
|
87
|
+
lines.append(f"Allowed shell commands: {', '.join(policy.commands.allow)}")
|
|
88
|
+
if policy and policy.commands and policy.commands.deny:
|
|
89
|
+
lines.append(f"Denied shell commands: {', '.join(policy.commands.deny)}")
|
|
90
|
+
return "\n".join(lines)
|
|
91
|
+
|
|
92
|
+
def system_prompt(self) -> str:
|
|
93
|
+
return "You are a MAESTRO software engineering agent."
|
|
94
|
+
|
|
95
|
+
async def run(
|
|
96
|
+
self,
|
|
97
|
+
prompt: str,
|
|
98
|
+
model: str | object | None = None,
|
|
99
|
+
animation: ThinkingAnimation | None = None,
|
|
100
|
+
timeout: float | None = None,
|
|
101
|
+
) -> AgentResult:
|
|
102
|
+
"""Execute the agent with optional thinking animation and timeout."""
|
|
103
|
+
if animation is not None:
|
|
104
|
+
animation.start()
|
|
105
|
+
try:
|
|
106
|
+
run_kwargs: dict[str, object] = {}
|
|
107
|
+
if model is not None:
|
|
108
|
+
run_kwargs["model"] = model
|
|
109
|
+
|
|
110
|
+
coro = self._agent.run(
|
|
111
|
+
prompt, deps=self._deps, **run_kwargs # type: ignore[call-overload]
|
|
112
|
+
)
|
|
113
|
+
if timeout is not None and timeout > 0:
|
|
114
|
+
coro = asyncio.wait_for(coro, timeout=timeout)
|
|
115
|
+
result = await coro
|
|
116
|
+
output = str(result.output)
|
|
117
|
+
|
|
118
|
+
# Extract token usage if available
|
|
119
|
+
input_tokens = 0
|
|
120
|
+
output_tokens = 0
|
|
121
|
+
try:
|
|
122
|
+
usage = result.usage
|
|
123
|
+
if usage is not None:
|
|
124
|
+
input_tokens = getattr(usage, "request_tokens", 0) or 0
|
|
125
|
+
output_tokens = getattr(usage, "response_tokens", 0) or 0
|
|
126
|
+
except Exception:
|
|
127
|
+
pass
|
|
128
|
+
|
|
129
|
+
if animation is not None:
|
|
130
|
+
animation.stop()
|
|
131
|
+
animation.typewrite(output, prefix=f"[cyan]{self.agent_id}[/] ")
|
|
132
|
+
|
|
133
|
+
return AgentResult(
|
|
134
|
+
output=output,
|
|
135
|
+
input_tokens=input_tokens,
|
|
136
|
+
output_tokens=output_tokens,
|
|
137
|
+
)
|
|
138
|
+
except TimeoutError:
|
|
139
|
+
if animation is not None:
|
|
140
|
+
animation.stop()
|
|
141
|
+
return AgentResult(
|
|
142
|
+
output="",
|
|
143
|
+
success=False,
|
|
144
|
+
error=f"Agent timed out after {timeout}s",
|
|
145
|
+
)
|
|
146
|
+
except Exception as e:
|
|
147
|
+
if animation is not None:
|
|
148
|
+
animation.stop()
|
|
149
|
+
return AgentResult(output="", success=False, error=str(e))
|
|
150
|
+
|
|
151
|
+
def run_stream(
|
|
152
|
+
self,
|
|
153
|
+
prompt: str,
|
|
154
|
+
model: str | object | None = None,
|
|
155
|
+
) -> AgentEventStream[Any] | None:
|
|
156
|
+
"""Return an event stream for real-time monitoring.
|
|
157
|
+
|
|
158
|
+
Returns ``None`` if the underlying model does not support streaming
|
|
159
|
+
(e.g. TestModel).
|
|
160
|
+
"""
|
|
161
|
+
run_kwargs: dict[str, object] = {"deps": self._deps}
|
|
162
|
+
if model is not None:
|
|
163
|
+
run_kwargs["model"] = model
|
|
164
|
+
try:
|
|
165
|
+
return self._agent.run_stream_events(prompt, **run_kwargs) # type: ignore[call-overload, no-any-return]
|
|
166
|
+
except Exception:
|
|
167
|
+
# TestModel and some mock environments don't support streaming
|
|
168
|
+
return None
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Non-LLM contract validator: rule engine for consumer-producer consistency."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
from maestro_ssot.hub import SSOTHub
|
|
8
|
+
from maestro_ssot.models import ContractStatus
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Violation:
|
|
13
|
+
contract_id: str
|
|
14
|
+
message: str
|
|
15
|
+
severity: str = "error" # error | warning
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class ValidationResult:
|
|
20
|
+
valid: bool
|
|
21
|
+
violations: list[Violation] = field(default_factory=list)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def validate_contracts(hub: SSOTHub) -> ValidationResult:
|
|
25
|
+
"""Check all contracts for consistency violations.
|
|
26
|
+
|
|
27
|
+
Rules:
|
|
28
|
+
1. Stable/Deprecated contracts must have a non-empty interface_schema.
|
|
29
|
+
2. Consumers of STABLE contracts must not reference DRAFT contracts.
|
|
30
|
+
3. Contract version should be >= 1.
|
|
31
|
+
4. All listed consumers should have known activity.
|
|
32
|
+
"""
|
|
33
|
+
violations: list[Violation] = []
|
|
34
|
+
contracts = hub._persistence.list_contracts()
|
|
35
|
+
|
|
36
|
+
for contract in contracts:
|
|
37
|
+
# Rule 1: Stable/Deprecated contracts must have interface schema
|
|
38
|
+
if (
|
|
39
|
+
contract.status in (ContractStatus.STABLE, ContractStatus.DEPRECATED)
|
|
40
|
+
and not contract.interface_schema
|
|
41
|
+
):
|
|
42
|
+
violations.append(
|
|
43
|
+
Violation(
|
|
44
|
+
contract_id=contract.id,
|
|
45
|
+
message=f"Contract '{contract.name}' is {contract.status.value} "
|
|
46
|
+
f"but has empty interface_schema",
|
|
47
|
+
severity="error",
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Rule 2: Check consumer references
|
|
52
|
+
for consumer in contract.consumers:
|
|
53
|
+
consumer_known = any(c.owner == consumer for c in contracts)
|
|
54
|
+
if not consumer_known:
|
|
55
|
+
log_entries = hub.query_log(agent_id=consumer, limit=1)
|
|
56
|
+
if not log_entries:
|
|
57
|
+
violations.append(
|
|
58
|
+
Violation(
|
|
59
|
+
contract_id=contract.id,
|
|
60
|
+
message=f"Consumer '{consumer}' of contract '{contract.name}' "
|
|
61
|
+
f"has no known activity",
|
|
62
|
+
severity="warning",
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Rule 3: Version check
|
|
67
|
+
if contract.version < 1:
|
|
68
|
+
violations.append(
|
|
69
|
+
Violation(
|
|
70
|
+
contract_id=contract.id,
|
|
71
|
+
message=f"Contract '{contract.name}' has invalid version {contract.version}",
|
|
72
|
+
severity="error",
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Rule 4: DRAFT contracts should not have STABLE consumers
|
|
77
|
+
if contract.status == ContractStatus.DRAFT:
|
|
78
|
+
for other in contracts:
|
|
79
|
+
if (
|
|
80
|
+
other.id != contract.id
|
|
81
|
+
and contract.id in other.consumers
|
|
82
|
+
and other.status == ContractStatus.STABLE
|
|
83
|
+
):
|
|
84
|
+
violations.append(
|
|
85
|
+
Violation(
|
|
86
|
+
contract_id=other.id,
|
|
87
|
+
message=f"STABLE contract '{other.name}' depends on "
|
|
88
|
+
f"DRAFT contract '{contract.name}'",
|
|
89
|
+
severity="error",
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
errors = [v for v in violations if v.severity == "error"]
|
|
94
|
+
return ValidationResult(valid=len(errors) == 0, violations=violations)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Agent dependency types shared between base and tools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
from maestro_harness.harness import Harness
|
|
8
|
+
from maestro_ssot.hub import SSOTHub
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class AgentDeps:
|
|
13
|
+
hub: SSOTHub
|
|
14
|
+
harness: Harness
|
|
15
|
+
agent_id: str
|
|
16
|
+
role: str
|
|
17
|
+
memory: dict[str, str] = field(default_factory=dict)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""FrontendAgent: implements UI components consuming API contracts from SSOT."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from maestro_agents.base import MaestroAgent
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FrontendAgent(MaestroAgent):
|
|
9
|
+
"""Implements frontend modules by reading contracts and writing client code.
|
|
10
|
+
|
|
11
|
+
Uses SSOT tools to:
|
|
12
|
+
1. Claim a requirement assigned to the frontend
|
|
13
|
+
2. Read the relevant API contracts
|
|
14
|
+
3. Write implementation code (React, HTML, CSS, etc.) via the harness sandbox
|
|
15
|
+
4. Update requirement status to DONE when complete
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
role = "frontend"
|
|
19
|
+
|
|
20
|
+
def system_prompt(self) -> str:
|
|
21
|
+
return (
|
|
22
|
+
"You are the Frontend Agent in the MAESTRO multi-agent system. "
|
|
23
|
+
"Your job is to implement frontend code based on requirements and "
|
|
24
|
+
"API contracts from the SSOT. "
|
|
25
|
+
"Read the assigned requirement and its contracts, then write "
|
|
26
|
+
"clean, well-structured client-side code (React, HTML, CSS, JavaScript) "
|
|
27
|
+
"to actual files. You have file_read, file_write, and shell_exec tools "
|
|
28
|
+
"to interact with the codebase. Use file_write to create/modify source files, "
|
|
29
|
+
"shell_exec to run build/lint commands and verify your implementation. "
|
|
30
|
+
"Do NOT use 'cd' in shell commands — the sandbox cwd is already "
|
|
31
|
+
"the project root. Use the SSOT tools to read contracts and requirements, "
|
|
32
|
+
"and update the requirement status when done."
|
|
33
|
+
)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Agent permission model for SSOT tool access control."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AgentPermissions(BaseModel):
|
|
9
|
+
"""SSOT permissions for an agent — controls which tools are registered.
|
|
10
|
+
|
|
11
|
+
All permissions default to True for backward compatibility.
|
|
12
|
+
Set individual fields to False to restrict an agent's capabilities.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
read_requirements: bool = True
|
|
16
|
+
write_requirements: bool = True
|
|
17
|
+
read_contracts: bool = True
|
|
18
|
+
write_contracts: bool = True
|
|
19
|
+
read_memory: bool = True
|
|
20
|
+
write_memory: bool = True
|
|
21
|
+
read_log: bool = True
|
|
22
|
+
read_files: bool = True
|
|
23
|
+
write_files: bool = True
|
|
24
|
+
execute_shell: bool = True
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""PlanningAgent: decomposes requirements into a structured task tree."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from maestro_agents.base import MaestroAgent
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PlanningAgent(MaestroAgent):
|
|
9
|
+
"""Decomposes high-level requirements into sub-tasks and registers contracts.
|
|
10
|
+
|
|
11
|
+
Uses SSOT tools to:
|
|
12
|
+
1. Read the root requirement
|
|
13
|
+
2. Break it into sub-requirements with dependencies
|
|
14
|
+
3. Register interface contracts between modules
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
role = "planner"
|
|
18
|
+
|
|
19
|
+
def system_prompt(self) -> str:
|
|
20
|
+
return (
|
|
21
|
+
"You are the Planning Agent in the MAESTRO multi-agent system. "
|
|
22
|
+
"Your job is to decompose a high-level software requirement into "
|
|
23
|
+
"a tree of sub-requirements, each with clear acceptance criteria "
|
|
24
|
+
"and dependencies. You should also register API contracts between "
|
|
25
|
+
"modules that need to communicate. "
|
|
26
|
+
"Use the SSOT tools to add requirements and register contracts. "
|
|
27
|
+
"Be specific: each sub-requirement should be small enough for a "
|
|
28
|
+
"single agent to implement in one iteration.\n\n"
|
|
29
|
+
"CRITICAL RULES:\n"
|
|
30
|
+
"1. Every sub-requirement MUST specify a valid parent_id. "
|
|
31
|
+
" Pass the root requirement ID as parent_id for all sub-tasks.\n"
|
|
32
|
+
"2. Do NOT create duplicate tasks. Check existing requirements first.\n"
|
|
33
|
+
"3. Limit decomposition to 5-7 sub-tasks maximum. "
|
|
34
|
+
" If the requirement is simple, create fewer tasks or even just one.\n"
|
|
35
|
+
"4. Dependencies must reference task IDs that you have already created "
|
|
36
|
+
" in this same planning session. Do NOT reference tasks from previous sessions.\n"
|
|
37
|
+
"5. Only register contracts for actual API boundaries between modules.\n"
|
|
38
|
+
"6. Do NOT assign tasks to specific agents yourself — "
|
|
39
|
+
" the scheduler will handle assignment."
|
|
40
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""ReviewAgent: checks contract compliance, code quality, and security issues."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from maestro_agents.base import MaestroAgent
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ReviewAgent(MaestroAgent):
|
|
9
|
+
"""Validates code and contract quality before COMMIT.
|
|
10
|
+
|
|
11
|
+
Uses SSOT tools to:
|
|
12
|
+
1. Read all contracts and their consumers
|
|
13
|
+
2. Read recent execution logs and code changes
|
|
14
|
+
3. Check for contract violations, security issues, and code smells
|
|
15
|
+
4. Annotate the execution log with review findings
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
role = "review"
|
|
19
|
+
|
|
20
|
+
def system_prompt(self) -> str:
|
|
21
|
+
return (
|
|
22
|
+
"You are the Review Agent in the MAESTRO multi-agent system. "
|
|
23
|
+
"Your job is to review code and contracts for quality, security, "
|
|
24
|
+
"and compliance before the system commits an epoch. "
|
|
25
|
+
"You have file_read and shell_exec tools. Use file_read to inspect "
|
|
26
|
+
"source files, and shell_exec to run linters (ruff, mypy) and static "
|
|
27
|
+
"analysis. Do NOT use 'cd' in shell commands — the sandbox cwd is already "
|
|
28
|
+
"the project root. Read the contracts and execution log from the SSOT. "
|
|
29
|
+
"Report any issues you find and suggest fixes. "
|
|
30
|
+
"Use the SSOT tools to read state and annotate the execution log."
|
|
31
|
+
)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""TestAgent: writes and runs tests, reports coverage."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from maestro_agents.base import MaestroAgent
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestAgent(MaestroAgent):
|
|
9
|
+
"""Generates and executes tests for backend and frontend modules.
|
|
10
|
+
|
|
11
|
+
Note: ``__test__ = False`` prevents pytest from collecting this as a test class.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
__test__ = False
|
|
15
|
+
"""Generates and executes tests for backend and frontend modules.
|
|
16
|
+
|
|
17
|
+
Uses SSOT tools to:
|
|
18
|
+
1. Claim testing-related requirements
|
|
19
|
+
2. Read contracts to derive test cases
|
|
20
|
+
3. Write pytest/jest tests via the harness sandbox
|
|
21
|
+
4. Run tests and report results to the execution log
|
|
22
|
+
5. Update requirement status to DONE when complete
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
role = "test"
|
|
26
|
+
|
|
27
|
+
def system_prompt(self) -> str:
|
|
28
|
+
return (
|
|
29
|
+
"You are the Test Agent in the MAESTRO multi-agent system. "
|
|
30
|
+
"Your job is to write and run tests for the codebase. "
|
|
31
|
+
"Read the requirements and contracts from the SSOT, then generate "
|
|
32
|
+
"comprehensive unit and integration tests. "
|
|
33
|
+
"You have file_read, file_write, and shell_exec tools. Use file_read "
|
|
34
|
+
"to inspect existing code, file_write to create test files, and "
|
|
35
|
+
"shell_exec to run pytest/jest and verify results. "
|
|
36
|
+
"Do NOT use 'cd' in shell commands — the sandbox cwd is already "
|
|
37
|
+
"the project root. Report test results via the SSOT execution log tools "
|
|
38
|
+
"and update the requirement status when done."
|
|
39
|
+
)
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""PydanticAI function tools for SSOT read/write operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic_ai import Agent, RunContext
|
|
8
|
+
|
|
9
|
+
from maestro_harness.validator import Action
|
|
10
|
+
|
|
11
|
+
from .deps import AgentDeps
|
|
12
|
+
from .permissions import AgentPermissions
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def register_tools(
|
|
16
|
+
agent: Agent[AgentDeps, str], permissions: AgentPermissions | None = None
|
|
17
|
+
) -> None:
|
|
18
|
+
"""Register SSOT tools onto a PydanticAI Agent, filtered by permissions.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
agent: The PydanticAI agent to register tools on.
|
|
22
|
+
permissions: Permission bitmask controlling which tools are available.
|
|
23
|
+
If None, all tools are registered (backward-compatible default).
|
|
24
|
+
"""
|
|
25
|
+
perms = permissions or AgentPermissions()
|
|
26
|
+
|
|
27
|
+
if perms.read_requirements:
|
|
28
|
+
|
|
29
|
+
@agent.tool
|
|
30
|
+
async def ssot_read_requirements(ctx: RunContext[AgentDeps]) -> str:
|
|
31
|
+
"""Read all pending requirements from the SSOT."""
|
|
32
|
+
pending = ctx.deps.hub.list_pending()
|
|
33
|
+
if not pending:
|
|
34
|
+
return "No pending requirements."
|
|
35
|
+
lines = []
|
|
36
|
+
for r in pending:
|
|
37
|
+
assignee = r.assignee or "unassigned"
|
|
38
|
+
lines.append(
|
|
39
|
+
f"[{r.id}] {r.description} (status={r.status.value}, assignee={assignee})"
|
|
40
|
+
)
|
|
41
|
+
return "\n".join(lines)
|
|
42
|
+
|
|
43
|
+
@agent.tool
|
|
44
|
+
async def ssot_get_requirement(ctx: RunContext[AgentDeps], node_id: str) -> str:
|
|
45
|
+
"""Get a specific requirement by ID."""
|
|
46
|
+
node = ctx.deps.hub.get_requirement(node_id)
|
|
47
|
+
if node is None:
|
|
48
|
+
return f"Requirement {node_id} not found."
|
|
49
|
+
lines = [
|
|
50
|
+
f"ID: {node.id}",
|
|
51
|
+
f"Description: {node.description}",
|
|
52
|
+
f"Status: {node.status.value}",
|
|
53
|
+
]
|
|
54
|
+
if node.assignee:
|
|
55
|
+
lines.append(f"Assignee: {node.assignee}")
|
|
56
|
+
if node.acceptance_criteria:
|
|
57
|
+
lines.append("Acceptance criteria:")
|
|
58
|
+
for ac in node.acceptance_criteria:
|
|
59
|
+
lines.append(f" - {ac}")
|
|
60
|
+
if node.dependencies:
|
|
61
|
+
lines.append(f"Dependencies: {', '.join(node.dependencies)}")
|
|
62
|
+
return "\n".join(lines)
|
|
63
|
+
|
|
64
|
+
if perms.write_requirements:
|
|
65
|
+
|
|
66
|
+
@agent.tool
|
|
67
|
+
async def ssot_add_requirement(
|
|
68
|
+
ctx: RunContext[AgentDeps],
|
|
69
|
+
description: str,
|
|
70
|
+
parent_id: str,
|
|
71
|
+
acceptance_criteria: list[str] | None = None,
|
|
72
|
+
dependency_ids: list[str] | None = None,
|
|
73
|
+
) -> str:
|
|
74
|
+
"""Add a new requirement to the SSOT. parent_id is REQUIRED.
|
|
75
|
+
|
|
76
|
+
dependency_ids must be a list of existing requirement IDs (not descriptions)
|
|
77
|
+
that must be completed before this task can start.
|
|
78
|
+
"""
|
|
79
|
+
node = ctx.deps.hub.add_requirement(
|
|
80
|
+
description=description,
|
|
81
|
+
parent_id=parent_id,
|
|
82
|
+
acceptance_criteria=acceptance_criteria,
|
|
83
|
+
dependencies=dependency_ids,
|
|
84
|
+
agent_id=ctx.deps.agent_id,
|
|
85
|
+
)
|
|
86
|
+
return f"Created requirement {node.id}: {node.description}"
|
|
87
|
+
|
|
88
|
+
@agent.tool
|
|
89
|
+
async def ssot_update_requirement_status(
|
|
90
|
+
ctx: RunContext[AgentDeps], node_id: str, status: str
|
|
91
|
+
) -> str:
|
|
92
|
+
"""Update a requirement's status. Status values: PENDING, IN_PROGRESS, DONE, BLOCKED."""
|
|
93
|
+
from maestro_ssot.models import ReqStatus
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
req_status = ReqStatus(status)
|
|
97
|
+
except ValueError:
|
|
98
|
+
valid = ", ".join(s.value for s in ReqStatus)
|
|
99
|
+
return f"Invalid status '{status}'. Valid: {valid}"
|
|
100
|
+
ctx.deps.hub.update_requirement_status(node_id, req_status, ctx.deps.agent_id)
|
|
101
|
+
return f"Requirement {node_id} status updated to {status}"
|
|
102
|
+
|
|
103
|
+
@agent.tool
|
|
104
|
+
async def ssot_update_requirement_dependencies(
|
|
105
|
+
ctx: RunContext[AgentDeps], node_id: str, dependency_ids: list[str]
|
|
106
|
+
) -> str:
|
|
107
|
+
"""Update the dependencies of an existing requirement.
|
|
108
|
+
|
|
109
|
+
dependency_ids must be a list of existing requirement IDs.
|
|
110
|
+
"""
|
|
111
|
+
node = ctx.deps.hub.update_requirement_dependencies(
|
|
112
|
+
node_id, dependency_ids, ctx.deps.agent_id
|
|
113
|
+
)
|
|
114
|
+
return f"Requirement {node_id} dependencies updated to {', '.join(node.dependencies)}"
|
|
115
|
+
|
|
116
|
+
if perms.read_contracts:
|
|
117
|
+
|
|
118
|
+
@agent.tool
|
|
119
|
+
async def ssot_read_contracts(ctx: RunContext[AgentDeps]) -> str:
|
|
120
|
+
"""Read all contracts from the SSOT."""
|
|
121
|
+
contracts = ctx.deps.hub._persistence.list_contracts()
|
|
122
|
+
if not contracts:
|
|
123
|
+
return "No contracts registered."
|
|
124
|
+
lines = []
|
|
125
|
+
for c in contracts:
|
|
126
|
+
consumers = ", ".join(c.consumers) if c.consumers else "none"
|
|
127
|
+
lines.append(
|
|
128
|
+
f"[{c.id}] {c.name} (owner={c.owner}, v{c.version}, "
|
|
129
|
+
f"status={c.status.value}, consumers={consumers})"
|
|
130
|
+
)
|
|
131
|
+
return "\n".join(lines)
|
|
132
|
+
|
|
133
|
+
@agent.tool
|
|
134
|
+
async def ssot_get_contract(ctx: RunContext[AgentDeps], contract_id: str) -> str:
|
|
135
|
+
"""Get a specific contract by ID."""
|
|
136
|
+
contract = ctx.deps.hub.get_contract(contract_id)
|
|
137
|
+
if contract is None:
|
|
138
|
+
return f"Contract {contract_id} not found."
|
|
139
|
+
import json
|
|
140
|
+
|
|
141
|
+
lines = [
|
|
142
|
+
f"ID: {contract.id}",
|
|
143
|
+
f"Name: {contract.name}",
|
|
144
|
+
f"Owner: {contract.owner}",
|
|
145
|
+
f"Version: {contract.version}",
|
|
146
|
+
f"Status: {contract.status.value}",
|
|
147
|
+
f"Consumers: {', '.join(contract.consumers) if contract.consumers else 'none'}",
|
|
148
|
+
f"Schema: {json.dumps(contract.interface_schema)}",
|
|
149
|
+
]
|
|
150
|
+
return "\n".join(lines)
|
|
151
|
+
|
|
152
|
+
if perms.write_contracts:
|
|
153
|
+
|
|
154
|
+
@agent.tool
|
|
155
|
+
async def ssot_register_contract(
|
|
156
|
+
ctx: RunContext[AgentDeps],
|
|
157
|
+
name: str,
|
|
158
|
+
consumers: list[str] | None = None,
|
|
159
|
+
interface_schema: dict[str, Any] | None = None,
|
|
160
|
+
) -> str:
|
|
161
|
+
"""Register a new API contract in the SSOT.
|
|
162
|
+
|
|
163
|
+
The contract owner is automatically set to this agent.
|
|
164
|
+
"""
|
|
165
|
+
schema = interface_schema if interface_schema is not None else {}
|
|
166
|
+
contract = ctx.deps.hub.register_contract(
|
|
167
|
+
name=name,
|
|
168
|
+
owner=ctx.deps.agent_id,
|
|
169
|
+
consumers=consumers,
|
|
170
|
+
interface_schema=schema,
|
|
171
|
+
)
|
|
172
|
+
return f"Registered contract {contract.id}: {contract.name} (v{contract.version})"
|
|
173
|
+
|
|
174
|
+
@agent.tool
|
|
175
|
+
async def ssot_update_contract_schema(
|
|
176
|
+
ctx: RunContext[AgentDeps], contract_id: str, new_schema: dict[str, Any]
|
|
177
|
+
) -> str:
|
|
178
|
+
"""Update a contract's interface schema. This bumps the version."""
|
|
179
|
+
contract = ctx.deps.hub.update_contract_schema(
|
|
180
|
+
contract_id, new_schema, ctx.deps.agent_id
|
|
181
|
+
)
|
|
182
|
+
return f"Contract {contract_id} updated to v{contract.version}"
|
|
183
|
+
|
|
184
|
+
if perms.read_memory:
|
|
185
|
+
|
|
186
|
+
@agent.tool
|
|
187
|
+
async def ssot_read_memory(ctx: RunContext[AgentDeps], key: str) -> str:
|
|
188
|
+
"""Read a value from this agent's working memory."""
|
|
189
|
+
value = ctx.deps.hub.read_memory(ctx.deps.agent_id, key)
|
|
190
|
+
if value is None:
|
|
191
|
+
return f"No memory found for key '{key}'."
|
|
192
|
+
return value
|
|
193
|
+
|
|
194
|
+
if perms.write_memory:
|
|
195
|
+
|
|
196
|
+
@agent.tool
|
|
197
|
+
async def ssot_write_memory(ctx: RunContext[AgentDeps], key: str, value: str) -> str:
|
|
198
|
+
"""Write a key-value pair to this agent's working memory."""
|
|
199
|
+
ctx.deps.hub.write_memory(ctx.deps.agent_id, key, value)
|
|
200
|
+
return f"Memory saved: {key}"
|
|
201
|
+
|
|
202
|
+
if perms.read_log:
|
|
203
|
+
|
|
204
|
+
@agent.tool
|
|
205
|
+
async def ssot_query_log(ctx: RunContext[AgentDeps], agent_id: str | None = None) -> str:
|
|
206
|
+
"""Query the execution log, optionally filtered by agent_id."""
|
|
207
|
+
entries = ctx.deps.hub.query_log(agent_id=agent_id)
|
|
208
|
+
if not entries:
|
|
209
|
+
return "No log entries found."
|
|
210
|
+
lines = []
|
|
211
|
+
for e in entries[:20]:
|
|
212
|
+
status = e.status.value if hasattr(e.status, "value") else str(e.status)
|
|
213
|
+
lines.append(
|
|
214
|
+
f"[{e.timestamp}] {e.agent_id} {e.action_type} -> {e.target} ({status})"
|
|
215
|
+
)
|
|
216
|
+
return "\n".join(lines)
|
|
217
|
+
|
|
218
|
+
if perms.read_files:
|
|
219
|
+
|
|
220
|
+
@agent.tool
|
|
221
|
+
async def file_read(ctx: RunContext[AgentDeps], path: str) -> str:
|
|
222
|
+
"""Read the contents of a file from the sandbox."""
|
|
223
|
+
action = Action(action_type="FILE_READ", target=path, agent_id=ctx.deps.agent_id)
|
|
224
|
+
result = ctx.deps.harness.execute_action(ctx.deps.agent_id, action)
|
|
225
|
+
if result.exit_code != 0:
|
|
226
|
+
return f"Error reading file: {result.stderr}"
|
|
227
|
+
content = result.stdout
|
|
228
|
+
if len(content) > 2000:
|
|
229
|
+
content = content[:2000] + f"\n... ({len(content) - 2000} chars truncated)"
|
|
230
|
+
return content
|
|
231
|
+
|
|
232
|
+
if perms.write_files:
|
|
233
|
+
|
|
234
|
+
@agent.tool
|
|
235
|
+
async def file_write(ctx: RunContext[AgentDeps], path: str, content: str) -> str:
|
|
236
|
+
"""Write content to a file in the sandbox (creates parent dirs if needed)."""
|
|
237
|
+
action = Action(
|
|
238
|
+
action_type="FILE_WRITE",
|
|
239
|
+
target=path,
|
|
240
|
+
content=content,
|
|
241
|
+
agent_id=ctx.deps.agent_id,
|
|
242
|
+
)
|
|
243
|
+
result = ctx.deps.harness.execute_action(ctx.deps.agent_id, action)
|
|
244
|
+
if result.exit_code != 0:
|
|
245
|
+
return f"Error writing file: {result.stderr}"
|
|
246
|
+
return result.stdout or f"File written: {path}"
|
|
247
|
+
|
|
248
|
+
if perms.execute_shell:
|
|
249
|
+
|
|
250
|
+
@agent.tool
|
|
251
|
+
async def shell_exec(ctx: RunContext[AgentDeps], command: str) -> str:
|
|
252
|
+
"""Execute a shell command in the sandbox and return stdout/stderr."""
|
|
253
|
+
action = Action(command=command, agent_id=ctx.deps.agent_id)
|
|
254
|
+
result = ctx.deps.harness.execute_action(ctx.deps.agent_id, action)
|
|
255
|
+
lines = [f"exit_code: {result.exit_code}"]
|
|
256
|
+
if result.stdout:
|
|
257
|
+
stdout = result.stdout
|
|
258
|
+
if len(stdout) > 2000:
|
|
259
|
+
stdout = stdout[:2000] + f"\n... ({len(stdout) - 2000} chars truncated)"
|
|
260
|
+
lines.append(f"stdout:\n{stdout}")
|
|
261
|
+
if result.stderr:
|
|
262
|
+
stderr = result.stderr
|
|
263
|
+
if len(stderr) > 1000:
|
|
264
|
+
stderr = stderr[:1000] + f"\n... ({len(stderr) - 1000} chars truncated)"
|
|
265
|
+
lines.append(f"stderr:\n{stderr}")
|
|
266
|
+
return "\n".join(lines)
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Shared fixtures for maestro-agents tests."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from maestro_harness.harness import Harness
|
|
6
|
+
from maestro_ssot.hub import SSOTHub
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture
|
|
10
|
+
def hub() -> SSOTHub:
|
|
11
|
+
return SSOTHub(":memory:")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.fixture
|
|
15
|
+
def harness() -> Harness:
|
|
16
|
+
return Harness("configs/harness_policy.yaml", sandbox_root=".")
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Tests for MaestroAgent base class."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from maestro_agents.base import AgentResult, MaestroAgent
|
|
6
|
+
from maestro_agents.deps import AgentDeps
|
|
7
|
+
from maestro_agents.permissions import AgentPermissions
|
|
8
|
+
from maestro_harness.harness import Harness
|
|
9
|
+
from maestro_ssot.hub import SSOTHub
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestAgentResult:
|
|
13
|
+
def test_success_result(self) -> None:
|
|
14
|
+
r = AgentResult(output="done")
|
|
15
|
+
assert r.success
|
|
16
|
+
assert r.error is None
|
|
17
|
+
|
|
18
|
+
def test_error_result(self) -> None:
|
|
19
|
+
r = AgentResult(output="", success=False, error="boom")
|
|
20
|
+
assert not r.success
|
|
21
|
+
assert r.error == "boom"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TestAgentDeps:
|
|
25
|
+
def test_deps_fields(self, hub: SSOTHub, harness: Harness) -> None:
|
|
26
|
+
deps = AgentDeps(hub=hub, harness=harness, agent_id="test", role="generic")
|
|
27
|
+
assert deps.agent_id == "test"
|
|
28
|
+
assert deps.role == "generic"
|
|
29
|
+
assert deps.memory == {}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TestMaestroAgent:
|
|
33
|
+
def test_init(self, hub: SSOTHub, harness: Harness) -> None:
|
|
34
|
+
agent = MaestroAgent("test-agent", hub, harness)
|
|
35
|
+
assert agent.agent_id == "test-agent"
|
|
36
|
+
assert agent.hub is hub
|
|
37
|
+
assert agent.harness is harness
|
|
38
|
+
assert agent.role == "generic"
|
|
39
|
+
|
|
40
|
+
def test_default_system_prompt(self, hub: SSOTHub, harness: Harness) -> None:
|
|
41
|
+
agent = MaestroAgent("test-agent", hub, harness)
|
|
42
|
+
assert "MAESTRO" in agent.system_prompt()
|
|
43
|
+
|
|
44
|
+
def test_permissions_all_true_by_default(self, hub: SSOTHub, harness: Harness) -> None:
|
|
45
|
+
agent = MaestroAgent("test-agent", hub, harness)
|
|
46
|
+
toolset = agent._agent._function_toolset # type: ignore[attr-defined]
|
|
47
|
+
tool_names = {name for name in toolset.tools}
|
|
48
|
+
assert "file_read" in tool_names
|
|
49
|
+
assert "file_write" in tool_names
|
|
50
|
+
assert "shell_exec" in tool_names
|
|
51
|
+
|
|
52
|
+
def test_permissions_restrict_file_tools(self, hub: SSOTHub, harness: Harness) -> None:
|
|
53
|
+
restricted = AgentPermissions(
|
|
54
|
+
read_files=False, write_files=False, execute_shell=False
|
|
55
|
+
)
|
|
56
|
+
agent = MaestroAgent("restricted", hub, harness, permissions=restricted)
|
|
57
|
+
toolset = agent._agent._function_toolset # type: ignore[attr-defined]
|
|
58
|
+
tool_names = {name for name in toolset.tools}
|
|
59
|
+
assert "file_read" not in tool_names
|
|
60
|
+
assert "file_write" not in tool_names
|
|
61
|
+
assert "shell_exec" not in tool_names
|
|
62
|
+
# SSOT tools should still be present
|
|
63
|
+
assert "ssot_read_requirements" in tool_names
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Tests for BackendAgent."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from maestro_agents.backend import BackendAgent
|
|
6
|
+
from maestro_harness.harness import Harness
|
|
7
|
+
from maestro_ssot.hub import SSOTHub
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestBackendAgent:
|
|
11
|
+
def test_role(self, hub: SSOTHub, harness: Harness) -> None:
|
|
12
|
+
agent = BackendAgent("backend-1", hub, harness)
|
|
13
|
+
assert agent.role == "backend"
|
|
14
|
+
|
|
15
|
+
def test_system_prompt(self, hub: SSOTHub, harness: Harness) -> None:
|
|
16
|
+
agent = BackendAgent("backend-1", hub, harness)
|
|
17
|
+
prompt = agent.system_prompt()
|
|
18
|
+
assert "Backend Agent" in prompt
|
|
19
|
+
assert "implement" in prompt.lower()
|
|
20
|
+
|
|
21
|
+
def test_agent_id(self, hub: SSOTHub, harness: Harness) -> None:
|
|
22
|
+
agent = BackendAgent("backend-1", hub, harness)
|
|
23
|
+
assert agent.agent_id == "backend-1"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Tests for the non-LLM contract validator."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from maestro_agents.contract_validator import validate_contracts
|
|
6
|
+
from maestro_harness.harness import Harness
|
|
7
|
+
from maestro_ssot.hub import SSOTHub
|
|
8
|
+
from maestro_ssot.models import ContractStatus
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestContractValidator:
|
|
12
|
+
def test_empty_contracts_valid(self, hub: SSOTHub, harness: Harness) -> None:
|
|
13
|
+
result = validate_contracts(hub)
|
|
14
|
+
assert result.valid
|
|
15
|
+
assert result.violations == []
|
|
16
|
+
|
|
17
|
+
def test_stable_contract_without_schema(self, hub: SSOTHub, harness: Harness) -> None:
|
|
18
|
+
contract = hub.register_contract("API", owner="backend_agent", interface_schema={})
|
|
19
|
+
hub.contracts.update_status(contract.id, ContractStatus.STABLE)
|
|
20
|
+
result = validate_contracts(hub)
|
|
21
|
+
assert not result.valid
|
|
22
|
+
assert any("empty interface_schema" in v.message for v in result.violations)
|
|
23
|
+
|
|
24
|
+
def test_draft_consumer_of_stable_contract(self, hub: SSOTHub, harness: Harness) -> None:
|
|
25
|
+
draft = hub.register_contract(
|
|
26
|
+
"DraftAPI", owner="backend_agent", interface_schema={"type": "object"}
|
|
27
|
+
)
|
|
28
|
+
active = hub.register_contract(
|
|
29
|
+
"ActiveAPI",
|
|
30
|
+
owner="frontend_agent",
|
|
31
|
+
consumers=[draft.id],
|
|
32
|
+
interface_schema={"type": "object"},
|
|
33
|
+
)
|
|
34
|
+
hub.contracts.update_status(active.id, ContractStatus.STABLE)
|
|
35
|
+
|
|
36
|
+
result = validate_contracts(hub)
|
|
37
|
+
assert not result.valid
|
|
38
|
+
assert any("DRAFT contract" in v.message for v in result.violations)
|
|
39
|
+
|
|
40
|
+
def test_valid_stable_contract_with_schema(self, hub: SSOTHub, harness: Harness) -> None:
|
|
41
|
+
contract = hub.register_contract(
|
|
42
|
+
"GoodAPI",
|
|
43
|
+
owner="backend_agent",
|
|
44
|
+
interface_schema={"type": "object", "properties": {"id": {"type": "string"}}},
|
|
45
|
+
)
|
|
46
|
+
hub.contracts.update_status(contract.id, ContractStatus.STABLE)
|
|
47
|
+
|
|
48
|
+
result = validate_contracts(hub)
|
|
49
|
+
assert result.valid
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Tests for PlanningAgent."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from maestro_agents.planning import PlanningAgent
|
|
6
|
+
from maestro_harness.harness import Harness
|
|
7
|
+
from maestro_ssot.hub import SSOTHub
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestPlanningAgent:
|
|
11
|
+
def test_role(self, hub: SSOTHub, harness: Harness) -> None:
|
|
12
|
+
agent = PlanningAgent("planner-1", hub, harness)
|
|
13
|
+
assert agent.role == "planner"
|
|
14
|
+
|
|
15
|
+
def test_system_prompt(self, hub: SSOTHub, harness: Harness) -> None:
|
|
16
|
+
agent = PlanningAgent("planner-1", hub, harness)
|
|
17
|
+
prompt = agent.system_prompt()
|
|
18
|
+
assert "Planning Agent" in prompt
|
|
19
|
+
assert "decompose" in prompt.lower()
|
|
20
|
+
|
|
21
|
+
def test_agent_id(self, hub: SSOTHub, harness: Harness) -> None:
|
|
22
|
+
agent = PlanningAgent("planner-1", hub, harness)
|
|
23
|
+
assert agent.agent_id == "planner-1"
|