gajae-code · 서브에이전트·태스크 위임 (Role Agents)
한 줄 요약
큰 작업을 메인 에이전트가 혼자 하지 않고, 역할이 정해진 부하 에이전트에게 쪼개 맡긴 뒤 한 장짜리 “영수증”만 받아오는 구조다. 왜 배우나 — 일을 병렬로 나누면서도 부모의 대화 토큰이 폭증하지 않고, “누가 손대도 되고 누가 보기만 하는지” 권한을 명찰처럼 못 박을 수 있기 때문이다.
그림
flowchart TD A[메인 에이전트가 task 도구 호출] --> G{"게이팅: 5명 이상인데 계획 없나?"} G -- "예: 계획 누락" --> R[거부하고 빠진 항목 보고] G -- "아니오: 통과" --> N["이름 부여: SwiftFalcon 같은 고유 식별자"] N --> O["순번 ID 부여: 0-Auth.1-Sub 같은 prefix"] O --> I["격리된 책상 준비: 워크트리 작업공간 분리"] I --> P["동시 실행: 정해진 인원 상한 안에서 병렬"] P --> X[각 부하가 일하고 끝날 때까지 관찰] X --> C[바뀐 부분만 캡처해 임시 브랜치에 보관] C --> M[부모 작업본에 차례로 합치기] M --> RC["영수증 생성: 압축 요약본"] RC --> S["금지 키 차단: 풀텍스트 누출 막기"] S --> A[부모는 요약본만 받음]
쉽게 풀기
회사 팀장(메인 에이전트)이 큰 프로젝트를 받았다고 생각하자. 혼자 다 하면 책상이 서류로 넘쳐난다(= 토큰 폭증). 그래서 신입 여러 명에게 나눠 맡긴다. 다만 그냥 맡기지 않는다.
-
명찰부터 붙인다 (역할 정의) — 각 부하는 마크다운 파일 한 장(
prompts/agents/*.md)으로 정의된다. 파일 맨 위 “프론트매터”가 명찰이다. 이름, 할 일 한 줄, 쓸 수 있는 도구, 생각 깊이, “손대도 됨/보기만” 같은 권한이 적혀 있다. 그 아래 본문은 그 부하의 성격(시스템 프롬프트)이다. -
네 종류 신입이 있다 — executor는 실제로 코드를 고치는 쓰기 가능 일꾼이다. architect / planner / critic은 파일을 읽고 진단·계획·심사만 하는 읽기전용 검토자다. 비유하면 한 명은 공사를 하고, 셋은 각각 설계 검토관, 작업 계획자, 최종 심사관이다.
-
별도 책상을 준다 (격리) — 부하들이 같은 서류를 동시에 고치면 엉킨다. 그래서 각자에게 원본을 복제한 격리 작업공간(워크트리)을 준다. 일이 끝나면 바뀐 부분만 떼어내 임시 폴더에 모았다가 부모 작업본에 차례로 합친다.
-
너무 많이 부르려 하면 막는다 (게이팅) — 부하를 5명 이상 동시에 부르려 하면 시스템이 “왜 병렬이어야 하지? 왜 혼자 못 하지? 서로 독립적인가?”를 적은 계획서를 먼저 요구한다. 안 적으면 거부한다.
-
보고는 한 장으로만 받는다 (영수증) — 부하가 토해낸 전체 출력물을 그대로 받지 않는다. 상태, 결과를 다시 찾아볼 수 있는 주소(
agent://<id>), 검토 의견 정도만 담은 압축 영수증을 받는다. 풀텍스트가 필요하면 그 주소로 다시 읽는다.
핵심 정리
명찰(프론트매터)에 적히는 주요 항목 — 파싱은 parseAgentFields()가 담당한다.
| 필드 | 역할 | 메모 |
|---|---|---|
name / description | 이름과 한 줄 역할 | 둘 중 하나라도 없으면 파싱 자체가 실패(null) |
tools | 허용 도구 화이트리스트 | 명시하면 결과 제출용 yield가 자동 추가됨 |
spawns | 다시 부를 수 있는 하위 에이전트 | tools에 task만 있고 미지정이면 "*"로 추론 |
thinkingLevel / blocking | 생각 깊이 / 부모가 끝까지 기다릴지 | architect는 blocking: true |
forkContext / bashAllowedPrefixes | 부모 대화 포크 여부 / 예외 bash 허용 | 읽기전용도 gjc state 등은 예외 허용 |
네 종류 신입의 계약 차이
- executor — 쓰기 가능. 도구 전체, 생각 medium,
forkContext: allowed. 산출물: 변경 파일·결정·검증 증거.- architect — 읽기전용. 생각 high,
blocking: true. 산출물: Architectural Status(CLEAR/WATCH/BLOCK) + 리뷰 권고(APPROVE/COMMENT/REQUEST CHANGES).- planner — 읽기전용. 생각 medium. 산출물: scope/steps/acceptance/risks/verification 계획.
- critic — 읽기전용. 생각 high. 산출물: OKAY / ITERATE / REJECT 판정.
- 읽기전용 3종 공통: 본문
<constraints>에서 “never write, edit, format, commit, push, or mutate files” 명시. 예외 bash는gjc ralplan --write(아티팩트는 파일이 아니라 인라인 마크다운)와gjc state만.
동작을 떠받치는 부품들
- 병렬 실행(
parallel.ts) —mapWithConcurrencyLimit가 워커풀 + 동시성 상한. 중단 시 부분결과(aborted:true) 보존, 일반 에러는 fail-fast.- 격리(
worktree.ts) —ensureIsolation이 최적 백엔드(apfs/btrfs/zfs/reflink/overlayfs 등) 선택, 불가하면 폴백. baseline → 델타 패치 →gjc/task/<taskId>브랜치 → cherry-pick으로 부모 HEAD에 순차 병합(충돌 시 멈추고 보고).- 이름 생성(
name-generator.ts) — 형용사+명사(SwiftFalcon, 약 42만 조합). 충돌 시Task<n>숫자 폴백.- 순번 ID(
output-manager.ts) —0-AuthProvider식 prefix, 중첩은0-Parent.0-Child.agent://<id>URL 안정화.- 게이팅(
spawn-gate.ts) — 임계값DEFAULT_SPAWN_THRESHOLD = 4. 초과 시SpawnPlanReceipt5필드(whyParallel/whyNotLocal/independence/expectedReceiptShape/maxInlineTokens) 제출 필수.- 영수증(
receipt.ts) —buildTaskReceipt가 압축.BANNED_RAW_TASK_KEYS(output/stdout/resultText 등)는assertNoRawTaskFields가 차단해 풀텍스트 표면 누출을 막는다.
await 타임아웃은 실패가 아니다 (AGENTS.md 44행 규약)
원문: “Subagent await timeouts are observation windows, not failure signals.” await가 타임아웃됐다고 부하를 취소하지 말 것 — 살펴보고, 독립 작업을 계속하다가, 실제로 실패/이탈/회복불능일 때만 취소하라. 런타임도 wall-clock 하드 리밋은 기본 비활성(
task.maxRuntimeMs > 0일 때만)이라 타임아웃 자체가 자동 취소를 부르지 않는다.
실제 예시
실제 architect 명찰(프론트매터):
# /home/seunghyeong/harness-work/gajae-code/packages/coding-agent/src/prompts/agents/architect.md
---
name: architect
description: Read-only architecture and code-review agent with severity-rated findings and status verdicts
tools: read, search, find, lsp, ast_grep, web_search, bash, report_finding
thinking-level: high
blocking: true
forkContext: allowed
bashAllowedPrefixes:
- gjc ralplan --write
- gjc state
---명찰을 파싱하는 규칙 — 이름/설명 없으면 실패, yield/spawns 자동 추론:
// /home/seunghyeong/harness-work/gajae-code/packages/coding-agent/src/discovery/helpers.ts
export function parseAgentFields(frontmatter: Record<string, unknown>): ParsedAgentFields | null {
const name = typeof frontmatter.name === "string" ? frontmatter.name : undefined;
const description = typeof frontmatter.description === "string" ? frontmatter.description : undefined;
if (!name || !description) {
return null; // name/description 없으면 파싱 실패
}
let tools = parseArrayOrCSV(frontmatter.tools)?.map(tool => tool.toLowerCase());
// Subagents with explicit tool lists always need yield
if (tools && !tools.includes("yield")) {
tools = [...tools, "yield"]; // tools 명시되면 yield 강제 추가
}
// Backward compat: infer spawns: "*" when tools includes "task"
if (spawns === undefined && tools?.includes("task")) {
spawns = "*";
}
// ... model/blocking/hide/forkContext/bashAllowedPrefixes 파싱 ...
}범용 위임 에이전트 task는 파일 대신 코드에 직접 박혀 있다:
// /home/seunghyeong/harness-work/gajae-code/packages/coding-agent/src/task/agents.ts
// task.md는 코드에 직접 프론트매터를 박아 임베드한다 (범용 위임 에이전트)
{
fileName: "task.md",
frontmatter: {
name: "task",
description: "General-purpose subagent with full capabilities for delegated multi-step tasks",
spawns: "*",
model: "pi/default",
thinkingLevel: Effort.Medium,
hide: true,
},
template: taskMd,
},직접 읽기전용 검토자를 만들 때 복붙용 최소 템플릿:
---
name: my-reviewer
description: Read-only reviewer that returns PASS/FAIL with file-backed evidence
tools: read, search, find, bash
thinking-level: high
blocking: true
bashAllowedPrefixes:
- gjc state
---
<identity>
You are MyReviewer. You inspect and judge. You are read-only.
</identity>
<constraints>
- Read-only: never write, edit, commit, or mutate files.
- Cite concrete files for every claim.
</constraints>
<output_contract>
**[PASS / FAIL]** + 근거 + 수정 제안.
</output_contract>쓰기 가능 일꾼이면:
tools줄을 빼서 전체 도구를 주거나 명시(이 경우yield자동 추가),forkContext: allowed,<constraints>에 “diff는 작고 되돌릴 수 있게” 류를 넣는다.
만들 때 체크리스트:
-
name+description둘 다 있는가 (없으면 파싱 자체가 실패) - 읽기전용이면
<constraints>에 mutate 금지 명시 + 필요한bashAllowedPrefixes만 화이트리스트 -
tools명시했다면yield자동 추가됨을 이해(결과 제출 경로) - 하위 위임 시키려면
spawns(또는tools에task) 설정 - 5명 이상 병렬이면
SpawnPlanReceipt5필드 채울 준비 -
<output_contract>에 판정 라벨/영수증 형태 고정(부모가 파싱) - 신규 번들 에이전트면
agents.ts의EMBEDDED_AGENT_DEFS에.mdimport 추가 - await 타임아웃을 실패로 다루지 말 것(관찰창 규약)
요약 & 셀프체크
3줄 요약:
- 메인 에이전트는
task도구로 역할이 정해진 부하들을 격리된 작업공간에서 병렬 실행하고, 끝나면 압축 영수증만 받는다. - 네 종류(executor 쓰기 / architect·planner·critic 읽기전용)는 명찰(프론트매터)로 권한과 산출 계약이 못 박혀 있다.
- 5명 초과 병렬은 계획서 제출이 강제되고, await 타임아웃은 실패가 아니라 관찰창일 뿐이다.
스스로 답해보기:
- 부하 명찰에
name만 있고description이 없으면 어떻게 되나? (힌트: 파싱 결과) - 읽기전용 검토자가 그래도 실행할 수 있는 bash 명령은 무엇이고 왜 예외인가?
- 부모는 왜 부하의 풀텍스트 출력을 직접 받지 않고 영수증만 받나? 풀텍스트가 필요하면 어떻게 하나?
연결
Codex 교차검증 보존
기존 분석 노트의 근거 파일 목록은 모두 유효하다. 핵심 소스:
task/agents.ts— 빌드타임 임베드,EMBEDDED_AGENT_DEFS,loadBundledAgents,task인라인 프론트매터prompts/agents/executor.md·architect.md·planner.md·critic.md— 4종 계약prompts/agents/frontmatter.md— 프론트매터 직렬화 Handlebars 템플릿discovery/helpers.ts—parseAgentFields(yield/spawns 추론)task/parallel.ts—mapWithConcurrencyLimit,Semaphoretask/worktree.ts— 격리 백엔드/baseline/델타패치/브랜치 병합task/name-generator.ts—generateTaskNametask/output-manager.ts—AgentOutputManagertask/spawn-gate.ts—DEFAULT_SPAWN_THRESHOLD,SpawnPlanReceipt, 게이팅task/receipt.ts—buildTaskReceipt,BANNED_RAW_TASK_KEYS, ROI/sanitizetask/executor.ts— 서브에이전트 실행 루프, AbortReason(signal/terminate/timeout), maxRuntimeMs 하드리밋AGENTS.md(44행) — “Subagent await timeouts are observation windows, not failure signals” 모든 경로 접두사:/home/seunghyeong/harness-work/gajae-code/packages/coding-agent/src/