스폰서LobeHubLobeHub자세히 알아보기
dshfind

4과: 도구와 실행: 에이전트가 진짜로 손을 움직이게 하기

한 줄 요약: 에이전트는 「생각」만 할 수 있어서는 안 되고 「실행」도 할 수 있어야 합니다——DSH에서는 모델이 「어떤 도구를 어떤 인자로 호출할지」 선언하는 일만 담당하고, 도구 레지스트리 ctx.tools 가 디스패치를 담당하며, bash·pty·subprocess 같은 교체 가능한 실행 백엔드가 실제로 손을 움직이고, 결과가 모델 컨텍스트로 돌아와 다음 사고 라운드가 시작됩니다.

1. 사용자 스토리: Bash 한 번에서 여러 단계에 걸친 터미널 세션까지

개념은 잠시 접어 두고 실제 작업을 봅시다: 「이 프로젝트의 최근 변경 사항을 확인하고 테스트를 한번 돌려 줘.」

첫 번째 라운드: 평범한 명령 하나

모델은 「생각」한 끝에 직접 손을 대지 않고 도구 호출을 선언하기로 결정합니다:

{
  "name": "bash",
  "arguments": {
    "command": "git log --oneline -3",
    "description": "Show last 3 commits"
  }
}

주목할 점: 모델이 실제로 키보드를 두드린 것이 아니라 「bash를 호출하겠다, 인자는 이렇다」라고 말했을 뿐입니다. 프레임워크는 이 선언을 받으면:

  1. 도구 레지스트리 ctx.tools 가 인자를 검증합니다;
  2. 호출을 실행 파이프라인에 넣습니다;
  3. bash 실행기가 실제로 bash -c "git log --oneline -3" 를 실행합니다;
  4. 결과가 텍스트로 묶여 모델 컨텍스트로 돌아가며, 끝에는 [exit code: 0] 이라는 표시가 붙습니다.

모델은 결과를 보고 「생각」을 이어 갑니다——커밋을 요약할 수도 있고, 다음 호출을 발행할 수도 있습니다.

두 번째 라운드: 오래 걸리는 작업

명령이 오래 걸리는 경우(예: 「테스트를 전부 돌려 줘」), 모델은 run_in_background: true 라는 인자를 추가할 수 있습니다. 이 호출은 블로킹하며 기다리지 않고 즉시 반환됩니다:

started background job <id>

명령은 백그라운드에서 계속 실행됩니다. 모델은 먼저 다른 일을 하다가, 나중에 job_output 으로 출력을 읽고, job_list 로 어떤 작업이 있는지 보고, job_kill 로 더 이상 필요 없는 작업을 멈춥니다. DSH에서 백그라운드 작업은 범용 백그라운드 잡 런타임 ctx.jobs 에 등록되며, 소유권과 정리 기록이 모두 남습니다——아무도 관리하지 않는 「고아 프로세스」가 되지 않습니다.

세 번째 라운드: 「현장감」이 필요한 작업

「의존성을 설치하고, 컴파일하고, 그다음 단위 테스트를 실행해」——이 세 단계에는 순서가 있고, 게다가 같은 작업 현장을 공유하고 싶습니다: 이전 단계의 현재 디렉터리, 환경 변수, 심지어 대화형 입력까지 다음 단계에서도 그대로 남아 있기를 바랍니다.

일반적인 bash 호출은 매번 초기화됩니다: 매번 새 셸에서 실행되며, 호출 사이에 상태가 보존되지 않습니다. 그래서 이때 모델은 다른 도구로 바꿉니다——영속 터미널을 여는 것입니다:

  1. terminal_open: 터미널 세션을 열고 session id를 얻습니다;
  2. terminal_send: 그 안에 명령을 보냅니다(예: npm install);
  3. terminal_read: 터미널 출력을 읽어 옵니다;
  4. 몇 단계가 지나면 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 에는 commanddescription 만 있다는 점에 주목하세요——모델은 최소한 「무엇을 실행할지」와 「이것이 무엇을 하는지에 대한 한 줄 설명」을 명확히 말해야 하며, 나머지 인자(타임아웃, 작업 디렉터리, 백그라운드 실행)는 모두 선택 사항입니다.

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 함수는 한 줄도 바꿀 필요가 없습니다.

