Skip to content

Commit 15f9458

Browse files
feat: omc_spawn + omc_pipe process execution builtins
Add omc_spawn(cmd, args?, env_vars?, timeout_ms?) and omc_pipe(commands) to omnimcode-core. Both return {stdout, stderr, exit_code, ok} dicts and are wired into the interpreter dispatch, is_known_builtin guard, and ALL_BUILTINS list. Enables the recursive self-improvement loop: OMC can spawn omc itself as a subprocess. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 2f9b217 commit 15f9458

7 files changed

Lines changed: 843 additions & 0 deletions

File tree

examples/test_batch_llm.omc

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
h prompts = [
2+
"What is 2+2?",
3+
"What is the capital of France?",
4+
"Name one programming language."
5+
]
6+
7+
print("Running batch LLM call (3 prompts)...")
8+
h results = batch_llm_call(prompts, null, 2)
9+
print(str_concat("Got ", to_str(arr_len(results)), " results"))
10+
11+
h i = 0
12+
while i < arr_len(results) {
13+
print(str_concat(" [", to_str(i), "] ", str_slice(results[i], 0, 50)))
14+
i = i + 1
15+
}
16+
17+
# With per-prompt system messages
18+
h prompts_with_sys = [
19+
{prompt: "What is 2+2?", system: "Answer in one word."},
20+
{prompt: "Capital of France?", system: "Answer in one word."}
21+
]
22+
h results2 = batch_llm_call(prompts_with_sys, null, 2)
23+
print(results2)

