ag_ui/client/verify.rs
1//! Client-side protocol verification.
2//!
3//! The TypeScript SDK puts its verifier on the *client*, and that is the right
4//! instinct for a consumer: the events arrive from someone else's process, and
5//! a stream that breaks the rules should produce one clear error rather than a
6//! confused UI. This module is that check, as an ordering state machine.
7//!
8//! [`crate::client::Session`] runs it by default. Turn it off with
9//! [`SessionBuilder::verify`](crate::client::SessionBuilder::verify) when talking to a
10//! producer whose quirks you have decided to live with.
11//!
12//! ```
13//! use ag_ui::client::verify::Verifier;
14//! use ag_ui::Event;
15//!
16//! let mut verifier = Verifier::new();
17//! verifier.verify(&Event::run_started("thread-1", "run-1"))?;
18//!
19//! // Content for a message that was never opened.
20//! let orphan = Event::text_message_content("msg-1", "Hello");
21//! assert!(verifier.verify(&orphan).is_err());
22//! # Ok::<(), ag_ui::client::Error>(())
23//! ```
24//!
25//! # The rules
26//!
27//! 1. `RUN_STARTED` opens the stream, and does so exactly once. Only `RAW` and
28//! `CUSTOM` may precede it.
29//! 2. `RUN_FINISHED` and `RUN_ERROR` close it. Nothing may follow.
30//! 3. `TEXT_MESSAGE_CONTENT` and `TEXT_MESSAGE_END` require an open message
31//! with the same id, and `TEXT_MESSAGE_START` may not re-open an id that is
32//! already open.
33//! 4. The same, for `TOOL_CALL_*` and for `REASONING_MESSAGE_*`.
34//! 5. `TOOL_CALL_RESULT` may not answer a call that has not ended.
35//! 6. `STEP_FINISHED` requires a matching `STEP_STARTED`, and step names do not
36//! nest with themselves.
37//! 7. Everything open must be closed before `RUN_FINISHED`.
38//! 8. An `interrupt` outcome must carry at least one interrupt — the one rule
39//! the type system cannot express, checked by
40//! [`RunOutcome::validate`](https://kimsoungryoul.github.io/ag-ui-rust/api/ag_ui/outcome/enum.RunOutcome.html#method.validate).
41//! 9. A continuation, terminator or re-open that *names* a subagent must name
42//! the one that opened the entity — a message, a reasoning block or the
43//! message inside it, a tool call, an activity, or whatever a
44//! `REASONING_ENCRYPTED_VALUE` attaches to. One that names none is
45//! accepted: attribution is optional per event, and a bare continuation is
46//! what a pre-subagent producer sends. It does not hand the entity to the
47//! parent either: the first writer stays the owner.
48//! 10. A tool call belongs to the message its `parentMessageId` names, so a
49//! `TOOL_CALL_START` tagged with one subagent while that message belongs
50//! to another is rejected; an untagged one inherits the message's owner.
51//! 11. Steps are scoped to the agent that opened them: a subagent cannot close
52//! the parent's step, or a sibling's, and the same name may be open under
53//! two owners at once.
54//! 12. `SUBAGENT_STARTED` names an invocation that is neither active nor
55//! already finished in this run, and a `parentSubagentRunId` that was
56//! started. `SUBAGENT_FINISHED` and `SUBAGENT_ERROR` name an active one.
57//! 13. Every started subagent is closed before `RUN_FINISHED` — not before
58//! `RUN_ERROR`, where an unclosed subagent is the expected shape.
59//!
60//! What is deliberately *not* a rule: that one stream must close before the
61//! next opens. Everything here is keyed by id, exactly as the TypeScript
62//! verifier keys its `activeMessages` / `activeToolCalls` maps. Two messages
63//! may stream at once, two tool calls may stream at once, and a tool call may
64//! open inside the message that narrates it — which is what every provider
65//! doing parallel tool calls actually sends. Nor must an attributing
66//! `subagentRunId` have been announced: attribution without lifecycle events
67//! is a supported mode. Events outside these families (state, activity, raw,
68//! custom) are unordered and never close anything. A `MESSAGES_SNAPSHOT`
69//! seeds ownership from the messages it carries and is authoritative; the
70//! `RUN_STARTED` input echo seeds it too, for ids not yet recorded; and a
71//! `TOOL_CALL_RESULT` mints the tool message it names under its own
72//! attribution.
73
74// The THINKING_* events are deprecated but a verifier still has to recognise
75// them.
76#![allow(deprecated)]
77
78use std::collections::{HashMap, HashSet};
79
80use crate::{
81 Event, EventType, Message, MessageId, ReasoningEncryptedValueSubtype, StepName, SubagentRunId,
82 ToolCallId,
83};
84
85use crate::client::error::{Error, Result};
86
87/// Who opened an entity: a subagent, or the parent agent when `None`.
88type Owner = Option<SubagentRunId>;
89
90fn describe(owner: &Owner) -> String {
91 match owner {
92 None => "the parent agent".to_owned(),
93 Some(id) => format!("subagent {:?}", id.as_str()),
94 }
95}
96
97/// An ordering state machine for one run's event stream.
98///
99/// One verifier per run: it is stateful, and its state is that run's progress.
100#[derive(Clone, Debug, Default)]
101pub struct Verifier {
102 started: bool,
103 finished: bool,
104 /// What is open, by id, with the owner that opened it — several at once
105 /// is legal, the same id twice is not. `Vec` rather than a map so a
106 /// complaint names whichever was opened first, which is the one a human
107 /// is looking for.
108 text: Vec<(MessageId, Owner)>,
109 tool: Vec<(ToolCallId, Owner)>,
110 reasoning: Vec<(MessageId, Owner)>,
111 /// Open steps, keyed by owner as well as name.
112 steps: Vec<(Owner, StepName)>,
113 /// Every text message ever introduced and who owns it: the first writer.
114 /// A tool call inherits the owner of the message that carries it, which
115 /// may have closed.
116 message_owners: HashMap<MessageId, Owner>,
117 /// The same for reasoning — the block and the message inside it share an
118 /// id and an owner. A bucket of its own, as upstream keeps it, so a
119 /// producer that reuses an id across the two kinds is not accused of
120 /// contradicting itself.
121 reasoning_owners: HashMap<MessageId, Owner>,
122 /// Every tool call ever introduced and who owns it.
123 tool_call_owners: HashMap<ToolCallId, Owner>,
124 /// Every activity ever introduced and who owns it: opened by a snapshot,
125 /// continued by deltas against the same id.
126 activity_owners: HashMap<MessageId, Owner>,
127 subagents: Vec<SubagentRunId>,
128 /// Ids closed in this run: an id names one invocation.
129 closed_subagents: HashSet<SubagentRunId>,
130}
131
132impl Verifier {
133 /// A verifier for a stream that has not started yet.
134 pub fn new() -> Self {
135 Self::default()
136 }
137
138 /// Whether the run has reached a terminal event.
139 pub fn is_finished(&self) -> bool {
140 self.finished
141 }
142
143 /// Checks one event against the rules, and records it.
144 ///
145 /// # Errors
146 ///
147 /// [`Error::Protocol`], naming the rule that was broken.
148 pub fn verify(&mut self, event: &Event) -> Result<()> {
149 let kind = event.event_type();
150
151 if self.finished {
152 return Err(Error::protocol(format!(
153 "{kind} arrived after the run had already finished"
154 )));
155 }
156
157 // RAW and CUSTOM are outside the protocol's vocabulary by definition,
158 // so they are outside its ordering too.
159 if matches!(kind, EventType::Raw | EventType::Custom) {
160 return Ok(());
161 }
162
163 if !self.started && kind != EventType::RunStarted {
164 return Err(Error::protocol(format!(
165 "{kind} arrived before RUN_STARTED"
166 )));
167 }
168
169 match event {
170 Event::RunStarted(e) => {
171 if self.started {
172 return Err(Error::protocol("RUN_STARTED arrived twice in one stream"));
173 }
174 self.started = true;
175 // The input echo replays history the applier applies, so it
176 // seeds ownership like a snapshot does — for ids not yet
177 // recorded, since it is history rather than a rewrite.
178 if let Some(input) = &e.input {
179 self.seed_owners(&input.messages, false);
180 }
181 }
182
183 Event::TextMessageStart(e) => {
184 let owner = self.claim(kind, &e.message_id, &e.subagent_run_id, false)?;
185 self.not_already_open(
186 self.text.iter().map(|(id, _)| id),
187 &e.message_id,
188 "message",
189 kind,
190 )?;
191 self.text.push((e.message_id.clone(), owner));
192 }
193 Event::TextMessageContent(e) => {
194 self.expect_text(&e.message_id, &e.subagent_run_id, kind)?;
195 }
196 Event::TextMessageEnd(e) => {
197 self.expect_text(&e.message_id, &e.subagent_run_id, kind)?;
198 self.text.retain(|(open, _)| open != &e.message_id);
199 }
200
201 Event::ToolCallStart(e) => {
202 let owner = self.resolve_tool_call_owner(
203 kind,
204 &e.tool_call_id,
205 e.parent_message_id.as_ref(),
206 &e.subagent_run_id,
207 )?;
208 self.not_already_open(
209 self.tool.iter().map(|(id, _)| id),
210 &e.tool_call_id,
211 "tool call",
212 kind,
213 )?;
214 self.tool.push((e.tool_call_id.clone(), owner.clone()));
215 self.tool_call_owners.insert(e.tool_call_id.clone(), owner);
216 }
217 Event::ToolCallArgs(e) => {
218 self.expect_tool(&e.tool_call_id, &e.subagent_run_id, kind)?;
219 }
220 Event::ToolCallEnd(e) => {
221 self.expect_tool(&e.tool_call_id, &e.subagent_run_id, kind)?;
222 self.tool.retain(|(open, _)| open != &e.tool_call_id);
223 }
224 // The call this answers has to be over. Anything *else* still
225 // streaming is none of this event's business — a result arriving
226 // while the assistant keeps narrating is ordinary. Its attribution
227 // is its own, too: the party that executes a call can differ from
228 // the one that requested it.
229 Event::ToolCallResult(e) => {
230 if self.tool.iter().any(|(open, _)| open == &e.tool_call_id) {
231 return Err(Error::protocol(format!(
232 "{kind} for tool call {:?}, which has not ended yet",
233 e.tool_call_id.as_str()
234 )));
235 }
236 // A result mints the tool message it names, under its own
237 // attribution — so the newest mint wins, and a re-open of
238 // that message by someone else has an owner to disagree with.
239 self.message_owners
240 .insert(e.message_id.clone(), e.subagent_run_id.clone());
241 }
242
243 // The block and the message inside it share an id and claim the
244 // same owner: `REASONING_START` under one subagent and
245 // `REASONING_MESSAGE_START` under another is a contradiction. The
246 // block's bracketing is not otherwise tracked (rule 4 is about the
247 // message), only its ownership.
248 Event::ReasoningStart(e) => {
249 self.claim(kind, &e.message_id, &e.subagent_run_id, true)?;
250 }
251 Event::ReasoningEnd(e) => {
252 if let Some(owner) = self.reasoning_owners.get(&e.message_id) {
253 Self::expect_owner(
254 kind,
255 "reasoning message",
256 e.message_id.as_str(),
257 owner,
258 &e.subagent_run_id,
259 )?;
260 }
261 }
262 Event::ReasoningMessageStart(e) => {
263 let owner = self.claim(kind, &e.message_id, &e.subagent_run_id, true)?;
264 self.not_already_open(
265 self.reasoning.iter().map(|(id, _)| id),
266 &e.message_id,
267 "reasoning message",
268 kind,
269 )?;
270 self.reasoning.push((e.message_id.clone(), owner));
271 }
272 Event::ReasoningMessageContent(e) => {
273 self.expect_reasoning(&e.message_id, &e.subagent_run_id, kind)?;
274 }
275 Event::ReasoningMessageEnd(e) => {
276 self.expect_reasoning(&e.message_id, &e.subagent_run_id, kind)?;
277 self.reasoning.retain(|(open, _)| open != &e.message_id);
278 }
279 // Continues an entity by id, and `subtype` says which kind — a
280 // tool call's owner lives in a different map from a message's.
281 Event::ReasoningEncryptedValue(e) => {
282 let (what, owner) = match e.subtype {
283 ReasoningEncryptedValueSubtype::ToolCall => (
284 "tool call",
285 self.tool_call_owners
286 .get(&ToolCallId::new(e.entity_id.clone())),
287 ),
288 // "message" spans both kinds; ids are unique per kind, so
289 // at most one bucket answers.
290 ReasoningEncryptedValueSubtype::Message => {
291 let id = MessageId::new(e.entity_id.clone());
292 (
293 "message",
294 self.message_owners
295 .get(&id)
296 .or_else(|| self.reasoning_owners.get(&id)),
297 )
298 }
299 };
300 if let Some(owner) = owner {
301 Self::expect_owner(kind, what, &e.entity_id, owner, &e.subagent_run_id)?;
302 }
303 }
304
305 // An activity is opened by a snapshot and continued by deltas
306 // against the same id. Only a *replacing* snapshot re-mints it and
307 // so re-owns it; with `replace: false` the applier leaves the
308 // existing message where it was, and so does the recorded owner.
309 Event::ActivitySnapshot(e) => {
310 if e.replace || !self.activity_owners.contains_key(&e.message_id) {
311 self.activity_owners
312 .insert(e.message_id.clone(), e.subagent_run_id.clone());
313 }
314 }
315 Event::ActivityDelta(e) => {
316 if let Some(owner) = self.activity_owners.get(&e.message_id) {
317 Self::expect_owner(
318 kind,
319 "activity",
320 e.message_id.as_str(),
321 owner,
322 &e.subagent_run_id,
323 )?;
324 }
325 }
326
327 Event::StepStarted(e) => {
328 let key = (e.subagent_run_id.clone(), e.step_name.clone());
329 if self.steps.contains(&key) {
330 return Err(Error::protocol(format!(
331 "STEP_STARTED for {:?}, which is already running under {}",
332 e.step_name.as_str(),
333 describe(&e.subagent_run_id)
334 )));
335 }
336 self.steps.push(key);
337 }
338 Event::StepFinished(e) => {
339 let key = (e.subagent_run_id.clone(), e.step_name.clone());
340 match self.steps.iter().position(|open| open == &key) {
341 Some(index) => {
342 self.steps.remove(index);
343 }
344 None => {
345 return Err(Error::protocol(format!(
346 "STEP_FINISHED for {:?}, which never started under {}",
347 e.step_name.as_str(),
348 describe(&e.subagent_run_id)
349 )));
350 }
351 }
352 }
353
354 Event::SubagentStarted(e) => {
355 let id = &e.subagent_run_id;
356 if self.subagents.contains(id) {
357 return Err(Error::protocol(format!(
358 "SUBAGENT_STARTED for subagent {:?}, which is already active",
359 id.as_str()
360 )));
361 }
362 if self.closed_subagents.contains(id) {
363 return Err(Error::protocol(format!(
364 "SUBAGENT_STARTED for subagent {:?}, which already finished in this run; \
365 an id names one invocation",
366 id.as_str()
367 )));
368 }
369 if let Some(parent) = &e.parent_subagent_run_id {
370 if !self.subagents.contains(parent) && !self.closed_subagents.contains(parent) {
371 return Err(Error::protocol(format!(
372 "SUBAGENT_STARTED for subagent {:?} names parent {:?}, which was never started",
373 id.as_str(),
374 parent.as_str()
375 )));
376 }
377 }
378 self.subagents.push(id.clone());
379 }
380 Event::SubagentFinished(e) => self.close_subagent(&e.subagent_run_id, kind)?,
381 Event::SubagentError(e) => self.close_subagent(&e.subagent_run_id, kind)?,
382
383 // Authoritative: the snapshot restates the conversation, so its
384 // owners replace whatever was recorded.
385 Event::MessagesSnapshot(e) => self.seed_owners(&e.messages, true),
386
387 Event::RunFinished(e) => {
388 // Recorded before the checks: the run is over whether or not it
389 // ended tidily, and reporting the untidiness twice — once here
390 // and again from `finish` — helps nobody.
391 self.finished = true;
392 self.expect_all_closed("RUN_FINISHED")?;
393 if let Some(outcome) = &e.outcome {
394 outcome.validate()?;
395 }
396 }
397 Event::RunError(_) => {
398 // A failing run is allowed to abandon whatever it had open —
399 // that is what failing means.
400 self.finished = true;
401 }
402
403 // Chunk events are self-contained; expanding them into brackets is
404 // [`crate::client::chunks`]'s job, and it runs before this one.
405 _ => {}
406 }
407
408 Ok(())
409 }
410
411 /// Checks the stream ended where it was supposed to.
412 ///
413 /// # Errors
414 ///
415 /// [`Error::Protocol`] when the transport ended the stream before the agent
416 /// finished the run — a truncated response, which otherwise looks exactly
417 /// like a short answer.
418 pub fn finish(&self) -> Result<()> {
419 if !self.started {
420 return Err(Error::protocol("the stream ended before RUN_STARTED"));
421 }
422 if !self.finished {
423 return Err(Error::protocol(
424 "the stream ended before RUN_FINISHED or RUN_ERROR",
425 ));
426 }
427 Ok(())
428 }
429
430 // ---- rules ----------------------------------------------------------
431
432 /// Records who a message belongs to — the first writer — or rejects a
433 /// claim that disagrees with the recorded owner, and returns the owner in
434 /// force. An untagged claim on an owned message is accepted and does
435 /// *not* hand the message to the parent: an absent tag agrees with any
436 /// owner, and the applier keeps the message where it was. Upstream's
437 /// verifier records the first writer for the same reason.
438 fn claim(
439 &mut self,
440 kind: EventType,
441 id: &MessageId,
442 tag: &Owner,
443 reasoning: bool,
444 ) -> Result<Owner> {
445 let (what, owners) = if reasoning {
446 ("reasoning message", &mut self.reasoning_owners)
447 } else {
448 ("message", &mut self.message_owners)
449 };
450 if let Some(owner) = owners.get(id) {
451 if tag.is_some() && owner != tag {
452 return Err(Error::protocol(format!(
453 "{kind} for {what} {:?} names {}, but the {what} belongs to {}",
454 id.as_str(),
455 describe(tag),
456 describe(owner)
457 )));
458 }
459 return Ok(owner.clone());
460 }
461 owners.insert(id.clone(), tag.clone());
462 Ok(tag.clone())
463 }
464
465 /// Records the owners of replayed messages, and of the tool calls they
466 /// carry: authoritatively for a `MESSAGES_SNAPSHOT`, which restates the
467 /// conversation, and for ids not yet recorded when the `RUN_STARTED` echo
468 /// replays history.
469 fn seed_owners(&mut self, messages: &[Message], authoritative: bool) {
470 for message in messages {
471 let owner = message.subagent_run_id().cloned();
472 let bucket = match message {
473 Message::Activity(_) => &mut self.activity_owners,
474 Message::Reasoning(_) => &mut self.reasoning_owners,
475 _ => &mut self.message_owners,
476 };
477 if authoritative || !bucket.contains_key(message.id()) {
478 bucket.insert(message.id().clone(), owner.clone());
479 }
480 if let Message::Assistant(assistant) = message {
481 for call in assistant.tool_calls.iter().flatten() {
482 if authoritative || !self.tool_call_owners.contains_key(&call.id) {
483 self.tool_call_owners.insert(call.id.clone(), owner.clone());
484 }
485 }
486 }
487 }
488 }
489
490 /// Who a tool call belongs to: its tag, else the owner of the message that
491 /// carries it, else whoever introduced it before, else the parent agent —
492 /// rejecting a tag that disagrees with the carrying message, and an
493 /// asserted owner that disagrees with a recorded one.
494 fn resolve_tool_call_owner(
495 &self,
496 kind: EventType,
497 id: &ToolCallId,
498 parent_message_id: Option<&MessageId>,
499 tag: &Owner,
500 ) -> Result<Owner> {
501 let inherited = parent_message_id
502 .and_then(|parent| self.message_owners.get(parent).map(|owner| (parent, owner)));
503 if let Some((parent, owner)) = inherited {
504 if tag.is_some() && owner != tag {
505 return Err(Error::protocol(format!(
506 "{kind} for tool call {:?} names {}, but its parent message {:?} belongs to {}; \
507 a tool call belongs to the message that carries it",
508 id.as_str(),
509 describe(tag),
510 parent.as_str(),
511 describe(owner)
512 )));
513 }
514 }
515 let asserted: Option<Owner> = if tag.is_some() {
516 Some(tag.clone())
517 } else {
518 inherited.map(|(_, owner)| owner.clone())
519 };
520 if let (Some(asserted), Some(known)) = (&asserted, self.tool_call_owners.get(id)) {
521 if asserted != known {
522 return Err(Error::protocol(format!(
523 "{kind} for tool call {:?} names {}, but the call belongs to {}",
524 id.as_str(),
525 describe(asserted),
526 describe(known)
527 )));
528 }
529 }
530 Ok(asserted
531 .or_else(|| self.tool_call_owners.get(id).cloned())
532 .unwrap_or(None))
533 }
534
535 fn close_subagent(&mut self, id: &SubagentRunId, kind: EventType) -> Result<()> {
536 match self.subagents.iter().position(|active| active == id) {
537 Some(index) => {
538 self.subagents.remove(index);
539 self.closed_subagents.insert(id.clone());
540 Ok(())
541 }
542 None => Err(Error::protocol(format!(
543 "{kind} for subagent {:?}, which is not active",
544 id.as_str()
545 ))),
546 }
547 }
548
549 fn expect_text(&self, id: &MessageId, tag: &Owner, kind: EventType) -> Result<()> {
550 match self.text.iter().find(|(open, _)| open == id) {
551 None => Err(Error::protocol(format!(
552 "{kind} for message {:?}, which was never opened",
553 id.as_str()
554 ))),
555 Some((_, owner)) => Self::expect_owner(kind, "message", id.as_str(), owner, tag),
556 }
557 }
558
559 fn expect_tool(&self, id: &ToolCallId, tag: &Owner, kind: EventType) -> Result<()> {
560 match self.tool.iter().find(|(open, _)| open == id) {
561 None => Err(Error::protocol(format!(
562 "{kind} for tool call {:?}, which was never opened",
563 id.as_str()
564 ))),
565 Some((_, owner)) => Self::expect_owner(kind, "tool call", id.as_str(), owner, tag),
566 }
567 }
568
569 fn expect_reasoning(&self, id: &MessageId, tag: &Owner, kind: EventType) -> Result<()> {
570 match self.reasoning.iter().find(|(open, _)| open == id) {
571 None => Err(Error::protocol(format!(
572 "{kind} for reasoning message {:?}, which was never opened",
573 id.as_str()
574 ))),
575 Some((_, owner)) => {
576 Self::expect_owner(kind, "reasoning message", id.as_str(), owner, tag)
577 }
578 }
579 }
580
581 /// The whole of the attribution rule for continuations: a tag, when there
582 /// is one, names the opener.
583 fn expect_owner(
584 kind: EventType,
585 what: &str,
586 id: &str,
587 owner: &Owner,
588 tag: &Owner,
589 ) -> Result<()> {
590 if tag.is_some() && owner != tag {
591 return Err(Error::protocol(format!(
592 "{kind} for {what} {id:?} names {}, but the {what} was opened by {}",
593 describe(tag),
594 describe(owner)
595 )));
596 }
597 Ok(())
598 }
599
600 /// Rejects a start for an id that is already streaming.
601 ///
602 /// The whole of the concurrency rule: two ids may overlap, one id may not
603 /// overlap itself.
604 fn not_already_open<'a, T: PartialEq + AsRef<str> + 'a>(
605 &self,
606 mut open: impl Iterator<Item = &'a T>,
607 id: &T,
608 what: &str,
609 kind: EventType,
610 ) -> Result<()> {
611 if open.any(|open| open == id) {
612 return Err(Error::protocol(format!(
613 "{kind} for {what} {:?}, which is already open",
614 id.as_ref()
615 )));
616 }
617 Ok(())
618 }
619
620 fn expect_all_closed(&self, what: &str) -> Result<()> {
621 if let Some((id, _)) = self.text.first() {
622 return Err(Error::protocol(format!(
623 "{what} arrived while message {:?} was still open",
624 id.as_str()
625 )));
626 }
627 if let Some((id, _)) = self.tool.first() {
628 return Err(Error::protocol(format!(
629 "{what} arrived while tool call {:?} was still open",
630 id.as_str()
631 )));
632 }
633 if let Some((id, _)) = self.reasoning.first() {
634 return Err(Error::protocol(format!(
635 "{what} arrived while reasoning message {:?} was still open",
636 id.as_str()
637 )));
638 }
639 if let Some((owner, name)) = self.steps.first() {
640 return Err(Error::protocol(format!(
641 "{what} arrived while step {:?} was still running under {}",
642 name.as_str(),
643 describe(owner)
644 )));
645 }
646 if let Some(id) = self.subagents.first() {
647 return Err(Error::protocol(format!(
648 "{what} arrived while subagent {:?} was still active",
649 id.as_str()
650 )));
651 }
652 Ok(())
653 }
654}
655
656/// Verifies a whole run in one call.
657///
658/// The streaming form is [`Verifier`]; this is the convenience for recorded
659/// streams and tests.
660pub fn verify_all<'a>(events: impl IntoIterator<Item = &'a Event>) -> Result<()> {
661 let mut verifier = Verifier::new();
662 for event in events {
663 verifier.verify(event)?;
664 }
665 verifier.finish()
666}