agent-orchestration-process 0.2.2__tar.gz → 0.3.0__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.
- {agent_orchestration_process-0.2.2 → agent_orchestration_process-0.3.0}/PKG-INFO +1 -1
- {agent_orchestration_process-0.2.2 → agent_orchestration_process-0.3.0}/pyproject.toml +1 -1
- {agent_orchestration_process-0.2.2 → agent_orchestration_process-0.3.0}/pyproject.toml.orig +1 -1
- {agent_orchestration_process-0.2.2 → agent_orchestration_process-0.3.0}/src/agent_orchestration_process/cli.py +10 -1
- agent_orchestration_process-0.3.0/src/agent_orchestration_process/migration.py +519 -0
- {agent_orchestration_process-0.2.2 → agent_orchestration_process-0.3.0}/src/agent_orchestration_process/models.py +5 -0
- {agent_orchestration_process-0.2.2 → agent_orchestration_process-0.3.0}/src/agent_orchestration_process/pricing.py +81 -12
- {agent_orchestration_process-0.2.2 → agent_orchestration_process-0.3.0}/src/agent_orchestration_process/runner.py +460 -514
- {agent_orchestration_process-0.2.2 → agent_orchestration_process-0.3.0}/src/agent_orchestration_process/worktrees.py +40 -9
- {agent_orchestration_process-0.2.2 → agent_orchestration_process-0.3.0}/src/agent_orchestration_process/zcode.py +3 -15
- agent_orchestration_process-0.2.2/src/agent_orchestration_process/migration.py +0 -283
- {agent_orchestration_process-0.2.2 → agent_orchestration_process-0.3.0}/LICENSE +0 -0
- {agent_orchestration_process-0.2.2 → agent_orchestration_process-0.3.0}/README.md +0 -0
- {agent_orchestration_process-0.2.2 → agent_orchestration_process-0.3.0}/src/agent_orchestration_process/__init__.py +0 -0
- {agent_orchestration_process-0.2.2 → agent_orchestration_process-0.3.0}/src/agent_orchestration_process/__main__.py +0 -0
- {agent_orchestration_process-0.2.2 → agent_orchestration_process-0.3.0}/src/agent_orchestration_process/batch.py +0 -0
- {agent_orchestration_process-0.2.2 → agent_orchestration_process-0.3.0}/src/agent_orchestration_process/codex_auth.py +0 -0
- {agent_orchestration_process-0.2.2 → agent_orchestration_process-0.3.0}/src/agent_orchestration_process/codex_routes.py +0 -0
- {agent_orchestration_process-0.2.2 → agent_orchestration_process-0.3.0}/src/agent_orchestration_process/dsh_runner.mjs +0 -0
- {agent_orchestration_process-0.2.2 → agent_orchestration_process-0.3.0}/src/agent_orchestration_process/egress.py +0 -0
- {agent_orchestration_process-0.2.2 → agent_orchestration_process-0.3.0}/src/agent_orchestration_process/egress_bridge.py +0 -0
- {agent_orchestration_process-0.2.2 → agent_orchestration_process-0.3.0}/src/agent_orchestration_process/integration.py +0 -0
- {agent_orchestration_process-0.2.2 → agent_orchestration_process-0.3.0}/src/agent_orchestration_process/isolation.py +0 -0
- {agent_orchestration_process-0.2.2 → agent_orchestration_process-0.3.0}/src/agent_orchestration_process/locks.py +0 -0
- {agent_orchestration_process-0.2.2 → agent_orchestration_process-0.3.0}/src/agent_orchestration_process/model_catalog.py +0 -0
- {agent_orchestration_process-0.2.2 → agent_orchestration_process-0.3.0}/src/agent_orchestration_process/model_listing.py +0 -0
- {agent_orchestration_process-0.2.2 → agent_orchestration_process-0.3.0}/src/agent_orchestration_process/prompt_transport.py +0 -0
- {agent_orchestration_process-0.2.2 → agent_orchestration_process-0.3.0}/src/agent_orchestration_process/provider_versions.py +0 -0
- {agent_orchestration_process-0.2.2 → agent_orchestration_process-0.3.0}/src/agent_orchestration_process/zcode_protocol.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-orchestration-process
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Run bounded, non-interactive jobs across agent harnesses with isolation, exact resume, and provenance
|
|
5
5
|
Keywords: agent-orchestration,ai-agents,autoresearch,coding-agents,git-worktrees
|
|
6
6
|
Author: AOP contributors
|
|
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agent-orchestration-process"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Run bounded, non-interactive jobs across agent harnesses with isolation, exact resume, and provenance"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agent-orchestration-process"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Run bounded, non-interactive jobs across agent harnesses with isolation, exact resume, and provenance"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -30,12 +30,15 @@ from .runner import (
|
|
|
30
30
|
plan_inputs,
|
|
31
31
|
provider_runtime_record,
|
|
32
32
|
)
|
|
33
|
-
from .migration import migrate_runs, migration_roots, print_counts
|
|
33
|
+
from .migration import migrate_archive, migrate_runs, migration_roots, print_counts
|
|
34
34
|
from .worktrees import (
|
|
35
35
|
AOPError,
|
|
36
36
|
WorktreeManager,
|
|
37
|
+
compact_state_roots,
|
|
37
38
|
register_state_root,
|
|
38
39
|
registered_state_roots,
|
|
40
|
+
results_archive_dir,
|
|
41
|
+
state_roots_file,
|
|
39
42
|
)
|
|
40
43
|
|
|
41
44
|
|
|
@@ -748,6 +751,12 @@ def _migrate_results(runs_dirs: Sequence[Path], *, dry_run: bool) -> int:
|
|
|
748
751
|
errors += sum(
|
|
749
752
|
count for outcome, count in counts.items() if outcome.endswith("error")
|
|
750
753
|
)
|
|
754
|
+
counts = migrate_archive(dry_run=dry_run)
|
|
755
|
+
print_counts(results_archive_dir(), counts)
|
|
756
|
+
errors += counts["archive: error"]
|
|
757
|
+
if not dry_run:
|
|
758
|
+
kept, removed = compact_state_roots()
|
|
759
|
+
print(f"{state_roots_file()}: kept {kept} roots, removed {removed}")
|
|
751
760
|
return 1 if errors else 0
|
|
752
761
|
|
|
753
762
|
|
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
"""One-time migration of derived run records to their current formats.
|
|
2
|
+
|
|
3
|
+
Raw provider artifacts such as events.jsonl are never changed. Each rewritten
|
|
4
|
+
record keeps its original beside it and records the conversions applied.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import shutil
|
|
11
|
+
from collections import Counter
|
|
12
|
+
from collections.abc import Iterable
|
|
13
|
+
from dataclasses import replace
|
|
14
|
+
from datetime import UTC, datetime
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from .models import TOKEN_USAGE_SCHEMA, RunRequest, RunResult
|
|
19
|
+
from .pricing import TokenUsage, estimate_api_cost, nonnegative_int
|
|
20
|
+
from .runner import (
|
|
21
|
+
AgyAdapter,
|
|
22
|
+
ClaudeAdapter,
|
|
23
|
+
CursorAdapter,
|
|
24
|
+
DeepSeekHarnessAdapter,
|
|
25
|
+
GrokAdapter,
|
|
26
|
+
OpenCodeAdapter,
|
|
27
|
+
RunStore,
|
|
28
|
+
)
|
|
29
|
+
from .worktrees import results_archive_dir
|
|
30
|
+
|
|
31
|
+
ORIGINAL_SUFFIX = ".pre-v3.json"
|
|
32
|
+
LEGACY_TOKEN_USAGE_SCHEMA = "aop-token-usage-v1"
|
|
33
|
+
# v2 is v3's convention, except that Agy runs recorded Agy's result usage, the
|
|
34
|
+
# conversation's running total across every invocation that resumed it.
|
|
35
|
+
PREVIOUS_TOKEN_USAGE_SCHEMA = "aop-token-usage-v2"
|
|
36
|
+
_CACHE_WRITE_EVENT_PROVIDERS = frozenset(
|
|
37
|
+
{"claude", "cursor", "dsh", "grok", "opencode"}
|
|
38
|
+
)
|
|
39
|
+
_DERIVED_RESULT_FIELDS = (
|
|
40
|
+
"execution_completed",
|
|
41
|
+
"response_available",
|
|
42
|
+
"status",
|
|
43
|
+
"succeeded",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def migrate_runs(runs_dir: Path, *, dry_run: bool = False) -> Counter[str]:
|
|
48
|
+
"""Bring every run record under runs_dir to the current formats, and copy
|
|
49
|
+
each current result into the results archive."""
|
|
50
|
+
counts: Counter[str] = Counter()
|
|
51
|
+
for run_dir in sorted(path for path in runs_dir.iterdir() if path.is_dir()):
|
|
52
|
+
for name, upgrade in (
|
|
53
|
+
("request.json", _upgraded_request),
|
|
54
|
+
("result.json", _upgraded_result),
|
|
55
|
+
):
|
|
56
|
+
path = run_dir / name
|
|
57
|
+
value = _migrate_file(path, run_dir, upgrade, name, counts, dry_run)
|
|
58
|
+
if name == "result.json" and value is not None:
|
|
59
|
+
_archive(value, counts, dry_run)
|
|
60
|
+
return counts
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def migrate_archive(*, dry_run: bool = False) -> Counter[str]:
|
|
64
|
+
"""Bring archived results whose run directory is gone to the current format.
|
|
65
|
+
|
|
66
|
+
A result whose run directory still exists is migrated there and copied
|
|
67
|
+
here by migrate_runs, so the two stay identical.
|
|
68
|
+
"""
|
|
69
|
+
counts: Counter[str] = Counter()
|
|
70
|
+
archive = results_archive_dir()
|
|
71
|
+
if not archive.is_dir():
|
|
72
|
+
return counts
|
|
73
|
+
for path in sorted(archive.glob("*.json")):
|
|
74
|
+
try:
|
|
75
|
+
raw = json.loads(path.read_text())
|
|
76
|
+
run_dir = Path(raw["state_root"]) / "runs" / raw["run_id"]
|
|
77
|
+
except (OSError, ValueError, TypeError, KeyError) as error:
|
|
78
|
+
counts["archive: error"] += 1
|
|
79
|
+
print(f"aop: could not read archived result {path}: {error}", flush=True)
|
|
80
|
+
continue
|
|
81
|
+
if (run_dir / "result.json").is_file():
|
|
82
|
+
continue
|
|
83
|
+
_migrate_file(path, run_dir, _upgraded_result, "archive", counts, dry_run)
|
|
84
|
+
return counts
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _migrate_file(
|
|
88
|
+
path: Path,
|
|
89
|
+
run_dir: Path,
|
|
90
|
+
upgrade: Any,
|
|
91
|
+
label: str,
|
|
92
|
+
counts: Counter[str],
|
|
93
|
+
dry_run: bool,
|
|
94
|
+
) -> dict[str, Any] | None:
|
|
95
|
+
"""Upgrade one record in place, keeping its original; return the current value."""
|
|
96
|
+
if not path.is_file():
|
|
97
|
+
return None
|
|
98
|
+
try:
|
|
99
|
+
raw = json.loads(path.read_text())
|
|
100
|
+
upgraded = upgrade(raw, run_dir)
|
|
101
|
+
except (OSError, ValueError, TypeError, KeyError) as error:
|
|
102
|
+
counts[f"{label}: error"] += 1
|
|
103
|
+
print(f"aop: could not migrate {path}: {error}", flush=True)
|
|
104
|
+
return None
|
|
105
|
+
if upgraded is None:
|
|
106
|
+
counts[f"{label}: current"] += 1
|
|
107
|
+
return raw
|
|
108
|
+
value, outcome = upgraded
|
|
109
|
+
counts[f"{label}: {outcome}"] += 1
|
|
110
|
+
if not dry_run:
|
|
111
|
+
# Archived originals go in a subdirectory, so the archive's top level
|
|
112
|
+
# holds exactly one current record per run.
|
|
113
|
+
folder = path.parent / "originals" if label == "archive" else path.parent
|
|
114
|
+
original = folder / (path.name.removesuffix(".json") + ORIGINAL_SUFFIX)
|
|
115
|
+
if not original.exists():
|
|
116
|
+
folder.mkdir(exist_ok=True)
|
|
117
|
+
shutil.copy2(path, original)
|
|
118
|
+
RunStore.write_json(path, value)
|
|
119
|
+
return value
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _archive(value: dict[str, Any], counts: Counter[str], dry_run: bool) -> None:
|
|
123
|
+
path = results_archive_dir() / f"{value['run_id']}.json"
|
|
124
|
+
try:
|
|
125
|
+
current = json.loads(path.read_text()) == value
|
|
126
|
+
except (OSError, ValueError):
|
|
127
|
+
current = False
|
|
128
|
+
if current:
|
|
129
|
+
counts["archive: current"] += 1
|
|
130
|
+
return
|
|
131
|
+
counts["archive: copied"] += 1
|
|
132
|
+
if not dry_run:
|
|
133
|
+
RunStore.archive_result(value)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _upgraded_request(
|
|
137
|
+
raw: dict[str, Any], run_dir: Path
|
|
138
|
+
) -> tuple[dict[str, Any], str] | None:
|
|
139
|
+
try:
|
|
140
|
+
RunRequest.from_dict(raw)
|
|
141
|
+
return None
|
|
142
|
+
except (TypeError, ValueError, KeyError):
|
|
143
|
+
pass
|
|
144
|
+
fields = dict(raw)
|
|
145
|
+
conversions = []
|
|
146
|
+
if "sandbox" in fields:
|
|
147
|
+
# A Codex sandbox mode that no execution profile resolves, so a resume
|
|
148
|
+
# of such a run fails instead of guessing a profile.
|
|
149
|
+
fields.setdefault("profile", fields.pop("sandbox"))
|
|
150
|
+
conversions.append("sandbox-to-profile")
|
|
151
|
+
if "read_paths" in fields:
|
|
152
|
+
fields.setdefault("inputs", fields.pop("read_paths"))
|
|
153
|
+
conversions.append("read-paths-to-inputs")
|
|
154
|
+
for key, default in (
|
|
155
|
+
("mode", "agent"),
|
|
156
|
+
("inference_provider", None),
|
|
157
|
+
("inference_route", None),
|
|
158
|
+
("no_web", False),
|
|
159
|
+
("effective_policy", {}),
|
|
160
|
+
("artifacts", []),
|
|
161
|
+
("inputs", []),
|
|
162
|
+
):
|
|
163
|
+
if key not in fields:
|
|
164
|
+
fields[key] = default
|
|
165
|
+
conversions.append(f"default-{key.replace('_', '-')}")
|
|
166
|
+
fields["migration"] = _record(None, conversions, "request.json")
|
|
167
|
+
request = RunRequest.from_dict(fields)
|
|
168
|
+
return request.to_dict(), "+".join(conversions) or "normalized"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _upgraded_result(
|
|
172
|
+
raw: dict[str, Any], run_dir: Path
|
|
173
|
+
) -> tuple[dict[str, Any], str] | None:
|
|
174
|
+
if raw.get("state_root"):
|
|
175
|
+
return _upgraded_result_fields(raw, run_dir)
|
|
176
|
+
# Results recorded before state_root take it from where they are stored.
|
|
177
|
+
raw = {**raw, "state_root": str(run_dir.parent.parent.resolve())}
|
|
178
|
+
upgraded = _upgraded_result_fields(raw, run_dir)
|
|
179
|
+
if upgraded is None:
|
|
180
|
+
result = RunResult.from_dict(raw)
|
|
181
|
+
result = replace(
|
|
182
|
+
result,
|
|
183
|
+
migration=_record(
|
|
184
|
+
raw.get("usage_schema"), ["state-root"], "result.json"
|
|
185
|
+
),
|
|
186
|
+
)
|
|
187
|
+
return result.to_dict(), "state-root"
|
|
188
|
+
value, outcome = upgraded
|
|
189
|
+
value["migration"]["conversions"].append("state-root")
|
|
190
|
+
conversions, note, rest = outcome.partition(" (")
|
|
191
|
+
return value, f"{conversions}+state-root{note}{rest}"
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _upgraded_result_fields(
|
|
195
|
+
raw: dict[str, Any], run_dir: Path
|
|
196
|
+
) -> tuple[dict[str, Any], str] | None:
|
|
197
|
+
try:
|
|
198
|
+
current = RunResult.from_dict(raw)
|
|
199
|
+
return _upgraded_v3_result(raw, current, run_dir)
|
|
200
|
+
except (TypeError, ValueError, KeyError):
|
|
201
|
+
pass
|
|
202
|
+
schema = raw.get("usage_schema")
|
|
203
|
+
previous_usage = _raw_usage_counts(raw.get("usage"))
|
|
204
|
+
fields = {
|
|
205
|
+
key: value for key, value in raw.items() if key not in _DERIVED_RESULT_FIELDS
|
|
206
|
+
}
|
|
207
|
+
conversions = []
|
|
208
|
+
for key in (
|
|
209
|
+
"mode",
|
|
210
|
+
"model",
|
|
211
|
+
"effort",
|
|
212
|
+
"time_to_first_event_seconds",
|
|
213
|
+
"time_to_first_response_seconds",
|
|
214
|
+
"provider_duration_seconds",
|
|
215
|
+
"provider_status",
|
|
216
|
+
"provider_error",
|
|
217
|
+
"inference_provider",
|
|
218
|
+
"provider_reported_cost",
|
|
219
|
+
):
|
|
220
|
+
fields.setdefault(key, "agent" if key == "mode" else None)
|
|
221
|
+
if "read_paths" in fields:
|
|
222
|
+
fields.setdefault("inputs", fields.pop("read_paths"))
|
|
223
|
+
conversions.append("read-paths-to-inputs")
|
|
224
|
+
_fold_legacy_costs(fields, conversions)
|
|
225
|
+
|
|
226
|
+
if schema is None:
|
|
227
|
+
conversions.append("unversioned-usage")
|
|
228
|
+
_legacy_accounting(fields, _legacy_usage(fields))
|
|
229
|
+
elif schema == LEGACY_TOKEN_USAGE_SCHEMA:
|
|
230
|
+
conversions.append("v1-accounting")
|
|
231
|
+
_legacy_accounting(fields, TokenUsage.from_dict(fields.get("usage")))
|
|
232
|
+
elif schema == PREVIOUS_TOKEN_USAGE_SCHEMA:
|
|
233
|
+
conversions.append("v2-schema")
|
|
234
|
+
elif schema != TOKEN_USAGE_SCHEMA:
|
|
235
|
+
raise ValueError(f"unsupported token usage schema: {schema}")
|
|
236
|
+
if fields.get("provider") == "agy" and schema != TOKEN_USAGE_SCHEMA:
|
|
237
|
+
conversions.append("agy-step-usage")
|
|
238
|
+
_agy_step_usage(fields, run_dir)
|
|
239
|
+
if isinstance(fields.get("usage"), TokenUsage):
|
|
240
|
+
fields["usage"] = fields["usage"].to_dict()
|
|
241
|
+
|
|
242
|
+
fields["usage_schema"] = TOKEN_USAGE_SCHEMA
|
|
243
|
+
fields["migration"] = _record(schema, conversions, "result.json")
|
|
244
|
+
result = RunResult.from_dict(fields)
|
|
245
|
+
recovered, event_conversions = _recover_event_accounting(result, run_dir)
|
|
246
|
+
if event_conversions:
|
|
247
|
+
conversions.extend(event_conversions)
|
|
248
|
+
result = replace(
|
|
249
|
+
recovered,
|
|
250
|
+
migration=_record(schema, conversions, "result.json"),
|
|
251
|
+
)
|
|
252
|
+
outcome = "+".join(conversions) or "normalized"
|
|
253
|
+
if result.usage != previous_usage:
|
|
254
|
+
if "claude-resume-cost" not in event_conversions:
|
|
255
|
+
result = _repriced(result)
|
|
256
|
+
outcome += " (usage changed)"
|
|
257
|
+
return result.to_dict(), outcome
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _upgraded_v3_result(
|
|
261
|
+
raw: dict[str, Any], current: RunResult, run_dir: Path
|
|
262
|
+
) -> tuple[dict[str, Any], str] | None:
|
|
263
|
+
raw_usage = raw.get("usage")
|
|
264
|
+
if (
|
|
265
|
+
current.usage is None
|
|
266
|
+
or not isinstance(raw_usage, dict)
|
|
267
|
+
or "cache_write_input_tokens" in raw_usage
|
|
268
|
+
or current.provider not in _CACHE_WRITE_EVENT_PROVIDERS
|
|
269
|
+
):
|
|
270
|
+
return None
|
|
271
|
+
recovered, conversions = _recover_event_accounting(current, run_dir)
|
|
272
|
+
if not conversions:
|
|
273
|
+
return None
|
|
274
|
+
usage_changed = recovered.usage != current.usage
|
|
275
|
+
if usage_changed and "claude-resume-cost" not in conversions:
|
|
276
|
+
recovered = _repriced(recovered)
|
|
277
|
+
recovered = replace(
|
|
278
|
+
recovered,
|
|
279
|
+
migration=_record(TOKEN_USAGE_SCHEMA, conversions, "result.json"),
|
|
280
|
+
)
|
|
281
|
+
outcome = "+".join(conversions)
|
|
282
|
+
if usage_changed:
|
|
283
|
+
outcome += " (usage changed)"
|
|
284
|
+
return recovered.to_dict(), outcome
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _recover_event_accounting(
|
|
288
|
+
result: RunResult, run_dir: Path
|
|
289
|
+
) -> tuple[RunResult, list[str]]:
|
|
290
|
+
if result.usage is None or result.provider not in _CACHE_WRITE_EVENT_PROVIDERS:
|
|
291
|
+
return result, []
|
|
292
|
+
try:
|
|
293
|
+
events_text = (run_dir / "events.jsonl").read_text()
|
|
294
|
+
except OSError:
|
|
295
|
+
return result, []
|
|
296
|
+
conversions: list[str] = []
|
|
297
|
+
new_usage = result.usage
|
|
298
|
+
new_cost = result.calculated_cost
|
|
299
|
+
if result.provider == "claude":
|
|
300
|
+
parent_run_id = _parent_run_id(run_dir)
|
|
301
|
+
previous_total_cost_usd = (
|
|
302
|
+
ClaudeAdapter._reported_total_cost_usd(
|
|
303
|
+
run_dir.parent / parent_run_id / "events.jsonl"
|
|
304
|
+
)
|
|
305
|
+
if parent_run_id
|
|
306
|
+
else None
|
|
307
|
+
)
|
|
308
|
+
parsed = ClaudeAdapter._parse(
|
|
309
|
+
events_text,
|
|
310
|
+
resumed=parent_run_id is not None,
|
|
311
|
+
previous_total_cost_usd=previous_total_cost_usd,
|
|
312
|
+
)
|
|
313
|
+
if not parsed["usage_observed"]:
|
|
314
|
+
return result, []
|
|
315
|
+
event_usage = parsed["usage"]
|
|
316
|
+
if (
|
|
317
|
+
event_usage.cache_write_input_tokens > 0
|
|
318
|
+
and result.usage.cached_input_tokens + event_usage.cache_write_input_tokens
|
|
319
|
+
<= result.usage.input_tokens
|
|
320
|
+
):
|
|
321
|
+
new_usage = replace(
|
|
322
|
+
result.usage,
|
|
323
|
+
cache_write_input_tokens=event_usage.cache_write_input_tokens,
|
|
324
|
+
cache_write_1h_input_tokens=event_usage.cache_write_1h_input_tokens,
|
|
325
|
+
)
|
|
326
|
+
if parsed["cost_is_cumulative"]:
|
|
327
|
+
new_cost = parsed["cost"]
|
|
328
|
+
if new_cost is None and result.model:
|
|
329
|
+
new_cost = estimate_api_cost(
|
|
330
|
+
result.model,
|
|
331
|
+
new_usage,
|
|
332
|
+
provider="anthropic",
|
|
333
|
+
started_at=result.started_at,
|
|
334
|
+
finished_at=result.finished_at,
|
|
335
|
+
)
|
|
336
|
+
else:
|
|
337
|
+
if result.provider == "grok":
|
|
338
|
+
parsed = GrokAdapter._parse(events_text, result.model)
|
|
339
|
+
elif result.provider == "cursor":
|
|
340
|
+
parsed = CursorAdapter._parse(events_text)
|
|
341
|
+
elif result.provider == "opencode":
|
|
342
|
+
parsed = OpenCodeAdapter._parse(events_text, result.model)
|
|
343
|
+
else:
|
|
344
|
+
parsed = DeepSeekHarnessAdapter._parse(events_text)
|
|
345
|
+
if not parsed["usage_observed"]:
|
|
346
|
+
return result, []
|
|
347
|
+
event_usage = parsed["usage"]
|
|
348
|
+
if (
|
|
349
|
+
event_usage.cache_write_input_tokens > 0
|
|
350
|
+
and result.usage.cached_input_tokens + event_usage.cache_write_input_tokens
|
|
351
|
+
<= result.usage.input_tokens
|
|
352
|
+
):
|
|
353
|
+
new_usage = replace(
|
|
354
|
+
result.usage,
|
|
355
|
+
cache_write_input_tokens=event_usage.cache_write_input_tokens,
|
|
356
|
+
cache_write_1h_input_tokens=event_usage.cache_write_1h_input_tokens,
|
|
357
|
+
)
|
|
358
|
+
if new_usage != result.usage:
|
|
359
|
+
conversions.append("cache-write-usage")
|
|
360
|
+
if new_cost != result.calculated_cost:
|
|
361
|
+
conversions.append("claude-resume-cost")
|
|
362
|
+
if not conversions:
|
|
363
|
+
return result, []
|
|
364
|
+
return replace(result, usage=new_usage, calculated_cost=new_cost), conversions
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _parent_run_id(run_dir: Path) -> str | None:
|
|
368
|
+
try:
|
|
369
|
+
raw_request = json.loads((run_dir / "request.json").read_text())
|
|
370
|
+
except (OSError, ValueError):
|
|
371
|
+
return None
|
|
372
|
+
if not isinstance(raw_request, dict):
|
|
373
|
+
return None
|
|
374
|
+
parent_run_id = raw_request.get("parent_run_id")
|
|
375
|
+
return parent_run_id if isinstance(parent_run_id, str) and parent_run_id else None
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _record(schema: str | None, conversions: list[str], name: str) -> dict[str, Any]:
|
|
379
|
+
return {
|
|
380
|
+
"from_usage_schema": schema,
|
|
381
|
+
"conversions": conversions,
|
|
382
|
+
"original": name.removesuffix(".json") + ORIGINAL_SUFFIX,
|
|
383
|
+
"migrated_at": datetime.now(UTC).isoformat(),
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _fold_legacy_costs(fields: dict[str, Any], conversions: list[str]) -> None:
|
|
388
|
+
legacy_cost = fields.pop("api_equivalent_cost", None)
|
|
389
|
+
billing = fields.get("billing")
|
|
390
|
+
reported = isinstance(billing, dict) and billing.get("actual_cost_known") is True
|
|
391
|
+
if isinstance(billing, dict) and "actual_cost_known" in billing:
|
|
392
|
+
fields["billing"] = {
|
|
393
|
+
key: value for key, value in billing.items() if key != "actual_cost_known"
|
|
394
|
+
}
|
|
395
|
+
if legacy_cost is not None:
|
|
396
|
+
conversions.append("api-equivalent-cost")
|
|
397
|
+
if reported and fields.get("provider_reported_cost") is None:
|
|
398
|
+
fields["provider_reported_cost"] = {
|
|
399
|
+
"amount_usd": legacy_cost["amount_usd"],
|
|
400
|
+
"currency": legacy_cost.get("currency", "USD"),
|
|
401
|
+
"source": legacy_cost.get("pricing_source", "legacy run result"),
|
|
402
|
+
}
|
|
403
|
+
elif not reported:
|
|
404
|
+
fields.setdefault("calculated_cost", legacy_cost)
|
|
405
|
+
fields.setdefault("calculated_cost", None)
|
|
406
|
+
cost = fields.get("calculated_cost")
|
|
407
|
+
if isinstance(cost, dict) and "estimated" in cost:
|
|
408
|
+
fields["calculated_cost"] = {
|
|
409
|
+
key: value for key, value in cost.items() if key != "estimated"
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _raw_usage_counts(raw: object) -> TokenUsage | None:
|
|
414
|
+
if not isinstance(raw, dict):
|
|
415
|
+
return None
|
|
416
|
+
try:
|
|
417
|
+
return TokenUsage.from_dict(raw)
|
|
418
|
+
except ValueError:
|
|
419
|
+
return None
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _legacy_usage(fields: dict[str, Any]) -> TokenUsage:
|
|
423
|
+
raw = fields.get("usage") if isinstance(fields.get("usage"), dict) else {}
|
|
424
|
+
counts = {
|
|
425
|
+
key: nonnegative_int(raw.get(key))
|
|
426
|
+
for key in (
|
|
427
|
+
"input_tokens",
|
|
428
|
+
"cached_input_tokens",
|
|
429
|
+
"cache_write_input_tokens",
|
|
430
|
+
"cache_write_1h_input_tokens",
|
|
431
|
+
"output_tokens",
|
|
432
|
+
"reasoning_output_tokens",
|
|
433
|
+
)
|
|
434
|
+
}
|
|
435
|
+
# These adapters persisted their provider's disjoint uncached input
|
|
436
|
+
# bucket; OpenCode persisted visible output without its reasoning.
|
|
437
|
+
if fields.get("provider") in {"agy", "cursor", "dsh"}:
|
|
438
|
+
counts["input_tokens"] += (
|
|
439
|
+
counts["cached_input_tokens"] + counts["cache_write_input_tokens"]
|
|
440
|
+
)
|
|
441
|
+
if fields.get("provider") == "opencode":
|
|
442
|
+
counts["output_tokens"] += counts["reasoning_output_tokens"]
|
|
443
|
+
return TokenUsage(**counts)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _legacy_accounting(fields: dict[str, Any], usage: TokenUsage) -> None:
|
|
447
|
+
if not fields.get("timed_out"):
|
|
448
|
+
fields["usage"] = usage
|
|
449
|
+
fields["accounting_status"] = "complete"
|
|
450
|
+
return
|
|
451
|
+
observed = usage.total_tokens > 0
|
|
452
|
+
if not observed:
|
|
453
|
+
fields["calculated_cost"] = None
|
|
454
|
+
fields["usage"] = usage if observed else None
|
|
455
|
+
fields["accounting_status"] = (
|
|
456
|
+
"partial"
|
|
457
|
+
if observed or fields.get("provider_reported_cost") is not None
|
|
458
|
+
else "unavailable"
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def _agy_step_usage(fields: dict[str, Any], run_dir: Path) -> None:
|
|
463
|
+
try:
|
|
464
|
+
parsed = AgyAdapter._parse((run_dir / "events.jsonl").read_text())
|
|
465
|
+
except OSError:
|
|
466
|
+
parsed = {"usage_observed": False}
|
|
467
|
+
if parsed["usage_observed"]:
|
|
468
|
+
fields["usage"] = parsed["usage"]
|
|
469
|
+
if fields.get("accounting_status") == "unavailable":
|
|
470
|
+
fields["accounting_status"] = "partial"
|
|
471
|
+
return
|
|
472
|
+
fields.update(
|
|
473
|
+
usage=None,
|
|
474
|
+
calculated_cost=None,
|
|
475
|
+
provider_reported_cost=None,
|
|
476
|
+
accounting_status="unavailable",
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _repriced(result: RunResult) -> RunResult:
|
|
481
|
+
"""Price changed usage at the rates in effect when the run happened."""
|
|
482
|
+
if (
|
|
483
|
+
result.provider == "claude"
|
|
484
|
+
and result.calculated_cost is not None
|
|
485
|
+
and result.calculated_cost.pricing_version == "claude-cli-reported"
|
|
486
|
+
):
|
|
487
|
+
return result
|
|
488
|
+
provider = {
|
|
489
|
+
"agy": "google",
|
|
490
|
+
"claude": "anthropic",
|
|
491
|
+
"dsh": DeepSeekHarnessAdapter.pricing_provider(result.inference_provider),
|
|
492
|
+
"grok": "xai",
|
|
493
|
+
}.get(result.provider)
|
|
494
|
+
priced = result.provider in {"agy", "claude", "dsh", "grok", "opencode"}
|
|
495
|
+
cost = (
|
|
496
|
+
estimate_api_cost(
|
|
497
|
+
result.model,
|
|
498
|
+
result.usage,
|
|
499
|
+
provider=provider,
|
|
500
|
+
started_at=result.started_at,
|
|
501
|
+
finished_at=result.finished_at,
|
|
502
|
+
)
|
|
503
|
+
if priced and result.usage is not None and result.model
|
|
504
|
+
else None
|
|
505
|
+
)
|
|
506
|
+
return replace(result, calculated_cost=cost)
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def migration_roots(registered: Iterable[Path], current: Path | None) -> list[Path]:
|
|
510
|
+
roots = [root / "runs" for root in registered]
|
|
511
|
+
if current is not None:
|
|
512
|
+
roots.append(current / "runs")
|
|
513
|
+
return [root for root in dict.fromkeys(roots) if root.is_dir()]
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def print_counts(root: Path, counts: Counter[str]) -> None:
|
|
517
|
+
print(root)
|
|
518
|
+
for outcome, count in sorted(counts.items()):
|
|
519
|
+
print(f" {count:>7} {outcome}")
|
|
@@ -205,6 +205,9 @@ class RunResult:
|
|
|
205
205
|
provider_error: str | None = None
|
|
206
206
|
accounting_status: str = "complete"
|
|
207
207
|
usage_schema: str = TOKEN_USAGE_SCHEMA
|
|
208
|
+
# The .aop state directory the run was recorded in, as listed in the
|
|
209
|
+
# state-roots registry. Set when the result is written.
|
|
210
|
+
state_root: str | None = None
|
|
208
211
|
# How `aop migrate-results` converted an older record, if it did.
|
|
209
212
|
migration: dict[str, Any] | None = None
|
|
210
213
|
|
|
@@ -285,6 +288,8 @@ class RunResult:
|
|
|
285
288
|
f"unsupported token usage schema {fields.get('usage_schema')!r}; "
|
|
286
289
|
f"{MIGRATE_HINT}"
|
|
287
290
|
)
|
|
291
|
+
if not fields.get("state_root"):
|
|
292
|
+
raise ValueError(f"run result has no state_root; {MIGRATE_HINT}")
|
|
288
293
|
usage = fields["usage"]
|
|
289
294
|
fields["usage"] = None if usage is None else TokenUsage.from_dict(usage)
|
|
290
295
|
fields["calculated_cost"] = CalculatedCost.from_dict(fields["calculated_cost"])
|