Models & Algorithms•SOTAAZ Lab••EN

DeepSeek의 코딩 에이전트는 누구의 말을 들을까: GitHub 댓글 하나를 코드로 따라가 봤습니다

DeepSeek의 코딩 에이전트 dsh-superagent는 GitHub 댓글을 보고 일합니다. 기본 설정에서 허용 목록에 없는 사람의 부탁은 코드 관문에서 멈추고, 그 부탁만으로는 모델을 부르지 않습니다. 그런데 이미 작업 중인 이슈에 단 댓글은 목록과 상관없이 에이전트에게 전달됩니다. 코드를 읽어 확인했고, 실행하지는 않았습니다.

DeepSeek의 코딩 에이전트는 누구의 말을 들을까: GitHub 댓글 하나를 코드로 따라가 봤습니다

DeepSeek의 코딩 에이전트는 누구의 말을 들을까: GitHub 댓글 하나를 코드로 따라가 봤습니다

앨리스와 밥이 각자 다른 GitHub 이슈에 똑같은 댓글을 답니다. AI 에이전트에게 버튼 이름을 바꿔 달라는 내용입니다. 앨리스의 댓글은 모델에게 넘어가 새 작업으로 받아들여집니다. 밥의 댓글은 새 작업을 판단하는 모델까지 가지 못하고 코드의 권한 검사에서 멈춥니다. 앨리스는 허용 목록에 있고 밥은 없기 때문입니다.

한 시간 뒤, 에이전트가 앨리스의 작업을 하고 있을 때 밥이 이번에는 앨리스의 이슈에 댓글을 답니다. 이번 댓글은 새 작업을 받을 때의 허용 목록 검사를 거치지 않습니다. 이미 진행 중인 작업의 후속 메시지로 작업자에게 전달됩니다.

이것은 DeepSeek이 공개한 dsh-superagent-prototype의 동작이고, 둘 다 문서에 명시된 설계입니다. 핵심은 새 작업을 시작할 권한과, 이미 시작한 작업에 의견을 전달할 수 있는지를 따로 다룬다는 점입니다. 이 글에서는 두 댓글이 서로 다른 경로를 타는 이유를 코드로 따라갑니다.

앨리스와 밥은 설명을 위해 저희가 만든 예시입니다. 아래 내용은 10월 31일 공개 버전(6337fc8) 코드와 문서, 테스트 파일을 읽고 구성한 설명입니다. 실제로 실행해 두 댓글에 대한 모델의 반응을 시험한 것은 아닙니다. 앨리스의 요청이 새 작업으로 받아들여졌다고 가정하고 경로를 따라갑니다.

어떤 프로젝트인가요

dsh-superagent는 GitHub 저장소의 대화를 지켜보다가 댓글로 들어온 요청을 처리하는 코딩 에이전트입니다. 요청을 받으면 코드를 고치고, 대개 풀 리퀘스트(PR)를 만들어 같은 대화에 답하도록 설계돼 있습니다. 사람이 터미널 세션을 열고 매번 일을 입력하는 사용 방식과 달리, 요청이 생기는 대화 자리를 계속 확인합니다. 여기서는 Codex나 Claude Code의 GitHub 연동 기능과 비교하지 않습니다.

에이전트를 직접 부르지 않은 대화에서도 할 일을 고를 수 있습니다. 이 기능은 해당 대화의 메시지를 모두 읽도록 설정했을 때 적용됩니다. 다만 기본값인 observe 모드에서는 무엇을 작업으로 골랐을지 기록만 합니다. 허용 목록에 있는 사람이 에이전트를 직접 불러 맡긴 작업은 이 모드에서도 진행할 수 있습니다.

DeepSeek이 10월 31일에 공개했고, 11월 3일 기준 스타가 13,632개입니다. DeepSeek의 에이전트 실행 환경인 DeepSeek Harness 위에서 돌아갑니다. README는 "참고용 연구 프로토타입이며 DeepSeek의 공식 제품이 아니다"라고 밝힙니다. 내부 버전과 코드가 다르고, 상당 부분을 코딩 에이전트가 만들었다고도 적혀 있습니다.

설계 문서는 이 프로젝트를 프레임워크이면서 에이전트와 일하는 방식이라고 소개합니다. 코드와 프롬프트에는 에이전트와 협업할 때 필요한 규칙이 자세히 들어 있습니다. 누가 일을 시킬 수 있는지, 에이전트가 언제 사람에게 말을 거는지, 끝난 작업을 어떻게 이어 가는지 같은 규칙입니다. 이 글에서는 그 규칙이 실제로 어디에서 적용되는지 살펴봅니다.

