minion-cli 1.0.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.
Files changed (157) hide show
  1. minion/__init__.py +5 -0
  2. minion/__main__.py +3 -0
  3. minion/a2a/__init__.py +37 -0
  4. minion/a2a/card.py +69 -0
  5. minion/a2a/client.py +450 -0
  6. minion/a2a/config.py +95 -0
  7. minion/a2a/manager.py +116 -0
  8. minion/a2a/models.py +233 -0
  9. minion/a2a/server.py +454 -0
  10. minion/agents/__init__.py +89 -0
  11. minion/agents/builtin/coder.yaml +35 -0
  12. minion/agents/builtin/researcher.yaml +34 -0
  13. minion/agents/builtin/reviewer.yaml +33 -0
  14. minion/agents/builtin/tester.yaml +35 -0
  15. minion/agents/display.py +285 -0
  16. minion/agents/manifest.py +79 -0
  17. minion/agents/persist.py +28 -0
  18. minion/agents/registry.py +55 -0
  19. minion/agents/runner.py +253 -0
  20. minion/cli/__init__.py +3 -0
  21. minion/cli/_core.py +287 -0
  22. minion/cli/agents.py +60 -0
  23. minion/cli/config.py +39 -0
  24. minion/cli/doctor.py +82 -0
  25. minion/cli/mcp.py +54 -0
  26. minion/cli/memory.py +104 -0
  27. minion/cli/remote.py +88 -0
  28. minion/cli/skills.py +27 -0
  29. minion/compact/__init__.py +39 -0
  30. minion/compact/base.py +31 -0
  31. minion/compact/summary.py +113 -0
  32. minion/compact/truncate.py +58 -0
  33. minion/config/__init__.py +52 -0
  34. minion/config/file.py +422 -0
  35. minion/config/interactive.py +198 -0
  36. minion/config/model_catalog.py +165 -0
  37. minion/config/wizard.py +151 -0
  38. minion/context/__init__.py +11 -0
  39. minion/context/filetree.py +151 -0
  40. minion/context/manifest.py +248 -0
  41. minion/context/project.py +83 -0
  42. minion/context/prompts.py +106 -0
  43. minion/hooks/__init__.py +37 -0
  44. minion/hooks/builtin/__init__.py +3 -0
  45. minion/hooks/builtin/minion_md.py +53 -0
  46. minion/hooks/events.py +122 -0
  47. minion/hooks/handler.py +24 -0
  48. minion/hooks/handlers/__init__.py +3 -0
  49. minion/hooks/handlers/shell.py +76 -0
  50. minion/hooks/manifest.py +73 -0
  51. minion/hooks/persist.py +60 -0
  52. minion/hooks/registry.py +115 -0
  53. minion/hooks/result.py +14 -0
  54. minion/hooks/runner.py +119 -0
  55. minion/llm/__init__.py +4 -0
  56. minion/llm/anthropic.py +370 -0
  57. minion/llm/base.py +210 -0
  58. minion/llm/conversation.py +252 -0
  59. minion/llm/factory.py +78 -0
  60. minion/llm/openai.py +148 -0
  61. minion/llm/reflection.py +344 -0
  62. minion/llm/validate.py +70 -0
  63. minion/mcp/__init__.py +13 -0
  64. minion/mcp/config.py +124 -0
  65. minion/mcp/manager.py +766 -0
  66. minion/memory/__init__.py +13 -0
  67. minion/memory/config.py +42 -0
  68. minion/memory/embedder.py +68 -0
  69. minion/memory/extractor.py +231 -0
  70. minion/memory/injection.py +88 -0
  71. minion/memory/record.py +156 -0
  72. minion/memory/store.py +481 -0
  73. minion/memory/triggers.py +70 -0
  74. minion/memory/vector_store.py +123 -0
  75. minion/output/__init__.py +16 -0
  76. minion/output/base.py +202 -0
  77. minion/output/console.py +217 -0
  78. minion/output/diff.py +181 -0
  79. minion/output/display_utils.py +200 -0
  80. minion/output/formatter.py +154 -0
  81. minion/output/tui.py +199 -0
  82. minion/planner/__init__.py +10 -0
  83. minion/planner/creator.py +401 -0
  84. minion/planner/storage.py +54 -0
  85. minion/repl/__init__.py +22 -0
  86. minion/repl/agent_handlers.py +94 -0
  87. minion/repl/commands.py +659 -0
  88. minion/repl/config_cmd.py +271 -0
  89. minion/repl/init_md.py +70 -0
  90. minion/repl/input.py +168 -0
  91. minion/repl/mcp.py +188 -0
  92. minion/repl/session.py +1521 -0
  93. minion/repl/state.py +103 -0
  94. minion/runner/__init__.py +42 -0
  95. minion/runner/context.py +91 -0
  96. minion/runner/loop.py +616 -0
  97. minion/runner/parallel.py +318 -0
  98. minion/runner/session.py +172 -0
  99. minion/skills/__init__.py +12 -0
  100. minion/skills/builtin/commit.yaml +16 -0
  101. minion/skills/builtin/explain.yaml +27 -0
  102. minion/skills/builtin/refactor.yaml +33 -0
  103. minion/skills/builtin/review.yaml +28 -0
  104. minion/skills/builtin/test.yaml +24 -0
  105. minion/skills/manifest.py +80 -0
  106. minion/skills/persist.py +28 -0
  107. minion/skills/registry.py +76 -0
  108. minion/skills/runner.py +140 -0
  109. minion/theme/__init__.py +75 -0
  110. minion/theme/banner.py +297 -0
  111. minion/theme/console.py +31 -0
  112. minion/theme/palette.py +32 -0
  113. minion/theme/printers.py +295 -0
  114. minion/tools/__init__.py +1 -0
  115. minion/tools/confirmation.py +137 -0
  116. minion/tools/definitions.py +357 -0
  117. minion/tools/executor.py +903 -0
  118. minion/tools/implementations.py +549 -0
  119. minion/tools/outline.py +241 -0
  120. minion/tools/permissions.py +280 -0
  121. minion/tracing/__init__.py +12 -0
  122. minion/tracing/cli.py +161 -0
  123. minion/tracing/events.py +310 -0
  124. minion/tracing/server.py +85 -0
  125. minion/tracing/tracer.py +154 -0
  126. minion/tracing/ui.html +1166 -0
  127. minion/tui/__init__.py +46 -0
  128. minion/tui/agent_registry.py +96 -0
  129. minion/tui/app.py +1417 -0
  130. minion/tui/choice_panel.py +98 -0
  131. minion/tui/conversation.py +319 -0
  132. minion/tui/inspector.py +604 -0
  133. minion/tui/keys.py +20 -0
  134. minion/tui/messages.py +11 -0
  135. minion/tui/permission.py +188 -0
  136. minion/tui/render.py +141 -0
  137. minion/tui/screens/__init__.py +13 -0
  138. minion/tui/screens/agents_screen.py +2963 -0
  139. minion/tui/screens/base.py +373 -0
  140. minion/tui/screens/completion_setup.py +261 -0
  141. minion/tui/screens/config_panel.py +513 -0
  142. minion/tui/screens/help_screen.py +755 -0
  143. minion/tui/screens/hooks_screen.py +2367 -0
  144. minion/tui/screens/load_screen.py +528 -0
  145. minion/tui/screens/memories_screen.py +932 -0
  146. minion/tui/screens/model_config.py +930 -0
  147. minion/tui/screens/skills_screen.py +2682 -0
  148. minion/tui/setup_checklist.py +219 -0
  149. minion/tui/slots.py +180 -0
  150. minion/tui/status.py +144 -0
  151. minion/tui/terminal.py +92 -0
  152. minion/tui/theme.py +186 -0
  153. minion_cli-1.0.0.dist-info/METADATA +336 -0
  154. minion_cli-1.0.0.dist-info/RECORD +157 -0
  155. minion_cli-1.0.0.dist-info/WHEEL +4 -0
  156. minion_cli-1.0.0.dist-info/entry_points.txt +3 -0
  157. minion_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
