Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/user/compose-and-local-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Retries repeat the body. Do not retry a write unless repeating it is safe or the

Background calls no longer hold their originating turn open, and interrupting that turn does not stop them. When a call detaches, the model receives its tool-call ID and can stop it with `close({ call_id: "call_..." })`. Cancellation is delivered through the same result lifecycle as completion, as a failed result reporting that tool execution was cancelled.

The TUI keeps every running call visible. A running compose card shows its Runlet source inline, with live call states, binding resolution, and loop or retry counts. Completion replaces the source with the compose output. Unless the user explicitly opened or closed it, the output collapses when a later tool call or model message arrives and remains available from the tool card. Completion or failure is delivered back to the owning session and wakes the session loop directly without inserting synthetic user content. Background work is process- and session-scoped rather than a durable operating-system job, so closing Kit ends its inspectable lifetime.
The TUI keeps every running call visible. A running compose card groups its canonical ACP child tool calls. Its Runlet source is available as a bounded, neutral view; the display does not infer binding, loop, or retry state. Completion replaces the source with the compose output. Unless the user explicitly opened or closed it, the output collapses when a later tool call or model message arrives and remains available from the tool card. Completion or failure is delivered back to the owning session and wakes the session loop directly without inserting synthetic user content. Background work is process- and session-scoped rather than a durable operating-system job, so closing Kit ends its inspectable lifetime.

## Ordering, dependencies, and concurrency

