From wasp
Help write, understand, or modify the Rust logic of a UOMI WASM agent. Use when a developer wants to build a specific type of agent, customize lib.rs, use UOMI host functions, or implement patterns like RAG, multi-step reasoning, or structured I/O.
How this skill is triggered — by the user, by Claude, or both
Slash command
/wasp:agentThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
You are helping a developer write or modify the Rust code for a UOMI WASM agent.
You are helping a developer write or modify the Rust code for a UOMI WASM agent.
What the developer wants to build: $ARGUMENTS
agent-template/src/lib.rs directly — don't just explain, write the actual codeIf the developer already has a lib.rs, read it first before suggesting changes.
On-chain call → host runtime → WASM (lib.rs) → host runtime → on-chain output
reads input.txt run() function writes output.txt
The agent lifecycle per invocation:
input.txt (the on-chain inputData bytes)agent_template.wasm and calls run()run(), use the utils API to read input, call LLMs, fetch IPFS, write outputsave_output() wrote and returns it on-chainEverything is synchronous. No async, no threads, no file system, no network (only via host functions).
All functions live in utils.rs — import with mod utils; at the top of lib.rs.
// Read the raw input bytes (the inputData passed to callAgent on-chain)
pub fn read_input() -> Vec<u8>
// Parse the input as a JSON array of {role, content} messages
// Panics if input is not valid JSON messages array
pub fn parse_messages(input: &[u8]) -> Vec<Message>
// Create a system message
pub fn system_message(content: String) -> Message
// Prepend system message to a messages array
pub fn process_messages(system: Message, messages: Vec<Message>) -> Vec<Message>
// Call an LLM model. model is the key in uomi.config.json (1, 2, 3, ...)
// content is the request body as bytes (use prepare_request to build it)
// Returns the raw LLM response bytes
pub fn call_ai_service(model: i32, content: Vec<u8>) -> Vec<u8>
// Convert a JSON string body to bytes ready for call_ai_service
pub fn prepare_request(body: &str) -> Vec<u8>
Request body format:
// Standard chat format
let body = format!("{{\"messages\": {}}}", serde_json::to_string(&messages).unwrap());
Response format (UOMI model):
{ "response": "...", "time_taken": 1.2, "tokens_per_second": 45, "total_tokens_generated": 54 }
Response format (OpenAI-compatible model):
{ "choices": [{ "message": { "content": "..." } }], "usage": { "total_tokens": 42 } }
// Fetch a file from IPFS by its CID
// cid is the CID string as bytes: "bafkrei...".as_bytes().to_vec()
pub fn get_cid_file_service(cid: Vec<u8>) -> Vec<u8>
// Read the input file (the inputCidFile parameter from callAgent on-chain,
// or the local_file_path from uomi.config.json during dev)
pub fn get_input_file_service() -> Vec<u8>
// Write the agent output — this becomes the on-chain result
// Call this exactly once at the end of run()
pub fn save_output(data: &[u8])
// Print a debug message visible in the host terminal during local dev
pub fn log(message: &str)
Defined in lib.rs (not in utils):
#[derive(Serialize, Deserialize, Debug)]
struct Message {
role: String, // "system" | "user" | "assistant"
content: String,
}
Reference implementations are in the patterns/ directory next to this file.
patterns/chat.rsThe simplest agent. Adds a system prompt and calls the LLM. Use when: building a custom chatbot, assistant, or any conversational agent.
let input = utils::read_input();
let messages = utils::parse_messages(&input);
let system = utils::system_message("You are ...".to_string());
let msgs = utils::process_messages(system, messages);
let body = format!("{{\"messages\": {}}}", serde_json::to_string(&msgs).unwrap());
let response = utils::call_ai_service(1, utils::prepare_request(&body));
utils::save_output(&response);
patterns/structured_input.rsReceives a custom JSON object, outputs a custom JSON object. Use when: the caller sends structured data (not just chat messages) or you need a structured response.
Key additions:
#[derive(Deserialize)] struct AgentInput { ... }#[derive(Serialize)] struct AgentOutput { ... }serde_json::from_slice::<AgentInput>(&raw)serde_json::to_string(&output).unwrap().as_bytes()patterns/ipfs_rag.rsFetches a knowledge document from IPFS, injects it as context. Use when: building a Q&A bot over a specific document, knowledge base, or dataset.
Key step:
let doc_bytes = utils::get_cid_file_service("bafkrei...".as_bytes().to_vec());
let doc = String::from_utf8_lossy(&doc_bytes).to_string();
// Inject doc into system prompt, truncate to ~12k chars to stay within limits
patterns/multi_step.rsCalls the LLM multiple times. First to classify/plan, then to answer. Use when: you need routing logic, chain-of-thought, or different prompts for different query types.
Key constraint: each call_ai_service is a blocking call — keep chains short (2-3 steps) to avoid timeouts.
[package]
name = "your-agent-name" # must match directory name (hyphens → underscores for WASM output)
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"] # required — produces a .wasm file
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
# Add pure-Rust crates here. Avoid crates that need OS, network, or threads.
Works in WASM (pure Rust, no OS deps):
serde / serde_json — already includedbase64 — encode/decode base64hex — hex encodingsha2 / sha3 / md-5 — hashinguuid (with v4 feature disabled) — UUID parsingregex — pattern matchingchrono (with wasmbind feature) — date/time parsingWon't work in WASM (need OS/network):
reqwest, hyper, tokio — use call_ai_service insteadstd::fs — no file systemstd::thread — no threadsrand with OS entropy — use a deterministic seed or a WASM-compatible RNG// ❌ panics if input is empty or malformed
let messages = utils::parse_messages(&input);
// ✅ handle gracefully
let messages = serde_json::from_slice::<Vec<Message>>(&input).unwrap_or_else(|e| {
utils::log(&format!("Parse error: {}", e));
vec![Message { role: "user".to_string(), content: String::from_utf8_lossy(&input).to_string() }]
});
// ❌ agent completes but returns nothing
let response = utils::call_ai_service(1, request);
// missing: utils::save_output(&response);
// ✅
utils::save_output(&response);
Only the last call to save_output wins (host overwrites). Call it once at the end.
MAX_INPUT_SIZE is 1MB. Keep injected documents under ~12,000 characters to avoid hitting model context limits.
When you need to extract the text content from the LLM response:
fn extract_content(response_bytes: &[u8]) -> String {
let s = String::from_utf8_lossy(response_bytes);
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&s) {
// OpenAI format
if let Some(c) = json["choices"][0]["message"]["content"].as_str() {
return c.to_string();
}
// UOMI format
if let Some(c) = json["response"].as_str() {
return c.to_string();
}
}
s.to_string()
}
Always write unit tests for the decide function (or any pure logic function).
Because utils.rs uses extern "C" host symbols that don't exist in a native binary,
you must gate out the host-dependent code during test builds.
Required pattern — add these two #[cfg(not(test))] guards:
// 1. Gate the utils module — prevents unresolved extern "C" link errors
#[cfg(not(test))]
mod utils;
// 2. Gate run() — it calls utils functions
#[cfg(not(test))]
#[no_mangle]
pub extern "C" fn run() { ... }
// 3. Gate save() helper if it calls utils::save_output
#[cfg(not(test))]
fn save(output: AgentOutput) {
utils::save_output(serde_json::to_string(&output).unwrap().as_bytes());
}
Run tests on the native target (not wasm32):
# from agent-template/
cargo test
Test module template:
#[cfg(test)]
mod tests {
use super::*;
fn make_input(/* ... */) -> AgentInput { /* ... */ }
#[test]
fn test_basic_case() {
let out = decide(&make_input(/* ... */));
assert_eq!(out.action, "buy");
assert!(out.reason.contains("expected text"));
}
}
Common pitfall — identity conditions:
Conditions like value >= value / n are always true for n >= 1. Double-check
that your can_buy/can_sell guards actually gate on zero balance:
// ❌ always true
let can_buy = portfolio.quote >= buy_amount * buy_price; // = quote >= quote/levels
// ✅ actually guards zero balance
let can_buy = portfolio.quote > 0.0;
Set local_file_path in uomi.config.json to a file with your test input, then:
# from project root
echo '{"your":"input"}' > host/src/input.txt
npm run build
cat host/src/output.txt
Logs from utils::log() appear prefixed with [WASM] in the terminal.
Provides UI/UX resources: 50+ styles, color palettes, font pairings, guidelines, charts for web/mobile across React, Next.js, Vue, Svelte, Tailwind, React Native, Flutter. Aids planning, building, reviewing interfaces.
Fetches up-to-date documentation from Context7 for libraries and frameworks like React, Next.js, Prisma. Use for setup questions, API references, and code examples.
npx claudepluginhub uomi-network/agents-skills --plugin wasp