minion/a2a/manager.py ADDED
@@ -0,0 +1,116 @@
1
+ """A2A manager — connects to all configured remote agents, routes send_task() calls.
2
+
3
+ One A2AClient per configured agent. Emits Nefario trace events around the
4
+ task lifecycle. Gracefully handles unknown agent names and connection failures.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import time
10
+ from pathlib import Path
11
+ from typing import Optional
12
+
13
+ from ..theme import console
14
+ from ..tracing import get_tracer
15
+ from .client import A2AClient, A2AError
16
+ from .config import A2AAgentConfig, load_a2a_config
17
+ from .models import AgentCard
18
+
19
+
20
+ class A2AManager:
21
+ """Routes named agent calls to the correct A2AClient.
22
+
23
+ Created once at REPL startup. Loaded from a2a.json config (user + project
24
+ tiers). Optionally fetches Agent Cards at startup for the /a2a list display.
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ clients: dict[str, A2AClient],
30
+ cards: Optional[dict[str, Optional[AgentCard]]] = None,
31
+ ) -> None:
32
+ self._clients = clients
33
+ self._cards: dict[str, Optional[AgentCard]] = cards or {}
34
+
35
+ def agent_names(self) -> list[str]:
36
+ return list(self._clients.keys())
37
+
38
+ def has_agents(self) -> bool:
39
+ return bool(self._clients)
40
+
41
+ def agent_summary(self) -> list[dict]:
42
+ """Return display info for each agent (name, url, card name/description)."""
43
+ result = []
44
+ for name, client in self._clients.items():
45
+ card = self._cards.get(name)
46
+ result.append({
47
+ "name": name,
48
+ "url": f"{client._scheme}://{client._netloc}",
49
+ "card_name": card.name if card else "(unreachable)",
50
+ "card_description": card.description if card else "",
51
+ })
52
+ return result
53
+
54
+ def send_task(self, agent_name: str, task_text: str) -> str:
55
+ """Send a task to the named remote agent and return the result text.
56
+
57
+ Emits a2a_task_send, then a2a_task_complete or a2a_task_error.
58
+ Returns an error string (never raises) so the LLM sees the error as
59
+ a tool result rather than an uncaught exception.
60
+ """
61
+ client = self._clients.get(agent_name)
62
+ if client is None:
63
+ available = ", ".join(self._clients) if self._clients else "(none configured)"
64
+ return (
65
+ f"Error: unknown A2A agent '{agent_name}'. "
66
+ f"Available: {available}"
67
+ )
68
+
69
+ remote_url = f"{client._scheme}://{client._netloc}"
70
+ get_tracer().emit(
71
+ "a2a_task_send",
72
+ agent_name=agent_name,
73
+ task=task_text,
74
+ remote_url=remote_url,
75
+ )
76
+
77
+ start = time.monotonic()
78
+ try:
79
+ result = client.send_task(task_text)
80
+ latency_ms = int((time.monotonic() - start) * 1000)
81
+ get_tracer().emit(
82
+ "a2a_task_complete",
83
+ agent_name=agent_name,
84
+ task=task_text,
85
+ result=result[:500],
86
+ result_length=len(result),
87
+ latency_ms=latency_ms,
88
+ )
89
+ return result
90
+ except A2AError as e:
91
+ get_tracer().emit(
92
+ "a2a_task_error",
93
+ agent_name=agent_name,
94
+ task=task_text,
95
+ error=str(e),
96
+ )
97
+ return f"Error: {e}"
98
+
99
+
100
+ def load_a2a_manager(cwd: Path | None = None) -> A2AManager:
101
+ """Load A2A config and construct one A2AClient per configured agent.
102
+
103
+ Attempts to fetch each agent's Agent Card at startup so /a2a list can
104
+ show descriptions. Card fetch failures are silent — the agent is still
105
+ registered (it may be temporarily unreachable).
106
+ """
107
+ configs = load_a2a_config(cwd)
108
+ clients: dict[str, A2AClient] = {}
109
+ cards: dict[str, Optional[AgentCard]] = {}
110
+
111
+ for name, cfg in configs.items():
112
+ client = A2AClient(name=cfg.name, url=cfg.url, timeout_seconds=cfg.timeout_seconds)
113
+ clients[name] = client
114
+ cards[name] = client.fetch_agent_card() # None if unreachable
115
+
116
+ return A2AManager(clients=clients, cards=cards)
minion/a2a/models.py ADDED
@@ -0,0 +1,233 @@
1
+ """A2A protocol data model — Task, Artifact, AgentCard.
2
+
3
+ Spec-compliant implementation of the Agent-to-Agent (A2A) protocol:
4
+ - Artifacts use {parts: [{type, text}]} wire format
5
+ - Task status uses {state, timestamp} object
6
+ - AgentCard includes defaultInputModes/defaultOutputModes and full capabilities
7
+ - Message uses {role, parts: [{type, text}]} wire format
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import uuid
14
+ from dataclasses import dataclass, field
15
+ from datetime import datetime, timezone
16
+ from enum import Enum
17
+ from typing import Optional
18
+
19
+
20
+ def _iso_now() -> str:
21
+ return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
22
+
23
+
24
+ def _extract_text_from_message(msg) -> str:
25
+ """Extract plain text from a spec Message object or bare string."""
26
+ if isinstance(msg, str):
27
+ return msg
28
+ if isinstance(msg, dict):
29
+ parts = msg.get("parts", [])
30
+ if parts:
31
+ texts = [p.get("text", "") for p in parts if p.get("type") == "text"]
32
+ return "\n".join(t for t in texts if t) or msg.get("text", "")
33
+ return msg.get("text", "")
34
+ return str(msg)
35
+
36
+
37
+ def _make_message(text: str, role: str = "user") -> dict:
38
+ """Build a spec-compliant Message object."""
39
+ return {
40
+ "role": role,
41
+ "parts": [{"type": "text", "text": text}],
42
+ "messageId": str(uuid.uuid4()),
43
+ }
44
+
45
+
46
+ class TaskStatus(str, Enum):
47
+ SUBMITTED = "submitted"
48
+ WORKING = "working"
49
+ INPUT_REQUIRED = "input-required"
50
+ COMPLETED = "completed"
51
+ FAILED = "failed"
52
+ CANCELED = "canceled"
53
+
54
+
55
+ @dataclass
56
+ class Artifact:
57
+ """Text result produced by a remote agent for a task.
58
+
59
+ Wire format (spec): {"artifactId": "...", "parts": [{"type": "text", "text": "..."}]}
60
+ """
61
+ text: str
62
+ artifact_id: str = field(default_factory=lambda: str(uuid.uuid4()))
63
+
64
+ def to_dict(self) -> dict:
65
+ return {
66
+ "artifactId": self.artifact_id,
67
+ "parts": [{"type": "text", "text": self.text}],
68
+ }
69
+
70
+ @classmethod
71
+ def from_dict(cls, data: dict) -> "Artifact":
72
+ # Handle spec format: {parts: [{type, text}]}
73
+ parts = data.get("parts", [])
74
+ if parts:
75
+ text = "\n".join(
76
+ p.get("text", "") for p in parts if p.get("type") == "text"
77
+ )
78
+ else:
79
+ # Fallback for older non-spec format
80
+ text = data.get("text", "")
81
+ return cls(text=text, artifact_id=data.get("artifactId", str(uuid.uuid4())))
82
+
83
+
84
+ @dataclass
85
+ class Task:
86
+ """Unit of work in the A2A protocol.
87
+
88
+ Wire format (spec): status is {state, timestamp}, not a bare string.
89
+ Lifecycle: submitted → working → completed / failed / canceled
90
+ contextId groups related tasks into a session (multi-turn conversation).
91
+ """
92
+ id: str
93
+ status: TaskStatus
94
+ input_message: str
95
+ artifacts: list[Artifact] = field(default_factory=list)
96
+ error: Optional[str] = None
97
+ created_at: str = field(default_factory=_iso_now)
98
+ context_id: Optional[str] = None
99
+
100
+ def to_dict(self) -> dict:
101
+ d: dict = {
102
+ "id": self.id,
103
+ "status": {
104
+ "state": self.status.value,
105
+ "timestamp": _iso_now(),
106
+ },
107
+ "input": _make_message(self.input_message),
108
+ }
109
+ if self.context_id is not None:
110
+ d["contextId"] = self.context_id
111
+ if self.artifacts:
112
+ d["artifacts"] = [a.to_dict() for a in self.artifacts]
113
+ if self.error is not None:
114
+ d["error"] = self.error
115
+ return d
116
+
117
+ def status_event(self, final: bool = False) -> dict:
118
+ """Build a spec TaskStatusUpdateEvent for SSE streaming."""
119
+ d: dict = {
120
+ "id": self.id,
121
+ "status": {
122
+ "state": self.status.value,
123
+ "timestamp": _iso_now(),
124
+ },
125
+ "final": final,
126
+ }
127
+ if self.context_id is not None:
128
+ d["contextId"] = self.context_id
129
+ return d
130
+
131
+ def artifact_event(self, artifact: Artifact, final: bool = True) -> dict:
132
+ """Build a spec TaskArtifactUpdateEvent for SSE streaming."""
133
+ d: dict = {
134
+ "id": self.id,
135
+ "artifact": artifact.to_dict(),
136
+ "final": final,
137
+ }
138
+ if self.context_id is not None:
139
+ d["contextId"] = self.context_id
140
+ return d
141
+
142
+ @classmethod
143
+ def from_dict(cls, data: dict) -> "Task":
144
+ # Parse status — handle both spec {state, timestamp} and legacy bare string
145
+ raw_status = data.get("status", "submitted")
146
+ if isinstance(raw_status, dict):
147
+ status_str = raw_status.get("state", "submitted")
148
+ else:
149
+ status_str = raw_status
150
+ try:
151
+ status = TaskStatus(status_str)
152
+ except ValueError:
153
+ status = TaskStatus.FAILED
154
+
155
+ artifacts = [Artifact.from_dict(a) for a in data.get("artifacts", [])]
156
+
157
+ # Parse input — handle both spec Message and legacy {message: str}
158
+ input_data = data.get("input", {})
159
+ if isinstance(input_data, dict):
160
+ input_message = _extract_text_from_message(input_data)
161
+ else:
162
+ input_message = str(input_data)
163
+
164
+ return cls(
165
+ id=data.get("id", ""),
166
+ status=status,
167
+ input_message=input_message,
168
+ artifacts=artifacts,
169
+ error=data.get("error"),
170
+ context_id=data.get("contextId"),
171
+ )
172
+
173
+
174
+ @dataclass
175
+ class AgentCard:
176
+ """Capability advertisement for an A2A agent.
177
+
178
+ Served at /.well-known/agent.json.
179
+ """
180
+ name: str
181
+ description: str
182
+ url: str
183
+ version: str
184
+ capabilities: dict = field(default_factory=lambda: {
185
+ "streaming": True,
186
+ "pushNotifications": False,
187
+ "stateTransitionHistory": False,
188
+ })
189
+ skills: list[dict] = field(default_factory=list)
190
+ default_input_modes: list[str] = field(default_factory=lambda: ["text"])
191
+ default_output_modes: list[str] = field(default_factory=lambda: ["text"])
192
+ # Optional spec fields — omitted from wire format when None
193
+ provider: Optional[dict] = None # {"organization": "...", "url": "..."}
194
+ authentication: Optional[dict] = None # {"schemes": ["Bearer"]}
195
+
196
+ def to_dict(self) -> dict:
197
+ d: dict = {
198
+ "name": self.name,
199
+ "description": self.description,
200
+ "url": self.url,
201
+ "version": self.version,
202
+ "capabilities": self.capabilities,
203
+ "defaultInputModes": self.default_input_modes,
204
+ "defaultOutputModes": self.default_output_modes,
205
+ "skills": self.skills,
206
+ }
207
+ if self.provider is not None:
208
+ d["provider"] = self.provider
209
+ if self.authentication is not None:
210
+ d["authentication"] = self.authentication
211
+ return d
212
+
213
+ def to_json(self) -> str:
214
+ return json.dumps(self.to_dict(), indent=2)
215
+
216
+ @classmethod
217
+ def from_dict(cls, data: dict) -> "AgentCard":
218
+ return cls(
219
+ name=data.get("name", ""),
220
+ description=data.get("description", ""),
221
+ url=data.get("url", ""),
222
+ version=data.get("version", ""),
223
+ capabilities=data.get("capabilities", {
224
+ "streaming": True,
225
+ "pushNotifications": False,
226
+ "stateTransitionHistory": False,
227
+ }),
228
+ skills=data.get("skills", []),
229
+ default_input_modes=data.get("defaultInputModes", ["text"]),
230
+ default_output_modes=data.get("defaultOutputModes", ["text"]),
231
+ provider=data.get("provider"),
232
+ authentication=data.get("authentication"),
233
+ )