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

제 2 과: ctx 알아보기: 모든 능력의 입구

한 문장 요약: ctx는 DSH에서 모든 능력으로 통하는 입구입니다. 모델, 도구, 세션, 명령, 샌드박스, 스킬이 모두 ctx라는 컨텍스트 객체에 매달려 있습니다. ctx.xxx가 바로 DSH의 API 표면이며, ctx를 배우면 DSH 전체를 여는 열쇠를 손에 쥐는 것입니다.


1. 사용자 스토리: 왜 모든 문서가 ctx.xxx를 말하는가

D 씨는 어제 dsh --profile headless "이 저장소를 요약해 줘"로 첫 번째 작업을 성공시킨 참입니다. 오늘은 신이 나서 DSH의 플러그인 문서를 펼쳤는데, 자신이 「가장 익숙한 낯선 사람」에게 둘러싸여 있음을 발견했습니다:

  • 도구를 추가하고 싶다면, 문서에는 ctx.tools.register(...)라고 적혀 있습니다;
  • 모델을 바꾸고 싶다면, 문서에는 ctx.llm.registerAdapter(...)라고 적혀 있습니다;
  • 세션을 관리하고 싶다면, 문서에는 ctx.sessions라고 적혀 있습니다;
  • 명령을 실행하거나, 샌드박스를 제한하거나, 스킬을 걸고 싶다면, 각각 ctx.shell, ctx.sandbox, ctx.skills라고 적혀 있습니다.

거의 모든 예제 코드가 ctx로 시작하는데, 어떤 문서도 가장 기본적인 질문에 먼저 답하지 않습니다: ctx는 도대체 무엇일까요? 왜 모든 능력이 여기에서 자라나는 걸까요?

D 씨의 혼란은 그가 둔해서가 아니라, 우연히 DSH의 가장 핵심적인 설계에 닿았기 때문입니다: DSH는 모든 능력을 같은 객체에 매달아 두며, 이 객체가 바로 ctx(context, 컨텍스트)입니다. 이번 과의 임무는 이 「입구」를 철저히 파헤치는 것입니다. 이번 과를 마치면 어떤 ctx.xxx를 보든 그것이 어느 종류의 능력에 속하는지 한눈에 알아볼 수 있습니다.

🎁 비유를 들자면: ctx는 에이전트 방 안의 「배전반」과 같습니다. 조명(모델), 콘센트(도구), 전화(세션), 수도관(명령)…… 모든 설비의 전기가 이 하나의 상자에서 나갑니다. 새 장비를 설치하려면 배전반에서 새 인터페이스를 찾으면 되고, 어떤 장비에 전기가 들어오는지 확인하려면 역시 배전반에서 조회하면 됩니다.


2. ctx = 컨텍스트 객체: 모든 능력이 여기에 매달린다

먼저 Cordis 입문 문서의 한 구절을 기억해 둡시다(출처: docs/cordis-primer.zh.md):

컨텍스트는 서비스의 컨테이너다. 각 서비스는 안정적인 ctx.<key>(예: ctx.tools, ctx.llm, ctx.sessions)를 차지하며, 다른 플러그인은 구체적인 구현을 임포트하는 대신 key로 서비스를 조회한다.

풀어서 볼까요. 이 문장은 세 가지를 말합니다:

  1. ctx는 「컨테이너」: 그 자체로는 일을 하지 않고, 「서비스」를 담기 위한 것입니다;
  2. 각 서비스는 안정적인 key를 하나씩 차지: ctx.tools는 도구 레지스트리, ctx.llm은 모델 어댑터 레지스트리, ctx.sessions는 세션…… 능력이 다르면 key도 다르며, 서로 충돌하지 않습니다;
  3. key로 서비스를 찾고, 구현을 import 하지 않음: 플러그인 작성자가 어떤 능력을 쓰고 싶을 때, 그것이 구체적으로 어느 패키지에서 구현되었는지 알 필요가 없습니다. key를 선언하고 바로 호출하면 됩니다. 이것이 바로 제 1 장에서 말한 「능력은 곧 이음매(seam)」입니다: 구현을 바꾸되 인터페이스는 바꾸지 않습니다.

