Copilot은 예약을 기억했다. 왜 워크플로우를 재개하지 못했을까?
요약
본 글은 GitHub Copilot이 사용자의 요청을 받아 동적 워크플로우를 생성하는 내부 작동 원리를 깊이 있게 분석합니다. 모델이 어떻게 API를 인식하고, 실제 런타임에서 코드가 어떤 과정을 거쳐 실행 가능한 확장으로 변환되는지 기술적인 관점에서 추적했습니다. 특히, 단순한 로그 기록과 실제로 상태가 유지되며 재개 가능한 내구성 있는 워크플로우 간의 근본적인 차이를 탐구하며 개발자로서의 통찰을 제공합니다.
핵심 포인트
- Copilot은 마법이 아닌 3단계 핸드셰이크 과정을 거쳐 워크플로우를 생성한다.
- 네이티브 런타임과 Copilot SDK가 협력하여 도구 스키마를 모델에 노출시킨다.
- 워크플로우는 세션의 특정 폴더에 ES 모듈 스캐폴드로 저장되어 실행된다.
- 단순 로그 기록(logging)과 내구성 있는 워크플로우(durable workflow)는 개념적으로 큰 차이가 있다.
단순한 호기심에서 시작했습니다. "GitHub Copilot에게 동적 워크플로우를 생성하도록 요청하면, 실제로 내부적으로 어떻게 작동하는 걸까?" 라는 질문이었습니다.
사용자가 "항공편 예약, 호텔 및 렌터카 예약을 처리하는 워크플로우를 만들어줘"와 같은 일반 영어 문장을 입력하면, 잠시 후 Copilot은 세션 내에 실행 가능한 멀티 에이전트(multi-agent) 워크플로우를 등록합니다. 정말 귀엽죠. 정말 마법 같고요. 마치 "엄마, 보세요. 에이전트들이 제 휴가를 계획했어요."라고 말하는 것 같습니다.
하지만 저는 런타임에서 마법 같은 건 좋아하지 않습니다. 저는 프롬프트와 실행 중인 워크플로우 사이에서 실제로 무슨 일이 일어나고 있는지 알고 싶었습니다. 모델은 어떻게 워크플로우 API를 알까요? 모델 가중치에 내장되어 있나요? 거대한 숨겨진 시스템 프롬프트 안에 채워져 있나요? 비밀 스킬에서 로드되나요? 생성된 코드는 디스크의 어디에 존재하며, 실제로 무엇이 그것을 실행할까요?
그래서 저는 늘 하던 대로, 기능이 블랙박스처럼 느껴질 때 파고들기 시작했습니다. CLI 바이너리를 통해 툴 스키마(tool schemas)를 추적했고, 세션 로그를 검사했으며, Procmon을 열어 프로세스가 실제로 디스크의 어떤 파일들을 건드리는지 지켜보았습니다.
그리고 물론, 런타임이 워크플로우를 빌드하고 저널링하는 방식을 본 후, 분산 시스템(distributed-systems)에 대한 제 두뇌는 (제가 작성한 Azure Durable Task Scheduler 관련 이전 포스트를 읽으셨다면 이미 어디로 향하는지 아실 겁니다) 다음 질문을 던질 수밖에 없었습니다. "호텔 예약 직후, 렌터카 에이전트가 완료되기 전에 프로세스를 강제 종료하면 어떻게 될까?"
저와 함께해 주세요. 커튼 뒤를 살짝 들여다보는 것으로 시작된 이 탐구는 **'무슨 일이 일어났는지에 대한 로그(log)를 가지고 있는 것'**과 '실제로 재개할 수 있는 내구성 있는 워크플로우(durable workflow)' 사이의 차이에 대한 훌륭한 교훈으로 바뀌었습니다.
커튼 뒤 들여다보기: Copilot이 워크플로우 작성법을 배우는 방법
Copilot에게 동적 워크플로우(dynamic workflow) 생성을 요청할 때, 영어 코드를 상태 기계(state machine)로 변환하는 마법 같은 Markdown 컴파일러는 존재하지 않습니다. 이는 네이티브 CLI 런타임과 번들된 Copilot SDK 간의 매우 실용적인 3단계 핸드셰이크 과정입니다.
첫 번째 단계에서, 네이티브 런타임은 표준 도구 설명(tool descriptions) 및 스키마를 통해 워크플로우 도구(dynamic_workflows_manage와 run_dynamic_workflow)를 모델에 노출합니다. 만약 CLI의 문자열들(1.0.94-3, 제가 초기 추적에서 본 것과 같이)을 따라가 보면, 이들은 자바스크립트 브릿지를 거쳐 번들된 prebuilds\win32-x64\runtime.node로 곧장 이어집니다.
그리고 dynamic_workflows_manage의 도구 설명 내부에는 런타임이 모델에게 일종의 발자취(breadcrumb)를 남깁니다: _워크플로우 코드를 작성하기 전에 반드시 `operation:
세 번째 단계는 그 지식을 실행 가능한 확장(runnable extension)으로 변환하는 것입니다. author 플로우에서 모델은 dynamic_workflows_manage({ operation: "author", meta, run })을 호출하며, 워크플로우 메타데이터와 JavaScript의 run 함수 본문을 전달합니다. 호스트는 이를 검증하고, 세션의 extensions<name>\extension.mjs 폴더 아래에 작은 ES 모듈 스캐폴드(scaffold)로 감싸고, 그 옆에 .authored-workflow 마커를 떨어뜨린 다음, 확장 기능을 세션으로 다시 로드합니다:
import { defineWorkflow, joinSession } from "@github/copilot-sdk/extension";
const workflow = defineWorkflow({
...
이 노동 분할의 깔끔한 구분을 주목하세요: 모델은 meta 선언과 run 본문을 작성하고; 호스트는 import와 joinSession 보일러플레이트(boilerplate)를 작성하며; @github/copilot-sdk/extension은 확장 호스트 내에 번들링된 SDK로 해석됩니다. 워크플로우를 생성하고 등록하는 것이 실행하는 것은 아닙니다 — 실행은 run_dynamic_workflow를 통해 별도의 단계입니다.
CLI에서 실시간으로 작동하는 것을 관찰하기
트레이스(traces)를 읽는 것과, 그것이 실시간으로 발생하는 것을 보는 것은 훨씬 더 재미있습니다. 저는 새로운 Copilot CLI 세션( cli-tunnel을 통해 1.0.99-2 실행)을 설정하고, 시애틀 여행 계획 워크플로우를 만들도록 요청했습니다 — 등록만 하고 아직 실행하지 않도록 했습니다.
Copilot CLI가 시애틀 여행 계획 워크플로우를 생성하고 등록하도록 요청하는 모습.
예상대로, Copilot이 가장 먼저 한 일은 디스크에 번들링된 workflows.md 가이드를 읽을 권한을 요청하는 것이었습니다:

Copilot CLI가 워크플로우 코드를 단 한 줄 작성하기 전에 번들링된 copilot-sdk/docs/workflows.md 가이드를 읽을 권한을 요청하는 모습.
새로운 CLI 버전(1.0.99-2)에서는 단발성 author 호출 대신, Copilot이 확장(extension) 라이프사이클을 직접 사용했습니다 — guide → scaffold → edit → reload → inspect — 그리고 다섯 개의 ctx.step 작업(세 번의 병렬 시뮬레이션 예약, 검토 단계, 보호된 확인)으로 seattle-trip-planning을 실행하지 않고 등록했습니다:
이 워크플로우는 레지스트리에서 스캐폴딩(scaffolded)되고, 리로드(reloaded)되며, 검증되었습니다 — 아직 예약은 이루어지지 않은 상태로 실행 준비가 완료된 것입니다.
제가 내부적으로 무슨 일이 일어났는지 확인하고 싶었기 때문에, 두 가지 각도에서 생성 턴을 확인했습니다. 첫째, 세션의 기록된 도구 실행 로그를 조회하여 정확한 도구 호출 순서(guide가 경로를 반환 → view가 workflows.md를 읽음 → 확장 스캐폴딩 → extension.mjs를 읽고 패치함)를 확인했습니다:
세션의 도구 로그를 검사해보니: guide가 SDK 문서 경로를 반환하고, view가 workflows.md를 읽으며, 확장 스캐폴딩이 제자리에 패치되었습니다.
그리고 둘째, 운영체제(OS) 수준의 진실성이 최고라고 생각하여 Sysinternals Process Monitor (Procmon)를 열고 프로세스가 디스크에서 copilot-sdk\docslows.md에 접근하는 것을 관찰했습니다:
Procmon이 디스크에 설치된 copilot-sdk\docs\workflows.md 가이드에 대한 파일 시스템 접근을 포착했습니다.
만약 당신이 그 워크플로우를 '이상적인 경로(happy path)'로 실행한다면—예를 들어 2026년 11월 10일부터 14일까지 SFO에서 시애틀로의 가상 여행을 계획하는 경우—그것은 한 번에 순조롭게 진행되며 깔끔한 예약 테이블을 출력합니다:
이상적인 경로(happy path): 세 가지 가상 예약이 모두 완료되고 첫 시도에 여행이 깔끔하게 확정됩니다.
Copilot이 당신에게 코칭받지 않을 때 실제로 작성하는 내용
문제를 일으키기 전에, 제가 새로운 데스크톱 세션에서 개방형 프롬프트를 주었을 때 Copilot이 생성한 코드를 살펴보는 것이 가치가 있습니다:
목적지를 받으면 항공편, 호텔, 렌터카를 예약하고, 예약 결과를 검토한 다음, 확정하는 여행 계획용 Copilot 동적 워크플로우를 만들어 주세요. 생성/등록만 하고, 실행하거나 예약을 진행하지 마세요.
저는 의도적으로 Durable Task Scheduler, idempotency keys, 또는 replay boundaries에 대해서는 언급하지 않았습니다. 저는
애플리케이션 로직으로 보자면, 정말 사려 깊은 코드입니다.
하지만 **내구성 있는 분산 워크플로우(durable distributed workflow)**라는 관점에서 보면, 빠진 부분이 있습니다. 제공자 아이덴티티 키(provider idempotency keys)가 없습니다. 논리적인 여행을 재개 가능한 실행으로 매핑하는 외부 오케스트레이션 상태도 없습니다. 그리고 최종 반환 객체를 ctx.step("confirm-trip", ...)로 감싸는 것은 최종 요약본만 체크포인팅할 뿐입니다 — 그 세 개의 병렬 ctx.agent(...) 호출 내부에서 발생하는 외부 도구 호출을 내구성 있고, 아이덴티티하며(idempotent) 경계 안에 자동으로 포장해주지는 않습니다.
이것이 재미있는 부분으로 이어집니다: 전원을 뽑는 것(pulling the plug).
"예약됨"과 "완료됨" 사이의 연결 끊기
충돌 테스트를 실행해 봅시다. (그리고 제가 회사 Amex 카드를 걱정하는 분들께 미리 말씀드립니다: 이 테스트에서 모든 예약은 로컬 가짜 예약 제공자(fake booking providers)를 사용합니다. Copilot 워크플로우와 에이전트는 실제였지만, 항공사는 아니었습니다.)
저는 정확히 그 생성된 여행 워크플로우를 기본 로컬 실행기(1.0.95-3)에서 실행했고, 매우 구체적인 충돌 지점을 만들었습니다:
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기



