MALDA™ Reference Manual

The AI-First Programming Language - Version 1.0.39

19. Agent Orchestration

This chapter is the agent runtime: LLM clients, conversations, tools, Agent, @Tool, dashboard, and teams. Specialized kinds and Ralph are Agent kits — tool bundles on top of that runtime, not a second language. Prompts and schemas are in 10. Prompts; cassettes and malda eval are in 24. Agentic Runs.

Agents, LLM clients, and tool calling run on the interpreter and C# transpile. They are not available on the JavaScript backend — keep agents on the host and call them over HTTP from browser code. See the backend capability matrix.

19.0 First read

An Agent is a named runtime object: instructions, an LLM client, and optional tools. Agents are host-only. A live agent.think needs a client or API key. Start at Examples/Prompts/prompt_with_agent.malda or malda new agent. Skip 19.1 on this pass — it is the production risk list, not the first lesson.

First-read stop. agents.define constructs a kit without calling a model. The rest of this chapter is the runtime catalog.
var coder = agents.define({
    name: "Coder",
    kind: "CodingAgent",
    role: "programmer",
    instructions: "Write small diffs.",
    workingDirectory: "."
});
io.print(coder.kind);

Chapter map

19.1 ⚠️ Critical Security and Risk Disclaimer

Autonomous agent systems with tool access pose significant security, safety, and operational risks. Before deploying agents with tools in production environments, you must understand and mitigate these risks.

Fundamental Risks

Real-World Risk Scenarios

1. Data Loss and File System Damage

2. Unauthorized Code Execution

3. Git Repository Corruption

4. Unauthorized Access and Information Disclosure

5. Infinite Loops and Resource Exhaustion

6. Cascading Failures in Multi-Agent Systems

7. Production System Modification

8. Malicious Code Injection

Mitigation Strategies

Best Practices

By using MALDA's agent orchestration features, you acknowledge that you understand these risks and are solely responsible for the actions taken by agents you create and deploy. The developers of MALDA and this reference manual provide no warranties regarding the safety, security, or reliability of autonomous agent systems.

19.2 LLM Client

The LLMClient class provides an interface to OpenAI-compatible LLM APIs (OpenAI, OpenRouter, LMStudio, OLLAMA, etc.).

Constructor

var client = new LLMClient(apiUrl, apiKey, model);

Methods

Example

var apiKey = getEnv("OPENAI_API_KEY");
var client = new LLMClient(
    "https://api.openai.com/v1/chat/completions",
    apiKey,
    "gpt-3.5-turbo"
);

var response = client.complete("Explain quantum computing in one sentence.");
print(response);

19.3 OpenRouterClient

For OpenRouter specifically, there's a specialized OpenRouterClient class that simplifies configuration.

Constructor

// With default model (deepseek/deepseek-v4-flash)
var client = new OpenRouterClient();

// With custom model
var client = new OpenRouterClient("openai/gpt-4");
Automatic Configuration: OpenRouterClient automatically reads the API key from the OPENROUTER_API_KEY environment variable and uses the correct OpenRouter endpoint.

App attribution (OpenRouter analytics)

Set these properties so OpenRouter can attribute token usage to each of your apps (App Attribution). Use a distinct httpReferer URL per application. Analytics appear at https://openrouter.ai/apps?url=<your-url>.

var client = new OpenRouterClient();
client.httpReferer = "https://example.com/secondbrain";  // HTTP-Referer (required for attribution)
client.appTitle = "Second Brain ASK";                     // X-OpenRouter-Title
client.appCategories = "cli-agent";                       // optional X-OpenRouter-Categories

19.3.1 Default local LLM (when no client is provided)

When you create an agent, prompt, or use built-ins like decomposeTask without passing an LLM client, MALDA uses a default local LLM: Qwen/Qwen2.5-0.5B-Instruct, downloaded as the GGUF build qwen2.5-0.5b-instruct-q4_k_m.gguf from Hugging Face on first use. The model is cached under your user profile (e.g. %LOCALAPPDATA%\MaldaLang\Models\default on Windows). No API key or network is required after the first download. To use a remote model instead, pass an OpenRouterClient, LLMClient, or LlamaCppClient explicitly.

You can override the default model with the environment variable MALDA_DEFAULT_LOCAL_MODEL. Set it to modelId/fileName.gguf (e.g. org/repo/MyModel-Q4_K_M.gguf) to use a different Hugging Face GGUF model; that model will be downloaded and cached in a subfolder named after the model ID. If unset, the built-in default Qwen/Qwen2.5-0.5B-Instruct GGUF build is used.

19.4 LlamaCppClient

The LlamaCppClient class enables local LLM inference using LLAMA.cpp with GGUF format models. No API keys or network connection required. If you omit modelPath, MALDA uses the default local Qwen/Qwen2.5-0.5B-Instruct GGUF model and auto-downloads it on first use.

Constructor

var client = new LlamaCppClient(modelPath);   // Custom GGUF file
var client = new LlamaCppClient();            // Default auto-downloaded local model

Example

var client = new LlamaCppClient();
client.setTemperature(0.7);
client.setMaxTokens(2000);
client.setGpuLayerCount(35);  // Offload layers to GPU (optional)

var conversation = new Conversation(client, "You are a helpful assistant.");
conversation.addUserMessage("Explain quantum computing.");
var response = conversation.send();
print(response.content);

19.5 Conversation Management

The Conversation class manages multi-turn conversations with LLMs, including automatic tool call handling.

Constructor

var conversation = new Conversation(client, systemPrompt);

Methods

Example

var conversation = new Conversation(client, "You are a helpful assistant.");
conversation.addUserMessage("What is 2+2?");
var response = conversation.send();
print(response.content);

Declared context Name { … } is a separate notebook you pass to a prompt yourself; Conversation does not read it. See 24.5.

Parallel read-only tool calls

When the LLM returns multiple read-only tool calls in a single response, Conversation executes them in parallel by default. This applies to all agents (Agent, DevAgent, Ralph Wiggum loops, IDE chat) because they share the same runtime.

Parallel-safe built-in tools: read_file, grep, list_directory, glob, get_symbols, get_parse_errors, check_malda, validate_json, web_search, web_fetch, recall_progress, and read-only git tools (git_status, git_log, git_diff, git_branch).

