DSH만의 도구 면: terminal / lsp / jobs
다른 제품에는 없는 도구들이 각각 어떤 문제를 푸는지. 핵심 소스: packages/terminal/, packages/lsp/, packages/jobs/; 도구 설명 전문은 docs/tool-catalog.zh.md.
같은 과제예요: Python REPL을 띄우고 코드를 세 단계로 디버깅합니다. 시나리오 A는 왼쪽에 bash 단일 도구만, 오른쪽에 terminal 6종 세트를 주고 라운드 수와 반복 노동 차이를 바로 봐요. 시나리오 B는 jobs 패널: 전혀 다른 백그라운드 작업 세 종류가 한 목록으로 어떻게 관리되는지 보여 줍니다.
docs/tool-catalog.zh.md의 bash·terminal_* 실제 schema에 맞고, 시나리오 B 작업 형태는 @deepseek-ai/dsh-tool-jobs 세 도구에 대응합니다. SEND_ACTIVE 오류 동작은 packages/terminal/terminal/src/index.ts 246행과 같아요.- 도구 여섯 개:
terminal_open / send / read / signal / close / list, 뒤에는 지속 PTY 세션이 있어요. - REPL, gdb, ssh 같은 상태ful 프로그램이 호출을 넘어 살아 있어요.
- 각 세션은 정확한 Agent 인스턴스에 귀속되고, 다른 agent가 id를 알아도 조작할 수 없어요.
- 딱 네 가지 쿼리: 정의로 이동, 참조 찾기, 구현으로 이동, hover — 닫힌 union, 하나 더하면 컴파일 단계 대공사예요.
- 범용 JSON-RPC 탈출구는 일부러 안 열어요. 모델이 프로토콜 꼼수를 못 써요.
- provider가 없어도 schema는 그대로, 구조화된
LSP_UNAVAILABLE를 돌려줘요.
- 백그라운드 bash, 백그라운드 PTY send, 백그라운드 subagent — 전부
<kind>-N작업으로 등록돼요. job_list / job_output / job_kill세 도구로 전부 관리해요.- 권한은 소유자 세션 기준이고, id가 예측 가능해도 괜찮아요. owner당 기본 최대 10개 동시 실행.
가장 과소평가되기 쉬운 terminal부터 볼게요. 일회성 bash 문제는 데모에서 이미 봤어요: REPL 변수는 한 호출을 못 넘기고, 모델은 옛 코드를 통째로 다시 보내야 해서 라운드와 토큰이 두 배로 탑니다. DSH 해법은 PTY 세션을 일등 리소스로 두는 거예요: terminal_open으로 세션을 만들고 id를 받고, 이후 send·read·signal·close는 전부 id로 조작해요. 세션은 백엔드와 도구 플러그인 핫 리로드 동안에도 살아 있어요(docs/subsystems/terminal.zh.md 「귀속과 지속성」 절).
도구 설명 자체가 프롬프트 엔지니어링 모범이에요. 1878줄 도구 카탈로그에서 terminal_send 항목이 대기 의미를 한 문장으로 밝혀요:
「지속 터미널로 텍스트를 보냅니다. 기본적으로 Enter를 제출하고, 프롬프트·stdin 대기·출력 정적·타임아웃·세션 종료를 기다립니다. 백그라운드 모드는 job_output/job_kill에 쓸 job id를 돌려줍니다.」
그다음 동시성 규율이에요: PTY 세션 하나는 한 시각에 활성 send 하나만 받아요. 두 호출이 같은 터미널을 두드리면 두 번째는 구조화된 오류를 먹고, 남의 명령 한가운데 글자를 끼워 넣을 수 없어요.
startSend()의 검문 순서를 보세요. 세 가지 검사는 하나도 빠질 수 없어요. 첫 관문은 expectOwned(owner, id): 권한은 세션을 소유한 정확한 Agent 인스턴스와 비교하고, id는 비밀이 아니에요. 경계는 귀속이고, 다른 agent가 id를 맞춰도 이 관문은 못 넘어요. 둘째는 세션이 닫히는 중인지 봐요. 반쯤 닫힌 세션은 새 입력을 거부해요. 셋째는 record.active: 이전 send가 아직 정산되지 않았으면 새 send는 SEND_ACTIVE 코드가 달린 TerminalError를 던져요. 모델은 구조화된 실패를 보고, 정산이 끝날 때까지 기다리면 돼요. 검사를 모두 통과하면 이번 작업이 그 세션의 유일한 활성 send로 등록되고, done 약속이 정산될 때(성공·실패 모두) 등록이 비워져야 다음 send가 들어올 수 있어요. 귀속이 곧 경계 — 이 규율은 jobs에서 다시 만나요.
출처: packages/terminal/terminal/src/index.ts 243–254행 (startSend()), 확인일 2026-08-13.
lsp 도구는 언어 서버 능력을 네 가지 의미 쿼리로 좁혀요. LSP 프로토콜 전체를 그대로 노출하지 않는 이유는, 모델 앞에 끝없는 schema가 펼쳐지고 provider를 바꿀 때마다 동작이 흔들리기 때문이에요. DSH는 닫힌 union이에요: seam·제공자·도구 세 층이 같은 네 연산 union을 공유하고, 다섯 번째를 넣으면 세 층을 다 고칠 때까지 컴파일이 실패해요. 라우팅도 단순해요: 파일 확장자로 provider를 찾고, 확장자 하나는 한 집만 담당해요:
async query(request: LspQueryRequest, signal?: AbortSignal): Promise<LspQueryResult> {
const route = this.routes.get(finalExtension(request.filePath))
if (route === undefined) {
throw new LspError(`no LSP provider handles "${request.filePath}"`, 'LSP_UNAVAILABLE')
}
return route.provider.query({ ...request, languageId: route.languageId }, signal)
}
packages/lsp/lsp/src/index.ts, 확인일 2026-08-13. 코드 블록은 소스 원문을 유지합니다.provider가 없을 때가 핵심이에요: lsp 도구는 카탈로그에 그대로 있고 schema는 한 글자도 안 바뀌며, 호출은 LSP_UNAVAILABLE 코드가 달린 구조화된 실패를 돌려줘요. 모델은 이 프로젝트에 언어 서버가 없다는 걸 배우고, 도구가 왜 사라졌는지 추측할 필요가 없어요. 다운그레이드는 구조화, 어휘는 안정 — bash가 [sandbox: …]를 보고하는 것과 같은 결이에요.
마지막은 jobs예요. 백그라운드 bash, terminal_send 백그라운드 모드, 백그라운드 subagent — 생산자 형태는 전혀 달라요. DSH는 전부 하나의 ctx.jobs 레지스트리에 등록하고 bash-1, subagent-2처럼 종류+순번 id를 주며, 모델은 같은 job_list / job_output / job_kill로 전부 다룹니다. 생산자는 실행 리소스를, 레지스트리는 신원·접근·수명 상태를 가져요(docs/subsystems/jobs.zh.md).
권한은 id 비밀에 의존 안 함id는 <kind>-N 순서로 발급돼 완전히 예측 가능해요. 방어선은 소유자 권한: 읽기·kill·대기 모두 호출 세션과 작업 owner가 일치하는지 검사하고, 남의 작업 label도 못 봐요.
done은 리소스 해제를 기다림생산자의 done 약속은 리소스가 해제된 뒤에야 resolve돼요. 일이 끝났다고 끝이 아니에요. owner가 파괴되면 레지스트리가 작업을 취소·대기하고 고아 프로세스를 남기지 않아요.
완료 알림 중복 방지어떤 인터페이스가 이미 종료 상태를 전달했으면 reported 플래그가 중복 완료 알림을 막아요. 작업 한 번 끝에 모델이 메시지 두 번·turn 두 번 낭비하는 일을 피해요.
PTY에서 Grok Build와 DSH는 같은 생각에 도달했어요. 저장소에 독립 ptyctl crate(crates/codegen/ptyctl/, pty·session·server·term·wait 모듈, ptyctl-cli 포함)가 있고 터미널 제어를 재사용 인프라로 만들었어요. 둘 다 상태ful 터미널을 일등 시민으로 봐요. 차이는 조합: Grok는 컴파일 타임 Rust crate, DSH는 런타임 플러그인+모델-facing 도구 여섯 개예요.
Claude Code 복원 소스 도구 목록(원고 2장)에는 일등 PTY 세션 도구도, LSP 도구도 없어요 — 공개된 복원 소스 근거예요. 백그라운드는 있어요: bash의 run_in_background, agent 작업의 시간 기반 자동 백그라운드(tengu_auto_background_agents, 원고 2장 448–458행 인용). 하지만 bash와 subagent를 각각 감싼 것이지, 종류를 가로지르는 통합 작업 레지스트리는 없어요. 격차가 게으름이 아니에요: CC는 제품이라 대화형 디버그는 IDE, 의미 탐색은 에디터가 받쳐 줘요. DSH는 런타임이라 헤드리스에서 모델에 이 능력을 혼자 공급해야 해요. 제품이 안 해도 되는 일, 런타임은 할 만해요. 한마디 더: 이 세 능력은 agent loop trunk에 없고 전부 선택 플러그인이에요. minimal 프리셋은 하나도 안 달아도 완전한 coding agent예요.
경계 시나리오 두 가지 추론
시나리오 1: agent가 같은 terminal 세션에 run_in_background: true send를 보낸 뒤 바로 포그라운드 send를 보냅니다. 두 번째는 어떻게 되고, 오류 코드는 무엇인가요? job 패널에는 무엇이 보이나요? 시나리오 2: 프로젝트에 언어 서버가 없는데 모델이 lsp로 a.py 정의를 한 번 조회합니다. 모델이 받는 결과 형태를 쓰고, lsp를 카탈로그에서 빼는 것보다 왜 더 나은지 설명하세요.