설계 문서의 첫 원칙은 사람의 주의를 되도록 적게 쓰고 가치는 되도록 많이 돌려주자는 것입니다. 댓글을 전부 지켜보는 에이전트는 자칫 성가신 존재가 되기 쉽습니다. 작업을 받을지 판단하는 검사와, 불필요한 메시지를 줄이는 프롬프트는 이 원칙을 구현하는 부분입니다.

새 요청은 아래 순서로 처리됩니다. 댓글 수집(Source), 기록과 전달(Intake), 권한 검사(Gate), 작업 배분(Main Session), 실제 수정(Worker) 단계입니다.

댓글 하나가 지나가는 길을 보여 주는 움직이는 그림입니다. Source, Intake, Gate, Main Session, Worker 다섯 칸이 나란히 있고 현재 단계의 테두리가 굵게 표시됩니다

1단계: 새 댓글에서 작성자와 호출 여부를 읽습니다

앨리스가 42번 이슈에 이렇게 씁니다.

@agent change the "Save changes" button to "Save"

에이전트는 GitHub을 주기적으로 확인해서 새 댓글을 Trigger라는 기록으로 바꿉니다. 이 이야기에서 필요한 내용은 두 가지입니다. 쓴 쪽이 사람인지, 그리고 에이전트를 불렀는지입니다. 사람인지 가리는 코드는 이렇습니다.

ts
export const isHumanLogin = (user: { login: string; type?: string }): boolean =>
  !/\[bot\]$/.test(user.login) && (!user.type || user.type === 'User')

이 함수는 아이디가 [bot]으로 끝나거나, 계정 유형 type이 있는데 User가 아니면 봇으로 분류합니다. type이 없는 경우에는 통과시킵니다. 실제 사람이 작성했는지를 인증하는 검사는 아닙니다. 이 검사에 빈틈이 있다는 것은 함수 위 주석에도 적혀 있습니다. 일반 사용자 계정으로 돌아가는 리뷰 봇은 사람으로 통과합니다. 불렀는지는 본문에서 @agent를 찾는 정규식으로 확인합니다.

ts
export const mentions = (text: string, login: string): boolean =>
  new RegExp(`(?:^|[^a-zA-Z0-9_@])@${escapeRegExp(login)}(?![a-zA-Z0-9_-])`, 'i').test(text)

이제 앨리스의 기록에는 작성자 alice, 사람으로 분류됨, 에이전트를 호출함이라는 정보가 담깁니다. 밥의 같은 댓글도 이름만 다른 기록이 됩니다. 댓글의 뜻을 판단한 코드는 아직 하나도 없습니다.

2단계: 이 이슈를 맡은 작업이 이미 있는지 확인합니다

댓글 기록은 intake.ts로 갑니다. 같은 댓글을 두 번 받아도 작업이 두 개 생기지 않도록 한 번만 저장하고, 이 이슈의 후속 메시지를 받을 작업이 이미 있는지 확인합니다. 시스템이 받아들인 작업 하나를 Ticket이라고 부릅니다. 이후에는 코드의 이름과 맞춰 Ticket이라고 쓰겠습니다.

42번 이슈에는 아직 Ticket이 없으니, 아래 줄이 댓글을 판단 단계로 보냅니다.

ts
if (!this.owners(trigger).length) this.queueForEval(trigger)

이 줄은 뒤에서 다시 나옵니다. 댓글을 후속 요청으로 처리할 Ticket이 있으면 새 작업의 접수·평가 단계를 건너뜁니다. 참고용으로만 대화를 구독한 Ticket은 여기에 해당하지 않습니다. 밥의 두 번째 댓글이 그런 경우입니다.

3단계: 새 작업을 요청할 권한을 코드로 확인합니다

앨리스와 밥의 길은 여기서 갈립니다. 새 요청을 평가하는 코드는 권한 검사부터 하고, 모델은 그다음에야 고려합니다. 이 검사에서 다음 줄이 앨리스를 통과시킵니다.

ts
if (allowed && trigger.addressed) return { admissible: true, strength: 'explicit', basis: { ...recorded, via: 'addressed', addressed: trigger.addressed } }

allowed는 아이디가 운영자의 허용 목록에 있다는 뜻이고, addressed는 에이전트를 불렀다는 뜻입니다. 둘 다 맞으면 직접 요청한 작업이라는 뜻의 explicit 등급을 받습니다. 이 등급을 받은 뒤에야 모델이 요청 내용을 분류합니다.