그럼 DSH를 개봉했을 때 ctx에는 실제로 어떤 서비스가 매달려 있을까요? 아키텍처 문서에는 실제 「능력 서비스」 목록이 있습니다(출처: docs/architecture.zh.md). 그중 몇 줄을 발췌합니다:

ctx 키패키지 계열책임
ctx.llmllm/어댑터 레지스트리와 모델 스트리밍 호출
ctx.toolscore/tools도구 레지스트리와 실행 파이프라인
ctx.sessionsdsh-session메모리 내 이벤트 소싱 세션
ctx.shellshell/포어그라운드 및 백그라운드 명령 실행
ctx.sandboxsandbox/호스트의 파일 시스템과 커널을 공유하도록 제한된 프로세스
ctx.skillsskill/skill(스킬) 제공자 레지스트리와 점진적 공개
ctx一切的入口ctx.llm模型调用ctx.tools工具注册表ctx.sessions会话ctx.skills技能ctx.shell命令执行ctx.sandbox沙箱

ctx.xxx 就是 DSH 的 API 面:所有能力都挂在 ctx 这个入口上

그림에 있는 것들은 빙산의 일각일 뿐입니다. 전체 목록에는 ctx.agents(활성 에이전트), ctx.fs(파일 시스템), ctx.lsp(코드 시맨틱), ctx.web(검색), ctx.jobs(백그라운드 작업), ctx.goals(목표), ctx.workflowEngine(워크플로)…… 물경 수십 개의 서비스가 더 있습니다. 기억해야 할 결론은 하나뿐입니다: DSH에서 능력과 관련된 일은 모두 「ctx에서 key를 하나 꺼내 호출하는」 방식입니다. 이것이 이번 과 제목이 말하는 바입니다: ctx.xxx가 바로 DSH의 API 표면입니다.

실제 코드는 어떻게 생겼을까요? 저장소에 있는 「최소 도구 플러그인」의 완전한 형태입니다(출처: docs/cookbook/adding-a-tool.zh.md):

import type { Context } from 'cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'

export const inject = ['tools']          // ① 선언: 이 플러그인은 ctx.tools 서비스가 필요함

export function apply(ctx: Context) {
  ctx.tools.register(defineTool({        // ② 사용: ctx.tools에 도구를 등록
    name: 'read_file',
    description: 'Read a file from disk.',
    parameters: {
      path: { type: 'string', required: true, description: 'Absolute path' },
      limit: { type: 'number' },
    },
    output: {
      schema: { type: 'string' },
      render: (_args, value) => [{ type: 'text', text: value }],
    },
    async execute(args, exec) {
      return readFile(args.path, { encoding: 'utf8', signal: exec.signal })
    },
  }))
}

핵심 코드 두 줄, 이것을 이해하면 DSH 플러그인을 작성할 줄 아는 것이나 다름없습니다:

  • export const inject = ['tools']: 의존성 선언. 플러그인이 「나는 ctx.tools라는 서비스가 필요합니다」라고 말하면, Cordis는 서비스가 준비된 후에야 플러그인을 시작합니다(제 1 과의 「플러그인은 서비스 가용성에 따라 활성화된다」와 호응);
  • ctx.tools.register(...): 능력 등록. 도구를 ctx에 마운트하면 schema가 자동으로 프롬프트 조립으로 흘러 들어갑니다. 모델은 즉시 이 도구를 보고 호출할 수 있습니다.

등록 자체는 가역적인 부수 효과입니다: 플러그인이 언로드되면 등록이 자동으로 취소되고, 도구는 ctx에서 사라지며, 시스템은 원래대로 복원됩니다(출처: docs/cordis-primer.zh.md: 「등록은 가역적인 부수 효과…… reload와 teardown 시 예상대로 취소된다」).


3. 하나의 ctx에 담긴 세 가지: 이펙트 + 코이펙트의 통합 실체

