실행
실행은 하나의 행이고, 커서이고, 시도의 저널이고, 마감 시각의 집합이고, 멈춤의 집합입니다. 이 페이지는 실행에 무엇을 할 수 있는지와, 실행이 무엇을 되돌려 말해 주는지 다룹니다.
실행 시작하기
실행은 정의 id와 입력으로 시작합니다. 정의에 스키마가 있으면 데몬이 입력을 그 스키마로 검증하고, 저장된 것 중 가장 높은 버전을 고정하고, 실행과 그 트레이스를 만들고, 저장된 그대로의 실행을 답으로 돌려줍니다. 그때부터 실행은 스스로 움직입니다. 데몬이 그 단계들을 워커에게 내주고, 슬립과 대기를 지나가며, 무슨 일이 있었는지 저널에 씁니다.
시작, 응답, 따라가기
// Start a run from anywhere that holds an execute key, answer the wait from a
// webhook, then follow the journal to the end and read the run.
const run = await mcpd.runs.start({
definitionId: 'support_ticket',
input: { ticket: 'Q-42' },
})
await mcpd.events.publish({
event: 'approved',
key: 'Q-42',
payload: { by: 'ops' },
eventId: 'hook-1', // a repeat of this id is a duplicate, not a second event
})
for await (const event of mcpd.runs.watch(run.runId)) {
console.log(event.seq, event.kind)
}
const detail = await mcpd.runs.get(run.runId)
console.log(detail.run.status, detail.run.output) # Start a run from anywhere that holds an execute key, answer the wait from a
# webhook, then follow the journal to the end and read the run.
run = mcpd.runs.start(support, input={"ticket": "Q-42"})
# A repeat of this event id is a duplicate, not a second event.
mcpd.events.publish("approved", "Q-42", payload={"by": "ops"}, event_id="hook-1")
for event in mcpd.runs.watch(run.run_id):
print(event.seq, event.kind)
detail = mcpd.runs.get(run.run_id)
print(detail.run.status, detail.run.output) // Start a run from anywhere that holds an execute key, answer the wait from
// a webhook, then follow the journal to the end and read the run.
let start = StartRun::new("support_ticket").input(json!({ "ticket": "Q-42" }));
let run = mcpd.start_run(start).await?;
// A repeat of this event id is a duplicate, not a second event.
let payload = json!({ "by": "ops" });
let approval = PublishEvent::new("approved", "Q-42").payload(payload).event_id("hook-1");
mcpd.publish_event(approval).await?;
mcpd.watch_run(&run.run_id, -1, |e| println!("{} {:?}", e.seq, e.kind)).await?;
let detail = mcpd.get_run(&run.run_id).await?;
println!("{:?} {:?}", detail.run.status, detail.run.output); // Start a run from anywhere that holds an execute key, answer the wait from
// a webhook, then follow the journal to the end and read the run.
run, err := m.StartRun(ctx, mcpd.StartRun{
Definition: "support_ticket",
Input: map[string]any{"ticket": "Q-42"},
})
if err != nil {
panic(err)
}
// A repeat of this event id is a duplicate, not a second event.
_, err = m.PublishEvent(ctx, mcpd.PublishEvent{
Event: "approved",
Key: "Q-42",
Payload: map[string]any{"by": "ops"},
EventID: "hook-1",
})
if err != nil {
panic(err)
}
_ = m.WatchRun(ctx, run.RunID, -1, func(e mcpd.RunEvent) error {
fmt.Println(e.Seq, e.Kind)
return nil
})
detail, _ := m.GetRun(ctx, run.RunID)
fmt.Println(detail.Run.Status, string(detail.Run.Output)) // Start a run from anywhere that holds an execute key, answer the wait from
// a webhook, then follow the journal to the end and read the run.
val run = mcpd.startRun("support_ticket", input = mapOf("ticket" to "Q-42"))
// A repeat of this event id is a duplicate, not a second event.
mcpd.publishEvent("approved", "Q-42", payload = mapOf("by" to "ops"), eventId = "hook-1")
mcpd.watchRun(run.runId) { event -> println("${event.seq} ${event.kind}") }
val detail = mcpd.getRun(run.runId)
println("${detail.run.status} ${detail.run.output}") # Start a run from anywhere that holds an execute key, answer the wait from a
# webhook, then follow the journal to the end and read the run.
{:ok, run} = Mcpd.start_run(mcpd, support, input: %{"ticket" => "Q-42"})
# A repeat of this event id is a duplicate, not a second event.
{:ok, _} =
Mcpd.publish_event(mcpd, "approved", "Q-42",
payload: %{"by" => "ops"},
event_id: "hook-1"
)
mcpd |> Mcpd.watch_run(run.run_id) |> Enum.each(&IO.inspect({&1.seq, &1.kind}))
{:ok, detail} = Mcpd.get_run(mcpd, run.run_id)
IO.inspect({detail.run.status, detail.run.output})
재시도될 수 있는 호출자라면 idempotency_key를 함께 보내십시오.
실행이 보관되는 동안 프로젝트 안에서 고유하며, 같은 호출을 다시 하면 두 번째
실행을 시작하는 대신 같은 실행을 답으로 돌려줍니다. 두 번 발화하는 웹훅이나
다시 시작하는 잡 러너가 그래서 아무 대가도 치르지 않습니다.
The cursor
The daemon holds the run between steps: where it is, what each step answered, what it is waiting for and until when. A worker holds one step at a time, and only while it runs. During a wait no process is alive at all.
저널
실행에 대한 모든 변화는 순번이 붙은 이벤트입니다. 0부터 시작하고 빈자리가 없습니다. 실행이 시작되었다, 커서가 움직였다, 단계가 리스되었다, 시작되었다, 끝났다 또는 실패했다, 실행이 기다렸다가 재개되었다, 실행이 끝났다. 각각은 그 종류에 맞는 페이로드를 싣습니다. 저널은 실행의 역사이며, 인터페이스가 그리는 것이 바로 이것입니다.
after부터 페이지 단위로 읽거나, Accept: text/event-stream
으로 이벤트 엔드포인트를 열어 둔 채 커밋되는 대로 각 이벤트를 받으십시오. 멈췄다가
다시 시작한 클라이언트는 마지막으로 본 순번을 넘기고, 아무것도 놓치지 않습니다.
모든 SDK의 watch가 이 일을 대신해 주고 실행이 끝날 때 함께 끝납니다.
wait는 저널을 끝까지 따라가 출력을 돌려주고, 성공하지 못했을 때는
그 실행을 실은 타입이 정해진 실패를 돌려줍니다.
이벤트와 대기
wait_event 단계는 자기 이름과 렌더링된 키를 가진 이벤트가 도착할
때까지 실행을 붙들어 둡니다. 이벤트는 문 하나로 들어옵니다.
대기에 응답하기
curl -X POST http://localhost:4317/v1/api/events \
-H 'content-type: application/json' \
-d '{"project":"support","event":"approved","key":"Q-42","payload":{"by":"ops"},"event_id":"hook-1"}'
답은 세 결과 중 하나입니다. delivered는 기다리던 실행이 있었고 같은
트랜잭션 안에서 이벤트를 받아 갔다는 뜻이라, 영수증을 읽을 때쯤이면 실행은 이미
움직였습니다. parked는 기다리던 실행이 없었고, 나중에 열리면서 보류된
이벤트를 받겠다고 말하는 대기를 위해 그 이벤트를 보관해 두었다는 뜻입니다.
duplicate는 그 event_id를 전에 본 적이 있어 아무것도
쓰지 않았다는 뜻입니다. 두 번 발화할 수 있는 것에서는 직접 만든 id를 보내십시오.
마감 시각에 이른 대기는 자기 on_timeout이 말하는 대로 합니다. 실패한
다음 on_error가 결정하게 하거나, 출력 없이 계속 갑니다. 실행 하나에
그 실행이 열어 둔 대기를 통해 직접 보내는 것은
POST /v1/api/runs/{run}/signal입니다. 같은 본문을 키 없이
받는데, 주소 노릇을 하는 것이 실행 자신이기 때문입니다.
취소
POST /v1/api/runs/{run}/cancel은 실행과 그 아래의 모든 자식
실행을 끝냅니다. 진행 중인 단계를 통신 도중에 끊지는 않습니다. 워커는 다음
하트비트에서 실행이 끝났음을 알게 되고, 그 컨텍스트가 본문에 신호를 보내며,
본문이 그 뒤에 올리는 것은 run_terminal로 거절됩니다. 이미 끝난
실행을 취소하면 아무것도 바뀌지 않고, 있는 그대로의 실행을 답으로 돌려줍니다.
타이머와 한계
슬립, 대기 마감 시각, 재시도 백오프, 리스 만료는 모두 데몬이 들고 있는 타이머입니다.
데몬의 시계로 1초에 한 번 발화하므로, 100밀리초짜리
retry_after_ms는 정확히 그만큼이 아니라 적어도 그만큼으로 지켜집니다.
데몬을 다시 시작해도 하나도 잃지 않습니다. 타이머는 행이고, 실행은 그 행에서
재개됩니다. 어떤 기간도 3650일보다 길 수 없고, 워커가 보낸 그보다 긴
retry_after_ms는 400으로 거절됩니다. 2262년을 넘는
sleep_until은 결코 기한에 이르지 않는 것으로 다룹니다.
정의의 max_steps, run_timeout_ms,
max_cost_usd는 상한에서 실행을 끝내고 그 상한을 오류로 이름 붙입니다.
비용은 각 단계가 보고한 사용량에 값을 매긴 합이며, 스팬에 값을 매기는 것과 같은
장부를 씁니다. 그래서 토큰을 보고하는 에이전트 단계는 완료되는 순간 실행의 예산에서
차감됩니다.
트레이스 안의 실행
모든 실행은 트레이스이기도 합니다. 데몬은 실행에 대해 루트 스팬 하나를 열고 시도마다
스팬 하나를 열며, 거기에 그 단계의 입력과 출력을 담습니다. 단계 컨텍스트를 통해 자기
스팬을 기록하는 본문은 그 아래에 스팬을 놓습니다. 그래서 에이전트 단계 안의 모델
호출은 그 단계 아래, 그 실행 아래에 놓이고, 어떤 트레이스에서든 동작하는 검색과 비용
산정과 스코어러가 실행의 트레이스에서도 그대로 동작합니다. 그 스팬들은 SDK의 스팬이
거치는 것과 같은 수집 경로로 쓰이므로, 같은 방식으로 샘플링되고, 마스킹되고, 값이
매겨지고, 전달됩니다. 토큰은 그것을 쓴 모델 스팬에 남고, 단계 스팬은 값이 매겨진
합을 자기 속성에 싣습니다. 그래서 트레이스 합계는 모든 토큰을 한 번씩만 셉니다.
실행은 자기 trace_id를 싣고, 인터페이스가 둘을 연결합니다.
데몬은 그 스팬들을 아웃박스를 통해 자기 시계에 맞춰, 그것들을 만든 행이 커밋된 뒤에 씁니다. 단계가 완료된 직후에 읽은 트레이스에는 그 단계의 스팬이 아직 없을 수 있습니다. 실행 자신의 상세에는 언제나 있습니다.
실행 다시 읽기
GET /v1/api/runs는 프로젝트의 실행을 가장 최근에 갱신된 것부터 나열하고,
상태나 정의로 거를 수 있습니다. GET /v1/api/runs/{run}은 그 실행과
그에 대해 지속되는 모든 것입니다. 리스 소유자와 시각과 오류와 사용량을 갖춘 모든 시도,
아직 남아 있는 타이머, 아직 열려 있는 대기입니다. 인터페이스의
워크플로 화면은 이
엔드포인트를 그린 것이고, 그 옆에 저널이 실시간으로 붙습니다.
둘 다 실행이 받은 것을 돌려주므로, 둘 다 감사 로그에 list_runs와
get_run으로 기록됩니다. 저널(list_run_events, 요청마다
한 번이며 스트림도 포함), 원인 추적(get_step_cause),
취소(cancel_run)도 마찬가지입니다. 거부된 읽기는 그 실행을 소유한
프로젝트 앞으로 기록됩니다. 워커 호출은 기록되지 않습니다.
워커를 보십시오.
저장
실행은 트레이스 저장소 옆의 자기 SQLite 파일에 삽니다.
--runs-data가 달리 말하지 않는 한
<data>.runs.<ext>이고,
--runs-data off는 실행 기능을 통째로 끕니다. 그 경우 모든 실행
엔드포인트가 503으로 답합니다. 트레이스 저장소와 함께 백업하십시오.
mcpd doctor는 이것을 별도 그룹으로 채점합니다.
실행 저장소는 페이로드를 보낸 그대로 보관합니다. 실행의 입력, 상태, 출력, 상관
정보, 모든 단계의 입력, 출력, 오류, 모든 이벤트 페이로드, 그리고 트레이스
저장소에 쓰이기를 아직 기다리는 스팬 페이로드는 마스킹되지 않은 채로 저장되고,
실행의 상관 정보에 있는 user_id는 해시되지 않은 채로 저장됩니다.
이것은 의도한 것입니다. 이들은 워크플로의 작업 상태이고, 뒤의 단계가 이것을 다시
읽으며, 마스킹기가 고쳐 쓴 값은 실행이 하는 일을 바꿀 것입니다. 데몬이 트레이스
저장소에 쓰는 스팬은 SDK가 보내는 스팬과 같은 마스킹과 식별자 해시를 거치므로,
트레이스 저장소에는 마스킹된 사본이 있습니다. 실행 파일에는 단계들이 받은 모든
것이 들어 있다고 보고, 그에 맞게 보호하고 백업하십시오.
보존 기간은 실행 저장소에도 적용됩니다. 트레이스 저장소를 정리하는, 한 시간마다 도는 정리가 같은 프로젝트별 기간으로, 루트가 기준 시점보다 먼저 끝난 모든 실행도 지웁니다. 그 자식 실행과, 그것들에 대해 저장된 모든 것도 함께입니다. 시도, 이벤트, 마감 시각, 대기, 큐에 든 스팬, 그리고 그 실행들이 받아 간 이벤트입니다. 끝나지 않은 실행은 아무리 오래되었어도 결코 지워지지 않습니다. 정의는 남습니다.
삭제 요청도 여기에 닿습니다. DELETE /v1/api/subjects/{user_id}는
프로젝트 안의 끝난 실행 가운데 상관 정보에 그 user_id를 담고 있는
모든 실행을 지웁니다. 대조는 실행을 시작할 때 준 값 그대로 합니다. 그 자식 실행의
트리 전체와, 그것들에 대해 저장된 모든 것도 함께 지우며, 그 수를
runs_erased로 보고합니다. 그 사람의 실행 가운데 끝나지 않은 것은
손대지 않고 남겨 두고, runs_in_flight에 세고, 응답의
note에 이름을 밝힙니다.
그 실행을 취소하거나 끝나게 둔 다음, 삭제 요청을 다시 보내십시오. 아직 자기 대기에
이르지 않은 실행을 위해 보류된 이벤트는 사람이 아니라 자기 상관 키로 구분되므로,
삭제 요청이 닿지 않습니다. 그 이벤트는 24시간 뒤 만료될 때 지워집니다. 응답의
나머지는 마스킹과 보존 기간을
보십시오.
다음
- 정의가 말할 수 있는 것: 워크플로.
- 단계를 처리하는 프로세스: 워커.
- 명세에서 생성된 모든 실행 엔드포인트: HTTP API, Runs.