콘텐츠로 이동
테마 선택어두운 테마밝은 테마자동
언어 선택한국어

event reference

AG-UI run은 event의 나열입니다. wire에서는 각각이 JSON 객체입니다. type field에 SCREAMING_SNAKE_CASE 이름이 들어갑니다. Rust에서는 각각이 Event의 variant입니다. EventType은 그 discriminator만 따로 뗀 것입니다.

모두 36개입니다. 그 숫자는 EventType::ALL.len()입니다. cargo run -p xtask -- drift-check가 모든 pull request에서 upstream TypeScript schema의 snapshot과 맞대어 보는 것도 그 숫자입니다. 검증 체계를 보십시오.

두 enum 모두 일부러 exhaustive합니다. 그래서 protocol에 무언가 추가되면 match하는 자리에서 compile error가 납니다. _ 갈래가 삼켜 버리지 않습니다. 설계 원칙이 그 이유와 대가를 설명합니다.

variant마다 자기 이름을 딴 payload struct를 감쌉니다. Event::TextMessageStartTextMessageStartEvent를 싣습니다. 아래로 쭉 내려가며 전부 그렇습니다. payload의 field는 type 옆에 나란히 직렬화됩니다. 어떤 key 아래에 중첩되지 않습니다. 모든 payload는 BaseEvent의 optional field인 timestamp, rawEvent, metadata도 같은 객체 안에 평평하게 함께 싣습니다. metadata는 key로 열린 객체입니다. token 사용량, trace id, application이 실어야 하는 무엇이든 담습니다. 없거나 객체이고, null은 아닙니다. consumer는 event마다의 metadata를 그 event가 만드는 message에 key별로 merge합니다. 마지막 쓰기가 이깁니다. ag_ui::metadata에 규칙과 예약된 key 하나가 있습니다.

아래 순서는 EventType::ALL의 순서이고, 그것이 upstream의 순서입니다.

