pub struct PersistentSubprocessController {
tab_id: String,
block_id: String,
inner: Arc<Mutex<PersistentInner>>,
broker: Option<Arc<Broker>>,
event_bus: Option<Arc<EventBus>>,
wstore: Option<Arc<Store>>,
filestore: Option<Arc<FileStore>>,
health_monitor: Arc<HealthMonitor>,
stdout_seq: Arc<AtomicU64>,
}Expand description
PersistentSubprocessController keeps a long-running CLI process alive, sending user messages as NDJSON lines on stdin.
Fields§
§tab_id: String§block_id: String§inner: Arc<Mutex<PersistentInner>>§broker: Option<Arc<Broker>>§event_bus: Option<Arc<EventBus>>§wstore: Option<Arc<Store>>§filestore: Option<Arc<FileStore>>FileStore for write-through persistence of output lines (Phase 1.3).
health_monitor: Arc<HealthMonitor>§stdout_seq: Arc<AtomicU64>Monotonic counter bumped for every stdout line (including control frames).
The AskUserQuestion dead-air fallback snapshots this before sending the
answer and re-checks after a short window; any increment means the CLI
produced output (assistant content OR a follow-up control_request), i.e.
the turn resumed. Counting all frames — not just record_output, which
the reader skips for control frames — avoids a spurious fallback when the
resumed turn’s first activity is a tool-permission round-trip.
Implementations§
Source§impl PersistentSubprocessController
impl PersistentSubprocessController
pub fn new( tab_id: String, block_id: String, broker: Option<Arc<Broker>>, event_bus: Option<Arc<EventBus>>, wstore: Option<Arc<Store>>, filestore: Option<Arc<FileStore>>, ) -> Self
fn set_status(inner: &mut PersistentInner, status: &str)
fn get_status_snapshot(&self) -> BlockControllerRuntimeStatus
fn publish_status(&self)
fn is_running(&self) -> bool
Sourcefn emit_message_accepted(&self, message_id: Option<&str>)
fn emit_message_accepted(&self, message_id: Option<&str>)
Send a user message to the running CLI process.
If the process isn’t spawned yet, spawns it first.
Emit agent-message-accepted for a given message_id, if set.
Mirrors the subprocess controller’s emit_message_accepted — signals the
frontend to promote the pending entry from queued to in-document.
pub fn send_message( &self, message: String, config: PersistentSpawnConfig, ) -> Result<(), String>
Sourcepub fn send_user_message(&self, message: String) -> Result<(), String>
pub fn send_user_message(&self, message: String) -> Result<(), String>
Deliver a user message to the already-running persistent process,
without a spawn config. Unlike send_message, this never spawns — it errors
if the process is not running. Used for controller-aware muxbus/reactive
delivery (deliver_agent_message), where the agent is live (busy or idle)
and we have no PersistentSpawnConfig to hand. Writing on the live stdin lets
the message land mid-turn (steering) instead of waiting for idle.
Spec: docs/specs/SPEC_AGENT_CONTROL_PROTOCOL_2026_06_15.md §6 (Phase 3).
Sourcepub fn answer_question(
&self,
tool_use_id: String,
answers: Value,
) -> Result<(), String>
pub fn answer_question( &self, tool_use_id: String, answers: Value, ) -> Result<(), String>
Answer a parked AskUserQuestion via the Agent SDK control protocol.
The CLI asked us with a can_use_tool control_request (parked in
pending_questions by the stdout reader); we reply with a
control_response carrying updatedInput.answers. This is the ONLY
mechanism the CLI accepts — delivering a tool_result on stdin does NOT
work (the CLI auto-rejects AskUserQuestion within the turn). answers is
the JSON object mapping each question’s text to the selected label(s) or
free-text. Process must already be running (agent is mid-turn, blocked on
this answer). Spec: docs/specs/SPEC_AGENT_CONTROL_PROTOCOL_2026_06_15.md §2.3.
Sourcefn push_stdin(inner: &Arc<Mutex<PersistentInner>>, line: String)
fn push_stdin(inner: &Arc<Mutex<PersistentInner>>, line: String)
Push a raw NDJSON line to the live stdin (used to emit control_responses
from the stdout-reader task, which only holds an Arc<Mutex<Inner>>).
Sourcefn handle_control_frame(
kind: &str,
parsed: &Value,
block_id: &str,
inner: &Arc<Mutex<PersistentInner>>,
)
fn handle_control_frame( kind: &str, parsed: &Value, block_id: &str, inner: &Arc<Mutex<PersistentInner>>, )
Handle a control-protocol frame from the CLI’s stdout. control_request
of subtype can_use_tool: AskUserQuestion is parked (the frontend
panel — rendered from the assistant stream — answers it via
answer_question); every other tool is auto-allowed to preserve the
current bypass/yolo UX (Phase 1; Phase 2 routes these to the decision
prompt, #551). control_response frames (replies to requests we initiate,
none today) are logged and dropped. These frames are NOT conversation
output and never reach the blockfile.
Spec: docs/specs/SPEC_AGENT_CONTROL_PROTOCOL_2026_06_15.md §4.2.
Sourcefn spawn_process(&self, config: PersistentSpawnConfig) -> Result<(), String>
fn spawn_process(&self, config: PersistentSpawnConfig) -> Result<(), String>
Spawn the persistent CLI process.
pub fn stop_process(&self, force: bool) -> Result<(), String>
pub fn session_id(&self) -> Option<String>
Trait Implementations§
Source§impl Controller for PersistentSubprocessController
impl Controller for PersistentSubprocessController
Source§fn start(
&self,
_block_meta: HashMap<String, Value>,
_rt_opts: Option<Value>,
_force: bool,
) -> Result<(), String>
fn start( &self, _block_meta: HashMap<String, Value>, _rt_opts: Option<Value>, _force: bool, ) -> Result<(), String>
force restarts even if already running.Source§fn stop(&self, _graceful: bool, new_status: &str) -> Result<(), String>
fn stop(&self, _graceful: bool, new_status: &str) -> Result<(), String>
graceful waits for process to exit; new_status is the target state.Source§fn get_runtime_status(&self) -> BlockControllerRuntimeStatus
fn get_runtime_status(&self) -> BlockControllerRuntimeStatus
Source§fn send_input(
&self,
input: BlockInputUnion,
_seq: Option<u64>,
) -> Result<(), String>
fn send_input( &self, input: BlockInputUnion, _seq: Option<u64>, ) -> Result<(), String>
seq is the per-TermViewModel monotonic counter; None means fire-and-forget (no ordering).Source§fn controller_type(&self) -> &str
fn controller_type(&self) -> &str
Auto Trait Implementations§
impl Freeze for PersistentSubprocessController
impl RefUnwindSafe for PersistentSubprocessController
impl Send for PersistentSubprocessController
impl Sync for PersistentSubprocessController
impl Unpin for PersistentSubprocessController
impl UnwindSafe for PersistentSubprocessController
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>. Box<dyn Any> can
then be further downcast into Box<ConcreteType> where ConcreteType implements Trait.§fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
Rc<Trait> (where Trait: Downcast) to Rc<Any>. Rc<Any> can then be
further downcast into Rc<ConcreteType> where ConcreteType implements Trait.§fn as_any(&self) -> &(dyn Any + 'static)
fn as_any(&self) -> &(dyn Any + 'static)
&Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &Any’s vtable from &Trait’s.§fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
&mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &mut Any’s vtable from &mut Trait’s.§impl<T> DowncastSync for T
impl<T> DowncastSync for T
§impl<T> Instrument for T
impl<T> Instrument for T
§fn instrument(self, span: Span) -> Instrumented<Self>
fn instrument(self, span: Span) -> Instrumented<Self>
§fn in_current_span(self) -> Instrumented<Self>
fn in_current_span(self) -> Instrumented<Self>
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self>
fn into_either(self, into_left: bool) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more