Skip to main content

monoloop_interpreter/
engine.rs

1//! Interpretation instance: feed raw bytes, assemble, publish canonical events.
2
3use 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/// Request to start an interpretation on one connection output.
26#[derive(Clone, Debug)]
27pub struct StartInterpretation {
28    /// Interpretation identity.
29    pub interpretation_id: InterpretationId,
30    /// Connection identity.
31    pub connection_id: ConnectionId,
32    /// External session when present (propagated unchanged).
33    pub external_session_id: Option<ExternalSessionId>,
34    /// Frozen dialect binding from Connector open.
35    pub dialect: DialectBinding,
36    /// Assembly limits.
37    pub limits: InterpretationLimits,
38}
39
40/// Handle returned by the factory.
41pub struct Interpretation {
42    /// Feed raw bytes here.
43    pub input: InterpretationInput,
44    /// Canonical event stream.
45    pub events: Arc<CanonicalEventStream>,
46    /// Status snapshot.
47    pub status: InterpretationStatus,
48    /// Completes with InterpretationEnd.
49    pub completion: InterpretationCompletion,
50}
51
52/// Cloneable input handle for raw Connector output chunks.
53#[derive(Clone)]
54pub struct InterpretationInput {
55    tx: mpsc::Sender<InputCmd>,
56}
57
58enum InputCmd {
59    Bytes(bytes::Bytes),
60    /// Clean dialect/source end (remote EOF after clean response).
61    FinishClean,
62    /// Abrupt cancel.
63    Cancel,
64    /// Transport failure.
65    TransportFailed,
66}
67
68impl InterpretationInput {
69    /// Push an ordered raw chunk (fragment boundaries carry no meaning).
70    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    /// Signal clean source completion (may seal final sentences).
78    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    /// Cancel interpretation.
86    pub async fn cancel(&self) -> Result<(), InterpreterError> {
87        self.tx
88            .send(InputCmd::Cancel)
89            .await
90            .map_err(|_| InterpreterError::cancelled())
91    }
92
93    /// Abrupt transport failure.
94    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/// Lightweight status.
103#[derive(Clone, Debug, Default)]
104pub struct InterpretationStatus {
105    /// Whether terminal end was published.
106    pub terminal: Arc<AtomicBool>,
107    /// Source bytes consumed.
108    pub bytes_consumed: Arc<AtomicU64>,
109}
110
111/// Completion handle.
112pub struct InterpretationCompletion {
113    rx: Mutex<Option<oneshot::Receiver<InterpretationEnd>>>,
114}
115
116impl InterpretationCompletion {
117    /// Wait for exactly one terminal InterpretationEnd.
118    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    /// Per-channel text assembly + dialect source-time windows.
214    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 Chat Completions SSE assembler (idle for other dialects).
227    openai: OpenAiSseState,
228}
229
230/// Sentence assembly for one text channel, with observational source spans.
231struct ChannelAssembly {
232    segmenter: SentenceSegmenter,
233    /// Run-length spans: `(byte_len, source_time_ms, source_step)`.
234    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
246/// Observational dialect metadata attributed to a completed sentence.
247struct 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    /// Attribute times/steps from the content region; drop trailing whitespace spans.
289    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            // Only content_bytes contribute (not trailing whitespace).
299            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    /// Earliest dialect stream step observed for this tool action.
342    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        // Input dropped without finish
445        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    /// OpenAI Chat Completions streaming SSE.
475    async fn ingest_openai_chat(&mut self) -> Result<(), InterpreterError> {
476        // Consume the latest chunk from frame_buf (append already done by caller).
477        // OpenAiSseState owns its own line carry; feed only the new portion.
478        // frame_buf accumulates full stream for limit check; we process delta.
479        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    /// Test dialect: raw UTF-8 text assembly (no JSON framing).
495    async fn ingest_test_text(&mut self) -> Result<(), InterpreterError> {
496        // Only process complete UTF-8; keep incomplete trailing bytes.
497        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    /// Z.ai CLI headless: one OpenAI chat message per NDJSON line.
526    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    /// Claude Code headless: stream-json events per NDJSON line.
544    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            // Flush any trailing line / require [DONE].
641            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        // Do not promote incomplete sentences.
651        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        // Mark incomplete tools
665        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            // Only expose complete payloads
814            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        // Select lane before ordinal so each channel keeps independent ordering.
856        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        // Created-and-complete: emit Created (complete state)
880        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// silence unused import warning for UsageObservation if not used yet
1024#[allow(dead_code)]
1025fn _u() -> Option<UsageObservation> {
1026    None
1027}