okstra 0.163.2 → 0.165.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 +8 -6
- package/docs/architecture.md +24 -15
- package/docs/cli.md +15 -8
- package/docs/for-ai/README.md +2 -2
- package/docs/for-ai/skills/okstra-inspect.md +2 -2
- package/docs/for-ai/skills/okstra-user-response.md +2 -2
- package/docs/project-structure-overview.md +22 -14
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/antigravity-worker.md +9 -7
- package/runtime/agents/workers/claude-worker.md +1 -0
- package/runtime/agents/workers/codex-worker.md +9 -7
- package/runtime/agents/workers/grok-worker.md +6 -4
- package/runtime/agents/workers/kimi-worker.md +6 -4
- package/runtime/bin/lib/okstra/cli.sh +5 -0
- package/runtime/bin/lib/okstra/globals.sh +2 -0
- package/runtime/bin/lib/okstra/usage.sh +5 -5
- package/runtime/bin/okstra-antigravity-exec.sh +1 -340
- package/runtime/bin/okstra-claude-exec.sh +1 -178
- package/runtime/bin/okstra-codex-exec.sh +1 -467
- package/runtime/bin/okstra-provider-exec.py +165 -190
- package/runtime/bin/okstra-trace-cleanup.sh +14 -7
- package/runtime/bin/okstra-wrapper-status.py +26 -19
- package/runtime/bin/okstra.sh +87 -91
- package/runtime/prompts/lead/adapters/cmux.md +2 -2
- package/runtime/prompts/lead/convergence.md +36 -8
- package/runtime/prompts/lead/okstra-lead-contract.md +24 -1
- package/runtime/prompts/lead/plan-body-verification.md +9 -1
- package/runtime/prompts/lead/report-writer.md +1 -0
- package/runtime/prompts/lead/team-contract.md +3 -3
- package/runtime/prompts/profiles/_common-contract.md +9 -1
- package/runtime/prompts/profiles/_coverage-critic.md +1 -1
- package/runtime/prompts/profiles/_implementation-diff-review.md +3 -1
- package/runtime/prompts/profiles/_implementation-self-check.md +1 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -1
- package/runtime/prompts/profiles/implementation-planning.md +6 -4
- package/runtime/python/okstra_ctl/adapters/accounting/__init__.py +11 -0
- package/runtime/python/okstra_ctl/adapters/accounting/claude_jsonl.py +17 -0
- package/runtime/python/okstra_ctl/adapters/accounting/cli_artifact.py +17 -0
- package/runtime/python/okstra_ctl/adapters/accounting/unavailable.py +19 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/__init__.py +92 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/cli_wrapper.py +54 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/cmux.py +68 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/native_team.py +13 -0
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +60 -0
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/antigravity.md → python/okstra_ctl/adapters/hosts/antigravity/relay.md} +52 -0
- package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +292 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +120 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/claude-code.md → python/okstra_ctl/adapters/hosts/claude-code/relay.md} +112 -1
- package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +60 -0
- package/runtime/python/okstra_ctl/adapters/hosts/codex/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/codex.md → python/okstra_ctl/adapters/hosts/codex/relay.md} +52 -0
- package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +72 -0
- package/runtime/python/okstra_ctl/adapters/hosts/external/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/external.md → python/okstra_ctl/adapters/hosts/external/relay.md} +53 -1
- package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +63 -0
- package/runtime/python/okstra_ctl/adapters/hosts/grok/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +90 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +63 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +90 -0
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +183 -0
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +110 -0
- package/runtime/python/okstra_ctl/adapters/providers/claude/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +84 -0
- package/runtime/python/okstra_ctl/adapters/providers/codex/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +76 -0
- package/runtime/python/okstra_ctl/adapters/providers/grok/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +80 -0
- package/runtime/python/okstra_ctl/adapters/providers/kimi/manifest.json +1 -0
- package/runtime/python/okstra_ctl/application/__init__.py +1 -0
- package/runtime/python/okstra_ctl/application/advance_wizard.py +25 -0
- package/runtime/python/okstra_ctl/application/collect_usage.py +15 -0
- package/runtime/python/okstra_ctl/application/dispatch_assignments.py +15 -0
- package/runtime/python/okstra_ctl/application/resolve_assignment.py +93 -0
- package/runtime/python/okstra_ctl/application/resume_run.py +21 -0
- package/runtime/python/okstra_ctl/application/start_run.py +21 -0
- package/runtime/python/okstra_ctl/codex_dispatch.py +49 -826
- package/runtime/python/okstra_ctl/dispatch_core.py +244 -29
- package/runtime/python/okstra_ctl/dispatch_state.py +27 -0
- package/runtime/python/okstra_ctl/domain/__init__.py +34 -0
- package/runtime/python/okstra_ctl/domain/host.py +100 -0
- package/runtime/python/okstra_ctl/domain/provider.py +70 -0
- package/runtime/python/okstra_ctl/domain/wizard/__init__.py +19 -0
- package/runtime/python/okstra_ctl/domain/wizard/interaction.py +140 -0
- package/runtime/python/okstra_ctl/domain/worker_exec.py +102 -0
- package/runtime/python/okstra_ctl/domain/worker_role.py +34 -0
- package/runtime/python/okstra_ctl/domain/worker_stream.py +261 -0
- package/runtime/python/okstra_ctl/entrypoints/__init__.py +1 -0
- package/runtime/python/okstra_ctl/entrypoints/hosts.py +334 -0
- package/runtime/python/okstra_ctl/incremental_scope.py +16 -4
- package/runtime/python/okstra_ctl/models.py +54 -269
- package/runtime/python/okstra_ctl/ports/__init__.py +15 -0
- package/runtime/python/okstra_ctl/ports/host.py +32 -0
- package/runtime/python/okstra_ctl/ports/interaction.py +15 -0
- package/runtime/python/okstra_ctl/ports/lead_session.py +25 -0
- package/runtime/python/okstra_ctl/ports/usage_accounting.py +23 -0
- package/runtime/python/okstra_ctl/ports/worker_dispatch.py +32 -0
- package/runtime/python/okstra_ctl/registry/__init__.py +13 -0
- package/runtime/python/okstra_ctl/registry/factory_loader.py +32 -0
- package/runtime/python/okstra_ctl/registry/host_discovery.py +124 -0
- package/runtime/python/okstra_ctl/registry/host_registry.py +365 -0
- package/runtime/python/okstra_ctl/registry/provider_registry.py +149 -0
- package/runtime/python/okstra_ctl/render.py +145 -47
- package/runtime/python/okstra_ctl/report_html/common.py +71 -25
- package/runtime/python/okstra_ctl/report_html/models.py +5 -0
- package/runtime/python/okstra_ctl/report_html/render.py +1 -1
- package/runtime/python/okstra_ctl/report_html/run_usage.py +19 -0
- package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +14 -0
- package/runtime/python/okstra_ctl/report_views.py +44 -16
- package/runtime/python/okstra_ctl/run.py +80 -58
- package/runtime/python/okstra_ctl/session.py +1 -1
- package/runtime/python/okstra_ctl/stage_citations.py +52 -15
- package/runtime/python/okstra_ctl/team.py +44 -32
- package/runtime/python/okstra_ctl/user_response.py +45 -29
- package/runtime/python/okstra_ctl/wizard.py +175 -73
- package/runtime/python/okstra_ctl/worker_audit_ledger.py +29 -4
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +10 -3
- package/runtime/python/okstra_ctl/worker_request.py +140 -0
- package/runtime/python/okstra_ctl/worker_runner.py +622 -0
- package/runtime/python/okstra_token_usage/collect.py +42 -7
- package/runtime/python/okstra_token_usage/report.py +42 -0
- package/runtime/python/okstra_token_usage/task_totals.py +88 -0
- package/runtime/schemas/final-report-v1.0.schema.json +4040 -1066
- package/runtime/schemas/final-report-v2.0.schema.json +5673 -1412
- package/runtime/skills/okstra-inspect/SKILL.md +1 -2
- package/runtime/skills/okstra-inspect/facets/logs.md +5 -5
- package/runtime/skills/okstra-inspect/facets/run-audit.md +3 -3
- package/runtime/skills/okstra-run/SKILL.md +74 -29
- package/runtime/skills/okstra-user-response/SKILL.md +15 -5
- package/runtime/templates/implementation-worker-preamble.md +1 -1
- package/runtime/templates/report-writer-prompt-preamble.md +1 -0
- package/runtime/templates/reports/final-report.template.md +3 -3
- package/runtime/templates/reports/html/assets/base.css +8 -4
- package/runtime/templates/reports/html/base.template.html +12 -6
- package/runtime/templates/reports/html/i18n/en.json +32 -7
- package/runtime/templates/reports/html/i18n/ko.json +32 -7
- package/runtime/templates/reports/html/macros/forms.html +9 -3
- package/runtime/templates/reports/html/tasks/implementation-planning.template.html +14 -19
- package/runtime/templates/reports/report.js +59 -26
- package/runtime/templates/reports/user-response.template.md +12 -8
- package/runtime/templates/worker-prompt-preamble.md +1 -1
- package/runtime/validators/validate-implementation-plan-stages.py +17 -22
- package/runtime/validators/validate-report-views.py +0 -39
- package/runtime/validators/validate-run.py +96 -14
- package/runtime/validators/validate_session_conformance.py +69 -3
- package/src/cli-registry.mjs +0 -7
- package/src/commands/execute/render-bundle.mjs +4 -4
- package/src/commands/execute/run.mjs +8 -25
- package/src/commands/execute/wizard.mjs +33 -13
- package/src/commands/lifecycle/doctor.mjs +10 -10
- package/src/commands/lifecycle/install.mjs +53 -30
- package/src/commands/lifecycle/preflight.mjs +14 -4
- package/src/lib/host-registry-client.mjs +176 -0
- package/src/lib/runtime-manifest.mjs +6 -8
- package/runtime/bin/okstra-wrapper-agy-stream.py +0 -61
- package/runtime/python/okstra_ctl/error_issue.py +0 -640
- package/runtime/python/okstra_ctl/issue_signals.py +0 -186
- package/runtime/python/okstra_ctl/lead_runtime.py +0 -115
- package/runtime/python/okstra_ctl/runner_resolution.py +0 -103
- package/runtime/skills/okstra-inspect/facets/error-issue.md +0 -77
- package/src/commands/inspect/error-issue.mjs +0 -27
- package/src/lib/runtime-readiness.mjs +0 -90
- package/src/lib/runtime-resolver.mjs +0 -123
|
@@ -22,7 +22,6 @@ from .antigravity import (
|
|
|
22
22
|
from .paths import claude_project_dir, utc_now
|
|
23
23
|
from .pricing import antigravity_cost_usd, provider_cost_usd
|
|
24
24
|
from okstra_ctl.models import provider_wrappers
|
|
25
|
-
from okstra_ctl.lead_runtime import ARTIFACT_ONLY_LEAD_RUNTIMES
|
|
26
25
|
from okstra_ctl.wrapper_status import read_wrapper_status, status_path_for_prompt
|
|
27
26
|
|
|
28
27
|
|
|
@@ -705,14 +704,17 @@ def _populate_usage_summary(
|
|
|
705
704
|
workers = state.get("workers", [])
|
|
706
705
|
lead = state.get("leadUsage") or {}
|
|
707
706
|
lead_total = lead.get("totalTokens", 0) or 0
|
|
707
|
+
lead_cache_read = lead.get("cacheReadTokens", 0) or 0
|
|
708
708
|
lead_billable = lead.get("billableEquivalentTokens", 0) or 0
|
|
709
709
|
lead_cost = lead.get("estimatedCostUsd", 0) or 0
|
|
710
710
|
worker_total = sum((w.get("usage") or {}).get("totalTokens", 0) or 0 for w in workers)
|
|
711
|
+
worker_cache_read = sum((w.get("usage") or {}).get("cacheReadTokens", 0) or 0 for w in workers)
|
|
711
712
|
worker_billable = sum((w.get("usage") or {}).get("billableEquivalentTokens", 0) or 0 for w in workers)
|
|
712
713
|
worker_cost = sum((w.get("usage") or {}).get("estimatedCostUsd", 0) or 0 for w in workers)
|
|
713
714
|
cli_cost = sum((w.get("usage") or {}).get("cliEstimatedCostUsd", 0) or 0 for w in workers)
|
|
714
715
|
if unattributed_usage is not None:
|
|
715
716
|
worker_total += unattributed_usage.get("totalTokens", 0) or 0
|
|
717
|
+
worker_cache_read += unattributed_usage.get("cacheReadTokens", 0) or 0
|
|
716
718
|
worker_billable += unattributed_usage.get("billableEquivalentTokens", 0) or 0
|
|
717
719
|
worker_cost += unattributed_usage.get("estimatedCostUsd", 0) or 0
|
|
718
720
|
|
|
@@ -734,6 +736,9 @@ def _populate_usage_summary(
|
|
|
734
736
|
"leadTotalTokens": lead_total,
|
|
735
737
|
"workerTotalTokens": worker_total,
|
|
736
738
|
"grandTotalTokens": lead_total + worker_total,
|
|
739
|
+
"leadCacheReadTokens": lead_cache_read,
|
|
740
|
+
"workerCacheReadTokens": worker_cache_read,
|
|
741
|
+
"grandCacheReadTokens": lead_cache_read + worker_cache_read,
|
|
737
742
|
"leadBillableEquivalentTokens": lead_billable,
|
|
738
743
|
"workerBillableEquivalentTokens": worker_billable,
|
|
739
744
|
"grandBillableEquivalentTokens": lead_billable + worker_billable,
|
|
@@ -751,26 +756,28 @@ def _populate_usage_summary(
|
|
|
751
756
|
"unattributedTeamSessions": unattributed_sessions or [],
|
|
752
757
|
"unattributedWorkerUsage": unattributed_usage,
|
|
753
758
|
"definitions": {
|
|
754
|
-
"totalTokens": "Sum of input + output + cache_creation
|
|
759
|
+
"totalTokens": "Sum of input + output + cache_creation tokens — the volume the session put through the model once. cache_read is excluded and reported separately as cacheReadTokens: a session re-reads its whole context from cache every turn, so folding it in here would count the same tokens once per turn.",
|
|
760
|
+
"cacheReadTokens": "Context re-read from cache. Billed at 0.1x base input, so it lands in billableEquivalentTokens and in the cost even though it is not part of totalTokens.",
|
|
755
761
|
"billableEquivalentTokens": "Tokens normalized to base-input-price units (cache_creation_5m x1.25, cache_creation_1h x2.0, cache_read x0.1, output x5). 5m vs 1h is split from usage.cache_creation when the API breakdown is present; otherwise all cache_creation falls into 5m.",
|
|
756
762
|
"estimatedCostUsd": "USD cost using public list pricing for the model recorded in the session. cliWorkers covers attributable registered-provider CLI calls.",
|
|
757
763
|
},
|
|
758
764
|
}
|
|
759
765
|
|
|
760
766
|
|
|
761
|
-
def
|
|
762
|
-
|
|
767
|
+
def collect_claude_runtime_usage(
|
|
768
|
+
team_state_path: Path,
|
|
769
|
+
project_root: Path | None = None,
|
|
770
|
+
*,
|
|
771
|
+
incremental: bool = True,
|
|
772
|
+
) -> dict:
|
|
763
773
|
# incremental: 세션 jsonl 스캔에 byte cursor 캐시 사용 (P6). 캐시는 윈도우
|
|
764
774
|
# 적용 전 이벤트를 저장하므로 결과는 전체 스캔과 동일 — False 는 캐시 경로를
|
|
765
775
|
# 완전히 우회하는 정확성 폴백(CLI --no-cache).
|
|
766
776
|
state = json.loads(team_state_path.read_text())
|
|
767
777
|
cwd = project_root or _infer_project_root(team_state_path, state)
|
|
768
778
|
run_since, run_until = resolve_run_window(team_state_path, state)
|
|
769
|
-
task_key = state.get("taskKey", "")
|
|
770
779
|
team_name = resolve_team_name(state)
|
|
771
780
|
lead_sid = (state.get("lead") or {}).get("sessionId")
|
|
772
|
-
if state.get("leadRuntime") in ARTIFACT_ONLY_LEAD_RUNTIMES:
|
|
773
|
-
return _collect_cli_runtime_usage(state, cwd)
|
|
774
781
|
|
|
775
782
|
# 1) Claude sessions (lead + claude-side workers). Cache totals at scan
|
|
776
783
|
# time so we don't re-read the jsonl when a worker matches multiple
|
|
@@ -936,6 +943,34 @@ def collect(team_state_path: Path, project_root: Path | None = None, *,
|
|
|
936
943
|
return state
|
|
937
944
|
|
|
938
945
|
|
|
946
|
+
def collect_cli_runtime_usage(
|
|
947
|
+
team_state_path: Path,
|
|
948
|
+
project_root: Path | None = None,
|
|
949
|
+
*,
|
|
950
|
+
incremental: bool = True,
|
|
951
|
+
) -> dict:
|
|
952
|
+
state = json.loads(team_state_path.read_text())
|
|
953
|
+
cwd = project_root or _infer_project_root(team_state_path, state)
|
|
954
|
+
return _collect_cli_runtime_usage(state, cwd)
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
def collect(
|
|
958
|
+
team_state_path: Path,
|
|
959
|
+
project_root: Path | None = None,
|
|
960
|
+
*,
|
|
961
|
+
incremental: bool = True,
|
|
962
|
+
) -> dict:
|
|
963
|
+
from okstra_ctl.application.collect_usage import collect_usage
|
|
964
|
+
from okstra_ctl.ports.usage_accounting import UsageRequest
|
|
965
|
+
from okstra_ctl.registry.host_registry import default_host_registry
|
|
966
|
+
|
|
967
|
+
state = json.loads(team_state_path.read_text())
|
|
968
|
+
cwd = project_root or _infer_project_root(team_state_path, state)
|
|
969
|
+
adapter = default_host_registry().resolve(str(state.get("leadRuntime") or ""))
|
|
970
|
+
request = UsageRequest(team_state_path, cwd, incremental)
|
|
971
|
+
return collect_usage(request, adapter.usage_accounting()).payload
|
|
972
|
+
|
|
973
|
+
|
|
939
974
|
def _infer_project_root(team_state_path: Path, state: dict) -> Path:
|
|
940
975
|
rel = state.get("runDirectoryPath") or ""
|
|
941
976
|
p = team_state_path.resolve().parent
|
|
@@ -24,6 +24,8 @@ from okstra_ctl.render_final_report import ( # noqa: E402
|
|
|
24
24
|
render_to_file,
|
|
25
25
|
)
|
|
26
26
|
|
|
27
|
+
from .task_totals import task_cumulative_usage # noqa: E402
|
|
28
|
+
|
|
27
29
|
|
|
28
30
|
class SubstituteRefusedError(RuntimeError):
|
|
29
31
|
"""Raised when substitution would write zero-only token cells.
|
|
@@ -89,6 +91,8 @@ def _populate_execution_row(row: dict, source: dict) -> None:
|
|
|
89
91
|
return
|
|
90
92
|
if "totalTokens" in usage:
|
|
91
93
|
row["totalTokens"] = usage["totalTokens"]
|
|
94
|
+
if "cacheReadTokens" in usage:
|
|
95
|
+
row["cacheReadTokens"] = usage["cacheReadTokens"]
|
|
92
96
|
if "billableEquivalentTokens" in usage:
|
|
93
97
|
row["billableTokens"] = usage["billableEquivalentTokens"]
|
|
94
98
|
if "estimatedCostUsd" in usage:
|
|
@@ -114,6 +118,7 @@ def _worker_detail_row(label: str, usage: dict) -> dict:
|
|
|
114
118
|
return {
|
|
115
119
|
"label": label,
|
|
116
120
|
"totalTokens": None,
|
|
121
|
+
"cacheReadTokens": None,
|
|
117
122
|
"billableTokens": None,
|
|
118
123
|
"costUsd": None,
|
|
119
124
|
"cliTotalTokens": None,
|
|
@@ -122,6 +127,7 @@ def _worker_detail_row(label: str, usage: dict) -> dict:
|
|
|
122
127
|
return {
|
|
123
128
|
"label": label,
|
|
124
129
|
"totalTokens": usage.get("totalTokens"),
|
|
130
|
+
"cacheReadTokens": usage.get("cacheReadTokens"),
|
|
125
131
|
"billableTokens": usage.get("billableEquivalentTokens"),
|
|
126
132
|
"costUsd": usage.get("estimatedCostUsd"),
|
|
127
133
|
"cliTotalTokens": usage.get("cliTotalTokens"),
|
|
@@ -129,6 +135,35 @@ def _worker_detail_row(label: str, usage: dict) -> dict:
|
|
|
129
135
|
}
|
|
130
136
|
|
|
131
137
|
|
|
138
|
+
def _cache_reads(summary: dict, team_state: dict) -> dict[str, int]:
|
|
139
|
+
"""Lead / worker / grand cache-read totals for this run.
|
|
140
|
+
|
|
141
|
+
The figure joined `usageSummary` after most team-states on disk were
|
|
142
|
+
written, and re-rendering one of those reports is exactly when the reader
|
|
143
|
+
goes looking for the corrected table — so a state without the summary keys
|
|
144
|
+
is added up from the usage blocks it does carry.
|
|
145
|
+
"""
|
|
146
|
+
lead = summary.get("leadCacheReadTokens")
|
|
147
|
+
worker = summary.get("workerCacheReadTokens")
|
|
148
|
+
if isinstance(lead, int) and isinstance(worker, int):
|
|
149
|
+
return {"lead": lead, "worker": worker, "grand": lead + worker}
|
|
150
|
+
lead = _cache_read_tokens(team_state.get("leadUsage"))
|
|
151
|
+
worker = sum(
|
|
152
|
+
_cache_read_tokens(entry.get("usage"))
|
|
153
|
+
for entry in team_state.get("workers") or []
|
|
154
|
+
if isinstance(entry, dict)
|
|
155
|
+
)
|
|
156
|
+
worker += _cache_read_tokens(summary.get("unattributedWorkerUsage"))
|
|
157
|
+
return {"lead": lead, "worker": worker, "grand": lead + worker}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _cache_read_tokens(usage: object) -> int:
|
|
161
|
+
if not isinstance(usage, dict):
|
|
162
|
+
return 0
|
|
163
|
+
value = usage.get("cacheReadTokens")
|
|
164
|
+
return value if isinstance(value, int) else 0
|
|
165
|
+
|
|
166
|
+
|
|
132
167
|
def _worker_detail_rows(team_state: dict) -> list[dict]:
|
|
133
168
|
rows = []
|
|
134
169
|
for worker in team_state.get("workers") or []:
|
|
@@ -178,19 +213,23 @@ def populate_data_token_cells(data_path: Path, team_state: dict) -> int:
|
|
|
178
213
|
worker_cost = cost.get("claudeWorkers") or 0
|
|
179
214
|
cli_cost = cost.get("cliWorkers") or 0
|
|
180
215
|
|
|
216
|
+
cache_reads = _cache_reads(summary, team_state)
|
|
181
217
|
token_usage = data.setdefault("tokenUsage", {})
|
|
182
218
|
token_usage.setdefault("lead", {}).update({
|
|
183
219
|
"totalTokens": summary.get("leadTotalTokens"),
|
|
220
|
+
"cacheReadTokens": cache_reads["lead"],
|
|
184
221
|
"billableTokens": summary.get("leadBillableEquivalentTokens"),
|
|
185
222
|
"costUsd": lead_cost,
|
|
186
223
|
})
|
|
187
224
|
token_usage.setdefault("worker", {}).update({
|
|
188
225
|
"totalTokens": summary.get("workerTotalTokens"),
|
|
226
|
+
"cacheReadTokens": cache_reads["worker"],
|
|
189
227
|
"billableTokens": summary.get("workerBillableEquivalentTokens"),
|
|
190
228
|
"costUsd": worker_cost,
|
|
191
229
|
})
|
|
192
230
|
token_usage.setdefault("grand", {}).update({
|
|
193
231
|
"totalTokens": summary.get("grandTotalTokens"),
|
|
232
|
+
"cacheReadTokens": cache_reads["grand"],
|
|
194
233
|
"billableTokens": summary.get("grandBillableEquivalentTokens"),
|
|
195
234
|
# CLI tracked on its own row per the template — grand here means
|
|
196
235
|
# lead + claudeWorkers, not lead + claudeWorkers + cli.
|
|
@@ -198,6 +237,9 @@ def populate_data_token_cells(data_path: Path, team_state: dict) -> int:
|
|
|
198
237
|
})
|
|
199
238
|
token_usage.setdefault("cli", {})["costUsd"] = cli_cost
|
|
200
239
|
token_usage["workerDetails"] = _worker_detail_rows(team_state)
|
|
240
|
+
cumulative = task_cumulative_usage(data_path)
|
|
241
|
+
if cumulative is not None:
|
|
242
|
+
token_usage["taskCumulative"] = cumulative
|
|
201
243
|
changes += 4
|
|
202
244
|
|
|
203
245
|
# Execution Status by Agent — per-row token / cost / duration.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""What a whole task has spent, across every run of every phase.
|
|
2
|
+
|
|
3
|
+
A report's own figures cover one run, but a task reaches its plan through
|
|
4
|
+
several: the first pass, a clarification re-run, a conformance fix. Each of
|
|
5
|
+
those bills separately, and a reader who only ever sees the last one has no way
|
|
6
|
+
to learn what the task cost. The per-run team-states are the only record of the
|
|
7
|
+
earlier passes, so the total is summed from them here rather than carried
|
|
8
|
+
forward inside any single run's state.
|
|
9
|
+
|
|
10
|
+
Every team-state is read directly rather than through its `usageSummary`: the
|
|
11
|
+
summary predates the cache-read figure, so an older run would drop out of that
|
|
12
|
+
column while still counting in the others.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
_TEAM_STATE_GLOB = "runs/*/state/team-state-*.json"
|
|
20
|
+
_SUMMED_KEYS = (
|
|
21
|
+
("totalTokens", "totalTokens"),
|
|
22
|
+
("cacheReadTokens", "cacheReadTokens"),
|
|
23
|
+
("billableTokens", "billableEquivalentTokens"),
|
|
24
|
+
("costUsd", "estimatedCostUsd"),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _task_root(data_path: Path) -> Path | None:
|
|
29
|
+
"""The task-bundle directory a report lives under.
|
|
30
|
+
|
|
31
|
+
Report path shape: `<task-id>/runs/<task-type>/reports/<name>.data.json`.
|
|
32
|
+
A path that does not match is not inside a task bundle — a fixture under a
|
|
33
|
+
tmp dir, most often — and has no sibling runs to total.
|
|
34
|
+
"""
|
|
35
|
+
parents = data_path.parents
|
|
36
|
+
if len(parents) < 4:
|
|
37
|
+
return None
|
|
38
|
+
if parents[0].name != "reports" or parents[2].name != "runs":
|
|
39
|
+
return None
|
|
40
|
+
return parents[3]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _usage_blocks(team_state: dict) -> list[dict]:
|
|
44
|
+
blocks = [team_state.get("leadUsage") or {}]
|
|
45
|
+
for worker in team_state.get("workers") or []:
|
|
46
|
+
if isinstance(worker, dict):
|
|
47
|
+
blocks.append(worker.get("usage") or {})
|
|
48
|
+
unattributed = (team_state.get("usageSummary") or {}).get("unattributedWorkerUsage")
|
|
49
|
+
if isinstance(unattributed, dict):
|
|
50
|
+
blocks.append(unattributed)
|
|
51
|
+
return [block for block in blocks if isinstance(block, dict)]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _number(value: object) -> int | float:
|
|
55
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
56
|
+
return 0
|
|
57
|
+
return value
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def task_cumulative_usage(data_path: Path) -> dict | None:
|
|
61
|
+
"""Sum every measured run of this report's task, or None when there is none.
|
|
62
|
+
|
|
63
|
+
A run whose collector found no session contributes zero tokens and is left
|
|
64
|
+
out of `runCount` — counting it would make the average per run look cheaper
|
|
65
|
+
than the runs that were actually measured.
|
|
66
|
+
"""
|
|
67
|
+
root = _task_root(data_path)
|
|
68
|
+
if root is None:
|
|
69
|
+
return None
|
|
70
|
+
totals = {name: 0 for name, _ in _SUMMED_KEYS}
|
|
71
|
+
run_count = 0
|
|
72
|
+
for state_path in sorted(root.glob(_TEAM_STATE_GLOB)):
|
|
73
|
+
try:
|
|
74
|
+
team_state = json.loads(state_path.read_text(encoding="utf-8"))
|
|
75
|
+
except (OSError, ValueError):
|
|
76
|
+
continue
|
|
77
|
+
if not isinstance(team_state, dict):
|
|
78
|
+
continue
|
|
79
|
+
blocks = _usage_blocks(team_state)
|
|
80
|
+
measured = sum(_number(block.get("totalTokens")) for block in blocks)
|
|
81
|
+
if measured <= 0:
|
|
82
|
+
continue
|
|
83
|
+
run_count += 1
|
|
84
|
+
for name, source_key in _SUMMED_KEYS:
|
|
85
|
+
totals[name] += sum(_number(block.get(source_key)) for block in blocks)
|
|
86
|
+
if not run_count:
|
|
87
|
+
return None
|
|
88
|
+
return {**totals, "runCount": run_count}
|