omnimcode-core/src/docs.rs

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1313,6 +1313,90 @@ pub const BUILTINS: &[BuiltinDoc] = &[
13131313
example: r#"let models = llm_models(); println(models[0]["id"])"#,
13141314
unique_to_omc: true,
13151315
},
1316+
BuiltinDoc {
1317+
name: "batch_llm_call", category: "llm_workflow",
1318+
signature: "(prompts: (string|dict)[], model?: string, concurrency?: int) -> string[]",
1319+
description: concat!(
1320+
"Send multiple prompts to the LLM sequentially, return all replies in order. ",
1321+
"`prompts` is an array of strings or dicts {prompt: string, system?: string, model?: string}. ",
1322+
"`model` sets a default model for all calls; per-prompt dict `model` takes precedence. ",
1323+
"`concurrency` is accepted for API compatibility but calls are currently sequential ",
1324+
"with a 200 ms inter-call sleep to respect rate limits. ",
1325+
"Uses the same provider/key detection as llm_call."
1326+
),
1327+
example: r#"batch_llm_call(["2+2?", "Capital of France?"], "claude-3-5-haiku-20241022")"#,
1328+
unique_to_omc: true,
1329+
},
1330+
BuiltinDoc {
1331+
name: "batch_llm_chat", category: "llm_workflow",
1332+
signature: "(messages_array: dict[][], model?: string, concurrency?: int) -> string[]",
1333+
description: concat!(
1334+
"Send multiple chat conversations to the LLM sequentially, return all replies in order. ",
1335+
"`messages_array` is an array of arrays, where each inner array is the messages ",
1336+
"(dicts with `role` and `content`) for one chat call. ",
1337+
"Same provider/key detection and sequential execution as batch_llm_call."
1338+
),
1339+
example: r#"batch_llm_chat([[{"role":"user","content":"hi"}], [{"role":"user","content":"bye"}]])"#,
1340+
unique_to_omc: true,
1341+
},
1342+
// ── HTTP builtins ─────────────────────────────────────────────────────────
1343+
BuiltinDoc {
1344+
name: "http_get", category: "http",
1345+
signature: "(url: string, headers?: dict) -> dict",
1346+
description: concat!(
1347+
"Perform a synchronous HTTP GET request. ",
1348+
"`headers` is an optional dict of {header_name: header_value}; pass null to omit. ",
1349+
"Returns a dict with keys: `status` (int), `body` (string), `ok` (bool). ",
1350+
"`ok` is true when 200 <= status < 300. Requires the native-llm Cargo feature (default on)."
1351+
),
1352+
example: r#"let r = http_get("https://httpbin.org/get", null); print(r["status"])"#,
1353+
unique_to_omc: true,
1354+
},
1355+
BuiltinDoc {
1356+
name: "http_post", category: "http",
1357+
signature: "(url: string, body: string, headers?: dict) -> dict",
1358+
description: concat!(
1359+
"Perform a synchronous HTTP POST request with a raw string body. ",
1360+
"`body` is sent as-is; set Content-Type via `headers` if needed. ",
1361+
"Returns {status: int, body: string, ok: bool}."
1362+
),
1363+
example: r#"http_post("https://httpbin.org/post", "hello", {"Content-Type": "text/plain"})"#,
1364+
unique_to_omc: true,
1365+
},
1366+
BuiltinDoc {
1367+
name: "http_post_json", category: "http",
1368+
signature: "(url: string, data: dict|array, headers?: dict) -> dict",
1369+
description: concat!(
1370+
"Perform a synchronous HTTP POST with a JSON body. ",
1371+
"`data` (dict or array) is serialised to JSON automatically; ",
1372+
"Content-Type is set to application/json. ",
1373+
"Returns {status: int, body: string, ok: bool, json: any}. ",
1374+
"`json` holds the parsed JSON response body, or null if parsing fails."
1375+
),
1376+
example: r#"let r = http_post_json("https://httpbin.org/post", {key: "value"}, null)"#,
1377+
unique_to_omc: true,
1378+
},
1379+
BuiltinDoc {
1380+
name: "http_put", category: "http",
1381+
signature: "(url: string, body: string, headers?: dict) -> dict",
1382+
description: concat!(
1383+
"Perform a synchronous HTTP PUT request with a raw string body. ",
1384+
"Same signature as http_post but uses the PUT method. ",
1385+
"Returns {status: int, body: string, ok: bool}."
1386+
),
1387+
example: r#"http_put("https://httpbin.org/put", "{\"x\":1}", {"Content-Type":"application/json"})"#,
1388+
unique_to_omc: true,
1389+
},
1390+
BuiltinDoc {
1391+
name: "http_delete", category: "http",
1392+
signature: "(url: string, headers?: dict) -> dict",
1393+
description: concat!(
1394+
"Perform a synchronous HTTP DELETE request. ",
1395+
"Returns {status: int, body: string, ok: bool}."
1396+
),
1397+
example: r#"http_delete("https://httpbin.org/delete", null)"#,
1398+
unique_to_omc: true,
1399+
},
13161400
BuiltinDoc {
13171401
name: "omc_token_vocab_dump", category: "tokenizer",
13181402
signature: "(n?: int) -> string",
@@ -1952,6 +2036,21 @@ pub const BUILTINS: &[BuiltinDoc] = &[
19522036
example: "h add_one = py_callback(\"add_one\"); py_call(df, \"apply\", [add_one]);",
19532037
unique_to_omc: true,
19542038
},
2039+
// ---- Process execution builtins ----
2040+
BuiltinDoc {
2041+
name: "omc_spawn", category: "process",
2042+
signature: "(cmd: string, args?: string[], env_vars?: dict, timeout_ms?: int) -> dict",
2043+
description: "Spawn a subprocess and wait for it to complete. Returns {stdout, stderr, exit_code, ok}. Critical for the recursive self-improvement loop: OMC can run omc itself as a subprocess.",
2044+
example: "h r = omc_spawn(\"echo\", [\"hello\"], null, 5000); print(r[\"stdout\"]);",
2045+
unique_to_omc: true,
2046+
},
2047+
BuiltinDoc {
2048+
name: "omc_pipe", category: "process",
2049+
signature: "(commands: array[][]) -> dict",
2050+
description: "Pipe multiple commands together like a shell pipe (cmd1 | cmd2 | cmd3). Each element is an array whose first item is the program and remaining items are arguments. Returns {stdout, stderr, exit_code, ok} from the final stage.",
2051+
example: "h r = omc_pipe([[\"echo\", \"hello world\"], [\"tr\", \"a-z\", \"A-Z\"]]); print(r[\"stdout\"]);",
2052+
unique_to_omc: true,
2053+
},
19552054
];
19562055

