콘텐츠로 이동

태스크 에이전트 탐색 및 선택

이 문서는 태스크 서브시스템이 에이전트 정의를 탐색하고, 여러 소스를 병합하며, 실행 시점에 요청된 에이전트를 해석하는 방법을 설명합니다.

선행 순위, 잘못된 정의 처리, 에이전트를 사실상 사용 불가 상태로 만들 수 있는 스폰/깊이 제약을 포함하여 현재 구현된 런타임 동작을 다룹니다.


태스크 에이전트는 AgentDefinition으로 정규화됩니다 (src/task/types.ts):

  • name, description, systemPrompt (유효하게 로드된 에이전트에 필수)
  • 선택적 tools, spawns, model, thinkingLevel, output
  • source: "bundled" | "user" | "project"
  • 선택적 filePath

파싱은 parseAgentFields()를 통해 프론트매터에서 이루어집니다 (src/discovery/helpers.ts):

  • name 또는 description 누락 => 유효하지 않음 (null), 호출자는 파싱 실패로 처리
  • tools는 CSV 또는 배열 허용; 제공된 경우 submit_result가 자동 추가됨
  • spawns*, CSV, 또는 배열 허용
  • 하위 호환성 동작: spawns가 없지만 toolstask가 포함된 경우 spawns*이 됨
  • output은 불투명한 스키마 데이터로 그대로 전달됨

번들 에이전트는 텍스트 임포트를 사용하여 빌드 시점에 내장됩니다 (src/task/agents.ts).

EMBEDDED_AGENT_DEFS 정의:

  • 프롬프트 파일에서 explore, plan, designer, reviewer
  • 공유 task.md 본문과 주입된 프론트매터에서 taskquick_task

로딩 경로:

  1. loadBundledAgents()parseAgent(..., "bundled", "fatal")로 내장 마크다운을 파싱
  2. 결과는 메모리 내 캐시에 저장됨 (bundledAgentsCache)
  3. clearBundledAgentsCache()는 테스트 전용 캐시 초기화

번들 파싱은 level: "fatal"을 사용하므로, 잘못된 번들 프론트매터는 예외를 발생시키고 탐색 전체를 실패하게 만들 수 있습니다.

discoverAgents(cwd, home) (src/task/discovery.ts)는 번들 정의를 추가하기 전에 여러 위치의 에이전트를 병합합니다.

  1. getConfigDirs("agents", { project: false })의 사용자 설정 에이전트 디렉터리
  2. findAllNearestProjectConfigDirs("agents", cwd)의 가장 가까운 프로젝트 에이전트 디렉터리
  3. listClaudePluginRoots(home)의 Claude 플러그인 루트와 agents/ 하위 디렉터리
  4. 번들 에이전트 (loadBundledAgents())

소스 패밀리 순서는 getConfigDirs("", { project: false })에서 가져오며, 이는 src/config.tspriorityList에서 파생됩니다:

  1. .xcsh
  2. .claude
  3. .codex
  4. .gemini

각 소스 패밀리에 대해 탐색 순서는 다음과 같습니다:

  1. 해당 소스의 가장 가까운 프로젝트 디렉터리 (발견된 경우)
  2. 해당 소스의 사용자 디렉터리

모든 소스 패밀리 디렉터리 이후, 플러그인 agents/ 디렉터리가 추가됩니다 (프로젝트 범위 플러그인 먼저, 그 다음 사용자 범위).

번들 에이전트는 마지막에 추가됩니다.

중요한 주의사항: 오래된 주석 vs 현재 코드

섹션 제목: “중요한 주의사항: 오래된 주석 vs 현재 코드”

discovery.ts 헤더 주석은 여전히 .pi를 언급하며 .codex/.gemini는 언급하지 않습니다. 실제 런타임 순서는 src/config.ts에 의해 결정되며 현재 .xcsh, .claude, .codex, .gemini를 사용합니다.

탐색은 정확한 agent.name으로 선착순 중복 제거를 사용합니다:

  • Set<string>이 이미 확인된 이름을 추적합니다.
  • 로드된 에이전트는 디렉터리 순서로 평탄화되며 이름이 처음 확인된 경우에만 유지됩니다.
  • 번들 에이전트는 동일한 집합에 대해 필터링되며 아직 확인되지 않은 경우에만 추가됩니다.

함의:

  • 동일한 소스 패밀리의 경우 프로젝트가 사용자를 재정의합니다.
  • 더 높은 우선순위의 소스 패밀리가 더 낮은 순위를 재정의합니다 (.xcsh.claude보다 앞).
  • 번들되지 않은 에이전트가 동일한 이름의 번들 에이전트를 재정의합니다.
  • 이름 매칭은 대소문자를 구분합니다 (Tasktask는 다름).
  • 하나의 디렉터리 내에서 마크다운 파일은 중복 제거 전에 파일 이름 사전순으로 읽힙니다.

유효하지 않은/누락된 에이전트 파일 동작

섹션 제목: “유효하지 않은/누락된 에이전트 파일 동작”

