agentpool-cli 0.1.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.
- agentpool/__init__.py +3 -0
- agentpool/agent_io.py +134 -0
- agentpool/artifacts.py +151 -0
- agentpool/cli.py +1199 -0
- agentpool/config.py +373 -0
- agentpool/docs/agentpool-skill.md +85 -0
- agentpool/docs/onboarding.md +169 -0
- agentpool/event_detection.py +150 -0
- agentpool/fixtures/__init__.py +1 -0
- agentpool/fixtures/fake_agents/__init__.py +1 -0
- agentpool/fixtures/fake_agents/fake_approval_agent.py +16 -0
- agentpool/fixtures/fake_agents/fake_common.py +44 -0
- agentpool/fixtures/fake_agents/fake_completed_agent.py +13 -0
- agentpool/fixtures/fake_agents/fake_idle_agent.py +16 -0
- agentpool/fixtures/fake_agents/fake_limit_agent.py +14 -0
- agentpool/fixtures/fake_agents/fake_patch_agent.py +17 -0
- agentpool/fixtures/fake_agents/fake_question_agent.py +16 -0
- agentpool/git_worktree.py +144 -0
- agentpool/mcp/__init__.py +1 -0
- agentpool/mcp/resources.py +64 -0
- agentpool/mcp/tools.py +259 -0
- agentpool/mcp_server.py +487 -0
- agentpool/models.py +310 -0
- agentpool/onboarding.py +1279 -0
- agentpool/policy.py +63 -0
- agentpool/provider_model_catalog.json +997 -0
- agentpool/providers/__init__.py +3 -0
- agentpool/providers/base.py +411 -0
- agentpool/providers/registry.py +139 -0
- agentpool/redaction.py +30 -0
- agentpool/runtimes/__init__.py +3 -0
- agentpool/runtimes/base.py +36 -0
- agentpool/runtimes/tmux.py +133 -0
- agentpool/session_manager.py +1061 -0
- agentpool/stats/__init__.py +6 -0
- agentpool/stats/card.py +74 -0
- agentpool/stats/compute.py +496 -0
- agentpool/stats/queries.py +138 -0
- agentpool/stats/render.py +103 -0
- agentpool/stats/window.py +85 -0
- agentpool/store.py +478 -0
- agentpool/usage/__init__.py +1 -0
- agentpool/usage/_common.py +223 -0
- agentpool/usage/ccusage.py +130 -0
- agentpool/usage/claude.py +23 -0
- agentpool/usage/codex.py +210 -0
- agentpool/usage/codexbar.py +186 -0
- agentpool/usage/combine.py +71 -0
- agentpool/usage/copilot.py +146 -0
- agentpool/usage/devin.py +265 -0
- agentpool/usage/parsers.py +41 -0
- agentpool/usage/probes.py +52 -0
- agentpool/usage/provider_parsers.py +276 -0
- agentpool/usage/summary.py +166 -0
- agentpool/utils.py +59 -0
- agentpool_cli-0.1.0.dist-info/METADATA +292 -0
- agentpool_cli-0.1.0.dist-info/RECORD +60 -0
- agentpool_cli-0.1.0.dist-info/WHEEL +4 -0
- agentpool_cli-0.1.0.dist-info/entry_points.txt +2 -0
- agentpool_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
agentpool/__init__.py
ADDED
agentpool/agent_io.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import secrets
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
from agentpool.models import ToolError
|
|
8
|
+
|
|
9
|
+
Detail = Literal["summary", "excerpt", "full"]
|
|
10
|
+
|
|
11
|
+
DETAILS: set[str] = {"summary", "excerpt", "full"}
|
|
12
|
+
EXCERPT_CHARS = 1600
|
|
13
|
+
FULL_CHARS = 8000
|
|
14
|
+
RAW_ARTIFACT_KINDS = {"transcript", "events", "screen", "summary", "result", "diff"}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def parse_detail(value: str) -> Detail:
|
|
18
|
+
normalized = value.strip().lower()
|
|
19
|
+
if normalized in DETAILS:
|
|
20
|
+
return normalized # type: ignore[return-value]
|
|
21
|
+
raise ToolError(
|
|
22
|
+
"INVALID_DETAIL",
|
|
23
|
+
"Detail must be summary, excerpt, or full.",
|
|
24
|
+
{"detail": value, "example": "--detail excerpt"},
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def read_stdin_text(stdin_text: str, label: str, example: str) -> str:
|
|
29
|
+
text = stdin_text.strip()
|
|
30
|
+
if text:
|
|
31
|
+
return text
|
|
32
|
+
raise ToolError(
|
|
33
|
+
"INVALID_STDIN",
|
|
34
|
+
f"No {label} was provided on stdin.",
|
|
35
|
+
{"example": example},
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def wrap_untrusted(text: str, detail: Detail) -> dict[str, Any]:
|
|
40
|
+
limit = EXCERPT_CHARS if detail == "excerpt" else FULL_CHARS
|
|
41
|
+
token = secrets.token_hex(8)
|
|
42
|
+
begin = f"BEGIN_UNTRUSTED_WORKER_OUTPUT_{token}"
|
|
43
|
+
end = f"END_UNTRUSTED_WORKER_OUTPUT_{token}"
|
|
44
|
+
clipped = tail_text(text, limit)
|
|
45
|
+
escaped = clipped.replace(begin, f"ESCAPED_{begin}").replace(end, f"ESCAPED_{end}")
|
|
46
|
+
return {
|
|
47
|
+
"included": True,
|
|
48
|
+
"detail": detail,
|
|
49
|
+
"truncated": len(text.strip()) > len(clipped),
|
|
50
|
+
"chars": len(escaped),
|
|
51
|
+
"text": f"{begin}\n{escaped}\n{end}",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def omitted_worker_output(detail: Detail, lockdown: bool = False) -> dict[str, Any]:
|
|
56
|
+
reason = "lockdown" if lockdown else f"detail={detail}"
|
|
57
|
+
return {"included": False, "detail": detail, "reason": reason}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def tail_text(text: str, limit: int) -> str:
|
|
61
|
+
stripped = text.strip()
|
|
62
|
+
if len(stripped) <= limit:
|
|
63
|
+
return stripped
|
|
64
|
+
return stripped[-limit:]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def compact_artifact_manifest(manifest: dict[str, Any], lockdown: bool = False) -> dict[str, Any]:
|
|
68
|
+
files = []
|
|
69
|
+
for file in manifest.get("files") or []:
|
|
70
|
+
files.append(gate_raw_artifact(file, lockdown))
|
|
71
|
+
return {
|
|
72
|
+
"session_id": manifest.get("session_id"),
|
|
73
|
+
"artifact_dir": manifest.get("artifact_dir"),
|
|
74
|
+
"files": files,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def observe_payload(
|
|
79
|
+
response: dict[str, Any],
|
|
80
|
+
artifact_manifest: dict[str, Any],
|
|
81
|
+
detail: Detail,
|
|
82
|
+
lockdown: bool = False,
|
|
83
|
+
) -> dict[str, Any]:
|
|
84
|
+
payload = {
|
|
85
|
+
"session_id": response.get("session_id"),
|
|
86
|
+
"state": response.get("state"),
|
|
87
|
+
"event": response.get("event"),
|
|
88
|
+
"confidence": response.get("confidence"),
|
|
89
|
+
"metadata": response.get("metadata") or {},
|
|
90
|
+
"artifact_manifest": compact_artifact_manifest(artifact_manifest, lockdown=lockdown),
|
|
91
|
+
}
|
|
92
|
+
if response.get("parsed_question") and detail != "summary" and not lockdown:
|
|
93
|
+
payload["parsed_question"] = wrap_untrusted(str(response["parsed_question"]), "excerpt")
|
|
94
|
+
else:
|
|
95
|
+
payload["parsed_question_available"] = bool(response.get("parsed_question"))
|
|
96
|
+
text = response.get("screen_excerpt") or response.get("recent_log") or ""
|
|
97
|
+
if detail == "summary" or lockdown or not text:
|
|
98
|
+
payload["worker_output"] = omitted_worker_output(detail, lockdown)
|
|
99
|
+
else:
|
|
100
|
+
payload["worker_output"] = wrap_untrusted(str(text), detail)
|
|
101
|
+
return payload
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def collect_payload(result: dict[str, Any], detail: Detail, lockdown: bool = False) -> dict[str, Any]:
|
|
105
|
+
payload = {
|
|
106
|
+
"session_id": result.get("session_id"),
|
|
107
|
+
"state": result.get("state"),
|
|
108
|
+
"artifact_dir": result.get("artifact_dir"),
|
|
109
|
+
"artifacts": [gate_raw_artifact(artifact, lockdown) for artifact in result.get("artifacts") or []],
|
|
110
|
+
"git": result.get("git"),
|
|
111
|
+
}
|
|
112
|
+
summary = str(result.get("summary") or "")
|
|
113
|
+
if detail == "summary" or lockdown or not summary:
|
|
114
|
+
payload["worker_output"] = omitted_worker_output(detail, lockdown)
|
|
115
|
+
else:
|
|
116
|
+
payload["worker_output"] = wrap_untrusted(summary, detail)
|
|
117
|
+
return payload
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def lockdown_resource(path: str | Path, kind: str) -> dict[str, Any]:
|
|
121
|
+
return {
|
|
122
|
+
"blocked": True,
|
|
123
|
+
"reason": "lockdown",
|
|
124
|
+
"kind": kind,
|
|
125
|
+
"path": str(path),
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def gate_raw_artifact(artifact: dict[str, Any], lockdown: bool) -> dict[str, Any]:
|
|
130
|
+
row = dict(artifact)
|
|
131
|
+
if lockdown and row.get("kind") in RAW_ARTIFACT_KINDS:
|
|
132
|
+
row["gated"] = True
|
|
133
|
+
row["gated_reason"] = "lockdown"
|
|
134
|
+
return row
|
agentpool/artifacts.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from agentpool.event_detection import extract_result_body
|
|
7
|
+
from agentpool.git_worktree import changed_files, git_diff, git_status, is_git_repo
|
|
8
|
+
from agentpool.models import AgentSession, ArtifactRecord
|
|
9
|
+
from agentpool.redaction import redact_text
|
|
10
|
+
from agentpool.utils import repo_hash, sha256_file, write_json
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def create_artifact_dir(root: Path, repo_path: Path, session_id: str) -> Path:
|
|
14
|
+
artifact_dir = root / repo_hash(repo_path) / session_id
|
|
15
|
+
(artifact_dir / "raw" / "tmux-captures").mkdir(parents=True, exist_ok=True)
|
|
16
|
+
return artifact_dir
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def initialize_artifacts(session: AgentSession, prompt: str) -> None:
|
|
20
|
+
artifact_dir = Path(session.artifact_dir)
|
|
21
|
+
artifact_dir.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
Path(session.transcript_path).write_text("", encoding="utf-8")
|
|
23
|
+
Path(session.events_path).write_text("", encoding="utf-8")
|
|
24
|
+
(artifact_dir / "prompt.md").write_text(prompt, encoding="utf-8")
|
|
25
|
+
write_json(artifact_dir / "metadata.json", session.model_dump(mode="json"))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def append_transcript(session: AgentSession, text: str) -> None:
|
|
29
|
+
path = Path(session.transcript_path)
|
|
30
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
existing = path.read_text(encoding="utf-8") if path.exists() else ""
|
|
32
|
+
if text and text not in existing[-max(len(text), 1) :]:
|
|
33
|
+
with path.open("a", encoding="utf-8") as fh:
|
|
34
|
+
if existing and not existing.endswith("\n"):
|
|
35
|
+
fh.write("\n")
|
|
36
|
+
fh.write(text)
|
|
37
|
+
if not text.endswith("\n"):
|
|
38
|
+
fh.write("\n")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def collect_artifacts(session: AgentSession, screen: str, include_diff: bool = True) -> dict[str, Any]:
|
|
42
|
+
artifact_dir = Path(session.artifact_dir)
|
|
43
|
+
artifact_dir.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
(artifact_dir / "latest_screen.txt").write_text(screen, encoding="utf-8")
|
|
45
|
+
append_transcript(session, screen)
|
|
46
|
+
workdir = Path(session.worktree_path or session.repo_path)
|
|
47
|
+
status_text = git_status(workdir)
|
|
48
|
+
diff_text = redact_text(git_diff(workdir)) if include_diff else ""
|
|
49
|
+
(artifact_dir / "git-status.txt").write_text(status_text, encoding="utf-8")
|
|
50
|
+
if include_diff:
|
|
51
|
+
(artifact_dir / "diff.patch").write_text(diff_text, encoding="utf-8")
|
|
52
|
+
summary = materialize_result_artifacts(session, screen) or "No AGENTPOOL result marker found."
|
|
53
|
+
(artifact_dir / "summary.md").write_text(summary, encoding="utf-8")
|
|
54
|
+
(artifact_dir / "result.md").write_text(summary, encoding="utf-8")
|
|
55
|
+
write_json(
|
|
56
|
+
artifact_dir / "metadata.json",
|
|
57
|
+
{
|
|
58
|
+
**session.model_dump(mode="json"),
|
|
59
|
+
"git": {
|
|
60
|
+
"is_repo": is_git_repo(workdir),
|
|
61
|
+
"dirty": bool(status_text.strip()),
|
|
62
|
+
"changed_files": changed_files(workdir) if is_git_repo(workdir) else [],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
)
|
|
66
|
+
artifacts: list[ArtifactRecord] = []
|
|
67
|
+
for kind, filename in [
|
|
68
|
+
("metadata", "metadata.json"),
|
|
69
|
+
("prompt", "prompt.md"),
|
|
70
|
+
("transcript", "transcript.txt"),
|
|
71
|
+
("events", "events.jsonl"),
|
|
72
|
+
("screen", "latest_screen.txt"),
|
|
73
|
+
("summary", "summary.md"),
|
|
74
|
+
("result", "result.md"),
|
|
75
|
+
("git_status", "git-status.txt"),
|
|
76
|
+
("diff", "diff.patch"),
|
|
77
|
+
]:
|
|
78
|
+
path = artifact_dir / filename
|
|
79
|
+
if path.exists():
|
|
80
|
+
artifacts.append(ArtifactRecord(kind=kind, path=str(path), sha256=sha256_file(path)))
|
|
81
|
+
return {
|
|
82
|
+
"session_id": session.id,
|
|
83
|
+
"state": session.state.value if hasattr(session.state, "value") else session.state,
|
|
84
|
+
"artifact_dir": str(artifact_dir),
|
|
85
|
+
"artifacts": [artifact.model_dump(mode="json") for artifact in artifacts],
|
|
86
|
+
"summary": summary,
|
|
87
|
+
"git": {
|
|
88
|
+
"is_repo": is_git_repo(workdir),
|
|
89
|
+
"dirty": bool(status_text.strip()),
|
|
90
|
+
"changed_files": changed_files(workdir) if is_git_repo(workdir) else [],
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def artifact_manifest(session: AgentSession) -> dict[str, Any]:
|
|
96
|
+
materialize_result_artifacts(session)
|
|
97
|
+
artifact_dir = Path(session.artifact_dir)
|
|
98
|
+
files = []
|
|
99
|
+
for kind, filename in [
|
|
100
|
+
("metadata", "metadata.json"),
|
|
101
|
+
("prompt", "prompt.md"),
|
|
102
|
+
("transcript", "transcript.txt"),
|
|
103
|
+
("events", "events.jsonl"),
|
|
104
|
+
("screen", "latest_screen.txt"),
|
|
105
|
+
("summary", "summary.md"),
|
|
106
|
+
("result", "result.md"),
|
|
107
|
+
("git_status", "git-status.txt"),
|
|
108
|
+
("diff", "diff.patch"),
|
|
109
|
+
]:
|
|
110
|
+
path = artifact_dir / filename
|
|
111
|
+
files.append(
|
|
112
|
+
{
|
|
113
|
+
"kind": kind,
|
|
114
|
+
"path": str(path),
|
|
115
|
+
"exists": path.exists(),
|
|
116
|
+
"sha256": sha256_file(path) if path.exists() else None,
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
return {
|
|
120
|
+
"session_id": session.id,
|
|
121
|
+
"artifact_dir": str(artifact_dir),
|
|
122
|
+
"files": files,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def materialize_result_artifacts(session: AgentSession, screen: str = "") -> str | None:
|
|
127
|
+
artifact_dir = Path(session.artifact_dir)
|
|
128
|
+
candidates = [screen]
|
|
129
|
+
latest_screen = artifact_dir / "latest_screen.txt"
|
|
130
|
+
if latest_screen.exists():
|
|
131
|
+
candidates.append(latest_screen.read_text(encoding="utf-8"))
|
|
132
|
+
transcript = Path(session.transcript_path)
|
|
133
|
+
if transcript.exists():
|
|
134
|
+
candidates.append(transcript.read_text(encoding="utf-8"))
|
|
135
|
+
for candidate in candidates:
|
|
136
|
+
summary = extract_result(candidate)
|
|
137
|
+
if summary:
|
|
138
|
+
artifact_dir.mkdir(parents=True, exist_ok=True)
|
|
139
|
+
(artifact_dir / "summary.md").write_text(summary, encoding="utf-8")
|
|
140
|
+
(artifact_dir / "result.md").write_text(summary, encoding="utf-8")
|
|
141
|
+
return summary
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def extract_result(screen: str) -> str | None:
|
|
146
|
+
body = extract_result_body(screen)
|
|
147
|
+
if body:
|
|
148
|
+
return body
|
|
149
|
+
if "AGENTPOOL_SMOKE_DONE" in screen:
|
|
150
|
+
return "AGENTPOOL_SMOKE_DONE"
|
|
151
|
+
return None
|