agentic-devtools 0.2.613__py3-none-any.whl → 0.2.615__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.
- agentic_devtools/_version.py +2 -2
- agentic_devtools/cli/ci/agent_task_lifecycle.py +143 -0
- agentic_devtools/cli/ci/cooldown.py +10 -70
- agentic_devtools/cli/ci/cooldown_persistence.py +89 -0
- agentic_devtools/cli/ci/github_provider.py +99 -176
- agentic_devtools/cli/ci/read_token_pool.py +19 -4
- agentic_devtools/cli/ci/read_token_pool_execution.py +299 -0
- {agentic_devtools-0.2.613.dist-info → agentic_devtools-0.2.615.dist-info}/METADATA +1 -1
- {agentic_devtools-0.2.613.dist-info → agentic_devtools-0.2.615.dist-info}/RECORD +12 -10
- {agentic_devtools-0.2.613.dist-info → agentic_devtools-0.2.615.dist-info}/WHEEL +0 -0
- {agentic_devtools-0.2.613.dist-info → agentic_devtools-0.2.615.dist-info}/entry_points.txt +0 -0
- {agentic_devtools-0.2.613.dist-info → agentic_devtools-0.2.615.dist-info}/licenses/LICENSE +0 -0
agentic_devtools/_version.py
CHANGED
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.2.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 2,
|
|
21
|
+
__version__ = version = '0.2.615'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 615)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -134,3 +134,146 @@ def select_task_lifecycle_timestamp(task: AgentTaskLifecycle) -> datetime | None
|
|
|
134
134
|
if session_timestamp is not None:
|
|
135
135
|
timestamps.append(session_timestamp)
|
|
136
136
|
return max(timestamps, default=None)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _agent_task_lifecycle_detail_value(record: Mapping[str, object], *keys: str) -> object:
|
|
140
|
+
"""Return the first matching record or metadata alias."""
|
|
141
|
+
for key in keys:
|
|
142
|
+
if key in record:
|
|
143
|
+
return record[key]
|
|
144
|
+
metadata = record.get("metadata")
|
|
145
|
+
if isinstance(metadata, dict):
|
|
146
|
+
for key in keys:
|
|
147
|
+
if key in metadata:
|
|
148
|
+
return metadata[key]
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _map_agent_task_lifecycle_session(
|
|
153
|
+
task: Mapping[str, object],
|
|
154
|
+
session: object,
|
|
155
|
+
*,
|
|
156
|
+
task_id: str,
|
|
157
|
+
task_state: str,
|
|
158
|
+
top_level_session_record: bool,
|
|
159
|
+
) -> tuple[AgentTaskSessionLifecycle | None, tuple[str, ...]]:
|
|
160
|
+
"""Normalize one session record while preserving identity and malformed-data facts."""
|
|
161
|
+
if not isinstance(session, dict):
|
|
162
|
+
return None, (f"malformed-session:{task_id}",)
|
|
163
|
+
reasons: list[str] = []
|
|
164
|
+
session_id_value = _agent_task_lifecycle_detail_value(session, "session_id", "sessionId") or (
|
|
165
|
+
_agent_task_lifecycle_detail_value(task, "session_id", "sessionId")
|
|
166
|
+
if top_level_session_record
|
|
167
|
+
else _agent_task_lifecycle_detail_value(session, "id")
|
|
168
|
+
)
|
|
169
|
+
if not isinstance(session_id_value, str) or not session_id_value:
|
|
170
|
+
reasons.append(f"missing-session-id:{task_id}")
|
|
171
|
+
session_id_value = ""
|
|
172
|
+
session_state_value = _agent_task_lifecycle_detail_value(session, "state", "status")
|
|
173
|
+
if top_level_session_record:
|
|
174
|
+
session_state_value = task_state
|
|
175
|
+
started, session_created_at, idle = session_lifecycle_timestamp_values(
|
|
176
|
+
session,
|
|
177
|
+
include_created=not top_level_session_record,
|
|
178
|
+
fallback=task if top_level_session_record else None,
|
|
179
|
+
)
|
|
180
|
+
session_state = session_state_value if isinstance(session_state_value, str) else ""
|
|
181
|
+
session_head_sha = _agent_task_lifecycle_detail_value(session, "head_sha", "headSha")
|
|
182
|
+
head_sha = session_head_sha if isinstance(session_head_sha, str) else ""
|
|
183
|
+
session_run_id = _agent_task_lifecycle_detail_value(session, "workflow_run_id", "workflowRunId", "run_id", "runId")
|
|
184
|
+
run_id = (
|
|
185
|
+
str(session_run_id) if isinstance(session_run_id, (str, int)) and not isinstance(session_run_id, bool) else ""
|
|
186
|
+
)
|
|
187
|
+
session_values = (session_state, started, session_created_at, idle, head_sha, run_id)
|
|
188
|
+
if not session_id_value and not any(session_values):
|
|
189
|
+
return None, tuple(reasons)
|
|
190
|
+
return (
|
|
191
|
+
AgentTaskSessionLifecycle(
|
|
192
|
+
session_id=session_id_value,
|
|
193
|
+
state=session_state,
|
|
194
|
+
started_at=started,
|
|
195
|
+
idle_at=idle,
|
|
196
|
+
head_sha=head_sha,
|
|
197
|
+
workflow_run_id=run_id,
|
|
198
|
+
created_at=session_created_at,
|
|
199
|
+
),
|
|
200
|
+
tuple(reasons),
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _map_agent_task_lifecycle_sessions(
|
|
205
|
+
task: Mapping[str, object],
|
|
206
|
+
detail: Mapping[str, object],
|
|
207
|
+
*,
|
|
208
|
+
task_id: str,
|
|
209
|
+
task_state: str,
|
|
210
|
+
) -> tuple[tuple[AgentTaskSessionLifecycle, ...], tuple[str, ...]]:
|
|
211
|
+
"""Normalize nested or singleton sessions and return task-scoped diagnostics."""
|
|
212
|
+
session_records = detail.get("sessions", task.get("sessions"))
|
|
213
|
+
reasons: list[str] = []
|
|
214
|
+
top_level_session_record = False
|
|
215
|
+
if not isinstance(session_records, list):
|
|
216
|
+
if session_records is not None:
|
|
217
|
+
reasons.append(f"malformed-session:{task_id}")
|
|
218
|
+
top_level_session_record = bool(
|
|
219
|
+
_agent_task_lifecycle_detail_value(detail, "session_id", "sessionId")
|
|
220
|
+
or has_session_activity_timestamp(detail, fallback=task)
|
|
221
|
+
)
|
|
222
|
+
session_records = [detail] if top_level_session_record else []
|
|
223
|
+
sessions: list[AgentTaskSessionLifecycle] = []
|
|
224
|
+
for session in session_records:
|
|
225
|
+
normalized, diagnostics = _map_agent_task_lifecycle_session(
|
|
226
|
+
task,
|
|
227
|
+
session,
|
|
228
|
+
task_id=task_id,
|
|
229
|
+
task_state=task_state,
|
|
230
|
+
top_level_session_record=top_level_session_record,
|
|
231
|
+
)
|
|
232
|
+
reasons.extend(diagnostics)
|
|
233
|
+
if normalized is not None:
|
|
234
|
+
sessions.append(normalized)
|
|
235
|
+
return tuple(sessions), tuple(reasons)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _map_agent_task_lifecycle_detail(
|
|
239
|
+
task: Mapping[str, object],
|
|
240
|
+
detail: Mapping[str, object],
|
|
241
|
+
*,
|
|
242
|
+
task_id: str,
|
|
243
|
+
repository: str,
|
|
244
|
+
pr_number: int,
|
|
245
|
+
task_pr_number: int | None,
|
|
246
|
+
head_branch: str,
|
|
247
|
+
) -> tuple[AgentTaskLifecycle, tuple[str, ...]]:
|
|
248
|
+
"""Normalize one fetched task detail record and retain task-scoped diagnostics."""
|
|
249
|
+
state_value = _agent_task_lifecycle_detail_value(detail, "state", "status")
|
|
250
|
+
if not isinstance(state_value, str) or not state_value.strip():
|
|
251
|
+
state_value = _agent_task_lifecycle_detail_value(task, "state", "status")
|
|
252
|
+
state = state_value if isinstance(state_value, str) else ""
|
|
253
|
+
created_at = select_valid_timestamp_alias(detail, "created_at", "createdAt", fallback=task)
|
|
254
|
+
started_at = select_valid_timestamp_alias(detail, "started_at", "startedAt", fallback=task)
|
|
255
|
+
idle_at = select_valid_timestamp_alias(detail, "idle_at", "idleAt", "idle_since", "idleSince", fallback=task)
|
|
256
|
+
sessions, reasons = _map_agent_task_lifecycle_sessions(
|
|
257
|
+
task,
|
|
258
|
+
detail,
|
|
259
|
+
task_id=task_id,
|
|
260
|
+
task_state=state,
|
|
261
|
+
)
|
|
262
|
+
task_head_sha = _agent_task_lifecycle_detail_value(detail, "head_sha", "headSha")
|
|
263
|
+
task_run_id = _agent_task_lifecycle_detail_value(detail, "workflow_run_id", "workflowRunId", "run_id", "runId")
|
|
264
|
+
lifecycle = AgentTaskLifecycle(
|
|
265
|
+
task_id=task_id,
|
|
266
|
+
repository=repository,
|
|
267
|
+
pr_number=task_pr_number if task_pr_number is not None else pr_number,
|
|
268
|
+
head_branch=head_branch,
|
|
269
|
+
state=state,
|
|
270
|
+
created_at=created_at,
|
|
271
|
+
sessions=sessions,
|
|
272
|
+
head_sha=task_head_sha if isinstance(task_head_sha, str) else "",
|
|
273
|
+
workflow_run_id=str(task_run_id)
|
|
274
|
+
if isinstance(task_run_id, (str, int)) and not isinstance(task_run_id, bool)
|
|
275
|
+
else "",
|
|
276
|
+
started_at=started_at,
|
|
277
|
+
idle_at=idle_at,
|
|
278
|
+
)
|
|
279
|
+
return lifecycle, reasons
|
|
@@ -23,12 +23,8 @@ from agentic_devtools.cli.ci.credential_roles import (
|
|
|
23
23
|
REPO_VARIABLE_WRITER_PAT,
|
|
24
24
|
)
|
|
25
25
|
from agentic_devtools.cli.shared.retry import (
|
|
26
|
-
DEFAULT_RATE_LIMIT_FALLBACK_DELAY,
|
|
27
26
|
DEFAULT_RATE_LIMIT_MAX_DELAY,
|
|
28
|
-
DEFAULT_RATE_LIMIT_SAFETY_MARGIN,
|
|
29
27
|
ProviderRateLimitError,
|
|
30
|
-
RateLimitDelay,
|
|
31
|
-
calculate_rate_limit_delay,
|
|
32
28
|
)
|
|
33
29
|
|
|
34
30
|
if TYPE_CHECKING:
|
|
@@ -234,13 +230,13 @@ def read_cooldowns(
|
|
|
234
230
|
) -> dict[str, CooldownRecord]:
|
|
235
231
|
"""Read active cooldowns; unavailable state fails open for scheduling."""
|
|
236
232
|
try:
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
logger.warning("Could not read provider cooldowns; continuing fail-open: %s", type(exc).__name__)
|
|
242
|
-
return {}
|
|
233
|
+
from agentic_devtools.cli.ci.read_token_pool_execution import _read_cooldown_value
|
|
234
|
+
|
|
235
|
+
value = _read_cooldown_value(provider, use_writer_token=use_writer_token)
|
|
236
|
+
return parse_cooldowns(value, now=now)
|
|
243
237
|
except Exception as exc:
|
|
238
|
+
if isinstance(exc, ProviderRateLimitError) and exc.is_rate_limit:
|
|
239
|
+
raise
|
|
244
240
|
logger.warning("Could not read provider cooldowns; continuing fail-open: %s", type(exc).__name__)
|
|
245
241
|
return {}
|
|
246
242
|
|
|
@@ -270,68 +266,12 @@ def persist_cooldown(
|
|
|
270
266
|
*,
|
|
271
267
|
now: float | None = None,
|
|
272
268
|
retries: int = 3,
|
|
269
|
+
observed_at: float | None = None,
|
|
273
270
|
) -> tuple[str, CooldownRecord] | None:
|
|
274
|
-
"""Persist a
|
|
271
|
+
"""Persist a provider cooldown, optionally anchoring its expiry to its observation time."""
|
|
272
|
+
from agentic_devtools.cli.ci.cooldown_persistence import persist_cooldown as persist
|
|
275
273
|
|
|
276
|
-
|
|
277
|
-
reads the current variable, merges all known records, writes the merged state,
|
|
278
|
-
and verifies that no concurrent writer shortened an active cooldown. The
|
|
279
|
-
in-process ``_WRITE_LOCK`` serializes concurrent callers within the same process.
|
|
280
|
-
"""
|
|
281
|
-
current = time.time() if now is None else float(now)
|
|
282
|
-
identity = error.credential_identity or credential_identity_from_environment()
|
|
283
|
-
key = cooldown_key(identity, provider=error.provider or DEFAULT_PROVIDER)
|
|
284
|
-
delay: RateLimitDelay = calculate_rate_limit_delay(
|
|
285
|
-
retry_after_seconds=error.retry_after_seconds,
|
|
286
|
-
reset_timestamp=error.reset_timestamp,
|
|
287
|
-
now=current,
|
|
288
|
-
fallback_delay=DEFAULT_RATE_LIMIT_FALLBACK_DELAY,
|
|
289
|
-
safety_margin=DEFAULT_RATE_LIMIT_SAFETY_MARGIN,
|
|
290
|
-
max_delay=DEFAULT_RATE_LIMIT_MAX_DELAY,
|
|
291
|
-
)
|
|
292
|
-
record = CooldownRecord(
|
|
293
|
-
resume_at=delay.resume_at,
|
|
294
|
-
reason="rate_limit",
|
|
295
|
-
source=error.source if error.source in _ALLOWED_SOURCE_VALUES else delay.source,
|
|
296
|
-
updated_at=current,
|
|
297
|
-
)
|
|
298
|
-
known_records: dict[str, CooldownRecord] = {}
|
|
299
|
-
with _WRITE_LOCK:
|
|
300
|
-
for _ in range(max(1, retries)):
|
|
301
|
-
try:
|
|
302
|
-
raw = provider.get_variable(COOLDOWN_VARIABLE, use_writer_token=True)
|
|
303
|
-
observed_records = parse_cooldowns(raw, now=current)
|
|
304
|
-
for observed_key, observed_record in observed_records.items():
|
|
305
|
-
known_record = known_records.get(observed_key)
|
|
306
|
-
if known_record is None or (
|
|
307
|
-
observed_record.resume_at > known_record.resume_at
|
|
308
|
-
or (
|
|
309
|
-
observed_record.resume_at == known_record.resume_at
|
|
310
|
-
and observed_record.updated_at > known_record.updated_at
|
|
311
|
-
)
|
|
312
|
-
):
|
|
313
|
-
known_records[observed_key] = observed_record
|
|
314
|
-
records = dict(known_records)
|
|
315
|
-
existing = records.get(key)
|
|
316
|
-
if existing is None or existing.resume_at < record.resume_at:
|
|
317
|
-
records[key] = record
|
|
318
|
-
merged = serialize_cooldowns(records)
|
|
319
|
-
provider.set_variable(COOLDOWN_VARIABLE, merged)
|
|
320
|
-
verified_raw = provider.get_variable(COOLDOWN_VARIABLE, use_writer_token=True)
|
|
321
|
-
verified_records = parse_cooldowns(verified_raw, now=current)
|
|
322
|
-
effective = verified_records.get(key)
|
|
323
|
-
if not _preserves_cooldown_records(records, verified_records):
|
|
324
|
-
logger.warning("Provider cooldown write was stale; reconciling records before retrying")
|
|
325
|
-
continue
|
|
326
|
-
if effective is None: # pragma: no cover - covered by the preservation check
|
|
327
|
-
continue
|
|
328
|
-
return key, effective
|
|
329
|
-
except Exception as exc:
|
|
330
|
-
logger.warning("Could not persist provider cooldown; retrying: %s", type(exc).__name__)
|
|
331
|
-
if _ + 1 < max(1, retries):
|
|
332
|
-
time.sleep(1.0)
|
|
333
|
-
logger.warning("Provider cooldown could not be persisted; keeping rate-limit pause in effect")
|
|
334
|
-
return None
|
|
274
|
+
return persist(provider, error, now=now, retries=retries, observed_at=observed_at)
|
|
335
275
|
|
|
336
276
|
|
|
337
277
|
def format_resume_at(timestamp: float) -> str:
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Persistence implementation for durable provider cooldowns."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import time
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
import agentic_devtools.cli.ci.cooldown as cooldown
|
|
10
|
+
from agentic_devtools.cli.shared.retry import (
|
|
11
|
+
DEFAULT_RATE_LIMIT_FALLBACK_DELAY,
|
|
12
|
+
DEFAULT_RATE_LIMIT_MAX_DELAY,
|
|
13
|
+
DEFAULT_RATE_LIMIT_SAFETY_MARGIN,
|
|
14
|
+
ProviderRateLimitError,
|
|
15
|
+
RateLimitDelay,
|
|
16
|
+
calculate_rate_limit_delay,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from agentic_devtools.cli.ci.provider import CIPlatformProvider
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def persist_cooldown(
|
|
26
|
+
provider: CIPlatformProvider,
|
|
27
|
+
error: ProviderRateLimitError,
|
|
28
|
+
*,
|
|
29
|
+
now: float | None = None,
|
|
30
|
+
retries: int = 3,
|
|
31
|
+
observed_at: float | None = None,
|
|
32
|
+
) -> tuple[str, cooldown.CooldownRecord] | None:
|
|
33
|
+
"""Persist a cooldown using current time for validation and observation time for its expiry."""
|
|
34
|
+
current = time.time() if now is None else float(now)
|
|
35
|
+
event_time = current if observed_at is None else float(observed_at)
|
|
36
|
+
identity = error.credential_identity or cooldown.credential_identity_from_environment()
|
|
37
|
+
key = cooldown.cooldown_key(identity, provider=error.provider or cooldown.DEFAULT_PROVIDER)
|
|
38
|
+
delay: RateLimitDelay = calculate_rate_limit_delay(
|
|
39
|
+
retry_after_seconds=error.retry_after_seconds,
|
|
40
|
+
reset_timestamp=error.reset_timestamp,
|
|
41
|
+
now=event_time,
|
|
42
|
+
fallback_delay=DEFAULT_RATE_LIMIT_FALLBACK_DELAY,
|
|
43
|
+
safety_margin=DEFAULT_RATE_LIMIT_SAFETY_MARGIN,
|
|
44
|
+
max_delay=DEFAULT_RATE_LIMIT_MAX_DELAY,
|
|
45
|
+
)
|
|
46
|
+
record = cooldown.CooldownRecord(
|
|
47
|
+
resume_at=delay.resume_at,
|
|
48
|
+
reason="rate_limit",
|
|
49
|
+
source=error.source if error.source in cooldown._ALLOWED_SOURCE_VALUES else delay.source,
|
|
50
|
+
updated_at=event_time,
|
|
51
|
+
)
|
|
52
|
+
known_records: dict[str, cooldown.CooldownRecord] = {}
|
|
53
|
+
with cooldown._WRITE_LOCK:
|
|
54
|
+
for attempt in range(max(1, retries)):
|
|
55
|
+
try:
|
|
56
|
+
raw = provider.get_variable(cooldown.COOLDOWN_VARIABLE, use_writer_token=True)
|
|
57
|
+
observed_records = cooldown.parse_cooldowns(raw, now=current)
|
|
58
|
+
for observed_key, observed_record in observed_records.items():
|
|
59
|
+
known_record = known_records.get(observed_key)
|
|
60
|
+
if known_record is None or (
|
|
61
|
+
observed_record.resume_at > known_record.resume_at
|
|
62
|
+
or (
|
|
63
|
+
observed_record.resume_at == known_record.resume_at
|
|
64
|
+
and observed_record.updated_at > known_record.updated_at
|
|
65
|
+
)
|
|
66
|
+
):
|
|
67
|
+
known_records[observed_key] = observed_record
|
|
68
|
+
records = dict(known_records)
|
|
69
|
+
existing = records.get(key)
|
|
70
|
+
if existing is None or existing.resume_at < record.resume_at:
|
|
71
|
+
records[key] = record
|
|
72
|
+
merged = cooldown.serialize_cooldowns(records)
|
|
73
|
+
provider.set_variable(cooldown.COOLDOWN_VARIABLE, merged)
|
|
74
|
+
verified_raw = provider.get_variable(cooldown.COOLDOWN_VARIABLE, use_writer_token=True)
|
|
75
|
+
verified_records = cooldown.parse_cooldowns(verified_raw, now=current)
|
|
76
|
+
effective = verified_records.get(key)
|
|
77
|
+
expected_records = {item_key: item for item_key, item in records.items() if item.resume_at > current}
|
|
78
|
+
if not cooldown._preserves_cooldown_records(expected_records, verified_records):
|
|
79
|
+
logger.warning("Provider cooldown write was stale; reconciling records before retrying")
|
|
80
|
+
continue
|
|
81
|
+
if effective is None:
|
|
82
|
+
return None
|
|
83
|
+
return key, effective
|
|
84
|
+
except Exception as exc:
|
|
85
|
+
logger.warning("Could not persist provider cooldown; retrying: %s", type(exc).__name__)
|
|
86
|
+
if attempt + 1 < max(1, retries):
|
|
87
|
+
time.sleep(1.0)
|
|
88
|
+
logger.warning("Provider cooldown could not be persisted; keeping rate-limit pause in effect")
|
|
89
|
+
return None
|
|
@@ -93,6 +93,7 @@ from agentic_devtools.cli.ci.provider import (
|
|
|
93
93
|
RepairDispatchNotAttemptedError,
|
|
94
94
|
)
|
|
95
95
|
from agentic_devtools.cli.ci.read_token_pool import ReadTokenPool
|
|
96
|
+
from agentic_devtools.cli.ci.read_token_pool_execution import ReadTokenPoolOperations
|
|
96
97
|
from agentic_devtools.cli.ci.reconciliation import config
|
|
97
98
|
from agentic_devtools.cli.ci.reconciliation.models import ObservationOutcome
|
|
98
99
|
from agentic_devtools.cli.ci.resolution.engine import TieredResolutionEngine
|
|
@@ -2317,6 +2318,9 @@ def _build_repair_comment(
|
|
|
2317
2318
|
return _enforce_comment_size_limit(body)
|
|
2318
2319
|
|
|
2319
2320
|
|
|
2321
|
+
_InventoryDeadlineExceeded = ReadTokenPoolOperations.DeadlineExpired
|
|
2322
|
+
|
|
2323
|
+
|
|
2320
2324
|
def _gh_api(
|
|
2321
2325
|
endpoint: str,
|
|
2322
2326
|
*,
|
|
@@ -2330,6 +2334,9 @@ def _gh_api(
|
|
|
2330
2334
|
response_metadata: dict[str, float | int | None] | None = None,
|
|
2331
2335
|
read_pool: bool = True,
|
|
2332
2336
|
timeout_seconds: int = _GH_CLI_TIMEOUT_SECONDS,
|
|
2337
|
+
_deadline: float | None = None,
|
|
2338
|
+
_monotonic: Callable[[], float] | None = None,
|
|
2339
|
+
_pool_managed: bool = False,
|
|
2333
2340
|
) -> str:
|
|
2334
2341
|
"""Call the GitHub API via the ``gh`` CLI.
|
|
2335
2342
|
|
|
@@ -2357,12 +2364,15 @@ def _gh_api(
|
|
|
2357
2364
|
"""
|
|
2358
2365
|
if not isinstance(timeout_seconds, int) or isinstance(timeout_seconds, bool) or timeout_seconds <= 0:
|
|
2359
2366
|
raise ValueError("timeout_seconds must be a positive integer")
|
|
2367
|
+
_deadline, _monotonic = ReadTokenPoolOperations.resolve_request_context(_deadline, _monotonic)
|
|
2360
2368
|
target_repository = _target_repository_from_gh_arguments(["api", endpoint])
|
|
2361
2369
|
if method.upper() == "GET" and _should_use_read_token_pool(
|
|
2362
2370
|
token=token,
|
|
2363
2371
|
credential_identity=credential_identity,
|
|
2364
2372
|
read_pool=read_pool,
|
|
2365
2373
|
target_repository=target_repository,
|
|
2374
|
+
deadline=_deadline,
|
|
2375
|
+
monotonic=_monotonic,
|
|
2366
2376
|
):
|
|
2367
2377
|
|
|
2368
2378
|
def pooled_request(selected_token: str | None, identity: str) -> tuple[str, int | None]:
|
|
@@ -2379,13 +2389,21 @@ def _gh_api(
|
|
|
2379
2389
|
response_metadata=request_metadata,
|
|
2380
2390
|
read_pool=False,
|
|
2381
2391
|
timeout_seconds=timeout_seconds,
|
|
2392
|
+
_deadline=_deadline,
|
|
2393
|
+
_monotonic=_monotonic,
|
|
2394
|
+
_pool_managed=True,
|
|
2382
2395
|
)
|
|
2383
2396
|
if response_metadata is not None:
|
|
2384
2397
|
response_metadata.update(request_metadata)
|
|
2385
2398
|
remaining_value = request_metadata.get("remaining")
|
|
2386
2399
|
return response, remaining_value if isinstance(remaining_value, int) else None
|
|
2387
2400
|
|
|
2388
|
-
return _call_with_read_token_pool(
|
|
2401
|
+
return _call_with_read_token_pool(
|
|
2402
|
+
"rest",
|
|
2403
|
+
pooled_request,
|
|
2404
|
+
deadline=_deadline,
|
|
2405
|
+
monotonic=_monotonic,
|
|
2406
|
+
)
|
|
2389
2407
|
|
|
2390
2408
|
cmd = ["gh", "api", endpoint, "--method", method]
|
|
2391
2409
|
if include_headers:
|
|
@@ -2407,16 +2425,21 @@ def _gh_api(
|
|
|
2407
2425
|
is_agent_tasks = endpoint.partition("?")[0].startswith(AGENT_TASKS_ENDPOINT_TEMPLATE.partition("{owner}")[0])
|
|
2408
2426
|
diagnostics = AgentTaskDiagnostics(endpoint, env, _sanitize_gh_error, is_agent_tasks=is_agent_tasks)
|
|
2409
2427
|
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2428
|
+
request_timeout_seconds = ReadTokenPoolOperations.request_timeout(timeout_seconds, _deadline, _monotonic)
|
|
2429
|
+
result = ReadTokenPoolOperations.run_request(
|
|
2430
|
+
lambda: diagnostics.run(
|
|
2431
|
+
run_safe,
|
|
2432
|
+
cmd,
|
|
2433
|
+
f"gh api {endpoint} timed out after {request_timeout_seconds}s",
|
|
2434
|
+
capture_output=True,
|
|
2435
|
+
text=True,
|
|
2436
|
+
shell=False,
|
|
2437
|
+
input=json.dumps(body) if body else None,
|
|
2438
|
+
env=env,
|
|
2439
|
+
timeout=request_timeout_seconds,
|
|
2440
|
+
),
|
|
2441
|
+
deadline=_deadline,
|
|
2442
|
+
monotonic=_monotonic,
|
|
2420
2443
|
)
|
|
2421
2444
|
if include_headers:
|
|
2422
2445
|
status, response_headers, response_body = _parse_gh_api_output(result.stdout or "")
|
|
@@ -2435,6 +2458,12 @@ def _gh_api(
|
|
|
2435
2458
|
remaining=remaining,
|
|
2436
2459
|
)
|
|
2437
2460
|
if result.returncode != 0:
|
|
2461
|
+
ReadTokenPoolOperations.check_http_error_deadline(
|
|
2462
|
+
_deadline,
|
|
2463
|
+
_monotonic,
|
|
2464
|
+
pool_managed=_pool_managed,
|
|
2465
|
+
is_rate_limit=is_rate_limit,
|
|
2466
|
+
)
|
|
2438
2467
|
if is_rate_limit:
|
|
2439
2468
|
raise RetryableError(
|
|
2440
2469
|
diagnostics.format_error(f"GitHub API rate limited (HTTP {status or 429})"),
|
|
@@ -2499,10 +2528,17 @@ def _extract_existing_pr_url(text: str) -> str:
|
|
|
2499
2528
|
return match.group(0) if match else ""
|
|
2500
2529
|
|
|
2501
2530
|
|
|
2502
|
-
def _get_read_token_pool(
|
|
2531
|
+
def _get_read_token_pool(
|
|
2532
|
+
*,
|
|
2533
|
+
deadline: float | None = None,
|
|
2534
|
+
monotonic: Callable[[], float] | None = None,
|
|
2535
|
+
) -> ReadTokenPool | None:
|
|
2503
2536
|
"""Create and cache the validated read-token pool for this process and repository."""
|
|
2504
2537
|
global _READ_TOKEN_POOL, _READ_TOKEN_POOL_REPO
|
|
2505
2538
|
|
|
2539
|
+
ReadTokenPoolOperations.remaining_seconds(deadline, monotonic)
|
|
2540
|
+
if deadline is None:
|
|
2541
|
+
ReadTokenPoolOperations.flush_deferred_rate_limits()
|
|
2506
2542
|
repo = os.environ.get("GITHUB_REPOSITORY", "").strip()
|
|
2507
2543
|
secrets_json = os.environ.get("AGDT_GITHUB_SECRETS_JSON")
|
|
2508
2544
|
if not repo or not secrets_json:
|
|
@@ -2522,6 +2558,7 @@ def _get_read_token_pool() -> ReadTokenPool | None:
|
|
|
2522
2558
|
credential_identity=identity,
|
|
2523
2559
|
response_metadata=metadata,
|
|
2524
2560
|
read_pool=False,
|
|
2561
|
+
_pool_managed=True,
|
|
2525
2562
|
)
|
|
2526
2563
|
remaining = metadata.get("remaining")
|
|
2527
2564
|
return response, remaining if isinstance(remaining, int) else None
|
|
@@ -2545,39 +2582,29 @@ def _get_read_token_pool() -> ReadTokenPool | None:
|
|
|
2545
2582
|
return {key.partition(":")[2]: record.resume_at for key, record in records.items() if key.startswith("github:")}
|
|
2546
2583
|
|
|
2547
2584
|
def persist_rate_limit(identity: str, error: RetryableError | ProviderRateLimitError) -> float | None:
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
provider,
|
|
2555
|
-
ProviderRateLimitError(
|
|
2556
|
-
retry_after_seconds=retry_after,
|
|
2557
|
-
reset_timestamp=error.reset_timestamp,
|
|
2558
|
-
remaining=error.remaining,
|
|
2559
|
-
provider=error.provider or "github",
|
|
2560
|
-
credential_identity=identity,
|
|
2561
|
-
source=error.source,
|
|
2562
|
-
is_rate_limit=error.is_rate_limit,
|
|
2563
|
-
),
|
|
2564
|
-
)
|
|
2565
|
-
return persisted[1].resume_at if persisted is not None else None
|
|
2566
|
-
finally:
|
|
2567
|
-
_READ_POOL_BYPASS.reset(bypass)
|
|
2585
|
+
return ReadTokenPoolOperations.persist_rate_limit(
|
|
2586
|
+
provider,
|
|
2587
|
+
identity,
|
|
2588
|
+
error,
|
|
2589
|
+
bypass=_READ_POOL_BYPASS,
|
|
2590
|
+
)
|
|
2568
2591
|
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2592
|
+
with ReadTokenPoolOperations.request_context(deadline, monotonic):
|
|
2593
|
+
cooldown_identities = active_cooldown_identities()
|
|
2594
|
+
ReadTokenPoolOperations.remaining_seconds(deadline, monotonic)
|
|
2595
|
+
pool = ReadTokenPool(
|
|
2596
|
+
repo=repo,
|
|
2597
|
+
secrets_json=secrets_json,
|
|
2598
|
+
default_token=default_token,
|
|
2599
|
+
default_identity=default_identity,
|
|
2600
|
+
request=request,
|
|
2601
|
+
active_cooldowns=cooldown_identities,
|
|
2602
|
+
on_rate_limit=persist_rate_limit,
|
|
2603
|
+
)
|
|
2604
|
+
ReadTokenPoolOperations.remaining_seconds(deadline, monotonic)
|
|
2605
|
+
_READ_TOKEN_POOL = pool
|
|
2579
2606
|
_READ_TOKEN_POOL_REPO = repo
|
|
2580
|
-
return
|
|
2607
|
+
return pool
|
|
2581
2608
|
|
|
2582
2609
|
|
|
2583
2610
|
def _reset_read_token_pool() -> None:
|
|
@@ -2593,10 +2620,13 @@ def _should_use_read_token_pool(
|
|
|
2593
2620
|
credential_identity: str | None,
|
|
2594
2621
|
read_pool: bool,
|
|
2595
2622
|
target_repository: str | None = None,
|
|
2623
|
+
deadline: float | None = None,
|
|
2624
|
+
monotonic: Callable[[], float] | None = None,
|
|
2596
2625
|
) -> bool:
|
|
2597
2626
|
if not read_pool or _READ_POOL_BYPASS.get():
|
|
2598
2627
|
return False
|
|
2599
|
-
|
|
2628
|
+
ReadTokenPoolOperations.remaining_seconds(deadline, monotonic)
|
|
2629
|
+
pool = _get_read_token_pool() if deadline is None else _get_read_token_pool(deadline=deadline, monotonic=monotonic)
|
|
2600
2630
|
if pool is None:
|
|
2601
2631
|
return False
|
|
2602
2632
|
if target_repository is not None and target_repository.casefold() != pool.repo.casefold():
|
|
@@ -2648,64 +2678,23 @@ def _target_repository_from_gh_arguments(arguments: Sequence[str]) -> str | None
|
|
|
2648
2678
|
def _call_with_read_token_pool[ReadResult](
|
|
2649
2679
|
resource: str,
|
|
2650
2680
|
operation: Callable[[str | None, str], tuple[ReadResult, int | None]],
|
|
2681
|
+
*,
|
|
2682
|
+
deadline: float | None = None,
|
|
2683
|
+
monotonic: Callable[[], float] | None = None,
|
|
2651
2684
|
) -> ReadResult:
|
|
2652
2685
|
"""Run one read with ranked pool identities and fail over on rate limits."""
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
return result
|
|
2657
|
-
|
|
2658
|
-
attempted: set[str] = set()
|
|
2659
|
-
last_rate_limit: RetryableError | None = None
|
|
2660
|
-
while (credential := pool.select(resource, excluded=attempted)) is not None:
|
|
2661
|
-
attempted.add(credential.secret_name)
|
|
2662
|
-
try:
|
|
2663
|
-
result, remaining = operation(credential.token, credential.secret_name)
|
|
2664
|
-
except RetryableError as exc:
|
|
2665
|
-
if not exc.is_rate_limit:
|
|
2666
|
-
raise
|
|
2667
|
-
pool.record_remaining(credential.secret_name, resource, exc.remaining)
|
|
2668
|
-
pool.mark_rate_limited(credential.secret_name, exc)
|
|
2669
|
-
last_rate_limit = exc
|
|
2670
|
-
continue
|
|
2671
|
-
pool.record_remaining(credential.secret_name, resource, remaining)
|
|
2672
|
-
return result
|
|
2673
|
-
|
|
2674
|
-
if last_rate_limit is not None and not pool.default_token:
|
|
2675
|
-
raise ProviderRateLimitError(
|
|
2676
|
-
retry_after_seconds=last_rate_limit.retry_after,
|
|
2677
|
-
reset_timestamp=last_rate_limit.reset_timestamp,
|
|
2678
|
-
remaining=last_rate_limit.remaining,
|
|
2679
|
-
provider=last_rate_limit.provider or "github",
|
|
2680
|
-
credential_identity=last_rate_limit.credential_identity,
|
|
2681
|
-
source=last_rate_limit.source,
|
|
2682
|
-
is_rate_limit=True,
|
|
2686
|
+
with ReadTokenPoolOperations.request_context(deadline, monotonic):
|
|
2687
|
+
pool = (
|
|
2688
|
+
_get_read_token_pool() if deadline is None else _get_read_token_pool(deadline=deadline, monotonic=monotonic)
|
|
2683
2689
|
)
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
+
return ReadTokenPoolOperations.run(
|
|
2691
|
+
resource,
|
|
2692
|
+
operation,
|
|
2693
|
+
pool,
|
|
2694
|
+
_credential_identity_for_token,
|
|
2695
|
+
deadline=deadline,
|
|
2696
|
+
monotonic=monotonic,
|
|
2690
2697
|
)
|
|
2691
|
-
try:
|
|
2692
|
-
result, remaining = operation(pool.default_token or None, pool.default_identity)
|
|
2693
|
-
except RetryableError as exc:
|
|
2694
|
-
if not exc.is_rate_limit:
|
|
2695
|
-
raise
|
|
2696
|
-
pool.record_remaining(pool.default_identity, resource, exc.remaining)
|
|
2697
|
-
pool.mark_rate_limited(pool.default_identity, exc)
|
|
2698
|
-
raise ProviderRateLimitError(
|
|
2699
|
-
retry_after_seconds=exc.retry_after,
|
|
2700
|
-
reset_timestamp=exc.reset_timestamp,
|
|
2701
|
-
remaining=exc.remaining,
|
|
2702
|
-
provider=exc.provider or "github",
|
|
2703
|
-
credential_identity=pool.default_identity,
|
|
2704
|
-
source=exc.source,
|
|
2705
|
-
is_rate_limit=True,
|
|
2706
|
-
) from exc
|
|
2707
|
-
pool.record_remaining(pool.default_identity, resource, remaining)
|
|
2708
|
-
return result
|
|
2709
2698
|
|
|
2710
2699
|
|
|
2711
2700
|
def _run_readonly_gh_command(
|
|
@@ -5725,85 +5714,19 @@ class GitHubActionsProvider(GitHubPRFilesMixin, CIPlatformProvider):
|
|
|
5725
5714
|
complete = False
|
|
5726
5715
|
reasons.append(f"task-detail:{task_id_value}")
|
|
5727
5716
|
detail = task
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
top_level_session_record = False
|
|
5737
|
-
if not isinstance(session_records, list):
|
|
5738
|
-
if session_records is not None:
|
|
5739
|
-
complete = False
|
|
5740
|
-
reasons.append(f"malformed-session:{task_id_value}")
|
|
5741
|
-
top_level_session_record = bool(
|
|
5742
|
-
_task_value(detail, "session_id", "sessionId")
|
|
5743
|
-
or _agent_task_lifecycle.has_session_activity_timestamp(detail, fallback=task)
|
|
5744
|
-
)
|
|
5745
|
-
session_records = [detail] if top_level_session_record else []
|
|
5746
|
-
sessions: list[AgentTaskSessionLifecycle] = []
|
|
5747
|
-
for session in session_records:
|
|
5748
|
-
if not isinstance(session, dict):
|
|
5749
|
-
complete = False
|
|
5750
|
-
reasons.append(f"malformed-session:{task_id_value}")
|
|
5751
|
-
continue
|
|
5752
|
-
session_id_value = _task_value(session, "session_id", "sessionId") or (
|
|
5753
|
-
_task_value(task, "session_id", "sessionId")
|
|
5754
|
-
if top_level_session_record
|
|
5755
|
-
else _task_value(session, "id")
|
|
5756
|
-
)
|
|
5757
|
-
if not isinstance(session_id_value, str) or not session_id_value:
|
|
5758
|
-
complete = False
|
|
5759
|
-
reasons.append(f"missing-session-id:{task_id_value}")
|
|
5760
|
-
session_id_value = ""
|
|
5761
|
-
session_state_value = state if top_level_session_record else _task_value(session, "state", "status")
|
|
5762
|
-
started_at, session_created_at, idle_at = _agent_task_lifecycle.session_lifecycle_timestamp_values(
|
|
5763
|
-
session,
|
|
5764
|
-
include_created=not top_level_session_record,
|
|
5765
|
-
fallback=task if top_level_session_record else None,
|
|
5766
|
-
)
|
|
5767
|
-
session_head_sha = _task_value(session, "head_sha", "headSha")
|
|
5768
|
-
session_run_id = _task_value(session, "workflow_run_id", "workflowRunId", "run_id", "runId")
|
|
5769
|
-
session_state = session_state_value if isinstance(session_state_value, str) else ""
|
|
5770
|
-
head_sha = session_head_sha if isinstance(session_head_sha, str) else ""
|
|
5771
|
-
run_id = (
|
|
5772
|
-
str(session_run_id)
|
|
5773
|
-
if isinstance(session_run_id, (str, int)) and not isinstance(session_run_id, bool)
|
|
5774
|
-
else ""
|
|
5775
|
-
)
|
|
5776
|
-
session_values = (session_state, started_at, session_created_at, idle_at, head_sha, run_id)
|
|
5777
|
-
if not session_id_value and not any(session_values):
|
|
5778
|
-
continue
|
|
5779
|
-
sessions.append(
|
|
5780
|
-
AgentTaskSessionLifecycle(
|
|
5781
|
-
session_id=session_id_value,
|
|
5782
|
-
state=session_state,
|
|
5783
|
-
started_at=started_at,
|
|
5784
|
-
idle_at=idle_at,
|
|
5785
|
-
head_sha=head_sha,
|
|
5786
|
-
workflow_run_id=run_id,
|
|
5787
|
-
created_at=session_created_at,
|
|
5788
|
-
)
|
|
5789
|
-
)
|
|
5790
|
-
task_head_sha = _task_value(detail, "head_sha", "headSha")
|
|
5791
|
-
task_run_id = _task_value(detail, "workflow_run_id", "workflowRunId", "run_id", "runId")
|
|
5792
|
-
normalized.append(
|
|
5793
|
-
AgentTaskLifecycle(
|
|
5794
|
-
task_id=task_id_value,
|
|
5795
|
-
repository=repo,
|
|
5796
|
-
pr_number=task_pr if task_pr is not None else pr_number,
|
|
5797
|
-
head_branch=self._agent_task_branch(detail) or branch,
|
|
5798
|
-
state=state,
|
|
5799
|
-
created_at=created_at,
|
|
5800
|
-
sessions=tuple(sessions),
|
|
5801
|
-
head_sha=task_head_sha if isinstance(task_head_sha, str) else "",
|
|
5802
|
-
workflow_run_id=str(task_run_id)
|
|
5803
|
-
if isinstance(task_run_id, (str, int)) and not isinstance(task_run_id, bool)
|
|
5804
|
-
else "",
|
|
5805
|
-
)
|
|
5717
|
+
lifecycle, task_reasons = _agent_task_lifecycle._map_agent_task_lifecycle_detail(
|
|
5718
|
+
task,
|
|
5719
|
+
detail,
|
|
5720
|
+
task_id=task_id_value,
|
|
5721
|
+
repository=repo,
|
|
5722
|
+
pr_number=pr_number,
|
|
5723
|
+
task_pr_number=task_pr,
|
|
5724
|
+
head_branch=self._agent_task_branch(detail) or branch,
|
|
5806
5725
|
)
|
|
5726
|
+
if task_reasons:
|
|
5727
|
+
complete = False
|
|
5728
|
+
reasons.extend(task_reasons)
|
|
5729
|
+
normalized.append(lifecycle)
|
|
5807
5730
|
inventory = AgentTaskInventory(tuple(normalized), complete, tuple(dict.fromkeys(reasons)))
|
|
5808
5731
|
self._agent_task_inventory_cache[key] = inventory
|
|
5809
5732
|
return inventory
|
|
@@ -26,6 +26,10 @@ _POOL_SECRET_SUFFIX = "_SWICA_CLASSIC_REPO_WORKFLOW_PAT"
|
|
|
26
26
|
_HTTP_STATUS_RE = re.compile(r"\bHTTP\s+(\d{3})\b", re.IGNORECASE)
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
class _ReadTokenPoolDeadlineExceeded(RuntimeError):
|
|
30
|
+
"""Private signal that an inventory-scoped request exhausted its deadline."""
|
|
31
|
+
|
|
32
|
+
|
|
29
33
|
@dataclass(frozen=True)
|
|
30
34
|
class ReadCredential:
|
|
31
35
|
"""A validated read credential identified only by its Actions secret name."""
|
|
@@ -321,6 +325,8 @@ class ReadTokenPool:
|
|
|
321
325
|
expired = {identity for identity, deadline in self._cooldown_deadlines.items() if deadline <= now}
|
|
322
326
|
if not expired:
|
|
323
327
|
return
|
|
328
|
+
expired_deadlines = {identity: self._cooldown_deadlines[identity] for identity in expired}
|
|
329
|
+
expired_rate_limited = expired & self._rate_limited
|
|
324
330
|
for identity in expired:
|
|
325
331
|
self._cooldown_deadlines.pop(identity, None)
|
|
326
332
|
self._current_run_cooldowns.pop(identity, None)
|
|
@@ -332,10 +338,17 @@ class ReadTokenPool:
|
|
|
332
338
|
if self.default_identity in expired:
|
|
333
339
|
self._default_login = None
|
|
334
340
|
self._default_login_attempted = False
|
|
335
|
-
self.
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
341
|
+
expired_entries = self._secret_entries_list
|
|
342
|
+
else:
|
|
343
|
+
expired_entries = [(name, token) for name, token in self._secret_entries_list if name in expired]
|
|
344
|
+
try:
|
|
345
|
+
self._load_candidates(expired_entries)
|
|
346
|
+
except _ReadTokenPoolDeadlineExceeded:
|
|
347
|
+
for identity, deadline in expired_deadlines.items():
|
|
348
|
+
self._cooldown_deadlines.setdefault(identity, deadline)
|
|
349
|
+
self._active_cooldowns.update(expired)
|
|
350
|
+
self._rate_limited.update(expired_rate_limited)
|
|
351
|
+
raise
|
|
339
352
|
|
|
340
353
|
def _resolve_login(self, token: str, identity: str) -> str:
|
|
341
354
|
if token in self._logins_by_token:
|
|
@@ -362,6 +375,8 @@ class ReadTokenPool:
|
|
|
362
375
|
self._warn_candidate_failure(identity, stage, error)
|
|
363
376
|
|
|
364
377
|
def _warn_candidate_failure(self, identity: str, stage: str, error: Exception) -> None:
|
|
378
|
+
if isinstance(error, _ReadTokenPoolDeadlineExceeded):
|
|
379
|
+
raise error.with_traceback(error.__traceback__)
|
|
365
380
|
status = self._http_status(str(error))
|
|
366
381
|
if status in {401, 403, 404}:
|
|
367
382
|
logger.warning("Read credential %s failed %s (HTTP %d); excluding it", identity, stage, status)
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""Deadline-aware execution for pooled GitHub read requests."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from collections import deque
|
|
7
|
+
from collections.abc import Callable, Iterator
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
from contextvars import ContextVar
|
|
10
|
+
from math import floor
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from agentic_devtools.cli.ci.read_token_pool import ReadTokenPool, _ReadTokenPoolDeadlineExceeded
|
|
14
|
+
from agentic_devtools.cli.ci.retry import ProviderRateLimitError, RetryableError
|
|
15
|
+
from agentic_devtools.cli.shared.retry import (
|
|
16
|
+
DEFAULT_RATE_LIMIT_FALLBACK_DELAY,
|
|
17
|
+
DEFAULT_RATE_LIMIT_MAX_DELAY,
|
|
18
|
+
DEFAULT_RATE_LIMIT_SAFETY_MARGIN,
|
|
19
|
+
calculate_rate_limit_delay,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
_REQUEST_CONTEXT: ContextVar[tuple[float | None, Callable[[], float] | None]] = ContextVar(
|
|
23
|
+
"github_read_pool_request_context",
|
|
24
|
+
default=(None, None),
|
|
25
|
+
)
|
|
26
|
+
_DEFERRED_RATE_LIMIT_PERSISTENCE: deque[Callable[[], float | None]] = deque()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ReadTokenPoolOperations:
|
|
30
|
+
"""Execute pooled requests while optionally enforcing an inventory deadline."""
|
|
31
|
+
|
|
32
|
+
DeadlineExpired = _ReadTokenPoolDeadlineExceeded
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
@contextmanager
|
|
36
|
+
def request_context(deadline: float | None, monotonic: Callable[[], float] | None) -> Iterator[None]:
|
|
37
|
+
token = _REQUEST_CONTEXT.set((deadline, monotonic))
|
|
38
|
+
try:
|
|
39
|
+
yield
|
|
40
|
+
finally:
|
|
41
|
+
_REQUEST_CONTEXT.reset(token)
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def current_request_context() -> tuple[float | None, Callable[[], float] | None]:
|
|
45
|
+
return _REQUEST_CONTEXT.get()
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def resolve_request_context(
|
|
49
|
+
deadline: float | None,
|
|
50
|
+
monotonic: Callable[[], float] | None,
|
|
51
|
+
) -> tuple[float | None, Callable[[], float] | None]:
|
|
52
|
+
request_deadline, request_monotonic = _REQUEST_CONTEXT.get()
|
|
53
|
+
return (
|
|
54
|
+
deadline if deadline is not None else request_deadline,
|
|
55
|
+
monotonic if monotonic is not None else request_monotonic,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def persist_rate_limit(
|
|
60
|
+
provider: Any,
|
|
61
|
+
identity: str,
|
|
62
|
+
error: RetryableError | ProviderRateLimitError,
|
|
63
|
+
*,
|
|
64
|
+
bypass: ContextVar[bool],
|
|
65
|
+
observed_at: float | None = None,
|
|
66
|
+
) -> float | None:
|
|
67
|
+
deadline, _ = _REQUEST_CONTEXT.get()
|
|
68
|
+
if deadline is not None:
|
|
69
|
+
observed_at = time.time()
|
|
70
|
+
_DEFERRED_RATE_LIMIT_PERSISTENCE.append(
|
|
71
|
+
lambda: ReadTokenPoolOperations.persist_rate_limit(
|
|
72
|
+
provider,
|
|
73
|
+
identity,
|
|
74
|
+
error,
|
|
75
|
+
bypass=bypass,
|
|
76
|
+
observed_at=observed_at,
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
retry_after = error.retry_after if isinstance(error, RetryableError) else error.retry_after_seconds
|
|
80
|
+
return calculate_rate_limit_delay(
|
|
81
|
+
retry_after_seconds=retry_after,
|
|
82
|
+
reset_timestamp=error.reset_timestamp,
|
|
83
|
+
now=observed_at,
|
|
84
|
+
fallback_delay=DEFAULT_RATE_LIMIT_FALLBACK_DELAY,
|
|
85
|
+
safety_margin=DEFAULT_RATE_LIMIT_SAFETY_MARGIN,
|
|
86
|
+
max_delay=DEFAULT_RATE_LIMIT_MAX_DELAY,
|
|
87
|
+
).resume_at
|
|
88
|
+
|
|
89
|
+
from agentic_devtools.cli.ci.cooldown import persist_cooldown
|
|
90
|
+
|
|
91
|
+
retry_after = error.retry_after if isinstance(error, RetryableError) else error.retry_after_seconds
|
|
92
|
+
token = bypass.set(True)
|
|
93
|
+
try:
|
|
94
|
+
persisted = persist_cooldown(
|
|
95
|
+
provider,
|
|
96
|
+
ProviderRateLimitError(
|
|
97
|
+
retry_after_seconds=retry_after,
|
|
98
|
+
reset_timestamp=error.reset_timestamp,
|
|
99
|
+
remaining=error.remaining,
|
|
100
|
+
provider=error.provider or "github",
|
|
101
|
+
credential_identity=identity,
|
|
102
|
+
source=error.source,
|
|
103
|
+
is_rate_limit=error.is_rate_limit,
|
|
104
|
+
),
|
|
105
|
+
observed_at=observed_at,
|
|
106
|
+
)
|
|
107
|
+
return persisted[1].resume_at if persisted is not None else None
|
|
108
|
+
finally:
|
|
109
|
+
bypass.reset(token)
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def flush_deferred_rate_limits() -> None:
|
|
113
|
+
if _REQUEST_CONTEXT.get()[0] is not None:
|
|
114
|
+
return
|
|
115
|
+
while True:
|
|
116
|
+
try:
|
|
117
|
+
persist = _DEFERRED_RATE_LIMIT_PERSISTENCE.popleft()
|
|
118
|
+
except IndexError:
|
|
119
|
+
return
|
|
120
|
+
try:
|
|
121
|
+
persist()
|
|
122
|
+
except Exception:
|
|
123
|
+
_DEFERRED_RATE_LIMIT_PERSISTENCE.appendleft(persist)
|
|
124
|
+
raise
|
|
125
|
+
|
|
126
|
+
@staticmethod
|
|
127
|
+
def run_request[Result](
|
|
128
|
+
request: Callable[[], Result],
|
|
129
|
+
*,
|
|
130
|
+
deadline: float | None,
|
|
131
|
+
monotonic: Callable[[], float] | None,
|
|
132
|
+
) -> Result:
|
|
133
|
+
try:
|
|
134
|
+
return request()
|
|
135
|
+
except RetryableError:
|
|
136
|
+
if deadline is not None:
|
|
137
|
+
ReadTokenPoolOperations.request_timeout(1, deadline, monotonic)
|
|
138
|
+
raise
|
|
139
|
+
|
|
140
|
+
@staticmethod
|
|
141
|
+
def check_http_error_deadline(
|
|
142
|
+
deadline: float | None,
|
|
143
|
+
monotonic: Callable[[], float] | None,
|
|
144
|
+
*,
|
|
145
|
+
pool_managed: bool,
|
|
146
|
+
is_rate_limit: bool,
|
|
147
|
+
) -> None:
|
|
148
|
+
if pool_managed and is_rate_limit:
|
|
149
|
+
return
|
|
150
|
+
ReadTokenPoolOperations.remaining_seconds(deadline, monotonic)
|
|
151
|
+
|
|
152
|
+
@staticmethod
|
|
153
|
+
def _run_without_pool[ReadResult](
|
|
154
|
+
operation: Callable[[str | None, str], tuple[ReadResult, int | None]],
|
|
155
|
+
identity_for_token: Callable[[str | None], str],
|
|
156
|
+
deadline: float | None,
|
|
157
|
+
monotonic: Callable[[], float] | None,
|
|
158
|
+
) -> ReadResult:
|
|
159
|
+
try:
|
|
160
|
+
result, _ = operation(None, identity_for_token(None))
|
|
161
|
+
return result
|
|
162
|
+
except RetryableError:
|
|
163
|
+
ReadTokenPoolOperations.remaining_seconds(deadline, monotonic)
|
|
164
|
+
raise
|
|
165
|
+
|
|
166
|
+
@staticmethod
|
|
167
|
+
def remaining_seconds(
|
|
168
|
+
deadline: float | None,
|
|
169
|
+
monotonic: Callable[[], float] | None,
|
|
170
|
+
) -> float | None:
|
|
171
|
+
if deadline is None:
|
|
172
|
+
return None
|
|
173
|
+
remaining = deadline - (monotonic or time.monotonic)()
|
|
174
|
+
if remaining <= 0:
|
|
175
|
+
raise ReadTokenPoolOperations.DeadlineExpired("GitHub API inventory deadline expired")
|
|
176
|
+
return remaining
|
|
177
|
+
|
|
178
|
+
@staticmethod
|
|
179
|
+
def request_timeout(
|
|
180
|
+
timeout_seconds: int,
|
|
181
|
+
deadline: float | None,
|
|
182
|
+
monotonic: Callable[[], float] | None,
|
|
183
|
+
) -> int:
|
|
184
|
+
remaining = ReadTokenPoolOperations.remaining_seconds(deadline, monotonic)
|
|
185
|
+
if remaining is None:
|
|
186
|
+
return timeout_seconds
|
|
187
|
+
timeout = min(timeout_seconds, floor(remaining))
|
|
188
|
+
if timeout < 1:
|
|
189
|
+
raise ReadTokenPoolOperations.DeadlineExpired("GitHub API inventory deadline expired")
|
|
190
|
+
return timeout
|
|
191
|
+
|
|
192
|
+
@staticmethod
|
|
193
|
+
def run[ReadResult](
|
|
194
|
+
resource: str,
|
|
195
|
+
operation: Callable[[str | None, str], tuple[ReadResult, int | None]],
|
|
196
|
+
pool: ReadTokenPool | None,
|
|
197
|
+
identity_for_token: Callable[[str | None], str],
|
|
198
|
+
*,
|
|
199
|
+
deadline: float | None = None,
|
|
200
|
+
monotonic: Callable[[], float] | None = None,
|
|
201
|
+
) -> ReadResult:
|
|
202
|
+
if pool is None:
|
|
203
|
+
return ReadTokenPoolOperations._run_without_pool(operation, identity_for_token, deadline, monotonic)
|
|
204
|
+
|
|
205
|
+
attempted: set[str] = set()
|
|
206
|
+
last_rate_limit: RetryableError | None = None
|
|
207
|
+
while True:
|
|
208
|
+
ReadTokenPoolOperations.remaining_seconds(deadline, monotonic)
|
|
209
|
+
credential = pool.select(resource, excluded=attempted)
|
|
210
|
+
ReadTokenPoolOperations.remaining_seconds(deadline, monotonic)
|
|
211
|
+
if credential is None:
|
|
212
|
+
break
|
|
213
|
+
attempted.add(credential.secret_name)
|
|
214
|
+
try:
|
|
215
|
+
result, remaining = operation(credential.token, credential.secret_name)
|
|
216
|
+
except RetryableError as exc:
|
|
217
|
+
if not exc.is_rate_limit:
|
|
218
|
+
ReadTokenPoolOperations.remaining_seconds(deadline, monotonic)
|
|
219
|
+
raise
|
|
220
|
+
pool.record_remaining(credential.secret_name, resource, exc.remaining)
|
|
221
|
+
pool.mark_rate_limited(credential.secret_name, exc)
|
|
222
|
+
ReadTokenPoolOperations.remaining_seconds(deadline, monotonic)
|
|
223
|
+
last_rate_limit = exc
|
|
224
|
+
continue
|
|
225
|
+
pool.record_remaining(credential.secret_name, resource, remaining)
|
|
226
|
+
return result
|
|
227
|
+
|
|
228
|
+
if last_rate_limit is not None and not pool.default_token:
|
|
229
|
+
raise ProviderRateLimitError(
|
|
230
|
+
retry_after_seconds=last_rate_limit.retry_after,
|
|
231
|
+
reset_timestamp=last_rate_limit.reset_timestamp,
|
|
232
|
+
remaining=last_rate_limit.remaining,
|
|
233
|
+
provider=last_rate_limit.provider or "github",
|
|
234
|
+
credential_identity=last_rate_limit.credential_identity,
|
|
235
|
+
source=last_rate_limit.source,
|
|
236
|
+
is_rate_limit=True,
|
|
237
|
+
)
|
|
238
|
+
if pool.default_is_active:
|
|
239
|
+
raise ProviderRateLimitError(
|
|
240
|
+
provider="github",
|
|
241
|
+
credential_identity=pool.default_identity,
|
|
242
|
+
source="fallback",
|
|
243
|
+
is_rate_limit=True,
|
|
244
|
+
)
|
|
245
|
+
ReadTokenPoolOperations.remaining_seconds(deadline, monotonic)
|
|
246
|
+
try:
|
|
247
|
+
result, remaining = operation(pool.default_token or None, pool.default_identity)
|
|
248
|
+
except RetryableError as exc:
|
|
249
|
+
if not exc.is_rate_limit:
|
|
250
|
+
ReadTokenPoolOperations.remaining_seconds(deadline, monotonic)
|
|
251
|
+
raise
|
|
252
|
+
pool.record_remaining(pool.default_identity, resource, exc.remaining)
|
|
253
|
+
pool.mark_rate_limited(pool.default_identity, exc)
|
|
254
|
+
ReadTokenPoolOperations.remaining_seconds(deadline, monotonic)
|
|
255
|
+
raise ProviderRateLimitError(
|
|
256
|
+
retry_after_seconds=exc.retry_after,
|
|
257
|
+
reset_timestamp=exc.reset_timestamp,
|
|
258
|
+
remaining=exc.remaining,
|
|
259
|
+
provider=exc.provider or "github",
|
|
260
|
+
credential_identity=pool.default_identity,
|
|
261
|
+
source=exc.source,
|
|
262
|
+
is_rate_limit=True,
|
|
263
|
+
) from exc
|
|
264
|
+
pool.record_remaining(pool.default_identity, resource, remaining)
|
|
265
|
+
return result
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _read_cooldown_value(provider: Any, *, use_writer_token: bool) -> str | None:
|
|
269
|
+
"""Read cooldown data without nested retries when an inventory deadline is active."""
|
|
270
|
+
deadline, monotonic = _REQUEST_CONTEXT.get()
|
|
271
|
+
getter = provider.get_variable
|
|
272
|
+
if deadline is not None:
|
|
273
|
+
function = getattr(getter, "__func__", getter)
|
|
274
|
+
original = getattr(function, "__wrapped__", None)
|
|
275
|
+
if original is not None:
|
|
276
|
+
getter = original.__get__(provider, type(provider))
|
|
277
|
+
try:
|
|
278
|
+
from agentic_devtools.cli.ci.cooldown import COOLDOWN_VARIABLE
|
|
279
|
+
|
|
280
|
+
return getter(COOLDOWN_VARIABLE, use_writer_token=use_writer_token)
|
|
281
|
+
except RetryableError as exc:
|
|
282
|
+
if deadline is None:
|
|
283
|
+
raise
|
|
284
|
+
ReadTokenPoolOperations.remaining_seconds(deadline, monotonic)
|
|
285
|
+
if exc.is_rate_limit:
|
|
286
|
+
raise ProviderRateLimitError(
|
|
287
|
+
retry_after_seconds=exc.retry_after,
|
|
288
|
+
reset_timestamp=exc.reset_timestamp,
|
|
289
|
+
remaining=exc.remaining,
|
|
290
|
+
provider=exc.provider,
|
|
291
|
+
credential_identity=exc.credential_identity,
|
|
292
|
+
source=exc.source,
|
|
293
|
+
is_rate_limit=True,
|
|
294
|
+
) from exc
|
|
295
|
+
raise
|
|
296
|
+
except Exception:
|
|
297
|
+
if deadline is not None:
|
|
298
|
+
ReadTokenPoolOperations.remaining_seconds(deadline, monotonic)
|
|
299
|
+
raise
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
agentic_devtools/__init__.py,sha256=gvLzsZ6L3Abr4sPHr4OJ_rdZDD8cFzXacaV0LGs8dVc,539
|
|
2
|
-
agentic_devtools/_version.py,sha256=
|
|
2
|
+
agentic_devtools/_version.py,sha256=rn1c15V5w4TsR9WAAMV4hET9UCHsIiBfWyOomDAk1UY,524
|
|
3
3
|
agentic_devtools/agdt_gitignore.py,sha256=vVVqTjBZ_ijn6U3Lnh_JGrARLschYVN-fFal9jDk0Ak,2694
|
|
4
4
|
agentic_devtools/background_tasks.py,sha256=sFgmqOnCdCjclABJ8V__yDwBXDyi45b1Z-MVv3G2zB8,17664
|
|
5
5
|
agentic_devtools/config.py,sha256=DEVxTVZhsQbVQwO9qdB_1h19aBpjMzF6xJY_ePYLGLs,15629
|
|
@@ -232,14 +232,15 @@ agentic_devtools/cli/ci/ado_provider.py,sha256=6FpNOEF7zeeKdYwIgN56v7F7xpUZUYQLH
|
|
|
232
232
|
agentic_devtools/cli/ci/agent_assignment.py,sha256=xALWk47PMo2uW44kgMm6K7EXfQN5NCv3zyyzWScIAfE,46026
|
|
233
233
|
agentic_devtools/cli/ci/agent_task_diagnostics.py,sha256=7kVeBKqHn3h4yOXO3sTskOW1NuRj2uMAUYuhIAXRlGE,4306
|
|
234
234
|
agentic_devtools/cli/ci/agent_task_inventory.py,sha256=1JUh6v1Wcgeo7vA49D_IU5LAm1-ohT21iEllV-HYjaY,14006
|
|
235
|
-
agentic_devtools/cli/ci/agent_task_lifecycle.py,sha256=
|
|
235
|
+
agentic_devtools/cli/ci/agent_task_lifecycle.py,sha256=aIy_uTtlORd14GJ40CVI_N3TvpBhKvtGqrvyHzwm5_s,10917
|
|
236
236
|
agentic_devtools/cli/ci/approver_preflight.py,sha256=uM_LF0qrf_nMyGJT8CUvdqh0comD_AAsS9Sw_9HqBTA,5865
|
|
237
237
|
agentic_devtools/cli/ci/canonical_dispatch_identity.py,sha256=VmWs3eHBs6HU2IRV_ngMUzk24pZJVE1KBFahtTvGZPA,2799
|
|
238
238
|
agentic_devtools/cli/ci/commands.py,sha256=REVb_t8Ei4F37JmAn57FafIwzBa_u7PNhKkJHvchx5o,27282
|
|
239
239
|
agentic_devtools/cli/ci/comment_format.py,sha256=-jbcUa1AtArU95gnGyhQ1asm704j1zYAf1m1bz7WhS8,1415
|
|
240
240
|
agentic_devtools/cli/ci/conflict_repair_dispatch.py,sha256=nqdRa8DWOPx4DjLVvojUlsG9LmfFiZPtNt9eWxi_NJg,6715
|
|
241
241
|
agentic_devtools/cli/ci/conflict_repair_markers.py,sha256=Bs4WG2aSlSz7MJG0o84fGwn-Zx0uFe67OGVWXZLbpsc,1528
|
|
242
|
-
agentic_devtools/cli/ci/cooldown.py,sha256=
|
|
242
|
+
agentic_devtools/cli/ci/cooldown.py,sha256=yMmqQZB3wegKk8fO3mFw4sAPANKXrNJhHArrLfckB7g,10578
|
|
243
|
+
agentic_devtools/cli/ci/cooldown_persistence.py,sha256=YrJdiGPnAZDYZkcgMG_iV4NsNcnEwBjKs9N0iYppwTE,4129
|
|
243
244
|
agentic_devtools/cli/ci/credential_roles.py,sha256=p057fFf-g7jvaCcnoqWp4MjaPLOp3VcDfxNUaNf92nE,1649
|
|
244
245
|
agentic_devtools/cli/ci/critique_adjudicator.py,sha256=pUe19bKhr8HkwCqAscScXQHm48XLz5qBiyTtEMLARd8,18533
|
|
245
246
|
agentic_devtools/cli/ci/critique_provider.py,sha256=tP_lu4UiwTEn9ti4pHAEiMiWmKCAkByKhAZOIwgYIqs,3551
|
|
@@ -253,7 +254,7 @@ agentic_devtools/cli/ci/exceptions.py,sha256=zpKWOgnBbHHQgAQRvdRW4VFnE_qdgqgmo8r
|
|
|
253
254
|
agentic_devtools/cli/ci/finalization_state.py,sha256=jQm_cH0J09LCZfEUdZK2XQNZFhF7KhyilKcGZ4g8Wzc,8978
|
|
254
255
|
agentic_devtools/cli/ci/github_api_error.py,sha256=SvaOqag7OrnGyFMDjbl3Mr5LkjDM4bix9kXXvL5sHsQ,2382
|
|
255
256
|
agentic_devtools/cli/ci/github_pr_files.py,sha256=7QWw5adLFKMci8AtewkJfLWle3I3irF6pZ4qdhQWGU8,2697
|
|
256
|
-
agentic_devtools/cli/ci/github_provider.py,sha256=
|
|
257
|
+
agentic_devtools/cli/ci/github_provider.py,sha256=k1Ch8gTa6qliMOyTFoBxEv5KoFVuQUizNUQiKW6Yz-E,498035
|
|
257
258
|
agentic_devtools/cli/ci/guards.py,sha256=MQbtBWTpwWgNlWPJ1VJL4oZA8XciKs4J1jsN28BQBfQ,59920
|
|
258
259
|
agentic_devtools/cli/ci/initial_delivery_claims.py,sha256=Aze3eNjk0XrK8FwX87MGt0Mjz9RTDl66xdx45p659ic,9764
|
|
259
260
|
agentic_devtools/cli/ci/job_logs.py,sha256=YUiW487uO0usSwWD-mpWrs6q7IedQuYyhhBJIc0EAfk,12772
|
|
@@ -274,7 +275,8 @@ agentic_devtools/cli/ci/provider_repository.py,sha256=0FqIbTExEFN5Xrj7eq9p6OmUke
|
|
|
274
275
|
agentic_devtools/cli/ci/provider_reviews.py,sha256=z63kEFkBXs8kUJW-OE8tzYtEmUEtRIZPzmvFWh-KQKs,6673
|
|
275
276
|
agentic_devtools/cli/ci/provider_scheduler.py,sha256=SgoQaA2mwF_EmD_OpsJUGkr2-bSkuq34uhIuZ-B_dUM,7086
|
|
276
277
|
agentic_devtools/cli/ci/push_attribution.py,sha256=BcG5p1OAbdE_LrX8w7dKNHsXI2RchP4z5T6ObxOHfRI,4276
|
|
277
|
-
agentic_devtools/cli/ci/read_token_pool.py,sha256=
|
|
278
|
+
agentic_devtools/cli/ci/read_token_pool.py,sha256=aHhPsAXaCHIThmNuzjX6H0KXYCbPubO7q88smfhXcBM,18574
|
|
279
|
+
agentic_devtools/cli/ci/read_token_pool_execution.py,sha256=i6LP6LLT12V6vOY91js2dFgpnDcR08yhSU4asANjw10,11575
|
|
278
280
|
agentic_devtools/cli/ci/repair_accounting.py,sha256=BHMtgKGVmcziFyxbNAmRCNJzk1MDnBVr9a0Gx7CLAPM,18495
|
|
279
281
|
agentic_devtools/cli/ci/repair_prompt.py,sha256=KnNyDRuTmDyDUP7M47FDPZeREmZMFlri8Yntkb38pEU,3241
|
|
280
282
|
agentic_devtools/cli/ci/retry.py,sha256=aCYvnKEoqfP6lAM3Bh744uARZtl5XI1EuVsJmOBvnEU,8651
|
|
@@ -1318,8 +1320,8 @@ agentic_devtools/_bundled_skills/skills/speckit-plan/SKILL.md,sha256=qYLpzgLCIZ6
|
|
|
1318
1320
|
agentic_devtools/_bundled_skills/skills/speckit-specify/SKILL.md,sha256=HbHRbdphMqbtNw6fL6NcYCj6sSFmUyk2S4U0bZ9p_l4,13681
|
|
1319
1321
|
agentic_devtools/_bundled_skills/skills/write-github-commit-message/SKILL.md,sha256=Ep1lR8NDN117Iytj172VZgqazFiLpJ56Z7RXgaTnxR8,2626
|
|
1320
1322
|
agentic_devtools/_bundled_skills/skills/write-github-commit-message/commit-types.md,sha256=bTy_t68ZD6R87rFIzq9wybTu8Wudoahb7nB9IWC4bFo,1839
|
|
1321
|
-
agentic_devtools-0.2.
|
|
1322
|
-
agentic_devtools-0.2.
|
|
1323
|
-
agentic_devtools-0.2.
|
|
1324
|
-
agentic_devtools-0.2.
|
|
1325
|
-
agentic_devtools-0.2.
|
|
1323
|
+
agentic_devtools-0.2.615.dist-info/METADATA,sha256=19HQlADjD0-el0pLulpWfRQs0Vk7XODAxOuCo9b_qZg,35589
|
|
1324
|
+
agentic_devtools-0.2.615.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
1325
|
+
agentic_devtools-0.2.615.dist-info/entry_points.txt,sha256=kL35_Y5OHI6XpPneeBTcX82ORjjfx2VIK6XfmKHgNm4,13630
|
|
1326
|
+
agentic_devtools-0.2.615.dist-info/licenses/LICENSE,sha256=yBEDdICksxhBYLWoERKp9MTqwGnUF6Ryj9BTLwXTc6k,1082
|
|
1327
|
+
agentic_devtools-0.2.615.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|