1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
use super::*;
pub trait SessionBackend: Send + Sync {
fn name(&self) -> &'static str;
fn capabilities(&self) -> Capabilities;
fn available(&self) -> Result<bool>;
fn spawn(&self, spec: SpawnSpec) -> Result<SessionRef>;
fn attach(&self, session: &SessionRef) -> Result<SessionRef>;
fn probe(&self, session: &SessionRef) -> Result<ResourceProbe>;
fn close(&self, session: &SessionRef, reason: CloseReason, force: bool) -> Result<()>;
fn rename(&self, _session: &SessionRef, _title: &str) -> Result<()> {
Err(unsupported(
self.name(),
"rename",
"backend does not expose rename",
))
}
fn focus(&self, _session: &SessionRef) -> Result<()> {
Err(unsupported(
self.name(),
"focus",
"backend does not expose focus",
))
}
/// Whether this backend carries task payloads itself. A backend that
/// answers true delivers through [`SessionBackend::deliver`]; one that
/// answers false keeps the adapter-socket path where a mounted plugin
/// receives `assign` or `config_get{key:"stdin:<text>"}`.
fn self_driven(&self) -> bool {
false
}
/// Hand one delivery's rendered text to a session this backend owns.
///
/// The text is the delivery and nothing else: a backend that needs words of
/// its own sends them through [`SessionBackend::nudge`].
fn deliver(&self, _session: &SessionRef, _task_id: &str, _prompt: &str) -> Result<()> {
Err(unsupported(
self.name(),
"deliver",
"backend delivers through an adapter socket",
))
}
/// Tell one session its turn ended without a completion, in this client's
/// own words (`docs/v2-CONTRACT.md` §3c).
///
/// A backend that can hand the text over answers `Ok`; one that cannot —
/// because the drive has no injection channel, or its process is gone —
/// answers an error, and the turn-end rule settles the delivery at that
/// ending instead: a drive that cannot be nudged must not be told it was.
/// The text is this client's, verbatim; a backend composes none of it and
/// keeps no copy.
fn nudge(&self, _session: &SessionRef, _task_id: &str, _text: &str) -> Result<()> {
Err(unsupported(
self.name(),
"nudge",
"backend has no channel to hand the sentence to",
))
}
/// Install the client-owned destination for records journalled by this
/// backend. Backends without a journal keep the default no-op.
fn set_content_sink(&self, _sink: Arc<dyn crate::content::ContentSink>) {}
/// Terminal facts this backend observed without an adapter report.
/// Absent for backends whose sessions end through the adapter socket.
fn outcomes(&self) -> Option<OutcomeFeed> {
None
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommandOutput {
pub status: i32,
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
}
pub trait Runner: Send + Sync {
fn run(
&self,
program: &str,
args: &[String],
cwd: Option<&Path>,
env: &BTreeMap<String, String>,
) -> Result<CommandOutput>;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct ProcessRunner;
impl Runner for ProcessRunner {
fn run(
&self,
program: &str,
args: &[String],
cwd: Option<&Path>,
env: &BTreeMap<String, String>,
) -> Result<CommandOutput> {
let mut command = std::process::Command::new(program);
command.args(args);
if let Some(cwd) = cwd {
command.current_dir(cwd);
}
command.envs(env);
let output = command.output()?;
Ok(CommandOutput {
status: output.status.code().unwrap_or(-1),
stdout: output.stdout,
stderr: output.stderr,
})
}
}