22
33import fnmatch
44import math
5+ import time
56import uuid
6- from dataclasses import dataclass , field
7+ from dataclasses import dataclass , field , replace
78from typing import Dict , List , Mapping , Sequence
89
910from argus .broker .budgets import BudgetTracker , PROVIDER_TIERS
1011from argus .broker .health import HealthExecutionClaim , HealthTracker
1112from 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+ )
1222from argus .broker .reachability import ReachabilityClaim , ReachabilityMatrix
13- from argus .config import EgressNode
23+ from argus .config import EgressNode , NodeConfig
1424from argus .logging import get_logger
1525from argus .models import ProviderName , ProviderTrace , SearchQuery , SearchResult
1626from 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
90108class 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