| Crates.io | pctx_code_mode |
| lib.rs | pctx_code_mode |
| version | 0.1.1 |
| created_at | 2026-01-12 20:31:15.852675+00 |
| updated_at | 2026-01-21 01:47:35.908805+00 |
| description | TypeScript code execution engine for AI agents with tool schemas, sandboxed Deno runtime, and Rust callbacks |
| homepage | |
| repository | https://github.com/portofcontext/pctx |
| max_upload_size | |
| id | 2038811 |
| size | 197,558 |
A TypeScript code execution engine that enables AI agents to dynamically call tools through generated code. Code Mode converts tool schemas (like MCP tools) into TypeScript interfaces, executes LLM-generated code in a sandboxed Deno runtime, and bridges function calls back to your Rust callbacks.
use pctx_code_mode::{CodeMode, Tool, ToolSet, RootSchema, CallbackRegistry};
use schemars::schema_for;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Serialize, Deserialize, schemars::JsonSchema)]
struct GreetInput {
name: String,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// 1. Define your tools with JSON schemas
let tool = Tool::new_callback(
"greet",
Some("Greets a person by name".to_string()),
serde_json::from_value(serde_json::to_value(schema_for!(GreetInput))?)?,
None,
)?;
let toolset = ToolSet::new("Greeter", "Greeting functions", vec![tool]);
// 2. Create CodeMode instance with your tools
let mut code_mode = CodeMode::default();
code_mode.tool_sets = vec![toolset];
// 3. Register callbacks that execute when tools are called
let registry = CallbackRegistry::default();
registry.add("Greeter.greet", Arc::new(|args| {
Box::pin(async move {
let name = args
.and_then(|v| v.get("name"))
.and_then(|v| v.as_str())
.unwrap_or("World");
Ok(serde_json::json!({ "message": format!("Hello, {name}!") }))
})
}))?;
// 4. Execute LLM-generated TypeScript code
let code = r#"
async function run() {
const result = await Greeter.greet({ name: "Alice" });
return result;
}
"#;
let output = code_mode.execute(code, Some(registry)).await?;
if output.success {
println!("Result: {}", serde_json::to_string_pretty(&output.output)?);
} else {
eprintln!("Error: {}", output.stderr);
}
Ok(())
}
Tools represent individual functions that can be called from TypeScript code. They are organized into ToolSets (namespaces).
use pctx_code_mode::{Tool, ToolSet};
// Create a tool with input/output schemas
let tool = Tool::new_callback(
"fetchData", // Function name
Some("Fetches data from API"), // Description
input_schema, // JSON Schema for input
Some(output_schema), // Optional output schema
)?;
// Organize tools into a namespace
let toolset = ToolSet::new(
"DataApi", // Namespace
"Data fetching functions", // Description
vec![tool], // Tools
);
The main execution engine that manages tools and executes TypeScript code.
let mut code_mode = CodeMode::default();
code_mode.tool_sets = vec![toolset1, toolset2];
// List available functions
let list = code_mode.list_functions();
for func in list.functions {
println!("{}.{}: {:?}", func.namespace, func.name, func.description);
}
// Get detailed type information
let details = code_mode.get_function_details(GetFunctionDetailsInput {
functions: vec![
FunctionId { mod_name: "DataApi".into(), fn_name: "fetchData".into() }
],
});
println!("TypeScript definitions:\n{}", details.code);
Callbacks are Rust functions that execute when TypeScript code calls your tools.
use pctx_code_mode::{CallbackRegistry, CallbackFn};
use std::sync::Arc;
let registry = CallbackRegistry::default();
let callback: CallbackFn = Arc::new(|args| {
Box::pin(async move {
// Extract arguments
let id = args
.and_then(|v| v.get("id"))
.and_then(|v| v.as_i64())
.ok_or("Missing id")?;
// Do async work
let data = fetch_from_database(id).await?;
// Return JSON result
Ok(serde_json::to_value(data)?)
})
});
// Register with namespace.function format
registry.add("DataApi.fetchData", callback)?;
Execute LLM-generated TypeScript code that calls your registered tools.
let code = r#"
async function run() {
// Call your registered tools
const user = await DataApi.fetchData({ id: 123 });
const greeting = await Greeter.greet({ name: user.name });
// Chain multiple calls
const result = await DataApi.saveData({
id: user.id,
message: greeting.message
});
// Return the final result
return result;
}
"#;
let output = code_mode.execute(code, Some(registry)).await?;
match output.success {
true => println!("Success: {:?}", output.output),
false => eprintln!("Error: {}", output.stderr),
}
The main execution engine.
new() / default()let code_mode = CodeMode::default();
list_functions() -> ListFunctionsOutputLists all available functions with their TypeScript interface declarations.
let list = code_mode.list_functions();
println!("Available functions:\n{}", list.code);
for func in list.functions {
println!(" {}.{}", func.namespace, func.name);
}
get_function_details(input: GetFunctionDetailsInput) -> GetFunctionDetailsOutputGets detailed TypeScript type definitions for specific functions.
use pctx_code_mode::model::{GetFunctionDetailsInput, FunctionId};
let details = code_mode.get_function_details(GetFunctionDetailsInput {
functions: vec![
FunctionId {
mod_name: "DataApi".to_string(),
fn_name: "fetchData".to_string(),
}
],
});
println!("TypeScript code:\n{}", details.code);
execute(code: &str, callbacks: Option<CallbackRegistry>) -> Result<ExecuteOutput>Executes TypeScript code in a sandboxed Deno runtime.
let output = code_mode.execute(typescript_code, Some(callback_registry)).await?;
if output.success {
println!("Return value: {:?}", output.output);
println!("Stdout: {}", output.stdout);
} else {
eprintln!("Stderr: {}", output.stderr);
}
add_callback(config: &CallbackConfig) -> Result<()>Dynamically adds a callback-based tool to the code mode.
use pctx_code_mode::model::CallbackConfig;
code_mode.add_callback(&CallbackConfig {
name: "logMessage".to_string(),
namespace: "Logger".to_string(),
description: Some("Logs a message".to_string()),
input_schema: Some(serde_json::json!({
"type": "object",
"properties": {
"message": { "type": "string" }
},
"required": ["message"]
})),
output_schema: None,
})?;
Thread-safe registry for managing callback functions.
default() -> CallbackRegistrylet registry = CallbackRegistry::default();
add(id: &str, callback: CallbackFn) -> Result<()>Registers a callback with a specific ID (format: Namespace.functionName).
registry.add("DataApi.fetchData", Arc::new(|args| {
Box::pin(async move {
// Your implementation
Ok(serde_json::json!({"result": "data"}))
})
}))?;
has(id: &str) -> boolChecks if a callback is registered.
if registry.has("DataApi.fetchData") {
println!("Callback is registered");
}
Toolpub struct Tool {
pub name: String,
pub fn_name: String,
pub description: Option<String>,
pub input_signature: String,
pub output_signature: String,
pub types: String,
// ... internal fields
}
Create tools for MCP-style tools or callbacks:
// MCP-style tool
let tool = Tool::new_mcp(
"toolName",
Some("Description"),
input_schema,
output_schema,
)?;
// Callback-based tool
let tool = Tool::new_callback(
"toolName",
Some("Description"),
input_schema,
output_schema,
)?;
ToolSetpub struct ToolSet {
pub name: String,
pub namespace: String,
pub description: String,
pub tools: Vec<Tool>,
}
let toolset = ToolSet::new("MyNamespace", "Description", vec![tool1, tool2]);
ExecuteOutputpub struct ExecuteOutput {
pub success: bool,
pub stdout: String,
pub stderr: String,
pub output: Option<serde_json::Value>,
}
CallbackFnType alias for callback functions:
pub type CallbackFn = Arc<
dyn Fn(Option<serde_json::Value>) -> Pin<Box<dyn Future<Output = Result<serde_json::Value, String>> + Send>>
+ Send
+ Sync
>;
Convert MCP (Model Context Protocol) tools into Code Mode tools:
use rmcp::model::Tool as McpTool;
fn convert_mcp_tool(mcp_tool: &McpTool) -> Result<Tool> {
let mut schema_value = serde_json::to_value(&mcp_tool.input_schema)?;
// Dereference JSON Schema $refs
unbinder::dereference_schema(&mut schema_value, unbinder::Options::default());
let input_schema: RootSchema = serde_json::from_value(schema_value)?;
Tool::new_mcp(
&mcp_tool.name,
mcp_tool.description.as_ref().map(|s| s.to_string()),
input_schema,
None,
)
}
Register tools at runtime based on configuration:
for config in tool_configs {
code_mode.add_callback(&CallbackConfig {
name: config.name,
namespace: config.namespace,
description: Some(config.description),
input_schema: Some(config.input_schema),
output_schema: config.output_schema,
})?;
// Register the corresponding callback
let callback_id = format!("{}.{}", config.namespace, config.name);
registry.add(&callback_id, create_callback_for_config(&config))?;
}
Callbacks support full async operations:
registry.add("Database.query", Arc::new(|args| {
Box::pin(async move {
let query = args
.and_then(|v| v.get("sql"))
.and_then(|v| v.as_str())
.ok_or("Missing sql parameter")?;
// Perform async database query
let pool = get_db_pool().await;
let rows = sqlx::query(query)
.fetch_all(&pool)
.await
.map_err(|e| e.to_string())?;
Ok(serde_json::to_value(rows)?)
})
}))?;
let output = code_mode.execute(code, Some(registry)).await?;
if !output.success {
// Check stderr for execution errors
if output.stderr.contains("TypeError") {
eprintln!("Type error in generated code: {}", output.stderr);
} else if output.stderr.contains("not found") {
eprintln!("Tool not found: {}", output.stderr);
} else {
eprintln!("Execution failed: {}", output.stderr);
}
}
LLM-generated code must follow this pattern:
async function run() {
// Your code that calls registered tools
const result = await Namespace.toolName({ param: value });
// MUST return a value
return result;
}
The code execution engine:
run() and captures its return valueExecuteOutput.outputCode is executed in a Deno runtime with:
Configure allowed hosts:
code_mode.servers = vec![
ServerConfig {
// Your server configuration
// Only hosts in server configs are allowed network access
}
];
let allowed = code_mode.allowed_hosts();
println!("Allowed hosts: {:?}", allowed);
let code = r#"
async function run() {
// Fetch user data
const user = await UserApi.getUser({ id: 123 });
// Process the data
const processed = await DataProcessor.transform({
input: user.data,
format: "normalized"
});
// Save results
const saved = await Storage.save({
key: `user_${user.id}`,
value: processed
});
return {
userId: user.id,
saved: saved.success,
location: saved.url
};
}
"#;
let output = code_mode.execute(code, Some(registry)).await?;
let code = r#"
async function run() {
try {
return await RiskyApi.operation({ id: 1 });
} catch (error) {
console.error("Operation failed:", error);
// Fall back to safe default
return await SafeApi.getDefault();
}
}
"#;
let output = code_mode.execute(code, Some(registry)).await?;
// Check console output
if !output.stdout.is_empty() {
println!("Console output: {}", output.stdout);
}
let code = r#"
async function run() {
// Execute multiple operations in parallel
const [users, posts, comments] = await Promise.all([
UserApi.listUsers(),
PostApi.listPosts(),
CommentApi.listComments()
]);
return { users, posts, comments };
}
"#;
pctx_codegen: TypeScript code generation from JSON schemaspctx_executor: Deno runtime execution enginepctx_code_execution_runtime: Runtime environment and callback systemMIT