19572056
/// Look up a builtin by name. Returns None when there's no docs entry
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
//! Native HTTP builtins: `http_get`, `http_post`, `http_post_json`, `http_put`, `http_delete`.
2+
//!
3+
//! These builtins give OMC programs direct access to HTTP without needing LLM
4+
//! credentials. They are built on the same `ureq` crate already used by
5+
//! `llm_builtins` and are gated behind the same `native-llm` Cargo feature
6+
//! (which controls `dep:ureq`).
7+
//!
8+
//! ## Return value
9+
//!
10+
//! Every function returns a dict with at least:
11+
//! - `status` — HTTP status code as int
12+
//! - `body` — response body as string
13+
//! - `ok` — bool, true when 200 <= status < 300
14+
//!
15+
//! `http_post_json` additionally includes:
16+
//! - `json` — parsed JSON body as OMC value, or null on parse failure
17+
18+
use crate::value::Value;
19+
use std::collections::BTreeMap;
20+
21+
// ---------------------------------------------------------------------------
22+
// Public entry points (called from interpreter.rs dispatch)
23+
// ---------------------------------------------------------------------------
24+
25+
/// `http_get(url: string, headers?: dict) -> dict`
26+
#[cfg(feature = "native-llm")]
27+
pub fn http_get(args: &[Value]) -> Result<Value, String> {
28+
if args.is_empty() {
29+
return Err("http_get requires (url: string, headers?: dict)".to_string());
30+
}
31+
let url = args[0].to_display_string();
32+
let headers = extract_headers(args.get(1))?;
33+
34+
let mut req = ureq::get(&url);
35+
for (k, v) in &headers {
36+
req = req.set(k, v);
37+
}
38+
39+
let (status, body) = send_request(req)?;
40+
Ok(make_response_dict(status, body))
41+
}
42+
43+
/// `http_post(url: string, body: string, headers?: dict) -> dict`
44+
#[cfg(feature = "native-llm")]
45+
pub fn http_post(args: &[Value]) -> Result<Value, String> {
46+
if args.len() < 2 {
47+
return Err("http_post requires (url: string, body: string, headers?: dict)".to_string());
48+
}
49+
let url = args[0].to_display_string();
50+
let body_str = args[1].to_display_string();
51+
let headers = extract_headers(args.get(2))?;
52+
53+
let mut req = ureq::post(&url);
54+
for (k, v) in &headers {
55+
req = req.set(k, v);
56+
}
57+
58+
let resp = req
59+
.send_string(&body_str)
60+
.map_err(|e| format!("http_post failed: {e}"))?;
61+
let status = resp.status();
62+
let body = resp
63+
.into_string()
64+
.map_err(|e| format!("http_post: read body failed: {e}"))?;
65+
Ok(make_response_dict(status, body))
66+
}
67+
68+
/// `http_post_json(url: string, data: dict|array, headers?: dict) -> dict`
69+
///
70+
/// Serialises `data` to JSON, sends with `Content-Type: application/json`,
71+
/// and additionally attempts to parse the response body as JSON, returning it
72+
/// under the `json` key (null on parse failure).
73+
#[cfg(feature = "native-llm")]
74+
pub fn http_post_json(args: &[Value]) -> Result<Value, String> {
75+
if args.len() < 2 {
76+
return Err(
77+
"http_post_json requires (url: string, data: dict|array, headers?: dict)".to_string(),
78+
);
79+
}
80+
let url = args[0].to_display_string();
81+
let json_body = crate::interpreter::value_to_json(&args[1]);
82+
let json_str = serde_json::to_string(&json_body)
83+
.map_err(|e| format!("http_post_json: JSON serialisation failed: {e}"))?;
84+
let headers = extract_headers(args.get(2))?;
85+
86+
let mut req = ureq::post(&url).set("Content-Type", "application/json");
87+
for (k, v) in &headers {
88+
req = req.set(k, v);
89+
}
90+
91+
let resp = req
92+
.send_string(&json_str)
93+
.map_err(|e| format!("http_post_json failed: {e}"))?;
94+
let status = resp.status();
95+
let body = resp
96+
.into_string()
97+
.map_err(|e| format!("http_post_json: read body failed: {e}"))?;
98+
99+
Ok(make_json_response_dict(status, body))
100+
}
101+
102+
/// `http_put(url: string, body: string, headers?: dict) -> dict`
103+
#[cfg(feature = "native-llm")]
104+
pub fn http_put(args: &[Value]) -> Result<Value, String> {
105+
if args.len() < 2 {
106+
return Err("http_put requires (url: string, body: string, headers?: dict)".to_string());
107+
}
108+
let url = args[0].to_display_string();
109+
let body_str = args[1].to_display_string();
110+
let headers = extract_headers(args.get(2))?;
111+
112+
let mut req = ureq::put(&url);
113+
for (k, v) in &headers {
114+
req = req.set(k, v);
115+
}
116+
117+
let resp = req
118+
.send_string(&body_str)
119+
.map_err(|e| format!("http_put failed: {e}"))?;
120+
let status = resp.status();
121+
let body = resp
122+
.into_string()
123+
.map_err(|e| format!("http_put: read body failed: {e}"))?;
124+
Ok(make_response_dict(status, body))
125+
}
126+
127+
/// `http_delete(url: string, headers?: dict) -> dict`
128+
#[cfg(feature = "native-llm")]
129+
pub fn http_delete(args: &[Value]) -> Result<Value, String> {
130+
if args.is_empty() {
131+
return Err("http_delete requires (url: string, headers?: dict)".to_string());
132+
}
133+
let url = args[0].to_display_string();
134+
let headers = extract_headers(args.get(1))?;
135+
136+
let mut req = ureq::delete(&url);
137+
for (k, v) in &headers {
138+
req = req.set(k, v);
139+
}
140+
141+
let (status, body) = send_request(req)?;
142+
Ok(make_response_dict(status, body))
143+
}
144+
145+
// ---------------------------------------------------------------------------
146+
// Stubs for non-native builds
147+
// ---------------------------------------------------------------------------
148+
149+
#[cfg(not(feature = "native-llm"))]
150+
pub fn http_get(_args: &[Value]) -> Result<Value, String> {
151+
Err("http_get: recompile with --features native-llm".to_string())
152+
}
153+
154+
#[cfg(not(feature = "native-llm"))]
155+
pub fn http_post(_args: &[Value]) -> Result<Value, String> {
156+
Err("http_post: recompile with --features native-llm".to_string())
157+
}
158+
159+
#[cfg(not(feature = "native-llm"))]
160+
pub fn http_post_json(_args: &[Value]) -> Result<Value, String> {
161+
Err("http_post_json: recompile with --features native-llm".to_string())
162+
}
163+
164+
#[cfg(not(feature = "native-llm"))]
165+
pub fn http_put(_args: &[Value]) -> Result<Value, String> {
166+
Err("http_put: recompile with --features native-llm".to_string())
167+
}
168+
169+
#[cfg(not(feature = "native-llm"))]
170+
pub fn http_delete(_args: &[Value]) -> Result<Value, String> {
171+
Err("http_delete: recompile with --features native-llm".to_string())
172+
}
173+
174+
// ---------------------------------------------------------------------------
175+
// Helpers
176+
// ---------------------------------------------------------------------------
177+
178+
/// Extract optional header dict (Value::Dict) into a Vec<(String, String)>.
179+
/// Accepts null / missing arg gracefully.
180+
fn extract_headers(v: Option<&Value>) -> Result<Vec<(String, String)>, String> {
181+
match v {
182+
None | Some(Value::Null) => Ok(vec![]),
183+
Some(Value::Dict(d)) => {
184+
let map = d.borrow();
185+
let mut out = Vec::with_capacity(map.len());
186+
for (k, val) in map.iter() {
187+
out.push((k.clone(), val.to_display_string()));
188+
}
189+
Ok(out)
190+
}
191+
Some(other) => Err(format!(
192+
"http headers must be a dict or null, got {}",
193+
other.to_display_string()
194+
)),
195+
}
196+
}
197+
198+
/// Fire a GET/DELETE-style request and return (status, body).
199+
#[cfg(feature = "native-llm")]
200+
fn send_request(req: ureq::Request) -> Result<(u16, String), String> {
201+
let resp = req.call().map_err(|e| format!("HTTP request failed: {e}"))?;
202+
let status = resp.status();
203+
let body = resp
204+
.into_string()
205+
.map_err(|e| format!("read body failed: {e}"))?;
206+
Ok((status, body))
207+
}
208+
209+
/// Build the standard {status, body, ok} response dict.
210+
fn make_response_dict(status: u16, body: String) -> Value {
211+
let mut map = BTreeMap::new();
212+
map.insert("status".to_string(), Value::HInt(crate::value::HInt::new(status as i64)));
213+
map.insert("body".to_string(), Value::String(body));
214+
map.insert("ok".to_string(), Value::Bool(status >= 200 && status < 300));
215+
Value::dict_from(map)
216+
}
217+
218+
/// Build the {status, body, ok, json} response dict used by http_post_json.
219+
fn make_json_response_dict(status: u16, body: String) -> Value {
220+
let parsed_json = serde_json::from_str::<serde_json::Value>(&body)
221+
.ok()
222+
.map(crate::interpreter::json_to_value)
223+
.unwrap_or(Value::Null);
224+
225+
let mut map = BTreeMap::new();
226+
map.insert("status".to_string(), Value::HInt(crate::value::HInt::new(status as i64)));
227+
map.insert("body".to_string(), Value::String(body));
228+
map.insert("ok".to_string(), Value::Bool(status >= 200 && status < 300));
229+
map.insert("json".to_string(), parsed_json);
230+
Value::dict_from(map)
231+
}

0 commit comments

Comments
 (0)