agent-message 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,278 @@
1
+ use std::collections::HashMap;
2
+ use std::path::Path;
3
+ use std::process::Stdio;
4
+ use std::sync::Arc;
5
+ use std::sync::atomic::{AtomicU64, Ordering};
6
+
7
+ use anyhow::{Context, Result, anyhow, bail};
8
+ use serde_json::Value;
9
+ use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
10
+ use tokio::process::{Child, ChildStdin, ChildStdout, Command};
11
+ use tokio::sync::{Mutex, mpsc, oneshot};
12
+
13
+ #[derive(Debug)]
14
+ pub(crate) enum IncomingMessage {
15
+ Request {
16
+ method: String,
17
+ id: Value,
18
+ params: Value,
19
+ },
20
+ Notification {
21
+ method: String,
22
+ params: Value,
23
+ },
24
+ }
25
+
26
+ #[derive(Debug, Clone)]
27
+ pub(crate) struct RpcError {
28
+ pub(crate) code: Option<i64>,
29
+ pub(crate) message: String,
30
+ }
31
+
32
+ impl std::fmt::Display for RpcError {
33
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34
+ match self.code {
35
+ Some(code) => write!(f, "rpc error {code}: {}", self.message),
36
+ None => write!(f, "rpc error: {}", self.message),
37
+ }
38
+ }
39
+ }
40
+
41
+ impl std::error::Error for RpcError {}
42
+
43
+ pub(crate) struct CodexAppServer {
44
+ child: Child,
45
+ stdin: Arc<Mutex<ChildStdin>>,
46
+ next_request_id: AtomicU64,
47
+ pending: Arc<Mutex<HashMap<String, oneshot::Sender<Result<Value, RpcError>>>>>,
48
+ events_rx: mpsc::UnboundedReceiver<IncomingMessage>,
49
+ }
50
+
51
+ impl CodexAppServer {
52
+ pub(crate) async fn start(codex_bin: &Path, cwd: &Path) -> Result<Self> {
53
+ let mut child = Command::new(codex_bin)
54
+ .arg("app-server")
55
+ .current_dir(cwd)
56
+ .stdin(Stdio::piped())
57
+ .stdout(Stdio::piped())
58
+ .stderr(Stdio::piped())
59
+ .kill_on_drop(true)
60
+ .spawn()
61
+ .with_context(|| format!("spawn `{}`", codex_bin.display()))?;
62
+
63
+ let stdin = child.stdin.take().context("capture codex stdin")?;
64
+ let stdout = child.stdout.take().context("capture codex stdout")?;
65
+ let stderr = child.stderr.take().context("capture codex stderr")?;
66
+
67
+ let pending = Arc::new(Mutex::new(HashMap::new()));
68
+ let (events_tx, events_rx) = mpsc::unbounded_channel();
69
+
70
+ spawn_stdout_pump(stdout, Arc::clone(&pending), events_tx);
71
+ spawn_stderr_pump(stderr);
72
+
73
+ Ok(Self {
74
+ child,
75
+ stdin: Arc::new(Mutex::new(stdin)),
76
+ next_request_id: AtomicU64::new(1),
77
+ pending,
78
+ events_rx,
79
+ })
80
+ }
81
+
82
+ pub(crate) async fn initialize(&self) -> Result<()> {
83
+ let initialize = serde_json::json!({
84
+ "clientInfo": {
85
+ "name": "codex_message",
86
+ "title": "Codex Message",
87
+ "version": env!("CARGO_PKG_VERSION"),
88
+ },
89
+ "capabilities": {
90
+ "experimentalApi": true,
91
+ }
92
+ });
93
+ self.request("initialize", initialize).await?;
94
+ self.notify("initialized", serde_json::json!({})).await
95
+ }
96
+
97
+ pub(crate) async fn request(&self, method: &str, params: Value) -> Result<Value> {
98
+ let id = self.next_request_id.fetch_add(1, Ordering::SeqCst);
99
+ let request_id = Value::from(id);
100
+ let key = id_key(&request_id)?;
101
+ let (tx, rx) = oneshot::channel();
102
+ self.pending.lock().await.insert(key, tx);
103
+ self.write_message(serde_json::json!({
104
+ "id": request_id,
105
+ "method": method,
106
+ "params": params,
107
+ }))
108
+ .await?;
109
+
110
+ match rx.await {
111
+ Ok(Ok(result)) => Ok(result),
112
+ Ok(Err(error)) => Err(error.into()),
113
+ Err(_) => bail!("rpc response channel dropped for method `{method}`"),
114
+ }
115
+ }
116
+
117
+ pub(crate) async fn notify(&self, method: &str, params: Value) -> Result<()> {
118
+ self.write_message(serde_json::json!({
119
+ "method": method,
120
+ "params": params,
121
+ }))
122
+ .await
123
+ }
124
+
125
+ pub(crate) async fn respond(&self, id: Value, result: Value) -> Result<()> {
126
+ self.write_message(serde_json::json!({
127
+ "id": id,
128
+ "result": result,
129
+ }))
130
+ .await
131
+ }
132
+
133
+ pub(crate) async fn next_event(&mut self) -> Result<IncomingMessage> {
134
+ self.events_rx
135
+ .recv()
136
+ .await
137
+ .ok_or_else(|| anyhow!("codex app-server event stream ended"))
138
+ }
139
+
140
+ pub(crate) async fn shutdown(&mut self) -> Result<()> {
141
+ if let Err(error) = self.child.start_kill() {
142
+ eprintln!("[codex] failed to signal shutdown: {error}");
143
+ }
144
+ let _ = self.child.wait().await;
145
+ Ok(())
146
+ }
147
+
148
+ async fn write_message(&self, message: Value) -> Result<()> {
149
+ let encoded = serde_json::to_vec(&message).context("encode rpc message")?;
150
+ let mut stdin = self.stdin.lock().await;
151
+ stdin
152
+ .write_all(&encoded)
153
+ .await
154
+ .context("write rpc payload to codex stdin")?;
155
+ stdin
156
+ .write_all(b"\n")
157
+ .await
158
+ .context("write rpc newline to codex stdin")?;
159
+ stdin.flush().await.context("flush codex stdin")
160
+ }
161
+ }
162
+
163
+ fn spawn_stdout_pump(
164
+ stdout: ChildStdout,
165
+ pending: Arc<Mutex<HashMap<String, oneshot::Sender<Result<Value, RpcError>>>>>,
166
+ events_tx: mpsc::UnboundedSender<IncomingMessage>,
167
+ ) {
168
+ tokio::spawn(async move {
169
+ let mut lines = BufReader::new(stdout).lines();
170
+ loop {
171
+ let next = lines.next_line().await;
172
+ let line = match next {
173
+ Ok(Some(line)) => line,
174
+ Ok(None) => break,
175
+ Err(error) => {
176
+ eprintln!("[codex] failed to read stdout: {error}");
177
+ break;
178
+ }
179
+ };
180
+ let trimmed = line.trim();
181
+ if trimmed.is_empty() {
182
+ continue;
183
+ }
184
+
185
+ let value: Value = match serde_json::from_str(trimmed) {
186
+ Ok(value) => value,
187
+ Err(error) => {
188
+ eprintln!("[codex] invalid JSON from app-server: {error}: {trimmed}");
189
+ continue;
190
+ }
191
+ };
192
+
193
+ if let Some(id) = value.get("id") {
194
+ if value.get("method").is_some() {
195
+ let Some(method) = value.get("method").and_then(Value::as_str) else {
196
+ eprintln!("[codex] request missing method string: {trimmed}");
197
+ continue;
198
+ };
199
+ let params = value.get("params").cloned().unwrap_or(Value::Null);
200
+ if events_tx
201
+ .send(IncomingMessage::Request {
202
+ method: method.to_string(),
203
+ id: id.clone(),
204
+ params,
205
+ })
206
+ .is_err()
207
+ {
208
+ break;
209
+ }
210
+ continue;
211
+ }
212
+
213
+ let key = match id_key(id) {
214
+ Ok(key) => key,
215
+ Err(error) => {
216
+ eprintln!("[codex] invalid response id: {error:#}");
217
+ continue;
218
+ }
219
+ };
220
+ let sender = pending.lock().await.remove(&key);
221
+ let Some(sender) = sender else {
222
+ eprintln!("[codex] dropped response for unknown request id: {trimmed}");
223
+ continue;
224
+ };
225
+
226
+ if let Some(result) = value.get("result") {
227
+ let _ = sender.send(Ok(result.clone()));
228
+ continue;
229
+ }
230
+
231
+ let error = value.get("error").cloned().unwrap_or(Value::Null);
232
+ let rpc_error = RpcError {
233
+ code: error.get("code").and_then(Value::as_i64),
234
+ message: error
235
+ .get("message")
236
+ .and_then(Value::as_str)
237
+ .unwrap_or("unknown rpc error")
238
+ .to_string(),
239
+ };
240
+ let _ = sender.send(Err(rpc_error));
241
+ continue;
242
+ }
243
+
244
+ if let Some(method) = value.get("method").and_then(Value::as_str) {
245
+ let params = value.get("params").cloned().unwrap_or(Value::Null);
246
+ if events_tx
247
+ .send(IncomingMessage::Notification {
248
+ method: method.to_string(),
249
+ params,
250
+ })
251
+ .is_err()
252
+ {
253
+ break;
254
+ }
255
+ }
256
+ }
257
+ });
258
+ }
259
+
260
+ fn spawn_stderr_pump(stderr: tokio::process::ChildStderr) {
261
+ tokio::spawn(async move {
262
+ let mut lines = BufReader::new(stderr).lines();
263
+ loop {
264
+ match lines.next_line().await {
265
+ Ok(Some(line)) => eprintln!("[codex] {line}"),
266
+ Ok(None) => break,
267
+ Err(error) => {
268
+ eprintln!("[codex] failed to read stderr: {error}");
269
+ break;
270
+ }
271
+ }
272
+ }
273
+ });
274
+ }
275
+
276
+ fn id_key(id: &Value) -> Result<String> {
277
+ serde_json::to_string(id).context("serialize request id")
278
+ }
@@ -0,0 +1,101 @@
1
+ mod agent_message;
2
+ mod app;
3
+ mod codex;
4
+ mod render;
5
+
6
+ use std::path::PathBuf;
7
+
8
+ use anyhow::Context;
9
+ use app::App;
10
+ use clap::Parser;
11
+
12
+ #[derive(Debug, Clone, clap::ValueEnum)]
13
+ enum ApprovalPolicyArg {
14
+ Untrusted,
15
+ OnFailure,
16
+ OnRequest,
17
+ Never,
18
+ }
19
+
20
+ impl ApprovalPolicyArg {
21
+ fn as_app_server_value(&self) -> &'static str {
22
+ match self {
23
+ Self::Untrusted => "untrusted",
24
+ Self::OnFailure => "on-failure",
25
+ Self::OnRequest => "on-request",
26
+ Self::Never => "never",
27
+ }
28
+ }
29
+ }
30
+
31
+ #[derive(Debug, Clone, clap::ValueEnum)]
32
+ enum SandboxArg {
33
+ ReadOnly,
34
+ WorkspaceWrite,
35
+ DangerFullAccess,
36
+ }
37
+
38
+ #[derive(Debug, Clone, Parser)]
39
+ #[command(author, version, about)]
40
+ struct Cli {
41
+ #[arg(long = "to", env = "CODEX_MESSAGE_TO", default_value = "jay")]
42
+ to_username: String,
43
+
44
+ #[arg(long, env = "CODEX_MESSAGE_CODEX_BIN", default_value = "codex")]
45
+ codex_bin: PathBuf,
46
+
47
+ #[arg(long, env = "CODEX_MESSAGE_MODEL")]
48
+ model: Option<String>,
49
+
50
+ #[arg(long, env = "CODEX_MESSAGE_CWD")]
51
+ cwd: Option<PathBuf>,
52
+
53
+ #[arg(long, env = "CODEX_MESSAGE_APPROVAL_POLICY")]
54
+ approval_policy: Option<ApprovalPolicyArg>,
55
+
56
+ #[arg(long, env = "CODEX_MESSAGE_SANDBOX", default_value = "workspace-write")]
57
+ sandbox: SandboxArg,
58
+
59
+ #[arg(long, env = "CODEX_MESSAGE_NETWORK_ACCESS", default_value_t = false)]
60
+ network_access: bool,
61
+ }
62
+
63
+ #[derive(Debug, Clone)]
64
+ pub(crate) struct Config {
65
+ pub(crate) to_username: String,
66
+ pub(crate) codex_bin: PathBuf,
67
+ pub(crate) model: Option<String>,
68
+ pub(crate) cwd: PathBuf,
69
+ pub(crate) approval_policy: Option<String>,
70
+ pub(crate) sandbox: SandboxArg,
71
+ pub(crate) network_access: bool,
72
+ }
73
+
74
+ impl TryFrom<Cli> for Config {
75
+ type Error = anyhow::Error;
76
+
77
+ fn try_from(value: Cli) -> Result<Self, Self::Error> {
78
+ let cwd = match value.cwd {
79
+ Some(path) => path,
80
+ None => std::env::current_dir().context("resolve current working directory")?,
81
+ };
82
+
83
+ Ok(Self {
84
+ to_username: value.to_username,
85
+ codex_bin: value.codex_bin,
86
+ model: value.model,
87
+ cwd,
88
+ approval_policy: value
89
+ .approval_policy
90
+ .map(|policy| policy.as_app_server_value().to_string()),
91
+ sandbox: value.sandbox,
92
+ network_access: value.network_access,
93
+ })
94
+ }
95
+ }
96
+
97
+ #[tokio::main]
98
+ async fn main() -> anyhow::Result<()> {
99
+ let config = Config::try_from(Cli::parse())?;
100
+ App::new(config).run().await
101
+ }
@@ -0,0 +1,125 @@
1
+ use serde_json::{Map, Value, json};
2
+
3
+ pub(crate) fn report_spec(badge: &str, title: &str, lines: &[String], body: Option<&str>) -> Value {
4
+ let mut elements = Map::new();
5
+ let mut children = Vec::new();
6
+
7
+ push_badge(&mut elements, &mut children, "badge", badge);
8
+ push_text(&mut elements, &mut children, "title", title);
9
+ push_separator(&mut elements, &mut children, "sep-top");
10
+
11
+ for (index, line) in lines.iter().enumerate() {
12
+ let key = format!("line-{index}");
13
+ push_text(&mut elements, &mut children, &key, line);
14
+ }
15
+
16
+ if let Some(body) = body.and_then(non_empty) {
17
+ if !lines.is_empty() {
18
+ push_separator(&mut elements, &mut children, "sep-body");
19
+ }
20
+ for (index, paragraph) in paragraphs(body).into_iter().enumerate() {
21
+ let key = format!("body-{index}");
22
+ push_text(&mut elements, &mut children, &key, &paragraph);
23
+ }
24
+ }
25
+
26
+ elements.insert(
27
+ "root".to_string(),
28
+ json!({
29
+ "type": "Stack",
30
+ "children": children,
31
+ }),
32
+ );
33
+
34
+ json!({
35
+ "root": "root",
36
+ "elements": elements,
37
+ })
38
+ }
39
+
40
+ pub(crate) fn approval_spec(
41
+ badge: &str,
42
+ title: &str,
43
+ details: &[String],
44
+ reply_hint: &str,
45
+ ) -> Value {
46
+ let mut lines = details.to_vec();
47
+ lines.push(format!("Reply: {reply_hint}"));
48
+ report_spec(badge, title, &lines, None)
49
+ }
50
+
51
+ fn push_badge(
52
+ elements: &mut Map<String, Value>,
53
+ children: &mut Vec<String>,
54
+ key: &str,
55
+ text: &str,
56
+ ) {
57
+ children.push(key.to_string());
58
+ elements.insert(
59
+ key.to_string(),
60
+ json!({
61
+ "type": "Badge",
62
+ "props": { "text": text },
63
+ }),
64
+ );
65
+ }
66
+
67
+ fn push_text(elements: &mut Map<String, Value>, children: &mut Vec<String>, key: &str, text: &str) {
68
+ children.push(key.to_string());
69
+ elements.insert(
70
+ key.to_string(),
71
+ json!({
72
+ "type": "Text",
73
+ "props": { "text": text },
74
+ }),
75
+ );
76
+ }
77
+
78
+ fn push_separator(elements: &mut Map<String, Value>, children: &mut Vec<String>, key: &str) {
79
+ children.push(key.to_string());
80
+ elements.insert(
81
+ key.to_string(),
82
+ json!({
83
+ "type": "Separator",
84
+ }),
85
+ );
86
+ }
87
+
88
+ fn paragraphs(text: &str) -> Vec<String> {
89
+ text.split("\n\n")
90
+ .map(str::trim)
91
+ .filter(|part| !part.is_empty())
92
+ .map(ToOwned::to_owned)
93
+ .collect()
94
+ }
95
+
96
+ fn non_empty(value: &str) -> Option<&str> {
97
+ let trimmed = value.trim();
98
+ if trimmed.is_empty() {
99
+ None
100
+ } else {
101
+ Some(trimmed)
102
+ }
103
+ }
104
+
105
+ #[cfg(test)]
106
+ mod tests {
107
+ use super::*;
108
+
109
+ #[test]
110
+ fn report_spec_has_expected_shape() {
111
+ let spec = report_spec(
112
+ "Completed",
113
+ "Request finished",
114
+ &["Status: ok".to_string()],
115
+ Some("First paragraph\n\nSecond paragraph"),
116
+ );
117
+ assert_eq!(spec["root"], "root");
118
+ assert_eq!(spec["elements"]["badge"]["type"], "Badge");
119
+ assert_eq!(spec["elements"]["title"]["type"], "Text");
120
+ assert_eq!(
121
+ spec["elements"]["body-1"]["props"]["text"],
122
+ "Second paragraph"
123
+ );
124
+ }
125
+ }