Skip to main content
A Function is a callable unit of work with a unique ID, description, and handler. Functions are registered by workers and can be invoked by other functions via trigger() - even across different programming languages.

What is a Function?

A function in AgentOS consists of:
  1. Unique ID: Namespaced identifier (e.g., agent::chat, memory::store)
  2. Description: Human-readable explanation of what the function does
  3. Handler: Async callback that processes input and returns output
  4. Optional Metadata: Category, tags, or other annotations
Functions are language-agnostic. A Rust function can call a TypeScript function, which can call a Python function, all via trigger().

Registering Functions

Rust Function Registration

Use register_function_with_description() to register functions:
// From crates/agent-core/src/main.rs:18-30
let iii = III::new("ws://localhost:49134");

let iii_clone = iii.clone();
iii.register_function_with_description(
    "agent::chat",
    "Process a message through the agent loop",
    move |input: Value| {
        let iii = iii_clone.clone();
        async move {
            let req: ChatRequest = serde_json::from_value(input)
                .map_err(|e| IIIError::Handler(e.to_string()))?;
            agent_chat(&iii, req).await
        }
    },
);

Multiple Functions

// From crates/agent-core/src/main.rs:32-70
iii.register_function_with_description(
    "agent::list_tools",
    "List tools available to an agent",
    move |input: Value| {
        let iii = iii_clone.clone();
        async move {
            let agent_id = input["agentId"].as_str().unwrap_or("default");
            list_tools(&iii, agent_id).await
        }
    },
);

iii.register_function_with_description(
    "agent::create",
    "Register a new agent",
    move |input: Value| {
        let iii = iii_clone.clone();
        async move {
            let config: AgentConfig = serde_json::from_value(input)
                .map_err(|e| IIIError::Handler(e.to_string()))?;
            create_agent(&iii, config).await
        }
    },
);

iii.register_function_with_description(
    "agent::list",
    "List all agents",
    move |_: Value| {
        let iii = iii_clone.clone();
        async move {
            iii.trigger("state::list", json!({ "scope": "agents" })).await
                .map_err(|e| IIIError::Handler(e.to_string()))
        }
    },
);

Invoking Functions

Functions call other functions using trigger(). This works across all languages.
// From crates/agent-core/src/main.rs:115-122
let memories: Value = iii
    .trigger("memory::recall", json!({
        "agentId": &req.agent_id,
        "query": &req.message,
        "limit": 20,
    }))
    .await
    .unwrap_or(json!([]));

let tools: Value = iii
    .trigger("agent::list_tools", json!({ "agentId": &req.agent_id }))
    .await
    .unwrap_or(json!([]));

let model: Value = iii
    .trigger("llm::route", json!({
        "message": &req.message,
        "toolCount": tools.as_array().map(|a| a.len()).unwrap_or(0),
    }))
    .await
    .map_err(|e| IIIError::Handler(e.to_string()))?;

Function Call Flow

Here’s a real example from the agent-core showing how functions chain together:

Real Agent Loop Example

