agent-team 0.5.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.
- agent_team/__init__.py +3 -0
- agent_team/agent.py +109 -0
- agent_team/budget.py +56 -0
- agent_team/cli.py +664 -0
- agent_team/config.py +239 -0
- agent_team/config_io.py +229 -0
- agent_team/context.py +126 -0
- agent_team/dashboard/__init__.py +1 -0
- agent_team/dashboard/server.py +410 -0
- agent_team/dashboard/static/__init__.py +0 -0
- agent_team/dashboard/static/activity.js +110 -0
- agent_team/dashboard/static/app.css +214 -0
- agent_team/dashboard/static/app.js +351 -0
- agent_team/dashboard/static/edit.js +312 -0
- agent_team/dashboard/static/index.html +55 -0
- agent_team/dashboard/static/projects.js +53 -0
- agent_team/dashboard/static/runs.js +148 -0
- agent_team/dashboard/static/settings.js +117 -0
- agent_team/discovery.py +88 -0
- agent_team/estimate.py +60 -0
- agent_team/evals.py +186 -0
- agent_team/files.py +247 -0
- agent_team/gates.py +71 -0
- agent_team/github.py +152 -0
- agent_team/issues.py +101 -0
- agent_team/jobs.py +244 -0
- agent_team/keys.py +110 -0
- agent_team/logs.py +106 -0
- agent_team/memory.py +87 -0
- agent_team/metrics.py +102 -0
- agent_team/notify.py +65 -0
- agent_team/projects.py +83 -0
- agent_team/prompts/__init__.py +0 -0
- agent_team/prompts/builder.md +15 -0
- agent_team/prompts/devops.md +28 -0
- agent_team/prompts/discuss.md +29 -0
- agent_team/prompts/docs.md +12 -0
- agent_team/prompts/factcheck.md +8 -0
- agent_team/prompts/lead.md +33 -0
- agent_team/prompts/marketing.md +18 -0
- agent_team/prompts/member.md +10 -0
- agent_team/prompts/qa.md +17 -0
- agent_team/prompts/review_marketing.md +14 -0
- agent_team/prompts/review_pr.md +26 -0
- agent_team/prompts/security.md +20 -0
- agent_team/prompts/signoff.md +13 -0
- agent_team/providers/__init__.py +8 -0
- agent_team/providers/anthropic_provider.py +150 -0
- agent_team/providers/base.py +96 -0
- agent_team/providers/http_providers.py +238 -0
- agent_team/rank.py +67 -0
- agent_team/readiness.py +189 -0
- agent_team/review_tools.py +142 -0
- agent_team/routing.py +28 -0
- agent_team/sandbox.py +105 -0
- agent_team/schedule.py +182 -0
- agent_team/scoreboard.py +83 -0
- agent_team/screenshots.py +135 -0
- agent_team/secret_scan.py +89 -0
- agent_team/server.py +119 -0
- agent_team/skills.py +96 -0
- agent_team/templates.py +109 -0
- agent_team/tool_loop.py +58 -0
- agent_team/ui.py +59 -0
- agent_team/usage.py +136 -0
- agent_team/workflows/__init__.py +1 -0
- agent_team/workflows/autopilot.py +163 -0
- agent_team/workflows/build.py +305 -0
- agent_team/workflows/discuss.py +84 -0
- agent_team/workflows/fix.py +173 -0
- agent_team/workflows/init.py +253 -0
- agent_team/workflows/market.py +86 -0
- agent_team/workflows/setup.py +56 -0
- agent_team/workflows/ship.py +539 -0
- agent_team-0.5.0.dist-info/METADATA +180 -0
- agent_team-0.5.0.dist-info/RECORD +80 -0
- agent_team-0.5.0.dist-info/WHEEL +5 -0
- agent_team-0.5.0.dist-info/entry_points.txt +2 -0
- agent_team-0.5.0.dist-info/licenses/LICENSE +21 -0
- agent_team-0.5.0.dist-info/top_level.txt +1 -0
agent_team/__init__.py
ADDED
agent_team/agent.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""A team member at runtime: its config, its provider, and an append-only conversation."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from importlib import resources
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from . import budget, providers, ui, usage
|
|
8
|
+
from .config import AgentConfig, Team
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def role_prompt(team: Team, agent: AgentConfig, name: str | None = None) -> str:
|
|
12
|
+
"""The agent's prompt text: its own file if configured, else the packaged one for its role."""
|
|
13
|
+
if agent.prompt and name is None:
|
|
14
|
+
return (team.root / agent.prompt).read_text(encoding="utf-8")
|
|
15
|
+
fname = f"{name or agent.role}.md"
|
|
16
|
+
try:
|
|
17
|
+
return resources.files("agent_team.prompts").joinpath(fname).read_text(encoding="utf-8")
|
|
18
|
+
except FileNotFoundError:
|
|
19
|
+
return resources.files("agent_team.prompts").joinpath("member.md").read_text(encoding="utf-8")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def team_roster(team: Team) -> str:
|
|
23
|
+
lines = []
|
|
24
|
+
for a in team.agents.values():
|
|
25
|
+
where = team.providers[a.provider]
|
|
26
|
+
place = f"remote node {where.url}" if where.kind == "agent-http" else f"{where.kind} model {a.model}"
|
|
27
|
+
desc = f" {a.description}" if a.description else ""
|
|
28
|
+
owns = f" Owns: {', '.join(a.owns)}." if a.owns else ""
|
|
29
|
+
skills = f" Skills: {', '.join(_skill_names(team, a))}." if a.skills else ""
|
|
30
|
+
lines.append(f"- {a.name} ({a.role}, {place}).{desc}{owns}{skills}")
|
|
31
|
+
return "\n".join(lines)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Agent:
|
|
35
|
+
def __init__(self, team: Team, cfg: AgentConfig, system: str, label_suffix: str = ""):
|
|
36
|
+
self.team, self.cfg, self.system = team, cfg, system
|
|
37
|
+
pcfg = team.providers[cfg.provider]
|
|
38
|
+
self.provider = providers.build(pcfg, team.secret(pcfg))
|
|
39
|
+
self.label = ui.label(cfg.name, cfg.role + label_suffix, cfg.color)
|
|
40
|
+
self.messages: list[dict] = [] # append-only, so provider-side reasoning stays valid
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def name(self) -> str:
|
|
44
|
+
return self.cfg.name
|
|
45
|
+
|
|
46
|
+
def turn(self, content) -> str:
|
|
47
|
+
if not self.provider.supports_images and not isinstance(content, str):
|
|
48
|
+
content = providers.base.to_text(content)
|
|
49
|
+
budget.check(self.team)
|
|
50
|
+
self.messages.append({"role": "user", "content": content})
|
|
51
|
+
reply = self.provider.complete(self.system, self.messages, model=self.cfg.model,
|
|
52
|
+
effort=self.cfg.effort, label=self.label)
|
|
53
|
+
self.messages.append({"role": "assistant", "content": reply.content})
|
|
54
|
+
# A remote node reports the model it really ran, so its member is priced correctly.
|
|
55
|
+
u = dict(reply.usage or {})
|
|
56
|
+
kind, model = u.pop("kind", "") or self.provider.kind, u.pop("model", "") or self.cfg.model
|
|
57
|
+
usage.record(self.team.root, self.cfg.name, kind, model, u)
|
|
58
|
+
return reply.text
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _skill_names(team: Team, cfg: AgentConfig) -> list[str]:
|
|
62
|
+
from . import skills
|
|
63
|
+
names = []
|
|
64
|
+
for entry in cfg.skills:
|
|
65
|
+
try:
|
|
66
|
+
names.append(skills.load_one(team, entry).name)
|
|
67
|
+
except Exception:
|
|
68
|
+
names.append(entry)
|
|
69
|
+
return names
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def make(team: Team, cfg: AgentConfig, prompt_name: str | None = None, label_suffix: str = "",
|
|
73
|
+
**fields) -> Agent:
|
|
74
|
+
"""Build an agent whose system prompt is its role prompt filled with team and project facts."""
|
|
75
|
+
template = role_prompt(team, cfg, prompt_name)
|
|
76
|
+
values = {
|
|
77
|
+
"name": cfg.name, "role": cfg.role, "project": team.project.name, "repo": team.project.repo,
|
|
78
|
+
"roster": team_roster(team), "owns": ", ".join(cfg.owns) or "nothing",
|
|
79
|
+
"guarantees": "\n".join(f"- {g}" for g in team.project.guarantees) or "- (none listed)",
|
|
80
|
+
"test_command": team.project.test_command or "(none configured)",
|
|
81
|
+
**fields,
|
|
82
|
+
}
|
|
83
|
+
system = _fill(template, values)
|
|
84
|
+
from . import memory
|
|
85
|
+
learned = memory.for_prompt(team.root)
|
|
86
|
+
if learned:
|
|
87
|
+
system = f"{system.rstrip()}\n\n{learned}"
|
|
88
|
+
if cfg.skills:
|
|
89
|
+
from . import skills
|
|
90
|
+
system = f"{system.rstrip()}\n\n{skills.render(skills.load(team, cfg))}"
|
|
91
|
+
return Agent(team, cfg, system, label_suffix)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _fill(template: str, values: dict) -> str:
|
|
95
|
+
"""str.format that leaves unknown {placeholders} and literal braces alone."""
|
|
96
|
+
class Keep(dict):
|
|
97
|
+
def __missing__(self, key):
|
|
98
|
+
return "{" + key + "}"
|
|
99
|
+
try:
|
|
100
|
+
return template.format_map(Keep(values))
|
|
101
|
+
except (ValueError, IndexError):
|
|
102
|
+
out = template
|
|
103
|
+
for k, v in values.items():
|
|
104
|
+
out = out.replace("{" + k + "}", str(v))
|
|
105
|
+
return out
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def prompt_path(team: Team, agent: AgentConfig) -> Path | None:
|
|
109
|
+
return team.root / agent.prompt if agent.prompt else None
|
agent_team/budget.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Spend limits: stop a run before it costs more than `[limits]` allows.
|
|
2
|
+
|
|
3
|
+
[limits]
|
|
4
|
+
per_run = 5.0 # US dollars for one workflow run
|
|
5
|
+
per_day = 20.0 # US dollars per UTC day, across every run in this project
|
|
6
|
+
|
|
7
|
+
Checked before every model call, from the estimates in `.team/usage.jsonl`. A call already in
|
|
8
|
+
flight finishes, so a run can go over by at most one reply.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from . import usage
|
|
13
|
+
from .config import Team
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SpendLimitError(RuntimeError):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def spent(team: Team, run: str | None = None) -> dict:
|
|
21
|
+
"""Estimated dollars spent in this run and today (UTC)."""
|
|
22
|
+
run = usage.current_run() if run is None else run
|
|
23
|
+
today = usage.now()[:10]
|
|
24
|
+
out = {"run": 0.0, "today": 0.0}
|
|
25
|
+
for e in usage.load(team.root):
|
|
26
|
+
cost = usage.price_for(e.get("model", ""), e.get("kind", ""), team.pricing).cost(e)
|
|
27
|
+
if run and e.get("run") == run:
|
|
28
|
+
out["run"] += cost
|
|
29
|
+
if (e.get("at") or "")[:10] == today:
|
|
30
|
+
out["today"] += cost
|
|
31
|
+
return {k: round(v, 4) for k, v in out.items()}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def milestone_spent(team: Team, milestone: str) -> float:
|
|
35
|
+
return round(sum(usage.price_for(e.get("model", ""), e.get("kind", ""), team.pricing).cost(e)
|
|
36
|
+
for e in usage.load(team.root) if e.get("milestone") == milestone), 4)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def check(team: Team) -> None:
|
|
40
|
+
limits = team.limits
|
|
41
|
+
milestone = usage.current_milestone()
|
|
42
|
+
cap = limits.milestones.get(milestone) if milestone else None
|
|
43
|
+
if cap:
|
|
44
|
+
spent_m = milestone_spent(team, milestone)
|
|
45
|
+
if spent_m >= cap:
|
|
46
|
+
raise SpendLimitError(f"milestone {milestone} has spent an estimated ${spent_m:.2f}, reaching its "
|
|
47
|
+
f"[limits.milestones] budget of ${cap:.2f}")
|
|
48
|
+
if not (limits.per_run or limits.per_day):
|
|
49
|
+
return
|
|
50
|
+
s = spent(team)
|
|
51
|
+
if limits.per_run and s["run"] >= limits.per_run:
|
|
52
|
+
raise SpendLimitError(f"this run has spent an estimated ${s['run']:.2f}, reaching the "
|
|
53
|
+
f"[limits].per_run limit of ${limits.per_run:.2f}")
|
|
54
|
+
if limits.per_day and s["today"] >= limits.per_day:
|
|
55
|
+
raise SpendLimitError(f"this project has spent an estimated ${s['today']:.2f} today (UTC), reaching "
|
|
56
|
+
f"the [limits].per_day limit of ${limits.per_day:.2f}")
|