Skip to content

[Feat] 크레딧 그래프 배선#114

Merged
chowon442 merged 5 commits into
devfrom
feat/85-credit-graph-wiring
Jun 4, 2026
Merged

[Feat] 크레딧 그래프 배선#114
chowon442 merged 5 commits into
devfrom
feat/85-credit-graph-wiring

Conversation

@chowon442

@chowon442 chowon442 commented Jun 1, 2026

Copy link
Copy Markdown
Member

📌 관련 이슈

🏷️ PR 타입

  • ✨ 기능 추가 (Feature)
  • 🐛 버그 수정 (Bug Fix)
  • ♻️ 리팩토링 (Refactoring)
  • 📝 문서 수정 (Documentation)
  • 🎨 스타일 변경 (Style)
  • ✅ 테스트 추가 (Test)

📝 작업 내용

  • Planner가 계획 전체 예상 비용을 단일 크레딧 원장에 hold하도록 배선
    • 가용 크레딧 부족 시 credit_exhausted SSE 오류로 실행 전 all-or-nothing 거절
    • hold_id를 state에 저장해 이후 정산 노드가 같은 hold row를 사용
  • 사용자 영상 재시도 요청의 retry_source_job_id preflight를 hold 전에 수행
    • invalid retry와 이미 사용된 retry는 hold 생성 없이 사용자 오류로 중단
  • CreditSettler가 그래프 종료 시 solve/pdf 동기 작업 비용을 같은 원장에 finalize_hold로 capture
    • video_node 비용은 후속 Task 2.06 범위라 동기 정산에서 제외
    • 초과 예상분은 credit_settled.reserved/refunded payload에 반영
  • 그래프 노드가 앱 lifespan에서 생성한 credit/video runtime client를 contextvar로 사용할 수 있도록 런타임 의존성 배선 추가
  • 비용표 기반 hold 추정 helper와 graph wiring 테스트 추가
  • Task 2.03 문서 상태와 체크리스트를 완료로 갱신

📸 스크린샷

uv run ruff format --check .  →  139 files already formatted
uv run ruff check .           →  All checks passed!
uv run pytest                 →  230 passed, 21 skipped, 2 warnings
pre-commit                    →  ruff/check/format/yaml/toml/json/end-of-file/whitespace/merge-conflict 통과

✅ 체크리스트

  • 코드 리뷰를 받을 준비가 완료되었습니다
  • 테스트를 작성하고 모두 통과했습니다
  • 문서를 업데이트했습니다 (필요한 경우)
  • 코드 스타일 가이드를 준수했습니다
  • 셀프 리뷰를 완료했습니다

📎 기타 참고사항

  • 영상 10cr capture와 비동기 video job refund는 후속 Task 2.06/2.07 범위라 이번 PR에는 포함하지 않았습니다.
  • 실제 Postgres가 필요한 @pytest.mark.postgres 테스트는 DATABASE_URL 미설정으로 기존 정책대로 skip되었습니다.
  • pytest warning 2건은 기존 경고입니다: LangGraph serializer pending deprecation, tests/pdf_node/test_pdf_node.py::test_pdf_node_basic return-not-none.

Summary by CodeRabbit

  • 새로운 기능

    • 실행 전 플래닝 단계에서 크레딧 선예약(hold) 기능이 도입되어 계획 실행 전 필요한 금액을 확보합니다.
    • 비디오 재시도 요청에 대한 사전 검증이 추가되어 잘못된 재시도를 미리 차단합니다.
  • 개선 사항

    • 그래프 실행 중 오류 발생 시 보류된 크레딧을 자동으로 해제/정리합니다.
    • 크레딧 비용 산정 및 표시가 더 정확해져 사용자가 예상 비용을 확인하기 쉬워졌습니다.
  • 테스트

    • 크레딧 보류·정산·해제 및 재시도 전검증 흐름을 검증하는 통합/단위 테스트가 추가되었습니다.