Here’s the complete agent loop from the Rust agent-core, showing multiple function calls:
// From crates/agent-core/src/main.rs:103-265
async fn agent_chat(iii: &III, req: ChatRequest) -> Result<Value, IIIError> {
    let start = Instant::now();

    // 1. Get agent config
    let config: Option<AgentConfig> = iii
        .trigger("state::get", json!({
            "scope": "agents",
            "key": &req.agent_id,
        }))
        .await
        .ok()
        .and_then(|v| serde_json::from_value(v).ok());

    // 2. Recall memories
    let memories: Value = iii
        .trigger("memory::recall", json!({
            "agentId": &req.agent_id,
            "query": &req.message,
            "limit": 20,
        }))
        .await
        .unwrap_or(json!([]));

    // 3. Get available tools
    let tools: Value = iii
        .trigger("agent::list_tools", json!({ "agentId": &req.agent_id }))
        .await
        .unwrap_or(json!([]));

    // 4. Route to appropriate model
    let model: Value = iii
        .trigger("llm::route", json!({
            "message": &req.message,
            "toolCount": tools.as_array().map(|a| a.len()).unwrap_or(0),
        }))
        .await
        .map_err(|e| IIIError::Handler(e.to_string()))?;

    // 5. Security scan for injection
    let scan_result = iii
        .trigger("security::scan_injection", json!({ "text": &req.message }))
        .await
        .unwrap_or(json!({ "safe": true, "riskScore": 0.0 }));
    let risk_score = scan_result["riskScore"].as_f64().unwrap_or(0.0);
    if risk_score > 0.5 {
        return Err(IIIError::Handler(format!(
            "Message rejected: injection risk score {:.2} exceeds threshold",
            risk_score
        )));
    }

    // 6. Call LLM
    let mut response: Value = iii
        .trigger("llm::complete", json!({
            "model": model,
            "systemPrompt": system_prompt,
            "messages": messages,
            "tools": tools,
        }))
        .await
        .map_err(|e| IIIError::Handler(e.to_string()))?;

    // 7. Tool execution loop (up to 50 iterations)
    let mut iterations: u32 = 0;
    while let Some(tool_calls) = response.get("toolCalls").and_then(|v| v.as_array()) {
        if tool_calls.is_empty() || iterations >= MAX_ITERATIONS {
            break;
        }
        iterations += 1;

        for tc in &calls {
            // Check capability
            let cap_check = iii.trigger("security::check_capability", json!({
                "agentId": &req.agent_id,
                "capability": tc.id.split("::").next().unwrap_or(""),
                "resource": &tc.id,
            })).await;

            // Execute tool
            match iii.trigger(&tc.id, tc.arguments.clone()).await {
                Ok(result) => { /* use result */ }
                Err(e) => { /* handle error */ }
            }
        }

        // Get next response from LLM
        response = iii.trigger("llm::complete", json!({ /* ... */ })).await?;
    }

    // 8. Store in memory
    let _ = iii.trigger_void("memory::store", json!({
        "agentId": &req.agent_id,
        "sessionId": &session_id,
        "role": "user",
        "content": &req.message,
    }));

    Ok(json!({
        "content": response.get("content"),
        "model": response.get("model"),
        "usage": response.get("usage"),
        "iterations": iterations,
    }))
}

Fire-and-Forget with triggerVoid

For operations you don’t need to wait for (logging, metrics, etc.), use triggerVoid:
// From crates/agent-core/src/main.rs:85-88
let _ = iii.trigger_void("publish", json!({
    "topic": "agent.lifecycle",
    "data": { "type": "deleted", "agentId": agent_id },
}));

Function Naming Conventions

All AgentOS functions follow the namespace::action pattern:
NamespacePurposeExamples
agent::Agent operationsagent::chat, agent::create, agent::list
memory::Memory operationsmemory::store, memory::recall, memory::search
llm::LLM operationsllm::route, llm::complete
security::Security checkssecurity::check_capability, security::scan_injection
state::State managementstate::get, state::set, state::list, state::delete
tool::Tool executiontool::web_search, tool::file_read
file::File operationsfile::read, file::write, file::list
embedding::Embeddingsembedding::generate, embedding::similarity

Error Handling

// Return Result<Value, IIIError>
async fn my_function(iii: &III, input: Value) -> Result<Value, IIIError> {
    let result = iii
        .trigger("other::function", input)
        .await
        .map_err(|e| IIIError::Handler(e.to_string()))?;
    
    Ok(json!({ "data": result }))
}

Best Practices

Descriptive IDs

Use clear namespace::action format: memory::store not mem_st

Meaningful Descriptions

Write descriptions that explain why and when to use the function

Type Safety

Parse and validate input, don’t assume structure

Idempotency

Design functions to be safely retried when possible

Fail Fast

Validate early, return errors quickly

Use triggerVoid

Don’t wait for logging, metrics, or non-critical operations

Next Steps

Triggers

Learn how to bind functions to HTTP, queue, and cron events

Architecture

Understand how functions compose into the full system

Build docs developers (and LLMs) love