@team-agent/installer 0.1.0
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/README.md +201 -0
- package/crates/team-agent-core/Cargo.toml +12 -0
- package/crates/team-agent-core/src/lib.rs +287 -0
- package/crates/team-agent-core/src/main.rs +152 -0
- package/examples/team.spec.yaml +206 -0
- package/examples/team_state.md +35 -0
- package/npm/install.mjs +266 -0
- package/package.json +28 -0
- package/pyproject.toml +18 -0
- package/schemas/result-envelope.schema.json +76 -0
- package/schemas/team.schema.json +241 -0
- package/scripts/install.py +88 -0
- package/scripts/run_regression_tests.py +79 -0
- package/skills/team-agent/SKILL.md +173 -0
- package/src/team_agent/__init__.py +3 -0
- package/src/team_agent/__main__.py +5 -0
- package/src/team_agent/cli.py +857 -0
- package/src/team_agent/compiler.py +269 -0
- package/src/team_agent/coordinator.py +62 -0
- package/src/team_agent/errors.py +10 -0
- package/src/team_agent/events.py +37 -0
- package/src/team_agent/fake_worker.py +80 -0
- package/src/team_agent/mcp_server.py +579 -0
- package/src/team_agent/message_store.py +497 -0
- package/src/team_agent/paths.py +45 -0
- package/src/team_agent/permissions.py +123 -0
- package/src/team_agent/profiles.py +882 -0
- package/src/team_agent/providers.py +1045 -0
- package/src/team_agent/routing.py +84 -0
- package/src/team_agent/runtime.py +5213 -0
- package/src/team_agent/rust_core.py +156 -0
- package/src/team_agent/simple_yaml.py +236 -0
- package/src/team_agent/spec.py +308 -0
- package/src/team_agent/state.py +112 -0
- package/src/team_agent/task_graph.py +80 -0
- package/templates/team_state.md +32 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from team_agent.paths import repo_root
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def core_binary() -> Path | None:
|
|
14
|
+
configured = shutil.which("team-agent-core")
|
|
15
|
+
if configured:
|
|
16
|
+
return Path(configured)
|
|
17
|
+
local = repo_root() / "crates" / "team-agent-core" / "target" / "debug" / "team-agent-core"
|
|
18
|
+
if local.exists():
|
|
19
|
+
return local
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def call_core(command: str, payload: dict[str, Any] | str | None = None) -> dict[str, Any]:
|
|
24
|
+
binary = core_binary()
|
|
25
|
+
if not binary:
|
|
26
|
+
return {"ok": False, "error": "team-agent-core binary not found", "fallback": True}
|
|
27
|
+
raw = json.dumps(payload, ensure_ascii=False) if isinstance(payload, dict) else (payload or "")
|
|
28
|
+
proc = subprocess.run(
|
|
29
|
+
[str(binary), command, "--json"],
|
|
30
|
+
input=raw,
|
|
31
|
+
text=True,
|
|
32
|
+
capture_output=True,
|
|
33
|
+
timeout=10,
|
|
34
|
+
check=False,
|
|
35
|
+
)
|
|
36
|
+
try:
|
|
37
|
+
result = json.loads(proc.stdout or "{}")
|
|
38
|
+
except json.JSONDecodeError:
|
|
39
|
+
result = {"ok": False, "error": proc.stdout.strip() or proc.stderr.strip()}
|
|
40
|
+
if proc.returncode != 0:
|
|
41
|
+
result.setdefault("ok", False)
|
|
42
|
+
result.setdefault("error", proc.stderr.strip() or "team-agent-core failed")
|
|
43
|
+
result["engine"] = "rust" if result.get("ok") else "rust_failed"
|
|
44
|
+
return result
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def render_message(payload: dict[str, Any]) -> dict[str, Any]:
|
|
48
|
+
result = call_core("render-message", payload)
|
|
49
|
+
if result.get("ok"):
|
|
50
|
+
return result
|
|
51
|
+
sender = payload.get("from") or payload.get("sender") or "unknown"
|
|
52
|
+
task_id = payload.get("task_id")
|
|
53
|
+
content = payload.get("content") or ""
|
|
54
|
+
token = payload.get("message_id") or "missing"
|
|
55
|
+
header = f"Team Agent message from {sender}"
|
|
56
|
+
if task_id:
|
|
57
|
+
header += f" for {task_id}"
|
|
58
|
+
return {
|
|
59
|
+
"ok": True,
|
|
60
|
+
"text": f"{header}:\n\n{content}\n\n[team-agent-token:{token}]",
|
|
61
|
+
"token": token,
|
|
62
|
+
"engine": "python_fallback",
|
|
63
|
+
"fallback_reason": result.get("error"),
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def redact_text(text: str) -> dict[str, Any]:
|
|
68
|
+
result = call_core("redact", {"text": text})
|
|
69
|
+
if result.get("ok"):
|
|
70
|
+
return result
|
|
71
|
+
redacted = []
|
|
72
|
+
for chunk in text.split():
|
|
73
|
+
lower = chunk.lower()
|
|
74
|
+
if (
|
|
75
|
+
"api_key" in lower
|
|
76
|
+
or "apikey" in lower
|
|
77
|
+
or "token=" in lower
|
|
78
|
+
or "secret" in lower
|
|
79
|
+
or lower == "bearer"
|
|
80
|
+
or chunk.startswith("sk-")
|
|
81
|
+
or _looks_base64_secret(chunk)
|
|
82
|
+
):
|
|
83
|
+
redacted.append("[REDACTED]")
|
|
84
|
+
else:
|
|
85
|
+
redacted.append(chunk)
|
|
86
|
+
return {"ok": True, "text": " ".join(redacted), "engine": "python_fallback", "fallback_reason": result.get("error")}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def validate_profile_metadata(profile: dict[str, Any]) -> dict[str, Any]:
|
|
90
|
+
result = call_core("validate-profile", profile)
|
|
91
|
+
if result.get("ok") or result.get("errors"):
|
|
92
|
+
return result
|
|
93
|
+
errors: list[str] = []
|
|
94
|
+
if profile.get("auth_mode") not in {"subscription", "official_api", "compatible_api"}:
|
|
95
|
+
errors.append("auth_mode must be subscription, official_api, or compatible_api")
|
|
96
|
+
for field in ["provider", "model", "profile"]:
|
|
97
|
+
if not profile.get(field):
|
|
98
|
+
errors.append(f"{field} must not be empty")
|
|
99
|
+
if contains_inline_secret(str(profile.get(field) or "")):
|
|
100
|
+
errors.append("profile metadata contains a probable inline secret")
|
|
101
|
+
return {"ok": not errors, "errors": errors, "engine": "python_fallback", "fallback_reason": result.get("error")}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def list_targets() -> dict[str, Any]:
|
|
105
|
+
result = call_core("list-targets")
|
|
106
|
+
if result.get("ok"):
|
|
107
|
+
return result
|
|
108
|
+
proc = subprocess.run(
|
|
109
|
+
[
|
|
110
|
+
"tmux",
|
|
111
|
+
"list-panes",
|
|
112
|
+
"-a",
|
|
113
|
+
"-F",
|
|
114
|
+
"#{pane_id}\t#{session_name}\t#{window_index}\t#{window_name}\t#{pane_index}\t#{pane_tty}\t#{pane_current_command}\t#{pane_active}",
|
|
115
|
+
],
|
|
116
|
+
text=True,
|
|
117
|
+
capture_output=True,
|
|
118
|
+
timeout=5,
|
|
119
|
+
check=False,
|
|
120
|
+
)
|
|
121
|
+
if proc.returncode != 0:
|
|
122
|
+
return {"ok": False, "error": proc.stderr.strip() or "tmux list-panes failed", "engine": "python_fallback"}
|
|
123
|
+
targets = []
|
|
124
|
+
for line in proc.stdout.splitlines():
|
|
125
|
+
parts = line.split("\t")
|
|
126
|
+
if len(parts) != 8:
|
|
127
|
+
continue
|
|
128
|
+
target = {
|
|
129
|
+
"pane_id": parts[0],
|
|
130
|
+
"session_name": parts[1],
|
|
131
|
+
"window_index": parts[2],
|
|
132
|
+
"window_name": parts[3],
|
|
133
|
+
"pane_index": parts[4],
|
|
134
|
+
"pane_tty": parts[5],
|
|
135
|
+
"pane_current_command": parts[6],
|
|
136
|
+
"pane_active": parts[7] == "1",
|
|
137
|
+
}
|
|
138
|
+
target["fingerprint"] = f"{target['session_name']}|{target['window_index']}|{target['pane_index']}|{target['pane_tty']}"
|
|
139
|
+
targets.append(target)
|
|
140
|
+
return {"ok": True, "targets": targets, "engine": "python_fallback", "fallback_reason": result.get("error")}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def contains_inline_secret(value: str) -> bool:
|
|
144
|
+
lower = value.lower()
|
|
145
|
+
return (
|
|
146
|
+
"api_key" in lower
|
|
147
|
+
or "apikey" in lower
|
|
148
|
+
or "token" in lower
|
|
149
|
+
or "secret" in lower
|
|
150
|
+
or value.startswith("sk-")
|
|
151
|
+
or _looks_base64_secret(value)
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _looks_base64_secret(value: str) -> bool:
|
|
156
|
+
return len(value) >= 32 and re.fullmatch(r"[A-Za-z0-9+/=_-]+", value) is not None
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import json
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def loads(text: str) -> Any:
|
|
9
|
+
stripped = text.lstrip()
|
|
10
|
+
if stripped.startswith("{") or stripped.startswith("["):
|
|
11
|
+
return json.loads(text)
|
|
12
|
+
lines = text.splitlines()
|
|
13
|
+
value, index = _parse_block(lines, 0, 0)
|
|
14
|
+
while index < len(lines) and not _content(lines[index]):
|
|
15
|
+
index += 1
|
|
16
|
+
if index != len(lines):
|
|
17
|
+
raise ValueError(f"unexpected content at line {index + 1}: {lines[index]}")
|
|
18
|
+
return value
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def dumps(value: Any, indent: int = 0) -> str:
|
|
22
|
+
lines = _dump(value, indent)
|
|
23
|
+
return "\n".join(lines) + "\n"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _parse_block(lines: list[str], index: int, indent: int) -> tuple[Any, int]:
|
|
27
|
+
index = _skip_blank(lines, index)
|
|
28
|
+
if index >= len(lines):
|
|
29
|
+
return None, index
|
|
30
|
+
current_indent = _indent(lines[index])
|
|
31
|
+
if current_indent < indent:
|
|
32
|
+
return None, index
|
|
33
|
+
if _stripped(lines[index]).startswith("- "):
|
|
34
|
+
return _parse_list(lines, index, current_indent)
|
|
35
|
+
return _parse_dict(lines, index, current_indent)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _parse_dict(lines: list[str], index: int, indent: int) -> tuple[dict[str, Any], int]:
|
|
39
|
+
obj: dict[str, Any] = {}
|
|
40
|
+
while index < len(lines):
|
|
41
|
+
if not _content(lines[index]):
|
|
42
|
+
index += 1
|
|
43
|
+
continue
|
|
44
|
+
line_indent = _indent(lines[index])
|
|
45
|
+
if line_indent < indent:
|
|
46
|
+
break
|
|
47
|
+
if line_indent > indent:
|
|
48
|
+
raise ValueError(f"unexpected indentation at line {index + 1}: {lines[index]}")
|
|
49
|
+
stripped = _stripped(lines[index])
|
|
50
|
+
if stripped.startswith("- "):
|
|
51
|
+
break
|
|
52
|
+
key, raw = _split_key_value(stripped, index)
|
|
53
|
+
if raw == "|":
|
|
54
|
+
value, index = _parse_block_scalar(lines, index + 1, indent + 2)
|
|
55
|
+
elif raw == "":
|
|
56
|
+
value, index = _parse_block(lines, index + 1, indent + 2)
|
|
57
|
+
else:
|
|
58
|
+
value = _parse_scalar(raw)
|
|
59
|
+
index += 1
|
|
60
|
+
obj[key] = value
|
|
61
|
+
return obj, index
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _parse_list(lines: list[str], index: int, indent: int) -> tuple[list[Any], int]:
|
|
65
|
+
items: list[Any] = []
|
|
66
|
+
while index < len(lines):
|
|
67
|
+
if not _content(lines[index]):
|
|
68
|
+
index += 1
|
|
69
|
+
continue
|
|
70
|
+
line_indent = _indent(lines[index])
|
|
71
|
+
if line_indent < indent:
|
|
72
|
+
break
|
|
73
|
+
if line_indent != indent:
|
|
74
|
+
raise ValueError(f"unexpected list indentation at line {index + 1}: {lines[index]}")
|
|
75
|
+
stripped = _stripped(lines[index])
|
|
76
|
+
if not stripped.startswith("- "):
|
|
77
|
+
break
|
|
78
|
+
item_text = stripped[2:].strip()
|
|
79
|
+
if item_text == "":
|
|
80
|
+
value, index = _parse_block(lines, index + 1, indent + 2)
|
|
81
|
+
items.append(value)
|
|
82
|
+
continue
|
|
83
|
+
if _looks_like_key_value(item_text):
|
|
84
|
+
key, raw = _split_key_value(item_text, index)
|
|
85
|
+
item: dict[str, Any] = {}
|
|
86
|
+
if raw == "|":
|
|
87
|
+
value, next_index = _parse_block_scalar(lines, index + 1, indent + 2)
|
|
88
|
+
elif raw == "":
|
|
89
|
+
value, next_index = _parse_block(lines, index + 1, indent + 2)
|
|
90
|
+
else:
|
|
91
|
+
value = _parse_scalar(raw)
|
|
92
|
+
next_index = index + 1
|
|
93
|
+
item[key] = value
|
|
94
|
+
if next_index < len(lines) and _indent(lines[next_index]) == indent + 2:
|
|
95
|
+
extra, next_index = _parse_dict(lines, next_index, indent + 2)
|
|
96
|
+
item.update(extra)
|
|
97
|
+
items.append(item)
|
|
98
|
+
index = next_index
|
|
99
|
+
else:
|
|
100
|
+
items.append(_parse_scalar(item_text))
|
|
101
|
+
index += 1
|
|
102
|
+
return items, index
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _parse_block_scalar(lines: list[str], index: int, indent: int) -> tuple[str, int]:
|
|
106
|
+
block: list[str] = []
|
|
107
|
+
while index < len(lines):
|
|
108
|
+
if not lines[index].strip():
|
|
109
|
+
block.append("")
|
|
110
|
+
index += 1
|
|
111
|
+
continue
|
|
112
|
+
line_indent = _indent(lines[index])
|
|
113
|
+
if line_indent < indent:
|
|
114
|
+
break
|
|
115
|
+
block.append(lines[index][indent:])
|
|
116
|
+
index += 1
|
|
117
|
+
return "\n".join(block).rstrip() + "\n", index
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _parse_scalar(raw: str) -> Any:
|
|
121
|
+
if raw in {"null", "Null", "NULL", "~"}:
|
|
122
|
+
return None
|
|
123
|
+
if raw in {"true", "True", "TRUE"}:
|
|
124
|
+
return True
|
|
125
|
+
if raw in {"false", "False", "FALSE"}:
|
|
126
|
+
return False
|
|
127
|
+
try:
|
|
128
|
+
return int(raw)
|
|
129
|
+
except ValueError:
|
|
130
|
+
pass
|
|
131
|
+
if raw.startswith("[") and raw.endswith("]"):
|
|
132
|
+
try:
|
|
133
|
+
return ast.literal_eval(raw)
|
|
134
|
+
except (SyntaxError, ValueError):
|
|
135
|
+
return raw
|
|
136
|
+
if raw == "{}":
|
|
137
|
+
return {}
|
|
138
|
+
if (raw.startswith('"') and raw.endswith('"')) or (raw.startswith("'") and raw.endswith("'")):
|
|
139
|
+
try:
|
|
140
|
+
return ast.literal_eval(raw)
|
|
141
|
+
except (SyntaxError, ValueError):
|
|
142
|
+
return raw[1:-1]
|
|
143
|
+
return raw
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _dump(value: Any, indent: int) -> list[str]:
|
|
147
|
+
pad = " " * indent
|
|
148
|
+
if isinstance(value, dict):
|
|
149
|
+
lines: list[str] = []
|
|
150
|
+
for key, item in value.items():
|
|
151
|
+
if item == []:
|
|
152
|
+
lines.append(f"{pad}{key}: []")
|
|
153
|
+
elif item == {}:
|
|
154
|
+
lines.append(f"{pad}{key}: {{}}")
|
|
155
|
+
elif isinstance(item, (dict, list)):
|
|
156
|
+
lines.append(f"{pad}{key}:")
|
|
157
|
+
lines.extend(_dump(item, indent + 2))
|
|
158
|
+
elif isinstance(item, str) and "\n" in item:
|
|
159
|
+
lines.append(f"{pad}{key}: |")
|
|
160
|
+
for block_line in item.rstrip("\n").splitlines():
|
|
161
|
+
lines.append(f"{pad} {block_line}")
|
|
162
|
+
else:
|
|
163
|
+
lines.append(f"{pad}{key}: {_format_scalar(item)}")
|
|
164
|
+
return lines
|
|
165
|
+
if isinstance(value, list):
|
|
166
|
+
lines = []
|
|
167
|
+
for item in value:
|
|
168
|
+
if isinstance(item, dict):
|
|
169
|
+
if not item:
|
|
170
|
+
lines.append(f"{pad}- {{}}")
|
|
171
|
+
continue
|
|
172
|
+
first = True
|
|
173
|
+
for key, child in item.items():
|
|
174
|
+
prefix = "- " if first else " "
|
|
175
|
+
if child == []:
|
|
176
|
+
lines.append(f"{pad}{prefix}{key}: []")
|
|
177
|
+
elif child == {}:
|
|
178
|
+
lines.append(f"{pad}{prefix}{key}: {{}}")
|
|
179
|
+
elif isinstance(child, (dict, list)):
|
|
180
|
+
lines.append(f"{pad}{prefix}{key}:")
|
|
181
|
+
lines.extend(_dump(child, indent + 4))
|
|
182
|
+
else:
|
|
183
|
+
lines.append(f"{pad}{prefix}{key}: {_format_scalar(child)}")
|
|
184
|
+
first = False
|
|
185
|
+
elif isinstance(item, list):
|
|
186
|
+
lines.append(f"{pad}-")
|
|
187
|
+
lines.extend(_dump(item, indent + 2))
|
|
188
|
+
else:
|
|
189
|
+
lines.append(f"{pad}- {_format_scalar(item)}")
|
|
190
|
+
return lines
|
|
191
|
+
return [f"{pad}{_format_scalar(value)}"]
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _format_scalar(value: Any) -> str:
|
|
195
|
+
if value is None:
|
|
196
|
+
return "null"
|
|
197
|
+
if value is True:
|
|
198
|
+
return "true"
|
|
199
|
+
if value is False:
|
|
200
|
+
return "false"
|
|
201
|
+
if isinstance(value, int):
|
|
202
|
+
return str(value)
|
|
203
|
+
return json.dumps(str(value), ensure_ascii=False)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _split_key_value(stripped: str, index: int) -> tuple[str, str]:
|
|
207
|
+
if ":" not in stripped:
|
|
208
|
+
raise ValueError(f"expected key: value at line {index + 1}")
|
|
209
|
+
key, raw = stripped.split(":", 1)
|
|
210
|
+
return key.strip(), raw.strip()
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _looks_like_key_value(text: str) -> bool:
|
|
214
|
+
if ":" not in text:
|
|
215
|
+
return False
|
|
216
|
+
key = text.split(":", 1)[0]
|
|
217
|
+
return bool(key) and all(ch.isalnum() or ch in "_-" for ch in key)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _content(line: str) -> bool:
|
|
221
|
+
stripped = line.strip()
|
|
222
|
+
return bool(stripped) and not stripped.startswith("#")
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _skip_blank(lines: list[str], index: int) -> int:
|
|
226
|
+
while index < len(lines) and not _content(lines[index]):
|
|
227
|
+
index += 1
|
|
228
|
+
return index
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _indent(line: str) -> int:
|
|
232
|
+
return len(line) - len(line.lstrip(" "))
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _stripped(line: str) -> str:
|
|
236
|
+
return line.strip()
|