@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@chowon442, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 52 minutes and 30 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fcd9704b-c3dd-42a3-ab28-2f0189602251

📥 Commits

Reviewing files that changed from the base of the PR and between 3974bf8 and f5f0296.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • pyproject.toml
  • src/proovy_agent/app/api/_runner.py
  • src/proovy_agent/app/api/stream.py
  • src/proovy_agent/app/api/v1/solve.py
  • src/proovy_agent/app/main.py
  • src/proovy_agent/graph/nodes/ocr_node/command_parser.py
  • tests/app/test_health.py
  • tests/app/test_solve_endpoint.py
  • tests/app/test_stream_endpoint.py
  • tests/ocr_node/test_command_parser.py
📝 Walkthrough

Walkthrough

이 PR은 Planner의 실행 전 plan 홀드를 예약하고 런타임에서 활성 홀드를 추적해 예외 시 정리하며, CreditSettler가 동기 비용을 finalize 하도록 Postgres 기반 클라이언트·비용 산정·플래너 preflight·엔드포인트 수명주기·테스트를 통합합니다.

Changes

크레딧 그래프 배선: 홀드/정산 통합

Layer / File(s) Summary
크레딧 레저 클라이언트 서비스
pyproject.toml, src/proovy_agent/features/credits/service.py, src/proovy_agent/features/credits/__init__.py
psycopg-pool 의존성 추가. CreditLedgerClient 프로토콜과 PostgresCreditLedgerClient 구현, create_credit_ledger_client 팩토리 및 open_credit_ledger_client 앱 스코프 컨텍스트 매니저를 제공. __all__에 클라이언트/팩토리 심볼 노출 업데이트.
크레딧 비용 산정 및 상수
src/proovy_agent/graph/credit_pricing.py, src/proovy_agent/graph/agents/core_solver/agent.py
모델별 단가 및 고정 비용, CORE_SOLVER_MAX_ITERATIONS 등 상수 추가. estimate_plan_hold_amount, synchronous_credit_amount, format_credit_amount, _estimate_solve_cost 등 비용 추정/합산 유틸 제공. core_solver가 MODEL_CREDIT_COST/CORE_SOLVER_MAX_ITERATIONS를 사용하도록 변경.
플래너 hold 생성 및 preflight 검증
src/proovy_agent/graph/nodes/planner.py
video_retry 재시도 preflight(비디오 잡 존재/스레드/상태) 추가. 기존 stale hold 해제, estimate_plan_hold_amount 기반 hold 생성(ledger.hold) 및 크레딧 관련 예외 처리(에러 SSE + PlannerPreflightError). 반환 dict에 hold_idcredit_log 반영.
크레딧 세틀러 hold finalize
src/proovy_agent/graph/nodes/credit_settler.py
동기 경로 비용을 synchronous_credit_amount로 계산. hold_id가 있으면 finalize_hold 호출로 reserved/refunded를 산출하여 CreditSettledPayload에 조건부 포함. 시스템 메시지 표시 금액을 sync_cost로 업데이트.
그래프 런타임 홀드 추적
src/proovy_agent/graph/runtime.py
ActiveHoldTrackingCreditLedgerClient로 마지막 생성된 active_hold_id를 추적하고 필요 시 release_hold를 호출하는 래퍼 제공. track_active_credit_hold() 헬퍼와 current_credit_ledger_client/current_video_job_client ContextVar 정의.
엔드포인트 및 라이프사이클 통합
src/proovy_agent/app/main.py, src/proovy_agent/app/api/v1/solve.py
lifespanopen_credit_ledger_client 컨텍스트 추가하여 app.state.credit_ledger_client 설정. solve_endpointRequest에서 클라이언트를 읽어 current_* ContextVar와 current_emitter를 설정. 그래프 예외(및 Cancelled) 경로에서 _release_active_credit_hold 호출 및 emitter.close()와 ContextVar 리셋, in-flight solve task 취소/대기 로직 추가.
통합 및 단위 테스트
tests/app/test_health.py, tests/app/test_solve_endpoint.py, tests/graph/test_credit_graph_wiring.py
lifespan에서 credit client 오픈/클로즈 및 cancel 호출 순서 검증. solve SSE에서 그래프 예외 시 active hold release 검증. 플래너 홀드→세틀러 finalize→환급 흐름, preflight 실패/스테일 홀드 릴리즈/레저 미설정 시나리오 등 테스트 추가.
작업 메타데이터
tasks/phase-2-video-async-infra/03-credit-graph-wiring.md
작업 상태를 완료로 변경하고 completed_at 및 체크리스트 항목들을 완료로 표기.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • gaeunee2

