처리 모델
처리 모델 (Processing Model)
무엇이 살아남고, 무엇이 치명적이며, 미들웨어가 강제(enforcement)에 비해 어디에 위치하는지 설명해 드릴게요 — 1.0 기준이에요. 표면적으로 비슷해 보이지만 절대 혼동해서는 안 되는 두 가지가 있어요: 프로토콜이 서술하지 않는 자료와, 프로토콜이 서술하지만 틀린 값.
출처: 문서
본문
표면적으로 비슷해 보이고 절대 혼동해서는 안 되는 두 가지가 있어요: 프로토콜이 서술하지 않는 자료, 그리고 프로토콜이 서술하지만 틀린 값. 첫 번째는 프로토콜이 성장하는 방식이에요. 두 번째는 버그입니다.
규칙
인식되지 않은 자료는 오류가 아니에요. 형식이 잘못된 알려진 값은 오류예요.
- 이 구현이 인식하지 못하는 타입의 이벤트는 실행을 중단해서는 안 됩니다(MUST NOT).
- 이벤트에 서술되지 않은 속성은 실행을 중단해서는 안 됩니다(MUST NOT).
- 구현이 인식하지 못하는 유니언 멤버 — 새 종류의 콘텐츠 파트, 그것이 출시된 뒤 이름 붙은 outcome — 는 실행을 중단해서는 안 됩니다(MUST NOT).
- 프로토콜이 서술하는 필드가 스키마가 거부하는 값을 담는 것은 치명적이어야 합니다(MUST): 컨슈머는 값을 수리·강제·무시하기보다 실행을 실패시켜야 해요(MUST).
추론은 의도적으로 비대칭이에요. 구현이 출시된 뒤 추가된 이벤트 타입은 예상돼요 — 보내는 쪽이 더 새롭고, 더 오래된 쪽이 그것을 무효라고 부를 근거가 없죠. 하지만 messageId가 숫자를 담는 것은 더 새 프로토콜이 아니라 결함이고, 조용히 강제하는 컨슈머는 그것을 진단하기 더 어려운 무언가로 표면화될 때까지 숨깁니다.
모든 전송이 같은 방식으로 답한다
이 규칙들은 와이어 형식이 아니라 프로토콜을 묶어요. 둘 이상의 전송을 지원하는 컨슈머는, 어떻게 도착했든 같은 이벤트에 대해 같은 판정에 도달해야 해요(MUST): 스트림이 JSON으로 살아남으면 protobuf로도 살아남아야 하고(MUST), 실패하면 같은 방식으로 실패해야 합니다(MUST) — 이진 인코딩 자체가 판정이 의존하는 차이를 표현할 수 없는 경우는 제외하고, 이 페이지가 아래에 이름 붙이는 두 경우(이름 붙일 수 없는 봉투 변형과 protobuf가 구체화하는 부재)예요.
그것은 리더가 스스로 결정할 수 있는 것에 한계를 둡니다. 바이트를 이벤트로 바꾸는 것은 리더의 전부의 일이에요 — 어떤 이벤트로도 매핑되지 않는 바이트를 가진 프레임은 디코드 실패이고, 그 판단은 적절히 리더의 몫입니다. 성공적으로 읽힌 이벤트가 구현이 인식하는 것을 담는지 여부는 아니에요: 그 질문은 모든 전송이 공유하는 한 단계에 속합니다. 검증도 하는 리더는 자기 전송을 다른 것들보다 엄격하게 더 가혹하게 만들고, 프로듀서의 콘텐츠 타입 선택이 그때 실행이 살아남을지 결정합니다.
이진 경우는 하나의 배려가 필요해요. 텍스트 리더는 알 수 없는 타입의 이벤트를 넘겨줄 수 있어요, 타입이 문자열로 여행하고 공유 단계가 이름으로 그것을 버릴 수 있으니까요. 이진 리더는 할 수 없어요: 그것이 컴파일되지 않은 봉투 변형은 넘겨줄 이름이 없어요. 그러한 프레임은 실행을 실패시키기보다 경고와 함께 버려져야 합니다(MUST) — 그것은 공유 단계가 주었을 같은 답을, 그 질문을 연기할 수 없는 전송을 위해 철자한 것이에요.
이진 인코딩은 필드에 존재감을 주지 않는 곳이라면 어디서든 이것을 지킬 수 없어요. protobuf에서 그것은 필수 스칼라와 필수 목록을 모두 덮어요: 프로듀서가 생략한 필수 문자열은 빈 문자열로 도착하고, 필수 배열은 빈 배열로 도착하며, 각각 같은 이벤트가 JSON으로는 형식 오류로 거부되는 곳에서 받아들여집니다. 스키마는 이미 그것들을 생략하는 것을 금지하므로, 이것은 적합한 프로듀서가 보낼 수 있는 것을 바꾸기보다 결함 있는 프로듀서가 어떻게 진단되는지를 바꿔요.
컨슈머가 인식되지 않은 자료로 무엇을 하는가
이벤트를 애플리케이션 코드에 노출하는 컨슈머는 인식되지 않은 자료를 프로토콜이 서술한 것처럼 전달해서는 안 됩니다(MUST NOT). 그것을 읽었다면, 그러한 컨슈머는 다음 중 하나를 해야 해요(MUST):
- 버리기 (Drop), 이벤트 전체가 인식되지 않은 타입인 경우. 컨슈머는 타입을 이름 붙이는 경고를 내보내야 해요(SHOULD).
- 떼어내기 (Strip), 이벤트가 그 외에는 유효한데 알 수 없는 속성이나 인식되지 않은 유니언 멤버인 경우. 컨슈머는 제거된 경로를 이름 붙이는 경고를 내보내야 해요(SHOULD).
컨슈머는 떼어낸 속성이 애플리케이션 코드가 받는 값에 살아남게 해서는 안 되고(MUST NOT), 버려진 이벤트가 그것에 전혀 닿게 해서는 안 됩니다(MUST NOT).
이것은 스키마가 객체를 닫는 곳에만 적용돼요, 그리고 그것은 프로토콜 자신의 하나를 서술하는 모든 곳입니다. 스키마가 의도적으로 객체를 열어 두는 곳에서, 그것이 서술하지 않는 멤버는 전혀 인식되지 않은 자료가 아니며 보존되어야 해요(MUST). JSON Patch 연산이 그 경우예요: RFC 6902 섹션 4는 연산이 정의하지 않는 멤버를 거부하기보다 무시하도록 요구하므로, 남은 value를 담은 remove는 유효한 패치이고 온전히 도착합니다. 거기서 떼어내는 컨슈머는 적합한 데이터를 삭제하고, 그것이 내는 경고는 아무것도 아닌 것에 관한 것이에요.
이것은 파서가 아니라 파이프라인을 묶어요. 이벤트만 파싱하는 라이브러리 레이어 — 각 SDK의 생성 모델 같은 것 — 은 알 수 없는 속성을 의도적으로 유지해서, 그 위의 미들웨어가 여전히 그것들을 볼 수 있게 합니다. 그러한 파서를 강제 단계 없이 제공하는 SDK는 아직 이 규칙을 만족하지 못해요; 오늘날 그것은 Python과 .NET인데, 그 모델들은 애플리케이션 코드 앞에서 떼어내는 단계 없이 관대하게 파싱합니다.
참조 구현이 아직 충족하지 못한 한 가지 경우가 있어요: 스키마가 닫힌 문자열 값 집합인 필드 — 메시지
role같은 것 — 는 리프(leaf)로 검사되므로, 거기서의 인식되지 않은 값은 떼어내지는 않지만 치명적입니다. .NET 변환기들은 알 수 없는 역할이나 outcome을 같은 방식으로 거부해요. 위의 규칙은 프로토콜이 요구하는 것이고; 둘 다 그것에 대한 간극이지, 그것의 완화가 아니에요.
떼어내기는 끝까지 닿으며, 그것이 제거하는 것은 인식할 수 없는 값이 놓인 위치에 달려요:
- 선택적(optional) 위치에 있으면, 그 필드가 제거되고 주변의 모든 것이 살아남습니다.
- 필수(required) 위치에 있으면, 그것을 담는 값이 대신 제거됩니다. 소스가 이 구현이 한 번도 들어본 적 없는 종류인 미디어 파트는 전체가 제거되어요 — 소스 없는 파트를 남기면 검증자가 형식 오류로 취급해야 할 것을 주어, 추가를 치명적 오류로 바꾸게 되니까요.
- 목록(list) 안에 있으면, 인식할 수 없는 요소가 버려지고 목록은 살아남습니다.
미들웨어가 어디에 위치하는가
미들웨어 — 컨슈머가 스트림을 번역·관찰·재작성하기 위해 설치하는 무엇이든 — 는 강제 전에 실행되어야 하고(MUST), 강제는 검증 전에 그리고 애플리케이션 코드가 무엇이든 보기 전에 실행되어야 합니다(MUST).
producer → compatibility boundary → middleware → enforcement
→ chunk expansion → verification → application
호환성 경계는 프로토콜이 은퇴시킨 형태를 위한 항상 켜진 번역자예요. 그것은 스트림을 먼저 봐야 해요(MUST), 은퇴된 형태가 청크일 수 있고, 그것을 더 일찍 만난 단계는 그것에서 실패하거나 경계가 더 이상 인식할 수 없는 무언가로 바꾸기 때문이에요. 미들웨어 체인은 자기 자신의 단계 사이에서 청크를 확장할 수 있어요(MAY), 각 단계가 start/content/end 형태를 받도록요 — 참조 구현의 체이닝 헬퍼가 정확히 그렇게 합니다. 그러므로 미들웨어는 두 형태 모두에 대비해야 해요(MUST): 그것이 보는 것은 체인에서 어디에 앉았는지에 달려 있으니까요.
단계 사이의 확장은 한 보장을 좁히고, 그 좁아짐은 숨김보다 명시됩니다: 확장은 프로토콜이 정의하는 것만 담는 이벤트를 합성하므로, 청크 위의 인식되지 않은 필드들은 오직 위의 정규 순서에서만 강제까지 살아남고 — 그리고 그 경고를 얻습니다. 일찍 확장하는 체인은 그것들을 조용히 잃습니다. 적합한 프로듀서는 결코 그러한 필드를 내보내지 않으므로, 잃는 것은 일찍 확장하는 체인을 건너는 더 새 프로토콜 버전의 자료입니다. 확장이 전달하러 존재하는 자료 — 델타, 오프너 필드, 메타데이터, rawEvent, 귀속 — 는 온전히 실려 나릅니다; 청크의 timestamp는 그렇지 않은데, 합성된 이벤트가 청크가 아니기 때문이고, 알 수 없는 필드처럼 오직 정규 순서에서만 확장 전에 판단됩니다.
청크는 그 자체로 이벤트이고, 그렇게 판단되어야 해요(MUST): 어느 단계든 먼저 형식이 잘못된 청크를 만나면(정규 순서의 강제, 일찍 확장하는 미들웨어 체인의 확장) 그것을 수리하지 말고 거부해야 하고(MUST), 강제가 거부할 형식이 잘못된 알려진 값을 정규화·기본화·은폐해서는 안 됩니다(MUST NOT). 형식이 잘못된 역할에 기본값을 대입한 컨슈머는 같은 프로듀서 결함을 그대로 보내면 치명적으로, 청크로 보내면 보이지 않게 만들었으므로, 프로듀서가 우연히 쓴 형태가 그것의 버그가 보고됐는지 결정했어요.
검증은 확장 후에 실행되어야 해요(MUST), 그것이 검사하는 것 — 메시지와 호출이 계속되기 전에 열리고 끝난 뒤 닫히는 것 — 이 오직 청크가 그것이 나타내는 이벤트가 된 뒤에만 존재하기 때문이에요.
순서는 양방향으로 하중을 견디는(load-bearing) 것이에요:
- 미들웨어는 강제가 그것을 제거하기 전에 자료를 봐야 해요(MUST). shim은 정확히 현재 프로토콜이 서술하지 않는 형태를 번역하기 위해 존재하는데, 강제가 그 형태를 먼저 떼어내면 shim은 번역할 것이 아무것도 남지 않은 이벤트를 받고, 실행은 아무도 복구할 수 없는 데이터를 잃게 됩니다.
- 애플리케이션 코드는 강제가 제거할 자료를 봐서는 안 됩니다(MUST NOT). 번역자들이 기회를 가진 후, 여전히 인식되지 않은 것은 인식되지 않은 것이고, 그것은 거기서 멈춥니다.
컨슈머는 애플리케이션 코드를 위해 이벤트를 만드는 모든 경로에 이 순서를 적용해야 해요(MUST), 재연결과 재생을 포함해서요. 강제를 건너뛰는 경로는 보장의 구멍이지, 최적화가 아니에요.
나가는 자료
같은 경계가 컨슈머가 보내는 것에 적용됩니다. 전송 직전에, 컨슈머는 실행 입력에서 인식되지 않은 자료를 제거해야 하고(MUST), 무엇을 제거했는지 경고해야 하며(SHOULD), 무엇이든 보내기 전에 형식이 잘못된 알려진 필드를 치명적으로 취급해야 해요(MUST).
실행 입력을 받는 프로듀서는 같은 비대칭을 그것에 적용해야 해요(MUST): 인식되지 않은 자료는 입력을 무효로 만들지 않고, 형식이 잘못된 알려진 필드는 그렇게 합니다.
프로듀서가 해서는 안 되는 것
프로듀서는 컨슈머가 무시하리라 기대하며 프로토콜이 서술하지 않는 이벤트 타입을 내보내서는 안 됩니다(MUST NOT). 인식되지 않은 이벤트는 프로토콜이 추가할 수 있도록 살아남는 것이지, 프로듀서가 스트림을 통해 사적 신호를 몰래 통과시키도록 살아남는 게 아니에요; CUSTOM과 RAW가 그 목적으로 존재하고 스키마가 서술해요.
더 알아보기 (Learn more)
- 스펙 개요 — AG-UI 1.0 프로토콜의 공식 요구사항을 확인해 보세요.
- 전송 (Transports) — 이 처리 규칙이 전송에 어떻게 적용되는지 살펴보세요.
- 이벤트 스트림 — 여덟 이벤트 패밀리를 확인해 보세요.