Files
tai/taicpp/docs/wiring/shell.md
T
retoorandClaude Sonnet 5.5 ed2bfade80 taicpp: port shell, sandbox, pty, glow, markdown, diff and speech helpers from tai.py
Differentially verified against tai.py (4890 cases incl. real pty runs).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 05:02:18 +01:00

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::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<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.