linetta 1.7 에이전트 진행 표시와 조용히 끝난 턴 안내
linetta의 내장 에이전트가 도구 몇 개를 읽은 뒤 아무 말 없이 멈추는 일이 있었다. 화면만 봐서는 아직 도는 건지 끝난 건지 알 수 없었고, 다른 화면에 다녀오면 패널은 대화 기록만 복원한 채 “이 작품의 이전 요청이 아직 진행 중일 수 있습니다”라고 추측만 했다. 보내기 버튼은 살아 있는데 누르면 agent_busy로 거절됐다. 2026-10-14에 낸 v1.7.0은 이 두 가지를 고친 릴리스다.
답변 없이 끝난 턴 찾기
라이브러리의 대화 기록을 열어 보니 문제의 두 턴은 도구 행으로 끝나고 최종 답변 행이 없는데, 상태는 done이었다. 에이전트 루프는 도구 호출이 없는 응답이 오면 턴이 끝났다고만 판단해서, 모델이 빈 응답을 주거나 출력 한도에서 잘려도 정상 종료(agent.done)로 처리했고 패널에는 아무것도 그려지지 않았다. 그 두 턴이 정말 빈 응답이었는지, 턴 도중에 앱이 꺼진 것인지는 기록만으로 구분할 수 없었다. 그래서 두 경우가 모두 화면에 드러나게 하기로 했다.
도구 호출 없이 끝난 응답이 비어 있으면 agent_empty_reply, max_tokens나 length로 잘렸으면 agent_output_truncated 오류로 턴을 끝낸다. 패널에는 각각 “에이전트가 답변 없이 멈췄습니다”와 “응답이 너무 길어 중간에 끊겼습니다”로 시작하는 안내가 뜨고, 둘 다 여기까지의 내용은 남아 있으니 ‘계속’이라고 보내 보라고 이어진다. 잘린 경우에는 한 번에 한 씬씩 나눠서 요청해 보라는 말도 붙인다. 그 턴에서 이미 한 작업은 실제로 일어난 일이라서 대화 기록의 행 상태는 도구 한도에 걸린 턴처럼 done으로 둔다.
진행 중인 턴에 다시 붙기
엔진에 패널이 지금 턴 상태를 물어볼 수 있는 agent.status 호출(RPC)을 추가했다. 작품에서 도는 턴의 실행 id, 시작 시각, 마지막 이벤트 번호, 아직 기록되지 않은 스트리밍 텍스트, 실행 중인 도구를 돌려준다. 모든 agent.* 이벤트에는 이벤트 번호(seq)를 붙여서, 턴 도중에 붙은 패널이 스냅샷에 이미 들어 있는 이벤트를 버릴 수 있게 했다.
패널은 열릴 때 agent.status를 읽어 진행 중인 턴에 다시 붙고, 정지 버튼과 진행 표시, 이후 이벤트를 이어서 받는다. 턴이 도는 동안에는 로그 끝에 “생각하는 중…”, “답변을 쓰는 중…”, “작업하는 중…” 중 하나와 경과 시간이 뜬다. 아무것도 돌지 않는데 기록이 턴 중간에서 끊겨 있으면 요청이 끝까지 진행되지 못하고 중단되었다고 알리고, agent.status를 읽지 못하면 예전의 “진행 중일 수 있습니다” 문구로 돌아간다. 패널을 닫아 두어도 턴이 도는 동안에는 워크스페이스 툴바의 에이전트 버튼에 점이 뜬다.
패널이 대화를 그리는 방식도 두 가지 바꿨다. 도구 호출 뒤의 답변이 위쪽 말풍선에 이어 붙던 것을 도구 줄 아래 새 말풍선으로 옮겼고, 새 줄이 생기면 로그가 따라 내려가되 위로 올려 읽는 중이면 그대로 둔다.
Anthropic 출력 한도 올리기
빈 응답과 잘린 응답이 화면에 드러나게 됐지만, 잘리는 일 자체는 그대로였다. linetta는 Anthropic 클라이언트에 max_tokens를 지정하지 않아서 tars가 스트리밍하지 않는 호출용으로 정해 둔 보수적인 기본값 16,000이 들어가고 있었다. 에이전트의 작업 단위는 씬이고 씬 본문은 도구 호출 인자로 통째로 들어가는데, 모델의 사고 토큰도 같은 한도에서 빠진다. 그래서 한 응답에서 씬 여러 개를 다시 쓰면 16,000을 넘겨 도구 호출 도중에 잘리고, 아무것도 쓰지 못한 채 턴이 끝났다.
64,000 이상을 문서화한 Anthropic 모델군에만 max_tokens: 64000을 넘기게 했다. 명시한 한도가 모델 한도보다 크면 Anthropic이 요청을 거절해서, 목록에 없는 구형 모델이나 게이트웨이를 거치는 모델은 건드리지 않고 tars 기본값을 그대로 쓴다. 64,000은 목록에 있는 모델군의 한도 중 가장 낮은 값(haiku-4-5)이라 숫자 하나로 모두에게 유효하다. OpenAI 호환, Gemini, Codex 쪽은 바꾸지 않았다. 한도는 상한일 뿐이라 짧은 응답의 비용은 그대로지만, 긴 응답은 이제 끝까지 생성되는 만큼 출력 토큰이 더 나올 수 있다.
두 변경과 관련된 테스트를 v1.7.0 태그의 engine 디렉터리에서 골라 돌린 결과다. 앞의 두 개는 빈 응답과 잘린 응답이 각각 안내 오류로 끝나는지, 턴 도중의 agent.status가 스트리밍된 텍스트를 돌려주는지 보고, 뒤의 두 개는 모델군별 한도와 실제 요청 본문에 max_tokens: 64000이 실리는지 본다(디버그 로그 줄은 뺐다).
$ GOTOOLCHAIN=go1.26.9 CGO_ENABLED=0 go test ./internal/agent/ ./internal/provider/ \
-run 'TestRun_aTurnThatStopsWithoutFinishingSaysSo|TestStatus_reportsARunningTurnAndItsStreamedText|TestOptions_raisesAnthropicOutputLimitForKnownFamilies|TestOptions_anthropicOutputLimitReachesTheRequest' -v -count=1
=== RUN TestRun_aTurnThatStopsWithoutFinishingSaysSo
=== RUN TestRun_aTurnThatStopsWithoutFinishingSaysSo/an_empty_last_response
=== RUN TestRun_aTurnThatStopsWithoutFinishingSaysSo/a_response_cut_off_at_the_output_ceiling
=== RUN TestRun_aTurnThatStopsWithoutFinishingSaysSo/a_reply_cut_off_mid-sentence
--- PASS: TestRun_aTurnThatStopsWithoutFinishingSaysSo (0.09s)
--- PASS: TestRun_aTurnThatStopsWithoutFinishingSaysSo/an_empty_last_response (0.04s)
--- PASS: TestRun_aTurnThatStopsWithoutFinishingSaysSo/a_response_cut_off_at_the_output_ceiling (0.03s)
--- PASS: TestRun_aTurnThatStopsWithoutFinishingSaysSo/a_reply_cut_off_mid-sentence (0.03s)
=== RUN TestStatus_reportsARunningTurnAndItsStreamedText
--- PASS: TestStatus_reportsARunningTurnAndItsStreamedText (0.03s)
PASS
ok github.com/devlikebear/linetta/engine/internal/agent 0.132s
=== RUN TestOptions_raisesAnthropicOutputLimitForKnownFamilies
--- PASS: TestOptions_raisesAnthropicOutputLimitForKnownFamilies (0.00s)
=== RUN TestOptions_anthropicOutputLimitReachesTheRequest
--- PASS: TestOptions_anthropicOutputLimitReachesTheRequest (0.00s)
PASS
ok github.com/devlikebear/linetta/engine/internal/provider 0.006s
EntitySheet MCP 새로고침 테스트가 가끔 실패하던 구독 경합도 고쳤다.
확인한 것과 남은 것
진행 표시와 다시 붙기는 Tauri 셸 없이 브라우저의 웹 UI에서 느리게 스트리밍하는 가짜 모델로 확인했다. 일반 턴에서 말풍선, 도구 줄, 진행 줄, 최종 답변이 순서대로 그려지는지, 턴 도중에 라이브러리로 나갔다 오거나 페이지를 새로 고쳐도 진행 중으로 돌아오고 정지가 되는지, 엔진을 강제로 끄고 다시 켜면 “중단되었습니다”가 뜨는지를 봤다. Tauri 데스크톱 빌드에서 실제로 돌려 보지는 못했다. 출력 한도는 요청 본문까지 테스트로 확인했지만 실제 Anthropic API에 보낸 호출은 아직 없다.
App Store에서 받거나 brew install --cask devlikebear/tap/linetta로 설치한다. 직접 받은 데스크톱 앱은 메뉴의 업데이트 확인으로 올릴 수 있다.