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.
Files changed (60) hide show
  1. agentpool/__init__.py +3 -0
  2. agentpool/agent_io.py +134 -0
  3. agentpool/artifacts.py +151 -0
  4. agentpool/cli.py +1199 -0
  5. agentpool/config.py +373 -0
  6. agentpool/docs/agentpool-skill.md +85 -0
  7. agentpool/docs/onboarding.md +169 -0
  8. agentpool/event_detection.py +150 -0
  9. agentpool/fixtures/__init__.py +1 -0
  10. agentpool/fixtures/fake_agents/__init__.py +1 -0
  11. agentpool/fixtures/fake_agents/fake_approval_agent.py +16 -0
  12. agentpool/fixtures/fake_agents/fake_common.py +44 -0
  13. agentpool/fixtures/fake_agents/fake_completed_agent.py +13 -0
  14. agentpool/fixtures/fake_agents/fake_idle_agent.py +16 -0
  15. agentpool/fixtures/fake_agents/fake_limit_agent.py +14 -0
  16. agentpool/fixtures/fake_agents/fake_patch_agent.py +17 -0
  17. agentpool/fixtures/fake_agents/fake_question_agent.py +16 -0
  18. agentpool/git_worktree.py +144 -0
  19. agentpool/mcp/__init__.py +1 -0
  20. agentpool/mcp/resources.py +64 -0
  21. agentpool/mcp/tools.py +259 -0
  22. agentpool/mcp_server.py +487 -0
  23. agentpool/models.py +310 -0
  24. agentpool/onboarding.py +1279 -0
  25. agentpool/policy.py +63 -0
  26. agentpool/provider_model_catalog.json +997 -0
  27. agentpool/providers/__init__.py +3 -0
  28. agentpool/providers/base.py +411 -0
  29. agentpool/providers/registry.py +139 -0
  30. agentpool/redaction.py +30 -0
  31. agentpool/runtimes/__init__.py +3 -0
  32. agentpool/runtimes/base.py +36 -0
  33. agentpool/runtimes/tmux.py +133 -0
  34. agentpool/session_manager.py +1061 -0
  35. agentpool/stats/__init__.py +6 -0
  36. agentpool/stats/card.py +74 -0
  37. agentpool/stats/compute.py +496 -0
  38. agentpool/stats/queries.py +138 -0
  39. agentpool/stats/render.py +103 -0
  40. agentpool/stats/window.py +85 -0
  41. agentpool/store.py +478 -0
  42. agentpool/usage/__init__.py +1 -0
  43. agentpool/usage/_common.py +223 -0
  44. agentpool/usage/ccusage.py +130 -0
  45. agentpool/usage/claude.py +23 -0
  46. agentpool/usage/codex.py +210 -0
  47. agentpool/usage/codexbar.py +186 -0
  48. agentpool/usage/combine.py +71 -0
  49. agentpool/usage/copilot.py +146 -0
  50. agentpool/usage/devin.py +265 -0
  51. agentpool/usage/parsers.py +41 -0
  52. agentpool/usage/probes.py +52 -0
  53. agentpool/usage/provider_parsers.py +276 -0
  54. agentpool/usage/summary.py +166 -0
  55. agentpool/utils.py +59 -0
  56. agentpool_cli-0.1.0.dist-info/METADATA +292 -0
  57. agentpool_cli-0.1.0.dist-info/RECORD +60 -0
  58. agentpool_cli-0.1.0.dist-info/WHEEL +4 -0
  59. agentpool_cli-0.1.0.dist-info/entry_points.txt +2 -0
  60. agentpool_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
agentpool/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """AgentPool local agent control plane."""
2
+
3
+ __version__ = "0.1.0"
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