Poem

🐰 홀드가 쌓이고 레저가 속삭이네
플래너가 미리 잡고 세틀러가 마무리해
예외가 와도 홀드는 돌아오고
트랜잭션처럼 깔끔히 정리되네
당근 한 입, 배선 완료 축하!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 24.42% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 핵심 변경사항(크레딧 그래프 배선)을 명확하게 요약하고 있으며, 제공된 raw_summary의 모든 변경사항과 일관성 있게 관련되어 있습니다.
Description check ✅ Passed PR 설명이 템플릿의 필수 섹션(관련 이슈, PR 타입, 작업 내용, 체크리스트)을 충실히 작성했으며, 스크린샷과 기타 참고사항도 포함되어 있습니다.
Linked Issues check ✅ Passed PR이 #85의 모든 주요 요구사항을 충족합니다: Planner hold 배선, CreditSettler finalize 구현, solve/pdf 비용 차감, 재시도 preflight 검증, 통합 테스트 추가가 모두 완료되었습니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 #85 스코프와 일관성 있게 관련되어 있으며, 영상 capture와 비동기 video job refund는 명시적으로 Task 2.06/2.07로 제외되었습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/85-credit-graph-wiring

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요약

크레딧 hold → 동기 작업 finalize → SSE 정산 흐름과 retry preflight·contextvar 배선은 방향이 맞고 테스트도 핵심 경로를 잘 덮습니다. 다만 plan hold 추정이 core_solver가변 과금과 고정 가정으로 어긋나, all-or-nothing hold의 실효가 약해질 수 있습니다. 그래프 중단 시 hold 해제는 TTL-on-read(20분)에 의존하므로 설계상 허용 범위이나, 부분 실행·멀티턴에서는 추가 확인이 필요합니다.


[문제 1]

  • 심각도: High
  • 범주: 정확성
  • 근거: credit_pricing._estimate_solve_cost는 verify/explain LLM을 각 1회, code_generate/code_execute를 각 1회로 가정합니다. 반면 core_solver는 Phase 1 반복(_MAX_ITERATIONS=5)에 따라 llm_call_count, codegen_count, execute_count가 가변적으로 credit_log에 누적됩니다. hold는 고정 추정치로 잡히고 finalize_hold는 실제 sync_cost 전액을 balance에서 차감하며 hold 금액 상한을 검사하지 않습니다(태스크 문서의 “초과 허용”과 일치). 그 결과 hold가 실제보다 작으면 예약(hold) 없이 추가 차감이 발생할 수 있고, 사용자 입장에서는 “N cr 필요”로 거절됐어야 할 요청이 실행 후 더 많이 청구되는 경로가 생깁니다.
  • 수정안: (a) hold 추정에 core_solver와 동일한 상한(예: max iteration × 단가)을 반영하거나, (b) finalize_hold 전에 sync_cost > hold.amount이면 CreditHoldAmountExceededError로 처리·release_hold 후 사용자 오류를 반환하거나, (c) 최소한 MODEL_CREDIT_COST/_MODEL_COST를 단일 모듈로 통합해 추정·실청구가 같은 테이블을 참조하게 하세요. 단기적으로는 (a)+(c) 조합이 리스크 대비 변경 폭이 작습니다.

