Focus Mode
작업 하나를 끝까지, 가장 높은 수준으로 해내기 위한 작업 모드를 소개한다. Focus 가 켜져 있는 동안 sub-agent 넷이 메인 에이전트 곁에서 매 지시를 분석하고, 레퍼런스를 조사하고, 기록을 남기고, 진행률을 매긴다. 사람으로 치면 한 가지 일에 몰입할 수 있도록 곁에 보좌진을 두는 셈이다.
Focus Mode가 주목한 점
"신입 개발자 온보딩 1주차 계획 만들기" 같은 작업을 기존의 Chatbot Assistant와 대화로 이어 가면, 지시가 열 번쯤 쌓일 즈음 처음 목적과 앞에서 정한 것이 흐려진다. 비슷한 선례를 찾아보는 일, 지금까지 무엇을 했는지 적어 두는 일, 얼마나 남았는지 가늠하는 일은 결국 사용자가 따로 챙겨야 한다. 대화 기록을 다시 읽거나 메모를 따로 남기는 우회 방안도 있지만, 일반 대화 화면은 이 정보를 한눈에 보여 주지 않고 메모를 갱신하는 일도 사용자의 몫으로 남는다. Focus Mode는 이러한 부분을 개선하기 위해 작업 하나에 분석, 조사, 기록, 진행 관리를 맡는 sub-agent 를 붙이고, 그 결과를 한 화면에 모아 사용자가 작업 자체에만 집중할 수 있도록 한다.
어떤 기능을 가지고 있나요?
sub-agent 를 도구가 아니라 턴 훅으로 붙였다
AgentSession에 턴 훅(system_addendum,before_turn,after_turn)을 두고, 분석, 조사, 기록, 진행 sub-agent 를 세션이 직접 부른다. Focus 가 꺼져 있으면 훅이 없고, 기존 대화 경로는 그대로다.- 대안으로는 네 sub-agent 를 메인 에이전트의 도구로 주는 방식이 있었다.
- 도구로 주면 부를지 말지를 모델이 정하므로, "매 지시를 분석한다" 는 이 모드의 약속이 모델의 판단에 달린다. 대신 시점을 내줬다. 분석은 메인 응답과 동시에 시작하므로 그 결과는 다음 응답에 들어간다. 첫 지시만은 분석과 단계 계획을 최대 30초 기다렸다가 첫 응답에 넣는다. "예상 다음 지시", "미리 대비할 것" 은 원래 다음 턴을 위한 항목이라 잃는 것이 적다고 판단했다.
sub-agent 는 작업 동안 살아 있는 세션이다
- 역할마다
focus_<task_id>_<role>세션을 처음 필요할 때 한 번 만들고 작업이 끝날 때까지 쓴다. 세션은 동시에 두 프롬프트를 받지 않으므로 역할마다 잠금을 둬 같은 역할의 일은 줄을 세운다. sub-agent 가 실패하면 빈 결과로 넘어가고 메인 응답은 막지 않는다. - 대안으로는 Orchestration Pattern 의 서브 에이전트처럼 부를 때마다 새로 만드는 방식이 있었다.
- 매번 새로 만들면 서기는 앞의 기록을, 분석은 앞의 지시를 모른다. 작업 동안 세션을 유지해야 앞선 지시와 기록을 기억한 채로 일한다.
상태의 정본은 파일, 화면은 덮어쓴다
- 활성 표시는
active/<session_id>파일을 배타적으로 만들어,/focus가 동시에 두 번 와도 하나만 시작한다.state.json은 임시 파일에 쓰고 바꿔 끼워, 어디서 죽어도 반쯤 쓴 상태가 남지 않고 재시작하면 다음 접속 때 그대로 되살린다. 화면 이벤트(focus_state,focus_panel)는 매번 전체 상태를 보낸다. - 대안으로는 화면 이벤트에 바뀐 부분(차이)만 보내는 방식이 있었다.
- 이 프로젝트는 "같은 일을 두 번 해도 한 번 한 것과 같다" 를 기본값으로 둔다. 전체를 보내면 재접속, 중복 수신, 순서 뒤바뀜이 있어도 마지막에 받은 것이 화면이 된다.
조사가 실제로 무언가를 찾게 만들기
- 조사는 DuckDuckGo 의 HTML 검색 결과 페이지를 읽고, 조사 지시문에 "답하기 전에 web_search 를 한 번 이상 부른다" 와 선례 3개, 특징 두 줄, 목록 옮겨 적기 금지를 둔다. 조사는 파일에 쌓되 화면에는 가장 최근 조사만 보인다.
- 대안으로는 처음 구현처럼 DuckDuckGo 의 즉답 API 를 쓰고 결과 형식을 모델에 맡기는 방식이 있었다.
- 첫 실측에서 조사 영역은 늘 "선례를 찾지 못했다" 였다. 즉답 API 가 일반 검색어에는 결과를 주지 않았기 때문이다. HTML 결과를 읽고 검색을 강제한 뒤에야 출처가 달린 선례가 나왔다. 그다음에는 반대로 모델이 검색 결과의 "상위 10개" 목록을 그대로 옮겨 적어 영역이 넘쳤다. 형식 규칙을 넣고 같은 질문으로 한 번 다시 돌려 보니 열 줄짜리 목록이 선례 두 개, 한 줄짜리 특징으로 줄었다(gpt-4o-mini). 다만 아래 성능 비교실험에서 보듯 gpt-4o-mini 는 다른 작업에서 다시 번호 목록을 써, 지시문만으로는 완전히 막지 못한다.
음성 진입은 인식기의 실수를 전제로 한다
- "모드" 가 붙으면 비슷한 소리도 받고, 키워드를 문장 끝에 둬도 받고, 브라우저가 주는 인식 후보 셋을 모두 본다. Focus 가 켜져 있을 때는 "종료" 한마디(실측 오인식 "동요" 포함)로도 끝낸다. 문장 속 "종료" 는 받지 않아 "종료 시간 알려줘" 같은 말은 대화로 간다.
- 대안으로는 키워드 정규식을 정확하게 만드는 방식이 있었다.
- 실제 전사 기록에서 "포커스 모드" 는 "버스 모드" 로 들리거나, 마이크가 열리는 순간 통째로 잘려 "다음 주 회의 아젠다 만들어 줘" 만 남았다. 정확한 정규식은 이런 말을 모두 놓치므로, 인식기의 실수를 받아 주는 쪽을 골랐다.
세부 동작
한 눈에 보기
/focus 시작 때 분석을 기다려 첫 응답에 넣는다. Focus 동안은 먼저 거는 말을 내지 않는다. (b) Web HUD 는 좌우 카드를 띠로 접고, 줄어든 구체 둘레에 네 영역을 펼친다./focus <작업>이 오면 활성 표시 파일을 배타적으로 만들고 시작 줄을 기록에 남긴다. 조사는 오래 걸리므로 뒤에서 돌리고, 첫 분석과 단계 계획은 최대 30초 기다렸다가 첫 응답에 넣는다- 지시가 올 때마다(턴 앞) 분석 sub-agent 가 메인 응답과 같이 돈다. 분석이 검색어를 내면 조사를 부른다
- 메인 에이전트의 시스템 프롬프트에는 작업, 단계와 진행률, 직전 분석, 조사 요약(최근 1500자)이 붙는다. 메인 에이전트가 응답을 만드는 동안 sub-agent 를 직접 부르지는 않는다
- 응답이 끝나면(턴 뒤) 기록과 진행 sub-agent 가 돈다
- 상태가 바뀔 때마다 화면 이벤트로 전체 상태를 보낸다
구현은 core/focus/ 에 있다.
켜고 끄기
| 방법 | 동작 |
|---|---|
/focus <작업> | 시작. 첫 분석과 단계 계획을 기다린 뒤(최대 30초) 작업 문장으로 첫 응답을 시작한다 |
음성 포커스 모드 <작업> | /focus <작업> 과 같다 |
/focus stop, off, end, 종료, 끝, 그만 | 끝낸다. 음성은 "포커스 모드 종료", "포커스 종료". Focus 중에는 "종료", "끝", "그만해" 한마디로도 끝난다 |
/focus | 진행률과 단계를 보여준다 |
끝내면 화면이 기본 화면으로 돌아가고, 기록 파일은 남는다. 세션당 작업은 하나다. 진행 중에 다시 /focus <작업> 을 보내면 거절하고 지금 작업을 알려 준다.
네 sub-agent
| 영역 | 언제 | 하는 일 |
|---|---|---|
| 정보 분석 | 지시가 올 때마다(턴 앞) | 다섯 항목: 근본 목적, 이전 지시에서 쓸 것, 필요한 것, 예상 다음 지시, 미리 대비할 것. 조사할 검색어가 있으면 조사를 부른다. 화면에는 가장 최근 지시의 분석만, 어느 지시인지와 함께 보인다 |
| 외부 레퍼런스 조사 | 시작할 때, 분석이 검색어를 낼 때 | web_search, web_fetch 로 웹을, glob, grep, read_file 로 로컬을 찾아 선례의 특징과 출처, 적용할 점만 추린다. 선례 3개, 특징은 두 줄 이하로 한국어로 줄인다. 조사는 파일에 쌓이고 화면에는 가장 최근 조사만 보인다 |
| 작업 기록 | 응답이 끝날 때마다(턴 뒤) | 서기. 3인칭으로, "아까", "이것" 같은 상대 표현 없이, 진행 중에 나온 이름만 써서 기록한다. 오래 뒤에 읽어도 이해되게 하기 위해서다 |
| 진행 현황 | 시작할 때, 응답이 끝날 때마다 | 작업을 3~8단계로 나누고 끝난 단계를 표시한다 |
네 sub-agent 는 각자 한 가지 일만 하며, 오케스트레이션은 Focus 를 켠 세션의 메인 에이전트(JARVIS 나 K.A.R.E.N.)가 맡는다. Orchestration Pattern의 서브 에이전트는 부모 턴 안에서 잠깐 돌고 끝나지만, Focus 의 sub-agent 는 작업이 끝날 때까지 자기 세션을 유지한다.
분석이 이 모드의 중심이다. 다만 이번 지시의 분석은 메인 응답과 같이 돌기 때문에, 그 결과는 다음 응답의 시스템 프롬프트에 들어간다. 예상 다음 지시와 미리 대비할 것은 원래 다음 턴을 위한 항목이라 이렇게 해도 잃는 것이 적다. 첫 지시만은 시작할 때 기다려서 첫 응답에 넣는다.
진행률과 예상 시간
진행률은 끝난 단계 수 / 전체 단계 수다. 진행 sub-agent 가 범위가 넓어졌다고 보고 단계를 더하면 진행률이 내려간다.
예상 시간은 지금까지 걸린 시간 / 끝난 단계 수 × 남은 단계 수다. 끝난 단계가 없으면 계산하지 않는다. 일이 멈춰 있으면 단계당 시간이 늘어나 예상도 함께 늘어난다.
화면
Web HUD 는 Focus 동안 좌우 카드를 머리글만 세로로 세운 띠로 접고, 구체를 70% 크기로 줄이며 색을 진하게 한다. 구체 둘레에 호가 그려지고 네 영역이 나온다. 끝내면 같은 순서를 거꾸로 밟아 기본 화면으로 돌아간다.
띠는 눌러도 펼쳐지지 않는다. Conversation 띠만 예외로, 누르면 대화가 오른쪽에 덮여 뜬다. 음성 인식이 아직 완전하지 않아 대화를 확인할 길을 남겨 둔 것이다. 음성 동작과는 별개라 말로 이어 가는 대화 중에도 그대로 쓸 수 있다.
응답이 나오는 동안 Conversation 띠가 숨쉬듯 빛난다. 구체 아래 캡션은 가운데 빈 칸 안의 한 줄로 줄어 응답의 마지막 줄만 글자로 보이므로, 전체 응답은 그 띠를 눌러 본다.
| 영역 | 보이는 것 |
|---|---|
| Reference | 가장 최근 조사 하나. 선례마다 출처와 특징 두 줄 이하, 마지막에 적용할 점 |
| Analysis | 가장 최근 지시의 분석. 맨 위에 지시: … · 시각, 다섯 항목 아래 내용은 한 단계 들여 쓴 목록 |
| Log | 쌓인 기록 전체(끝으로 스크롤). 바닥에 기록 N건 · 마지막 HH:MM |
| Progress | 작업, 퍼센트, 막대, 예상 시간, 단계. 바닥에 단계 완료/전체 · 갱신 HH:MM |
위 줄(Reference, Analysis)이 남는 높이를 갖고, 아래 줄(Log, Progress)은 내용 높이만큼(화면의 30%에서 50% 사이)만 차지한다.
기록과 복구
작업마다 ~/.jarvis/focus/<task_id>/ 에 남는다. 끝내도 지우지 않는다.
| 파일 | 내용 |
|---|---|
log.md | 작업 기록. 시작과 종료 줄도 들어간다 |
references.md | 조사 결과 |
analysis.jsonl | 지시마다의 분석 |
state.json | 작업, 단계, 지시 목록 |
세션의 활성 작업 표시는 ~/.jarvis/focus/active/<session_id> 다. 파일을 배타적으로 만들어서 같은 세션에 /focus 가 동시에 두 번 와도 하나만 시작한다. 서버가 작업 도중 재시작되면, 다음 접속 때 이 표시와 state.json 으로 작업을 되살리고 화면도 Focus 로 돌려놓는다. sub-agent 세션은 처음 필요할 때 다시 만든다. 이전 대화 맥락은 이어지지 않지만 기록 파일은 그대로다.
설정
JARVIS_FOCUS__ENABLED=true
JARVIS_FOCUS__MODEL= # sub-agent 넷의 모델. 비우면 메인 모델
JARVIS_FOCUS__DIR=~/.jarvis/focus
JARVIS_FOCUS__READY_TIMEOUT_SEC=30 # 시작할 때 첫 분석과 단계 계획을 기다리는 상한
한계
- Focus 동안 매 턴 sub-agent 호출이 두세 번 늘고, 조사가 돌 때마다 검색과 호출이 더해진다. 클라우드 모델이면 턴당 비용이 서너 배가 된다(추정)
- Brave API 키가 없으면
web_search는 DuckDuckGo 검색 결과 페이지를 읽는다. 페이지 구조가 바뀌면 결과가 비고, 그때 조사는 "선례를 찾지 못했다" 고 쓴다 - Focus 동안에는 먼저 건네는 말(🔔)을 보내지 않는다. 💬 filler 와 맡긴 일의 완료 답은 그대로 온다
- 메인 에이전트가 응답하는 중에는 입력창이 잠기므로
/focus stop은 응답이 끝난 뒤에 보낼 수 있다 - 서버가 재시작되면 sub-agent 의 이전 대화 맥락은 이어지지 않는다. 기록 파일은 그대로다
구현 위치
| 모듈 | 하는 일 |
|---|---|
core/focus/focus_service.py | 세션마다 Focus 작업 하나를 열고 닫으며, 턴마다 sub-agent 넷을 돌려 화면에 보낸다 |
core/focus/focus_store.py | 작업을 ~/.jarvis/focus/ 아래 파일로 남기고 세션당 활성 작업 하나를 보증한다 |
core/focus/focus_types.py | 작업 상태, 진행률, 예상 시간, 명령 해석을 계산하는 순수 함수 |
core/focus/prompts.py | sub-agent 넷의 지시문과 메인 에이전트에 붙는 Focus 맥락 |
Performance Experiments
Focus Mode 를 켰을 때 사용자가 체감하는 대기 시간과, 조사 결과가 지시문이 정한 형식을 지키는지를 확인했다.
첫 응답 시간과 조사 형식
- 질문: Focus 를 시작해서 첫 응답까지 얼마나 걸리는가, 조사 sub-agent 가 지시문의 형식(선례 3개, 출처 URL, 특징 두 줄)을 지키는가
- 설정: 작업 두 개("신입 개발자 온보딩 1주차 계획 만들기", "사내 개발자 대상 LLM 에이전트 도입 사례 20분 발표 목차 만들기")로 Focus 를 시작하고, 첫 응답까지의 시간과 조사 결과의 형식을 봤다. 모델은 로컬 gemma4:e4b 와 클라우드 gpt-4o-mini 다. 조사 형식은 저장된
references.md를 검사기로 채점했다 - 결과:
그래프의 수치
| 지표 | gemma4:e4b | gpt-4o-mini |
|---|---|---|
| (a) 첫 응답까지, 온보딩 계획 | 20.08초 | 2.77초 |
| (a) 첫 응답까지, 발표 목차 | 19.47초 | 2.89초 |
| (b) 조사 결과가 남음(작업) | 2/2 | 2/2 |
| (b) 선례 3개 이하(조사 건) | 2/4 | 4/4 |
| (b) 출처가 URL(선례) | 9/12 | 6/8 |
| (b) 특징 두 줄 이하(선례) | 10/12 | 6/8 |
| (b) 특징 60자 이하(선례) | 5/12 | 6/8 |
| (b) 한국어(선례) | 10/12 | 8/8 |
| (b) 번호 목록 없음(조사 건) | 4/4 | 4/4 |
| 단계 계획의 단계 수, 온보딩 / 발표 목차 | 5 / 5 | 5 / 1 |
- 해석: 첫 응답이 늦은 것은 설계상의 대가다. 첫 지시만은 분석과 단계 계획이 끝나기를(최대 30초) 기다렸다가 응답을 시작한다. 클라우드에서는 약 3초다
- gpt-4o-mini 는 "발표 목차 만들기" 에서 선례를 찾는 대신 발표 목차 자체를 조사 칸에 썼다. 출처는 URL 이 아니라 "GitHub, DaVal, Lomin 블로그" 같은 이름이었고, 특징 칸에 하위 목록이 들어갔다. 같은 작업에서 단계 계획도 1단계로만 나눴다. 온보딩 작업에서는 형식을 거의 지켰다
- 로컬 gemma4 는 발표 목차 작업의 조사 두 건 모두에서 내용 없는 제목 줄("### … 참고 자료")을 하나 더 넣어 선례 칸이 4개로 잡혔다(실제 선례는 3개). 표의 로컬 분모 12 에는 이 빈 칸 2개가 들어 있어 URL, 두 줄, 60자, 한국어 규칙에서 각각 실패로 잡힌다. 빈 칸을 빼면 실제 선례 10개 중 URL 9개, 한국어 10개이고, 60자를 넘은 특징은 절반(5개)이다
- 측정을 고친 내역: 처음 채점기는 특징이 없는 선례를 60자 규칙을 지킨 것으로 셌고,
/만 있으면 URL 로 봤다. 고친 채점기로 산출물을 다시 만들어 채점했다. 이 과정에서 앞선 실행의 산출물은 정리하며 지워져 다시 채점하지 못했다 - 한계: 모델마다 작업 2개, 조사 4건뿐인 작은 표본이다. 수치보다 실패의 모양을 보는 용도다. 출처 규칙은 형식만 본다. URL 모양이면 통과하므로, 실제로 열리는 주소인지는 확인하지 않았다. sub-agent 모델은
JARVIS_FOCUS__MODEL로 메인과 따로 정할 수 있다(네 영역 공통) - 재현:
uv run python -m scripts.experiments.agent_turns focus --task "<작업>"(작업마다),python scripts/experiments/focus_reference_check.py <references.md>