
    OjG)                    v   d Z ddlmZ ddlZddlZddlmZmZmZ ddl	m
Z
  G d de      Z G d d	      Z ej                         Zdd
ZddZddZej&                  d        ZddZ G d d      Zej&                  ddd       ZddZd dZ G d de      Zej&                  ddd	 d!d       Zd"dZg dZy)#us  GROUNDING-V2 §8.5 — **조사 호출**의 누계 상한.

★★`ImageCallBudget` 과 **같은 꼴의 다른 owner** 다 (Codex). counter 를 나눠
쓰면 서로의 상한을 갉아먹는다 — 참조 이미지를 많이 구운 주행이 그날의 조사
예산을 먹어 치우는 식이다. 그래서 **세는 통을 따로** 둔다.

★그렇지만 **정지 기구는 새로 안 만든다.** `install_stop_check` ·
`get_current_stop_check` · `bind_current_stop_check` 는 이미지 전용이 아니라
일반 기구라 그대로 가져다 쓴다. 두 벌로 만들면 한쪽만 고쳐진다.

★★세는 것은 **물리 전송**이다 — 논리 호출이 아니다. 키 슬롯 failover 는
`FailoverOpenAIClient._invoke` 안에서 도므로 논리 하나가 물리 둘이 될 수 있다.
「몇 번 나갔나」를 막으려면 **나가는 자리**에서 세야 한다.
    )annotationsN)AnyDictOptional)get_current_stop_checkc                  $     e Zd ZdZd fdZ xZS )ResearchCallBudgetExceededu.  상한에 닿았다. ★**네트워크에 닿기 전에** 오른다.

    호출부는 이것을 잡아 그 대상을 `time_capped` ·
    `limit_kind=transmission_budget` 으로 남긴다 — 「조사할 필요가 없었다」가
    아니라 「이 주행의 상한 때문에 못 샀다」다.
    c                   t        |      | _        t        |      | _        t        |      | _        t
        |   d| j                   d| j                   d| j                         y )Nu'   research_call_budget 초과 — 상한=u    · 쓴 것=u    · 거절한 자리=)intcapusedstrsourcesuper__init__)selfr   r   r   	__class__s       S/Users/manta/Documents/Projects/TheRoad-I1/backend/app/core/research_call_budget.pyr   z#ResearchCallBudgetExceeded.__init__!   sZ    s8I	&k5dhhZ|DII; W  $0	1    )r   r   r   r   r   r   returnNone)__name__
__module____qualname____doc__r   __classcell__)r   s   @r   r	   r	      s    1 1r   r	   c                  ,    e Zd ZdZdZddZddZd	dZy)
ResearchCallBudgetuF   조사용 **물리 전송** counter. 상한을 넘으면 거절한다.)_cap_used_denied_lockc                    t        |      }|dk  rt        d|       || _        d| _        d| _        t        j                         | _        y )Nr   u$   상한은 0 이상이어야 한다: )r   
ValueErrorr   r    r!   	threadingRLockr"   )r   r   cap_ints      r   r   zResearchCallBudget.__init__/   sH    c(Q;CG9MNN	
__&
r   c                  | j                   5  | j                  | j                  k\  r7| xj                  dz  c_        t	        | j                  | j                  |      | xj                  dz  c_        ddd       y# 1 sw Y   yxY w)uE   전송 **직전에** 한 자리를 잡는다. 없으면 거절한다.   )r   r   r   N)r"   r    r   r!   r	   )r   r   s     r   reservezResearchCallBudget.reserve8   s\    ZZzzTYY&!0		

