use std::future::Future;
use std::process::ExitStatus;
use anyhow::Result;
use serde::Deserialize;
use tokio::io::AsyncRead;
use tokio::task::JoinHandle;
pub fn read_message_and_session() -> (String, String) {
let message = std::env::var("ILINK_MESSAGE").unwrap_or_default();
let session_id = std::env::var("ILINK_SESSION_ID").unwrap_or_default();
(message, session_id)
}
pub async fn with_session_resume_fallback<T, F, Fut>(
tool: &str,
message: &str,
session_id: &str,
op: F,
) -> Result<T>
where
F: Fn(String, String) -> Fut,
Fut: Future<Output = Result<T>>,
{
if session_id.is_empty() {
return op(message.to_string(), String::new()).await;
}
match op(message.to_string(), session_id.to_string()).await {
Ok(v) => Ok(v),
Err(e) => {
eprintln!("[{tool}] resume failed ({e:#}), retrying as new session");
op(message.to_string(), String::new()).await
}
}
}
pub fn emit_session_line(session_id: Option<&str>) {
if let Some(sid) = session_id {
if !sid.is_empty() {
println!("ILINK_SESSION:{sid}");
}
}
}
pub fn emit_partial(text: &str) -> Result<()> {
println!("ILINK_PARTIAL:{}", serde_json::to_string(text)?);
std::io::Write::flush(&mut std::io::stdout()).ok();
Ok(())
}
pub fn spawn_capped_drain<R>(reader: R) -> JoinHandle<String>
where
R: AsyncRead + Unpin + Send + 'static,
{
tokio::spawn(async move {
use tokio::io::{AsyncReadExt, BufReader};
let mut buf = Vec::new();
BufReader::new(reader)
.take(crate::bridge::MAX_CLI_CAPTURE_BYTES as u64)
.read_to_end(&mut buf)
.await
.ok();
String::from_utf8_lossy(&buf).into_owned()
})
}
pub fn ensure_success(tool: &str, status: ExitStatus, stderr: &str, recovered: bool) -> Result<()> {
if !status.success() && !recovered {
let detail = if stderr.is_empty() {
"(no output)"
} else {
stderr
};
anyhow::bail!(
"{tool} exited with status {:?}\nstderr: {detail}",
status.code()
);
}
Ok(())
}
#[derive(Debug, Deserialize)]
pub struct StreamJsonEvent {
#[serde(rename = "type")]
pub event_type: Option<String>,
pub result: Option<String>,
pub session_id: Option<String>,
#[allow(dead_code)]
pub subtype: Option<String>,
pub message: Option<StreamMessage>,
}
#[derive(Debug, Deserialize)]
pub struct StreamMessage {
pub content: Option<Vec<StreamContentBlock>>,
}
impl StreamMessage {
pub fn text(&self) -> String {
self.content
.as_deref()
.unwrap_or_default()
.iter()
.filter(|b| b.block_type.as_deref() == Some("text"))
.filter_map(|b| b.text.as_deref())
.collect::<Vec<_>>()
.join("")
}
}
#[derive(Debug, Deserialize)]
pub struct StreamContentBlock {
#[serde(rename = "type")]
pub block_type: Option<String>,
pub text: Option<String>,
}