1use std::collections::{HashMap, VecDeque};
9use std::sync::atomic::{AtomicBool, Ordering};
10use std::sync::{Arc, Mutex as StdMutex};
11
12use async_trait::async_trait;
13use serde_json::{json, Value};
14use tokio::sync::{broadcast, mpsc, oneshot};
15
16use super::{HarnessEvent, RuntimeCapabilities, RuntimeConnection, RuntimeHandle, RuntimeInput};
17use crate::frontend::{
18 FrontendActions, FrontendAttachment, FrontendConnectionState, FrontendDisplayCapabilities,
19 FrontendEvent, FrontendRuntime, FrontendRuntimeDescriptor, FrontendRuntimeError,
20 FrontendTurnState, FRONTEND_REPLAY_CAPACITY, FRONTEND_RUNTIME_SCHEMA_VERSION,
21};
22use crate::server::RuntimeSubmitError;
23use crate::{Error, FrontendResponse, Result};
24
25enum HostCommand {
26 Submit {
27 input: RuntimeInput,
28 reply: oneshot::Sender<Result<Option<String>>>,
29 },
30 Interrupt {
31 reply: oneshot::Sender<Result<()>>,
32 },
33 Steer {
34 text: String,
35 reply: oneshot::Sender<Result<()>>,
36 },
37 Respond {
38 request_id: Value,
39 response: Value,
40 reply: oneshot::Sender<Result<()>>,
41 },
42 Shutdown {
43 reply: oneshot::Sender<Result<()>>,
44 },
45}
46
47struct ProjectionState {
48 next_sequence: u64,
49 replay: VecDeque<FrontendEvent>,
50}
51
52pub struct HostedHarnessRuntime {
54 handle: RuntimeHandle,
55 capabilities: RuntimeCapabilities,
56 commands: mpsc::Sender<HostCommand>,
57 raw_events: broadcast::Sender<HarnessEvent>,
58 frontend_events: broadcast::Sender<FrontendEvent>,
59 projection: StdMutex<ProjectionState>,
60 pending_requests: StdMutex<HashMap<u64, Value>>,
67 busy: AtomicBool,
68 closed: AtomicBool,
69}
70
71impl HostedHarnessRuntime {
72 pub fn spawn(
75 runtime: Box<dyn RuntimeConnection>,
76 capabilities: RuntimeCapabilities,
77 ) -> (Arc<Self>, HostedHarnessConnection) {
78 let handle = runtime.handle().clone();
79 let (commands, command_rx) = mpsc::channel(32);
80 let (raw_events, raw_rx) = broadcast::channel(1024);
81 let (frontend_events, _) = broadcast::channel(1024);
82 let host = Arc::new(Self {
83 handle: handle.clone(),
84 capabilities,
85 commands,
86 raw_events,
87 frontend_events,
88 projection: StdMutex::new(ProjectionState {
89 next_sequence: 1,
90 replay: VecDeque::new(),
91 }),
92 pending_requests: StdMutex::new(HashMap::new()),
93 busy: AtomicBool::new(false),
94 closed: AtomicBool::new(false),
95 });
96 tokio::spawn(run_native_runtime(
97 runtime,
98 Arc::downgrade(&host),
99 command_rx,
100 ));
101 let connection = HostedHarnessConnection {
102 host: host.clone(),
103 handle,
104 events: raw_rx,
105 closed: false,
106 };
107 (host, connection)
108 }
109
110 pub fn frontend_sender(&self) -> broadcast::Sender<FrontendEvent> {
112 self.frontend_events.clone()
113 }
114
115 pub async fn shutdown(&self) -> Result<()> {
117 if self.closed.load(Ordering::SeqCst) {
118 return Ok(());
119 }
120 let (reply, response) = oneshot::channel();
121 self.commands
122 .send(HostCommand::Shutdown { reply })
123 .await
124 .map_err(|_| Error::Other("hosted harness runtime is closed".into()))?;
125 response
126 .await
127 .map_err(|_| Error::Other("hosted harness runtime stopped before shutdown".into()))?
128 }
129
130 fn claim_submit(&self) -> Result<()> {
131 if self.closed.load(Ordering::SeqCst) {
132 return Err(Error::Other("hosted harness runtime is closed".into()));
133 }
134 if self
135 .busy
136 .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
137 .is_err()
138 {
139 return Err(Error::Other("a harness turn is already in progress".into()));
140 }
141 Ok(())
142 }
143
144 async fn submit_native_claimed(&self, input: RuntimeInput) -> Result<Option<String>> {
145 self.publish(json!({"type":"user_message", "text":input.text}));
146 self.publish(json!({"type":"turn_started"}));
147 let (reply, response) = oneshot::channel();
148 if self
149 .commands
150 .send(HostCommand::Submit { input, reply })
151 .await
152 .is_err()
153 {
154 self.busy.store(false, Ordering::SeqCst);
155 self.publish(
156 json!({"type":"turn_failed", "message":"Hosted harness runtime is closed."}),
157 );
158 self.publish(json!({"type":"turn_completed"}));
159 self.mark_closed("Harness runtime command channel closed.");
160 return Err(Error::Other("hosted harness runtime is closed".into()));
161 }
162 match response.await {
163 Ok(Ok(turn)) => Ok(turn),
164 Ok(Err(error)) => {
165 self.busy.store(false, Ordering::SeqCst);
166 self.publish(json!({"type":"turn_failed", "message":error.to_string()}));
167 self.publish(json!({"type":"turn_completed"}));
168 Err(error)
169 }
170 Err(_) => {
171 self.busy.store(false, Ordering::SeqCst);
172 self.publish(json!({"type":"turn_failed", "message":"Hosted harness runtime stopped before accepting input."}));
173 self.publish(json!({"type":"turn_completed"}));
174 self.mark_closed("Harness runtime stopped before accepting input.");
175 Err(Error::Other(
176 "hosted harness runtime stopped before accepting input".into(),
177 ))
178 }
179 }
180 }
181
182 async fn submit_native(&self, text: String) -> Result<Option<String>> {
183 self.claim_submit()?;
184 self.submit_native_claimed(RuntimeInput {
185 text,
186 image_urls: Vec::new(),
187 })
188 .await
189 }
190
191 async fn interrupt_native(&self) -> Result<()> {
192 let (reply, response) = oneshot::channel();
193 self.commands
194 .send(HostCommand::Interrupt { reply })
195 .await
196 .map_err(|_| Error::Other("hosted harness runtime is closed".into()))?;
197 response
198 .await
199 .map_err(|_| Error::Other("hosted harness runtime stopped before interrupt".into()))?
200 }
201
202 async fn steer_native(&self, text: String) -> Result<()> {
203 let (reply, response) = oneshot::channel();
204 self.commands
205 .send(HostCommand::Steer { text, reply })
206 .await
207 .map_err(|_| Error::Other("hosted harness runtime is closed".into()))?;
208 response
209 .await
210 .map_err(|_| Error::Other("hosted harness runtime stopped before steering".into()))?
211 }
212
213 async fn respond_native(&self, request_id: Value, response: Value) -> Result<()> {
214 self.respond_native_as(request_id, response, None).await
215 }
216
217 async fn respond_native_as(
221 &self,
222 request_id: Value,
223 response: Value,
224 canonical: Option<Value>,
225 ) -> Result<()> {
226 let (reply, completed) = oneshot::channel();
227 self.commands
228 .send(HostCommand::Respond {
229 request_id: request_id.clone(),
230 response: response.clone(),
231 reply,
232 })
233 .await
234 .map_err(|_| Error::Other("hosted harness runtime is closed".into()))?;
235 completed
236 .await
237 .map_err(|_| Error::Other("hosted harness runtime stopped before response".into()))??;
238 let resolved = {
242 let mut pending = self
243 .pending_requests
244 .lock()
245 .unwrap_or_else(std::sync::PoisonError::into_inner);
246 let found = pending
247 .iter()
248 .find(|(_, native)| *native == &request_id)
249 .map(|(id, _)| *id);
250 found.and_then(|id| pending.remove(&id).map(|_| id))
251 };
252 if let Some(id) = resolved {
253 self.publish(json!({
254 "type": "request_resolved",
255 "request_id": id,
256 "response": canonical.unwrap_or(response),
257 }));
258 }
259 Ok(())
260 }
261
262 fn native_request_id(&self, request_id: u64) -> Option<Value> {
264 self.pending_requests
265 .lock()
266 .unwrap_or_else(std::sync::PoisonError::into_inner)
267 .get(&request_id)
268 .cloned()
269 }
270
271 fn publish(&self, payload: Value) {
272 let event = {
273 let mut projection = self
274 .projection
275 .lock()
276 .unwrap_or_else(std::sync::PoisonError::into_inner);
277 let event = FrontendEvent::new(projection.next_sequence, payload);
278 projection.next_sequence = projection.next_sequence.saturating_add(1);
279 projection.replay.push_back(event.clone());
280 while projection.replay.len() > FRONTEND_REPLAY_CAPACITY {
281 projection.replay.pop_front();
282 }
283 event
284 };
285 let _ = self.frontend_events.send(event);
286 }
287
288 fn accept_native_event(&self, event: HarnessEvent) {
289 let _ = self.raw_events.send(event.clone());
290 for payload in project_native_event(self.handle.harness.as_str(), &event) {
291 let terminal = matches!(
292 payload.get("type").and_then(Value::as_str),
293 Some("turn_succeeded" | "turn_interrupted" | "turn_failed")
294 );
295 if terminal {
296 self.busy.store(false, Ordering::SeqCst);
297 }
298 if payload.get("type").and_then(Value::as_str) == Some("request") {
299 if let (Some(id), Some(native)) = (
300 payload.pointer("/request/id").and_then(Value::as_u64),
301 payload
302 .pointer("/request/payload/native_request_id")
303 .cloned(),
304 ) {
305 self.pending_requests
306 .lock()
307 .unwrap_or_else(std::sync::PoisonError::into_inner)
308 .insert(id, native);
309 }
310 }
311 self.publish(payload);
312 }
313 }
314
315 fn mark_closed(&self, message: impl Into<String>) {
316 if self.closed.swap(true, Ordering::SeqCst) {
317 return;
318 }
319 let message = message.into();
320 self.busy.store(false, Ordering::SeqCst);
321 let _ = self.raw_events.send(HarnessEvent {
325 sequence: None,
326 kind: "transport_closed".into(),
327 payload: json!({"message":message, "terminal":true}),
328 });
329 self.publish(json!({"type":"runtime_disconnected", "message":message}));
330 }
331
332 fn descriptor(&self) -> FrontendRuntimeDescriptor {
333 FrontendRuntimeDescriptor {
334 schema_version: FRONTEND_RUNTIME_SCHEMA_VERSION,
335 session_id: self.handle.runtime_id.clone(),
336 source_harness: Some(self.handle.harness.as_str().to_string()),
337 emulation_profile: None,
338 active_modules: Vec::new(),
339 commands: Vec::new(),
340 operations: Vec::new(),
341 actions: FrontendActions {
342 submit: self.capabilities.send_input,
343 interrupt: self.capabilities.interrupt,
344 steer: self.capabilities.steer,
345 respond: !hosted_answerable_decisions(self.handle.harness.as_str()).is_empty(),
365 detach: true,
366 close: false,
367 },
368 display: FrontendDisplayCapabilities {
369 event_kinds: vec![
370 "user_message".into(),
371 "turn_started".into(),
372 "turn_succeeded".into(),
373 "turn_interrupted".into(),
374 "turn_failed".into(),
375 "text_delta".into(),
376 "reasoning".into(),
377 "tool_call_started".into(),
378 "tool_call_completed".into(),
379 "request".into(),
382 "native_event".into(),
383 "runtime_disconnected".into(),
384 ],
385 opaque_fallback: true,
386 },
387 model: self.handle.harness.as_str().to_string(),
388 turn_state: if self.busy.load(Ordering::SeqCst) {
389 FrontendTurnState::Busy
390 } else {
391 FrontendTurnState::Idle
392 },
393 connection_state: if self.closed.load(Ordering::SeqCst) {
394 FrontendConnectionState::ShuttingDown
395 } else {
396 FrontendConnectionState::Connected
397 },
398 extensions: Default::default(),
399 }
400 }
401}
402
403#[async_trait]
404impl FrontendRuntime for HostedHarnessRuntime {
405 async fn describe(
406 &self,
407 ) -> std::result::Result<FrontendRuntimeDescriptor, FrontendRuntimeError> {
408 Ok(self.descriptor())
409 }
410
411 async fn attach(
412 &self,
413 _history_limit: usize,
414 ) -> std::result::Result<FrontendAttachment, FrontendRuntimeError> {
415 let live = self.frontend_events.subscribe();
416 let projection = self
417 .projection
418 .lock()
419 .unwrap_or_else(std::sync::PoisonError::into_inner);
420 let replay = projection.replay.clone();
421 if let Some(first) = replay.front() {
422 if first.sequence > 1 {
423 return Err(FrontendRuntimeError::ReplayGap(first.sequence - 1));
424 }
425 }
426 Ok(FrontendAttachment::new(
427 self.descriptor(),
428 Vec::new(),
429 0,
430 replay,
431 live,
432 None,
433 ))
434 }
435
436 async fn send_input(
437 self: Arc<Self>,
438 prompt: String,
439 ) -> std::result::Result<(), FrontendRuntimeError> {
440 self.claim_submit().map_err(hosted_submit_error)?;
441 tokio::spawn(async move {
442 let _ = self
443 .submit_native_claimed(RuntimeInput {
444 text: prompt,
445 image_urls: Vec::new(),
446 })
447 .await;
448 });
449 Ok(())
450 }
451
452 async fn send_input_with_images(
453 self: Arc<Self>,
454 prompt: String,
455 image_urls: Vec<String>,
456 ) -> std::result::Result<(), FrontendRuntimeError> {
457 self.claim_submit().map_err(hosted_submit_error)?;
458 tokio::spawn(async move {
459 let _ = self
460 .submit_native_claimed(RuntimeInput {
461 text: prompt,
462 image_urls,
463 })
464 .await;
465 });
466 Ok(())
467 }
468
469 async fn submit(&self, prompt: String) -> std::result::Result<String, FrontendRuntimeError> {
470 self.submit_native(prompt)
471 .await
472 .map(|turn| turn.unwrap_or_default())
473 .map_err(hosted_submit_error)
474 }
475
476 async fn interrupt(&self) -> std::result::Result<bool, FrontendRuntimeError> {
477 if !self.busy.load(Ordering::SeqCst) {
478 return Ok(false);
479 }
480 self.interrupt_native()
481 .await
482 .map_err(|error| FrontendRuntimeError::Transport(error.to_string()))?;
483 Ok(true)
487 }
488
489 async fn steer(&self, prompt: String) -> std::result::Result<(), FrontendRuntimeError> {
490 if !self.busy.load(Ordering::SeqCst) || !self.capabilities.steer {
491 return Err(FrontendRuntimeError::UnsupportedAction("steer"));
492 }
493 self.steer_native(prompt)
494 .await
495 .map_err(|error| FrontendRuntimeError::Transport(error.to_string()))
496 }
497
498 async fn respond(
499 &self,
500 response: FrontendResponse,
501 ) -> std::result::Result<(), FrontendRuntimeError> {
502 let harness = self.handle.harness.as_str();
503 let FrontendResponse::Approval {
504 request_id,
505 decision,
506 } = &response
507 else {
508 return Err(FrontendRuntimeError::UnsupportedAction(
509 "respond: a hosted harness answers approval requests only",
510 ));
511 };
512 let Some(native_id) = self.native_request_id(*request_id) else {
513 return Err(FrontendRuntimeError::UnknownRequest(*request_id));
514 };
515 let body = hosted_permission_reply(harness, *decision)
516 .map_err(FrontendRuntimeError::InvalidResponse)?;
517 let canonical = serde_json::to_value(&response).unwrap_or(Value::Null);
518 self.respond_native_as(native_id, body, Some(canonical))
519 .await
520 .map_err(|error| FrontendRuntimeError::Execution {
521 operation: crate::SdkOperation::Respond,
522 message: error.to_string(),
523 })
524 }
525}
526
527fn hosted_submit_error(error: Error) -> FrontendRuntimeError {
528 if error.to_string().contains("already in progress") {
529 FrontendRuntimeError::Submit(RuntimeSubmitError::Busy)
530 } else {
531 FrontendRuntimeError::Transport(error.to_string())
532 }
533}
534
535pub struct HostedHarnessConnection {
538 host: Arc<HostedHarnessRuntime>,
539 handle: RuntimeHandle,
540 events: broadcast::Receiver<HarnessEvent>,
541 closed: bool,
542}
543
544#[async_trait]
545impl RuntimeConnection for HostedHarnessConnection {
546 fn handle(&self) -> &RuntimeHandle {
547 &self.handle
548 }
549
550 async fn send_input(&mut self, input: RuntimeInput) -> Result<Option<String>> {
551 self.host.claim_submit()?;
552 self.host.submit_native_claimed(input).await
553 }
554
555 async fn next_event(&mut self) -> Result<Option<HarnessEvent>> {
556 match self.events.recv().await {
557 Ok(event) => Ok(Some(event)),
558 Err(broadcast::error::RecvError::Lagged(count)) => Err(Error::Other(format!(
559 "hosted harness event stream lost {count} event(s)"
560 ))),
561 Err(broadcast::error::RecvError::Closed) => Ok(None),
562 }
563 }
564
565 async fn interrupt(&mut self) -> Result<()> {
566 self.host.interrupt_native().await
567 }
568
569 async fn steer(&mut self, text: String) -> Result<()> {
570 self.host.steer_native(text).await
571 }
572
573 async fn respond(&mut self, request_id: Value, response: Value) -> Result<()> {
574 self.host.respond_native(request_id, response).await
575 }
576
577 async fn close(&mut self) -> Result<()> {
578 if self.closed {
579 return Ok(());
580 }
581 self.host.shutdown().await?;
582 self.closed = true;
583 Ok(())
584 }
585}
586
587async fn run_native_runtime(
588 mut runtime: Box<dyn RuntimeConnection>,
589 host: std::sync::Weak<HostedHarnessRuntime>,
590 mut commands: mpsc::Receiver<HostCommand>,
591) {
592 loop {
593 tokio::select! {
594 command = commands.recv() => {
595 let Some(command) = command else {
596 let _ = runtime.close().await;
597 return;
598 };
599 match command {
600 HostCommand::Submit { input, reply } => {
601 let _ = reply.send(runtime.send_input(input).await);
602 }
603 HostCommand::Interrupt { reply } => {
604 let _ = reply.send(runtime.interrupt().await);
605 }
606 HostCommand::Steer { text, reply } => {
607 let _ = reply.send(runtime.steer(text).await);
608 }
609 HostCommand::Respond { request_id, response, reply } => {
610 let _ = reply.send(runtime.respond(request_id, response).await);
611 }
612 HostCommand::Shutdown { reply } => {
613 let result = runtime.close().await;
614 let closed = result.is_ok();
615 let _ = reply.send(result);
616 if closed {
617 if let Some(host) = host.upgrade() {
618 host.mark_closed("Harness runtime closed.");
619 }
620 return;
621 }
622 }
623 }
624 }
625 event = runtime.next_event() => {
626 let Some(host) = host.upgrade() else {
627 let _ = runtime.close().await;
628 return;
629 };
630 match event {
631 Ok(Some(event)) => host.accept_native_event(event),
632 Ok(None) => {
633 host.mark_closed("Harness runtime transport closed.");
634 return;
635 }
636 Err(error) => {
637 host.mark_closed(error.to_string());
638 return;
639 }
640 }
641 }
642 }
643 }
644}
645
646pub(crate) fn hosted_answerable_decisions(harness: &str) -> &'static [&'static str] {
655 match harness {
656 "claude-code" => &["allow", "deny"],
666 _ => &[],
667 }
668}
669
670fn hosted_permission_reply(
672 harness: &str,
673 decision: crate::FrontendApprovalDecision,
674) -> std::result::Result<Value, String> {
675 use crate::FrontendApprovalDecision as Decision;
676 let accepted = hosted_answerable_decisions(harness);
677 let wire = match decision {
678 Decision::Allow => "allow",
679 Decision::AllowForSession => "allow_for_session",
680 Decision::Deny => "deny",
681 };
682 if !accepted.contains(&wire) {
683 return Err(format!(
684 "`{wire}` is not a decision a hosted {harness} runtime can carry; this harness accepts \
685 {}",
686 if accepted.is_empty() {
687 "no portable decision — its requests are observe-only".to_string()
688 } else {
689 accepted
690 .iter()
691 .map(|decision| format!("`{decision}`"))
692 .collect::<Vec<_>>()
693 .join(" or ")
694 },
695 ));
696 }
697 match harness {
698 "claude-code" => Ok(match decision {
699 Decision::Allow => json!({"behavior": "allow"}),
700 Decision::Deny => json!({
702 "behavior": "deny",
703 "message": "denied through the supercode frontend",
704 }),
705 Decision::AllowForSession => unreachable!("refused above"),
706 }),
707 _ => Err(format!(
708 "no hosted permission reply is defined for {harness}"
709 )),
710 }
711}
712
713fn project_native_event(harness: &str, event: &HarnessEvent) -> Vec<Value> {
714 let key = event.kind.to_ascii_lowercase().replace('-', "_");
715 let payload = &event.payload;
716 if key == "transport_closed" {
717 return vec![
718 json!({"type":"runtime_disconnected", "message":extract_text(payload).unwrap_or_else(|| "Harness runtime transport closed.".into())}),
719 ];
720 }
721 if key == "transport_error" || key == "error" {
722 return vec![
723 json!({"type":"turn_failed", "message":extract_text(payload).unwrap_or_else(|| "Harness runtime failed.".into()), "raw":payload}),
724 ];
725 }
726 if key == "session/update" {
727 let update = payload
728 .pointer("/params/update")
729 .or_else(|| payload.get("update"))
730 .unwrap_or(payload);
731 let update_kind = update
732 .get("sessionUpdate")
733 .or_else(|| update.get("type"))
734 .and_then(Value::as_str)
735 .unwrap_or_default()
736 .to_ascii_lowercase();
737 if update_kind == "agent_message_chunk" {
738 return vec![
739 json!({"type":"text_delta", "text":extract_text(update.get("content").unwrap_or(update)).unwrap_or_default(), "raw":payload}),
740 ];
741 }
742 if update_kind == "agent_thought_chunk" {
743 return vec![
744 json!({"type":"reasoning", "text":extract_text(update.get("content").unwrap_or(update)).unwrap_or_default(), "raw":payload}),
745 ];
746 }
747 if matches!(update_kind.as_str(), "tool_call" | "tool_call_update") {
748 return vec![project_tool(update, payload)];
749 }
750 }
751 if key == "supercode/acp_request_completed" {
752 let failure = payload
753 .pointer("/params/error")
754 .or_else(|| payload.get("error"));
755 return completion(failure.and_then(extract_text));
756 }
757 match harness {
758 "codex" => project_codex(&key, payload),
759 "claude-code" => project_claude(&key, payload),
760 "pi" => project_pi(&key, payload),
761 "opencode" => project_opencode(&key, payload),
762 _ => project_generic(&key, payload),
763 }
764}
765
766fn project_codex(key: &str, payload: &Value) -> Vec<Value> {
767 if key == "turn/started" {
768 return vec![native_payload(key, payload)];
769 }
770 if key == "turn/completed" {
771 let status = payload
772 .pointer("/params/turn/status")
773 .or_else(|| payload.pointer("/turn/status"))
774 .and_then(Value::as_str)
775 .unwrap_or("completed")
776 .to_ascii_lowercase();
777 return completion(
778 (status.contains("fail") || status.contains("error") || status.contains("cancel"))
779 .then(|| extract_text(payload).unwrap_or_else(|| status.clone())),
780 );
781 }
782 if key.ends_with("/delta") {
783 let text = payload
784 .pointer("/params/delta")
785 .or_else(|| payload.get("delta"))
786 .and_then(extract_text)
787 .or_else(|| extract_text(payload))
788 .unwrap_or_default();
789 return vec![
790 json!({"type":if key.contains("reasoning") { "reasoning" } else { "text_delta" }, "text":text, "raw":payload}),
791 ];
792 }
793 if key.contains("commandexecution")
794 || key.contains("mcptool")
795 || key.contains("filechange")
796 || key.contains("tool")
797 {
798 let source = payload
799 .pointer("/params/item")
800 .or_else(|| payload.get("item"))
801 .unwrap_or(payload);
802 return vec![project_tool(source, payload)];
803 }
804 vec![native_payload(key, payload)]
805}
806
807fn project_claude(key: &str, payload: &Value) -> Vec<Value> {
819 if key == "result" {
820 let failed = payload.get("is_error").and_then(Value::as_bool) == Some(true)
821 || payload
822 .get("subtype")
823 .and_then(Value::as_str)
824 .is_some_and(|subtype| subtype != "success");
825 return completion(
826 failed.then(|| extract_text(payload).unwrap_or_else(|| "Claude turn failed.".into())),
827 );
828 }
829 if key == "stream_event" {
830 let stream = payload
835 .get("event")
836 .or_else(|| payload.get("stream_event"))
837 .unwrap_or(payload);
838 if stream.get("type").and_then(Value::as_str) == Some("content_block_delta") {
839 return vec![native_payload(key, payload)];
840 }
841 }
842 if key == "assistant" {
843 return project_claude_blocks(payload, true);
844 }
845 if key == "user" {
846 return project_claude_blocks(payload, false);
847 }
848 if key == "control_request" {
849 return project_claude_control_request(payload);
850 }
851 if key == "system" {
852 if payload.get("subtype").and_then(Value::as_str) == Some("init") {
855 return Vec::new();
856 }
857 }
858 if key == "rate_limit_event" {
859 let status = payload
864 .pointer("/rate_limit_info/status")
865 .and_then(Value::as_str)
866 .unwrap_or_default();
867 if status.starts_with("allowed") {
868 return Vec::new();
869 }
870 }
871 project_generic(key, payload)
872}
873
874fn claude_blocks(payload: &Value) -> Option<&Vec<Value>> {
876 payload
877 .pointer("/message/content")
878 .or_else(|| payload.get("content"))
879 .and_then(Value::as_array)
880}
881
882fn project_claude_blocks(payload: &Value, assistant: bool) -> Vec<Value> {
883 let Some(blocks) = claude_blocks(payload) else {
884 let text = extract_text(payload).unwrap_or_default();
887 if text.is_empty() {
888 return vec![native_payload(
889 if assistant { "assistant" } else { "user" },
890 payload,
891 )];
892 }
893 return vec![
894 json!({"type":if assistant { "text_delta" } else { "user_message" }, "text":text, "raw":payload}),
895 ];
896 };
897 let mut projected = Vec::new();
898 for block in blocks {
899 match block
900 .get("type")
901 .and_then(Value::as_str)
902 .unwrap_or_default()
903 {
904 "text" => {
905 let text = block
906 .get("text")
907 .and_then(Value::as_str)
908 .unwrap_or_default();
909 if !text.is_empty() {
910 projected.push(json!({
911 "type": if assistant { "text_delta" } else { "user_message" },
912 "text": text,
913 "raw": block,
914 }));
915 }
916 }
917 "thinking" | "redacted_thinking" => {
918 let text = extract_text(block).unwrap_or_default();
919 if !text.is_empty() {
920 projected.push(json!({"type":"reasoning", "text":text, "raw":block}));
921 }
922 }
923 "tool_use" => projected.push(json!({
924 "type": "tool_call_started",
925 "id": block.get("id").cloned().unwrap_or(Value::Null),
926 "name": block.get("name").cloned().unwrap_or(Value::Null),
927 "arguments": block.get("input").map(Value::to_string).unwrap_or_default(),
928 "raw": block,
929 })),
930 "tool_result" => projected.push(json!({
931 "type": "tool_call_completed",
935 "id": block.get("tool_use_id").cloned().unwrap_or(Value::Null),
936 "name": Value::Null,
937 "output": extract_text(block.get("content").unwrap_or(block)).unwrap_or_default(),
938 "is_error": block.get("is_error").and_then(Value::as_bool).unwrap_or(false),
939 "raw": block,
940 })),
941 _ => projected.push(native_payload(
942 if assistant { "assistant" } else { "user" },
943 block,
944 )),
945 }
946 }
947 projected
948}
949
950fn project_claude_control_request(payload: &Value) -> Vec<Value> {
959 let request = payload.get("request").unwrap_or(payload);
960 if request.get("subtype").and_then(Value::as_str) != Some("can_use_tool") {
961 return vec![native_payload("control_request", payload)];
962 }
963 let native_id = payload
964 .get("request_id")
965 .and_then(Value::as_str)
966 .unwrap_or_default();
967 vec![json!({
968 "type": "request",
969 "request": {
970 "id": claude_request_id(native_id),
971 "kind": "approval",
972 "payload": {
973 "tool": request.get("tool_name").or_else(|| request.get("toolName")).cloned().unwrap_or(Value::Null),
974 "arguments": request.get("input").cloned().unwrap_or(Value::Null),
975 "native_request_id": native_id,
976 "decisions": hosted_answerable_decisions("claude-code"),
980 },
981 },
982 })]
983}
984
985fn claude_request_id(native_id: &str) -> u64 {
992 let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
993 for byte in native_id.as_bytes() {
994 hash ^= u64::from(*byte);
995 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
996 }
997 hash & ((1_u64 << 53) - 1)
999}
1000
1001fn project_pi(key: &str, payload: &Value) -> Vec<Value> {
1002 match key {
1003 "agent_start" => vec![native_payload(key, payload)],
1004 "agent_end" => completion(payload.get("error").and_then(extract_text)),
1005 "message_update" => {
1006 let update = payload
1011 .get("assistantMessageEvent")
1012 .or_else(|| payload.get("event"))
1013 .unwrap_or(payload);
1014 let kind = update
1015 .get("type")
1016 .and_then(Value::as_str)
1017 .unwrap_or_default();
1018 match kind {
1019 "text_delta" => vec![
1020 json!({"type":"text_delta", "text":update.get("delta").and_then(Value::as_str).unwrap_or_default(), "raw":payload}),
1021 ],
1022 "thinking_delta" => vec![
1023 json!({"type":"reasoning", "text":update.get("delta").and_then(Value::as_str).unwrap_or_default(), "raw":payload}),
1024 ],
1025 _ => vec![native_payload(key, payload)],
1026 }
1027 }
1028 _ if key.starts_with("tool_execution_") => vec![project_tool(payload, payload)],
1029 _ => vec![native_payload(key, payload)],
1030 }
1031}
1032
1033fn project_opencode(key: &str, payload: &Value) -> Vec<Value> {
1034 if key == "session.idle" {
1035 return completion(None);
1036 }
1037 if key == "session.status" {
1038 let status = payload
1039 .pointer("/properties/status/type")
1040 .or_else(|| payload.pointer("/status/type"))
1041 .and_then(Value::as_str)
1042 .unwrap_or_default();
1043 if status == "busy" {
1044 return vec![native_payload(key, payload)];
1045 }
1046 if status == "idle" {
1047 return completion(None);
1048 }
1049 }
1050 if key == "message.part.updated" {
1051 let part = payload
1052 .pointer("/properties/part")
1053 .or_else(|| payload.get("part"))
1054 .unwrap_or(payload);
1055 if let Some(delta) = payload
1056 .pointer("/properties/delta")
1057 .or_else(|| payload.get("delta"))
1058 .and_then(Value::as_str)
1059 {
1060 let reasoning = part
1061 .get("type")
1062 .and_then(Value::as_str)
1063 .is_some_and(|kind| kind.contains("reasoning"));
1064 return vec![
1065 json!({"type":if reasoning { "reasoning" } else { "text_delta" }, "text":delta, "raw":payload}),
1066 ];
1067 }
1068 if part
1069 .get("type")
1070 .and_then(Value::as_str)
1071 .is_some_and(|kind| kind.contains("tool"))
1072 {
1073 return vec![project_tool(part, payload)];
1074 }
1075 }
1076 if key == "session.error" {
1077 return completion(Some(
1078 extract_text(payload).unwrap_or_else(|| "OpenCode session failed.".into()),
1079 ));
1080 }
1081 vec![native_payload(key, payload)]
1082}
1083
1084fn project_generic(key: &str, payload: &Value) -> Vec<Value> {
1085 match key {
1086 "turn_started" | "turn/started" | "agent_start" => {
1087 vec![native_payload(key, payload)]
1088 }
1089 "turn_completed" | "turn/completed" | "agent_end" => {
1090 completion(payload.get("error").and_then(extract_text))
1091 }
1092 "output_delta" | "content_delta" => vec![
1093 json!({"type":"text_delta", "text":extract_text(payload).unwrap_or_default(), "raw":payload}),
1094 ],
1095 "reasoning_delta" => vec![
1096 json!({"type":"reasoning", "text":extract_text(payload).unwrap_or_default(), "raw":payload}),
1097 ],
1098 "tool" => vec![project_tool(payload, payload)],
1099 _ => vec![native_payload(key, payload)],
1100 }
1101}
1102
1103fn completion(error: Option<String>) -> Vec<Value> {
1104 match error {
1105 Some(message) => vec![
1106 json!({"type":"turn_failed", "message":message}),
1107 json!({"type":"turn_completed"}),
1108 ],
1109 None => vec![
1110 json!({"type":"turn_succeeded"}),
1111 json!({"type":"turn_completed"}),
1112 ],
1113 }
1114}
1115
1116fn project_tool(source: &Value, raw: &Value) -> Value {
1117 let status = source
1118 .get("status")
1119 .or_else(|| source.get("state"))
1120 .or_else(|| source.get("sessionUpdate"))
1121 .and_then(Value::as_str)
1122 .unwrap_or_default()
1123 .to_ascii_lowercase();
1124 let completed = status.contains("complete")
1125 || status.contains("result")
1126 || status.contains("success")
1127 || status.contains("error")
1128 || status.contains("fail");
1129 let arguments = source
1130 .get("arguments")
1131 .or_else(|| source.get("input"))
1132 .or_else(|| source.get("rawInput"))
1133 .cloned()
1134 .unwrap_or(Value::Null);
1135 json!({
1136 "type": if completed { "tool_call_completed" } else { "tool_call_started" },
1137 "id": source.get("toolCallId").or_else(|| source.get("tool_call_id")).or_else(|| source.get("callId")).or_else(|| source.get("id")).cloned().unwrap_or(Value::Null),
1138 "name": source.get("title").or_else(|| source.get("name")).or_else(|| source.get("tool")).or_else(|| source.get("toolName")).cloned().unwrap_or(Value::Null),
1139 "arguments": if arguments.is_string() { arguments } else { Value::String(arguments.to_string()) },
1140 "output": extract_text(source.get("result").or_else(|| source.get("output")).or_else(|| source.get("content")).unwrap_or(&Value::Null)).unwrap_or_default(),
1141 "is_error": status.contains("error") || status.contains("fail") || status.contains("denied"),
1142 "raw": raw,
1143 })
1144}
1145
1146fn native_payload(kind: &str, payload: &Value) -> Value {
1147 json!({"type":"native_event", "kind":kind, "raw":payload})
1148}
1149
1150fn extract_text(value: &Value) -> Option<String> {
1151 match value {
1152 Value::String(text) => Some(text.clone()),
1153 Value::Array(values) => {
1154 let text = values
1155 .iter()
1156 .filter_map(extract_text)
1157 .collect::<Vec<_>>()
1158 .join("\n");
1159 (!text.is_empty()).then_some(text)
1160 }
1161 Value::Object(object) => {
1162 for key in ["text", "delta", "content", "message", "result", "error"] {
1163 if let Some(text) = object.get(key).and_then(Value::as_str) {
1164 return Some(text.to_string());
1165 }
1166 }
1167 for key in [
1168 "delta",
1169 "content",
1170 "message",
1171 "error",
1172 "data",
1173 "part",
1174 "params",
1175 "properties",
1176 "update",
1177 "event",
1178 ] {
1179 if let Some(text) = object.get(key).and_then(extract_text) {
1180 return Some(text);
1181 }
1182 }
1183 None
1184 }
1185 _ => None,
1186 }
1187}
1188
1189#[cfg(test)]
1190mod tests {
1191 #[test]
1192 fn pi_message_update_projects_only_the_deltas() {
1193 use serde_json::json;
1194 let delta = json!({"type":"message_update","assistantMessageEvent":{"type":"text_delta","contentIndex":0,"delta":"ack: hi"}});
1195 let end = json!({"type":"message_update","assistantMessageEvent":{"type":"text_end","contentIndex":0,"content":"ack: hi"}});
1196 let start = json!({"type":"message_update","assistantMessageEvent":{"type":"text_start","contentIndex":0}});
1197 let projected = super::project_pi("message_update", &delta);
1198 assert_eq!(projected[0]["type"], "text_delta");
1199 assert_eq!(projected[0]["text"], "ack: hi");
1200 for repeat in [&end, &start] {
1201 let projected = super::project_pi("message_update", repeat);
1202 assert_eq!(
1203 projected[0]["type"], "native_event",
1204 "{repeat} carries no new text"
1205 );
1206 }
1207 let thinking = json!({"type":"message_update","assistantMessageEvent":{"type":"thinking_delta","contentIndex":0,"delta":"hm"}});
1208 let projected = super::project_pi("message_update", &thinking);
1209 assert_eq!(projected[0]["type"], "reasoning");
1210 assert_eq!(projected[0]["text"], "hm");
1211 }
1212
1213 use super::*;
1214 use crate::{HarnessId, RuntimeEndpoint};
1215
1216 struct ControlledRuntime {
1217 handle: RuntimeHandle,
1218 events: mpsc::UnboundedReceiver<HarnessEvent>,
1219 close_failures: usize,
1220 }
1221
1222 #[async_trait]
1223 impl RuntimeConnection for ControlledRuntime {
1224 fn handle(&self) -> &RuntimeHandle {
1225 &self.handle
1226 }
1227
1228 async fn send_input(&mut self, _input: RuntimeInput) -> Result<Option<String>> {
1229 Ok(Some("turn-1".into()))
1230 }
1231
1232 async fn next_event(&mut self) -> Result<Option<HarnessEvent>> {
1233 Ok(self.events.recv().await)
1234 }
1235
1236 async fn interrupt(&mut self) -> Result<()> {
1237 Ok(())
1238 }
1239
1240 async fn respond(&mut self, _request_id: Value, _response: Value) -> Result<()> {
1241 Ok(())
1242 }
1243
1244 async fn close(&mut self) -> Result<()> {
1245 if self.close_failures > 0 {
1246 self.close_failures -= 1;
1247 return Err(Error::Other("cleanup temporarily unavailable".into()));
1248 }
1249 Ok(())
1250 }
1251 }
1252
1253 fn controlled_runtime() -> (
1254 Box<dyn RuntimeConnection>,
1255 mpsc::UnboundedSender<HarnessEvent>,
1256 ) {
1257 controlled_runtime_with_close_failures(0)
1258 }
1259
1260 fn controlled_runtime_with_close_failures(
1261 close_failures: usize,
1262 ) -> (
1263 Box<dyn RuntimeConnection>,
1264 mpsc::UnboundedSender<HarnessEvent>,
1265 ) {
1266 let (events, event_rx) = mpsc::unbounded_channel();
1267 (
1268 Box::new(ControlledRuntime {
1269 handle: RuntimeHandle {
1270 harness: HarnessId::from(HarnessId::PI),
1271 runtime_id: "shared-runtime".into(),
1272 endpoint: RuntimeEndpoint::LocalProcess {
1273 pid: None,
1274 command: vec!["controlled-runtime".into()],
1275 protocol: "test".into(),
1276 },
1277 },
1278 events: event_rx,
1279 close_failures,
1280 }),
1281 events,
1282 )
1283 }
1284
1285 #[tokio::test]
1286 async fn failed_runtime_close_does_not_stop_the_host_or_discard_its_owner() {
1287 let (runtime, _events) = controlled_runtime_with_close_failures(1);
1288 let (host, mut owner) = HostedHarnessRuntime::spawn(runtime, capabilities());
1289 assert!(owner.close().await.is_err());
1290 assert!(!owner.closed);
1291 assert!(!host.closed.load(Ordering::SeqCst));
1292 owner
1293 .send_input(RuntimeInput {
1294 text: "still usable".into(),
1295 image_urls: Vec::new(),
1296 })
1297 .await
1298 .unwrap();
1299 owner.close().await.unwrap();
1300 assert!(owner.closed);
1301 assert!(host.closed.load(Ordering::SeqCst));
1302 }
1303
1304 fn capabilities() -> RuntimeCapabilities {
1305 RuntimeCapabilities {
1306 start_session: true,
1307 resume_session: true,
1308 attach_existing_process: false,
1309 send_input: true,
1310 stream_events: true,
1311 interrupt: true,
1312 steer: false,
1313 respond_to_requests: false,
1314 }
1315 }
1316
1317 #[tokio::test]
1318 async fn native_eof_closes_the_raw_owner_connection() {
1319 let (runtime, events) = controlled_runtime();
1320 let (_host, mut connection) = HostedHarnessRuntime::spawn(runtime, capabilities());
1321 drop(events);
1322
1323 let event =
1324 tokio::time::timeout(std::time::Duration::from_secs(1), connection.next_event())
1325 .await
1326 .expect("raw owner should not hang after native EOF")
1327 .unwrap()
1328 .expect("EOF is projected as an explicit terminal event");
1329 assert_eq!(event.kind, "transport_closed");
1330 assert_eq!(event.payload["terminal"], true);
1331 }
1332
1333 #[tokio::test]
1334 async fn adapters_without_an_operation_route_preserve_the_requested_id() {
1335 let (runtime, _events) = controlled_runtime();
1336 let (host, _owner) = HostedHarnessRuntime::spawn(runtime, capabilities());
1337 let operation_id = "prompt:not-advertised".to_string();
1338 let error = FrontendRuntime::invoke(
1339 host.as_ref(),
1340 crate::FrontendOperationInvocation::Prompt {
1341 operation_id: operation_id.clone(),
1342 arguments: String::new(),
1343 },
1344 )
1345 .await
1346 .unwrap_err();
1347
1348 assert!(
1349 matches!(error, FrontendRuntimeError::UnsupportedOperation(id) if id == operation_id)
1350 );
1351 }
1352
1353 #[tokio::test]
1354 async fn interrupt_stays_busy_until_the_native_terminal_event() {
1355 let (runtime, events) = controlled_runtime();
1356 let (host, mut owner) = HostedHarnessRuntime::spawn(runtime, capabilities());
1357 let mut terminal = FrontendRuntime::attach(host.as_ref(), 100).await.unwrap();
1358
1359 assert_eq!(
1360 FrontendRuntime::submit(host.as_ref(), "hello".into())
1361 .await
1362 .unwrap(),
1363 "turn-1"
1364 );
1365 assert_eq!(terminal.next_event().await.unwrap().kind, "user_message");
1366 assert_eq!(terminal.next_event().await.unwrap().kind, "turn_started");
1367 assert!(FrontendRuntime::interrupt(host.as_ref()).await.unwrap());
1368 assert_eq!(
1369 FrontendRuntime::describe(host.as_ref())
1370 .await
1371 .unwrap()
1372 .turn_state,
1373 FrontendTurnState::Busy
1374 );
1375 assert!(
1376 tokio::time::timeout(std::time::Duration::from_millis(20), terminal.next_event())
1377 .await
1378 .is_err(),
1379 "interrupt acceptance must not manufacture turn completion"
1380 );
1381
1382 events
1383 .send(HarnessEvent {
1384 sequence: None,
1385 kind: "agent_end".into(),
1386 payload: json!({}),
1387 })
1388 .unwrap();
1389 assert_eq!(terminal.next_event().await.unwrap().kind, "turn_succeeded");
1390 assert_eq!(terminal.next_event().await.unwrap().kind, "turn_completed");
1391 assert_eq!(
1392 FrontendRuntime::describe(host.as_ref())
1393 .await
1394 .unwrap()
1395 .turn_state,
1396 FrontendTurnState::Idle
1397 );
1398 owner.close().await.unwrap();
1399 }
1400
1401 #[test]
1402 fn native_start_events_do_not_duplicate_the_hosted_turn_boundary() {
1403 let event = HarnessEvent {
1404 sequence: None,
1405 kind: "turn/started".into(),
1406 payload: json!({"method":"turn/started"}),
1407 };
1408 let projected = project_native_event(HarnessId::CODEX, &event);
1409 assert_eq!(projected.len(), 1);
1410 assert_eq!(projected[0]["type"], "native_event");
1411 }
1412}