6C CJJ!OJ ZZs   A&A<<Bc                    | j                   5  | j                  | j                  | j                  | j                  | j                  z
  dcd d d        S # 1 sw Y   y xY w)N)r   r   denied	remaining)r"   r   r    r!   r   s    r   snapshotzResearchCallBudget.snapshotA   s<    ZZ99djj"ll!%TZZ!79 ZZs   <AAN)r   r   r   r   r   r   r   r   )r   zDict[str, int])r   r   r   r   	__slots__r   r*   r/    r   r   r   r   *   s    P5I'9r   r   c                    | t         _        y N_localbudget)r7   s    r   install_budgetr8   M   s	    FMr   c                     d t         _        y r4   r5   r2   r   r   uninstall_budgetr:   Q   s	    FMr   c                 $    t        t        dd       S )Nr7   getattrr6   r2   r   r   get_current_budgetr>   U   s    68T**r   c            
   #     K   t        t        dd      dz   t        _        	 d t        dt        t        dd      dz
        t        _        y# t        dt        t        dd      dz
        t        _        w xY ww)u  이 블록 **안의** provider 전송만 조사 호출로 센다.

    ★★★왜 필요한가 (Codex ②). `reserve` 를 `FailoverOpenAIClient._invoke` 라는
    **공용 경계**에 두면, 같은 스레드에서 나가는 **다른** OpenAI 호출(이미지·
    요약 등)까지 조사 예산을 먹는다. 그러면 상한이 재는 것이 「조사 전송」이
    아니라 「그 스레드의 모든 전송」이 된다.

    그래서 조사 호출을 내는 자리에서만 **팔을 든다**. 밖에서는 no-op 이라
    다른 호출부의 동작이 **한 글자도 안 바뀐다**.
    ★중첩을 세는 것은 재진입(retry 안의 retry) 때문이다.
    armedr   r)   N)r=   r6   r@   maxr2   r   r   research_calls_armedrB   Y   s]      67A.2FL?1gfgq9A=>s1gfgq9A=>s   A9A (A9)A66A9c                 *    t        t        dd      dkD  S )Nr@   r   r<   r2   r   r   is_armedrD   m   s    67A&**r   c                      e Zd ZdZddZy)OutboundAttemptu  한 **논리 시도**(journal 의 send 하나) 동안 이 스레드에서 팔을 든 채 나간 전송 수.

    ★Codex BLOCK (PR #82 재리뷰 2026-09-03): 중앙 경로의 send 하나는 저작→검색→받기→판정 **여러 전송**이라, 우리 문이
    두 번째 전송 앞에서 세웠어도 첫 전송은 이미 나갔다. 「안 나갔다」는 이 계수가 **0** 일 때만 증명된다. 팔을 안 든 채
    돈 시도는 셀 수 없으므로(`armed=False`) 증명이 없다 → uncertain.
    c                0    d| _         t               | _        y )Nr   )countrD   r@   r.   s    r   r   zOutboundAttempt.__init__y   s    
Z
r   Nr   r   )r   r   r   r   r   r2   r   r   rF   rF   q   s     r   rF   c              #     K   | xs
 t               } t        t        dd      }|g }|t        _        |j	                  |        	 |  |j                          y# |j                          w xY ww)uq   이 블록의 전송을 `att.count` 에 센다 — 스레드 지역 stack 이라 worker 는 제 시도만 센다.attemptsN)rF   r=   r6   rK   appendpop)attstacks     r   outbound_attemptrP   ~   s\      
""CFJ-E}	LL					s   A A-A A-A**A-c                 b    t        t        dd       xs dD ]  } | xj                  dz  c_         y )NrK   r2   r)   )r=   r6   rH   )rN   s    r   _count_outboundrR      s*    vz406B6		Q	 7r   c                    t               syt               }| |        t               }||j                  |        t	                y)u5  조사 전송 **직전**. ★`research_calls_armed()` 밖에서는 **아무 일도 안 한다**.

    ★★그래서 이미지·다른 스텝의 호출부가 영향을 안 받는다 — 같은 공용 경계를
    지나가지만 팔을 안 들었으면 그냥 지나간다.

    ★정지 확인이 **예산보다 먼저**다 (Codex ①). 이미 마감된 주행은 예산도
    슬롯도 안 먹고 **provider 를 0회** 부른다 — 예산이 안 깔린 조사 경로에서도
    정지는 들려야 하므로, `budget is None` 보다 **앞에** 둔다.
    N)r   )rD   r   r>   r*   rR   )r   