模型「调用 get_weather」工具注册表ctx.tools执行后端bash · pty · fs · web(可替换的接缝)工具调用请求执行结果回到模型上下文

模型只声明要什么工具,注册表调度,后端执行——每个环节都可替换


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. 결과가 컨텍스트로 돌아온다: 다음 루프의 시작

도구 실행이 끝나도 이야기는 끝나지 않습니다——결과는 반드시 모델 컨텍스트로 돌아와야 합니다. 그렇지 않으면 모델은 「눈을 가린 상태」가 됩니다.

  1. 호출이 발행되면 세션에 tool/call 이벤트가 하나 기록됩니다(실행 전에 기록);
  2. 결과가 확정되면 tool/result 이벤트가 추가됩니다——이것이 모델이 보는 유일한 결과입니다;
  3. 결과는 텍스트 형태로 모델 컨텍스트에 들어갑니다: 명령 출력, [exit code: N] 표시, 가능한 잘림이나 오류 메시지;
  4. 모델은 결과를 다 읽고 새로운 「생각」 라운드를 시작합니다——요약할 수도 있고, 다시 도구 호출을 발행할 수도 있습니다.

2과의 단계 구조를 기억하시나요? 생각 → 행동 → 관찰 → 다시 생각. 도구 호출은 「행동 + 관찰」이라는 한 쌍의 동작이 프레임워크에 구현된 모습입니다: 행동 = 레지스트리가 호출을 실행 백엔드로 넘기는 것, 관찰 = 결과가 컨텍스트로 돌아오는 것. 한 번의 루프가 끝나고 다음 루프가 시작됩니다——여러 단계의 작업은 바로 이렇게 한 걸음씩 완료됩니다.

그렇기 때문에 매 라운드가 안전하고 제어 가능해야 합니다: 누가 어떤 도구를 호출할 수 있는지, 명령이 샌드박스 밖의 파일을 건드릴 수 있는지, 먼저 사용자에게 물어봐야 하는지……이것이 바로 다음 과인 「샌드박스와 보안」에서 다룰 문제입니다.


핵심 정리

  • 모델은 선언만 하고, 프레임워크가 손을 움직인다: 모델은 도구 호출 선언(도구 이름 + 인자)을 발행하고, 레지스트리가 디스패치하며, 실행 백엔드가 실제로 실행합니다.
  • 레지스트리 ctx.tools: 각 도구를 JSON Schema 「설명서」로 번역합니다. register로 등록, schemas로 모델에 제공, guard로 가드를 설정합니다.
  • 실행 파이프라인: tools/pre-execute → 가드 → tools/executetools/post-executefinalizeContenttools/result. 권한, 승인, 타임아웃, 재시도가 모두 고정된 심에 매달려 있습니다.
  • 백엔드는 모두 교체 가능한 심: bash(포그라운드/백그라운드), pty(소유자로 격리된 영속 터미널), subprocess(관리되는 프로세스 트리). 구현을 바꿔도 모델 측에는 영향이 없습니다.
  • 결과는 컨텍스트로 돌아온다: tool/result 가 모델이 보는 유일한 결과가 되어 다음 「생각 → 행동 → 관찰」 라운드를 촉발하고, 여러 단계의 작업이 이로써 완료됩니다.

🚀 다음 과(4과)에서는 「샌드박스와 보안」을 다룹니다: 명령이 어떤 파일을 건드릴 수 있는지, 언제 사용자에게 권한을 요청해야 하는지——에이전트가 「손을 움직일 수 있으면서」 동시에 「함부로 움직이지 않게」 하는 이야기입니다.

셀프 테스트 · 도구와 실행

답을 모두 선택한 후 「답안 제출」을 클릭하면 정답 여부와 해설을 확인할 수 있습니다.

1. DSH에서 모델이 bash 명령을 「실행」할 때, 실제로 명령을 실행하는 것은 누구인가요?
2. bash 도구의 run_in_background 인자를 true로 설정하면 어떻게 되나요?
3. DSH의 PTY(영속 터미널) 기능에 대한 설명으로 올바른 것은 무엇인가요?
4. DSH에서 한 번의 도구 호출이 거치는 실행 파이프라인의 올바른 순서는 무엇인가요?