Expand Down
35 changes: 10 additions & 25 deletions src/acp_child.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1248,12 +1248,7 @@ struct RunConfig {
fn harness_diagnostic(label: &str, line: &str) -> Option<String> {
if matches!(
crate::events::parse(line),
Some(
crate::events::RuntimeEvent::ChildStarted { .. }
| crate::events::RuntimeEvent::ChildFinished { .. }
| crate::events::RuntimeEvent::RunletProgress { .. }
| crate::events::RuntimeEvent::RunletTransport { .. }
)
Some(crate::events::RuntimeEvent::RunletTransport { .. })
) {
return None;
}
Expand Down Expand Up @@ -1361,12 +1356,12 @@ async fn run(
ancestor_id.as_deref(),
|output| match output {
ForwardedStderr::RuntimeLine(line) => {
if let Some(transport) = crate::runlet_progress::transport::global() {
if let Some(transport) = crate::diagnostic_transport::global() {
transport.publish_runtime_line(&line);
}
}
ForwardedStderr::Diagnostic(line) => {
if let Some(transport) = crate::runlet_progress::transport::global() {
if let Some(transport) = crate::diagnostic_transport::global() {
transport.publish_line(&line);
}
}
Expand Down Expand Up @@ -2003,9 +1998,8 @@ async fn forward_stderr(
}
match event {
crate::events::RuntimeEvent::RunletTransport { available: true } => {
deadline = Some(
tokio::time::Instant::now() + crate::runlet_progress::transport::LEASE,
);
deadline =
Some(tokio::time::Instant::now() + crate::diagnostic_transport::LEASE);
continue;
}
crate::events::RuntimeEvent::RunletTransport { available: false } => {
Expand All @@ -2017,8 +2011,7 @@ async fn forward_stderr(
_ => {}
}
if deadline.is_some() {
deadline =
Some(tokio::time::Instant::now() + crate::runlet_progress::transport::LEASE);
deadline = Some(tokio::time::Instant::now() + crate::diagnostic_transport::LEASE);
}
if event.forward_from_child() {
if let crate::events::RuntimeEvent::SubagentStateChanged {
Expand Down Expand Up @@ -2560,12 +2553,7 @@ mod tests {

#[test]
fn nested_runtime_events_are_not_forwarded_as_parent_events() {
let event = crate::events::RuntimeEvent::ChildStarted {
call: "subagent-call:compose:shell".into(),
tool: "shell".into(),
summary: "inspect".into(),
at: 0,
};
let event = crate::events::RuntimeEvent::RunletTransport { available: true };
let line = format!(
"{}{}",
crate::events::EVENT_MARKER,
Expand Down Expand Up @@ -4835,11 +4823,8 @@ for line in sys.stdin:
serde_json::to_string(&RuntimeEvent::RunletTransport { available: true })
.unwrap()
);
let started = RuntimeEvent::ChildStarted {
call: "parent:compose:0".into(),
tool: "shell".into(),
summary: "working".into(),
at: 1,
let started = RuntimeEvent::SubagentDescendantsRemoved {
ancestor_id: "parent".into(),
};
let start = format!(
"{EVENT_MARKER}{}\n",
Expand All @@ -4861,7 +4846,7 @@ for line in sys.stdin:
);
writer.write_all(reset.as_bytes()).await.unwrap();
} else {
tokio::time::advance(crate::runlet_progress::transport::LEASE).await;
tokio::time::advance(crate::diagnostic_transport::LEASE).await;
}
let reset = match rx.recv().await.unwrap() {
ForwardedStderr::RuntimeLine(line) => crate::events::parse(&line).unwrap(),
Expand Down
14 changes: 2 additions & 12 deletions src/runlet_progress/transport.rs → src/diagnostic_transport.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
//! One process-owned blocking writer, never joined by an execution future.
//! Publication is bounded and nonblocking, including cancellation/Drop paths.
use super::Progress;
use crate::events::{EVENT_MARKER, RuntimeEvent};
use std::{
io::{self, Write},
Expand Down Expand Up @@ -41,7 +40,7 @@ impl Transport {
let diagnostics_lost = Arc::new(AtomicBool::new(false));
let worker_loss = diagnostics_lost.clone();
std::thread::Builder::new()
.name("runlet-diagnostics".into())
.name("kit-diagnostics".into())
.spawn(move || {
// Guard invalidates publication on success, error, or unwind. No IO
// in its destructor and no restart that could reuse stale evidence.
Expand All @@ -54,10 +53,6 @@ impl Transport {
diagnostics_lost,
})
}
pub(crate) fn publish(&self, progress: Progress) {
self.publish_event(&RuntimeEvent::RunletProgress { progress });
}

/// Best-effort diagnostics remain available after authoritative loss.
pub(crate) fn publish_line(&self, line: &str) {
const TRUNCATED: &str = " [truncated]";
Expand Down Expand Up @@ -150,7 +145,7 @@ fn write_loop(
Frame::Authoritative(bytes) | Frame::Diagnostic(bytes) => bytes,
};
writer.write_all(&bytes)?;
// Busy legacy diagnostic traffic must not starve the lease.
// Busy diagnostic traffic must not starve the lease.
if !disabled.load(Ordering::Acquire) && heartbeat.elapsed() >= HEARTBEAT {
transport_status(&mut writer, runtime_events, true)?;
heartbeat = Instant::now();
Expand Down Expand Up @@ -183,11 +178,6 @@ fn write_frame(writer: &mut impl Write, event: &RuntimeEvent) -> io::Result<()>
}

fn encode_frame(event: &RuntimeEvent) -> io::Result<Vec<u8>> {
if let RuntimeEvent::RunletProgress { progress } = event
&& !progress.bounded()
{
return Err(io::Error::other("invalid progress metadata"));
}
// A bounded writer, not an unbounded serialization followed by a size check.
let mut frame = vec![0; MAX_FRAME_BYTES];
let len = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,8 @@
clippy::disallowed_macros
)]
use super::*;
use crate::runlet_progress::Change;
use std::io::{BufRead, BufReader, Read};

fn progress() -> Progress {
Progress {
owner: "parent".into(),
incarnation: 1,
sequence: 0,
change: Change::Started {
digest: "a".repeat(64),
healed: false,
},
}
}

#[cfg(unix)]
#[test]
fn stalled_writer_loss_resets_after_drain_and_never_resumes() {
Expand All @@ -40,14 +27,15 @@ fn stalled_writer_loss_resets_after_drain_and_never_resumes() {
writer.set_nonblocking(false).unwrap();
let transport = Transport::start(writer, 2, true).unwrap();
for _ in 0..4 {
transport.publish_event(&RuntimeEvent::ChildFinished {
call: "parent:compose:0".into(),
tool: "shell".into(),
transport.publish_event(&RuntimeEvent::CompactionFinished {
reason: "test".into(),
ok: true,
summary: "done".into(),
compacted: true,
millis: 1,
});
transport.publish(progress());
transport.publish_event(&RuntimeEvent::SubagentDescendantsRemoved {
ancestor_id: "parent".into(),
});
}
assert!(transport.disabled.load(Ordering::Acquire));
reader
Expand All @@ -68,7 +56,9 @@ fn stalled_writer_loss_resets_after_drain_and_never_resumes() {
crate::events::parse(line.trim_end()),
Some(RuntimeEvent::RunletTransport { available: false })
);
transport.publish(progress());
transport.publish_event(&RuntimeEvent::SubagentDescendantsRemoved {
ancestor_id: "parent".into(),
});
drop(transport);
line.clear();
assert_eq!(reader.read_line(&mut line).unwrap(), 0);
Expand Down Expand Up @@ -114,7 +104,9 @@ fn writer_error_and_unwind_fail_closed() {
diagnostics_lost: Arc::new(AtomicBool::new(false)),
};
assert!(transport.disabled.load(Ordering::Acquire));
transport.publish(progress());
transport.publish_event(&RuntimeEvent::SubagentDescendantsRemoved {
ancestor_id: "parent".into(),
});
}
}

Expand All @@ -135,12 +127,11 @@ fn last_sender_disconnect_finishes_transport() {

#[cfg(unix)]
#[test]
fn lifecycle_frames_share_queue_and_oversize_loss_invalidates_progress() {
let event = RuntimeEvent::ChildFinished {
call: "owner:compose:0".into(),
tool: "shell".into(),
fn lifecycle_frames_share_queue_and_oversize_loss_invalidates_lifecycle() {
let event = RuntimeEvent::CompactionFinished {
reason: "test".into(),
ok: true,
summary: "done".into(),
compacted: true,
millis: 1,
};
let (writer, mut reader) = std::os::unix::net::UnixStream::pair().unwrap();
Expand All @@ -166,7 +157,9 @@ fn lifecycle_frames_share_queue_and_oversize_loss_invalidates_progress() {
session_id: "x".repeat(MAX_FRAME_BYTES),
});
assert!(transport.disabled.load(Ordering::Acquire));
transport.publish(progress());
transport.publish_event(&RuntimeEvent::SubagentDescendantsRemoved {
ancestor_id: "parent".into(),
});
transport.publish_line("later child error");
drop(transport);
wire.clear();
Expand Down
Loading
Loading