4과: 도구와 실행: 에이전트가 진짜로 손을 움직이게 하기
한 줄 요약: 에이전트는 「생각」만 할 수 있어서는 안 되고 「실행」도 할 수 있어야 합니다——DSH에서는 모델이 「어떤 도구를 어떤 인자로 호출할지」 선언하는 일만 담당하고, 도구 레지스트리
ctx.tools가 디스패치를 담당하며, bash·pty·subprocess 같은 교체 가능한 실행 백엔드가 실제로 손을 움직이고, 결과가 모델 컨텍스트로 돌아와 다음 사고 라운드가 시작됩니다.
1. 사용자 스토리: Bash 한 번에서 여러 단계에 걸친 터미널 세션까지
개념은 잠시 접어 두고 실제 작업을 봅시다: 「이 프로젝트의 최근 변경 사항을 확인하고 테스트를 한번 돌려 줘.」
첫 번째 라운드: 평범한 명령 하나
모델은 「생각」한 끝에 직접 손을 대지 않고 도구 호출을 선언하기로 결정합니다:
{
"name": "bash",
"arguments": {
"command": "git log --oneline -3",
"description": "Show last 3 commits"
}
}
주목할 점: 모델이 실제로 키보드를 두드린 것이 아니라 「bash를 호출하겠다, 인자는 이렇다」라고 말했을 뿐입니다. 프레임워크는 이 선언을 받으면:
- 도구 레지스트리
ctx.tools가 인자를 검증합니다; - 호출을 실행 파이프라인에 넣습니다;
- bash 실행기가 실제로
bash -c "git log --oneline -3"를 실행합니다; - 결과가 텍스트로 묶여 모델 컨텍스트로 돌아가며, 끝에는
[exit code: 0]이라는 표시가 붙습니다.
모델은 결과를 보고 「생각」을 이어 갑니다——커밋을 요약할 수도 있고, 다음 호출을 발행할 수도 있습니다.
두 번째 라운드: 오래 걸리는 작업
명령이 오래 걸리는 경우(예: 「테스트를 전부 돌려 줘」), 모델은 run_in_background: true 라는 인자를 추가할 수 있습니다. 이 호출은 블로킹하며 기다리지 않고 즉시 반환됩니다:
started background job <id>
명령은 백그라운드에서 계속 실행됩니다. 모델은 먼저 다른 일을 하다가, 나중에 job_output 으로 출력을 읽고, job_list 로 어떤 작업이 있는지 보고, job_kill 로 더 이상 필요 없는 작업을 멈춥니다. DSH에서 백그라운드 작업은 범용 백그라운드 잡 런타임 ctx.jobs 에 등록되며, 소유권과 정리 기록이 모두 남습니다——아무도 관리하지 않는 「고아 프로세스」가 되지 않습니다.
세 번째 라운드: 「현장감」이 필요한 작업
「의존성을 설치하고, 컴파일하고, 그다음 단위 테스트를 실행해」——이 세 단계에는 순서가 있고, 게다가 같은 작업 현장을 공유하고 싶습니다: 이전 단계의 현재 디렉터리, 환경 변수, 심지어 대화형 입력까지 다음 단계에서도 그대로 남아 있기를 바랍니다.
일반적인 bash 호출은 매번 초기화됩니다: 매번 새 셸에서 실행되며, 호출 사이에 상태가 보존되지 않습니다. 그래서 이때 모델은 다른 도구로 바꿉니다——영속 터미널을 여는 것입니다:
terminal_open: 터미널 세션을 열고 session id를 얻습니다;terminal_send: 그 안에 명령을 보냅니다(예:npm install);terminal_read: 터미널 출력을 읽어 옵니다;- 몇 단계가 지나면
terminal_close: 다 쓰면 닫습니다.
세션이 열려 있는 한 단계 사이의 상태는 계속 보존됩니다——이것이 「여러 단계에 걸쳐 세션을 유지한다」는 것입니다.
💡 세 가지 시나리오의 공통점: 모델은 난내 「무엇을 원하는지 말하는」 일만 하고, 실제로 손을 움직이는 것은 백엔드입니다. 단계를 넘나드는 상태는 영속 터미널 같은 백엔드가 저장을 담당합니다.
2. 도구 레지스트리 ctx.tools: 모델의 「설명서」와 실행 파이프라인
모델은 세상에 어떤 도구가 있고 각각을 어떻게 쓰는지 어떻게 알까요? 답은: 도구 레지스트리가 각 도구를 한 장의 「설명서」로 번역합니다——JSON Schema로 도구의 이름, 용도, 인자를 기술합니다. 모델은 설명서를 보고 「아, bash라는 도구가 있고 command와 description을 넘겨야 하는구나」라고 알게 됩니다.
2.1 모델이 보는 설명서: bash 도구의 실제 schema
이것은 DSH 저장소에 있는 bash 도구의 실제 schema입니다(모델 측에서 보이는 완전한 형태):
{
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The bash command to execute."
},
"description": {
"type": "string",
"description": "Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\"."
},
"timeoutMs": {
"type": "number",
"description": "Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry."
},
"workdir": {
"type": "string",
"description": "Working directory for this command. Defaults to the session workspace; a relative path is resolved against it."
},
"run_in_background": {
"type": "boolean",
"description": "Run in the background and return a job id immediately (collect with job_output, stop with job_kill). No timeout applies."
}
},
"required": [
"command",
"description"
]
}
(출처: docs/tool-catalog.zh.md 의 bash 도구 장, schema는 packages/shell/tool-bash/src/index.ts 에서 생성)
required 에는 command 와 description 만 있다는 점에 주목하세요——모델은 최소한 「무엇을 실행할지」와 「이것이 무엇을 하는지에 대한 한 줄 설명」을 명확히 말해야 하며, 나머지 인자(타임아웃, 작업 디렉터리, 백그라운드 실행)는 모두 선택 사항입니다.
2.2 레지스트리 그 자체: ctx.tools
DSH에서 레지스트리는 컨텍스트 안의 ctx.tools 서비스이며, 몇 가지 핵심 작업을 제공합니다:
ctx.tools.register(definition): 도구를 등록합니다——「설명서」(schema)와 「실행기」(execute 함수)를 묶습니다;ctx.tools.schemas(scope): 현재 스코프에서 보이는 모든 schema를 반환합니다——이것이 모델이 매 요청마다 보는 「설명서 모음」입니다;ctx.tools.guard(guard): 가드를 등록합니다——호출이 실제로 실행되기 전에 허용/거부 판단을 합니다.
도구 플러그인이 등록되면 schema가 시스템 프롬프트 조립에 자동으로 흘러 들어가고, 모델은 다음 요청 라운드에서 그것을 보고 호출할 수 있게 됩니다.
2.3 실행 파이프라인: 한 번의 호출의 일생
도구 호출은 「바로 실행」처럼 단순하지 않고, 하나의 파이프라인 전체를 거칩니다. 저장소 문서의 원문:
「도구 플러그인은 각자의 schema와 실행기를 등록한다; agent loop(에이전트 루프)는 각 호출을 차례로
tools/pre-execute(확장 가능한 허용/거부 게이트) → 등록된 단조 가드 →tools/execute(타임아웃/재시도/메트릭 플러그인을 위한 래핑 디스패치 계층) →tools/post-execute(결과 검사/교체, 컨텍스트 첨부) → 정의가 소유한finalizeContent경계 → 관측 전용tools/result알림을 거치게 한다.」—— 출처:
packages/core/tools/README.zh.md
쉬운 말로 풀면:
| 단계 | 하는 일 | 일상 비유 |
|---|---|---|
tools/pre-execute | 허용/거부/확인 게이트(권한, 승인, 샌드박스 훅이 모두 여기에 매달림) | 입장 전 보안 검색대 통과 |
| 단조 가드 | 도구 소유자가 정한 거부 정책. 한 번 거부되면 이후 단계에서 뒤집을 수 없음 | 주인의 「출입 거부」 |
tools/execute | 래핑 디스패치 계층: 타임아웃, 재시도, 메트릭이 모두 이 층에 있음 | 계산대 옆의 「시간 초과 알림」 |
tools/post-execute | 결과 검사/교체, 차단, 추가 컨텍스트 첨부 | 포장할 때 물건이 맞는지 확인 |
finalizeContent | 도구 정의가 소유한 마지막 콘텐츠 가공. 최종 콘텐츠만 교체 가능 | 마지막 라벨 붙이기 |
tools/result | 관측 전용 최종 결과 알림 | 출입구의 감시 기록 |
핵심: 파이프라인은 「심(seam, 이음매)」 설계입니다——권한, 승인, 타임아웃, 재시도 같은 횡단 관심사가 모두 고정된 이벤트에 매달려 있고, 도구 본체는 그것들을 신경 쓸 필요가 없습니다. 어느 단계든 교체하거나 확장할 수 있으며, 도구 자체의 execute 함수는 한 줄도 바꿀 필요가 없습니다.
模型只声明要什么工具,注册表调度,后端执行——每个环节都可替换
3. 실행 백엔드: 전부 교체 가능한 「심」
레지스트리는 「디스패치」를 담당하지만, 실제로 손을 움직이는 것은 실행 백엔드입니다. DSH는 가장 흔한 세 종류의 실행 백엔드를 모두 교체 가능한 심으로 만들었습니다——모델 측에서 보이는 도구 인터페이스는 그대로이고, 아래쪽 구현은 마음대로 바꿀 수 있습니다.
3.1 bash: 포그라운드와 백그라운드
ctx.shell 은 bash 실행기 심(이음매)의 표준 규약이며, 모델 측의 bash 도구는 이 심에 등록되어 있습니다:
- 포그라운드: 명령이 끝날 때까지 기다렸다가 stdout/stderr와 종료 코드를 가져옵니다. bash 도구의 규약은 「매 호출이 새 셸에서 실행된다: 호출 사이에 어떤 상태(cwd, 변수, 함수)도 보존되지 않는다.
workdir를 전달하고cd는 사용하지 말 것」(출처:docs/tool-catalog.zh.md)입니다; - 백그라운드:
run_in_background: true, 즉시 job id를 반환하고 범용 작업 런타임ctx.jobs가 이어받습니다.
실행기는 누구일까요? 배포 설정을 볼 일입니다: dsh-bash-local 은 로컬 subprocess로 실행하고, dsh-bash-sandbox 는 먼저 샌드박스를 한 겹 씌운 뒤 실행하고, pwsh-local 은 PowerShell 의미 체계로 실행합니다. 실행기를 바꿔도 모델 측의 그 무엇도 바꿀 필요가 없습니다.
3.2 pty: 소유자로 격리된 영속 터미널
ctx.terminals 는 영속적이고 소유자 범위로 한정된 터미널 세션을 제공합니다. 저장소 문서의 원문:
「PTY는 Pseudo-Terminal(유사 터미널)의 약자입니다. 이 기능은 영속적이고 소유자 범위로 한정된 터미널 세션을 제공하며, 도구 호출을 넘나들어 상태를 보존해야 하거나 대화형 stdin을 사용하는 워크플로에 적합합니다.」
—— 출처:
packages/terminal/README.zh.md
모델에 6개의 도구를 공개합니다: terminal_open, terminal_send, terminal_read, terminal_signal, terminal_close, terminal_list. 특히 주의해야 할 것은 소유권 격리입니다: 모든 작업에서 완전히 동일한 발기 Agent(에이전트)를 요구합니다——모델이 다른 agent의 터미널 id를 알게 되더라도 그 터미널을 조작할 수 없습니다.
PTY는 단발 bash와 파일 시스템 도구의 보완이며, 후자의 더 엄격한 작업 단위 규약을 대체하지 않습니다: 일회성의 작은 작업에는 bash를 쓰고, 영속적인 현장이 필요한 워크플로에만 터미널을 엽니다.
3.3 subprocess: 관리되는 프로세스 트리
ctx.subprocess 는 더 낮은 계층의 공유 프로세스 기반입니다: 실행 파일 탐색, 명확한 명세를 가진 관리되는 자식 프로세스 트리, 그리고 PTY 할당과 포그라운드 프로세스 그룹을 담당하는 저수준 터미널 프로세스 프리미티브. bash 실행기와 PTY 셸 백엔드는 모두 그 위에 구축되어 있습니다.
「관리된다」는 것이 무슨 뜻일까요? 프로세스의 생명주기를 서비스가 관리합니다——spawn된 프로세스 트리, 핸들 수명, 시그널 전송, 먼저 종료하고 기다리는 리소스 해제까지 모두 명확한 규약이 있습니다. 소비자는 「프로세스의 의미」(예: 「bash 명령 하나」)만 정의하면 되고, 바퀴를 다시 발명할 필요가 없습니다.
3.4 왜 「심」이라고 부르는가
2과의 관점으로 돌아가 봅시다: DSH는 「할 수 있다」는 것을 세 개의 층으로 나눴습니다——모델 선언, 레지스트리 디스패치, 백엔드 실행. 각 층 사이의 인터페이스는 고정되어 있고(schema + 파이프라인 이벤트), 구현은 교체 가능합니다. 이것이 공학에서 말하는 「심」입니다: 샌드박스를 바꾸고 싶든, 실행기를 바꾸고 싶든, 타임아웃 정책을 추가하고 싶든, 심의 한쪽만 건드리면 되고 다른 쪽에는 영향이 없습니다.
4. 결과가 컨텍스트로 돌아온다: 다음 루프의 시작
도구 실행이 끝나도 이야기는 끝나지 않습니다——결과는 반드시 모델 컨텍스트로 돌아와야 합니다. 그렇지 않으면 모델은 「눈을 가린 상태」가 됩니다.
- 호출이 발행되면 세션에
tool/call이벤트가 하나 기록됩니다(실행 전에 기록); - 결과가 확정되면
tool/result이벤트가 추가됩니다——이것이 모델이 보는 유일한 결과입니다; - 결과는 텍스트 형태로 모델 컨텍스트에 들어갑니다: 명령 출력,
[exit code: N]표시, 가능한 잘림이나 오류 메시지; - 모델은 결과를 다 읽고 새로운 「생각」 라운드를 시작합니다——요약할 수도 있고, 다시 도구 호출을 발행할 수도 있습니다.
2과의 단계 구조를 기억하시나요? 생각 → 행동 → 관찰 → 다시 생각. 도구 호출은 「행동 + 관찰」이라는 한 쌍의 동작이 프레임워크에 구현된 모습입니다: 행동 = 레지스트리가 호출을 실행 백엔드로 넘기는 것, 관찰 = 결과가 컨텍스트로 돌아오는 것. 한 번의 루프가 끝나고 다음 루프가 시작됩니다——여러 단계의 작업은 바로 이렇게 한 걸음씩 완료됩니다.
그렇기 때문에 매 라운드가 안전하고 제어 가능해야 합니다: 누가 어떤 도구를 호출할 수 있는지, 명령이 샌드박스 밖의 파일을 건드릴 수 있는지, 먼저 사용자에게 물어봐야 하는지……이것이 바로 다음 과인 「샌드박스와 보안」에서 다룰 문제입니다.
핵심 정리
- 모델은 선언만 하고, 프레임워크가 손을 움직인다: 모델은 도구 호출 선언(도구 이름 + 인자)을 발행하고, 레지스트리가 디스패치하며, 실행 백엔드가 실제로 실행합니다.
- 레지스트리 ctx.tools: 각 도구를 JSON Schema 「설명서」로 번역합니다. register로 등록, schemas로 모델에 제공, guard로 가드를 설정합니다.
- 실행 파이프라인:
tools/pre-execute→ 가드 →tools/execute→tools/post-execute→finalizeContent→tools/result. 권한, 승인, 타임아웃, 재시도가 모두 고정된 심에 매달려 있습니다. - 백엔드는 모두 교체 가능한 심: bash(포그라운드/백그라운드), pty(소유자로 격리된 영속 터미널), subprocess(관리되는 프로세스 트리). 구현을 바꿔도 모델 측에는 영향이 없습니다.
- 결과는 컨텍스트로 돌아온다:
tool/result가 모델이 보는 유일한 결과가 되어 다음 「생각 → 행동 → 관찰」 라운드를 촉발하고, 여러 단계의 작업이 이로써 완료됩니다.
🚀 다음 과(4과)에서는 「샌드박스와 보안」을 다룹니다: 명령이 어떤 파일을 건드릴 수 있는지, 언제 사용자에게 권한을 요청해야 하는지——에이전트가 「손을 움직일 수 있으면서」 동시에 「함부로 움직이지 않게」 하는 이야기입니다.
셀프 테스트 · 도구와 실행
답을 모두 선택한 후 「답안 제출」을 클릭하면 정답 여부와 해설을 확인할 수 있습니다.