stop_checkr7   s      r   reserve_current_research_callrU      sA     :')J!Ff%r   c                      e Zd ZdZdZy)RunDeadlineExceededu   주행 전체 벽시계가 지났다. ★`limit_kind=run_deadline` 로 남는다.

    ★표식(`is_run_deadline`)을 달고 다닌다 — 정지 표는 호출부가 아무 예외나
    올릴 수 있어서, 타입 하나로 못 가른다.
    TN)r   r   r   r   is_run_deadliner2   r   r   rW   rW      s     Or   rW   )deadline_secondsnowc              #    K   ddl }ddlm}m}m} |xs |j
                          t               } |       }t        |       }	t        |	       |ffd	}
 ||
       	 |	 |t                nt        |       | |        y ||       yy# |t                nt        |       | |        w  ||       w w xY ww)u  한 **주행**의 조사 상한을 연다 — 전송 수 ②와 벽시계 ③.

    ★★③ 을 「새 대상을 안 시작한다」는 loop 조건으로만 두면 **팬아웃 안에서
    이미 제출된 것들**이 계속 나간다. 정지 표로 깔면 `reserve` 자리에서
    막힌다 — 그게 **네트워크 직전**이다 (GROUNDING-V2 §8.5).

    ★들어올 때 걸려 있던 예산·정지 표는 **되돌린다**. 안 그러면 이 주행이
    끝난 뒤에도 남의 호출이 이 상한을 본다.
    r   N)r   install_stop_checkuninstall_stop_check)r   c                b    |  |                 z
  t              k\  rt        d d      y )Nu   주행 마감 uO   초가 지났다 — 남은 대상은 `time_capped`(run_deadline) 로 남는다)floatrW   )_prevclockrY   starteds    r   _checkz"research_run_scope.<locals>._check   sL     w E*:$;;)$%5$6 7@ @A A <r   )
timeapp.core.image_call_budgetr   r\   r]   	monotonicr>   r   r8   r:   )r   rY   rZ   _timer   r\   r]   prev_budget
prev_checkr7   rc   ra   rb   s    `         @@r   research_run_scoperj      s      B B "5??EgG$&K')JC(F6## 	A 	6"/;''!$&":.	 (	 ;''!$&":.	 (s   A!C'B +.C/CCc                    ddl }ddlm} t               t	                ||       |j                  |       dfd       }|S )u   팬아웃으로 **예산과 정지 표를 같이** 실어 보낸다.

    ★`install_budget` 은 스레드 지역이라, 그냥 넘기면 worker 는 예산을 못 본다
    — 그러면 상한이 그 자리에서 통째로 no-op 이 된다.
    r   N)bind_current_stop_checkc                 2   t               }t               	 r4t               5   | i |cd d d        |t                S t        |       S  | i ||t                S t        |       S # 1 sw Y   )xY w# |t                w t        |       w xY wr4   )r>   r8   rB   r:   )argskwargsprevr@   r7   inners      r   _runz*bind_current_research_budget.<locals>._run   s    !#6"	%)+ $1&1 ,+ | "t$ $)&) | "t$ ,+ | "t$s'   A< A0	A< A< 0A95A< <B)rn   r   ro   r   r   r   )	functoolsre   rl   r>   rD   wraps)fnrs   rl   rr   r@   r7   rq   s       @@@r   bind_current_research_budgetrv      sF     B!FJE#B'E__R% %" Kr   )r   rD   rB   rj   rW   r	   rv   r>   r8   rU   r:   )r7   r   r   r   rI   )r   zOptional[ResearchCallBudget])r   boolr4   )rN   zOptional['OutboundAttempt']r0   )r   r   rY   zOptional[float]rZ   r   )ru   r   r   r   )r   
__future__r   
contextlibr%   typingr   r   r   re   r   RuntimeErrorr	   r   localr6   r8   r:   r>   contextmanagerrB   rD   rF   rP   rR   rU   rW   rj   rv   __all__r2   r   r   <module>r      s    #   & & >1 1"9 9@ 
	+ ? ?&+
  
   
.,  HL"&-/-/ -/` Fr   