밥은 목록에 없으므로, 코드는 목록에 없는 사람이 에이전트를 불렀을 때 어떻게 할지 찾아봅니다. 기본값은 무시입니다.

ts
export const ADMISSION_DEFAULTS: Readonly<Record<AdmissionCell, Admission>> = { listedUnaddressed: 'inferred', unlistedAddressed: 'ignore', unlistedUnaddressed: 'ignore' }

그래서 밥의 댓글은 관문의 마지막 줄에서 not_a_member라는 사유로 끝납니다.

ts
return { admissible: false, reason: !allowed ? 'not_a_member' : receivable && admission === 'ignore' ? 'admission_ignored' : 'no_mention' }

이 함수를 부른 쪽에서는 통과한 메시지가 없으면 바로 돌아갑니다. 모델 호출은 그보다 16줄 아래에 있습니다.

ts
this.options.assessor.request(this.candidateOf(thread))

이 순서에서 좋은 점이 두 가지 생깁니다. 밥의 댓글만으로는 새 작업을 평가하는 모델을 호출하지 않으므로, 이 평가에 토큰을 쓰지 않습니다. 그리고 이 예시의 권한 검사는 작성자와 호출 여부, 운영자가 정한 설정으로 결정됩니다. 밥이 댓글에 자신에게 권한이 있다고 적어도 이 판정은 바뀌지 않습니다. 모델이 밥의 글을 절대 읽지 않는다는 뜻은 아닙니다. 같은 이슈에서 통과한 댓글과 함께 처리되면 함께 대기한 댓글로 모델에게 전달되고, 나중에 그 이슈의 작업이 시작되면 앞선 대화로 읽힐 수 있습니다. 이렇게 글을 읽는 것과 밥에게 새 작업을 시작할 권한을 주는 것은 별개입니다. 팀이 기본값을 바꿔 목록 밖 사람에게 문을 열 수는 있지만, 그것은 운영자가 바꾸는 설정이고 댓글로 바꿀 수 있는 것이 아닙니다.

아래 표는 아직 Ticket이 없는 이슈에서 기본 설정일 때 댓글이 어디까지 가는지 보여 줍니다. inferred는 직접 요청으로 인정된 것은 아니지만, 대화에서 할 일을 추론할 수 있는지 모델이 평가하는 등급입니다.

쓴 사람에이전트 호출관문의 판정그다음
목록에 있는 사람있음explicit모델이 분류합니다. 받아들이면 새 Ticket이 됩니다
목록에 있는 사람없음inferred(그 자리의 메시지를 모두 읽도록 설정한 경우)모델이 평가하지만 기본 설정에서는 의견을 기록만 합니다
목록에 없는 사람상관없음거절: not_a_member모델 호출 없이 끝납니다
봇 목록에 없는 봇상관없음거절: not_human모델 호출 없이 끝납니다

4단계: 모델이 요청을 새 작업으로 받을지 정합니다

앨리스의 댓글은 이제 메인 세션의 모델에게 도착합니다. DeepSeek은 이 세션을 오케스트레이터라고 부르며, 작업 배분만 맡깁니다. 직접 시킨 요청에 대해 프롬프트가 주는 선택지는 넷입니다. 새 작업으로 받기, 기존 Ticket에 붙이기, Ticket 멈추기, 무시하기입니다. 이 예시에서는 모델이 앨리스의 요청을 새 작업으로 받아들이고, 무엇을 할지 한 줄로 답했다고 가정합니다. 허용 목록을 통과했다고 해서 모든 댓글이 자동으로 새 작업이 되는 것은 아닙니다.

작업을 받아들이면 Ticket과 함께 Mandate가 생깁니다. 작업 권한을 기록한 것으로, 누가 이 일을 허락했고 어디까지 맡겼는지를 담습니다. 직접 시킨 요청의 범위는 메인 세션이 적은 Ticket 제목으로 정해지는데, 이 예시에서는 버튼 하나로 적었다고 하겠습니다. Mandate는 나중에 고칠 수 없고, 범위를 넓히려면 새 Ticket이 필요합니다. 이 점은 뒤에서 중요해집니다.