Always sequential: file writes, git mutations, run_command, test_malda, ask_user, remember_progress, MCP tools, agent-as-tool wrappers, and custom @Tool handlers. A mutating tool in the middle of a batch splits execution so ordering stays safe.

Tool result messages are appended in the same order as the original tool_calls array before the next LLM request.

Configuration: parallel execution is on when MALDA_PARALLEL_TOOL_CALLS is unset. Set MALDA_PARALLEL_TOOL_CALLS=false (or 0, off) to disable. With verbose logging (MALDA_RALPH_VERBOSE or MALDA_AGENT_VERBOSE), parallel batches are logged explicitly.

Verbose logging, thinking feedback, and LLM streaming

During agent.think() (and any code path using Conversation.send()), the runtime can print live progress to the console:

With verbose logging off, an interactive terminal keeps a single activity line during send(). The line shows thinking… round N while the model runs, then the tool name and target (for example read_file src/App.malda) while a tool runs, then a short result such as · 120 lines or · ok. The same line is rewritten in place so tool chatter does not fill the screen. A failed tool is committed on its own line. The line is erased before the final answer and before ask_user. Set MALDA_AGENT_STATUS=off to disable it. Pipes, cron, and tests that redirect stdout stay quiet. Windows, macOS, and Linux terminals use the same carriage-return rewrite.

HTTP LLM clients (LLMClient, OpenRouterClient) use OpenAI-style SSE streaming (stream: true) by default. When verbose logging and thinking output are enabled, [think] text is written token-by-token as deltas arrive. Tool-call JSON is accumulated silently during the stream; tool names are logged after the stream completes.

Not streamed today: LlamaCppClient, bridge backends, and requests with structured response_format (those fall back to a single blocking response).

VariableDefaultPurpose
MALDA_AGENT_VERBOSEfalse (Ralph: true)Enable [llm] / [tool] logging
MALDA_AGENT_STATUSon for a terminalRewrite one activity line (thinking… round N, then the tool name and target). off, 0, false, or no disables it. Stays off when verbose logging is on or stdout is redirected. MALDA_RALPH_STATUS is an alias.
MALDA_RALPH_VERBOSE—Alias for MALDA_AGENT_VERBOSE
MALDA_AGENT_RICHtrueColored Spectre.Console output (auto-off when redirected)
MALDA_AGENT_TOOL_DETAILcompactcompact or full tool logs
MALDA_AGENT_LLM_THINKINGcompact[think] output: off, compact, or full
MALDA_AGENT_LLM_STREAMtrueOpenAI SSE streaming for HTTP clients; live [think] when thinking is enabled
MALDA_AGENT_LLM_PREVIEWcompactFinal-response preview when streaming/thinking is off
MALDA_AGENT_STATUS_EVERY4Repeat status banner every N LLM rounds (Ralph)
MALDA_RALPH_*—Aliases for the agent settings above in Ralph runs

The same diagnostics can be toggled from code. These helpers change presentation only and never change an agent's result:

enableAgentVerboseLogging();
setAgentStatusBanner("Nightly refactor run");
setAgentVerbosePhase("planning");

19.6 Tool System

Tools allow LLMs to call functions during conversations. Tools are defined using the Tool class and follow the OpenAI function calling format.

Built-in Tool Creators

All of these factories accept an optional workingDirectory except createSubmitPlanTool, createUpdatePlanTool, createMarkStepTool, createAskUserTool, createWebSearchTool, createWebFetchTool, and createValidateJsonTool. When a directory is set, file and path operations stay inside it and its subdirectories. That check is a string prefix on the factory — it is not an unforgeable capability token. Custom @Tool file handlers should use cap.* (see 19.8).

Every factory in this list is supported by the interpreter and C# transpile (not the JavaScript backend). tool.execute(args) runs the same host implementation as an agent think() call — including createGlobTool, createGrepTool, createReadFileTool, and the other file / git / MALDA factories. Custom new Tool(...) handlers are unchanged.

var globTool = createGlobTool(".");
var found = globTool.execute({ "pattern": "**/*.malda", "dirPath": "." });
print(found.count);

var grepTool = createGrepTool(".");
var hits = grepTool.execute({ "pattern": "function ", "filePath": "Examples" });

Example

var workingDir = ".";
var readTool = createReadFileTool(workingDir);
var writeTool = createWriteFileTool(workingDir);
var globTool = createGlobTool(workingDir);

conversation.addTool(readTool);
conversation.addTool(writeTool);
conversation.addTool(globTool);

conversation.addUserMessage("Read example.txt and summarize it.");
var response = conversation.send();  // LLM will call read_file tool automatically

19.7 Agent Class

The Agent class represents an autonomous agent with a specific role and instructions.

Constructor

// With default local LLM (auto-download from Hugging Face)
var agent = new Agent(name, role, instructions);

// With a custom client
var agent = new Agent(name, role, instructions, client);

Methods

Example

var client = new OpenRouterClient();
var agent = new Agent(
    "CodeMaster",
    "senior software engineer",
    "You write clean, efficient code. Always include comments and error handling.",
    client
);

agent.addTool(createReadFileTool("."));
agent.addTool(createWriteFileTool("."));

var response = agent.think("Read example.txt and add a comment to the add function.");
print(response.content);

19.8 Custom Tools with @Tool Decorator

Governance defaults: Validate tool / LLM-shaped payloads with a schema and validate() before side effects. Mark normalize/derive helpers @pure() and impure handlers @effects(...). Offline golden: Examples/Agents/agent_governance_golden.malda (see also 9.7 Decorators).

MALDA supports defining custom tools for LLMs using the @Tool decorator on functions.

Decorator Syntax

@Tool(name, description, schema?)

Example

@Tool("calculate_sum", "Adds two numbers together")
function calculateSum(a, b) {
    return int(a) + int(b);
}

var agent = new Agent("Assistant", "Helper", "You are helpful", client);
agent.addTool("calculate_sum");

var response = agent.think("What is 15 + 27?");
print(response.content);

Constructing a tool with new Tool

You can also build a Tool instance at runtime and pass a handler function. The usual pattern is a single args parameter (the LLM argument object). Call tool.execute(args) to invoke the handler directly, or agent.addTool(tool) for agent loops.

function echoHandler(args) {
    return "got " + string(args.message);
}

var echoTool = new Tool(
    "echo_message",
    "Echoes a message back",
    {
        "type": "object",
        "properties": {
            "message": { "type": "string", "description": "Text to echo" }
        },
        "required": ["message"]
    },
    echoHandler
);