[문제 2]

  • 심각도: Medium
  • 범주: 정확성 / 보안
  • 근거: _preflight_video_retry에서 video_job_clientNone이면 검증 없이 통과합니다(client is None: return). DB/ledger는 있으나 video client만 미주입·장애인 경우, video_retry 요청이 hold 없이 planner LLM까지 진행될 수 있습니다.
  • 수정안: retry 액션이면 client가 없을 때도 invalid_input(또는 503)로 _raise_preflight_error 처리하세요. dev/테스트용으로 ledger를 끄는 경우만 명시적 설정 플래그로 preflight skip을 허용하는 편이 안전합니다.

[문제 3]

  • 심각도: Medium
  • 범주: 정확성
  • 근거: _reserve_plan_holdstate.hold_id가 있으면 금액·plan 변경 없이 기존 ID를 그대로 재사용합니다. 체크포인트에 pending hold가 남은 멀티턴/재시도 시나리오에서 새 plan 비용과 hold amount가 어긋날 수 있습니다. (LangGraph 입력 merge가 매 턴 hold_id를 초기화하는지는 호출 경로에 따라 달라 추가 확인 필요)
  • 수정안: 턴 시작 시 새 hold가 필요하면 hold_id를 명시적으로 None으로 리셋하거나, 기존 hold의 plan_id/amount와 현재 estimate_plan_hold_amount를 비교해 불일치 시 release_hold 후 재 hold 하세요.

[문제 4]

  • 심각도: Low
  • 범주: 성능
  • 근거: PostgresCreditLedgerClienthold/finalize_hold/release_hold마다 AsyncConnection.connect로 새 연결을 엽니다. solve 한 번에 planner hold + settler finalize로 최소 2회 연결이 생깁니다.
  • 수정안: lifespan에서 connection pool(또는 AsyncConnectionPool)을 주입하고 client가 pool에서 conn을 빌려 쓰도록 바꾸면 됩니다. 당장은 기능상 문제는 없으나 트래픽 증가 시 우선 개선 후보입니다.

잔여 리스크 (수정 필수 아님): 그래프가 credit_settler 전에 예외 종료되면 pending hold는 TTL(기본 1200s) 만료까지 가용 잔액을 묶습니다. 설계 문서(TTL-on-read)와 일치하나, 장시간 실패 시 UX 이슈는 모니터링 대상입니다. 영상 10cr는 hold에 포함되나 동기 정산에서 제외되는 것은 후속 Task 2.06 범위로 보이며, 이번 PR 범위에서는 의도된 분리로 이해했습니다.

Open in Web View Automation 

Sent by Cursor Automation: Chowon Reviewer

explanation_mode: Literal["full", "brief"],
) -> Decimal:
model_cost = MODEL_CREDIT_COST.get(selected_model, MODEL_CREDIT_COST["flash"])
llm_call_count = Decimal("1") + (Decimal("1") if explanation_mode == "full" else Decimal("0"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[High · 정확성] _estimate_solve_cost는 LLM/code 호출을 각 1회로 고정하지만, core_solver는 iteration에 따라 llm_call_count·codegen_count·execute_count가 늘 수 있습니다. hold가 작게 잡히면 finalize_hold가 예약액을 넘는 sync_cost를 그대로 차감할 수 있어, all-or-nothing hold의 의미가 약해집니다. 상한 반영 또는 sync_cost > hold.amount 가드 검토를 권합니다.

Comment thread src/proovy_agent/graph/nodes/planner.py Outdated

client = _get_video_job_client()
if client is None:
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium · 정확성/보안] video_job_clientNone이면 retry preflight가 전부 스킵됩니다. retry 요청이면 client 부재 시에도 거절(또는 명시적 dev-only bypass)하는 편이 hold 전 검증 의도와 맞습니다.

Comment thread src/proovy_agent/graph/nodes/planner.py Outdated
planner_credit: CreditEntry,
) -> str | None:
if state.hold_id:
return state.hold_id

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium · 정확성] 기존 hold_id가 있으면 재추정·재 hold 없이 반환합니다. 체크포인트에 남은 pending hold와 새 plan 비용이 다를 때 정산 불일치 가능성이 있어, 턴 경계에서 hold 리셋/재 hold 조건을 한번 더 확인해 주세요.

