1use crate::acp::{drain_json_values, AcpDialect, AcpFragment, ToolSignal};
4use crate::claude_stream::{
5 drain_ndjson_lines as drain_claude_lines, map_stream_line as map_claude_stream_line,
6};
7use crate::openai_chat::OpenAiSseState;
8use crate::sentence::SentenceSegmenter;
9use crate::stream::{CanonicalEventStream, EventPublisher};
10use crate::zai_chat::{drain_ndjson_lines, map_chat_message_line};
11use monoloop_contracts::{
12 BoundaryKind, CanonicalUnit, CanonicalUnitEvent, CanonicalUnitSnapshot, ConnectionId,
13 DiagnosticKind, DialectBinding, DialectFamily, ExternalSessionId, FlowId, InterpretationEnd,
14 InterpretationEndKind, InterpretationId, InterpretationLimits, InterpreterError,
15 InterpreterErrorKind, InterpreterOutputEvent, LaneId, ModelDiagnostic, SemanticBoundary,
16 SourceTimeObservation, TextChannel, TextSentence, ToolActionEvent, ToolActionId,
17 ToolExecutionState, ToolRequestState, ToolResultState, ToolTerminalOutcome, UnitId, UnitState,
18 UsageObservation,
19};
20use std::collections::HashMap;
21use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
22use std::sync::Arc;
23use tokio::sync::{mpsc, oneshot, Mutex};
24
25#[derive(Clone, Debug)]
27pub struct StartInterpretation {
28 pub interpretation_id: InterpretationId,
30 pub connection_id: ConnectionId,
32 pub external_session_id: Option<ExternalSessionId>,
34 pub dialect: DialectBinding,
36 pub limits: InterpretationLimits,
38}
39
40pub struct Interpretation {
42 pub input: InterpretationInput,
44 pub events: Arc<CanonicalEventStream>,
46 pub status: InterpretationStatus,
48 pub completion: InterpretationCompletion,
50}
51
52#[derive(Clone)]
54pub struct InterpretationInput {
55 tx: mpsc::Sender<InputCmd>,
56}
57
58enum InputCmd {
59 Bytes(bytes::Bytes),
60 FinishClean,
62 Cancel,
64 TransportFailed,
66}
67
68impl InterpretationInput {
69 pub async fn push_bytes(&self, bytes: bytes::Bytes) -> Result<(), InterpreterError> {
71 self.tx
72 .send(InputCmd::Bytes(bytes))
73 .await
74 .map_err(|_| InterpreterError::cancelled())
75 }
76
77 pub async fn finish_clean(&self) -> Result<(), InterpreterError> {
79 self.tx
80 .send(InputCmd::FinishClean)
81 .await
82 .map_err(|_| InterpreterError::cancelled())
83 }
84
85 pub async fn cancel(&self) -> Result<(), InterpreterError> {
87 self.tx
88 .send(InputCmd::Cancel)
89 .await
90 .map_err(|_| InterpreterError::cancelled())
91 }
92
93 pub async fn transport_failed(&self) -> Result<(), InterpreterError> {
95 self.tx
96 .send(InputCmd::TransportFailed)
97 .await
98 .map_err(|_| InterpreterError::cancelled())
99 }
100}
101
102#[derive(Clone, Debug, Default)]
104pub struct InterpretationStatus {
105 pub terminal: Arc<AtomicBool>,
107 pub bytes_consumed: Arc<AtomicU64>,
109}
110
111pub struct InterpretationCompletion {
113 rx: Mutex<Option<oneshot::Receiver<InterpretationEnd>>>,
114}
115
116impl InterpretationCompletion {
117 pub async fn wait(self) -> InterpretationEnd {
119 let mut guard = self.rx.lock().await;
120 let rx = guard.take().expect("InterpretationCompletion polled twice");
121 rx.await.unwrap_or_else(|_| InterpretationEnd {
122 interpretation_id: InterpretationId::new("unknown"),
123 connection_id: ConnectionId::new("unknown"),
124 external_session_id: None,
125 kind: InterpretationEndKind::InvariantFailed,
126 canonical_event_count: 0,
127 completed_sentence_count: 0,
128 completed_structure_count: 0,
129 unresolved_text_bytes: 0,
130 source_bytes_consumed: 0,
131 safe_diagnostics: vec!["completion channel dropped".into()],
132 })
133 }
134}
135
136pub(crate) fn spawn_interpretation(
137 request: StartInterpretation,
138) -> Result<Interpretation, InterpreterError> {
139 validate_dialect(&request.dialect)?;
140
141 let (pub_, events) = EventPublisher::new(request.limits.max_output_queue_items);
142 let (cmd_tx, cmd_rx) = mpsc::channel::<InputCmd>(64);
143 let (end_tx, end_rx) = oneshot::channel();
144 let status = InterpretationStatus::default();
145
146 let input = InterpretationInput { tx: cmd_tx };
147 let events = Arc::new(events);
148
149 let status_terminal = Arc::clone(&status.terminal);
150 let status_bytes = Arc::clone(&status.bytes_consumed);
151
152 tokio::spawn(async move {
153 let openai = OpenAiSseState::new(
154 request.limits.max_frame_bytes.min(64 * 1024),
155 request.limits.max_frame_bytes,
156 request.limits.max_bytes_per_tool_action,
157 );
158 let mut owner = Owner {
159 request,
160 pub_,
161 channels: HashMap::new(),
162 tools: HashMap::new(),
163 lane_ordinals: HashMap::new(),
164 next_unit: 1,
165 sentence_count: 0,
166 structure_count: 0,
167 source_bytes: 0,
168 frame_buf: Vec::new(),
169 ended: false,
170 diagnostics: Vec::new(),
171 response_started: false,
172 unresolved_bytes_at_end: 0,
173 openai,
174 };
175 owner
176 .run(cmd_rx, end_tx, status_terminal, status_bytes)
177 .await;
178 });
179
180 Ok(Interpretation {
181 input,
182 events,
183 status,
184 completion: InterpretationCompletion {
185 rx: Mutex::new(Some(end_rx)),
186 },
187 })
188}
189
190fn validate_dialect(binding: &DialectBinding) -> Result<(), InterpreterError> {
191 match &binding.output.family {
192 DialectFamily::Acp
193 | DialectFamily::GrokBuild
194 | DialectFamily::CursorAcp
195 | DialectFamily::AgyAcp
196 | DialectFamily::CodexAcp
197 | DialectFamily::ZaiCli
198 | DialectFamily::ClaudeCode
199 | DialectFamily::OpenAiChatCompletions
200 | DialectFamily::Test => Ok(()),
201 DialectFamily::OpenAiResponses => Err(InterpreterError::unsupported_dialect(
202 "OpenAI Responses is not supported; use Chat Completions SSE",
203 )),
204 other => Err(InterpreterError::unsupported_dialect(format!(
205 "unsupported dialect family: {other:?}"
206 ))),
207 }
208}
209
210struct Owner {
211 request: StartInterpretation,
212 pub_: EventPublisher,
213 channels: HashMap<TextChannel, ChannelAssembly>,
215 tools: HashMap<String, ToolAssembler>,
216 lane_ordinals: HashMap<String, u64>,
217 next_unit: u64,
218 sentence_count: u64,
219 structure_count: u64,
220 source_bytes: u64,
221 frame_buf: Vec<u8>,
222 ended: bool,
223 diagnostics: Vec<String>,
224 response_started: bool,
225 unresolved_bytes_at_end: u64,
226 openai: OpenAiSseState,
228}
229
230struct ChannelAssembly {
232 segmenter: SentenceSegmenter,
233 spans: Vec<(usize, Option<u64>, Option<u64>)>,
235}
236
237impl Default for ChannelAssembly {
238 fn default() -> Self {
239 Self {
240 segmenter: SentenceSegmenter::new(),
241 spans: Vec::new(),
242 }
243 }
244}
245
246struct SentenceSourceMeta {
248 time: Option<SourceTimeObservation>,
249 step: Option<u64>,
250}
251
252impl ChannelAssembly {
253 fn push(
254 &mut self,
255 text: &str,
256 source_time_ms: Option<u64>,
257 source_step: Option<u64>,
258 ) -> Vec<(String, SentenceSourceMeta)> {
259 if !text.is_empty() {
260 self.spans.push((text.len(), source_time_ms, source_step));
261 }
262 let completed = self.segmenter.push(text);
263 completed
264 .into_iter()
265 .map(|c| {
266 let meta = self.take_spans(c.content_bytes, c.bytes_consumed);
267 (c.content, meta)
268 })
269 .collect()
270 }
271
272 fn seal(&mut self) -> Vec<(String, SentenceSourceMeta)> {
273 let completed = self.segmenter.seal_at_clean_end();
274 completed
275 .into_iter()
276 .map(|c| {
277 let meta = self.take_spans(c.content_bytes, c.bytes_consumed);
278 (c.content, meta)
279 })
280 .collect()
281 }
282
283 fn take_unresolved(&mut self) -> String {
284 self.spans.clear();
285 self.segmenter.take_unresolved()
286 }
287
288 fn take_spans(&mut self, content_bytes: usize, bytes_consumed: usize) -> SentenceSourceMeta {
290 let mut first = None;
291 let mut last = None;
292 let mut step_min = None;
293 let mut seen = 0usize;
294 let mut remaining = bytes_consumed;
295 while remaining > 0 && !self.spans.is_empty() {
296 let (len, t, step) = &mut self.spans[0];
297 let take = (*len).min(remaining);
298 let content_take = if seen < content_bytes {
300 take.min(content_bytes - seen)
301 } else {
302 0
303 };
304 if content_take > 0 {
305 if let Some(ms) = *t {
306 first = Some(first.map_or(ms, |f: u64| f.min(ms)));
307 last = Some(last.map_or(ms, |l: u64| l.max(ms)));
308 }
309 if let Some(s) = *step {
310 step_min = Some(step_min.map_or(s, |m: u64| m.min(s)));
311 }
312 }
313 seen += take;
314 *len -= take;
315 remaining -= take;
316 if *len == 0 {
317 self.spans.remove(0);
318 }
319 }
320 SentenceSourceMeta {
321 time: SourceTimeObservation::from_bounds(first, last),
322 step: step_min,
323 }
324 }
325}
326
327struct ToolAssembler {
328 action_id: ToolActionId,
329 unit_id: UnitId,
330 generation: u64,
331 tool_name: Option<String>,
332 request_state: ToolRequestState,
333 execution_state: ToolExecutionState,
334 result_state: ToolResultState,
335 request_payload: Option<String>,
336 result_payload: Option<String>,
337 terminal: Option<ToolTerminalOutcome>,
338 waiting_for: Option<String>,
339 first_ms: Option<u64>,
340 last_ms: Option<u64>,
341 first_step: Option<u64>,
343}
344
345impl ToolAssembler {
346 fn note_time(&mut self, t: Option<u64>) {
347 if let Some(ms) = t {
348 self.first_ms = Some(self.first_ms.map_or(ms, |f| f.min(ms)));
349 self.last_ms = Some(self.last_ms.map_or(ms, |l| l.max(ms)));
350 }
351 }
352
353 fn note_step(&mut self, s: Option<u64>) {
354 if let Some(step) = s {
355 self.first_step = Some(self.first_step.map_or(step, |f| f.min(step)));
356 }
357 }
358
359 fn source_time(&self) -> Option<SourceTimeObservation> {
360 SourceTimeObservation::from_bounds(self.first_ms, self.last_ms)
361 }
362
363 fn source_step(&self) -> Option<u64> {
364 self.first_step
365 }
366}
367
368impl Owner {
369 async fn run(
370 &mut self,
371 mut cmd_rx: mpsc::Receiver<InputCmd>,
372 end_tx: oneshot::Sender<InterpretationEnd>,
373 status_terminal: Arc<AtomicBool>,
374 status_bytes: Arc<AtomicU64>,
375 ) {
376 while let Some(cmd) = cmd_rx.recv().await {
377 match cmd {
378 InputCmd::Bytes(b) => {
379 if self.ended {
380 continue;
381 }
382 self.source_bytes += b.len() as u64;
383 status_bytes.store(self.source_bytes, Ordering::Relaxed);
384 if let Err(e) = self.ingest_bytes(&b).await {
385 let kind = match e.kind {
386 InterpreterErrorKind::Cancelled => InterpretationEndKind::Cancelled,
387 InterpreterErrorKind::FrameLimitExceeded
388 | InterpreterErrorKind::SentenceLimitExceeded
389 | InterpreterErrorKind::StructureLimitExceeded
390 | InterpreterErrorKind::ToolLimitExceeded => {
391 InterpretationEndKind::LimitExceeded
392 }
393 InterpreterErrorKind::MalformedFrame
394 | InterpreterErrorKind::MalformedSemanticPayload => {
395 InterpretationEndKind::DialectFailed
396 }
397 _ => InterpretationEndKind::DialectFailed,
398 };
399 self.finish(kind, end_tx, status_terminal).await;
400 return;
401 }
402 }
403 InputCmd::FinishClean => {
404 match self.seal_clean().await {
405 Ok(()) => {
406 self.finish(InterpretationEndKind::Complete, end_tx, status_terminal)
407 .await;
408 }
409 Err(e) => {
410 let kind = match e.kind {
411 InterpreterErrorKind::Cancelled => InterpretationEndKind::Cancelled,
412 InterpreterErrorKind::FrameLimitExceeded
413 | InterpreterErrorKind::SentenceLimitExceeded
414 | InterpreterErrorKind::StructureLimitExceeded
415 | InterpreterErrorKind::ToolLimitExceeded => {
416 InterpretationEndKind::LimitExceeded
417 }
418 _ => InterpretationEndKind::TransportFailed,
419 };
420 let _ = self.quarantine_partials().await;
421 self.finish(kind, end_tx, status_terminal).await;
422 }
423 }
424 return;
425 }
426 InputCmd::Cancel => {
427 let _ = self.quarantine_partials().await;
428 self.finish(InterpretationEndKind::Cancelled, end_tx, status_terminal)
429 .await;
430 return;
431 }
432 InputCmd::TransportFailed => {
433 let _ = self.quarantine_partials().await;
434 self.finish(
435 InterpretationEndKind::TransportFailed,
436 end_tx,
437 status_terminal,
438 )
439 .await;
440 return;
441 }
442 }
443 }
444 let _ = self.quarantine_partials().await;
446 self.finish(
447 InterpretationEndKind::TransportFailed,
448 end_tx,
449 status_terminal,
450 )
451 .await;
452 }
453
454 async fn ingest_bytes(&mut self, chunk: &[u8]) -> Result<(), InterpreterError> {
455 if self.frame_buf.len() + chunk.len() > self.request.limits.max_undecoded_bytes {
456 return Err(InterpreterError::limit("undecoded buffer limit exceeded"));
457 }
458 self.frame_buf.extend_from_slice(chunk);
459
460 match &self.request.dialect.output.family {
461 DialectFamily::Test => self.ingest_test_text().await,
462 DialectFamily::Acp
463 | DialectFamily::GrokBuild
464 | DialectFamily::CursorAcp
465 | DialectFamily::AgyAcp
466 | DialectFamily::CodexAcp => self.ingest_acp().await,
467 DialectFamily::ZaiCli => self.ingest_zai_cli().await,
468 DialectFamily::ClaudeCode => self.ingest_claude_code().await,
469 DialectFamily::OpenAiChatCompletions => self.ingest_openai_chat().await,
470 _ => Err(InterpreterError::unsupported_dialect("dialect")),
471 }
472 }
473
474 async fn ingest_openai_chat(&mut self) -> Result<(), InterpreterError> {
476 let chunk = std::mem::take(&mut self.frame_buf);
480 if chunk.len() > self.request.limits.max_undecoded_bytes {
481 return Err(InterpreterError::limit("undecoded buffer limit exceeded"));
482 }
483 let frags = self.openai.push_bytes(&chunk)?;
484 for frag in frags {
485 if !self.response_started {
486 self.response_started = true;
487 self.emit_boundary(BoundaryKind::ResponseStarted).await?;
488 }
489 self.on_fragment(frag).await?;
490 }
491 Ok(())
492 }
493
494 async fn ingest_test_text(&mut self) -> Result<(), InterpreterError> {
496 let (valid, rest) = split_valid_utf8(&self.frame_buf);
498 if valid.is_empty() && !rest.is_empty() {
499 return Ok(());
500 }
501 let text = String::from_utf8_lossy(valid).into_owned();
502 self.frame_buf = rest.to_vec();
503 self.on_text(TextChannel::PublicResponse, &text, None, None)
504 .await
505 }
506
507 async fn ingest_acp(&mut self) -> Result<(), InterpreterError> {
508 if self.frame_buf.len() > self.request.limits.max_frame_bytes {
509 return Err(InterpreterError::limit("frame buffer limit exceeded"));
510 }
511 let values =
512 drain_json_values(&mut self.frame_buf).map_err(InterpreterError::malformed_frame)?;
513 for value in values {
514 if !self.response_started {
515 self.response_started = true;
516 self.emit_boundary(BoundaryKind::ResponseStarted).await?;
517 }
518 for frag in AcpDialect::map_message(&value) {
519 self.on_fragment(frag).await?;
520 }
521 }
522 Ok(())
523 }
524
525 async fn ingest_zai_cli(&mut self) -> Result<(), InterpreterError> {
527 if self.frame_buf.len() > self.request.limits.max_frame_bytes {
528 return Err(InterpreterError::limit("frame buffer limit exceeded"));
529 }
530 let lines = drain_ndjson_lines(&mut self.frame_buf);
531 for line in lines {
532 if !self.response_started {
533 self.response_started = true;
534 self.emit_boundary(BoundaryKind::ResponseStarted).await?;
535 }
536 for frag in map_chat_message_line(&line) {
537 self.on_fragment(frag).await?;
538 }
539 }
540 Ok(())
541 }
542
543 async fn ingest_claude_code(&mut self) -> Result<(), InterpreterError> {
545 if self.frame_buf.len() > self.request.limits.max_frame_bytes {
546 return Err(InterpreterError::limit("frame buffer limit exceeded"));
547 }
548 let lines = drain_claude_lines(&mut self.frame_buf);
549 for line in lines {
550 if !self.response_started {
551 self.response_started = true;
552 self.emit_boundary(BoundaryKind::ResponseStarted).await?;
553 }
554 for frag in map_claude_stream_line(&line) {
555 self.on_fragment(frag).await?;
556 }
557 }
558 Ok(())
559 }
560
561 async fn on_fragment(&mut self, frag: AcpFragment) -> Result<(), InterpreterError> {
562 match frag {
563 AcpFragment::TextDelta {
564 channel,
565 text,
566 source_time_ms,
567 source_step,
568 } => {
569 self.on_text(channel, &text, source_time_ms, source_step)
570 .await
571 }
572 AcpFragment::Tool {
573 action_id,
574 signal,
575 source_time_ms,
576 source_step,
577 } => {
578 self.on_tool(action_id, signal, source_time_ms, source_step)
579 .await
580 }
581 AcpFragment::ResponseFinished => {
582 self.seal_text_channels().await?;
583 self.emit_boundary(BoundaryKind::ResponseFinished).await
584 }
585 AcpFragment::Diagnostic { message } => {
586 self.push_diagnostic(message.clone());
587 self.emit_diagnostic(DiagnosticKind::UnsupportedEvent, message)
588 .await
589 }
590 }
591 }
592
593 async fn on_text(
594 &mut self,
595 channel: TextChannel,
596 text: &str,
597 source_time_ms: Option<u64>,
598 source_step: Option<u64>,
599 ) -> Result<(), InterpreterError> {
600 let completed = {
601 let asm = self.channels.entry(channel).or_default();
602 if asm.segmenter.buffered_bytes() + text.len()
603 > self.request.limits.max_sentence_assembly_bytes
604 {
605 return Err(InterpreterError::new(
606 InterpreterErrorKind::SentenceLimitExceeded,
607 "sentence assembly limit exceeded",
608 ));
609 }
610 asm.push(text, source_time_ms, source_step)
611 };
612 for (sentence, meta) in completed {
613 self.emit_sentence(channel, sentence, meta.time, meta.step)
614 .await?;
615 }
616 Ok(())
617 }
618
619 async fn seal_text_channels(&mut self) -> Result<(), InterpreterError> {
620 let channels: Vec<TextChannel> = self.channels.keys().copied().collect();
621 for channel in channels {
622 let sealed = self
623 .channels
624 .get_mut(&channel)
625 .map(|asm| asm.seal())
626 .unwrap_or_default();
627 for (sentence, meta) in sealed {
628 self.emit_sentence(channel, sentence, meta.time, meta.step)
629 .await?;
630 }
631 }
632 Ok(())
633 }
634
635 async fn seal_clean(&mut self) -> Result<(), InterpreterError> {
636 if matches!(
637 self.request.dialect.output.family,
638 DialectFamily::OpenAiChatCompletions
639 ) {
640 let frags = self.openai.seal_clean()?;
642 for frag in frags {
643 self.on_fragment(frag).await?;
644 }
645 }
646 self.seal_text_channels().await
647 }
648
649 async fn quarantine_partials(&mut self) -> Result<(), InterpreterError> {
650 for (channel, asm) in self.channels.iter_mut() {
652 let unresolved = asm.take_unresolved();
653 if !unresolved.is_empty() {
654 self.unresolved_bytes_at_end += unresolved.len() as u64;
655 self.diagnostics.push(format!(
656 "unresolved text on {:?}: {} bytes",
657 channel,
658 unresolved.len()
659 ));
660 }
661 }
662 self.unresolved_bytes_at_end += self.frame_buf.len() as u64;
663 self.frame_buf.clear();
664 let ids: Vec<String> = self.tools.keys().cloned().collect();
666 for id in ids {
667 if let Some(tool) = self.tools.get_mut(&id) {
668 if tool.terminal.is_none() {
669 tool.request_state = match tool.request_state {
670 ToolRequestState::Ready => ToolRequestState::Ready,
671 _ => ToolRequestState::Incomplete,
672 };
673 tool.result_state = match tool.result_state {
674 ToolResultState::Complete => ToolResultState::Complete,
675 _ => ToolResultState::Incomplete,
676 };
677 tool.generation += 1;
678 let snap = self.tool_snapshot_from(&id);
679 if let Some(s) = snap {
680 self.pub_
681 .publish(InterpreterOutputEvent::Unit(Box::new(
682 CanonicalUnitEvent::Incomplete(s),
683 )))
684 .await?;
685 }
686 }
687 }
688 }
689 Ok(())
690 }
691
692 async fn on_tool(
693 &mut self,
694 action_id: ToolActionId,
695 signal: ToolSignal,
696 source_time_ms: Option<u64>,
697 source_step: Option<u64>,
698 ) -> Result<(), InterpreterError> {
699 if self.tools.len() >= self.request.limits.max_pending_tool_actions
700 && !self.tools.contains_key(action_id.as_str())
701 {
702 return Err(InterpreterError::new(
703 InterpreterErrorKind::ToolLimitExceeded,
704 "max pending tool actions",
705 ));
706 }
707
708 let is_new = !self.tools.contains_key(action_id.as_str());
709 if is_new {
710 let unit_id = UnitId::new(format!("tool-{}", action_id.as_str()));
711 self.tools.insert(
712 action_id.as_str().to_string(),
713 ToolAssembler {
714 action_id: action_id.clone(),
715 unit_id,
716 generation: 0,
717 tool_name: None,
718 request_state: ToolRequestState::Assembling,
719 execution_state: ToolExecutionState::NotObserved,
720 result_state: ToolResultState::Absent,
721 request_payload: None,
722 result_payload: None,
723 terminal: None,
724 waiting_for: None,
725 first_ms: None,
726 last_ms: None,
727 first_step: None,
728 },
729 );
730 }
731
732 let event_kind = {
733 let tool = self.tools.get_mut(action_id.as_str()).unwrap();
734 tool.note_time(source_time_ms);
735 tool.note_step(source_step);
736 tool.generation += 1;
737 match signal {
738 ToolSignal::Waiting {
739 tool_name,
740 waiting_for,
741 } => {
742 if tool_name.is_some() {
743 tool.tool_name = tool_name;
744 }
745 tool.waiting_for = Some(waiting_for);
746 tool.request_state = ToolRequestState::Assembling;
747 tool.execution_state = ToolExecutionState::Waiting;
748 if is_new {
749 "created"
750 } else {
751 "advanced"
752 }
753 }
754 ToolSignal::RequestReady {
755 tool_name,
756 arguments_json,
757 } => {
758 if arguments_json.len() > self.request.limits.max_bytes_per_tool_action {
759 return Err(InterpreterError::new(
760 InterpreterErrorKind::ToolLimitExceeded,
761 "tool payload limit",
762 ));
763 }
764 tool.tool_name = Some(tool_name);
765 tool.request_payload = Some(arguments_json);
766 tool.request_state = ToolRequestState::Ready;
767 tool.execution_state = ToolExecutionState::Waiting;
768 tool.waiting_for = Some("external execution".into());
769 tool.result_state = ToolResultState::Absent;
770 if is_new {
771 "created"
772 } else {
773 "advanced"
774 }
775 }
776 ToolSignal::Resolved {
777 success,
778 result_json,
779 } => {
780 tool.execution_state = ToolExecutionState::Terminal;
781 tool.result_state = ToolResultState::Complete;
782 tool.result_payload = result_json;
783 tool.terminal = Some(if success {
784 ToolTerminalOutcome::Success
785 } else {
786 ToolTerminalOutcome::Failure
787 });
788 tool.waiting_for = None;
789 "completed"
790 }
791 }
792 };
793
794 let snap = self.tool_snapshot_from(action_id.as_str()).unwrap();
795 let unit_event = match event_kind {
796 "created" => CanonicalUnitEvent::Created(snap),
797 "completed" => CanonicalUnitEvent::Completed(snap),
798 _ => CanonicalUnitEvent::Advanced(snap),
799 };
800 self.pub_
801 .publish(InterpreterOutputEvent::unit(unit_event))
802 .await
803 }
804
805 fn tool_snapshot_from(&self, id: &str) -> Option<CanonicalUnitSnapshot> {
806 let tool = self.tools.get(id)?;
807 let unit = CanonicalUnit::Tool(ToolActionEvent {
808 tool_action_id: tool.action_id.clone(),
809 tool_name: tool.tool_name.clone(),
810 request_state: tool.request_state,
811 execution_state: tool.execution_state,
812 result_state: tool.result_state,
813 request_payload: if tool.request_state == ToolRequestState::Ready {
815 tool.request_payload.clone()
816 } else {
817 None
818 },
819 result_payload: if tool.result_state == ToolResultState::Complete {
820 tool.result_payload.clone()
821 } else {
822 None
823 },
824 terminal_outcome: tool.terminal,
825 waiting_for: tool.waiting_for.clone(),
826 });
827 let state = if tool.terminal.is_some() {
828 UnitState::Complete
829 } else if tool.request_state == ToolRequestState::Incomplete {
830 UnitState::Incomplete
831 } else {
832 UnitState::Waiting
833 };
834 Some(self.snapshot(
835 tool.unit_id.clone(),
836 tool.generation,
837 state,
838 LaneId::tool(),
839 tool.source_time(),
840 tool.source_step(),
841 unit,
842 ))
843 }
844
845 async fn emit_sentence(
846 &mut self,
847 channel: TextChannel,
848 content: String,
849 source_time: Option<SourceTimeObservation>,
850 source_step: Option<u64>,
851 ) -> Result<(), InterpreterError> {
852 let unit_id = UnitId::new(format!("s-{}", self.next_unit));
853 self.next_unit += 1;
854 self.sentence_count += 1;
855 let lane = match channel {
857 TextChannel::PublicReasoningSummary => LaneId::reasoning(),
858 TextChannel::StatusNarration => LaneId::new("status"),
859 TextChannel::QuotedExternalContent => LaneId::new("quoted"),
860 TextChannel::PublicResponse => LaneId::response(),
861 };
862 let ordinal = self.next_lane_ordinal(lane.as_str());
863 let unit = CanonicalUnit::Text(TextSentence {
864 sentence_id: unit_id.clone(),
865 channel,
866 paragraph_id: None,
867 sentence_ordinal: ordinal,
868 content,
869 });
870 let snap = self.snapshot(
871 unit_id,
872 1,
873 UnitState::Complete,
874 lane,
875 source_time,
876 source_step,
877 unit,
878 );
879 self.pub_
881 .publish(InterpreterOutputEvent::Unit(Box::new(
882 CanonicalUnitEvent::Created(snap),
883 )))
884 .await
885 }
886
887 async fn emit_boundary(&mut self, kind: BoundaryKind) -> Result<(), InterpreterError> {
888 let unit_id = UnitId::new(format!("b-{}", self.next_unit));
889 self.next_unit += 1;
890 let unit = CanonicalUnit::Boundary(SemanticBoundary { kind });
891 let snap = self.snapshot(
892 unit_id,
893 1,
894 UnitState::Complete,
895 LaneId::response(),
896 None,
897 None,
898 unit,
899 );
900 self.pub_
901 .publish(InterpreterOutputEvent::Unit(Box::new(
902 CanonicalUnitEvent::Created(snap),
903 )))
904 .await
905 }
906
907 async fn emit_diagnostic(
908 &mut self,
909 kind: DiagnosticKind,
910 message: String,
911 ) -> Result<(), InterpreterError> {
912 let unit_id = UnitId::new(format!("d-{}", self.next_unit));
913 self.next_unit += 1;
914 let unit = CanonicalUnit::Diagnostic(ModelDiagnostic { kind, message });
915 let snap = self.snapshot(
916 unit_id,
917 1,
918 UnitState::Complete,
919 LaneId::response(),
920 None,
921 None,
922 unit,
923 );
924 self.pub_
925 .publish(InterpreterOutputEvent::Unit(Box::new(
926 CanonicalUnitEvent::Created(snap),
927 )))
928 .await
929 }
930
931 #[allow(clippy::too_many_arguments)]
932 fn snapshot(
933 &self,
934 unit_id: UnitId,
935 generation: u64,
936 state: UnitState,
937 lane_id: LaneId,
938 source_time: Option<SourceTimeObservation>,
939 source_step: Option<u64>,
940 unit: CanonicalUnit,
941 ) -> CanonicalUnitSnapshot {
942 let lane_ordinal = self
943 .lane_ordinals
944 .get(lane_id.as_str())
945 .copied()
946 .unwrap_or(0);
947 CanonicalUnitSnapshot {
948 unit_id,
949 unit_generation: generation,
950 unit_state: state,
951 interpretation_id: self.request.interpretation_id.clone(),
952 connection_id: self.request.connection_id.clone(),
953 external_session_id: self.request.external_session_id.clone(),
954 flow_id: FlowId::main(),
955 lane_id,
956 lane_ordinal,
957 causal_parent_id: None,
958 source_time,
959 source_step,
960 unit,
961 }
962 }
963
964 fn next_lane_ordinal(&mut self, lane: &str) -> u64 {
965 let e = self.lane_ordinals.entry(lane.to_string()).or_insert(0);
966 *e += 1;
967 *e
968 }
969
970 fn push_diagnostic(&mut self, msg: String) {
971 if self.diagnostics.len() < self.request.limits.max_safe_diagnostics {
972 self.diagnostics.push(msg);
973 }
974 }
975
976 async fn finish(
977 &mut self,
978 kind: InterpretationEndKind,
979 end_tx: oneshot::Sender<InterpretationEnd>,
980 status_terminal: Arc<AtomicBool>,
981 ) {
982 if self.ended {
983 return;
984 }
985 self.ended = true;
986 let mut unresolved = self.unresolved_bytes_at_end;
987 for asm in self.channels.values() {
988 unresolved += asm.segmenter.buffered_bytes() as u64;
989 }
990 unresolved += self.frame_buf.len() as u64;
991
992 let end = InterpretationEnd {
993 interpretation_id: self.request.interpretation_id.clone(),
994 connection_id: self.request.connection_id.clone(),
995 external_session_id: self.request.external_session_id.clone(),
996 kind,
997 canonical_event_count: self.pub_.count(),
998 completed_sentence_count: self.sentence_count,
999 completed_structure_count: self.structure_count,
1000 unresolved_text_bytes: unresolved,
1001 source_bytes_consumed: self.source_bytes,
1002 safe_diagnostics: self.diagnostics.clone(),
1003 };
1004 let _ = self
1005 .pub_
1006 .publish(InterpreterOutputEvent::Ended(end.clone()))
1007 .await;
1008 status_terminal.store(true, Ordering::SeqCst);
1009 let _ = end_tx.send(end);
1010 }
1011}
1012
1013fn split_valid_utf8(buf: &[u8]) -> (&[u8], &[u8]) {
1014 match std::str::from_utf8(buf) {
1015 Ok(_) => (buf, &[]),
1016 Err(e) => {
1017 let valid_up_to = e.valid_up_to();
1018 (&buf[..valid_up_to], &buf[valid_up_to..])
1019 }
1020 }
1021}
1022
1023#[allow(dead_code)]
1025fn _u() -> Option<UsageObservation> {
1026 None
1027}