직접 호출하지 않은 대화는 표의 둘째 줄처럼 처리됩니다. 앨리스가 에이전트를 부르지 않고 "이 버튼 이름이 이상한데요"라고만 쓰고, 동료가 "Save면 되겠네요"라고 답한 경우입니다. DeepSeek의 평가 프롬프트는 바로 이런 대화를 받아들일 예시로 듭니다. 누가 지시하지는 않았어도 무엇으로 바꿀지가 대화에 나와 있기 때문입니다. 무시할 예시는 "우리 앱도 저쪽처럼 안 읽은 수를 보여 주면 좋겠네요"입니다. 이것은 바람일 뿐이라는 판단입니다. 모델은 세 가지를 확인해야 합니다. 이 프로젝트 이야기인지, 구체적인 변경을 원하는지, 작은 수정으로 끝날 만큼 방향이 정해졌는지입니다. 확실하지 않으면 사람에게 묻지 않고 조용히 넘깁니다. 기본 규칙에서 이렇게 추론해 받아들일 수 있는 변경은 문구 수정과 작은 화면 수정, 두 종류뿐입니다.

이처럼 직접 요청하지 않은 일을 모델이 찾아냈더라도, 기본 observe 모드에서는 실제 작업을 시작하지 않습니다. 이 코드가 수락을 무시로 바꾸고, 원래 무엇을 했을지만 적어 둡니다.

ts
if (strength === 'inferred' && settings.eval.mode !== 'auto') {
  const proposal = { title, scope, worktree_name: name }
  return { kind: 'ignore', audit: { ...audit, would_accept: true, proposal }, reason: { code: 'observed', values: { text: assessment.reason } } }
}

운영자는 이 기록을 검토한 뒤 auto 모드로 바꿔, 대화에서 추론한 일도 실제로 맡길지 결정할 수 있습니다. 이 제한은 아무도 직접 시키지 않은 일에만 걸립니다. 앨리스처럼 직접 부탁한 요청은 막지 않습니다.

5단계: 전용 작업자가 명령별 승인 없이 수정합니다

Ticket에는 전용 세션인 작업자(Worker)와 전용 폴더가 붙습니다. 폴더는 운영자가 정한 준비 명령으로 만드는데, 보통 git worktree를 씁니다. 권한은 한 줄로 정해집니다.

ts
ctx.permissionPresets.set(result.agent.session, 'danger-full-access')

작업자는 명령마다 승인을 묻지 않고, 운영자의 OS 계정으로 명령을 실행합니다. 전용 폴더와 명령 제한 래퍼가 있기는 하지만, 문서는 이것들이 보안 경계가 아니라 기본값일 뿐이라고 분명히 적습니다. 절대 경로로 프로그램을 실행하면 막지 못한다는 설명까지 있습니다.

작업 중에도 사람의 말이 닿게 하는 장치가 두 가지 있습니다. 셸 명령이 5초를 넘으면 백그라운드로 옮겨집니다. 작업자가 긴 빌드의 종료만 기다리며 멈춰 있지 않고, 다음 단계에서 새 메시지를 받을 수 있게 하려는 설계입니다. 작업 슬롯이 다 차도 사람의 메시지를 위한 슬롯이 하나 더 있어서, 새 작업이 몰려도 진행 중인 작업에 말을 걸 수 있습니다.

작업 중에는 불필요한 진행 보고를 하지 않도록 프롬프트에 지시합니다. 프롬프트에 실린 완료 메시지 예시는 "Fix submitted: <actual PR link>, ready for review." 한 줄입니다. 무엇이든 보내기 전에는 프롬프트의 지시대로 이런 시험을 하게 됩니다.

Before sending, ask of each clause: would deleting this prevent the human from understanding the result or taking the required action?

문장의 각 부분을 지웠을 때 사람이 결과를 이해하거나 필요한 조치를 하는 데 지장이 있는지 묻는 규칙입니다. 지장이 없으면 지웁니다.

말을 거는 때는 정해져 있습니다. 설계 문서에 따르면 요청을 받았을 때 한 번 답하고, 그 뒤로는 다음 상황에 다시 쓰도록 합니다. 결과가 준비됐을 때, 사람의 결정이 필요할 때, 끝낼 수 없을 때입니다. 프롬프트는 여기에 더해 직접 받은 질문의 답과 리뷰 의견을 거절한 이유도 허용합니다. 실패했다는 사실만으로 진행 보고를 보내지는 말라고 합니다. 프롬프트는 에이전트가 스스로 해결할 수 있으면 조용히 계속하라고 지시합니다. 사람이 나서야 할 때의 메시지 예시는 이렇습니다.

The bot cannot read this document, so the request cannot be confirmed. Please give the bot read access.

