2026-10-22

tars 목표 모드가 같은 질문만 되풀이하는 에이전트를 멈추게 하기

tars의 포커스 모드에는 목표 모드가 있다. 파이프라인이 단계를 밟아 가다 에이전트가 질문을 던지면 사람 대신 “알아서 정해”라고 답하고 다음으로 넘어가는 모드라서, 화면 앞에 아무도 없어도 작업이 이어진다. 10월 21일에는 이 모드로 돌린 “소스 분석” 파이프라인(project-report 템플릿) 하나가 밤사이와 아침에 Claude의 5시간 사용량을 통째로 썼다. Sonnet 호출이 500번쯤이었고, 호출마다 맥락이 48만 토큰쯤 실렸다. 분석 단계의 에이전트가 “알아서 정해”라는 답을 받을 때마다 같은 질문 두 개를 다시 했는데, 목표 모드에는 이걸 멈출 장치가 없었다.

파이프라인 파일에서 원인 찾기

파이프라인 파일에 남은 값을 보니 따로따로 생긴 문제 네 개가 겹쳐 있었다.

decisions: 1921, pushes: 8 / 20      결정 카드에 답해도 push 예산이 줄지 않았다
turn: 11인 결정 카드 665장            답변 프롬프트가 4KB에서 104KB까지 커졌다
analyze turns: 644, 한도 3           질문만 하는 빌드 턴은 턴 한도에 걸리지 않았다
"You've hit your session limit"      평범한 실패 턴으로 보고 리셋될 때까지 재시도했다

첫째로 목표 모드가 결정 카드에 답해도 push 예산(목표 모드가 에이전트에게 턴을 보낼 수 있는 횟수)이 줄지 않아서, 질문에 답하는 일은 사실상 무제한이었다.

둘째는 답을 묶는 기준이었다. 답은 대화 기록의 턴 번호로 묶였는데, 서버가 돌린 턴은 대화 기록을 늘리지 않아서 턴 번호가 11에서 멈춰 있었다. 그래서 답변 턴마다 그 단계가 받은 모든 답을 다시 보냈고, 프롬프트가 4KB에서 104KB까지 불어났다.

셋째로 빌드 턴 한도는 블록이 없거나 검증이 끝난 뒤에만 검사해서, 질문만 하고 답을 기다리는 턴은 한도에 닿지 않았다. 한도가 3인 분석 단계가 644턴을 돌았다.

넷째로 “You’ve hit your session limit · resets 2:40pm” 같은 사용 한도 메시지를 그냥 실패한 턴으로 다뤄서, 최대 15분씩 쉬어 가며 밤새 재시도하다가 한도가 풀리자 같은 반복을 다시 돌았다. 오전 9시쯤부터 새 사용량이 쓰이기 시작했다.

네 군데를 따로 막기

하나만 고쳐도 이번 반복은 멈췄겠지만, 각각 다른 경우에 다시 터질 수 있어서 네 군데를 다 고치기로 했다.

답변 턴도 push 예산을 쓰게 했다. 한 턴에 열린 질문 중 마지막 질문, 곧 그 답으로 턴이 보내지는 질문에 Push: true를 달아서 질문이 여러 개인 턴도 한 번만 센다. 예산을 다 쓴 상태에서 질문이 열리면 목표 모드가 exhausted로 끝난다. 기본값으로는 답변 턴 20번이고 max_pushes로 100번까지 늘릴 수 있다.

답을 묶는 기준은 파이프라인이 직접 세는 턴 번호로 바꿨다. Pipeline.TurnSeq가 파이프라인이 도는 동안 끝난 턴 수를 세고, 카드마다 Card.Seq로 찍어서 turnAnswers가 이 값으로 묶는다. 콘솔과 질의응답 발췌(focusCardExcerpt)는 대화 기록의 턴 번호를 읽기 때문에 Card.Turn은 그대로 뒀고, Seq가 없는 예전 카드는 계속 Turn으로 묶는다.

질문 때문에 멈춘 빌드 턴도 buildTurnCap에 넣어 셌다. 한도에 닿으면 no_progress 게이트가 걸려 단계가 막힌다. 게이트가 막힌 동안 들어온 답은 이미 오래된 답으로 처리되기 때문에(SplitStaleAnswers는 활성 단계를 요구한다) 턴으로 나가지 않는다.

마지막으로 UsageLimitError가 “session limit”, “usage limit”, “weekly limit”, “5-hour limit”, “hit your limit” 같은 사용 한도 메시지를 알아보게 했다. 턴 실패 게이트의 오류가 여기에 걸리면 목표 모드가 새 종료 사유 usage_limit로 바로 끝나고, 알림 카드와 “Goal mode stopped” 알림, 콘솔 메시지를 남긴다. 콘솔에는 “목표 모드가 멈췄습니다: 모델 제공자의 사용 한도에 도달했습니다. 한도가 초기화된 뒤 열린 게이트를 다시 시도하세요.”라고 뜬다. 잠깐 걸리는 429 같은 요청 제한은 지금처럼 쉬었다가 다시 시도한다.

파이프라인 파일에는 seq와 turn_seq 필드가 새로 생겼는데 둘 다 omitempty라서 기존 파이프라인은 그대로 읽히고, 설정 변경도 없다.

확인한 것과 남은 것

이번 사고를 그대로 다시 돌리는 TestGoalModeEndsAnAgentThatKeepsAsking을 새로 넣었다. 대화 기록의 턴 번호를 11에 묶어 두고 매 턴 같은 질문 두 개를 던지게 한 뒤, 목표 모드가 예산에서 exhausted로 끝나는지, 그 전에 빌드 턴 한도가 단계를 막는지, 답변 턴이 예전 답을 다시 보내지 않는지를 본다. 글을 쓰면서 이 테스트와 사용 한도 메시지 판별 테스트를 다시 돌려 봤다.

$ go test ./internal/focuspipeline -run 'TestGoalModeEndsAnAgentThatKeepsAsking|TestUsageLimitError' -v
=== RUN   TestGoalModeEndsAnAgentThatKeepsAsking
--- PASS: TestGoalModeEndsAnAgentThatKeepsAsking (0.00s)
=== RUN   TestUsageLimitError
--- PASS: TestUsageLimitError (0.00s)
PASS
ok  	github.com/devlikebear/tars/internal/focuspipeline	0.005s

PR에서는 make test, 변경 줄 커버리지 100%(40/40줄), lint, 보안 검사를 통과했다. 콘솔 쪽 변경은 배너 문구 하나와 타입 하나라서 콘솔 e2e는 돌리지 않았다. 이제 질문을 많이 하는 파이프라인은 목표 모드가 더 일찍 멈추고, 질문이 많은 대화형 빌드 단계는 빌드 턴 한도에 걸려 막힐 수 있다. 일부러 그렇게 둔 것이라, 막히면 재시도로 한 번 더 돌리면 된다. 이 수정은 10월 21일에 main에 머지했고 아직 릴리스에는 들어가지 않았다.

이번 글에서 다룬 PR

  • #1297 질문을 반복하는 에이전트에서 목표 모드가 멈추게 하기

같은 작업대에서 나온 이웃 도구입니다.