print(echoTool.execute({ "message": "hello" }));  // got hello
agent.addTool(echoTool);

See Examples/Agents/secondbrain_semantic.malda (find_related_notes) for a GraphMemory-backed example used in ASK when tools are enabled.

Capability tokens for file tools

A custom file tool that takes path: string can still read /etc/passwd. @effects("io") only names the effect. The fix is an unforgeable token: the host mints a workspace root, the model passes a relative path, and cap.confine rejects .. and absolute paths.

Do not put the token in the tool schema. JSON cannot round-trip one — the model would send a dict, and cap.read would throw. Keep the root token in host memory.

io.writeFile("_rm_cap_agent_note.txt", "welcome");
var notesRoot = cap.fileRead(".");

schema NoteArgs {
    relativePath: string;
}

@effects("cap")
function readNoteWith(rootCap, relativePath) {
    var check = validate("NoteArgs", dict { "relativePath": relativePath });
    if (!check.ok) {
        return "invalid: " + check.error;
    }
    var child = cap.confine(rootCap, check.data.relativePath);
    return cap.read(child);
}

io.print(str.trim(readNoteWith(notesRoot, "_rm_cap_agent_note.txt")));
var escaped = false;
try {
    readNoteWith(notesRoot, "../secret.txt");
} catch (e) {
    escaped = true;
}
io.print(escaped);
io.deleteFile("_rm_cap_agent_note.txt");

On a live agent, wrap the helper with @Tool("read_note", "…", "NoteArgs") and call agent.addTool("read_note"). The model only ever sends relativePath. The same host-mint pattern applies to HTTP, MCP, and shell: cap.httpGet + cap.fetch / createWebFetchTool(httpCap), cap.mcpCall + cap.invoke / callTool(token), cap.shell + cap.run / createRunCommandTool(dir, shellCap). Full concept, kinds, and common mistakes: 12.6. Offline starter: malda new agent.

Specialized kinds are tool bundles, not sandboxes

The constructors in 19.9–19.14 register a fixed tool list. They do not take cap.* tokens. There is no removeTool. workingDirectory is forwarded to the file-tool factories as a string prefix — the same check as createReadFileTool(dir), not an unforgeable token. A file-level policy { shell: deny } runs on cap consume; it does not stop CodingAgent's default run_command, which calls runCommand with strings.

ConstructionWhat it confinesWhat it is not
new CodingAgent(..., workDir)File-tool paths under workDir (string prefix). run_command is created without cap.shell.A capability token. policy { } does not gate these string-path tools.
new GitAgent(..., workDir)Git-tool cwd under workDir.cap.*
new DevAgent(..., workDir, …, readOnly: true)Same prefix; readOnly omits write / git / run_command.cap.*. readOnly exists only on DevAgent.
new MALDACodingAgent(..., workDir)Same prefix on file tools plus run_malda / compile_malda.cap.*
new Agent + createRunCommandTool(dir, cap.shell(...)) / createWebFetchTool(httpCap) / @Tool + cap.confineUnforgeable path, host, or argv prefix. policy applies on consume.A preset kind.

Least privilege starts from new Agent and the tools you add. Use a specialized kind when you want that bundle, not when you want a sandbox. Host-mint pattern: capability tokens for file tools and 12.6. Offline starter: malda new agent.

Agent kits

The sections below are specialized tool bundles and a PRD loop, not extra runtime syntax. They sit on Agent + tools (19.6–19.8). Dashboard and teams (19.15–19.18) return to the runtime. Ralph is 19.19.

19.9 CodingAgent

The CodingAgent class is a specialized agent for coding tasks that automatically includes all file operation tools.

Included Tools

CodingAgent automatically includes the following file operation tools and command execution tool:

Constructor

// Simplest form - uses default local LLM (auto-download from Hugging Face)
var agent = new CodingAgent(name, role, instructions);

// With custom working directory
var agent = new CodingAgent(name, role, instructions, workingDirectory);

// With custom client
var agent = new CodingAgent(name, role, instructions, client);

// With custom client and working directory
var agent = new CodingAgent(name, role, instructions, client, workingDirectory);
Not a sandbox. This constructor does not accept cap.*. The bundled tools use the workingDirectory prefix in the table above.

Example

var coderAgent = new CodingAgent(
    "CodeMaster",
    "senior software engineer",
    "You write clean, efficient code."
);

// Agent already has all file tools - ready to use!
var response = coderAgent.think("Read example.txt and add error handling.");
print(response.content);

19.10 GitAgent

The GitAgent class is a specialized agent for git operations that automatically includes all git tools. workingDirectory is a string prefix on those tools, not a capability token — same table.

Included Tools

GitAgent automatically includes the following git operation tools:

Constructor

// Simplest form - uses default local LLM (auto-download from Hugging Face)
var agent = new GitAgent(name, role, instructions);

// With custom working directory
var agent = new GitAgent(name, role, instructions, workingDirectory);

// With custom client
var agent = new GitAgent(name, role, instructions, client);

// With custom client and working directory
var agent = new GitAgent(name, role, instructions, client, workingDirectory);

Example

var gitAgent = new GitAgent(
    "GitHelper",
    "git assistant",
    "You help manage git repositories."
);

// Agent already has all git tools - ready to use!
var response = gitAgent.think("Check git status, stage all changes, and commit with message 'Update code'");
print(response.content);

19.11 HumanAgent

The HumanAgent class is a specialized agent for human interaction that automatically includes the ask_user tool. This makes it easy to create agents that need to interact with humans for clarification, confirmation, or additional information.

Included Tools

HumanAgent automatically includes the following user interaction tool:

Constructor

// Simplest form - uses default local LLM (auto-download from Hugging Face)
var agent = new HumanAgent(name, role, instructions);

// With custom working directory
var agent = new HumanAgent(name, role, instructions, workingDirectory);

// With custom client
var agent = new HumanAgent(name, role, instructions, client);

// With custom client and working directory
var agent = new HumanAgent(name, role, instructions, client, workingDirectory);

Example

// Simplest usage - HumanAgent uses default local LLM
var humanAgent = new HumanAgent(
    "Assistant",
    "helpful assistant",
    "You help users by asking clarifying questions when needed."
);

// Agent already has ask_user tool - ready to use!
var response = humanAgent.think("Ask the user what their favorite programming language is, then provide a brief explanation of that language.");
print(response.content);