문서를 읽을 수 없어서 요청을 확인하지 못했으니 읽기 권한을 달라는 내용입니다. 무엇이 막혔는지와 사람이 할 일만 있습니다. 결정이 필요할 때는 구체적인 질문 하나를 구조화된 형식으로 보내고, 그 질문과 상관없는 일은 계속할 수 있습니다. 구조 문서에 따르면 질문은 메인 세션으로 바로 넘어가고, 평범한 진행 상황은 Ticket에 남습니다.

프롬프트는 GitHub에 쓰는 모든 메시지를 "This is by an automated agent, not a human."로 시작하도록 지시합니다. 자기 계정으로는 자동 병합을 켜지 않으며, 병합 도구를 따로 지정하지 않으면 모든 PR을 사람이 병합합니다. 다만 이것들은 프롬프트의 지시이고 코드가 강제하지는 않습니다.

이 지시대로 작업이 끝나면 앨리스는 수정 PR과 짧은 완료 메시지를 받습니다. 여기까지가 새 작업을 받아 처리하는 경로입니다.

이제 밥이 다시 댓글을 답니다

작업자가 앨리스의 일을 하는 동안 밥이 42번 이슈에 씁니다.

@agent also change "Cancel" to "Close"

앨리스의 요청을 받아들일 때 Ticket은 42번 이슈를 구독했습니다. 이후 수집 대상에 들어오는 그 이슈의 메시지를 해당 작업자에게 전달한다는 뜻입니다. 그래서 2단계에서 이 반복문이 Ticket을 찾아냅니다.

ts
for (const ticketId of thread.subscriptions) {
  const ticket = this.repo.tickets.get(ticketId)
  // Only a person's message on a non-context thread resumes an ended ticket; bots and CI do not (the Executor re-reads
  // threads when it resumes), and a human-stopped ticket receives nothing because its Mandate was withdrawn.
  const followed = !!thread.context?.includes(ticketId)
  const resumes = trigger.human && ticket?.ended?.by !== 'human' && !followed
  if (!ticket || (!liveTicket(ticket) && !resumes)) continue
  reached.set(ticketId, { ticket, context: followed && (reached.get(ticketId)?.context ?? true) })
}

여기에는 허용 목록을 보는 코드가 없습니다. 밥의 메시지를 후속 요청으로 처리할 Ticket이 있으므로, 2단계의 코드는 새 작업을 받는 권한 검사와 메인 세션의 평가로 보내지 않습니다. 작업자 모델이 메시지를 읽고 판단하는 과정은 남아 있습니다. 메시지는 이런 형식으로 작업자에게 갑니다.