제 2 장에서 정독한 그 논문을 기억한다면, 여기서 「아하!」 하게 되는 대응 관계가 있습니다: ctx는 아무렇게나 지은 이름이 아니라, 논문의 통합 컨텍스트 타입 Γ∞가 런타임에 구현된 모습입니다.

논문에 따르면 하나의 ctx에는 세 가지가 동시에 담겨 있습니다(Γ∞ ≔ μΓ. Γ × (Γ → Γ) × Σ):

구성 요소담긴 것쉬운 말로대응하는 질문
Γ현재 컨텍스트 상태세계가 지금 어떤 모습인가나는 어디에 있는가
Γ → Γ누적 역함수그동안 무엇을 바꿨고, 어떻게 되돌아가는가내가 무엇을 바꿨는가
Σ의존 테이블지금 무엇이 필요한가나는 무엇이 필요한가

이 중에서:

  • 「내가 무엇을 바꿨는가」가 이펙트(effect): 파일 수정, 프로세스 실행, 도구 등록…… 롤백 가능해야 합니다;
  • 「나는 무엇이 필요한가」가 코이펙트(coeffect): 어떤 서비스, 어떤 설정…… 자동으로 연결되어야 합니다.

논문의 가장 아름다운 한 걸음은 「이펙트 컨텍스트」와 「코이펙트 컨텍스트」를 같은 ctx 실체로 통합한 것입니다. 그래서 DSH에서는 언제나 하나의 ctx와만 상대하면 되며, 그것이 동시에 세 가지 질문에 답합니다: 나는 어디에 있는가, 내가 무엇을 바꿨는가, 나는 무엇이 필요한가.

이 이론은 Cordis 코어 라이브러리에서 몇 가지 실제 API로 구현되어 있습니다(출처: Cordis 코어 라이브러리의 단순화된 매핑):

ctx.effect(callback)              // 컨텍스트를 변경하는 「유일한 입구」: 자동 추적되며, 취소 가능한 dispose를 반환
ctx.set(key, value)               // 코이펙트 제공: 서비스/값을 ctx에 마운트(내부적으로는 한 번의 ctx.effect)
ctx.get(key)                      // 코이펙트 요구: key로 서비스를 가져오며, 절대 실패하지 않음

이제 숨겨진 이스터 에그 하나를 읽을 수 있습니다: 제 1 과에서 「로드하면 즉시 적용, 언로드하면 즉시 복원」이라고 했습니다. 왜 복원할 수 있을까요? 서비스를 거는 것(ctx.set, ctx.tools.register)은 본질적으로 모두 가역적인 이펙트이며, 언로드 시 역함수에 따라 항목별로 취소되기 때문입니다. 정확성은 개발자의 주의력에 의존하는 것이 아니라 ctx라는 구조 자체가 보장합니다. 논문의 한 문장에 호응하자면: 정확성이 「개발자의 자율」에서 「구조적 보장」으로 바뀐 것입니다.

💡 수식이 겁나시나요? 한 문장만 기억하면 충분합니다: ctx = 정체성(나는 어디에 있는가) + 변경 기록(내가 무엇을 바꿨는가) + 의존 목록(나는 무엇이 필요한가), 세 가지가 하나의 실체에 담겨 있어서, 탈착이 깔끔하면서도 추적 가능합니다.


4. 스코프: 모든 agent는 자신만의 전용 agent.ctx를 갖는다

마지막 핵심 개념: ctx는 「전역에서 유일」하지 않습니다. 모든 agent가 자신의 ctx를 가집니다.

아키텍처 문서의 원문(출처: docs/architecture.zh.md):

모든 agent는 스코프화된 agent.ctx를 소유한다. 공유 스토리지는 그 도구, 프롬프트, 명령 항목을 전역 항목 위에 겹쳐 올리면서 도메인별 뷰를 보존한다.

dsh-scope 패키지가 이 모든 것의 구현 기반입니다(출처: packages/core/scope/README.zh.md):

createScope(ctx, key)는 태그가 붙은 Cordis 컨텍스트를 생성하며, 그 하부 fiber는 해당 컨텍스트를 통해 이루어진 모든 등록을 소유한다. …… agent loop(에이전트 루프)는 각 라이브 agent마다 스코프를 생성한다.