wire 이름 Rust variant family 의미
TEXT_MESSAGE_START TextMessageStart Text messageId 아래에 text message를 엽니다. role의 기본값은 assistant이고, JSON null은 생략으로 읽힙니다.
TEXT_MESSAGE_CONTENT TextMessageContent Text 열린 message에 delta를 덧붙입니다.
TEXT_MESSAGE_END TextMessageEnd Text message를 닫습니다.
TEXT_MESSAGE_CHUNK TextMessageChunk Text start와 content와 end를 그 자체로 완결된 event 하나로 접은 것.
TOOL_CALL_START ToolCallStart Tool call을 엽니다. tool 이름과, 뒤의 모든 것을 묶는 toolCallId를 답니다.
TOOL_CALL_ARGS ToolCallArgs Tool 인자 JSON의 조각을 덧붙입니다. 조각은 이어 붙습니다. 하나만 떼면 대개 올바른 JSON이 아닙니다.
TOOL_CALL_END ToolCallEnd Tool call을 닫습니다. 인자가 완성되었습니다.
TOOL_CALL_CHUNK ToolCallChunk Tool start와 args와 end를 그 자체로 완결된 event 하나로 접은 것.
TOOL_CALL_RESULT ToolCallResult Tool 그 call의 result. thread에 덧붙는 tool message 형태입니다.
THINKING_START ThinkingStart Thinking (deprecated) thinking block을 엽니다. 제목은 optional입니다. REASONING_START를 쓰십시오.
THINKING_END ThinkingEnd Thinking (deprecated) thinking block을 닫습니다. REASONING_END를 쓰십시오.
THINKING_TEXT_MESSAGE_START ThinkingTextMessageStart Thinking (deprecated) thinking message를 엽니다. REASONING_MESSAGE_START를 쓰십시오.
THINKING_TEXT_MESSAGE_CONTENT ThinkingTextMessageContent Thinking (deprecated) thinking 텍스트를 덧붙입니다. message id를 싣지 않습니다. 그래서 block 하나가 동시에 가질 수 있는 message가 하나뿐이었고, 그것이 교체된 이유입니다.
THINKING_TEXT_MESSAGE_END ThinkingTextMessageEnd Thinking (deprecated) thinking message를 닫습니다. REASONING_MESSAGE_END를 쓰십시오.
STATE_SNAPSHOT StateSnapshot State shared state를 통째로 교체합니다. 자유 형식 JSON이고, protocol에는 불투명합니다.
STATE_DELTA StateDelta State RFC 6902 연산으로 shared state를 patch합니다. 순서대로 적용됩니다.
MESSAGES_SNAPSHOT MessagesSnapshot State message 이력을 교체합니다. 재연결 후, 또는 agent가 이력을 다시 쓸 때.
ACTIVITY_SNAPSHOT ActivitySnapshot Activity client가 정의한 activityType 아래로 activity의 내용을 발행합니다. replace의 기본값은 true입니다.
ACTIVITY_DELTA ActivityDelta Activity RFC 6902 연산으로 activity의 내용을 patch합니다.
RAW Raw Escape hatch provider event를 그대로 전달합니다. source는 optional입니다.
CUSTOM Custom Escape hatch 이름이 붙은, 애플리케이션이 정의한 event. protocol이 보증하는 것은 봉투뿐입니다.
RUN_STARTED RunStarted Lifecycle 모든 run의 첫 event. threadId, runId, 그리고 optional로 부모 run과 그 run을 시작시킨 입력.
RUN_FINISHED RunFinished Lifecycle run이 실패 없이 끝났습니다. outcome이 성공과 interrupt를 구분합니다. interrupt는 사람의 입력을 기다리며 멈춘 run입니다.
RUN_ERROR RunError Lifecycle run이 실패했습니다. 뒤따르는 것은 없습니다.
STEP_STARTED StepStarted Lifecycle run 안에서 이름 붙은 step을 엽니다.
STEP_FINISHED StepFinished Lifecycle 그 step을 닫습니다.
REASONING_START ReasoningStart Reasoning 어느 message id에 대한 reasoning block을 엽니다.
REASONING_MESSAGE_START ReasoningMessageStart Reasoning reasoning message를 엽니다. TEXT_MESSAGE_START와 달리 role이 필수이고, 언제나 reasoning입니다.
REASONING_MESSAGE_CONTENT ReasoningMessageContent Reasoning reasoning 텍스트를 덧붙입니다.
REASONING_MESSAGE_END ReasoningMessageEnd Reasoning reasoning message를 닫습니다.
REASONING_MESSAGE_CHUNK ReasoningMessageChunk Reasoning start와 content와 end를 그 자체로 완결된 event 하나로 접은 것.
REASONING_END ReasoningEnd Reasoning reasoning block을 닫습니다.
REASONING_ENCRYPTED_VALUE ReasoningEncryptedValue Reasoning provider의 불투명한 reasoning blob. zero-data-retention 모드를 위한 것입니다. subtypeentityIdtool-call을 가리키는지 message를 가리키는지 말합니다.
SUBAGENT_STARTED SubagentStarted Subagent subagentRunId 아래로 subagent 호출을 announce합니다. 표시용 name을 답니다. optional로 description, 바깥 parentSubagentRunId, 그리고 이 호출을 낳은 parentToolCallId / parentMessageId.
SUBAGENT_FINISHED SubagentFinished Subagent 호출을 닫습니다. outcomesuccess 또는 suspended입니다. 후자는 subagent가 소유한 interruptIds를 댑니다. 없으면 success로 읽습니다. resultRUN_FINISHED.result에 대응합니다.
SUBAGENT_ERROR SubagentError Subagent 호출이 실패했습니다. 사람을 위한 message와 optional인 기계 판독용 code.

Text 4개, Tool 5개, deprecated된 Thinking 5개, State 3개, Activity 2개, Escape hatch 2개, Lifecycle 5개, Reasoning 7개, Subagent 3개입니다.

