New sessions/session_export tools plus a sessions_block vault map in the system prompt (tai.py, ported to detz); recall/search carry a session footer instead of letting the model grep sealed bytes. write_file refuses new markdown at working/repo roots, and the blanket *.md gitignore is gone so docs stay visible. One LineInput owns fd 0 for every prompt (ask, approvals, shell_ask, secrets), ending phantom-empty answers; Ctrl-C re-prompts (double exits), Ctrl-D exits, Ctrl-Z suspends; mid-turn Ctrl-C announces itself and kills foreground shell children via RunInterrupted. Covered by SessionsTests, repl_keys.py pty checks, agent_diff sessions_block cases.
59 lines
2.6 KiB
C++
59 lines
2.6 KiB
C++
#pragma once
|
|
// RunLive: run_live() / run_live_pty() of tai.py. Every command an agent runs for the user
|
|
// streams live into a StreamSink and is returned to the agent. With an ask function the
|
|
// command runs on a pty; when it stops on a prompt (sudo, ssh, y/n) ask(text, secret) gets
|
|
// the question and its answer is typed into the command. nullopt declines: the command gets
|
|
// end-of-file. Time spent waiting for the user does not count against the timeout.
|
|
#include "proc.h"
|
|
#include "sink.h"
|
|
#include <functional>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <vector>
|
|
namespace detz::shell {
|
|
constexpr int kShellTimeout = 120; // SHELL_TIMEOUT
|
|
constexpr long kRunOutputCap = 4000000; // RUN_OUTPUT_CAP
|
|
constexpr double kRunPromptIdle = 1.0; // RUN_PROMPT_IDLE
|
|
constexpr int kRunMaxAsks = 20; // RUN_MAX_ASKS
|
|
constexpr size_t kRunAnswerCap = 1000; // RUN_ANSWER_CAP
|
|
|
|
// ask(text, secret): the user's answer, or nullopt to decline.
|
|
using AskFn = std::function<std::optional<std::string>(const std::string& text, bool secret)>;
|
|
|
|
// What to run: an argv list, or a shell command line (shell=True).
|
|
struct Command {
|
|
std::vector<std::string> argv;
|
|
std::optional<std::string> shell;
|
|
static Command Argv(std::vector<std::string> a) { Command c; c.argv = std::move(a); return c; }
|
|
static Command Shell(std::string line) { Command c; c.shell = std::move(line); return c; }
|
|
};
|
|
|
|
// (returncode, output, elapsed seconds, timed_out)
|
|
struct RunResult { int code{0}; std::string output; double elapsed{0}; bool timedOut{false}; };
|
|
// Ctrl-C landed while the command ran: the child is already killed and reaped.
|
|
struct RunInterrupted : public std::runtime_error {
|
|
RunInterrupted() : std::runtime_error("interrupted") {}
|
|
};
|
|
|
|
struct RunLiveOptions {
|
|
double timeout{kShellTimeout};
|
|
std::optional<std::string> cwd;
|
|
const EnvMap* env{nullptr}; // null: inherit
|
|
StreamSink* stream{nullptr};
|
|
AskFn ask; // empty: no pty, no questions
|
|
bool allow_interrupt{true}; // false: Ctrl-C is not ours (background workers)
|
|
};
|
|
|
|
class RunLive {
|
|
public:
|
|
// run_live(): pty path when ask is set, pipe path otherwise. Throws OsError when the
|
|
// command cannot start (missing program, bad cwd).
|
|
static RunResult Run(const Command& command, const RunLiveOptions& options = RunLiveOptions());
|
|
static RunResult RunPty(const Command& command, const RunLiveOptions& options);
|
|
// RUN_CTRL_RE.sub("", text)
|
|
static std::string StripControl(const std::string& text);
|
|
// RUN_PROMPT_RE.search(text) is not None
|
|
static bool LooksLikePrompt(const std::string& text);
|
|
};
|
|
} // namespace detz::shell
|