이 두 문장은 같은 메커니즘을 말합니다. 두 가지로 나누면 이해할 수 있습니다:

  1. 공유 스토리지 오버레이: 전역 계층에는 이미 기본 능력(모든 agent가 공유하는 도구, 프롬프트)이 마운트되어 있고, 각 agent의 스코프는 전역 위에 자신의 비공개 항목을 다시 겹쳐 올립니다. 오버레이 = 전역 것도 보이고 비공개 것도 보인다;
  2. 도메인별 뷰: agent A가 등록한 것은 A 자신에게만 보이고, agent B의 세계에는 존재하지 않습니다. 두 agent가 동시에 실행되어도 서로 간섭하지도, 서로를 망가뜨리지도 않습니다.

이것은 제 1 과 시작 흐름의 ③ 단계 「스코프화된 ctx 준비 완료」와 정확히 호응합니다: agent loop는 각 라이브 agent마다 스코프가 붙은 ctx를 생성하고, agent의 등록은 자기 스코프의 수명 주기에만 속합니다. agent가 끝나면 스코프가 dispose되고, 그 모든 등록이 흔적 없이 취소됩니다.

🎁 비유를 들자면: 한 회사(전역 ctx)에는 누구나 쓸 수 있는 공용 회의실과 프린터가 있고, 각 부서(agent의 스코프)에는 자기 사무실도 있습니다. 부서 담장 안의 일은 다른 부서에서 볼 수 없습니다. 부서가 해산되면 사무실은 비워지고, 공용 시설은 영향을 받지 않습니다.


핵심 정리

  1. ctx는 컨텍스트 객체이자 서비스의 컨테이너: 각 능력이 안정적인 ctx.<key>를 차지하고, 다른 코드는 key로 서비스를 찾으며 구현을 import 하지 않습니다. ctx.xxx가 바로 DSH의 API 표면입니다.
  2. 모든 능력은 ctx에 마운트된 서비스: ctx.llm은 모델, ctx.tools는 도구, ctx.sessions는 세션, ctx.shell은 명령, ctx.sandbox는 샌드박스, ctx.skills는 스킬…… 수십 개의 서비스, 하나의 입구.
  3. ctx는 「이펙트 컨텍스트 + 코이펙트 컨텍스트」의 통합 실체(Γ∞): 하나의 ctx에 「나는 어디에 있는가(상태)」「내가 무엇을 바꿨는가(이펙트, 롤백 가능)」「나는 무엇이 필요한가(코이펙트, 자동 연결)」가 동시에 담겨 있으며, 코어 라이브러리는 ctx.effect, ctx.set, ctx.get으로 이를 구현합니다.
  4. 등록하면 즉시 적용, 언로드하면 즉시 복원: 서비스를 거는 것은 본질적으로 가역적인 이펙트이며, 정확성은 개발자의 자율이 아니라 ctx의 구조가 보장합니다.
  5. 스코프: 모든 agent는 스코프화된 agent.ctx를 소유합니다. 비공개 항목이 전역 항목 위에 겹쳐지고, 도메인별 뷰는 서로 격리됩니다. agent가 끝나면 스코프가 dispose되고 모든 등록이 함께 취소됩니다.

🚀 다음 과에서는 능력이 빽빽이 걸린 ctx가 어떻게 움직이기 시작하는지 살펴보겠습니다: 제 3 과 「에이전트 루프와 세션」. agent가 어떻게 한 바퀴 한 바퀴 감지하고, 생각하고, 행동하며, 모든 단계를 추가 전용(append-only) 세션 로그에 기록하는지 알아봅니다.

셀프 퀴즈 · ctx 알아보기

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

1. ctx는 도대체 무엇입니까?
2. ctx.tools는 무엇을 나타냅니까?
3. 모든 agent가 스코프화된 agent.ctx를 소유한다는 것의 의미는 무엇입니까?
4. ctx와 논문의 Γ∞는 어떤 관계입니까?