Using HumanAgent as a Subagent (Human-in-the-Loop Pattern)

One of the key use cases for HumanAgent is as a subagent in complex multi-agent workflows. This enables human-in-the-loop patterns where orchestrator agents can delegate human interaction tasks to a specialized HumanAgent.

var client = new OpenRouterClient();

// Create a HumanAgent for user interaction
var humanAgent = new HumanAgent(
    "HumanInterface",
    "user interaction specialist",
    "You handle all interactions with human users. " +
    "Ask clear, concise questions when clarification is needed. " +
    "Request approval before making destructive changes.",
    client
);

// Create an orchestrator that can delegate to specialists
var orchestrator = new CodingAgent(
    "Orchestrator",
    "project coordinator",
    "You coordinate code changes. " +
    "When you need user approval or clarification, use the HumanInterface tool. " +
    "Always ask for confirmation before deleting files or making breaking changes.",
    client
);

// Add HumanAgent as a subagent
orchestrator.addSubAgent(
    humanAgent,
    "Handles user interaction, clarification requests, and approval confirmations. " +
    "Use this when you need to ask the user a question or get their approval."
);

// Now orchestrator can automatically request human input when needed
var response = orchestrator.think(
    "Refactor the codebase to use dependency injection. " +
    "Ask the user which design pattern they prefer before proceeding."
);

Benefits of Human-in-the-Loop Pattern:

19.12 DevAgent

The DevAgent class is a specialized agent for full development workflows that automatically includes all file operation tools, git tools, command execution tools, and optional code analysis tools. readOnly: true registers only the read tools (no writes, git, or run_command). That is a smaller bundle, not cap.* — same table.

Included Tools

DevAgent automatically includes the following tools:

Constructor

// Simplest form - uses default local LLM (auto-download from Hugging Face)
var agent = new DevAgent(name, role, instructions);

// With custom working directory
var agent = new DevAgent(name, role, instructions, workingDirectory);

// With custom client
var agent = new DevAgent(name, role, instructions, client);

// With custom client and working directory
var agent = new DevAgent(name, role, instructions, client, workingDirectory);

// With includeSymbols enabled (for code analysis)
var agent = new DevAgent(name, role, instructions, client, workingDirectory, true);

Example

// Simplest usage - DevAgent uses default local LLM
var devAgent = new DevAgent(
    "DevMaster",
    "senior developer",
    "You write, test, compile, and commit code."
);

// Agent already has all development tools - ready to use!
// Can handle full workflow: edit, test, commit
var response = devAgent.think("Read example.txt, add error handling, and commit the changes");
print(response.content);

Example with Code Analysis

// DevAgent with getSymbols enabled for better code understanding
var devAgent = new DevAgent(
    "CodeAnalyzer",
    "code refactoring expert",
    "You analyze code structure and make targeted improvements.",
    null,  // default client
    ".",   // working directory
    true   // includeSymbols - enables get_symbols tool
);

// Agent can now analyze code structure before making edits
var response = devAgent.think("Analyze the codebase structure and refactor the main function");
print(response.content);

Example with Custom Client

var client = new OpenRouterClient("openai/gpt-4");

var devAgent = new DevAgent(
    "FullStackDev",
    "full-stack developer",
    "You develop complete features: code, test, compile, and commit.",
    client,
    "./project",
    false  // includeSymbols
);

// Agent can handle complete development workflow
var response = devAgent.think("Implement a new feature: add a user authentication system");
print(response.content);

19.13 Structured task planning

MALDA supports breaking complex tasks into ordered steps and executing them via an agent. A plan is an object { steps: [{ id, description, dependsOn?, out?, fixture? }, ...], planId?, taskSummary?, think? }. Each step has a unique id (string), description (string), and optional dependsOn (array of step ids that must complete first). Optional out is a schema / sum-type / program(Api) name; optional fixture is offline JSON for that contract (see § 19.17.6).

// Decompose then execute from script
var plan = decomposeTask("Add unit tests for getSymbols and run the test suite");
var agent = new DevAgent("Dev", "developer", "You are a developer.", null, ".");
var result = executePlan(plan, agent);
print(result.completed);
print(result.results);

19.14 MALDACodingAgent

The MALDACodingAgent class is a specialized agent for MALDA script development and toolchain operations that automatically includes all file operation tools and MALDA execution/compilation tools.

Included Tools

MALDACodingAgent automatically includes the following tools:

