중간에 끼어들기: 이 턴, 다음 턴, 아니면 거절
Agent가 세 번째 파일을 고치는 중이에요. 방향이 빗긴 게 보여 “설정은 YAML로”를 보탭니다. 엔터 뒤 이 한 줄이 일 시작인지, 현재 턴에 끼워 넣는 것인지, 그 자리에서 거절인지, Core가 바로 판정하고 모델이 입을 열 때까지 기다리지 않아요.
현재 턴
다음 턴
문앞 거절
- TurnInputMode로 분배, 기본은 StartOrSteerturn_input.rs L141
- 먼저 steer_input, NoActiveTurn일 때만 일 시작turn_input.rs L195
- Regular만 받고, Review와 Compact는 그 자리에서 거절turn_input.rs L507
- 한가하면 apply_started 다음 spawn_taskturn_input.rs L242
- 끼어들기는 pending에 쓰고 위상을 CurrentTurn으로 되돌립니다turn_input.rs L558
- 최종 답은 전달 위상을 NextTurn으로 deferinput_queue.rs L206
- 도구 항목은 위상을 CurrentTurn으로 다시 acceptstream_events_utils.rs L302
- get_pending_input이 위상을 보고 우편함을 켤지 정해요input_queue.rs L297
일상 결말 세 가지가 다 불편해요. 지금 끊으면 앞 파일 둘의 수정이 반제품으로 디스크에 남을 수 있고, 다음 턴으로 미루면 남은 파일을 옛 방향으로 고치는 것을 봐야 합니다. 현재 컨텍스트에 넣되 루프를 깨우지 않으면 모델은 다음에 스스로 입을 열 때서야 보고, 당신이 보탠 제약은 늦은 셈이에요.
Codex는 이걸 입구 하나, 모드 셋으로 거둬요. 호출 쪽은 StartOrSteer, StartIfIdle, Steer를 고르고 start를 직접 외치지 않습니다. Core는 현장의 바쁨/한가함과 태스크 종류로 판정해 Started, Steered, NotSubmitted를 즉시 돌려줘요. 결정을 돌리면 끝. user-prompt hook도, 히스토리 기록도, 모델 샘플링도 기다리지 않아요.
출처: codex-rs/core/src/session/turn_input.rs 1–9행; codex-rs/protocol/src/turn_input.rs 127–136행
StartOrSteer의 순서는 함수 이름과 같아요. 먼저 끼어들기를 시도하고, NoActiveTurn일 때만 apply_started 다음 spawn_task. 다른 거절 이유는 그대로 NotSubmitted로 싸고, 몰래 일을 시작하지 않아요. TUI 실시간 음성도 이 길을 가며 기본 입구와 같은 판정을 씁니다.
출처: codex-rs/core/src/session/turn_input.rs 141–156행, 195–249행
설정은 판정 전에 바꾸면 안 돼요. prepare가 스레드 설정을 먼저 미리 보고, 실패하면 바로 InvalidRequest. 진짜 쓰기는 apply_started 또는 apply_steered에서. 거절된 입력은 설정조차 안 바꿉니다. 끼어들기가 성공하면 지속 설정만 착지하고, 현재 턴의 TurnContext는 안 바뀌어요. 일 시작 전용 옵션, 예를 들어 구조화 출력 schema는 Started에서만 씁니다.
출처: codex-rs/core/src/session/turn_input.rs 58–80행
일 시작과 끼어들기는 같은 잠금 안에서 끝내야 해요. 활동 턴이 있는지, kind, 그다음 pending 쓰기. 중간에 다른 제출이 턴을 바꾸면 안 됩니다. 설정은 미리 본 뒤에 쓰는데, 거절 경로가 스레드를 그대로 둬야 하거든요. 판정과 입큐는 잠금을 두 번 잡아요. 사용자 엔터와 서브 메일이 같이 오면, 판정 때는 한가했는데 입큐 때는 이미 자리가 찬 창이 생길 수 있습니다.
검토 태스크는 one-shot 자식 대화를 따로 열고, 압축 태스크는 창을 바꾸는 중이에요. 이때 사용자가 “YAML로”를 보태면 현재 턴의 도구 루프가 받을 곳이 없습니다. 그 때문에 일반 대화를 새로 열면 검토 결과와 압축 요약이 새 대화와 같은 active_turn을 다퉈요.
steer_input은 active_turn 잠금을 잡고 검사를 모두 끝냅니다. 활동 턴이 없거나 슬롯에 task가 없으면 NoActiveTurn. TaskKind는 Regular, Review, Compact 셋뿐. 검토와 압축은 ActiveTurnNotSteerable을 돌려주고, StartOrSteer도 그 때문에 일을 시작하지 않아요.
출처: codex-rs/core/src/session/turn_input.rs 507–519행; codex-rs/core/src/state/turn.rs 67–72행
검사를 통과하면 사용자 입력은 pending_input에 들어가고 우편함 위상은 CurrentTurn으로 돌아가요. 여기 exhaustive match에는 비즈니스 결과가 있어요. 태스크 종류를 하나 더하면 컴파일러가 끼어들기를 받을 수 있는지 답하라고 압박합니다.
출처: codex-rs/core/src/session/turn_input.rs 546–564행
내부 태스크는 자기 수명이 있어요. 사용자 끼어들기를 검토 턴에 용접하면 두 일이 같은 실행 슬롯을 다툽니다. 호출 쪽은 거절을 받고, 이 입력은 새 대화에 몰래 줄 서지 않아요. 다른 언어로 다시 써도 지켜야 할 것은 같아요. 끼어들기를 받을 수 있는 태스크와 못 받는 태스크는 타입에서 갈라야 합니다.
메인 agent가 화면에 최종 답처럼 보이는 글을 이미 찍었고, 서브 agent가 동시에 진행을 보내요. 합치면 사용자가 이미 본 답이 이어 쓰입니다. 무조건 다음 턴을 기다리면 서브 agent 결과는 샘플링 한 번을 건너뛰고서야 모델에 들어갈 수 있어요.
사용자 끼어들기는 턴 안 pending_input으로 들어가요. 서브 agent 편지는 세션급 mailbox_pending_mails. 재고 두 세트가 현재 턴에 합쳐질지는 MailboxDeliveryPhase가 정합니다. 위상은 CurrentTurn에서 시작해, 사용자가 최종 답을 본 뒤 NextTurn으로 넘어가요. 사용자가 한 줄 더 끼우거나 모델이 도구를 다시 부르면 위상이 다시 열립니다.
출처: codex-rs/core/src/state/turn.rs 37–56행
NextTurn으로 넘기는 데 예외가 하나 있어요. pending_input에 “줄만 서고 깨우지 않는” 서브 메일이 아닌 것이 남아 있으면 현재 위상을 유지합니다. 편지를 꺼낼 때도 이 카드를 봐요. NextTurn이면 턴 안 pending도 안 가져가고 세션 우편함은 더더욱 안 켭니다. CurrentTurn이면 턴 안 pending을 먼저 가져가고, 세션 우편함을 꺼내 뒤에 붙입니다. 그래서 최종 답이 착지한 뒤 서브 메일은 우편함에 누워 있어도, 루프는 처리할 입력이 없다고 보고 이 턴을 접어요.
출처: codex-rs/core/src/session/input_queue.rs 206–227행, 284–336행
사용자가 보는 최종 답은 무엇일까요? 어시스턴트 본문. phase가 Commentary인 것은 안 세고, trim 뒤 빈 것도 안 세요. 표시 없는 어시스턴트 메시지는 최종 답으로 취급합니다. 표시 없는 제공자는 더 안전한 길을 기본으로 해요. 우편함을 다음 턴으로 먼저 닫기. 승인·권한·질문·elicitation·동적 도구는 독립 oneshot 표 다섯 장으로, 이 우편함에 안 들어가고 각자 영수증을 기다려요.
출처: codex-rs/core/src/stream_events_utils.rs 486–501행; codex-rs/core/src/state/turn.rs 87–103행
“답을 사용자에게 이미 보여 줬다”는 제품 경계예요. Rust나 특정 우편함 구현에 묶이지 않습니다. 다른 언어로 다시 써도 뒤집개는 필요해요. 늦은 부대 메시지는 이미 화면에 오른 답을 기본으로 이어 쓰지 않고, 분명한 같은 턴 일 — 사용자가 한 줄 더 끼우거나 모델이 도구를 다시 부름 — 이 문을 다시 엽니다.
DSH: 파라미터 둘, 궤도 둘
DSH는 바깥에 별칭 셋, 아래는 send를 공유해요. 목표 큐와 깨울지는 직교 파라미터 둘. followup은 턴을 혼자 차지하고 깨우고, steer는 다음 정거장에 끼워 깨우며, inject는 타기만 하고 기사를 재촉하지 않아요. Inbox는 next-turn과 next-step 두 지속 목록. claim은 next-step을 먼저 비우고, 목표가 next-turn이면 하나를 더 가져갑니다.
Codex에는 공개 함수 셋이 없어요. StartOrSteer는 한가할 때 일 시작과 바쁠 때 끼어들기를 한 판정에 용접합니다. 위상 뒤집개는 DSH에 없어도 되는데, 턴 전체 대기와 다음 정거장 대기를 목록 둘로 썼거든요. 대가는 답이 화면에 오른 뒤에도 next-step이 비어 있지 않으면 현재 Turn을 이어 살린다는 것.
Claude Code: 큐 하나, 우선순위로 타이밍을 메워요
복원 소스에서 사용자 입력·태스크 알림·고아 권한은 같은 commandQueue를 가요. 우선순위는 now > next > later, 같은 등급은 FIFO. 사용자 명령은 기본 next, 태스크 알림은 later라 사용자 입력이 시스템 메시지에 굶지 않아요. 소비는 현재 스트림이 끝난 뒤라 스텝급 끼어들기는 없습니다. 당신이 보탠 YAML 제약은 현재 제너레이터가 끝난 뒤에야 모델에 들어갑니다.
답이 화면에 오른 뒤, 이 서브 메일은 언제 보이나요
최종 답이 착지한 뒤 trigger_turn: false 서브 메일을 먼저 넣고, FunctionCall 하나를 먹이세요. 종이 위에서 get_pending_input이 무엇을 돌려줘야 하는지 밀어 보세요.
대조 경로: 본문이 기록될 때 위상이 NextTurn으로 넘어가 이 편지는 안 보여요. 도구 항목이 도착하면 위상이 다시 열리고, 다음 바퀴에야 꺼내 같은 턴의 다음 모델 요청에 합칩니다.