디렉터리별 (loadAgentsFromDir):

  • 읽을 수 없거나 누락된 디렉터리: 비어 있는 것으로 처리됨 (readdir(...).catch(() => []))
  • 파일 읽기 또는 파싱 실패: 경고 기록, 파일 건너뜀
  • 파싱 경로는 parseAgent(..., level: "warn")을 사용

프론트매터 실패 동작은 parseFrontmatter에서 옵니다:

  • warn 수준의 파싱 오류는 경고를 기록
  • 파서는 단순한 key: value 줄 파서로 폴백
  • 필수 필드가 여전히 누락된 경우 parseAgentFields가 실패하고, AgentParsingError가 발생하여 호출자에게 잡힘 (파일 건너뜀)

결과적으로: 하나의 잘못된 커스텀 에이전트 파일이 다른 파일의 탐색을 중단시키지 않습니다.

조회는 정확한 이름으로 선형 검색합니다:

  • getAgent(agents, name) => agents.find(a => a.name === name)

태스크 실행 시 (TaskTool.execute):

  1. 호출 시점에 에이전트가 재탐색됨 (discoverAgents(this.session.cwd))
  2. 요청된 params.agentgetAgent를 통해 해석됨
  3. 에이전트 누락 시 즉각적인 도구 응답 반환:
    • Unknown agent "...". Available: ...
    • 서브프로세스 실행 없음

TaskTool.create()는 초기화 시점에 탐색 결과로 도구 설명을 빌드합니다 (buildDescription).

execute()는 에이전트를 다시 탐색합니다. 따라서 세션 중간에 에이전트 파일이 변경된 경우 런타임 집합이 이전 도구 설명에 나열된 내용과 다를 수 있습니다.

구조화된 출력 가드레일 및 스키마 우선순위

섹션 제목: “구조화된 출력 가드레일 및 스키마 우선순위”

TaskTool.execute의 런타임 출력 스키마 우선순위:

  1. 에이전트 프론트매터 output
  2. 태스크 호출 params.schema
  3. 부모 세션 outputSchema

(effectiveOutputSchema = effectiveAgent.output ?? outputSchema ?? this.session.outputSchema)

src/prompts/tools/task.md의 프롬프트 시점 가드레일 텍스트는 구조화 출력 에이전트(explore, reviewer)에 대한 불일치 동작에 대해 경고합니다: 산문에서의 출력 형식 지침이 내장 스키마와 충돌하여 null 출력을 생성할 수 있습니다.

이것은 discoverAgents의 하드 런타임 유효성 검사 로직이 아닌 지침입니다.

src/task/commands.ts는 워크플로우 명령어(에이전트 정의가 아님)를 위한 병렬 인프라이지만, 동일한 전반적인 패턴을 따릅니다:

  • 먼저 기능 제공자에서 탐색
  • 선착순으로 이름별 중복 제거
  • 아직 확인되지 않은 경우 번들 명령어 추가
  • getCommand를 통한 정확한 이름 조회

src/task/index.ts에서 명령어 헬퍼는 에이전트 탐색 헬퍼와 함께 재내보내집니다. 에이전트 탐색 자체는 런타임에 명령어 탐색에 의존하지 않습니다.

에이전트는 탐색 가능하더라도 실행 가드레일로 인해 여전히 실행할 수 없는 상태일 수 있습니다.

TaskTool.executesession.getSessionSpawns()를 확인합니다:

  • "*" => 모두 허용
  • "" => 모두 거부
  • CSV 목록 => 나열된 이름만 허용

거부된 경우: 즉각적인 Cannot spawn '...'. Allowed: ... 응답.

PI_BLOCKED_AGENT는 도구 생성 시점에 읽힙니다. 요청이 일치하는 경우 실행은 재귀 방지 메시지와 함께 거부됩니다.

재귀 깊이 게이팅 (자식 세션 내 태스크 도구 가용성)

섹션 제목: “재귀 깊이 게이팅 (자식 세션 내 태스크 도구 가용성)”

runSubprocess에서 (src/task/executor.ts):

  • 깊이는 taskDepth에서 계산됨
  • task.maxRecursionDepth가 차단 기준을 제어
  • 최대 깊이 도달 시:
    • task 도구가 자식 도구 목록에서 제거됨
    • 자식 spawns 환경이 비어 있게 설정됨

따라서 에이전트 정의에 spawns가 포함되어 있더라도 더 깊은 수준에서는 추가 태스크를 스폰할 수 없습니다.

플랜 모드 주의사항 (현재 구현)

섹션 제목: “플랜 모드 주의사항 (현재 구현)”

TaskTool.execute는 플랜 모드를 위한 effectiveAgent를 계산하지만 (플랜 모드 프롬프트 앞에 추가, 읽기 전용 도구 서브셋 강제, 스폰 초기화), runSubprocesseffectiveAgent 대신 agent로 호출됩니다.

현재 효과:

  • 모델 재정의 / 사고 수준 / 출력 스키마는 effectiveAgent에서 파생됨
  • effectiveAgent의 시스템 프롬프트 및 도구/스폰 제한은 이 호출 경로에서 전달되지 않음

이것은 플랜 모드 동작 기대치를 읽을 때 알아두어야 할 구현상의 주의사항입니다.