Skip to content

Build Your First Agent

This guide builds a simple task-tracking agent that receives instructions, calls tools, and stores results durably.

A Cosmictron WASM module that:

  1. Receives task instructions over WebSocket
  2. Records tool invocations with full idempotency
  3. Pushes real-time state deltas back to the client
  4. Survives server restarts without losing state
my-agent/
├── Cargo.toml
├── src/
│ └── lib.rs # Your agent logic (WASM module)
└── client/
└── index.ts # TypeScript client

In src/lib.rs, declare your tables and reducers:

use cosmictron_sdk::prelude::*;
/// Durable record of every agent message
#[table(name = "agent_state", public)]
pub struct AgentMessage {
#[primary_key]
#[auto_inc]
pub id: u64,
pub session_id: String,
pub role: String, // "user" | "assistant" | "system"
pub content: String,
pub created_at: Timestamp,
}
/// Tracks MCP tool invocations with idempotency
#[table(name = "tool_calls")]
pub struct ToolCall {
#[primary_key]
pub call_id: String, // client-provided idempotency key
pub tool_name: String,
pub arguments: String, // JSON-encoded
pub status: String, // "pending" | "completed" | "failed"
pub result: Option<String>,
pub invoked_at: Timestamp,
}

Reducers are state-mutating functions that execute inside the database:

/// Record a new message from user or assistant
#[reducer]
pub fn record_agent_message(
ctx: &ReducerContext,
session_id: String,
role: String,
content: String,
) {
AgentMessage::insert(AgentMessage {
id: 0, // auto_inc
session_id,
role,
content,
created_at: ctx.timestamp,
})
.expect("insert failed");
}
/// Start a tool call — idempotent by call_id
#[reducer]
pub fn record_tool_call(
ctx: &ReducerContext,
call_id: String,
tool_name: String,
arguments: String,
) {
// Idempotency: skip if already recorded
if ToolCall::filter_by_call_id(&call_id).is_some() {
return;
}
ToolCall::insert(ToolCall {
call_id,
tool_name,
arguments,
status: "pending".to_string(),
result: None,
invoked_at: ctx.timestamp,
})
.expect("insert failed");
}
/// Complete a tool call with result
#[reducer]
pub fn complete_tool_call(
_ctx: &ReducerContext,
call_id: String,
result: String,
success: bool,
) {
if let Some(mut call) = ToolCall::filter_by_call_id(&call_id) {
call.status = if success { "completed" } else { "failed" }.to_string();
call.result = Some(result);
ToolCall::update(call);
}
}
Terminal window
# Build to WASM
cargo build --target wasm32-unknown-unknown --release
# Deploy to running server
cosmictron-cli deploy \
target/wasm32-unknown-unknown/release/my_agent.wasm \
--name my-agent
import { CosmictronClient } from '@cosmictron/client';
const client = new CosmictronClient('ws://localhost:3000');
// Authenticate (email/password, passkey, or magic link)
await client.signIn({ email: 'dev@example.com', password: 'secret' });
// Subscribe to real-time agent state
const sub = await client.subscribe(
'SELECT * FROM agent_state WHERE session_id = :sid ORDER BY id',
{ sid: 'session-abc' }
);
sub.on('update', ({ inserts, deletes }) => {
console.log('New messages:', inserts);
});
// Send a user message
await client.callReducer('record_agent_message', {
session_id: 'session-abc',
role: 'user',
content: 'Summarise the Q1 sales report',
});
// Record a tool call (idempotent)
await client.callReducer('record_tool_call', {
call_id: 'tool-unique-id-001',
tool_name: 'read_file',
arguments: JSON.stringify({ path: '/reports/q1.pdf' }),
});
// Mark the tool call complete
await client.callReducer('complete_tool_call', {
call_id: 'tool-unique-id-001',
result: JSON.stringify({ pages: 42, summary: '...' }),
success: true,
});

Once this module is deployed:

  • State persists across server restarts via the WAL and snapshot system.
  • All clients subscribed to agent_state receive real-time deltas when new messages are inserted.
  • Audit trail — if you have signing enabled, every reducer call is signed with Ed25519 and linked into the hash chain. See Event Signing.