run_malda runs the interpreter. compile_malda accepts interpreter, transpile (C#), and js. Browser games (game.*, dom.*, three.*) only succeed with mode js. That check writes JavaScript; it does not start malda play.

Constructor

// Simplest form - uses default local LLM (auto-download from Hugging Face)
var agent = new MALDACodingAgent(name, role, instructions);

// With custom working directory
var agent = new MALDACodingAgent(name, role, instructions, workingDirectory);

// With custom client
var agent = new MALDACodingAgent(name, role, instructions, client);

// With custom client and working directory
var agent = new MALDACodingAgent(name, role, instructions, client, workingDirectory);

Example

// Simplest usage - MALDACodingAgent uses default local LLM
var maldaAgent = new MALDACodingAgent(
    "MALDADeveloper",
    "MALDA script developer",
    "You write, test, and compile MALDA scripts. Always verify your changes work by running the code."
);

// Agent already has all MALDA development tools - ready to use!
// Can handle MALDA workflow: edit, run, compile, test
var response = maldaAgent.think("Read example.malda, add error handling, test it with run_malda, and compile it");
print(response.content);

Example with Custom Client

var client = new OpenRouterClient("openai/gpt-4");

var maldaAgent = new MALDACodingAgent(
    "MALDAMaster",
    "MALDA expert",
    "You develop MALDA scripts with best practices and thorough testing.",
    client
);

// Agent can handle MALDA development workflow
var response = maldaAgent.think("Create a new MALDA script that calculates fibonacci numbers, test it, and compile it");

Example with Working Directory

var client = new OpenRouterClient("openai/gpt-4");

var maldaAgent = new MALDACodingAgent(
    "MALDAHelper",
    "MALDA assistant",
    "You help develop and test MALDA scripts.",
    client,
    "./malda-scripts"  // File tools stay under this directory (string prefix, not a cap token)
);

// File tool paths stay under ./malda-scripts (prefix check)
var response = maldaAgent.think("List all MALDA files and improve error handling in each one");

When to Use MALDACodingAgent vs DevAgent vs CodingAgent

19.15 Agent Dashboard

MALDA agents automatically report their activities to a central dashboard without requiring any modifications to MALDA scripts. This enables monitoring and observability across all agents, even when they run in different executables or processes.

Overview

The agent dashboard feature provides:

Configuration

The dashboard URL is configured via the SPL_AGENT_DASHBOARD_URL environment variable:

# Set custom dashboard URL
export SPL_AGENT_DASHBOARD_URL="http://my-dashboard.example.com/api/agent/status"

# If not set, defaults to:
# http://localhost:8080/api/agent/status

Note: If the environment variable is set but doesn't end with /api/agent/status, the endpoint path is automatically appended.

Reported Events

Agents automatically report the following events:

  1. Agent Created (agent_created)
    • Reported when an agent is initialized
    • Includes: agent name, role, process ID, timestamp
  2. Think Operation (think)
    • Reported when agent.think() is called
    • Includes: agent name, prompt (truncated to 500 chars), prompt length, process ID, timestamp
  3. Tool Call (tool_call)
    • Reported when tools are executed
    • Includes: agent name, tool name, success status, error message (if failed), process ID, timestamp
  4. Agent Reset (agent_reset)
    • Reported when agent.reset() is called
    • Includes: agent name, process ID, timestamp

Dashboard Payload Format

All events are sent as HTTP POST requests with JSON payloads:

{
  "agentId": "MyAgent",
  "processId": 12345,
  "eventType": "think",
  "timestamp": "2026-01-15T10:30:00.123Z",
  "data": {
    "prompt": "Analyze this code...",
    "promptLength": 20
  }
}

Payload Fields:

Event-Specific Data:

Dashboard Endpoint Requirements

The dashboard server should implement a POST endpoint that accepts JSON payloads:

Example Dashboard Server (MALDA)

var server = new HttpServer(8080);
var agentStatuses = {};

@POST("/api/agent/status")
function receiveAgentStatus(body) {
    var agentId = body.agentId;
    var processId = body.processId;
    var eventType = body.eventType;
    var timestamp = body.timestamp;
    var data = body.data;
    
    // Store or process agent status
    if (agentStatuses[agentId] == null) {
        agentStatuses[agentId] = {};
    }
    agentStatuses[agentId][eventType] = {
        "processId": processId,
        "timestamp": timestamp,
        "data": data
    };
    
    return {"status": 200, "message": "Status received"};
}

@GET("/api/agents")
function getAllAgents() {
    return {"status": 200, "agents": agentStatuses};
}

@PAGE("/")
function dashboardPage() {
    // Generate HTML dashboard showing all agents
    return generateDashboardHTML(agentStatuses);
}

server.start();
print("Agent Dashboard running at http://localhost:8080");

Usage

No MALDA script Changes Required!

Agents automatically report to the dashboard. Simply create agents as usual:

var client = new OpenRouterClient();

var agent = new Agent(
    "MyAgent",
    "coder",
    "You write code.",
    client
);

// Agent automatically reports:
// 1. agent_created event when initialized
// 2. think event when think() is called
// 3. tool_call events when tools are executed
// 4. agent_reset event when reset() is called

var response = agent.think("Write a hello world function");
// Dashboard receives think and tool_call events automatically

Error Handling

The dashboard reporting system is designed to be completely non-intrusive:

Benefits

19.16 Multi-Agent Orchestration

Multiple agents can work together, each with specialized roles and tools.

Parallel Agent Workflow

Agents can work independently on different tasks:

var readerAgent = new Agent("Reader", "file reader", "You read and analyze files.", client);
readerAgent.addTool(createReadFileTool("."));

var editorAgent = new Agent("Editor", "code editor", "You edit code files.", client);
editorAgent.addTool(createReadFileTool("."));
editorAgent.addTool(createReplaceInFileTool("."));

var code = readerAgent.think("Read main.MALDA and summarize it.");
var edited = editorAgent.think("Read main.MALDA and add error handling.");

Hierarchical Agent Systems with addSubAgent

The addSubAgent method allows you to create hierarchical agent systems where an orchestrator agent can delegate tasks to specialized subagents. This is useful when you want to separate detailed internal instructions from the tool description shown to the orchestrator.

Key Benefit: addSubAgent uses a simplified, tool-focused description rather than the agent's full internal instructions, keeping the orchestrator's context clean and focused.

Syntax

orchestrator.addSubAgent(subAgent, toolDescription);

Example

var client = new OpenRouterClient();

// Create specialized subagents with detailed internal instructions
var codeReviewer = new Agent(
    "CodeReviewer",
    "code review specialist",
    "You are an expert code reviewer. When reviewing code, you should:\n" +
    "1. Check for bugs and logical errors\n" +
    "2. Review code style and best practices\n" +
    "3. Suggest improvements\n" +
    "4. Consider performance implications\n" +
    "5. Check for security vulnerabilities\n" +
    "Provide detailed feedback with line numbers and specific suggestions.",
    client
);

var tester = new Agent(
    "Tester",
    "testing specialist",
    "You are an expert in software testing. When asked to write tests:\n" +
    "1. Analyze the code to understand what needs to be tested\n" +
    "2. Write comprehensive test cases covering edge cases\n" +
    "3. Include both positive and negative test cases\n" +
    "4. Ensure tests are clear and maintainable\n" +
    "5. Consider boundary conditions and error handling",
    client
);

// Create orchestrator agent
var orchestrator = new Agent(
    "Orchestrator",
    "project coordinator",
    "You coordinate tasks between specialized agents. " +
    "When you need code reviewed, use the CodeReviewer tool. " +
    "When you need tests written, use the Tester tool. " +
    "Delegate work to the appropriate specialist agent.",
    client
);

// Add subagents with simplified, tool-focused descriptions
orchestrator.addSubAgent(
    codeReviewer,
    "Reviews code for bugs, style issues, and improvements. Provide the code to review as the prompt."
);

orchestrator.addSubAgent(
    tester,
    "Writes comprehensive test cases for code. Provide the code to test as the prompt."
);

// Orchestrator can now delegate to subagents
var code = "function add(a, b) {\n    return a + b;\n}";
var response = orchestrator.think(
    "I have this code: " + code + "\n\n" +
    "Please have it reviewed and then write tests for it."
);
print(response.content);

When the orchestrator calls a subagent tool, it passes a prompt parameter, and the subagent's think() method is called automatically. The subagent's response is returned to the orchestrator.

When several roles share a job and you want the legal hops as data, use a declarative team — § 19.17.

See Also

19.17 Declarative Agent Teams

Multi-agent teams are data, not a keyword. agents.define(spec, client?) builds one agent from a spec object. agents.team(specs, topology, client?) binds an array of those specs to a graph directed. The graph is an allow-list of hops: a dependsOn edge between different roles must match a declared relation. There is no flat agents() / define() / team() alias. JavaScript: n/a (host-only).

When to use a team instead of addSubAgent:

19.17.1 agents.define

agents.define(spec, client?) returns an agent. Required spec fields: name, role, instructions. Optional fields:

FieldApplies toMeaning
kindallClass to construct. Default Agent.
toolsallArray of tool names added after construction.
memoryScopeallPassed to setMemoryScope.
workingDirectoryspecialized kinds onlySame as the matching constructor: a string prefix on bundled file tools, not a cap.* token. Illegal on plain Agent.
includeSymbols / readOnly / prdAuthorOnlyDevAgent onlySame as new DevAgent(...).

Allowed kind values use the same tool bundles as the matching constructors — not sandboxes (19.8). Read the class tag on agent.kind.

kindConstructor twin
Agent (default)new Agent
CodingAgentnew CodingAgent
GitAgentnew GitAgent
HumanAgentnew HumanAgent
DevAgentnew DevAgent
MALDACodingAgentnew MALDACodingAgent

Unknown kinds fail before any LLM call. You may declare schema AgentSpec { name: string; role: string; instructions: string; } and validate("AgentSpec", spec) yourself; if that schema is registered, define / team run the same check.

var coder = agents.define({
    name: "Coder",
    kind: "CodingAgent",
    role: "programmer",
    instructions: "Write small diffs.",
    workingDirectory: "."
});
print(coder.kind);

var human = agents.define({
    name: "Human",
    kind: "HumanAgent",
    role: "approver",
    instructions: "Approve or reject."
});
print(human.kind);

var threw = false;
try {
    agents.define({ name: "X", role: "r", instructions: "i", kind: "Wizard" });
} catch (e) {
    threw = true;
}
print(threw);

19.17.2 agents.team

agents.team(specs, topology, client?) takes a non-empty array of specs and a directed graph. Every graph node must match a spec name. Duplicate names fail. Extra keys on a literal edge besides from / to / weight / properties become edge properties, so rel and contract are first-class (see 15. Graphs). If rel is omitted it defaults to handoff. Unknown nodes or rels fail before any LLM call. The optional client is forwarded to each define.

19.17.3 Relations

Allowed rel values:

relHost hopNotes
handoffteam.handoffTransfer work along the edge. Default when rel is omitted.
reviewteam.reviewReturns approved (default true).
rejectteam.rejectReturns rejected (default true). Used by executePlan when the predecessor is not approved.
consultteam.consultOne-shot ask. Does not transfer the plan and does not set a verdict.
delegate—No typed hop method. Installs addSubAgent on the source.

delegate and consult edges also install addSubAgent on the source (supervisor tool list = out-neighbors). A handoff edge is not enough for review / reject / consult — hops are typed.

19.17.4 Inspecting a team

schema DraftCode { path: string; summary: string; }

var specs = [
    { name: "Writer", role: "programmer", instructions: "Write small, tested diffs." },
    { name: "Reviewer", role: "reviewer", instructions: "Find bugs. Do not edit files." }
];

var topology = graph directed {
    nodes: ["Writer", "Reviewer"],
    edges: [
        { from: "Writer", to: "Reviewer", rel: "handoff", contract: "DraftCode" }
    ]
};

var team = agents.team(specs, topology);
print(team.members().length);
print(team.get("Writer").role);
print(team.relations()[0].rel);

var ok = team.handoff("Writer", "Reviewer", { path: "calc.malda", summary: "add comments" });
print(ok.ok);

var bad = team.handoff("Writer", "Reviewer", { path: 1, summary: "nope" });
print(bad.ok);

var missing = team.handoff("Reviewer", "Writer", { path: "calc.malda", summary: "back" });
print(missing.ok);

19.17.5 Typed hops

team.handoff / team.review / team.reject / team.consult share the same contract: (from, to, payload, think?). The edge must exist with a matching rel. When contract is set, the hop runs validate(contract, payload) first. Failed hops return { ok: false, error } and do not call think().

The fourth argument is optional:

Return shapes:

MethodSuccess
handoff / consult{ ok, data } or { ok, data, response } when think ran
review{ ok, approved, data } (approved defaults true)
reject{ ok, rejected, data } (rejected defaults true)

Consult is a one-shot ask: it does not transfer the plan and does not set approved / rejected.

schema DraftCode { path: string; summary: string; }

var reviewTeam = agents.team([
    { name: "Writer", role: "programmer", instructions: "Write small diffs." },
    { name: "Reviewer", role: "reviewer", instructions: "Find bugs." }
], graph directed {
    nodes: ["Writer", "Reviewer"],
    edges: [
        { from: "Writer", to: "Reviewer", rel: "review", contract: "DraftCode" },
        { from: "Reviewer", to: "Writer", rel: "reject", contract: "DraftCode" }
    ]
});

var hopOff = reviewTeam.review("Writer", "Reviewer", { path: "calc.malda", summary: "add comments" }, false);
print(hopOff.ok);
print(hopOff.response == null);

var reviewed = reviewTeam.review("Writer", "Reviewer", { path: "calc.malda", summary: "add comments" });
print(reviewed.approved);

var no = reviewTeam.review("Writer", "Reviewer", { path: "calc.malda", summary: "add comments" }, { approved: false });
print(no.approved);

var sentBack = reviewTeam.reject("Reviewer", "Writer", { path: "calc.malda", summary: "needs tests" });
print(sentBack.rejected);
schema Ask { question: string; }

var consultTeam = agents.team([
    { name: "Writer", role: "programmer", instructions: "Write small diffs." },
    { name: "Researcher", role: "researcher", instructions: "Look things up." }
], graph directed {
    nodes: ["Writer", "Researcher"],
    edges: [{ from: "Writer", to: "Researcher", rel: "consult", contract: "Ask" }]
});

var asked = consultTeam.consult("Writer", "Researcher", { question: "which API?" });
print(asked.ok);
print(asked.data.question);

var bad = consultTeam.consult("Researcher", "Writer", { question: "nope" });
print(bad.ok);

var wrong = consultTeam.handoff("Writer", "Researcher", { question: "which API?" });
print(wrong.ok);

19.17.6 executePlan with a team

executePlan(plan, team) or team.run(plan) routes each step to step.role (or step.agent). Host-driven team plans do not pick a default member — a missing or unknown role returns { error }.

After topo-sort, a dependsOn hop between different roles must match a declared relation. The check runs before think(), so an illegal hop returns { error } with no LLM call. Same-role continuation does not need a self-edge.

A reject hop runs only when the predecessor is not approved. Every other hop runs only when it is approved. The verdict comes from, in order: step.approved / step.rejected, else JSON { approved } / { rejected } in the think output, else default approved: true. Skipped steps are listed in skipped (step ids) and recorded on the step result as skipped + reason; they do not call think(). Failed or skipped dependencies cascade.

Set plan.think: false to skip think() entirely (offline; the step output is the description unless out / fixture is set). When think() does run, the prompt is the description plus a Prior step <id>: block for every dependsOn that already ran. Each step result includes prompt, approved, and, for a role hop, the incoming rel. The top-level result is { planId, completed, failed, skipped, results } or { error }.

step.out names a schema, a sum type, or program(Api). The host extracts JSON from think() (or from step.fixture — a dict or an LLM-shaped JSON string) and coerces it through the same path as await prompt … -> Type / evalPrompt. Success sets results[i].data (a variant for sum types) and JSON on output so the next step sees structured prior text. Failed extract marks the step success: false, lists it in failed, and skips dependents. An unknown out fails the whole plan before any think(). A fixture skips think() for that step even when plan.think is true. With plan.think: false and out but no fixture, the step fails (no fixture).

var team = agents.team([
    { name: "Writer", role: "programmer", instructions: "Write small diffs." },
    { name: "Reviewer", role: "reviewer", instructions: "Find bugs." }
], graph directed {
    nodes: ["Writer", "Reviewer"],
    edges: [{ from: "Writer", to: "Reviewer", rel: "handoff" }]
});

var blocked = executePlan({
    steps: [
        { id: "review", description: "Review first", role: "Reviewer" },
        { id: "write", description: "Write after", role: "Writer", dependsOn: ["review"] }
    ]
}, team);
print(blocked.error != null);
var reviewTeam = agents.team([
    { name: "Writer", role: "programmer", instructions: "Write small diffs." },
    { name: "Reviewer", role: "reviewer", instructions: "Find bugs." }
], graph directed {
    nodes: ["Writer", "Reviewer"],
    edges: [
        { from: "Writer", to: "Reviewer", rel: "review" },
        { from: "Reviewer", to: "Writer", rel: "reject" }
    ]
});

var rejected = executePlan({
    think: false,
    steps: [
        { id: "write", description: "Write comments", role: "Writer" },
        { id: "review", description: "Review the draft", role: "Reviewer", dependsOn: ["write"], approved: false },
        { id: "rewrite", description: "Fix the draft", role: "Writer", dependsOn: ["review"] }
    ]
}, reviewTeam);
print(rejected.completed.length);
print(rejected.skipped.length);

var approved = executePlan({
    think: false,
    steps: [
        { id: "write", description: "Write comments", role: "Writer" },
        { id: "review", description: "Review the draft", role: "Reviewer", dependsOn: ["write"] },
        { id: "rewrite", description: "Fix the draft", role: "Writer", dependsOn: ["review"] }
    ]
}, reviewTeam);
print(approved.completed.length);
print(approved.skipped[0]);
schema DraftCode { path: string; summary: string; }

var outTeam = agents.team([
    { name: "Writer", role: "programmer", instructions: "Write small diffs." },
    { name: "Reviewer", role: "reviewer", instructions: "Find bugs." }
], graph directed {
    nodes: ["Writer", "Reviewer"],
    edges: [{ from: "Writer", to: "Reviewer", rel: "handoff" }]
});

var typed = executePlan({
    think: false,
    steps: [
        { id: "write", description: "Write comments", role: "Writer", out: "DraftCode", fixture: { path: "a.malda", summary: "add comments" } },
        { id: "review", description: "Review the draft", role: "Reviewer", dependsOn: ["write"] }
    ]
}, outTeam);
print(typed.completed.length);
print(typed.results[0].data.path);

var badOut = executePlan({
    think: false,
    steps: [
        { id: "write", description: "Write comments", role: "Writer", out: "DraftCode", fixture: { path: 1, summary: "nope" } },
        { id: "review", description: "Review the draft", role: "Reviewer", dependsOn: ["write"] }
    ]
}, outTeam);
print(badOut.failed[0]);
print(badOut.skipped[0]);

19.17.7 decomposeTask with a team

decomposeTask(instruction, client?, team?) or team.decompose(instruction, client?) puts member names, kinds, roles, instructions, and relations into the decomposer prompt. Each generated step must set role to a member name. Plans that omit role or hop off-graph are rejected. The second argument may be an LLM client or the team (when the team is passed as the second argument, the default local LLM is used for the decompose call itself).

This path calls an LLM. Prefer a host-written plan plus plan.think: false when you need an offline golden.

19.17.8 Offline goldens

These examples make no LLM call:

Few-shots for agents: docs/llm/few-shot/34_agents_team.malda through 41_agents_plan_out.malda. Static team topology (literal graphs / plans) is also reported as malda-agents by malda check — see 24.10.

See Also

19.18 Using Agents with Actors

Agents and actors are complementary features in MALDA. While agents are classes designed for synchronous AI interactions, actors provide concurrent, message-based processing. Combining them enables powerful patterns for distributed AI systems.

Design Decision: Agents are implemented as classes rather than native actors because:

Agent as Actor State

The most common pattern is storing an agent instance as a field in an actor. This allows the actor to use AI capabilities while maintaining isolated state and concurrent processing:

actor AIAssistant {
    var agent;
    var conversationHistory = [];
    
    function AIAssistant(client) {
        // Create an agent and store it in actor state
        agent = new Agent(
            "Assistant",
            "helpful assistant",
            "You help users with their questions.",
            client
        );
    }
    
    on ask(question) {
        // Use the agent to process questions
        var response = agent.think(question);
        append(conversationHistory, question);
        append(conversationHistory, response.content);
        print(response.content);
    }
    
    on getHistory() {
        print(conversationHistory);
    }
}

// Spawn multiple AI assistants with isolated state
var client = new OpenRouterClient();
var assistant1 = spawn AIAssistant(client);
var assistant2 = spawn AIAssistant(client);

send assistant1.ask("What is 2+2?");
send assistant2.ask("What is the weather?");

Concurrent Agent Orchestration

Actors enable true concurrent processing of multiple agents. While agents are synchronous, wrapping them in actors allows parallel execution:

actor CodingAgentActor {
    var agent;
    var taskQueue = [];
    
    function CodingAgentActor(client, workingDir) {
        agent = new CodingAgent(
            "Coder",
            "software engineer",
            "You write clean, efficient code.",
            client,
            workingDir
        );
    }
    
    on processTask(task) {
        var result = agent.think(task);
        print($"Task completed: {result.content}");
    }
}

// Spawn multiple coding agents for parallel processing
var client = new OpenRouterClient();
var agent1 = spawn CodingAgentActor(client, "./project1");
var agent2 = spawn CodingAgentActor(client, "./project2");
var agent3 = spawn CodingAgentActor(client, "./project3");

// Send tasks concurrently - all process in parallel
send agent1.processTask("Refactor the user authentication module");
send agent2.processTask("Add error handling to the API");
send agent3.processTask("Write unit tests for the database layer");

Multi-Agent Actor System

You can orchestrate multiple specialized agents within a single actor to create complex workflows:

actor AgentOrchestrator {
    var coderAgent;
    var reviewerAgent;
    var testerAgent;
    
    function AgentOrchestrator(client) {
        coderAgent = new CodingAgent("Coder", "developer", "Write clean code.", client);
        reviewerAgent = new Agent("Reviewer", "code reviewer", "Review code for quality.", client);
        testerAgent = new Agent("Tester", "QA engineer", "Write comprehensive tests.", client);
    }
    
    on develop(feature) {
        // Step 1: Code
        var code = coderAgent.think($"Write code for: {feature}");
        
        // Step 2: Review
        var review = reviewerAgent.think($"Review this code: {code.content}");
        
        // Step 3: Test
        var tests = testerAgent.think($"Write tests for: {code.content}");
        
        return {
            "code": code.content,
            "review": review.content,
            "tests": tests.content
        };
    }
}

var client = new OpenRouterClient();
var orchestrator = spawn AgentOrchestrator(client);
send orchestrator.develop("User authentication system");

Agent State Persistence via Actors

Actors can manage and persist agent conversation state, enabling long-running agent sessions:

actor AgentStateManager {
    var agents = {};  // Dictionary of agent instances
    var conversations = {};  // Conversation history per agent
    
    on createAgent(name, role, instructions, client) {
        var agent = new Agent(name, role, instructions, client);
        agents[name] = agent;
        conversations[name] = [];
    }
    
    on queryAgent(agentName, question) {
        if (has(agents, agentName)) {
            var agent = agents[agentName];
            var response = agent.think(question);
            append(conversations[agentName], {
                "question": question,
                "answer": response.content
            });
            return response.content;
        }
        return "Agent not found";
    }
    
    on getHistory(agentName) {
        return conversations[agentName];
    }
}

var client = new OpenRouterClient();
var stateManager = spawn AgentStateManager();
send stateManager.createAgent("Helper", "assistant", "You help users", client);

send stateManager.queryAgent("Helper", "What is MALDA?");

Benefits of Combining Agents and Actors

See Also

19.19 Ralph Wiggum (PRD-driven autonomous loop)

The repository includes Examples/RalphWiggum/RalphWiggum.malda, a production-ready autonomous loop for PRD-driven development. It uses DevAgent, a checklist PRD (PRD.md), post-iteration validation, resume state, and GraphMemory for continuity across iterations when conversation history is reset each round. See Examples/RalphWiggum/README.md for prerequisites and the Snake demo.

Running

dotnet run --project MaldaLang -- Examples/RalphWiggum/RalphWiggum.malda
# Or from a project directory with PRD.md:
# set MALDA_RALPH_WORKDIR=Examples/RalphWiggum/snake-demo
# Examples\RalphWiggum\snake-demo\run-ralph.bat

Persisted artifacts (in workdir)

Memory model

Ralph attaches shared GraphMemory to the agent via setupRalphGraphMemory() and:

Inspect Ralph memory from the workdir: malda memory stats --path .ralph-memory. See Examples/RalphWiggum/README.md for MALDA_RALPH_MEMORY_* variables.

Key environment variables

Implementation modules: Examples/RalphWiggum/ralph/*.malda (see ralph/ARCHITECTURE.md). Full env var list and git-worktree workflow: Examples/RalphWiggum/README.md.

Completion signals

Ralph accepts TASK_COMPLETE (case-insensitive), a dedicated RALPH_DONE line, JSON {"ralph":"done"}, or auto-complete when every PRD item is [DONE] and validation passes (unless MALDA_RALPH_REQUIRE_SIGNAL=true).

Custom validation hooks

After structural checks, Ralph may run MALDA_RALPH_VALIDATE_CMD, or a project-local .ralph-validate.sh / .ralph-validate.bat / (or .ralph-validate.malda) in the workdir. .malda files are diagnosed with checkMalda (same report as malda check --json), not syntax-only getParseErrors. Loop prompts tell the agent to call check_malda / test_malda during the turn and not to use submit_plan / update_plan / mark_step.

Execution strategy

Ralph prompts the agent to complete one PRD checklist item per iteration in as few LLM rounds as possible: batch read-only exploration (list_directory, grep, read_file) in one response, then batch edits and verification. This pairs with parallel read-only tool execution (19.5) and streamed [think] feedback during verbose runs.

See also 20. GraphMemory for remember metadata, getRecent, and hybrid query options.

reportRalphStatus(agentName, phase, iteration, maxIter, prdPercent, validationOk?, elapsedMs?, promptTokens?, completionTokens?, costUsd?) publishes one iteration of progress to the agent dashboard. The first five arguments are required; the rest default to false, 0, 0, 0, and 0.0.

reportRalphStatus("Ralph", "implement", 3, 10, 45, true, 12000, 1800, 640, 0.02);