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

94 lines
6.1 KiB
Markdown

# 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<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
```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<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.