ts
return { trigger_id: trigger.id, from: trigger.actor.id, thread: threadKey(trigger.threads[0]!), at: trigger.at,

작업자에게 보이는 것은 from: bob이고, 밥이 목록에 있는지 알려 주는 항목은 없습니다.

작업이 끝난 뒤에도 마찬가지입니다. 앨리스의 PR이 병합되고 일주일 뒤에 밥이 댓글을 달면, 위 반복문에서 재개 후보가 됩니다. 원래 작업 권한이 여전히 유효한지도 확인한 뒤 Ticket을 재개합니다. 권한 문서에도 사람의 메시지는 새 평가나 결정 없이 작업을 다시 시작시킬 수 있고, 누가 보냈는지는 따지지 않는다("regardless of who sent it")고 적혀 있습니다. 재개 전에 도는 검사도 처음 만든 Mandate, 즉 앨리스가 허락한 내용을 볼 뿐 새로 말을 건 사람은 입력으로 받지 않습니다. 이 검사를 통과하지 못하면 Ticket은 끝난 채로 남고 메시지는 판단 단계로 갑니다.

사람의 댓글이 작업자에게 가는 두 경로를 그린 그림입니다. 이슈에 Ticket이 없으면 허용 목록 관문과 메인 세션을 지납니다. Ticket이 이미 따라가고 있으면 구독 경로로 가며 허용 목록 검사와 평가가 없습니다

DeepSeek은 이것을 일부러 골랐습니다. 채택하지 않은 아이디어 목록에 구독으로 들어온 메시지마다 새 요청의 권한 검사를 반복하는 방안("Admission checks on Subscriptions")이 들어 있고, 이유도 적혀 있습니다. 구독은 이슈 같은 대화 자리 전체에 걸리며, 권한과 맥락을 나누는 것은 Mandate의 몫이라는 설명입니다. 쉽게 말해 밥의 메시지는 앨리스가 이미 허락한 일에 참고할 정보로 취급되고, 새 권한을 만들지 않습니다. 평범한 작업이라면 합리적인 선택입니다. 목록에 없는 리뷰어도 PR의 진짜 버그를 짚어 줄 수 있고, 작업자는 그 말을 들어야 합니다.

밥을 막는 장치는 일부는 코드에 있고, 일부는 프롬프트에만 있습니다.

코드는 밥의 중단 요청부터 제한합니다. 밥은 작업을 멈출 수 없습니다. 테스트를 보면 목록 밖 사람의 중단 요청은 stopNotExplicit로 거절되고, 목록에 있는 사람의 중단은 성공합니다. 작업자가 돌려보낸 밥의 요청은 판단 단계로 가지만 inferred로 판정되므로 기본 모드에서는 새 Ticket을 시작하지 못합니다. 사람이 멈춘 Ticket도 다시 열 수 없는데, 반복문의 ticket?.ended?.by !== 'human' 조건이 그 경우를 막습니다. 봇이 끝난 Ticket을 깨우지 못하는 것은 trigger.human 조건 때문입니다.

요청이 원래 작업 범위에 들어가는지는 프롬프트를 받은 작업자 모델이 판단합니다. 밥의 요청을 처리할지는 작업자가 정합니다. Ticket 범위 안의 요청은 처리하고, 범위를 넘는 요청은 돌려보내라는 지시입니다. 범위는 버튼 하나였고 밥은 다른 버튼을 말했으니, 돌려보낼 것으로 예상합니다. "s를 소문자로 해 주세요" 같은 요청은 분명히 범위 안이고, 프롬프트대로라면 처리합니다.

이 후속 메시지 경로에서는 원래 맡긴 범위를 지키는 판단을 작업자 모델에 맡깁니다. 그 작업자는 명령별 승인 없이 실행합니다. 비공개 팀 저장소에서는 댓글 작성자를 팀 내부로 제한할 수 있습니다. 공개 저장소에서는 에이전트가 작업 중인 이슈에 누구나 댓글을 달 수 있고, "수정을 확인하려면 이 명령을 실행해 주세요"라고 요청할 수도 있습니다. 코드는 그 댓글을 작업자에게 전달합니다. 프롬프트는 이런 메시지에 권한이 없다고 적고, 안전 장치와 권한을 우회하지 말라고 지시합니다. 작업자가 실제로 어떻게 하는지는 모델과 프롬프트에 달렸고, 저희는 시험하지 않았습니다.

저희가 읽은 영어 번역에 따르면 DeepSeek의 글도 슈퍼에이전트는 작고 신뢰할 수 있는 범위에서만 운영해야 한다고 말합니다. 이 코드와 함께 읽으면 그 말의 뜻이 구체적으로 보입니다. 허용 목록뿐 아니라 작업 중인 이슈에 누가 댓글을 쓸 수 있는지도 운영 범위를 정할 때 고려해야 한다는 뜻으로 읽힙니다.

작업 하나가 이슈와 PR을 끝까지 따라갑니다

밥의 댓글이 들어온 구독 경로는 작업의 맥락을 이어 주는 장치이기도 합니다. 이번에는 앨리스 쪽에서 보겠습니다.

PR을 여는 작업자에게는 Ticket이 그 PR도 구독하게 하라는 지시가 있습니다. 리뷰어가 PR에 의견을 달면 새 작업이 생기지 않고, 그 버튼을 고친 작업자에게 의견이 전달됩니다. Ticket마다 전용 세션과 폴더가 있으니, 작업자는 이미 고친 파일과 브랜치에서 바로 이어서 일합니다.

PR이 병합되면 Ticket은 끝나지만 Mandate와 구독은 남습니다. 일주일 뒤에 앨리스가 그 PR에 "Save 말고 save로 해 주세요"라고 쓰면 같은 Ticket이 다시 열리고, 작업자는 메시지와 함께 이 안내문을 받습니다.

ts
const RESUMED = 'This ticket ended earlier and the message that follows, on its thread, resumed it under the same mandate. If it asks for '
  + 'more within the original scope, inspect the worktree, git history and PR state and continue; if it asks for work beyond that scope or '
  + 'for another ticket, hand it back with orchestrator_trigger_handback and report dropped; if it needs nothing, such as thanks, '
  + 'report dropped without a message. Report ready when the follow-up is done. Do not repeat completed work.'

범위 안의 요청이면 폴더, git 기록, PR 상태를 확인하고 이어서 하되 이미 끝낸 일은 반복하지 말라는 내용입니다. 범위를 넘으면 돌려보내고, 고맙다는 인사처럼 할 일이 없는 메시지면 답하지 말고 끝내라고도 합니다.

Ticket 하나의 수명을 그린 그림입니다. 이슈에서 Ticket이 생기고, PR이 열리고, 병합되어 끝나고, 사람의 댓글로 다시 열립니다. Mandate와 구독은 Ticket에 남고, 폴더와 로컬 브랜치, PR은 dsh-superagent가 지우지 않습니다

폴더와 브랜치에 대해서는 단서가 있습니다. 쓰던 Ticket의 폴더나 로컬 브랜치를 지우는 코드는 찾지 못했고, 프롬프트도 폴더를 지우지 말라고 합니다. 다만 실행해 보지는 않았고, 병합된 브랜치를 자동으로 지우도록 설정한 저장소라면 원격 브랜치는 지워집니다.

작업을 이어 가는 데 필요한 정보는 모델의 대화 기록 외에도 남습니다. Ticket에는 작업 권한과 구독이 있고, 폴더와 브랜치에는 수정 내용이 있습니다. DeepSeek은 Ticket 상태를 세션 상태에 맞추는 방식을 채택하지 않았습니다. Ticket은 다른 쪽에 넘겨지거나, 다시 시작되거나, 여러 작업자를 거칠 수 있기 때문이라고 적었습니다. 사람의 재시도 요청은 같은 세션, 폴더, 브랜치, PR에서 이어 갑니다. 서버 재시작 후 작업자에게 주는 안내도 있습니다. 바깥에 영향을 주는 동작을 다시 하기 전에 폴더, git 기록, GitHub을 먼저 확인하라는 내용이고, 이미 끝낸 바깥 작업은 되풀이하지 말라고도 적혀 있습니다.

앨리스가 일주일 뒤에 단 댓글이 지나가는 경로와 앞에서 본 밥의 댓글이 지나간 경로는 같습니다. 이 전달 경로에서는 둘을 구분하지 않고, 범위를 판단하는 일은 작업자의 몫입니다.

에이전트를 팀원으로 둔다는 것

DeepSeek은 용어 정의에서 에이전트를 "자동화된 팀원(automated team member)"이라고 부릅니다. 프롬프트 폴더 이름도 prompts/team/이고, 메시지는 동료에게 말하듯 짧게 쓰라고 합니다. 새 동료가 오면 정해 주는 것들을 이 프로젝트가 어떻게 정했는지 표로 모았습니다.

정할 것dsh-superagent의 방식근거
어떤 계정으로 일하는가전용 GitHub 계정을 씁니다설치 안내
누구의 부탁을 받는가새 작업은 목록에 있는 사람만 시작합니다. 따라가는 이슈에서는 누구의 메시지든 작업자에게 갑니다설치 안내, 3단계 코드
어디까지 혼자 하는가작은 작업은 조사부터 PR까지 하고, 설계 결정이 필요하면 질문합니다작업 프롬프트, 설계 문서
누가 검토하고 병합하는가리뷰어를 대신해 승인하거나 병합하지 않고, 병합 도구를 지정하지 않으면 사람이 병합합니다전달 프롬프트, 같은 프롬프트
어디에 보고하는가요청이 나온 대화 자리에 보고합니다소통 프롬프트, 답글 코드

표에서 새 작업을 누가 시작하는지와 답글이 어느 대화로 가는지는 코드가 정합니다. 혼자 진행하는 범위와 병합은 프롬프트의 지시입니다. 사람이 병합해야 한다는 조건을 강제하려면, 프롬프트 외에도 GitHub 쪽에서 에이전트 계정의 권한과 브랜치 보호 규칙을 설정해야 합니다.

사람 동료와 다른 점도 분명히 합니다. 프롬프트는 GitHub에 쓰는 메시지마다 자동화된 에이전트라고 먼저 밝히라고 지시합니다. 책임도 에이전트에게 넘기지 않습니다. 설계 문서는 에이전트의 메시지 하나하나가 동료의 주의를 쓰며 그 책임은 운영자에게 있다고 적습니다. 그래서 좁게 시작하고, 시끄러우면 메시지를 더하지 말고 프롬프트를 줄이거나 멈추고, 양을 늘리는 데 쓰지 말라고 합니다.

DeepSeek이 밝힌 수치

DeepSeek은 글에서 벤치마크 결과를 공개했습니다. 저희는 X에 올라온 글을 직접 열지 못해서, 영어 번역본과 기계 번역이라고 밝힌 요약 글을 거쳐 확인했습니다. 저희가 재현한 수치는 하나도 없습니다.

실험은 그룹 채팅과 자동 빌드·테스트(CI) 메시지 약 300건을 기록된 순서대로 다시 입력하는 방식입니다. 그 안에 작업 요청이 약 20건 있고, 설정마다 5번씩 돌렸습니다. 실제 코드를 고치는 대신 정해진 시간 동안 실행되는 모의 도구를 썼습니다. 따라서 이 실험은 요청을 얼마나 빨리 배분하고 처리하는지 평가하며, 생성된 코드의 품질은 평가하지 않습니다. 메시지 간격과 작업 시간은 4분의 1로 줄였다고 하며, DeepSeek도 예비 참고치라고 부릅니다. README는 공개본이 그 글에서 설명한 구현과 같지 않다고 적고 있으니, 이 수치를 공개된 코드의 성능으로 읽으면 안 됩니다. 표의 p50은 요청 처리 시간의 중앙값이고, p95는 요청의 95%가 그 안에 끝난 시간입니다.

설정p50(분)p95(분)캐시 읽기 토큰
슈퍼에이전트12.415.72,323만
슈퍼에이전트, 후속 메시지를 작업자에게 바로 보내지 않음12.615.53,177만
세션 하나, 중립 프롬프트20.429.5605만
세션 하나, 위임 먼저 지시13.829.01,082만

세션 하나에 중립 프롬프트를 쓴 경우와 비교하면 처리 시간의 p95가 거의 절반(15.7분과 29.5분)인 대신, 캐시 읽기 토큰은 약 3.8배(2,323만 ÷ 605만)입니다. 밥의 후속 댓글을 작업자에게 바로 보낸 것은 첫째 줄의 구성입니다. 둘째 줄은 이 직접 전달을 끈 비교 조건입니다. 후속 메시지를 작업자에게 바로 보내면 이 비교에서 p50·p95는 거의 달라지지 않지만 캐시 읽기 토큰은 약 27%(3,177만에서 2,323만으로) 줄어듭니다.

써 보시거나 직접 만드신다면

아래는 코드를 실행해 얻은 결론이 아니라, 문서를 읽고 정리한 운영 제안입니다. 설치 안내는 에이전트 전용 GitHub 계정을 쓰고 본인 계정은 쓰지 말라고 합니다. 저희라면 비공개 저장소와 중요한 자료가 없는 컴퓨터에서 시작하고, 허용 목록에는 소수만 등록하겠습니다. 대화에서 추론한 작업은 observe 모드로 기록만 하게 두고, 병합 도구는 지정하지 않겠습니다. 사람이 PR을 병합하도록 GitHub 계정 권한과 브랜치 보호도 함께 설정하겠습니다. observe를 켜 둬도 직접 요청한 작업은 실행되므로, 전체 실행을 멈추는 모드로 생각하면 안 됩니다.

에이전트를 직접 만드신다면 가져다 쓰기 쉬운 아이디어가 다섯 가지 있습니다.

  • 새 작업을 시작할 권한은 모델을 부르기 전에 코드로 확인합니다. 허용되지 않은 요청을 평가하는 데 토큰을 쓰지 않고, 댓글 내용만으로 권한 검사를 바꾸지 못하게 합니다.
  • 대화에서 스스로 일을 고르는 기능은 기록 모드부터 운영합니다. 실제 작업을 시작하기 전에 어떤 요청을 골랐을지 검토할 수 있습니다.
  • 긴 명령이 에이전트의 메시지 처리를 막지 않게 합니다. 몇 초 뒤 백그라운드로 옮기면 사람의 메시지가 들어올 틈이 생깁니다.
  • 메시지를 보내기 전에 불필요한 문장을 지우도록 지시합니다. 사람이 결과를 이해하거나 다음 행동을 하는 데 필요한 내용만 남기는 규칙입니다.
  • 작업 권한과 진행 상태를 Ticket에 기록합니다. 세션이 바뀌어도 권한, 구독, 폴더, PR이 남아 있으면 다시 이어 갈 수 있습니다.

실제로 돌려 봐야 말씀드릴 수 있는 것이 세 가지 남았습니다. 맞는 일을 고르는지, 실제 저장소에서 비용이 얼마나 드는지, 낯선 사람의 댓글에 작업자가 어떻게 반응하는지입니다.

더 많은 콘텐츠를 받아보세요

SNS에서 새로운 글과 튜토리얼 소식을 가장 먼저 받아보세요

이메일로 받아보기