1use crate::{ConnectorDescription, generate_types, normalize_code};
2
3#[derive(Debug, Clone)]
5pub struct ProgramSourceOptions {
6 pub dispatch: String,
8 pub execution_id: String,
10 pub timeout_ms: Option<u64>,
12}
13
14pub fn build_program_source(
16 code: &str,
17 connectors: &[ConnectorDescription],
18 options: &ProgramSourceOptions,
19) -> Result<String, String> {
20 validate_names(connectors)?;
21 let code = normalize_code(code);
22 let bindings = connectors
23 .iter()
24 .map(|connector| {
25 let methods = connector
26 .tools
27 .iter()
28 .map(|tool| {
29 format!(
30 "{}: async (args = {{}}) => __unwrap(await __dispatch({{ kind: 'call', seq: __seq++, connector: {}, method: {}, arguments: __encode(args) }}))",
31 serde_json::to_string(&tool.name).unwrap(),
32 serde_json::to_string(&connector.name).unwrap(),
33 serde_json::to_string(&tool.name).unwrap(),
34 )
35 })
36 .collect::<Vec<_>>()
37 .join(",\n");
38 format!("const {} = {{\n{methods}\n}};", connector.name)
39 })
40 .collect::<Vec<_>>()
41 .join("\n");
42 let type_comment = connectors
43 .iter()
44 .map(generate_types)
45 .collect::<Vec<_>>()
46 .join("\n\n")
47 .replace("*/", "*\\/");
48 let run = if let Some(timeout_ms) = options.timeout_ms {
49 format!(
50 r#"await Promise.race([
51 __program(),
52 new Promise((_, reject) => setTimeout(() => reject(new Error("Code execution timed out after {timeout_ms}ms")), {timeout_ms}))
53 ])"#
54 )
55 } else {
56 "await __program()".to_string()
57 };
58 Ok(format!(
59 r#"/* Model-facing declarations:
60{type_comment}
61*/
62const __logs = [];
63let __seq = 0;
64const console = {{
65 log: (...values) => __logs.push(values.map(String).join(" ")),
66 info: (...values) => __logs.push(values.map(String).join(" ")),
67 warn: (...values) => __logs.push(values.map(String).join(" ")),
68 error: (...values) => __logs.push(values.map(String).join(" "))
69}};
70const __base64Encode = (value) => {{
71 const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
72 let result = "";
73 for (let index = 0; index < value.length; index += 3) {{
74 const first = value[index];
75 const second = value[index + 1];
76 const third = value[index + 2];
77 result += alphabet[first >> 2];
78 result += alphabet[((first & 3) << 4) | ((second ?? 0) >> 4)];
79 result += second === undefined ? "=" : alphabet[((second & 15) << 2) | ((third ?? 0) >> 6)];
80 result += third === undefined ? "=" : alphabet[third & 63];
81 }}
82 return result;
83}};
84const __base64Decode = (value) => {{
85 const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
86 const clean = value.replace(/=+$/, "");
87 const bytes = [];
88 let buffer = 0;
89 let bits = 0;
90 for (const char of clean) {{
91 buffer = (buffer << 6) | alphabet.indexOf(char);
92 bits += 6;
93 if (bits >= 8) {{
94 bits -= 8;
95 bytes.push((buffer >> bits) & 255);
96 }}
97 }}
98 return new Uint8Array(bytes);
99}};
100const __encode = (value) => {{
101 if (value === undefined) return {{ __codemode_type: "undefined" }};
102 if (typeof value === "bigint") return {{ __codemode_type: "bigint", value: value.toString() }};
103 if (value instanceof Uint8Array) return {{ __codemode_type: "binary", value: __base64Encode(value) }};
104 if (Array.isArray(value)) return value.map(__encode);
105 if (value && typeof value === "object") {{
106 return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, __encode(child)]));
107 }}
108 return value;
109}};
110const __decode = (value) => {{
111 if (Array.isArray(value)) return value.map(__decode);
112 if (value && typeof value === "object") {{
113 if (value.__codemode_type === "undefined") return undefined;
114 if (value.__codemode_type === "bigint") return BigInt(value.value);
115 if (value.__codemode_type === "binary") return __base64Decode(value.value);
116 return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, __decode(child)]));
117 }}
118 return value;
119}};
120const __dispatch = {dispatch};
121const __unwrap = (response) => {{
122 if (response.__codemode_control__ === "pause") throw new Error("__CODEMODE_PAUSE__");
123 if (response.__codemode_control__ === "error") throw new Error(response.message);
124 return __decode(response.result);
125}};
126{bindings}
127const __callBuiltin = async (method, args) => __unwrap(await __dispatch({{
128 kind: "call",
129 seq: __seq++,
130 connector: "codemode",
131 method,
132 arguments: __encode(args)
133}}));
134const codemode = {{
135 executionId: {execution_id},
136 search: (query) => __callBuiltin("search", {{ query }}),
137 describe: (target) => __callBuiltin("describe", {{ target }}),
138 step: async (name, fn) => {{
139 const seq = __seq++;
140 const decision = await __dispatch({{ kind: "begin_step", seq, name }});
141 if (decision.kind === "pause") throw new Error("__CODEMODE_PAUSE__");
142 if (decision.kind === "replay") return __decode(decision.result);
143 const result = await fn();
144 await __dispatch({{ kind: "record_step", seq, result: __encode(result) }});
145 return result;
146 }}
147}};
148try {{
149 const __program = ({code});
150 const __result = {run};
151 return {{ result: __encode(__result), error: null, logs: __logs }};
152}} catch (error) {{
153 return {{
154 result: null,
155 error: error instanceof Error ? error.message : String(error),
156 logs: __logs
157 }};
158}}"#,
159 dispatch = options.dispatch,
160 execution_id = options.execution_id,
161 ))
162}
163
164fn validate_names(connectors: &[ConnectorDescription]) -> Result<(), String> {
165 const RESERVED: &[&str] = &[
166 "__incursDispatch",
167 "__dispatch",
168 "__encode",
169 "__decode",
170 "__unwrap",
171 "__seq",
172 "__logs",
173 "__program",
174 "__result",
175 "Promise",
176 "setTimeout",
177 "Error",
178 "console",
179 "codemode",
180 "fetch",
181 ];
182 let mut seen = std::collections::BTreeSet::new();
183 for connector in connectors {
184 if RESERVED.contains(&connector.name.as_str()) {
185 return Err(format!("Connector name \"{}\" is reserved", connector.name));
186 }
187 if !valid_identifier(&connector.name) {
188 return Err(format!(
189 "Connector name \"{}\" is not a valid JavaScript identifier",
190 connector.name
191 ));
192 }
193 if !seen.insert(&connector.name) {
194 return Err(format!("Duplicate connector name \"{}\"", connector.name));
195 }
196 let mut methods = std::collections::BTreeSet::new();
197 for tool in &connector.tools {
198 if !methods.insert(&tool.name) {
199 return Err(format!(
200 "Duplicate tool name \"{}\" in connector \"{}\"",
201 tool.name, connector.name
202 ));
203 }
204 }
205 }
206 Ok(())
207}
208
209fn valid_identifier(value: &str) -> bool {
210 let mut chars = value.chars();
211 chars
212 .next()
213 .is_some_and(|ch| ch == '_' || ch == '$' || ch.is_ascii_alphabetic())
214 && chars.all(|ch| ch == '_' || ch == '$' || ch.is_ascii_alphanumeric())
215}
216
217#[cfg(test)]
218mod tests {
219 use serde_json::json;
220
221 use crate::{ConnectorTool, ToolAnnotations};
222
223 use super::*;
224
225 #[test]
226 fn emits_shared_connector_and_step_harness() {
227 let source = build_program_source(
228 "state.read({ id: 1 })",
229 &[ConnectorDescription {
230 name: "state".to_string(),
231 instructions: None,
232 tools: vec![ConnectorTool {
233 name: "read".to_string(),
234 description: None,
235 input_schema: json!({"type": "object"}),
236 output_schema: None,
237 instructions: None,
238 examples: Vec::new(),
239 annotations: ToolAnnotations::default(),
240 policy: crate::ToolPolicy::default(),
241 }],
242 }],
243 &ProgramSourceOptions {
244 dispatch: "async (payload) => payload".to_string(),
245 execution_id: "\"test\"".to_string(),
246 timeout_ms: None,
247 },
248 )
249 .unwrap();
250
251 assert!(source.contains("const state"));
252 assert!(source.contains("const __dispatch = async (payload) => payload"));
253 assert!(source.contains("step: async (name, fn)"));
254 }
255}