reserved: float | None = None
refunded: float | None = None

if state.hold_id:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[확인 부탁 · Medium] 그래프 에러 시 hold 누수 가능성

finalize_hold 경로가 hold를 해제하는 사실상 유일한 지점이고 그래프 끝(credit_settler)에서만 실행되는 것 같은데요. planner가 hold를 잡은 뒤 core_solver 등 중간 노드가 예외를 던지면 credit_settler가 실행되지 않아 hold가 PENDING으로 남고, 사용자 크레딧이 TTL 만료까지 묶이지 않을까요?

실패한 요청을 바로 재시도할 때 잔액이 줄어 있는 UX가 우려됩니다. _run 에러 경로(solve.py)나 그래프 finally에서 release_hold를 호출하는 방안은 검토되셨는지 확인 부탁드립니다. (TTL 자동 해제는 되지만 ~20분 윈도우라서요.)

*,
plan_id: str | None = None,
) -> CreditHold:
async with await AsyncConnection.connect(self._database_url) as conn:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[확인 부탁 · Medium] 연산마다 새 커넥션 — 풀링 검토

hold/finalize/release가 각각 AsyncConnection.connect로 새 커넥션을 여는 구조라, 한 요청에서 hold + finalize만으로도 커넥션이 2개 이상 생성됩니다. 동시 요청이 늘면 커넥션 churn/고갈이 걱정되는데, checkpointer처럼 풀(또는 공유 커넥션)을 재사용하는 방향은 어떨지 확인 부탁드립니다.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/proovy_agent/app/api/v1/solve.py (1)

80-97: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

종료 시 solve 태스크와 공유 리소스의 수명을 맞춰 주세요.

여기서 만든 _run()은 요청과 분리돼 계속 실행되는데, src/proovy_agent/app/main.pylifespan()은 shutdown 시 credit_ledger_client와 Daytona를 바로 닫습니다. 그 상태에서 태스크가 hold를 가진 채 취소되거나 닫힌 클라이언트로 finalize_hold()/release_hold()를 호출하면 크레딧이 만료 시점까지 묶일 수 있습니다. 앱 종료 전에 in-flight solve 태스크를 cancel+await한 뒤 공유 클라이언트를 닫고, CancelledError 경로에서도 _release_active_credit_hold()를 호출하도록 정리하는 편이 안전합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/proovy_agent/app/api/v1/solve.py` around lines 80 - 97, The solve
background task (_run()) outlives the request and may try to use shared clients
after shutdown; update shutdown flow to cancel and await in-flight solve tasks
before closing shared clients (see _active_tasks, task created for _run()), and
ensure the CancelledError handler in _run() calls
_release_active_credit_hold(credit_ledger_client, state.user_id) so any held
credits are released even when the task is cancelled; adjust
src/proovy_agent/app/main.py lifespan() to iterate over _active_tasks, cancel
and await them prior to closing credit_ledger_client/Daytona.
🧹 Nitpick comments (2)
src/proovy_agent/graph/credit_pricing.py (1)

37-53: ⚡ Quick win

Plan hold 추정 누락 우려 완화: PlanStep.action 범위로는 general_chat/image 미포함

estimate_plan_hold_amount에서 solve/pdf/video만 누적해도, PlanStep.action(및 _StepInput.action, PlanStepView.action)이 Literal["solve", "video", "pdf"]로 고정되어 있어 general_chat/image 액션이 계획에 들어올 수 없습니다. 다만 GENERAL_CHAT_COST·IMAGE_COSTcredit_pricing.py에서만 보이므로 실제 정산 경로에서 쓰이는지(데드 코드 여부) 점검이 필요합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/proovy_agent/graph/credit_pricing.py` around lines 37 - 53,
estimate_plan_hold_amount currently only accumulates costs for actions "solve",
"pdf", and "video" but PlanStep.action may include other action types (e.g.,
"general_chat", "image"), and GENERAL_CHAT_COST and IMAGE_COST exist in this
module; update estimate_plan_hold_amount to handle these additional actions by
adding branches for "general_chat" (add GENERAL_CHAT_COST) and "image" (add
IMAGE_COST), ensuring you still use credit_log_amount(accrued_log) and
_estimate_solve_cost(selected_model, explanation_mode) for solve, or if those
constants are truly unused elsewhere, add a note/remove dead code after
confirming via search; refer to estimate_plan_hold_amount, PlanStep.action,
GENERAL_CHAT_COST, IMAGE_COST, credit_log_amount, _estimate_solve_cost,
PDF_COST, VIDEO_FLAT_COST.
tests/app/test_solve_endpoint.py (1)

