# 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) ```cpp #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 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::Run` throws `detz::shell::OsError` (what() is Python's `str(OSError)`) when the program cannot start; `run_shell_box` catches it as `"error: " + ShortError(...)`. `OsError` and `TimeoutExpired` derive from `ProcessError`, the analog of `except (OSError, subprocess.SubprocessError)`. - Box variant (`run_shell_box`): `Command::Argv({engine, "exec", ...extra, ask ? "-it" : "-i", Sandbox::kName, "sh", "-c", command})` with `extra = {"--workdir", workdir, "-e", "NAME=value", ...}`; same stream/ask handling. - `ShellApp` is the adapter for `self.app`: implement `Auto()`, `Persist()`, `Depth()`, `HasPrompter()`/`Prompt()`, `MarkUserSteppedIn()`, `HasStreamFactory()`/`StreamFactory()` (the chat TUI returns its `TuiLiveStream`, which must implement `StreamSink`: `Feed(line)`, optional `Pause()`, `Close(status)`), and `Redact(command)` (`store.redact(command, profile)`, identity without a store). - `StreamSink` is the Python `feed/pause/close` protocol. `LiveStream` is 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.output` is 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(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 ```cpp 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)` 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.