lifecycle event 셋 말고도, 36개 type 중 24개가 자기를 만든 subagent를 가리키는 optional subagentRunId를 싣습니다. text, tool, state, activity, reasoning, step family, 그리고 RAWCUSTOM입니다. 이 field가 없는 event는 부모 agent의 것입니다. 그래서 이 field를 한 번도 쓰지 않는 stream은 subagent가 생기기 전의 stream과 정확히 같습니다. 실을 수 없는 아홉 개는 run lifecycle(RUN_STARTED, RUN_FINISHED, RUN_ERROR), 안의 message가 각자 자기 것을 싣는 MESSAGES_SNAPSHOT, 그리고 deprecated된 THINKING_* 다섯 개입니다. EventType::is_attributable이 그 목록을 method로 답합니다. Event::subagent_run_id는 어떤 event에서든 tag를 읽습니다. 그것으로 무엇을 하는지는 subagent에 있습니다.

type이 tag이고, payload는 그 옆에 평평하게 놓입니다.

use ag_ui::{Event, EventType};
fn main() {
// protocol이 정의하는 모든 event type, upstream 순서 그대로.
assert_eq!(EventType::ALL.len(), 36);
// discriminator는 양방향 모두 wire 이름입니다.
assert_eq!(EventType::TextMessageContent.as_str(), "TEXT_MESSAGE_CONTENT");
assert_eq!(
"TEXT_MESSAGE_CONTENT".parse::<EventType>().unwrap(),
EventType::TextMessageContent,
);
let event = Event::text_message_content("msg-1", "Hello");
assert_eq!(event.event_type(), EventType::TextMessageContent);
assert_eq!(
serde_json::to_string(&event).unwrap(),
r#"{"type":"TEXT_MESSAGE_CONTENT","messageId":"msg-1","delta":"Hello"}"#,
);
}

이 build가 모르는 event type은 deserialize에 실패합니다. 의도된 것입니다. 더 새로운 agent와 이야기하는 frontend는 모르는 type의 이름을 대며 error로 멈춥니다. 대화의 4분의 3만 조용히 그리지 않습니다.

다섯 개 모두 여전히 protocol에 있고, 여전히 parse되고, 그 변경보다 앞선 producer가 여전히 emit합니다. 그래서 여기에 있고, SDK도 이들을 싣습니다. REASONING_* event가 이들을 대체합니다. 대체본은 원본이 물러난 이유를 고칩니다. THINKING_TEXT_MESSAGE_CONTENT는 message id를 싣지 않습니다. 그래서 thinking block은 동시에 message 하나만 가질 수 있었습니다.

Rust variant와 payload struct에는 #[deprecated]가 붙습니다. ag-ui 자신의 event module은 #![allow(deprecated)]를 답니다. 이 module은 union에서도, event_type()에서도, factory에서도 이 type들의 이름을 대야 합니다. spec을 쓰인 대로 구현했다고 자기 자신에게 경고하는 것은 아무에게도 도움이 안 됩니다. 이 억제는 그 module 안에서만 유효합니다. 그래서 이들 중 하나를 쓰는 consumer는 자기 사용 지점에서 경고를 받습니다. 계속 쓸지 정하는 자리가 거기입니다.

Event::is_deprecated는 match 없이 runtime에 답합니다.

