bamboo_tools/tools/
write_stdin.rs1use async_trait::async_trait;
2use bamboo_agent_core::{Tool, ToolCtx, ToolError, ToolOutcome};
3use serde::Deserialize;
4use serde_json::json;
5
6use super::{
7 bash_runtime,
8 exec_command::{owner_session, poll_result, yield_duration},
9};
10
11const MAX_INPUT_BYTES: usize = 64 * 1024;
12
13#[derive(Deserialize)]
14#[serde(deny_unknown_fields)]
15struct WriteStdinArgs {
16 session_id: String,
17 #[serde(default)]
18 chars: String,
19 yield_time_ms: Option<u64>,
20 max_output_tokens: Option<usize>,
21 #[serde(default)]
22 eof: bool,
23}
24
25#[derive(Default)]
26pub struct WriteStdinTool;
27
28impl WriteStdinTool {
29 pub fn new() -> Self {
30 Self
31 }
32}
33
34#[async_trait]
35impl Tool for WriteStdinTool {
36 fn name(&self) -> &str {
37 "write_stdin"
38 }
39
40 fn description(&self) -> &str {
41 "Continue an exec_command session owned by this conversation. Empty chars waits/polls for incremental output and final exit status. Running results include polling elapsed/idle times, empty-readout count and exit_observed (false does not prove liveness). Follow a polling advisory by checking progress or doing useful work before polling again; silence does not prove completion. Nonempty chars writes exact bytes to an interactive stdin pipe; use eof=true to close it. A lone Ctrl-C (\\u0003) stops the process. Finished results omit session_id; nonzero exit codes are command outcomes."
42 }
43
44 fn parameters_schema(&self) -> serde_json::Value {
45 json!({"type":"object","properties":{
46 "session_id":{"type":"string","description":"Running handle returned by exec_command"},
47 "chars":{"type":"string","description":"Exact stdin bytes; empty or omitted waits/polls. A lone Ctrl-C stops the process"},
48 "yield_time_ms":{"type":"integer","description":"Wait before yielding again; default 10000, clamped to 250..30000 ms"},
49 "max_output_tokens":{"type":"integer","description":"Output string budget, default 2000, capped at 8192; four JSON-encoded bytes per token including quotes and escapes. Status/session/exit metadata is additional"},
50 "eof":{"type":"boolean","description":"Close the stdin pipe after sending chars"}
51 },"required":["session_id"],"additionalProperties":false})
52 }
53
54 async fn invoke(
55 &self,
56 args: serde_json::Value,
57 ctx: ToolCtx,
58 ) -> Result<ToolOutcome, ToolError> {
59 let parsed: WriteStdinArgs = serde_json::from_value(args).map_err(|error| {
60 ToolError::InvalidArguments(format!("Invalid write_stdin args: {error}"))
61 })?;
62 let owner = owner_session(&ctx)?;
63 let shell = bash_runtime::get_shell(&parsed.session_id)
64 .filter(|shell| shell.polled.is_some() && shell.session_id.as_deref() == Some(owner))
65 .ok_or_else(|| {
66 ToolError::Execution("Exec session not found or not owned by this session (unread completed results expire after five minutes; handles do not survive Host restart)".into())
67 })?;
68 if parsed.chars.len() > MAX_INPUT_BYTES {
69 return Err(ToolError::InvalidArguments(
70 "chars exceeds 65536 bytes".into(),
71 ));
72 }
73 let state = shell.polled.as_ref().expect("validated exec session");
74 let _interaction = state.interaction.lock().await;
77 let mut wait_ms = yield_duration(parsed.yield_time_ms);
78 if parsed.chars == "\u{3}" {
79 shell.kill().await.map_err(ToolError::Execution)?;
80 } else if !parsed.chars.is_empty() {
81 if let Err(error) = shell.write_stdin(&parsed.chars, false).await {
82 state.wait(wait_ms).await;
86 if !state.is_complete().await && !state.process_exited() {
87 return Err(ToolError::Execution(error));
88 }
89 wait_ms = 0;
90 } else {
91 state.reset_empty_readouts().await;
92 }
93 }
94 if parsed.eof {
95 shell.close_stdin().await;
96 state.reset_empty_readouts().await;
97 }
98 Ok(poll_result(&shell, wait_ms, parsed.max_output_tokens).await)
99 }
100}