63-109: 💤 Low value

_FakeCreditLedger/_hold 헬퍼가 tests/graph/test_credit_graph_wiring.py와 거의 동일하게 중복됩니다.

두 파일에서 동일한 스텁을 유지보수해야 하므로 공용 conftest.py 픽스처나 테스트 유틸로 추출하는 것을 고려해 보세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/app/test_solve_endpoint.py` around lines 63 - 109, The
_FakeCreditLedger class and _hold helper are duplicated across tests; extract
them into a shared test fixture or helper module (e.g., conftest.py or a
test_utils module) and update tests to import/use the shared fixture instead of
redefining _FakeCreditLedger and _hold; ensure the exported fixture preserves
the same constructor signature, async methods (hold, finalize_hold,
release_hold) and return values (CreditHold instances with the same fields) so
existing tests referencing _FakeCreditLedger and _hold continue to work.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/proovy_agent/graph/nodes/credit_settler.py`:
- Around line 55-59: The AIMessage call in credit_settler.py uses
metadata={"display":"system"}, which is not a LangChain 1.0 standard field;
update the AIMessage(...) invocation to pass the hint via response_metadata
(e.g., response_metadata={"display":"system"}) or additional_kwargs if you need
non-standard keys, keeping the message text (f"총 {float(sync_cost)}cr 사용")
intact and ensuring any downstream code that reads that hint reads
response_metadata instead of metadata.

