Skip to content

Commit 774d7d7

Browse files
authored
Normalize provider evidence batches (#87)
* feat: freeze accepted operation contracts * fix: harden accepted operation contracts * feat: resolve deterministic retrieval plans * fix: require validated retrieval plans * fix: preserve invalid retrieval outcomes * feat: normalize provider evidence batches * fix: harden provider evidence contracts * fix: execute provider evidence at adapter boundary * fix: close provider evidence cancellation gaps * fix: enforce provider failure evidence bounds * fix: align provider evidence applicability * fix: correct provider status authorities * fix: bound Wolfram quota evidence * fix: model complete provider statuses * fix: preserve provider status semantics * fix: retain all HTTP response statuses --------- Co-authored-by: Khamel83 <khamel83@users.noreply.github.com>
1 parent ef6718d commit 774d7d7

175 files changed

Lines changed: 9670 additions & 798 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

argus/broker/__init__.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1-
from argus.broker.router import SearchBroker, create_broker
1+
"""Broker public API with lazy exports to keep provider imports acyclic."""
22

33
__all__ = ["SearchBroker", "create_broker"]
4+
5+
6+
def __getattr__(name: str):
7+
if name in __all__:
8+
from argus.broker.router import SearchBroker, create_broker
9+
10+
return {"SearchBroker": SearchBroker, "create_broker": create_broker}[name]
11+
raise AttributeError(name)

argus/broker/execution.py

Lines changed: 107 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,25 @@
22

33
import fnmatch
44
import math
5+
import time
56
import uuid
6-
from dataclasses import dataclass, field
7+
from dataclasses import dataclass, field, replace
78
from typing import Dict, List, Mapping, Sequence
89

910
from argus.broker.budgets import BudgetTracker, PROVIDER_TIERS
1011
from argus.broker.health import HealthExecutionClaim, HealthTracker
1112
from argus.broker.planning import RetrievalPlan
13+
from argus.broker.provider_evidence import (
14+
LegacyProviderBatchAdapter,
15+
ProviderSearchBatch,
16+
EgressType,
17+
attempt_timeout_seconds,
18+
failure_batch,
19+
run_with_attempt_deadline,
20+
with_trusted_provenance,
21+
)
1222
from argus.broker.reachability import ReachabilityClaim, ReachabilityMatrix
13-
from argus.config import EgressNode
23+
from argus.config import EgressNode, NodeConfig
1424
from argus.logging import get_logger
1525
from argus.models import ProviderName, ProviderTrace, SearchQuery, SearchResult
1626
from argus.providers.base import BaseProvider
@@ -85,6 +95,14 @@ class ProviderExecutionOutcome:
8595
provider_results: Dict[str, List[SearchResult]]
8696
live_providers_used: int
8797
budget_pace_warnings: List[str] = field(default_factory=list)
98+
provider_batches: Dict[str, ProviderSearchBatch] = field(default_factory=dict)
99+
100+
101+
@dataclass(frozen=True)
102+
class ProviderInvocationOutcome:
103+
batch: ProviderSearchBatch
104+
uncertain_charge: bool
105+
compatibility_trace: ProviderTrace
88106

89107

90108
class ProviderExecutor:
@@ -98,6 +116,8 @@ def __init__(
98116
egress_nodes: dict[str, EgressNode] | None = None,
99117
caller_tier_caps: Mapping[str, int] | None = None,
100118
spend_repository=None,
119+
monotonic=time.monotonic,
120+
node_config: NodeConfig | None = None,
101121
):
102122
self._providers = providers
103123
self._health = health_tracker
@@ -106,6 +126,8 @@ def __init__(
106126
self._egress_nodes = egress_nodes or {}
107127
self._caller_tier_caps = dict(caller_tier_caps or {})
108128
self._spend = spend_repository
129+
self._monotonic = monotonic
130+
self._node_config = node_config or NodeConfig()
109131

110132
def _should_query_paid(self, provider: ProviderName, tier: int) -> tuple[bool, str]:
111133
"""Decide whether to query a paid provider based on budget pace.
@@ -141,6 +163,7 @@ async def execute(
141163
raise ValueError("validated operation deadlines are required")
142164
traces: List[ProviderTrace] = []
143165
provider_results: Dict[str, List[SearchResult]] = {}
166+
provider_batches: Dict[str, ProviderSearchBatch] = {}
144167
live_providers_used = 0
145168
pace_warnings: List[str] = []
146169
attempt_scope = str(query.metadata.get("attempt_scope") or uuid.uuid4().hex)
@@ -264,7 +287,10 @@ async def execute(
264287
continue
265288
try:
266289
try:
267-
results, trace = await remote.search(query)
290+
legacy = await remote.search(query)
291+
batch = LegacyProviderBatchAdapter.from_legacy(legacy)
292+
results, trace = batch.results, batch.trace
293+
provider_batches[pname.value] = batch
268294
uncertain = not self._trace_charge_known(pname, trace)
269295
except Exception as exc:
270296
# Network failures can occur after the provider accepted
@@ -273,7 +299,9 @@ async def execute(
273299
trace = ProviderTrace(
274300
provider=pname,
275301
status="error",
276-
error=str(exc),
302+
error=(
303+
f"remote provider request failed ({type(exc).__name__})"
304+
),
277305
egress=best_egress,
278306
)
279307
uncertain = True
@@ -395,9 +423,17 @@ async def execute(
395423
continue
396424

397425
try:
398-
results, trace, uncertain = await self._execute_provider(
399-
query, provider, pname
426+
invocation = await self._execute_provider(
427+
query,
428+
provider,
429+
pname,
430+
plan=plan,
431+
provider_phase_deadline=provider_phase_deadline,
400432
)
433+
batch = invocation.batch
434+
results, trace = batch.results, invocation.compatibility_trace
435+
provider_batches[pname.value] = batch
436+
uncertain = invocation.uncertain_charge
401437
finally:
402438
self._release_invocation_claims(claims)
403439
if not uncertain or tier <= 0:
@@ -421,6 +457,7 @@ async def execute(
421457
provider_results=provider_results,
422458
live_providers_used=live_providers_used,
423459
budget_pace_warnings=pace_warnings,
460+
provider_batches=provider_batches,
424461
)
425462

426463
def _claim_invocation(
@@ -444,9 +481,62 @@ async def _execute_provider(
444481
query: SearchQuery,
445482
provider: BaseProvider,
446483
provider_name: ProviderName,
447-
) -> tuple[List[SearchResult], ProviderTrace, bool]:
484+
*,
485+
plan: RetrievalPlan | None = None,
486+
provider_phase_deadline: float | None = None,
487+
) -> ProviderInvocationOutcome:
488+
metadata = dict(query.metadata)
489+
if plan is not None:
490+
metadata["_retrieval_plan"] = plan
491+
metadata["_freshness_window"] = plan.freshness
492+
if provider_phase_deadline is not None:
493+
metadata["_provider_phase_deadline"] = provider_phase_deadline
494+
metadata["_monotonic"] = self._monotonic
495+
adapter_query = replace(
496+
query,
497+
metadata=metadata,
498+
)
448499
try:
449-
results, trace = await provider.search(query)
500+
if provider_phase_deadline is None:
501+
raw_output = await provider.search(adapter_query)
502+
else:
503+
configured_timeout = float(
504+
getattr(getattr(provider, "_config", None), "timeout_seconds", 15)
505+
)
506+
# Refuse before constructing the provider coroutine, then enforce
507+
# the same absolute deadline around the real adapter execution.
508+
attempt_timeout_seconds(
509+
configured_timeout=configured_timeout,
510+
provider_phase_deadline=provider_phase_deadline,
511+
monotonic=self._monotonic,
512+
)
513+
raw_output = await run_with_attempt_deadline(
514+
provider.search(adapter_query),
515+
configured_timeout=configured_timeout,
516+
provider_phase_deadline=provider_phase_deadline,
517+
monotonic=self._monotonic,
518+
)
519+
if isinstance(raw_output, ProviderSearchBatch):
520+
batch = raw_output
521+
elif (
522+
isinstance(raw_output, tuple)
523+
and len(raw_output) == 2
524+
and isinstance(raw_output[1], ProviderTrace)
525+
):
526+
batch = LegacyProviderBatchAdapter.from_legacy(raw_output)
527+
else:
528+
raise TypeError("provider returned an invalid batch contract")
529+
try:
530+
trusted_egress = EgressType(self._node_config.egress_type)
531+
except ValueError:
532+
trusted_egress = EgressType.UNKNOWN
533+
batch = with_trusted_provenance(
534+
batch,
535+
egress=trusted_egress,
536+
machine=self._node_config.machine_name or None,
537+
)
538+
results = batch.results
539+
trace = batch.trace
450540
if trace.status == "success":
451541
self._health.record_success(provider_name)
452542
self._reachability.update_probe(
@@ -478,16 +568,6 @@ async def _execute_provider(
478568
"reservation left uncertain"
479569
)
480570

481-
# Inject provenance metadata if not already set by the provider
482-
from argus.config import get_config
483-
484-
cfg = get_config()
485-
for r in results:
486-
if "egress" not in r.metadata:
487-
r.metadata["egress"] = cfg.node.egress_type
488-
if "machine" not in r.metadata and cfg.node.machine_name:
489-
r.metadata["machine"] = cfg.node.machine_name
490-
491571
elif trace.status == "error":
492572
self._health.record_failure(provider_name)
493573
self._reachability.update_probe(
@@ -497,12 +577,16 @@ async def _execute_provider(
497577
latency_ms=trace.latency_ms,
498578
source="provider_execution",
499579
)
500-
return results, trace, not self._trace_charge_known(provider_name, trace)
501-
except Exception as exc:
580+
return ProviderInvocationOutcome(
581+
batch=batch,
582+
uncertain_charge=not self._trace_charge_known(provider_name, trace),
583+
compatibility_trace=trace,
584+
)
585+
except Exception as error:
502586
logger.warning(
503587
"Provider %s raised unhandled: %s",
504588
provider_name,
505-
type(exc).__name__,
589+
type(error).__name__,
506590
)
507591
self._health.record_failure(provider_name)
508592
self._reachability.update_probe(
@@ -512,11 +596,8 @@ async def _execute_provider(
512596
latency_ms=0,
513597
source="provider_execution",
514598
)
515-
return (
516-
[],
517-
ProviderTrace(provider=provider_name, status="error", error=str(exc)),
518-
True,
519-
)
599+
failure = failure_batch(provider_name, error)
600+
return ProviderInvocationOutcome(failure, True, failure.trace)
520601

521602
def _reserve_paid_attempt(
522603
self,

0 commit comments

Comments
 (0)