@team-agent/installer 0.1.11 → 0.2.1
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.
- package/crates/team-agent-core/src/lib.rs +50 -5
- package/package.json +1 -1
- package/schemas/team.schema.json +1 -0
- package/src/team_agent/approvals/__init__.py +65 -0
- package/src/team_agent/approvals/constants.py +6 -0
- package/src/team_agent/approvals/parsing.py +176 -0
- package/src/team_agent/approvals/runtime_prompts.py +171 -0
- package/src/team_agent/approvals/status.py +165 -0
- package/src/team_agent/cli/__init__.py +137 -0
- package/src/team_agent/cli/commands.py +339 -0
- package/src/team_agent/cli/e2e.py +202 -0
- package/src/team_agent/cli/helpers.py +137 -0
- package/src/team_agent/cli/parser.py +477 -0
- package/src/team_agent/compiler.py +98 -33
- package/src/team_agent/coordinator/__init__.py +53 -0
- package/src/team_agent/{coordinator.py → coordinator/__main__.py} +3 -1
- package/src/team_agent/coordinator/lifecycle.py +334 -0
- package/src/team_agent/coordinator/metadata.py +61 -0
- package/src/team_agent/coordinator/paths.py +17 -0
- package/src/team_agent/diagnose/__init__.py +48 -0
- package/src/team_agent/diagnose/checks.py +101 -0
- package/src/team_agent/diagnose/health.py +241 -0
- package/src/team_agent/diagnose/preflight.py +194 -0
- package/src/team_agent/diagnose/quick_start.py +233 -0
- package/src/team_agent/display/__init__.py +61 -0
- package/src/team_agent/display/close.py +147 -0
- package/src/team_agent/display/ghostty.py +77 -0
- package/src/team_agent/display/worker_window.py +110 -0
- package/src/team_agent/display/workspace.py +473 -0
- package/src/team_agent/launch/__init__.py +41 -0
- package/src/team_agent/launch/bootstrap.py +85 -0
- package/src/team_agent/launch/config.py +106 -0
- package/src/team_agent/launch/core.py +291 -0
- package/src/team_agent/launch/requirements.py +57 -0
- package/src/team_agent/leader/__init__.py +320 -0
- package/src/team_agent/lifecycle/__init__.py +5 -0
- package/src/team_agent/lifecycle/agents.py +226 -0
- package/src/team_agent/lifecycle/operations.py +321 -0
- package/src/team_agent/lifecycle/paste_buffer_hygiene.py +39 -0
- package/src/team_agent/lifecycle/start.py +363 -0
- package/src/team_agent/mcp_server/__init__.py +42 -0
- package/src/team_agent/mcp_server/__main__.py +7 -0
- package/src/team_agent/mcp_server/contracts.py +148 -0
- package/src/team_agent/mcp_server/normalize.py +257 -0
- package/src/team_agent/mcp_server/server.py +150 -0
- package/src/team_agent/mcp_server/tools.py +205 -0
- package/src/team_agent/message_store/__init__.py +23 -0
- package/src/team_agent/message_store/agent_health.py +109 -0
- package/src/team_agent/{message_store.py → message_store/core.py} +188 -245
- package/src/team_agent/message_store/result_watchers.py +102 -0
- package/src/team_agent/message_store/schema.py +266 -0
- package/src/team_agent/messaging/__init__.py +1 -0
- package/src/team_agent/messaging/activity_detector.py +190 -0
- package/src/team_agent/messaging/delivery.py +138 -0
- package/src/team_agent/messaging/deps.py +263 -0
- package/src/team_agent/messaging/idle_alerts.py +323 -0
- package/src/team_agent/messaging/internal_delivery.py +46 -0
- package/src/team_agent/messaging/leader.py +317 -0
- package/src/team_agent/messaging/leader_panes.py +343 -0
- package/src/team_agent/messaging/owner_bypass.py +29 -0
- package/src/team_agent/messaging/result_delivery.py +300 -0
- package/src/team_agent/messaging/results.py +456 -0
- package/src/team_agent/messaging/scheduler.py +428 -0
- package/src/team_agent/messaging/send.py +500 -0
- package/src/team_agent/messaging/session_drift.py +94 -0
- package/src/team_agent/messaging/tmux_io.py +337 -0
- package/src/team_agent/messaging/tmux_prompt.py +229 -0
- package/src/team_agent/orchestrator/__init__.py +376 -0
- package/src/team_agent/orchestrator/plan.py +122 -0
- package/src/team_agent/orchestrator/state.py +128 -0
- package/src/team_agent/profiles/__init__.py +82 -0
- package/src/team_agent/profiles/constants.py +19 -0
- package/src/team_agent/profiles/core.py +407 -0
- package/src/team_agent/profiles/helpers.py +69 -0
- package/src/team_agent/profiles/provider_env.py +188 -0
- package/src/team_agent/profiles/smoke.py +201 -0
- package/src/team_agent/provider_cli/__init__.py +43 -0
- package/src/team_agent/provider_cli/adapter.py +167 -0
- package/src/team_agent/provider_cli/base.py +48 -0
- package/src/team_agent/provider_cli/claude.py +457 -0
- package/src/team_agent/provider_cli/codex.py +319 -0
- package/src/team_agent/provider_cli/copilot.py +8 -0
- package/src/team_agent/provider_cli/fake.py +39 -0
- package/src/team_agent/provider_cli/gemini.py +95 -0
- package/src/team_agent/provider_cli/opencode.py +8 -0
- package/src/team_agent/provider_cli/prompt.py +62 -0
- package/src/team_agent/provider_cli/registry.py +18 -0
- package/src/team_agent/provider_cli/unsupported.py +32 -0
- package/src/team_agent/providers.py +67 -949
- package/src/team_agent/quality_gates.py +104 -0
- package/src/team_agent/restart/__init__.py +34 -0
- package/src/team_agent/restart/orchestration.py +328 -0
- package/src/team_agent/restart/selection.py +89 -0
- package/src/team_agent/restart/snapshot.py +70 -0
- package/src/team_agent/runtime.py +809 -5892
- package/src/team_agent/rust_core.py +22 -5
- package/src/team_agent/sessions/__init__.py +25 -0
- package/src/team_agent/sessions/capture.py +93 -0
- package/src/team_agent/sessions/inventory.py +44 -0
- package/src/team_agent/sessions/resume.py +135 -0
- package/src/team_agent/spec.py +3 -1
- package/src/team_agent/state.py +218 -4
- package/src/team_agent/status/__init__.py +63 -0
- package/src/team_agent/status/approvals.py +52 -0
- package/src/team_agent/status/compact.py +158 -0
- package/src/team_agent/status/constants.py +18 -0
- package/src/team_agent/status/inbox.py +28 -0
- package/src/team_agent/status/peek.py +117 -0
- package/src/team_agent/status/queries.py +168 -0
- package/src/team_agent/terminal.py +57 -0
- package/src/team_agent/cli.py +0 -858
- package/src/team_agent/mcp_server.py +0 -579
- package/src/team_agent/profiles.py +0 -882
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _compact_tool_result(result: dict[str, Any]) -> dict[str, Any]:
|
|
7
|
+
if result.get("ok") is False:
|
|
8
|
+
keys = [
|
|
9
|
+
"ok",
|
|
10
|
+
"status",
|
|
11
|
+
"reason",
|
|
12
|
+
"error",
|
|
13
|
+
"message_id",
|
|
14
|
+
"agent_id",
|
|
15
|
+
"new_agent_id",
|
|
16
|
+
"source_agent_id",
|
|
17
|
+
"role_file_sha",
|
|
18
|
+
"session_id",
|
|
19
|
+
"to",
|
|
20
|
+
"targets",
|
|
21
|
+
"delivered_count",
|
|
22
|
+
"failed_count",
|
|
23
|
+
"fallback_path",
|
|
24
|
+
"suggestion",
|
|
25
|
+
]
|
|
26
|
+
compact = {key: result[key] for key in keys if key in result}
|
|
27
|
+
if str(result.get("status") or "").startswith("fanout_"):
|
|
28
|
+
for key in ("deliveries", "recipients"):
|
|
29
|
+
if key in result:
|
|
30
|
+
compact[key] = result[key]
|
|
31
|
+
return compact
|
|
32
|
+
keys = [
|
|
33
|
+
"ok",
|
|
34
|
+
"status",
|
|
35
|
+
"message_id",
|
|
36
|
+
"to",
|
|
37
|
+
"targets",
|
|
38
|
+
"delivered_count",
|
|
39
|
+
"failed_count",
|
|
40
|
+
"submitted",
|
|
41
|
+
"visible",
|
|
42
|
+
"queued",
|
|
43
|
+
"durably_stored",
|
|
44
|
+
"result_id",
|
|
45
|
+
"task_id",
|
|
46
|
+
"agent_id",
|
|
47
|
+
"new_agent_id",
|
|
48
|
+
"source_agent_id",
|
|
49
|
+
"role_file_sha",
|
|
50
|
+
"session_id",
|
|
51
|
+
"leader_notified",
|
|
52
|
+
"notification_message_id",
|
|
53
|
+
"notification_status",
|
|
54
|
+
"notification_channel",
|
|
55
|
+
"notification_event_id",
|
|
56
|
+
]
|
|
57
|
+
compact = {key: result[key] for key in keys if key in result}
|
|
58
|
+
if str(result.get("status") or "").startswith("fanout_"):
|
|
59
|
+
for key in ("deliveries", "recipients"):
|
|
60
|
+
if key in result:
|
|
61
|
+
compact[key] = result[key]
|
|
62
|
+
if "acknowledged_messages" in result:
|
|
63
|
+
compact["acknowledged_count"] = len(result.get("acknowledged_messages") or [])
|
|
64
|
+
return compact or {"ok": True}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _normalize_report_envelope(env: dict[str, Any]) -> dict[str, Any]:
|
|
68
|
+
summary = _text(env.get("summary")) or "completed"
|
|
69
|
+
return {
|
|
70
|
+
"schema_version": "result_envelope_v1",
|
|
71
|
+
"task_id": _text(env.get("task_id")) or "manual",
|
|
72
|
+
"agent_id": _text(env.get("agent_id")) or "unknown",
|
|
73
|
+
"status": _normalize_result_status(env.get("status")),
|
|
74
|
+
"summary": summary,
|
|
75
|
+
"changes": _normalize_changes(env.get("changes"), summary),
|
|
76
|
+
"tests": _normalize_tests(env.get("tests")),
|
|
77
|
+
"risks": _normalize_risks(env.get("risks")),
|
|
78
|
+
"artifacts": _normalize_artifacts(env.get("artifacts")),
|
|
79
|
+
"next_actions": _normalize_next_actions(env.get("next_actions")),
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _items(value: Any) -> list[Any]:
|
|
84
|
+
if value is None:
|
|
85
|
+
return []
|
|
86
|
+
if isinstance(value, list):
|
|
87
|
+
return value
|
|
88
|
+
return [value]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _text(value: Any) -> str | None:
|
|
92
|
+
if value is None:
|
|
93
|
+
return None
|
|
94
|
+
text = str(value).strip()
|
|
95
|
+
return text or None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _first_text(item: dict[str, Any], *keys: str) -> str | None:
|
|
99
|
+
for key in keys:
|
|
100
|
+
text = _text(item.get(key))
|
|
101
|
+
if text:
|
|
102
|
+
return text
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _normalize_result_status(value: Any) -> str:
|
|
107
|
+
text = (_text(value) or "success").lower().replace("-", "_").replace(" ", "_")
|
|
108
|
+
mapping = {
|
|
109
|
+
"ok": "success",
|
|
110
|
+
"done": "success",
|
|
111
|
+
"complete": "success",
|
|
112
|
+
"completed": "success",
|
|
113
|
+
"passed": "success",
|
|
114
|
+
"pass": "success",
|
|
115
|
+
"blocked": "blocked",
|
|
116
|
+
"block": "blocked",
|
|
117
|
+
"failed": "failed",
|
|
118
|
+
"fail": "failed",
|
|
119
|
+
"error": "failed",
|
|
120
|
+
"partial": "partial",
|
|
121
|
+
"partially_done": "partial",
|
|
122
|
+
}
|
|
123
|
+
return mapping.get(text, text if text in {"success", "blocked", "failed", "partial"} else "success")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _normalize_changes(value: Any, fallback_summary: str) -> list[dict[str, str]]:
|
|
127
|
+
changes: list[dict[str, str]] = []
|
|
128
|
+
for item in _items(value):
|
|
129
|
+
if not isinstance(item, dict):
|
|
130
|
+
continue
|
|
131
|
+
path = _first_text(item, "path", "file", "filepath", "filename")
|
|
132
|
+
if not path:
|
|
133
|
+
continue
|
|
134
|
+
description = _first_text(item, "description", "summary", "detail", "details", "message") or fallback_summary
|
|
135
|
+
changes.append(
|
|
136
|
+
{
|
|
137
|
+
"path": path,
|
|
138
|
+
"kind": _normalize_change_kind(_first_text(item, "kind", "type", "action"), description),
|
|
139
|
+
"description": description,
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
return changes
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _normalize_change_kind(value: str | None, description: str) -> str:
|
|
146
|
+
text = (value or "").strip().lower().replace("-", "_").replace(" ", "_")
|
|
147
|
+
if text in {"created", "modified", "deleted", "observed"}:
|
|
148
|
+
return text
|
|
149
|
+
mapping = {
|
|
150
|
+
"create": "created",
|
|
151
|
+
"added": "created",
|
|
152
|
+
"add": "created",
|
|
153
|
+
"new": "created",
|
|
154
|
+
"changed": "modified",
|
|
155
|
+
"change": "modified",
|
|
156
|
+
"updated": "modified",
|
|
157
|
+
"update": "modified",
|
|
158
|
+
"edited": "modified",
|
|
159
|
+
"edit": "modified",
|
|
160
|
+
"removed": "deleted",
|
|
161
|
+
"remove": "deleted",
|
|
162
|
+
"delete": "deleted",
|
|
163
|
+
"observe": "observed",
|
|
164
|
+
"observed": "observed",
|
|
165
|
+
"inspected": "observed",
|
|
166
|
+
"inspect": "observed",
|
|
167
|
+
}
|
|
168
|
+
if text in mapping:
|
|
169
|
+
return mapping[text]
|
|
170
|
+
description_text = description.lower()
|
|
171
|
+
if any(word in description_text for word in ["created", "added", "new file"]):
|
|
172
|
+
return "created"
|
|
173
|
+
if any(word in description_text for word in ["deleted", "removed"]):
|
|
174
|
+
return "deleted"
|
|
175
|
+
if any(word in description_text for word in ["observed", "inspected", "verified"]):
|
|
176
|
+
return "observed"
|
|
177
|
+
return "modified"
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _normalize_tests(value: Any) -> list[dict[str, str]]:
|
|
181
|
+
tests: list[dict[str, str]] = []
|
|
182
|
+
for item in _items(value):
|
|
183
|
+
if not isinstance(item, dict):
|
|
184
|
+
command = _text(item)
|
|
185
|
+
if command:
|
|
186
|
+
tests.append({"command": command, "status": "not_run"})
|
|
187
|
+
continue
|
|
188
|
+
command = _first_text(item, "command", "cmd", "name", "test")
|
|
189
|
+
if not command:
|
|
190
|
+
continue
|
|
191
|
+
test = {"command": command, "status": _normalize_test_status(item.get("status"))}
|
|
192
|
+
detail = _first_text(item, "detail", "output", "stdout", "stderr", "summary", "message")
|
|
193
|
+
if detail:
|
|
194
|
+
test["detail"] = detail
|
|
195
|
+
tests.append(test)
|
|
196
|
+
return tests
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _normalize_test_status(value: Any) -> str:
|
|
200
|
+
text = (_text(value) or "not_run").lower().replace("-", "_").replace(" ", "_")
|
|
201
|
+
mapping = {
|
|
202
|
+
"pass": "passed",
|
|
203
|
+
"ok": "passed",
|
|
204
|
+
"success": "passed",
|
|
205
|
+
"fail": "failed",
|
|
206
|
+
"error": "failed",
|
|
207
|
+
"notrun": "not_run",
|
|
208
|
+
"not_run": "not_run",
|
|
209
|
+
"notrun_yet": "not_run",
|
|
210
|
+
"skip": "skipped",
|
|
211
|
+
}
|
|
212
|
+
return mapping.get(text, text if text in {"passed", "failed", "not_run", "skipped"} else "not_run")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _normalize_risks(value: Any) -> list[dict[str, str]]:
|
|
216
|
+
risks: list[dict[str, str]] = []
|
|
217
|
+
for item in _items(value):
|
|
218
|
+
if not isinstance(item, dict):
|
|
219
|
+
description = _text(item)
|
|
220
|
+
if description:
|
|
221
|
+
risks.append({"severity": "low", "description": description})
|
|
222
|
+
continue
|
|
223
|
+
description = _first_text(item, "description", "summary", "detail", "message")
|
|
224
|
+
if not description:
|
|
225
|
+
continue
|
|
226
|
+
severity = (_first_text(item, "severity", "level") or "low").lower()
|
|
227
|
+
if severity not in {"low", "medium", "high"}:
|
|
228
|
+
severity = "low"
|
|
229
|
+
risks.append({"severity": severity, "description": description})
|
|
230
|
+
return risks
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _normalize_artifacts(value: Any) -> list[dict[str, str]]:
|
|
234
|
+
artifacts: list[dict[str, str]] = []
|
|
235
|
+
for item in _items(value):
|
|
236
|
+
if not isinstance(item, dict):
|
|
237
|
+
path = _text(item)
|
|
238
|
+
if path:
|
|
239
|
+
artifacts.append({"path": path, "description": path})
|
|
240
|
+
continue
|
|
241
|
+
path = _first_text(item, "path", "file", "filepath", "filename")
|
|
242
|
+
if not path:
|
|
243
|
+
continue
|
|
244
|
+
artifacts.append({"path": path, "description": _first_text(item, "description", "summary", "detail") or path})
|
|
245
|
+
return artifacts
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _normalize_next_actions(value: Any) -> list[dict[str, str]]:
|
|
249
|
+
actions: list[dict[str, str]] = []
|
|
250
|
+
for item in _items(value):
|
|
251
|
+
if isinstance(item, dict):
|
|
252
|
+
description = _first_text(item, "description", "summary", "action", "todo", "message")
|
|
253
|
+
else:
|
|
254
|
+
description = _text(item)
|
|
255
|
+
if description:
|
|
256
|
+
actions.append({"description": description})
|
|
257
|
+
return actions
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from team_agent.mcp_server.contracts import TOOLS
|
|
10
|
+
from team_agent.mcp_server.tools import TeamOrchestratorTools
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
ARGUMENT_ERROR_TYPES = (TypeError, ValueError, KeyError, AttributeError)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def dispatch(tools: TeamOrchestratorTools, request: dict[str, Any]) -> dict[str, Any]:
|
|
17
|
+
tool = request.get("tool") or request.get("method")
|
|
18
|
+
args = request.get("arguments") or request.get("params") or {}
|
|
19
|
+
if tool == "assign_task":
|
|
20
|
+
return tools.assign_task(**args)
|
|
21
|
+
if tool == "send_message":
|
|
22
|
+
return tools.send_message(**args)
|
|
23
|
+
if tool == "report_result":
|
|
24
|
+
return tools.report_result(**args)
|
|
25
|
+
if tool == "update_state":
|
|
26
|
+
return tools.update_state(**args)
|
|
27
|
+
if tool == "get_team_status":
|
|
28
|
+
return tools.get_team_status()
|
|
29
|
+
if tool == "stop_agent":
|
|
30
|
+
return tools.stop_agent(**args)
|
|
31
|
+
if tool == "reset_agent":
|
|
32
|
+
return tools.reset_agent(**args)
|
|
33
|
+
if tool == "add_agent":
|
|
34
|
+
return tools.add_agent(**args)
|
|
35
|
+
if tool == "fork_agent":
|
|
36
|
+
return tools.fork_agent(**args)
|
|
37
|
+
if tool == "request_human":
|
|
38
|
+
return tools.request_human(**args)
|
|
39
|
+
if tool == "stuck_list":
|
|
40
|
+
return tools.stuck_list()
|
|
41
|
+
if tool == "stuck_cancel":
|
|
42
|
+
return tools.stuck_cancel(**args)
|
|
43
|
+
return _tool_error_result("unknown_tool", f"unknown tool {tool!r}", exc_type="UnknownTool")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def handle_mcp(tools: TeamOrchestratorTools, request: dict[str, Any]) -> dict[str, Any] | None:
|
|
47
|
+
method = request.get("method")
|
|
48
|
+
msg_id = request.get("id")
|
|
49
|
+
if method and method.startswith("notifications/"):
|
|
50
|
+
return None
|
|
51
|
+
if method == "initialize":
|
|
52
|
+
return {
|
|
53
|
+
"jsonrpc": "2.0",
|
|
54
|
+
"id": msg_id,
|
|
55
|
+
"result": {
|
|
56
|
+
"protocolVersion": request.get("params", {}).get("protocolVersion", "2024-11-05"),
|
|
57
|
+
"capabilities": {"tools": {}},
|
|
58
|
+
"serverInfo": {"name": "team_orchestrator", "version": "0.1.4"},
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
if method == "tools/list":
|
|
62
|
+
return {"jsonrpc": "2.0", "id": msg_id, "result": {"tools": TOOLS}}
|
|
63
|
+
if method == "tools/call":
|
|
64
|
+
params = request.get("params", {})
|
|
65
|
+
name = params.get("name")
|
|
66
|
+
arguments = params.get("arguments") or {}
|
|
67
|
+
try:
|
|
68
|
+
result = dispatch(tools, {"tool": name, "arguments": arguments})
|
|
69
|
+
except ARGUMENT_ERROR_TYPES as exc:
|
|
70
|
+
result = _tool_exception_result(exc, "invalid_tool_arguments")
|
|
71
|
+
except Exception as exc:
|
|
72
|
+
result = _tool_exception_result(exc, "internal_runtime_error")
|
|
73
|
+
is_error = result.get("ok") is False
|
|
74
|
+
return {
|
|
75
|
+
"jsonrpc": "2.0",
|
|
76
|
+
"id": msg_id,
|
|
77
|
+
"result": {
|
|
78
|
+
"content": [
|
|
79
|
+
{
|
|
80
|
+
"type": "text",
|
|
81
|
+
"text": json.dumps(result, ensure_ascii=False),
|
|
82
|
+
}
|
|
83
|
+
],
|
|
84
|
+
"isError": is_error,
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
"jsonrpc": "2.0",
|
|
89
|
+
"id": msg_id,
|
|
90
|
+
"error": {"code": -32601, "message": f"unknown method {method!r}"},
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _tool_exception_result(exc: Exception, reason: str) -> dict[str, str | bool]:
|
|
95
|
+
return _tool_error_result(reason, _public_exception_message(exc), exc_type=type(exc).__name__)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _tool_error_result(reason: str, message: str, exc_type: str) -> dict[str, str | bool]:
|
|
99
|
+
return {
|
|
100
|
+
"ok": False,
|
|
101
|
+
"reason": reason,
|
|
102
|
+
"error_code": reason,
|
|
103
|
+
"exc_type": exc_type,
|
|
104
|
+
"message": message,
|
|
105
|
+
"error": message,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _public_exception_message(exc: Exception) -> str:
|
|
110
|
+
message = str(exc).replace("\n", " ").strip()
|
|
111
|
+
return message[:200] if message else type(exc).__name__
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def main(argv: list[str] | None = None) -> None:
|
|
115
|
+
parser = argparse.ArgumentParser(description="TeamSpec team_orchestrator MCP stdio server")
|
|
116
|
+
parser.add_argument("--workspace", default=".", help="Workspace containing .team/runtime")
|
|
117
|
+
args = parser.parse_args(argv)
|
|
118
|
+
tools = TeamOrchestratorTools(Path(args.workspace))
|
|
119
|
+
for line in sys.stdin:
|
|
120
|
+
line = line.strip()
|
|
121
|
+
if not line:
|
|
122
|
+
continue
|
|
123
|
+
try:
|
|
124
|
+
request = json.loads(line)
|
|
125
|
+
if request.get("jsonrpc") == "2.0":
|
|
126
|
+
response = handle_mcp(tools, request)
|
|
127
|
+
if response is None:
|
|
128
|
+
continue
|
|
129
|
+
sys.stdout.write(json.dumps(response, ensure_ascii=False) + "\n")
|
|
130
|
+
sys.stdout.flush()
|
|
131
|
+
continue
|
|
132
|
+
result = dispatch(tools, request)
|
|
133
|
+
sys.stdout.write(json.dumps({"ok": result.get("ok", True), "result": result}, ensure_ascii=False) + "\n")
|
|
134
|
+
sys.stdout.flush()
|
|
135
|
+
except Exception as exc: # MCP transports need errors surfaced on stdout.
|
|
136
|
+
if "request" in locals() and isinstance(request, dict) and request.get("jsonrpc") == "2.0":
|
|
137
|
+
sys.stdout.write(
|
|
138
|
+
json.dumps(
|
|
139
|
+
{
|
|
140
|
+
"jsonrpc": "2.0",
|
|
141
|
+
"id": request.get("id"),
|
|
142
|
+
"error": {"code": -32000, "message": str(exc)},
|
|
143
|
+
},
|
|
144
|
+
ensure_ascii=False,
|
|
145
|
+
)
|
|
146
|
+
+ "\n"
|
|
147
|
+
)
|
|
148
|
+
else:
|
|
149
|
+
sys.stdout.write(json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False) + "\n")
|
|
150
|
+
sys.stdout.flush()
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from team_agent import runtime
|
|
9
|
+
from team_agent.events import EventLog
|
|
10
|
+
from team_agent.message_store import MessageStore
|
|
11
|
+
from team_agent.state import load_runtime_state, save_runtime_state, write_team_state
|
|
12
|
+
|
|
13
|
+
from team_agent.mcp_server.normalize import _compact_tool_result, _normalize_report_envelope, _text
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _requires_ack_for_target(to: str | list[str]) -> bool:
|
|
17
|
+
if isinstance(to, list):
|
|
18
|
+
return any(target not in {"leader", "Leader"} for target in to)
|
|
19
|
+
return to not in {"leader", "Leader"}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TeamOrchestratorTools:
|
|
23
|
+
def __init__(self, workspace: Path):
|
|
24
|
+
self.workspace = workspace.resolve()
|
|
25
|
+
self.agent_id = _text(os.environ.get("TEAM_AGENT_ID"))
|
|
26
|
+
|
|
27
|
+
def assign_task(self, task: dict[str, Any], message: str | None = None) -> dict[str, Any]:
|
|
28
|
+
state = load_runtime_state(self.workspace)
|
|
29
|
+
tasks = state.setdefault("tasks", [])
|
|
30
|
+
existing = next((item for item in tasks if item.get("id") == task.get("id")), None)
|
|
31
|
+
if existing:
|
|
32
|
+
existing.update(task)
|
|
33
|
+
else:
|
|
34
|
+
tasks.append(task)
|
|
35
|
+
save_runtime_state(self.workspace, state)
|
|
36
|
+
content = message or task.get("description") or task.get("title") or json.dumps(task)
|
|
37
|
+
return _compact_tool_result(runtime.send_message(self.workspace, task.get("assignee"), content, task_id=task["id"]))
|
|
38
|
+
|
|
39
|
+
def send_message(
|
|
40
|
+
self,
|
|
41
|
+
to: str | list[str],
|
|
42
|
+
content: str,
|
|
43
|
+
task_id: str | None = None,
|
|
44
|
+
sender: str | None = None,
|
|
45
|
+
requires_ack: bool | None = None,
|
|
46
|
+
) -> dict[str, Any]:
|
|
47
|
+
inferred_target = to if isinstance(to, str) else None
|
|
48
|
+
effective_sender = sender or self._infer_agent_id(task_id=task_id, target=inferred_target) or "unknown"
|
|
49
|
+
effective_requires_ack = requires_ack if requires_ack is not None else _requires_ack_for_target(to)
|
|
50
|
+
return _compact_tool_result(
|
|
51
|
+
runtime.send_message(
|
|
52
|
+
self.workspace,
|
|
53
|
+
to,
|
|
54
|
+
content,
|
|
55
|
+
task_id=task_id,
|
|
56
|
+
sender=effective_sender,
|
|
57
|
+
requires_ack=effective_requires_ack,
|
|
58
|
+
block_until_delivered=False,
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def report_result(
|
|
63
|
+
self,
|
|
64
|
+
envelope: dict[str, Any] | None = None,
|
|
65
|
+
summary: str | None = None,
|
|
66
|
+
status: str = "success",
|
|
67
|
+
changes: list[dict[str, Any]] | None = None,
|
|
68
|
+
tests: list[dict[str, Any]] | None = None,
|
|
69
|
+
risks: list[dict[str, Any]] | None = None,
|
|
70
|
+
artifacts: list[dict[str, Any]] | None = None,
|
|
71
|
+
next_actions: list[dict[str, Any]] | None = None,
|
|
72
|
+
task_id: str | None = None,
|
|
73
|
+
agent_id: str | None = None,
|
|
74
|
+
) -> dict[str, Any]:
|
|
75
|
+
env = dict(envelope or {})
|
|
76
|
+
effective_task = self._infer_task_id(
|
|
77
|
+
agent_id or _text(env.get("agent_id")) or self.agent_id,
|
|
78
|
+
task_id or _text(env.get("task_id")),
|
|
79
|
+
)
|
|
80
|
+
effective_agent = agent_id or _text(env.get("agent_id")) or self._infer_agent_id(task_id=effective_task) or "unknown"
|
|
81
|
+
env.setdefault("schema_version", "result_envelope_v1")
|
|
82
|
+
env.setdefault("agent_id", effective_agent)
|
|
83
|
+
env.setdefault("task_id", effective_task)
|
|
84
|
+
env.setdefault("status", status)
|
|
85
|
+
env.setdefault("summary", summary or env.get("summary") or "completed")
|
|
86
|
+
env.setdefault("changes", changes if changes is not None else [])
|
|
87
|
+
env.setdefault("tests", tests if tests is not None else [])
|
|
88
|
+
env.setdefault("risks", risks if risks is not None else [])
|
|
89
|
+
env.setdefault("artifacts", artifacts if artifacts is not None else [])
|
|
90
|
+
env.setdefault("next_actions", next_actions if next_actions is not None else [])
|
|
91
|
+
env = _normalize_report_envelope(env)
|
|
92
|
+
return _compact_tool_result(runtime.report_result(self.workspace, env))
|
|
93
|
+
|
|
94
|
+
def _infer_agent_id(self, provided: str | None = None, task_id: str | None = None, target: str | None = None) -> str | None:
|
|
95
|
+
if _text(provided):
|
|
96
|
+
return _text(provided)
|
|
97
|
+
if self.agent_id:
|
|
98
|
+
return self.agent_id
|
|
99
|
+
state = load_runtime_state(self.workspace)
|
|
100
|
+
leader_id = state.get("leader", {}).get("id") or "leader"
|
|
101
|
+
runtime_agents = {str(agent_id) for agent_id in state.get("agents", {})}
|
|
102
|
+
task = self._task_for_id(state, task_id)
|
|
103
|
+
if task and task.get("assignee") in runtime_agents:
|
|
104
|
+
return str(task["assignee"])
|
|
105
|
+
messages = MessageStore(self.workspace).messages()
|
|
106
|
+
if task_id:
|
|
107
|
+
for row in reversed(messages):
|
|
108
|
+
if row.get("task_id") != task_id:
|
|
109
|
+
continue
|
|
110
|
+
for key in ("recipient", "sender"):
|
|
111
|
+
candidate = row.get(key)
|
|
112
|
+
if candidate in runtime_agents and candidate not in {leader_id, "leader", "Leader"}:
|
|
113
|
+
return str(candidate)
|
|
114
|
+
active_assignees = {
|
|
115
|
+
str(task_item.get("assignee"))
|
|
116
|
+
for task_item in state.get("tasks", [])
|
|
117
|
+
if task_item.get("assignee") in runtime_agents and task_item.get("status") not in {"done", "failed"}
|
|
118
|
+
}
|
|
119
|
+
if len(active_assignees) == 1:
|
|
120
|
+
return next(iter(active_assignees))
|
|
121
|
+
if len(runtime_agents) == 1:
|
|
122
|
+
return next(iter(runtime_agents))
|
|
123
|
+
for row in reversed(messages):
|
|
124
|
+
for key in ("recipient", "sender"):
|
|
125
|
+
candidate = row.get(key)
|
|
126
|
+
if candidate in runtime_agents and candidate not in {leader_id, "leader", "Leader"}:
|
|
127
|
+
return str(candidate)
|
|
128
|
+
EventLog(self.workspace).write(
|
|
129
|
+
"mcp.identity_inference_failed",
|
|
130
|
+
target=target,
|
|
131
|
+
task_id=task_id,
|
|
132
|
+
runtime_agents=sorted(runtime_agents),
|
|
133
|
+
fallback="unknown",
|
|
134
|
+
)
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
def _infer_task_id(self, agent_id: str | None, provided: str | None = None) -> str:
|
|
138
|
+
if provided:
|
|
139
|
+
return provided
|
|
140
|
+
state = load_runtime_state(self.workspace)
|
|
141
|
+
for task in reversed(state.get("tasks", [])):
|
|
142
|
+
if agent_id and task.get("assignee") == agent_id and task.get("status") not in {"done", "failed"}:
|
|
143
|
+
return str(task["id"])
|
|
144
|
+
active_tasks = [
|
|
145
|
+
task
|
|
146
|
+
for task in state.get("tasks", [])
|
|
147
|
+
if task.get("assignee") and task.get("status") not in {"done", "failed"}
|
|
148
|
+
]
|
|
149
|
+
if len(active_tasks) == 1:
|
|
150
|
+
return str(active_tasks[0]["id"])
|
|
151
|
+
messages = MessageStore(self.workspace).messages()
|
|
152
|
+
for row in reversed(messages):
|
|
153
|
+
if agent_id and row.get("recipient") == agent_id and row.get("task_id"):
|
|
154
|
+
return str(row["task_id"])
|
|
155
|
+
for row in reversed(messages):
|
|
156
|
+
if agent_id and row.get("recipient") == agent_id:
|
|
157
|
+
return str(row["message_id"])
|
|
158
|
+
for row in reversed(messages):
|
|
159
|
+
if row.get("task_id"):
|
|
160
|
+
return str(row["task_id"])
|
|
161
|
+
EventLog(self.workspace).write("mcp.task_inference_failed", agent_id=agent_id, fallback="manual")
|
|
162
|
+
return "manual"
|
|
163
|
+
|
|
164
|
+
def _task_for_id(self, state: dict[str, Any], task_id: str | None) -> dict[str, Any] | None:
|
|
165
|
+
if not task_id:
|
|
166
|
+
return None
|
|
167
|
+
return next((task for task in state.get("tasks", []) if task.get("id") == task_id), None)
|
|
168
|
+
|
|
169
|
+
def update_state(self, note: str) -> dict[str, Any]:
|
|
170
|
+
state = load_runtime_state(self.workspace)
|
|
171
|
+
spec_path = Path(state.get("spec_path", self.workspace / "team.spec.yaml"))
|
|
172
|
+
from team_agent.spec import load_spec
|
|
173
|
+
|
|
174
|
+
spec = load_spec(spec_path)
|
|
175
|
+
state.setdefault("notes", []).append(note)
|
|
176
|
+
save_runtime_state(self.workspace, state)
|
|
177
|
+
path = write_team_state(self.workspace, spec, state)
|
|
178
|
+
return {"ok": True, "state_file": str(path)}
|
|
179
|
+
|
|
180
|
+
def get_team_status(self) -> dict[str, Any]:
|
|
181
|
+
return runtime.status(self.workspace, as_json=True, compact=True)
|
|
182
|
+
|
|
183
|
+
def stop_agent(self, agent_id: str) -> dict[str, Any]:
|
|
184
|
+
return _compact_tool_result(runtime.stop_agent(self.workspace, agent_id))
|
|
185
|
+
|
|
186
|
+
def reset_agent(self, agent_id: str, discard_session: bool = False) -> dict[str, Any]:
|
|
187
|
+
return _compact_tool_result(runtime.reset_agent(self.workspace, agent_id, discard_session=discard_session))
|
|
188
|
+
|
|
189
|
+
def add_agent(self, new_agent_id: str, role_file_path: str) -> dict[str, Any]:
|
|
190
|
+
return _compact_tool_result(runtime.add_agent(self.workspace, new_agent_id, role_file_path=role_file_path))
|
|
191
|
+
|
|
192
|
+
def fork_agent(self, source_agent_id: str, as_agent_id: str, label: str | None = None) -> dict[str, Any]:
|
|
193
|
+
return _compact_tool_result(runtime.fork_agent(self.workspace, source_agent_id, as_agent_id=as_agent_id, label=label))
|
|
194
|
+
|
|
195
|
+
def request_human(self, question: str, task_id: str | None = None, agent_id: str | None = None) -> dict[str, Any]:
|
|
196
|
+
store = MessageStore(self.workspace)
|
|
197
|
+
sender = agent_id or self._infer_agent_id(task_id=task_id, target="leader") or "unknown"
|
|
198
|
+
message_id = store.create_message(task_id, sender, "leader", question, requires_ack=True)
|
|
199
|
+
return {"ok": True, "message_id": message_id, "status": "needs_human"}
|
|
200
|
+
|
|
201
|
+
def stuck_list(self) -> dict[str, Any]:
|
|
202
|
+
return runtime.stuck_list(self.workspace)
|
|
203
|
+
|
|
204
|
+
def stuck_cancel(self, agent_id: str, alert_type: str = "stuck") -> dict[str, Any]:
|
|
205
|
+
return runtime.stuck_cancel(self.workspace, agent_id, alert_type=alert_type, suppressed_by=self.agent_id or "leader")
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from team_agent.message_store.agent_health import delete_agent_health, gc_agent_health, upsert_agent_health
|
|
4
|
+
from team_agent.message_store.core import MessageStore
|
|
5
|
+
from team_agent.message_store.result_watchers import create_result_watcher, mark_result_watcher
|
|
6
|
+
from team_agent.message_store.schema import SCHEMA_VERSION, initialize_schema, utcnow
|
|
7
|
+
|
|
8
|
+
_REQUIRED_EXPORTS = (
|
|
9
|
+
"MessageStore",
|
|
10
|
+
"SCHEMA_VERSION",
|
|
11
|
+
"initialize_schema",
|
|
12
|
+
"utcnow",
|
|
13
|
+
"upsert_agent_health",
|
|
14
|
+
"delete_agent_health",
|
|
15
|
+
"gc_agent_health",
|
|
16
|
+
"create_result_watcher",
|
|
17
|
+
"mark_result_watcher",
|
|
18
|
+
)
|
|
19
|
+
for _name in _REQUIRED_EXPORTS:
|
|
20
|
+
if _name not in globals():
|
|
21
|
+
raise ImportError(f"team_agent.message_store missing export: {_name}")
|
|
22
|
+
|
|
23
|
+
__all__ = list(_REQUIRED_EXPORTS)
|