Differentially verified against tai.py (4890 cases incl. real pty runs). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
6.1 KiB
wiring: shell and util, for the agent core and other callers
What other modules call. Python reference: Tools.run_shell, Tools.run_shell_box, Tools.shell_stream,
Tools.shell_ask in ../tai.py. Link flags: -pthread -lcurl (edge speech uses libcurl connect-only). Include
dirs: every src/* dir is already on the include path.
1. Running a command for the user (run_shell)
#include "runlive.h" // RunLive, Command, RunLiveOptions, RunResult, kShellTimeout
#include "shellio.h" // ShellApp, ShellIo
#include "livestream.h" // StreamStatus
#include "sandbox.h" // CurlHint
using namespace detz::shell;
std::string RunShell(ShellApp& app, bool live, const std::string& command, const std::string& workdir,
const EnvMap& env /* process env + secrets */) {
std::unique_ptr<StreamSink> stream = ShellIo::Stream(app, command, live); // Tools.shell_stream
RunLiveOptions o;
o.cwd = workdir; o.env = &env;
o.stream = stream.get();
o.ask = ShellIo::Ask(app, command); // Tools.shell_ask (empty = nobody can answer)
RunResult r = RunLive::Run(Command::Shell(command), o); // run_live(command, shell=True, ...)
if (stream) stream->Close(StreamStatus(r.code, r.output, r.elapsed, r.timedOut));
if (r.timedOut) return "error: timed out after " + std::to_string(kShellTimeout) + " seconds";
// "exit %d\n" + spill_or_truncate(store, "output", "shell: " + command[:80], output.strip() or "(no output)", ["shell"], profile) + curl_hint(command)
return "exit " + std::to_string(r.code) + "\n" + /* spill_or_truncate(...) */ "" + CurlHint(command);
}
RunLive::Runthrowsdetz::shell::OsError(what() is Python'sstr(OSError)) when the program cannot start;run_shell_boxcatches it as"error: " + ShortError(...).OsErrorandTimeoutExpiredderive fromProcessError, the analog ofexcept (OSError, subprocess.SubprocessError).- Box variant (
run_shell_box):Command::Argv({engine, "exec", ...extra, ask ? "-it" : "-i", Sandbox::kName, "sh", "-c", command})withextra = {"--workdir", workdir, "-e", "NAME=value", ...}; same stream/ask handling. ShellAppis the adapter forself.app: implementAuto(),Persist(),Depth(),HasPrompter()/Prompt(),MarkUserSteppedIn(),HasStreamFactory()/StreamFactory()(the chat TUI returns itsTuiLiveStream, which must implementStreamSink:Feed(line), optionalPause(),Close(status)), andRedact(command)(store.redact(command, profile), identity without a store).StreamSinkis the Pythonfeed/pause/closeprotocol.LiveStreamis the REPL implementation.- Never run an agent command any other way (Command Output Rule). Output is never shown twice: the stream is the
user's view,
RunResult.outputis the agent's.
2. Snapshot targets before a destructive command
ShellTargets::Paths(command, workdir) returns the existing files a command is about to change (25 max, absolute,
de-duplicated); feed them to ReadCapped(path) (AUDIT_MAX_CHARS) then audit_event / upsert_file_record.
3. Types other modules supply
| need | type | who provides |
|---|---|---|
agent records for lanes / Glow::AgentRowText |
shell::AgentRow{id,status,task,elapsed,tools?,collected} |
agent core maps list_agents() rows |
LaneBoard rows |
shell::AgentLister = std::function<std::vector<AgentRow>(const std::string& profile)> |
agent core (list_agents) |
| shell session persistence | shell::SessionDb (Execute, Query, Commit; throw SessionDbError on sqlite errors) |
store adapts its sqlite handle; table shell_sessions as in settings/store spec |
| errors from box/edge helpers | shell::BackendError(message, status) |
catch and rethrow as llm::BackendError where the Python raises BackendError |
| opencode in the box | Sandbox::BoxOpencodeExec(engine, model, prompt, timeout, agent) returns the process result; run opencode_parse_events(out, rc, err) on it |
llm module |
4. Speech
std::string audio = EdgeSpeech::Synthesize(text.substr(0, 8000), voice); // edge_synthesize; throws BackendError, OsError
Audio::Play(path); // play_audio
std::string heard = Audio::Listen(seconds, audio_dir); // listen_audio
std::string mp3 = Sandbox::BoxSpeak(engine, text, voice); // box_speak
std::string text2 = Sandbox::BoxTranscribe(engine, wavBytes); // box_transcribe
5. Replacements for the old placeholder API
| old (removed) | use |
|---|---|
ShellSession sh; sh.exec(cmd, ms) |
RunLive::Run(Command::Shell(cmd), o); ShellSession now means the shell identity (ShellSession::Current(), Register, Report) |
ShellSession::target_paths/segment_targets |
ShellTargets::Paths/Segment |
Sandbox(root, allow).run(cmd) |
Sandbox::BoxExec(engine, argv) / the box variant of run_shell |
PtyProc::spawn/read_all |
PtyProc(argv, shellCommand, cwd, env) + Read/Write or just RunLive::RunPty |
Spinner::tick/done |
Spinner s("label"); s.Start(); ...; s.Stop(); |
LiveStream::push/drain/joined |
LiveStream(height, width, sink, enabled) with Feed/Pause/Close |
LaneBoard::set/render |
LaneBoard(profile, lister) with Start/Stop, Glow::Lanes for the rows |
Glow::highlight, speak_text |
none in tai.py; use PyHighlight::Line, Glow::*, EdgeSpeech |
CappedText(string) returning string |
CappedText(optional<string>) returning Capped{present,text,size} |
detz::shell::curl_hint |
still available (inline alias of CurlHint) |
src/agent/exec.cpp (3 call sites of ShellSession::exec) and tests/test_main.cpp (shell section, updated in this
milestone) were written against the old API; exec.cpp is outside the shell/util area and must be moved to
RunLive by the agent core.
6. Threads
RunLive::Run may be called from any thread (the burst test runs 32 concurrent runs). Spinner, LaneBoard start a
worker thread and join on Stop() / destruction. Sandbox caches (caps stamp, python path, opencode presence) are
mutex-guarded process state, like the Python module globals.