In `@src/proovy_agent/graph/nodes/planner.py`:
- Around line 222-224: The hold cleanup (checking state.hold_id and calling
await ledger.release_hold(state.user_id, UUID(state.hold_id)) catching
CreditLedgerError/ValueError) must run before _preflight_video_retry, because
failed retry validation currently bypasses the release and leaves a stale hold;
move that release block ahead of the call to _preflight_video_retry or extract
it into a helper (e.g., _ensure_release_hold(state, ledger)) and invoke it
immediately before retry validation so old holds are always released even when
_preflight_video_retry fails.
- Around line 222-224: The current suppress(ValueError, CreditLedgerError)
around await ledger.release_hold(state.user_id, UUID(state.hold_id)) swallows
all ledger errors and can leave a prior hold PENDING, causing duplicate holds or
spurious credit_exhausted; change this to only ignore the parsing error
(ValueError) and handle ledger errors explicitly: remove CreditLedgerError from
the suppress, catch only narrow ledger exceptions such as HoldNotFound or
HoldAlreadyReleased (or the ledger method's specific "not found/already
released" exception classes) to treat as non-fatal (optionally log), and let
other CreditLedgerError variants propagate or be handled separately so failures
abort the flow; update references to state.hold_id, ledger.release_hold,
UUID(...) and CreditLedgerError accordingly and add logging for the caught
narrow exceptions.

In `@tests/graph/test_credit_graph_wiring.py`:
- Around line 321-336: The test
test_planner_works_without_ledger_client_for_unit_tests can accidentally create
a Postgres ledger because _reserve_plan_hold uses
current_credit_ledger_client.get() or create_credit_ledger_client() which falls
back to planner_module.settings.database_url; to fix it, in the test set
planner_module.settings.database_url to an empty string (e.g. via monkeypatch)
before calling planner_module.planner so the fallback path is disabled and
hold_id remains None—ensure you patch planner_module.settings.database_url in
this test (and restore/keep isolated) similar to the sibling tests that force
the no-database path.

---

Outside diff comments:
In `@src/proovy_agent/app/api/v1/solve.py`:
- Around line 80-97: The solve background task (_run()) outlives the request and
may try to use shared clients after shutdown; update shutdown flow to cancel and
await in-flight solve tasks before closing shared clients (see _active_tasks,
task created for _run()), and ensure the CancelledError handler in _run() calls
_release_active_credit_hold(credit_ledger_client, state.user_id) so any held
credits are released even when the task is cancelled; adjust
src/proovy_agent/app/main.py lifespan() to iterate over _active_tasks, cancel
and await them prior to closing credit_ledger_client/Daytona.

---

Nitpick comments:
In `@src/proovy_agent/graph/credit_pricing.py`:
- Around line 37-53: estimate_plan_hold_amount currently only accumulates costs
for actions "solve", "pdf", and "video" but PlanStep.action may include other
action types (e.g., "general_chat", "image"), and GENERAL_CHAT_COST and
IMAGE_COST exist in this module; update estimate_plan_hold_amount to handle
these additional actions by adding branches for "general_chat" (add
GENERAL_CHAT_COST) and "image" (add IMAGE_COST), ensuring you still use
credit_log_amount(accrued_log) and _estimate_solve_cost(selected_model,
explanation_mode) for solve, or if those constants are truly unused elsewhere,
add a note/remove dead code after confirming via search; refer to
estimate_plan_hold_amount, PlanStep.action, GENERAL_CHAT_COST, IMAGE_COST,
credit_log_amount, _estimate_solve_cost, PDF_COST, VIDEO_FLAT_COST.

In `@tests/app/test_solve_endpoint.py`:
- Around line 63-109: The _FakeCreditLedger class and _hold helper are
duplicated across tests; extract them into a shared test fixture or helper
module (e.g., conftest.py or a test_utils module) and update tests to import/use
the shared fixture instead of redefining _FakeCreditLedger and _hold; ensure the
exported fixture preserves the same constructor signature, async methods (hold,
finalize_hold, release_hold) and return values (CreditHold instances with the
same fields) so existing tests referencing _FakeCreditLedger and _hold continue
to work.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3713a4c0-578c-4581-b243-afb9fd9e81ba

📥 Commits

Reviewing files that changed from the base of the PR and between ce28914 and 3101576.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (14)
  • pyproject.toml
  • src/proovy_agent/app/api/v1/solve.py
  • src/proovy_agent/app/main.py
  • src/proovy_agent/features/credits/__init__.py
  • src/proovy_agent/features/credits/service.py
  • src/proovy_agent/graph/agents/core_solver/agent.py
  • src/proovy_agent/graph/credit_pricing.py
  • src/proovy_agent/graph/nodes/credit_settler.py
  • src/proovy_agent/graph/nodes/planner.py
  • src/proovy_agent/graph/runtime.py
  • tasks/phase-2-video-async-infra/03-credit-graph-wiring.md
  • tests/app/test_health.py
  • tests/app/test_solve_endpoint.py
  • tests/graph/test_credit_graph_wiring.py

Comment thread src/proovy_agent/graph/nodes/credit_settler.py
Comment thread src/proovy_agent/graph/nodes/planner.py Outdated
Comment thread tests/graph/test_credit_graph_wiring.py
@chowon442 chowon442 merged commit ace1d1f into dev Jun 4, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Sub] 크레딧 그래프 배선 — Planner hold + CreditSettler + solve/pdf 차감 [2.03]

3 participants