agentpool-cli 0.1.6__tar.gz → 0.1.8__tar.gz
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_cli-0.1.6 → agentpool_cli-0.1.8}/CHANGELOG.md +17 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/PKG-INFO +7 -3
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/README.md +6 -2
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/docs/agentpool-skill.md +3 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/docs/usage-detection.md +9 -2
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/pyproject.toml +1 -1
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/server.json +2 -2
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/__init__.py +1 -1
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/config.py +8 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/git_worktree.py +7 -18
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/mcp/tools.py +1 -1
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/onboarding.py +2 -1
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/runtimes/tmux.py +27 -31
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/session_manager.py +34 -1
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/stats/compute.py +2 -3
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/usage/_common.py +3 -29
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/usage/codex.py +3 -5
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/usage/summary.py +1 -4
- agentpool_cli-0.1.8/src/agentpool/utils.py +139 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/tests/unit/test_models_config_store.py +79 -0
- agentpool_cli-0.1.8/tests/unit/test_subprocess_safety.py +143 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/tests/unit/test_usage_probes.py +5 -13
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/tests/unit/test_usage_summary_enrichment.py +5 -4
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/uv.lock +1 -1
- agentpool_cli-0.1.6/src/agentpool/utils.py +0 -59
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/.cursor/mcp.json.example +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/.github/CODEOWNERS +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/.github/ISSUE_TEMPLATE/provider_probe.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/.github/dependabot.yml +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/.github/workflows/ci.yml +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/.github/workflows/release.yml +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/.gitignore +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/.mcp.json.example +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/AGENTS.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/CONTRIBUTING.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/LICENSE +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/SECURITY.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/docs/agent-cli-and-mcp.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/docs/architecture.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/docs/examples/README.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/docs/examples.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/docs/install.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/docs/mcp-clients.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/docs/mcp-tools.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/docs/model-catalog.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/docs/onboarding.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/docs/provider-adapters.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/docs/provider-lifecycle-matrix.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/docs/quickstart.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/docs/release.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/docs/security.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/docs/setup-claude-code.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/docs/setup-codex.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/docs/setup-copilot.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/docs/setup-cursor-cli.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/docs/setup-cursor.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/docs/setup-devin.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/docs/setup-droid.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/docs/stats.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/docs/usage-probe-matrix.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/scripts/install.sh +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/agent_io.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/artifacts.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/cli.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/event_detection.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/fixtures/__init__.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/fixtures/fake_agents/__init__.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/fixtures/fake_agents/fake_approval_agent.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/fixtures/fake_agents/fake_common.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/fixtures/fake_agents/fake_completed_agent.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/fixtures/fake_agents/fake_idle_agent.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/fixtures/fake_agents/fake_limit_agent.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/fixtures/fake_agents/fake_patch_agent.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/fixtures/fake_agents/fake_question_agent.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/mcp/__init__.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/mcp/resources.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/mcp_server.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/models.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/policy.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/preferences.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/provider_model_catalog.json +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/providers/__init__.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/providers/base.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/providers/registry.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/redaction.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/runtimes/__init__.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/runtimes/base.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/stats/__init__.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/stats/card.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/stats/queries.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/stats/render.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/stats/window.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/store.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/usage/__init__.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/usage/ccusage.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/usage/claude.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/usage/codexbar.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/usage/combine.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/usage/copilot.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/usage/devin.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/usage/parsers.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/usage/probes.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/src/agentpool/usage/provider_parsers.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/tests/fixtures/provider_model_catalog_golden.json +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/tests/fixtures/stats_seed.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/tests/fixtures/usage/claude_usage.txt +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/tests/fixtures/usage/codex_rate_limits.json +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/tests/fixtures/usage/copilot_user.json +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/tests/fixtures/usage/devin_plan_status.json +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/tests/integration/test_fake_tmux_flow.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/tests/unit/test_agent_io.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/tests/unit/test_cli.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/tests/unit/test_event_policy.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/tests/unit/test_mcp_surface.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/tests/unit/test_mcp_tools.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/tests/unit/test_onboarding.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/tests/unit/test_redaction.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/tests/unit/test_stats_cli.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/tests/unit/test_stats_mcp.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/tests/unit/test_stats_window.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.8}/tests/unit/test_usage_provider_parsers.py +0 -0
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.1.8 - 2026-06-01
|
|
6
|
+
|
|
7
|
+
- Treat stale usage snapshots as age metadata, not as an unusable-provider
|
|
8
|
+
reason. `usage-summary` still reports `stale` and `age_seconds`, but a stale
|
|
9
|
+
cache entry no longer overrides usable quota/status data.
|
|
10
|
+
- Add optional `policy.usage_auto_refresh_after_seconds` for users who want
|
|
11
|
+
cached usage summaries to refresh automatically after a configured age.
|
|
12
|
+
|
|
13
|
+
## 0.1.7 - 2026-06-01
|
|
14
|
+
|
|
15
|
+
- Centralize non-interactive subprocess execution behind terminal-safe helpers.
|
|
16
|
+
Git, tmux client operations, provider detection, Cursor status checks, Codex
|
|
17
|
+
app-server probes, and external usage helpers now detach from the host TTY by
|
|
18
|
+
default.
|
|
19
|
+
- Add a subprocess-safety regression test so new product code cannot introduce
|
|
20
|
+
raw `subprocess.run`/`Popen` calls outside the shared utility.
|
|
21
|
+
|
|
5
22
|
## 0.1.6 - 2026-06-01
|
|
6
23
|
|
|
7
24
|
- Run external usage helper commands in isolated, non-interactive subprocess
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentpool-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.8
|
|
4
4
|
Summary: Make full use of every coding-agent subscription you pay for: a local CLI + MCP server that surfaces live usage limits and offloads work to providers with headroom.
|
|
5
5
|
Author: AgentPool contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -171,14 +171,18 @@ agentpool usage-summary --refresh --backend ccusage --provider claude-code --jso
|
|
|
171
171
|
|
|
172
172
|
`usage-summary` returns a `providers` object keyed by provider id. It is not
|
|
173
173
|
ordered and it is not a recommendation list. Each row includes `usable`,
|
|
174
|
-
`unusable_reason`, quota windows, confidence, staleness, and reset timing when
|
|
174
|
+
`unusable_reason`, quota windows, confidence, age/staleness, and reset timing when
|
|
175
175
|
the provider exposes it. The older CLI `capacity-summary` command is retained
|
|
176
176
|
as a human convenience alias; the MCP surface only exposes `get_usage_snapshot`
|
|
177
177
|
and the opt-in `get_usage_summary` tool.
|
|
178
178
|
|
|
179
179
|
The default buffer is `policy.min_remaining_percent = 10`. If any reported
|
|
180
180
|
quota window is below that buffer, the provider row is marked unusable for the
|
|
181
|
-
summary.
|
|
181
|
+
summary. Staleness is reported as age information only; it does not by itself
|
|
182
|
+
make a provider unusable. If you want cached summary reads to refresh
|
|
183
|
+
automatically after a threshold, set `policy.usage_auto_refresh_after_seconds`
|
|
184
|
+
in `~/.agentpool/config.yaml`.
|
|
185
|
+
AgentPool still does not pick an alternative provider for you.
|
|
182
186
|
MCP usage refreshes are intentionally bounded and may return `partial=true`;
|
|
183
187
|
use the CLI commands above when a shell-capable agent needs a complete live
|
|
184
188
|
refresh.
|
|
@@ -150,14 +150,18 @@ agentpool usage-summary --refresh --backend ccusage --provider claude-code --jso
|
|
|
150
150
|
|
|
151
151
|
`usage-summary` returns a `providers` object keyed by provider id. It is not
|
|
152
152
|
ordered and it is not a recommendation list. Each row includes `usable`,
|
|
153
|
-
`unusable_reason`, quota windows, confidence, staleness, and reset timing when
|
|
153
|
+
`unusable_reason`, quota windows, confidence, age/staleness, and reset timing when
|
|
154
154
|
the provider exposes it. The older CLI `capacity-summary` command is retained
|
|
155
155
|
as a human convenience alias; the MCP surface only exposes `get_usage_snapshot`
|
|
156
156
|
and the opt-in `get_usage_summary` tool.
|
|
157
157
|
|
|
158
158
|
The default buffer is `policy.min_remaining_percent = 10`. If any reported
|
|
159
159
|
quota window is below that buffer, the provider row is marked unusable for the
|
|
160
|
-
summary.
|
|
160
|
+
summary. Staleness is reported as age information only; it does not by itself
|
|
161
|
+
make a provider unusable. If you want cached summary reads to refresh
|
|
162
|
+
automatically after a threshold, set `policy.usage_auto_refresh_after_seconds`
|
|
163
|
+
in `~/.agentpool/config.yaml`.
|
|
164
|
+
AgentPool still does not pick an alternative provider for you.
|
|
161
165
|
MCP usage refreshes are intentionally bounded and may return `partial=true`;
|
|
162
166
|
use the CLI commands above when a shell-capable agent needs a complete live
|
|
163
167
|
refresh.
|
|
@@ -27,6 +27,9 @@ need to delegate coding-agent work.
|
|
|
27
27
|
live refresh. MCP `refresh=true` is bounded and can return `partial=true`
|
|
28
28
|
with unknown rows for slow providers.
|
|
29
29
|
- Treat usage rows as a provider-id map. They are not ordered and not ranked.
|
|
30
|
+
- Treat `stale` and `age_seconds` as age metadata, not as an instruction to
|
|
31
|
+
avoid a provider. If the user configured usage auto-refresh, cached summary
|
|
32
|
+
reads may refresh themselves before returning.
|
|
30
33
|
- Inspect provider models before spawning when the model is not already chosen:
|
|
31
34
|
- CLI: `agentpool models --provider <provider-id>`
|
|
32
35
|
- MCP: `get_provider_models(provider_id=...)`
|
|
@@ -9,11 +9,18 @@ Live probes are only run by explicit usage requests. Inventory remains non-invas
|
|
|
9
9
|
`agentpool usage-summary` returns a `providers` map keyed by provider id. The
|
|
10
10
|
CLI `capacity-summary` command is a human convenience alias; MCP does not expose
|
|
11
11
|
a capacity alias. Each row includes `usable`, `unusable_reason`, `stale`, and
|
|
12
|
-
`age_seconds`. `
|
|
13
|
-
|
|
12
|
+
`age_seconds`. `stale` is informational age metadata only; it does not by
|
|
13
|
+
itself make a provider unusable. `usable` is derived from install/auth status,
|
|
14
|
+
provider usage status, confidence, and reported quota windows. The default
|
|
14
15
|
buffer is `policy.min_remaining_percent = 10`, and it applies to every reported
|
|
15
16
|
quota window.
|
|
16
17
|
|
|
18
|
+
Cached summary reads can optionally refresh themselves when old enough. Set
|
|
19
|
+
`policy.usage_auto_refresh_after_seconds` to a non-negative number in
|
|
20
|
+
`~/.agentpool/config.yaml` to refresh `usage-summary` / `get_usage_summary`
|
|
21
|
+
when the cached summary data is missing or older than that threshold. Leave it
|
|
22
|
+
as `null` to keep refreshes explicit.
|
|
23
|
+
|
|
17
24
|
Usage windows carry a stable `kind` in addition to provider-specific names:
|
|
18
25
|
|
|
19
26
|
- `daily`
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agentpool-cli"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.8"
|
|
8
8
|
description = "Make full use of every coding-agent subscription you pay for: a local CLI + MCP server that surfaces live usage limits and offloads work to providers with headroom."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "io.github.sidduHERE/agentpool",
|
|
4
4
|
"title": "AgentPool",
|
|
5
5
|
"description": "See each coding-agent subscription's live limits and offload work to one with headroom.",
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.8",
|
|
7
7
|
"repository": {
|
|
8
8
|
"url": "https://github.com/sidduHERE/agentpool",
|
|
9
9
|
"source": "github"
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
{
|
|
14
14
|
"registryType": "pypi",
|
|
15
15
|
"identifier": "agentpool-cli",
|
|
16
|
-
"version": "0.1.
|
|
16
|
+
"version": "0.1.8",
|
|
17
17
|
"transport": {
|
|
18
18
|
"type": "stdio"
|
|
19
19
|
},
|
|
@@ -68,6 +68,7 @@ class PolicyConfig(BaseModel):
|
|
|
68
68
|
default_isolation: str = "read_only"
|
|
69
69
|
min_remaining_percent: int = 10
|
|
70
70
|
usage_stale_after_seconds: int = 1800
|
|
71
|
+
usage_auto_refresh_after_seconds: int | None = None
|
|
71
72
|
allowed_providers: list[str] = Field(default_factory=list)
|
|
72
73
|
denied_providers: list[str] = Field(default_factory=list)
|
|
73
74
|
block_on_usage_statuses: list[str] = Field(
|
|
@@ -326,6 +327,13 @@ def validate_config(config: AgentPoolConfig) -> dict[str, Any]:
|
|
|
326
327
|
errors.append("policy.allow_auto_routing must stay false in AgentPool v0.1")
|
|
327
328
|
if "auto" in config.providers:
|
|
328
329
|
errors.append("providers.auto is not allowed")
|
|
330
|
+
if config.policy.usage_stale_after_seconds < 0:
|
|
331
|
+
errors.append("policy.usage_stale_after_seconds must be non-negative")
|
|
332
|
+
if (
|
|
333
|
+
config.policy.usage_auto_refresh_after_seconds is not None
|
|
334
|
+
and config.policy.usage_auto_refresh_after_seconds < 0
|
|
335
|
+
):
|
|
336
|
+
errors.append("policy.usage_auto_refresh_after_seconds must be non-negative or null")
|
|
329
337
|
if "factory-droid" in config.providers:
|
|
330
338
|
warnings.append("factory-droid is a PRD compatibility name; use droid-cli for the droid binary")
|
|
331
339
|
for provider_id, reason in DEPRECATED_PROVIDER_IDS.items():
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import subprocess
|
|
4
2
|
from pathlib import Path
|
|
5
3
|
|
|
6
4
|
from agentpool.models import ToolError
|
|
@@ -41,12 +39,9 @@ def create_worktree(repo_path: Path, provider_id: str, session_id: str) -> Path:
|
|
|
41
39
|
parent.mkdir(parents=True, exist_ok=True)
|
|
42
40
|
worktree_path = parent / session_id
|
|
43
41
|
branch = agentpool_branch(provider_id, session_id)
|
|
44
|
-
proc =
|
|
42
|
+
proc = run_capture(
|
|
45
43
|
["git", "worktree", "add", "-b", branch, str(worktree_path)],
|
|
46
|
-
cwd=
|
|
47
|
-
text=True,
|
|
48
|
-
capture_output=True,
|
|
49
|
-
check=False,
|
|
44
|
+
cwd=repo_path,
|
|
50
45
|
)
|
|
51
46
|
if proc.returncode != 0:
|
|
52
47
|
raise ToolError(
|
|
@@ -66,12 +61,9 @@ def delete_agentpool_branch(repo_path: Path, provider_id: str, session_id: str)
|
|
|
66
61
|
if not is_git_repo(repo_path):
|
|
67
62
|
return {"deleted": False, "reason": "not_git_repo"}
|
|
68
63
|
branch = agentpool_branch(provider_id, session_id)
|
|
69
|
-
proc =
|
|
64
|
+
proc = run_capture(
|
|
70
65
|
["git", "branch", "-D", branch],
|
|
71
|
-
cwd=
|
|
72
|
-
text=True,
|
|
73
|
-
capture_output=True,
|
|
74
|
-
check=False,
|
|
66
|
+
cwd=repo_path,
|
|
75
67
|
)
|
|
76
68
|
if proc.returncode != 0:
|
|
77
69
|
return {"deleted": False, "branch": branch, "stderr": proc.stderr.strip()}
|
|
@@ -81,12 +73,9 @@ def delete_agentpool_branch(repo_path: Path, provider_id: str, session_id: str)
|
|
|
81
73
|
def list_agentpool_worktrees(repo_path: Path) -> list[dict[str, str | bool]]:
|
|
82
74
|
if not is_git_repo(repo_path):
|
|
83
75
|
return []
|
|
84
|
-
proc =
|
|
76
|
+
proc = run_capture(
|
|
85
77
|
["git", "worktree", "list", "--porcelain"],
|
|
86
|
-
cwd=
|
|
87
|
-
text=True,
|
|
88
|
-
capture_output=True,
|
|
89
|
-
check=False,
|
|
78
|
+
cwd=repo_path,
|
|
90
79
|
)
|
|
91
80
|
if proc.returncode != 0:
|
|
92
81
|
return []
|
|
@@ -128,7 +117,7 @@ def cleanup_worktree(repo_path: Path, worktree_path: Path, force: bool = False)
|
|
|
128
117
|
if force:
|
|
129
118
|
args.append("--force")
|
|
130
119
|
args.append(str(worktree_path))
|
|
131
|
-
proc =
|
|
120
|
+
proc = run_capture(args, cwd=repo_path)
|
|
132
121
|
if proc.returncode != 0:
|
|
133
122
|
raise ToolError(
|
|
134
123
|
"WORKTREE_CLEANUP_FAILED",
|
|
@@ -492,7 +492,8 @@ def privacy_doctor(manager: SessionManager) -> dict[str, Any]:
|
|
|
492
492
|
},
|
|
493
493
|
"usage_refresh": {
|
|
494
494
|
"inventory_runs_live_usage_probes": False,
|
|
495
|
-
"live_usage_requires_explicit_refresh":
|
|
495
|
+
"live_usage_requires_explicit_refresh": manager.config.policy.usage_auto_refresh_after_seconds is None,
|
|
496
|
+
"summary_auto_refresh_after_seconds": manager.config.policy.usage_auto_refresh_after_seconds,
|
|
496
497
|
"default_backend": "combined",
|
|
497
498
|
"optional_backends": {
|
|
498
499
|
"codexbar": detect_codexbar(),
|
|
@@ -3,10 +3,10 @@ from __future__ import annotations
|
|
|
3
3
|
import os
|
|
4
4
|
import signal
|
|
5
5
|
import shutil
|
|
6
|
-
import subprocess
|
|
7
6
|
from pathlib import Path
|
|
8
7
|
|
|
9
8
|
from agentpool.models import RuntimeKind, TmuxSessionRef, ToolError
|
|
9
|
+
from agentpool.utils import run_capture
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class TmuxRuntime:
|
|
@@ -28,29 +28,22 @@ class TmuxRuntime:
|
|
|
28
28
|
merged_env = os.environ.copy()
|
|
29
29
|
if env:
|
|
30
30
|
merged_env.update(env)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
capture_output=True,
|
|
37
|
-
check=True,
|
|
38
|
-
)
|
|
39
|
-
except subprocess.CalledProcessError as exc:
|
|
31
|
+
proc = run_capture(
|
|
32
|
+
[tmux, "new-session", "-d", "-s", session_name, "-c", str(cwd), *command],
|
|
33
|
+
env=merged_env,
|
|
34
|
+
)
|
|
35
|
+
if proc.returncode != 0:
|
|
40
36
|
raise ToolError(
|
|
41
37
|
"SPAWN_FAILED",
|
|
42
38
|
f"Failed to create tmux session {session_name}.",
|
|
43
|
-
{"stderr":
|
|
44
|
-
)
|
|
39
|
+
{"stderr": proc.stderr, "stdout": proc.stdout, "command": command},
|
|
40
|
+
)
|
|
45
41
|
return TmuxSessionRef(session_name=session_name)
|
|
46
42
|
|
|
47
43
|
def capture(self, ref: TmuxSessionRef, lines: int = 300) -> str:
|
|
48
44
|
tmux = self.require_tmux()
|
|
49
|
-
proc =
|
|
45
|
+
proc = run_capture(
|
|
50
46
|
[tmux, "capture-pane", "-p", "-J", "-t", ref.target, "-S", f"-{lines}"],
|
|
51
|
-
text=True,
|
|
52
|
-
capture_output=True,
|
|
53
|
-
check=False,
|
|
54
47
|
)
|
|
55
48
|
if proc.returncode != 0:
|
|
56
49
|
raise ToolError(
|
|
@@ -66,18 +59,27 @@ class TmuxRuntime:
|
|
|
66
59
|
return
|
|
67
60
|
tmux = self.require_tmux()
|
|
68
61
|
buffer_name = f"agentpool-{ref.session_name}"
|
|
69
|
-
|
|
70
|
-
|
|
62
|
+
load = run_capture([tmux, "load-buffer", "-b", buffer_name, "-"], input_text=text)
|
|
63
|
+
if load.returncode != 0:
|
|
64
|
+
raise ToolError(
|
|
65
|
+
"TMUX_SEND_FAILED",
|
|
66
|
+
f"Could not load tmux paste buffer for {ref.target}.",
|
|
67
|
+
{"stderr": load.stderr},
|
|
68
|
+
)
|
|
69
|
+
paste = run_capture([tmux, "paste-buffer", "-b", buffer_name, "-t", ref.target])
|
|
70
|
+
if paste.returncode != 0:
|
|
71
|
+
raise ToolError(
|
|
72
|
+
"TMUX_SEND_FAILED",
|
|
73
|
+
f"Could not paste tmux buffer to {ref.target}.",
|
|
74
|
+
{"stderr": paste.stderr},
|
|
75
|
+
)
|
|
71
76
|
if submit and not text.endswith("\n"):
|
|
72
77
|
self.send_keys(ref, ["Enter"])
|
|
73
78
|
|
|
74
79
|
def send_keys(self, ref: TmuxSessionRef, keys: list[str]) -> None:
|
|
75
80
|
tmux = self.require_tmux()
|
|
76
|
-
proc =
|
|
81
|
+
proc = run_capture(
|
|
77
82
|
[tmux, "send-keys", "-t", ref.target, *keys],
|
|
78
|
-
text=True,
|
|
79
|
-
capture_output=True,
|
|
80
|
-
check=False,
|
|
81
83
|
)
|
|
82
84
|
if proc.returncode != 0:
|
|
83
85
|
raise ToolError(
|
|
@@ -95,7 +97,7 @@ class TmuxRuntime:
|
|
|
95
97
|
def terminate(self, ref: TmuxSessionRef) -> None:
|
|
96
98
|
tmux = self.require_tmux()
|
|
97
99
|
pgid = self._pane_process_group(ref)
|
|
98
|
-
|
|
100
|
+
run_capture([tmux, "kill-session", "-t", ref.session_name])
|
|
99
101
|
if pgid is not None:
|
|
100
102
|
try:
|
|
101
103
|
os.killpg(pgid, signal.SIGTERM)
|
|
@@ -107,22 +109,16 @@ class TmuxRuntime:
|
|
|
107
109
|
def exists(self, ref: TmuxSessionRef) -> bool:
|
|
108
110
|
if not self.tmux_binary:
|
|
109
111
|
return False
|
|
110
|
-
proc =
|
|
112
|
+
proc = run_capture(
|
|
111
113
|
[self.tmux_binary, "has-session", "-t", ref.session_name],
|
|
112
|
-
text=True,
|
|
113
|
-
capture_output=True,
|
|
114
|
-
check=False,
|
|
115
114
|
)
|
|
116
115
|
return proc.returncode == 0
|
|
117
116
|
|
|
118
117
|
def _pane_process_group(self, ref: TmuxSessionRef) -> int | None:
|
|
119
118
|
if not self.tmux_binary:
|
|
120
119
|
return None
|
|
121
|
-
proc =
|
|
120
|
+
proc = run_capture(
|
|
122
121
|
[self.tmux_binary, "display-message", "-p", "-t", ref.target, "#{pane_pid}"],
|
|
123
|
-
text=True,
|
|
124
|
-
capture_output=True,
|
|
125
|
-
check=False,
|
|
126
122
|
)
|
|
127
123
|
if proc.returncode != 0:
|
|
128
124
|
return None
|
|
@@ -155,9 +155,22 @@ class SessionManager:
|
|
|
155
155
|
for snapshot in snapshots:
|
|
156
156
|
self.store.save_usage_snapshot(snapshot)
|
|
157
157
|
source = "live_probe"
|
|
158
|
+
summary_backend = backend
|
|
158
159
|
else:
|
|
159
160
|
snapshots = self._configured_usage_snapshots(self.store.latest_usage_snapshots(provider_id), provider_id)
|
|
160
161
|
source = "sqlite_cache"
|
|
162
|
+
summary_backend = "cache"
|
|
163
|
+
if self._usage_summary_should_auto_refresh(snapshots, provider_id):
|
|
164
|
+
snapshots = self.registry.usage(
|
|
165
|
+
provider_id,
|
|
166
|
+
backend=backend,
|
|
167
|
+
allow_interactive=allow_interactive,
|
|
168
|
+
timeout_seconds=_remaining_seconds(deadline),
|
|
169
|
+
)
|
|
170
|
+
for snapshot in snapshots:
|
|
171
|
+
self.store.save_usage_snapshot(snapshot)
|
|
172
|
+
source = "live_probe"
|
|
173
|
+
summary_backend = backend
|
|
161
174
|
return {
|
|
162
175
|
**build_usage_summary(
|
|
163
176
|
snapshots,
|
|
@@ -167,7 +180,7 @@ class SessionManager:
|
|
|
167
180
|
),
|
|
168
181
|
"preferences": preferences_reference(),
|
|
169
182
|
"source": source,
|
|
170
|
-
"backend":
|
|
183
|
+
"backend": summary_backend,
|
|
171
184
|
"partial": _has_partial_usage(snapshots) or _has_partial_descriptors(descriptors),
|
|
172
185
|
}
|
|
173
186
|
|
|
@@ -865,6 +878,26 @@ class SessionManager:
|
|
|
865
878
|
configured = set(self.config.providers)
|
|
866
879
|
return [snapshot for snapshot in snapshots if snapshot.provider_id in configured]
|
|
867
880
|
|
|
881
|
+
def _usage_summary_should_auto_refresh(self, snapshots: list[Any], provider_id: str | None = None) -> bool:
|
|
882
|
+
threshold = self.config.policy.usage_auto_refresh_after_seconds
|
|
883
|
+
if threshold is None:
|
|
884
|
+
return False
|
|
885
|
+
if provider_id and not snapshots:
|
|
886
|
+
return True
|
|
887
|
+
if not provider_id:
|
|
888
|
+
configured = set(self.config.providers)
|
|
889
|
+
seen = {snapshot.provider_id for snapshot in snapshots}
|
|
890
|
+
if not configured.issubset(seen):
|
|
891
|
+
return True
|
|
892
|
+
now = datetime.now(timezone.utc)
|
|
893
|
+
for snapshot in snapshots:
|
|
894
|
+
checked_at = snapshot.checked_at
|
|
895
|
+
if checked_at.tzinfo is None:
|
|
896
|
+
checked_at = checked_at.replace(tzinfo=timezone.utc)
|
|
897
|
+
if max(0.0, (now - checked_at).total_seconds()) > threshold:
|
|
898
|
+
return True
|
|
899
|
+
return False
|
|
900
|
+
|
|
868
901
|
def _enforce_deadline(self, session: AgentSession) -> ObserveWorkerResponse | None:
|
|
869
902
|
deadline_at = session.metadata.get("deadline_at")
|
|
870
903
|
if not deadline_at or not active_state(session.state):
|
|
@@ -457,9 +457,8 @@ def _snapshot_usability(
|
|
|
457
457
|
}
|
|
458
458
|
for window in snapshot.windows
|
|
459
459
|
]
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
return _usable_reason(snapshot, windows, min_remaining_percent, descriptor, stale)
|
|
460
|
+
_ = stale_after_seconds
|
|
461
|
+
return _usable_reason(snapshot, windows, min_remaining_percent, descriptor)
|
|
463
462
|
|
|
464
463
|
|
|
465
464
|
def _is_quota_unusable_reason(reason: str | None) -> bool:
|
|
@@ -17,6 +17,7 @@ import certifi
|
|
|
17
17
|
|
|
18
18
|
from agentpool.models import CapacitySnapshot, Confidence, TmuxSessionRef, UsageStatus, UsageWindow, UsageWindowKind
|
|
19
19
|
from agentpool.runtimes.tmux import TmuxRuntime
|
|
20
|
+
from agentpool.utils import run_capture, terminate_process_group
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
class ProbeError(Exception):
|
|
@@ -65,34 +66,12 @@ def _urlopen(
|
|
|
65
66
|
return urllib.request.urlopen(request, timeout=timeout, context=context)
|
|
66
67
|
|
|
67
68
|
|
|
68
|
-
def _probe_env() -> dict[str, str]:
|
|
69
|
-
env = os.environ.copy()
|
|
70
|
-
env.update(
|
|
71
|
-
{
|
|
72
|
-
"TERM": "dumb",
|
|
73
|
-
"NO_COLOR": "1",
|
|
74
|
-
"CLICOLOR": "0",
|
|
75
|
-
"FORCE_COLOR": "0",
|
|
76
|
-
}
|
|
77
|
-
)
|
|
78
|
-
return env
|
|
79
|
-
|
|
80
|
-
|
|
81
69
|
def _run_probe_command(
|
|
82
70
|
command: list[str],
|
|
83
71
|
*,
|
|
84
72
|
timeout: float,
|
|
85
73
|
) -> subprocess.CompletedProcess[str]:
|
|
86
|
-
return
|
|
87
|
-
command,
|
|
88
|
-
stdin=subprocess.DEVNULL,
|
|
89
|
-
capture_output=True,
|
|
90
|
-
text=True,
|
|
91
|
-
timeout=timeout,
|
|
92
|
-
check=False,
|
|
93
|
-
start_new_session=True,
|
|
94
|
-
env=_probe_env(),
|
|
95
|
-
)
|
|
74
|
+
return run_capture(command, timeout=timeout, terminal_dumb=True)
|
|
96
75
|
|
|
97
76
|
|
|
98
77
|
def _request_json(request: urllib.request.Request) -> dict[str, Any]:
|
|
@@ -248,12 +227,7 @@ def _clean_optional_string(value: object) -> str | None:
|
|
|
248
227
|
|
|
249
228
|
|
|
250
229
|
def _terminate_process(proc: subprocess.Popen[str]) -> None:
|
|
251
|
-
|
|
252
|
-
proc.terminate()
|
|
253
|
-
try:
|
|
254
|
-
proc.wait(timeout=1)
|
|
255
|
-
except subprocess.TimeoutExpired:
|
|
256
|
-
proc.kill()
|
|
230
|
+
terminate_process_group(proc)
|
|
257
231
|
|
|
258
232
|
|
|
259
233
|
def _safe_read_pipe(pipe: Any) -> str:
|
|
@@ -20,6 +20,7 @@ from agentpool.usage._common import (
|
|
|
20
20
|
unavailable,
|
|
21
21
|
unknown,
|
|
22
22
|
)
|
|
23
|
+
from agentpool.utils import popen_text
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
def codex_cli_usage_snapshot(provider_id: str, binary: str | None = None) -> CapacitySnapshot:
|
|
@@ -79,12 +80,9 @@ def parse_codex_rate_limits(provider_id: str, payload: dict[str, Any]) -> Capaci
|
|
|
79
80
|
|
|
80
81
|
|
|
81
82
|
def _codex_rpc_rate_limits(executable: str) -> dict[str, Any]:
|
|
82
|
-
proc =
|
|
83
|
+
proc = popen_text(
|
|
83
84
|
[executable, "-s", "read-only", "-a", "untrusted", "app-server"],
|
|
84
|
-
|
|
85
|
-
stdout=subprocess.PIPE,
|
|
86
|
-
stderr=subprocess.PIPE,
|
|
87
|
-
text=True,
|
|
85
|
+
terminal_dumb=True,
|
|
88
86
|
)
|
|
89
87
|
try:
|
|
90
88
|
_json_rpc_request(proc, 1, "initialize", {"clientInfo": {"name": "agentpool", "version": "0.1.0"}}, 8.0)
|
|
@@ -65,7 +65,7 @@ def _provider_summary(
|
|
|
65
65
|
]
|
|
66
66
|
age_seconds = max(0.0, (now - snapshot.checked_at).total_seconds())
|
|
67
67
|
stale = age_seconds > stale_after_seconds
|
|
68
|
-
usable, unusable_reason = _usable_reason(snapshot, windows, min_remaining_percent, descriptor
|
|
68
|
+
usable, unusable_reason = _usable_reason(snapshot, windows, min_remaining_percent, descriptor)
|
|
69
69
|
return {
|
|
70
70
|
"provider_id": snapshot.provider_id,
|
|
71
71
|
"status": snapshot.status.value if hasattr(snapshot.status, "value") else str(snapshot.status),
|
|
@@ -90,7 +90,6 @@ def _usable_reason(
|
|
|
90
90
|
windows: list[dict[str, Any]],
|
|
91
91
|
min_remaining_percent: int,
|
|
92
92
|
descriptor: ProviderDescriptor | None,
|
|
93
|
-
stale: bool,
|
|
94
93
|
) -> tuple[bool, str | None]:
|
|
95
94
|
if descriptor and not descriptor.installed:
|
|
96
95
|
return False, "not_installed"
|
|
@@ -110,8 +109,6 @@ def _usable_reason(
|
|
|
110
109
|
}
|
|
111
110
|
if confidence not in allowed_confidence:
|
|
112
111
|
return False, f"confidence_{confidence}"
|
|
113
|
-
if stale:
|
|
114
|
-
return False, "usage_stale"
|
|
115
112
|
for window in windows:
|
|
116
113
|
remaining = window["remaining_percent"]
|
|
117
114
|
if remaining is not None and remaining < min_remaining_percent:
|