use ag_ui::Event;
fn main() {
let event: Event = serde_json::from_str(r#"{"type":"THINKING_END"}"#).unwrap();
assert_eq!(event.event_type().as_str(), "THINKING_END");
assert!(event.is_deprecated());
let current = Event::reasoning_end("msg-1");
assert!(!current.is_deprecated());
}

event 세 개가 start와 그 content와 end를 그 자체로 완결된 event 하나로 접습니다. TEXT_MESSAGE_CHUNK, TOOL_CALL_CHUNK, REASONING_MESSAGE_CHUNK입니다. 출력을 짝으로 묶을 수 없는 producer를 위해 존재합니다. 대부분의 provider adapter가 그렇습니다. upstream API가 message의 끝을 다음 message가 시작되기 전에는 알려 주지 않기 때문입니다.

id와 이름은 첫 chunk에만 실립니다. 그래서 한 stream의 끝은 다음 stream의 시작에서, 아니면 run의 끝에서만 알 수 있습니다.

TEXT_MESSAGE_CHUNK { messageId: "msg-1", delta: "Hel" }
TEXT_MESSAGE_CHUNK { delta: "lo" }
TEXT_MESSAGE_CHUNK { messageId: "msg-2", delta: "Bye" } <- msg-1이 방금 끝났습니다

소비하는 쪽에서 그 장부 정리는 ag_ui::client::chunks가 맡습니다. 연달아 이어진 chunk를 다른 무엇이 보기 전에 start/content/end 세 짝으로 되펼칩니다. emit하는 쪽에는 일부러 handle이 없습니다. ag_ui::server의 typestate emitter는 연 것이 닫히도록 보장하려고 있습니다. chunk에는 닫을 것이 없습니다. RAII handle로 감싸면 틀릴 방법만 하나 늘어납니다. 이들은 ctx.emit으로 emit하십시오. API를 기다리는 빈틈이 아니라 지원되는 경로입니다.

뒤섞인 병렬 tool call이 ctx.emit에 속하는 나머지 사례입니다. ToolCallHandle 두 개를 동시에 여는 것은 설계상 borrow check error입니다. 그래서 args(a) args(b) args(a) end(a) end(b)를 흘리는 provider를 call당 handle 하나로 그대로 옮길 수 없습니다. 방법은 둘입니다. call마다 인자를 모아 두었다가 완성되면 통째로 emit하십시오. 두 call의 인자가 서로 섞여 들어갈 수 없는 유일한 매핑입니다. 아니면 뒤섞인 그대로 직접 emit하십시오. ordering verifier는 모든 것을 id로 색인하므로 뒤섞인 stream을 받아들입니다. 허락하지 않는 것은 열지 않은 call을 닫는 일입니다. 검증 체계를 보십시오.

protocol은 protobuf encoding도 정의합니다. 그것은 손실 있는 부분집합입니다. upstream events.protoEvent message는 36개 type 중 21개만 담는 oneof입니다.

TEXT_MESSAGE_START, TEXT_MESSAGE_CONTENT, TEXT_MESSAGE_END, TEXT_MESSAGE_CHUNK, TOOL_CALL_START, TOOL_CALL_ARGS, TOOL_CALL_END, TOOL_CALL_CHUNK, STATE_SNAPSHOT, STATE_DELTA, MESSAGES_SNAPSHOT, RAW, CUSTOM, RUN_STARTED, RUN_FINISHED, RUN_ERROR, STEP_STARTED, STEP_FINISHED, SUBAGENT_STARTED, SUBAGENT_FINISHED, SUBAGENT_ERROR입니다.

나머지 15개는 binary 표현이 아예 없습니다. REASONING_* 일곱 개 전부, ACTIVITY_* 두 개 모두, deprecated된 THINKING_* 다섯 개 전부, 그리고 TOOL_CALL_RESULT입니다. reasoning을 하거나, activity를 보고하거나, tool result를 돌려주는 agent는 자기 stream을 그 형식으로 표현할 수 없습니다. 대부분의 agent가 그렇습니다.

그래서 ag-ui는 그중 무엇도 encode하지 않습니다. protobuf feature는 build가 media type을 협상하고 그 이름을 댈 수 있도록 존재합니다. formatter의 encode는 언제나 Error::UnsupportedTransport로 실패합니다. protocol의 절반 가까이를 조용히 버리는 것은 거절하는 것보다 나쁩니다. 36개를 모두 싣는 SSE를 쓰십시오. encode::protobuf module은 다뤄지는 집합을 COVERED_EVENT_TYPES로 나열하고 is_covered를 제공합니다. 그래서 주어진 stream이 binary transport에서 살아남았을지 test로 단언할 수 있습니다.

port를 proto 정의가 아니라 TypeScript Zod schema를 보고 쓴 이유도 이것입니다. 36개 중 15개가 빠진 진실의 원천은 원천 노릇을 할 수 없습니다.