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.
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
- Runtime — 19.1 risks · 19.2–19.5 clients · 19.6 tools · 19.7
Agent· 19.8@Tool· 19.15 dashboard · 19.16–19.18 teams and actors - Agent kits — 19.9–19.14
CodingAgent/GitAgent/HumanAgent/DevAgent/MALDACodingAgent· 19.19 Ralph Wiggum. Same files:Examples/Agents/,Examples/RalphWiggum/
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
- Unintended Actions: Agents may execute tools in ways you did not anticipate, leading to data loss, system corruption, or security breaches.
- Tool Misuse: Agents can combine tools in unexpected ways, potentially bypassing intended safeguards or restrictions.
- LLM Hallucination: Language models may misinterpret instructions, generate incorrect tool parameters, or make decisions based on flawed reasoning.
- Autonomous Execution: Once started, agents may perform many actions without human oversight, amplifying the impact of any single error.
- Context Limitations: Agents operate with limited context and may not understand the full implications of their actions.
- Prompt Injection: Malicious or malformed input can cause agents to behave in unintended ways, potentially executing dangerous operations.
Real-World Risk Scenarios
1. Data Loss and File System Damage
- Scenario: An agent tasked with "cleaning up old files" misinterprets the scope and deletes critical system files, configuration files, or entire project directories.
- Example: Agent receives instruction to "remove temporary files" but deletes
node_modules,.git, or source code files, causing irreversible data loss. - Impact: Project corruption, loss of work, system instability, inability to recover without backups.
2. Unauthorized Code Execution
- Scenario: An agent with
run_commandtool access executes malicious or destructive shell commands. - Example: Agent interprets a user request to "optimize the system" and runs
rm -rf /(or equivalent),format C:, or other destructive commands. - Impact: Complete system destruction, data loss, security compromise, unauthorized access to other systems.
3. Git Repository Corruption
- Scenario: A GitAgent with push permissions accidentally force-pushes to main/master, overwriting production code or deleting branches.
- Example: Agent attempts to "fix merge conflicts" by force-pushing, destroying commit history and causing team-wide disruption.
- Impact: Loss of version history, broken deployments, team productivity loss, potential data loss if backups are insufficient.
4. Unauthorized Access and Information Disclosure
- Scenario: An agent with file read access reads sensitive files (API keys, passwords, personal data) and includes them in responses or logs.
- Example: Agent reads
.envfiles, configuration files containing secrets, or private user data and exposes them in conversation history or error messages. - Impact: Security breach, privacy violations, regulatory compliance issues, financial liability.
5. Infinite Loops and Resource Exhaustion
- Scenario: An agent enters an infinite loop of tool calls, consuming system resources indefinitely.
- Example: Agent repeatedly reads and writes the same file, creates recursive directory structures, or makes endless API calls, exhausting CPU, memory, or API quotas.
- Impact: System crashes, service unavailability, unexpected costs (API usage), denial of service.
6. Cascading Failures in Multi-Agent Systems
- Scenario: One agent's error triggers a chain reaction of failures across multiple agents in a hierarchical system.
- Example: An orchestrator agent delegates a task that causes a subagent to corrupt files, which then causes other agents to fail, leading to widespread system damage.
- Impact: Amplified damage, difficult-to-trace root causes, complex recovery scenarios.
7. Production System Modification
- Scenario: An agent intended for development environments is accidentally run against production systems.
- Example: Agent modifies production database configurations, changes live API endpoints, or updates critical infrastructure code without proper testing.
- Impact: Service outages, data corruption, customer impact, financial losses, reputation damage.
8. Malicious Code Injection
- Scenario: An agent writes code that includes malicious payloads, backdoors, or security vulnerabilities.
- Example: Agent generates code that includes hardcoded credentials, executes arbitrary commands from user input, or introduces SQL injection vulnerabilities.
- Impact: Security vulnerabilities, unauthorized access, data breaches, compliance violations.
Mitigation Strategies
- Sandboxing: Always run agents in isolated environments with restricted file system access and network permissions.
- Working Directory Restrictions: Pass
workingDirectoryso bundled file tools stay under a folder. That check is a string prefix, not a capability token. - Human-in-the-Loop: Require explicit approval for destructive operations (deletes, git pushes, command execution).
- Tool Filtering: Only provide agents with the minimum set of tools necessary for their specific task.
- Input Validation: Validate and sanitize all user inputs before passing them to agents.
- Rate Limiting: Implement limits on tool call frequency and total operations per agent session.
- Monitoring and Logging: Log all tool calls and agent decisions for audit trails and debugging.
- Backup and Recovery: Maintain regular backups and test recovery procedures before deploying agents.
- Testing: Thoroughly test agents in safe, isolated environments before production use.
- Principle of Least Privilege: Grant agents only the minimum permissions required for their tasks.
- Timeouts: Set maximum execution times to prevent infinite loops and resource exhaustion.
- Code Review: Review all code generated by agents before deployment, especially for security-sensitive operations.
Best Practices
- Start Small: Begin with read-only operations and gradually add write capabilities only after thorough testing.
- Least privilege: Specialized kinds (
CodingAgent,GitAgent,DevAgent,MALDACodingAgent) are tool bundles, not sandboxes. A bareAgentplus the tools you add is narrower. For unforgeable path / host / argv limits, host-mintcap.*— see specialized kinds are not sandboxes. - Explicit Instructions: Provide clear, specific instructions to agents and avoid ambiguous or overly broad directives.
- Version Control: Always use version control and never allow agents to force-push or modify protected branches without explicit approval.
- Environment Separation: Maintain strict separation between development, staging, and production environments.
- Regular Audits: Periodically review agent behavior, tool usage patterns, and system changes.
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
chat(messages, tools)→object: Send conversation to LLM with optional tool definitionscomplete(prompt)→string: Send single prompt and get completionsetTemperature(temp): Set temperature parameter (0.0 to 2.0)setMaxTokens(tokens): Set maximum tokens in response
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");
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
addUserMessage(content): Add a user messageaddAssistantMessage(content): Add an assistant messageaddTool(tool): Add a tool definition for function callingsend()→object: Send conversation and get response (handles tool calls automatically)getMessages()→array: Get all messagesclear(): Clear conversation history
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:
[llm] round N— each LLM request/response round (tool-call summary or final response size)[think]— model-native reasoning or planning text before tool calls[tool]— tool executions (compact by default)
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).
| Variable | Default | Purpose |
|---|---|---|
MALDA_AGENT_VERBOSE | false (Ralph: true) | Enable [llm] / [tool] logging |
MALDA_AGENT_STATUS | on for a terminal | Rewrite 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_RICH | true | Colored Spectre.Console output (auto-off when redirected) |
MALDA_AGENT_TOOL_DETAIL | compact | compact or full tool logs |
MALDA_AGENT_LLM_THINKING | compact | [think] output: off, compact, or full |
MALDA_AGENT_LLM_STREAM | true | OpenAI SSE streaming for HTTP clients; live [think] when thinking is enabled |
MALDA_AGENT_LLM_PREVIEW | compact | Final-response preview when streaming/thinking is off |
MALDA_AGENT_STATUS_EVERY | 4 | Repeat 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(enabled?)— turns verbose conversation logging on or off. Called with no argument it enables logging and returnstrue.setAgentVerbosePhase(phase)— labels the phase shown alongside verbose output, for example"planning"or"review".setAgentStatusBanner(banner)— sets the banner text displayed above agent progress output.
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
createReadFileTool(workingDirectory?)→ToolcreateWriteFileTool(workingDirectory?)→ToolcreateReplaceInFileTool(workingDirectory?)→Tool- Single replacementcreateEditFileTool(workingDirectory?)→Tool- Multiple replacementscreateInsertAtLineTool(workingDirectory?)→ToolcreateGrepTool(workingDirectory?)→ToolcreateListDirectoryTool(workingDirectory?)→ToolcreateDeleteFileTool(workingDirectory?)→Tool- Delete a file. Parameters:filePath. Returns{ success, error? }.createCopyFileTool(workingDirectory?)→Tool- Copy a file. Parameters:srcPath,destPath. Both paths must stay inside the working directory. Returns{ success, error? }.createEnsureDirTool(workingDirectory?)→Tool- Create a directory and parents. Parameters:dirPath. Returns{ success, path?, error? }.createAskUserTool()→ToolcreateGitStatusTool(workingDirectory?)→ToolcreateGitAddTool(workingDirectory?)→ToolcreateGitCommitTool(workingDirectory?)→ToolcreateGitLogTool(workingDirectory?)→ToolcreateGitDiffTool(workingDirectory?)→ToolcreateGitBranchTool(workingDirectory?)→ToolcreateGitCheckoutTool(workingDirectory?)→ToolcreateGitPushTool(workingDirectory?)→ToolcreateGitPullTool(workingDirectory?)→ToolcreateRunCommandTool(workingDirectory?, shellCap?)→Tool- Execute shell commands (e.g.,dotnet,npm,python) with safety checks. Pass acap.shelltoken so the model cannot change the executable. Parameters:command(string),args(array of strings, optional),workingDirectory(optional),timeoutMs(optional). Returns object withexitCode,stdout, andstderr.createRunMALDATool(workingDirectory?)→Tool- Execute MALDA code from a file path or source string. Parameters:sourceOrFilePath(string),input(optional string for stdin). Returns object withsuccess(boolean),output(string),error(string for parse errors), and optionalruntimeError(string).createCompileMALDATool(workingDirectory?)→Tool- Compile MALDA source code to executable. Parameters:sourcePath(string),outputPath(optional string),mode(optional:"interpreter"|"transpile"). Returns object withsuccess(boolean),outputPath(string or null),error(string), anderrors(array of error objects).createGetSymbolsTool(workingDirectory?)→Tool- Extract classes, functions, actors, and prompts from a filecreateGetParseErrorsTool(workingDirectory?)→Tool- Syntax-only parse check (does not run type/schema diagnostics). Prefercheck_maldafor the diagnose loop.createCheckMaldaTool(workingDirectory?)→Tool- Diagnose MALDA with the same LanguageService diagnostics asmalda check --json(parse, types, schema). This is the generate → diagnose → patch loop — notget_parse_errors(syntax-only) and notrun_command+malda check. Parameters:sourceOrFilePath(file path or inline source),typeMode?(default|strict|lenient). Returns{ ok, executed, file, errorCount, warningCount, infoCount, error?, diagnostics }. Parallel-safe; does not execute the program.createValidateJsonTool()→Tool- Validate a value against a registered schema or sum-type name, or an inline schema object (same asvalidate(schema, value)). Parameters:schema(string or object),value(any). Returns{ ok, data }or{ ok: false, error }. No working directory. Parallel-safe.createTestMaldaTool(workingDirectory?)→Tool- Run MALDA tests viaTestCommandRunner(same host path asmalda test --format ci). Parameters:path?(file or directory, default.relative to the working directory),filter?,listOnly?. Confined to the working directory. Returns{ success, exitCode, output, report? }. Not parallel-safe.createWebSearchTool()→Tool- Brave Search API (no working directory)createWebFetchTool(httpCap?)→Tool- Fetch anhttp://orhttps://URL (same host path ashttpGet; no working directory). Pass acap.httpGettoken so the model cannot invent a host. Parameters:url,maxBytes?(default 100000, cap 500000),timeoutMs?(default 15000, cap 60000). Returns{ ok, status, url, content, truncated, error? }. JSON object/array bodies are returned as a JSON string. Parallel-safe.createGlobTool(workingDirectory?)→Tool- Pattern-based path search. Supported by the interpreter and C# transpile (not the JavaScript backend). Returns the same{ items, count, truncated }shape asio.glob.createCreateMcpAgentScriptTool(workingDirectory?)→Tool- Generates MCP agent scriptscreateSubmitPlanTool()→Tool- Submit a structured plan (steps withid,description, optionaldependsOn). Parameters:plan(object withsteps) orsteps(array), optionaltaskSummary. Returns{ accepted: true, planId, stepCount }or{ accepted: false, error }. Stores the plan in process memory with every steppending. No working directory.createUpdatePlanTool()→Tool- Update a stored plan. Parameters:planId(required), optionalsteps(re-validated; surviving step ids keep their status; new ids startpending), optionaltaskSummary. Returns{ accepted: true, planId, stepCount, steps }or{ accepted: false, error }. No working directory. Not parallel-safe.createMarkStepTool()→Tool- Set a stored step status (pending|in_progress|done|blocked). Parameters:planId,id,status, optionalnote. Returns{ accepted: true, planId, id, status }or{ accepted: false, error }. No working directory. Not parallel-safe.
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
think(prompt)→object: Process a prompt and return response (handles tool calls automatically)addTool(tool)oraddTool(toolName): Add a tool the agent can useaddToolByName(toolName): Add a registered tool by nameaddAllTools(): Add all registered tools to the agentaddSubAgent(subAgent, toolDescription): Add another agent as a tool with a simplified descriptiongetAvailableTools()→array: Get list of available tool namesgetConversation()→Conversation: Get the agent's conversation objectreset(): Reset agent's conversation history (tools are preserved)enableMemory(),enableMemory(path),useMemory(memory),saveMemory(path?),remember(fact, context?),getMemory(): GraphMemory integration (see 20. GraphMemory).enableMemory(path)shares one GraphMemory instance across agents using the same path.setMemoryScope(scope),setMemoryScopeParent(parentScope),setMemoryScopeHierarchy(scopes): Control hierarchical memory visibility duringthink()(e.g.chat:123→project:foo→org:acme→global). UsesetMemoryScopeHierarchyfor multi-level config; otherwise parent scope falls back toMALDA_MEMORY_SCOPE_PARENT.setMemoryRerank(enabled, mode?, modelPath?, topK?): Enable GraphMemory rerank duringthink()memory queries (rerankMode:llm,cross, oronnx). Env fallbacks:MALDA_MEMORY_RERANK,MALDA_MEMORY_RERANK_MODE,MALDA_MEMORY_RERANK_MODEL_PATH.setAutoRememberOnThink(enabled): Whentrue, eachthink()stores the full prompt and response in memory (defaulttrue)addMemoryProgressTools(): Addsremember_progressandrecall_progresstools for explicit LLM-controlled notes
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
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.
| Construction | What it confines | What 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.confine | Unforgeable 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:
read_file- Read file contentswrite_file- Write content to filesreplace_in_file- Replace text in files (single replacement)edit_file- Apply multiple edits to a file (multiple replacements)insert_at_line- Insert content at specific line numbersgrep- Search for patterns in fileslist_directory- List directory contentsrun_command- Execute shell commands (e.g.,dotnet,npm,python) with safety checks to prevent dangerous commands
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);
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:
git_status- Check repository statusgit_add- Stage filesgit_commit- Create commitsgit_log- View commit historygit_diff- View changesgit_branch- Manage branches (list/create only - deletion disabled for safety)git_checkout- Switch branchesgit_push- Push to remotegit_pull- Pull from remote
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:
ask_user- Ask questions to the user for clarification, confirmation, or additional information
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:
- Safety: Orchestrators can request approval before destructive operations
- Clarification: Agents can ask for missing information instead of guessing
- Flexibility: Complex workflows can pause for human decision-making
- Separation of Concerns: Human interaction logic is isolated in the HumanAgent
- Reusability: Same HumanAgent can be used by multiple orchestrators
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:
- File tools:
read_file,write_file,replace_in_file,insert_at_line,edit_file,delete_file,copy_file,ensure_dir,grep,glob,list_directory - Git tools:
git_status,git_add,git_commit,git_log,git_diff,git_branch,git_checkout,git_push,git_pull - Execution tools:
run_command,test_malda - Web tools:
web_search,web_fetch - Interaction tools:
ask_user - Structured task planning:
submit_plan,update_plan,mark_step- Agents can submit a plan (steps withid,description, optionaldependsOn), replace steps, and mark step status (pending|in_progress|done|blocked). - Diagnosis:
check_maldaandvalidate_jsonare always registered. Usecheck_malda(same report asmalda check --json) for the diagnose loop;get_parse_errorsis syntax-only. - Optional analysis:
get_symbolsandget_parse_errors(whenincludeSymbolsis true)
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).
- decomposeTask(instruction, client?, team?) – Uses an LLM to turn a high-level instruction into a plan. Optional second argument is an LLM client or an
AgentTeam; if the client is omitted, the default local LLM (Qwen/Qwen2.5-0.5B-Instruct, auto-downloaded as a GGUF build from Hugging Face) is used. With a team, each step must setroleto a member name. Returns a plan object or{ error }on failure. - executePlan(plan, agent) – Validates the plan, topo-sorts steps by
dependsOn, then runsthink(step.description)for each step in order. The second argument may be anAgentor anAgentTeamfromagents.team. A team requires each step to namerole(oragent). Optionalstep.outcoerces think JSON orstep.fixturelike a typed prompt. Team hop checks, review/reject verdicts,plan.think: false, and theskippedlist are in § 19.17. Returns{ planId, completed, failed, skipped, results }. - submit_plan tool – Agents can call this tool to submit a plan (e.g. after a first "think" that produces the plan). DevAgent includes this tool by default. Parameters:
plan(object withsteps) orsteps(array), optionaltaskSummary. Returns{ accepted: true, planId, stepCount }or{ accepted: false, error }. The accepted plan is stored in process memory with every steppending. - update_plan tool – Updates a stored plan. Parameters:
planId(required), optionalsteps(re-validated; surviving ids keep their status), optionaltaskSummary. Returns{ accepted: true, planId, stepCount, steps }or{ accepted: false, error }. Not parallel-safe. Factory:createUpdatePlanTool(). - mark_step tool – Sets a stored step status. Parameters:
planId,id,status(pending|in_progress|done|blocked), optionalnote. Returns{ accepted: true, planId, id, status }or{ accepted: false, error }. Not parallel-safe. Factory:createMarkStepTool(). - PromptInstance metadata – When calling
agent.think(promptInstance), optional fieldsmodel,temperature,maxTokens, andtools(array of tool names) apply to that single LLM request. - DevAgent code memory –
devAgent.enableCodeMemory(path?, scope?)loads GraphMemory at.dev-agent-memoryunder the workdir (by default), sets scopecode:<workdir-name>, and registersindex_code_file/find_code_relationshipstools.devAgent.indexCodebase(extensions?)bulk-indexes source files (default:.cs,.malda,.js,.ts,.py,.html,.css,.json,.md).
// 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:
- File tools:
read_file,write_file,replace_in_file,insert_at_line,edit_file,grep,list_directory - MALDA tools:
run_malda,compile_malda
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
- Use
MALDACodingAgentwhen you need MALDA-specific development: file operations + MALDA execution/compilation (focused on MALDA script development) - Use
DevAgentwhen you need a complete development workflow: file operations, git operations, command execution, and user interaction - Use
CodingAgentwhen you only need file operations and command execution (lighter weight, fewer tools, for general coding tasks) - Use
GitAgentwhen you only need git operations - None of these kinds is a capability sandbox. Pick a kind for the tool list; confine with
workingDirectory,DevAgentreadOnly, ornew Agent+cap.*— specialized kinds are not sandboxes.
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:
- Automatic Reporting: All agents automatically report lifecycle events, think() calls, and tool executions
- Zero Configuration: Works out of the box with default localhost dashboard
- Cross-Process Support: Agents from different executables are distinguished by process ID
- Error Tolerant: Dashboard failures never affect agent execution
- Non-Blocking: All reporting is asynchronous and fire-and-forget
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:
- Agent Created (
agent_created)- Reported when an agent is initialized
- Includes: agent name, role, process ID, timestamp
- Think Operation (
think)- Reported when
agent.think()is called - Includes: agent name, prompt (truncated to 500 chars), prompt length, process ID, timestamp
- Reported when
- Tool Call (
tool_call)- Reported when tools are executed
- Includes: agent name, tool name, success status, error message (if failed), process ID, timestamp
- Agent Reset (
agent_reset)- Reported when
agent.reset()is called - Includes: agent name, process ID, timestamp
- Reported when
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:
agentId(string): The name of the agentprocessId(integer): Process ID to distinguish agents from different executableseventType(string): Type of event (agent_created,think,tool_call,agent_reset)timestamp(string): UTC timestamp in ISO 8601 formatdata(object): Event-specific data
Event-Specific Data:
- agent_created:
{ "role": "..." } - think:
{ "prompt": "...", "promptLength": 123 } - tool_call:
{ "toolName": "...", "success": true/false, "errorMessage": "..." } - agent_reset:
{}
Dashboard Endpoint Requirements
The dashboard server should implement a POST endpoint that accepts JSON payloads:
- URL: Configured via
SPL_AGENT_DASHBOARD_URL(default:http://localhost:8080/api/agent/status) - Method: POST
- Content-Type:
application/json - Body: JSON object with agent status information (see payload format above)
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:
- Network Failures: If the dashboard is unreachable, agents continue working normally
- Timeout Protection: HTTP requests timeout after 2 seconds to prevent hanging
- Silent Failures: All dashboard errors are caught and ignored - they never affect agent execution
- Fire-and-Forget: All reporting is asynchronous and non-blocking
Benefits
- Observability: Monitor all agents across your system in real-time
- Debugging: Track agent activities and tool usage patterns
- Analytics: Collect metrics on agent performance and tool usage
- Multi-Process Support: Track agents running in different executables via process ID
- Zero Overhead: No performance impact on agent execution
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.
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);
subAgent: AnAgentinstance to be used as a tooltoolDescription: A concise description of what the subagent does (shown to the orchestrator)
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 —
agents.define/agents.team, typed hops, and role-awareexecutePlan - 9. Functions - @Tool decorator syntax
- 10. Prompts - Prompt templates and
agent.think - 21. MCP Server - Host-launched STDIO tools
- 12.6 Capability tokens — why
cap.*exists and the host-mint pattern - Specialized kinds are not sandboxes —
CodingAgentand siblings are tool bundles;policydoes not wrap their string-path tools - 24.6 Program-boundary policy — last gate on cap consume
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).
addSubAgent:
addSubAgent(§ 19.16) is a supervisor tool loop — the orchestrator decides when to call a specialist.agents.teamis a directed graph — host code (orexecutePlan) may hop only along declaredreledges, optionallyvalidated by a schemacontract.- Construction does not auto-download a local model. Pass
clientwhen you later callthink(), or usenew CodingAgent(...)when you want the default GGUF.
19.17.1 agents.define
agents.define(spec, client?) returns an agent. Required spec fields: name, role, instructions. Optional fields:
| Field | Applies to | Meaning |
|---|---|---|
kind | all | Class to construct. Default Agent. |
tools | all | Array of tool names added after construction. |
memoryScope | all | Passed to setMemoryScope. |
workingDirectory | specialized kinds only | Same as the matching constructor: a string prefix on bundled file tools, not a cap.* token. Illegal on plain Agent. |
includeSymbols / readOnly / prdAuthorOnly | DevAgent only | Same 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.
kind | Constructor twin |
|---|---|
Agent (default) | new Agent |
CodingAgent | new CodingAgent |
GitAgent | new GitAgent |
HumanAgent | new HumanAgent |
DevAgent | new DevAgent |
MALDACodingAgent | new 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:
rel | Host hop | Notes |
|---|---|---|
handoff | team.handoff | Transfer work along the edge. Default when rel is omitted. |
review | team.review | Returns approved (default true). |
reject | team.reject | Returns rejected (default true). Used by executePlan when the predecessor is not approved. |
consult | team.consult | One-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
team.members()— member names, sorted.team.get(name)— the agent instance; throws if the name is missing.team.relations()— declared edges as{ from, to, rel, contract }(contractisnullwhen unset).team.graph— the topology graph.
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:
- Omit it, or pass
false/{ think: false }, to stay validate-only (offline-safe). This is the default. - Pass
trueor{ think: true }/{ run: true }to callto.think(payload)after the hop is allowed. Object payloads are JSON-encoded. - For review / reject,
{ approved: false }/{ rejected: true }record a verdict without an LLM call.
Return shapes:
| Method | Success |
|---|---|
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:
Examples/Agents/agent_team_graph.malda— specs + graph + contractExamples/Agents/agent_team_kinds.malda— specializedkindExamples/Agents/agent_team_plan.malda— illegal hop rejected beforethink()Examples/Agents/agent_team_handoff_run.malda— validate-onlyhandoffExamples/Agents/agent_team_review.malda— typedreview/rejectExamples/Agents/agent_team_plan_verdict.malda—approvedvsrejectbranchingExamples/Agents/agent_team_consult.malda— typedconsultExamples/Agents/agent_team_plan_out.malda—step.out+fixture
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
- 15. Graphs — directed literals and extra edge keys
- 19.13 Structured task planning — plan shape,
submit_plan/update_plan/mark_step - 19.16 Multi-Agent Orchestration —
addSubAgentsupervisor loops - 10. Prompts —
agent.think - 24. Agentic Runs —
malda-agentsteam check, journal,AgentError
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.
- Most agent usage is synchronous and straightforward
- Tool execution is naturally synchronous
- Agents can be easily stored in actor state when concurrency is needed
- This provides flexibility without forcing actor complexity for simple cases
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
- Concurrency: Multiple agents can process tasks in parallel without blocking
- State Isolation: Each actor maintains its own agent instance and conversation history
- Scalability: Spawn many agent actors for distributed AI processing
- Fault Tolerance: If one agent actor fails, others continue independently
- Message-Based Coordination: Use actor messaging to coordinate agent workflows
- Long-Running Sessions: Actors can maintain agent state across multiple interactions
See Also
- 18. Actors - Actor model and message passing
- 18.5 Actor State Isolation - Understanding actor state
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)
.ralph-state.json— iteration history, validation notes, resume checkpoint (lastSuccessfulIteration,failedIterations, token totals when available).ralph-memory.graph.json,.ralph-memory.metadata.json,.ralph-memory.vectordb.bin— GraphMemory persistenceralph-run-report.json/ralph-run-report.html— run summary whenMALDA_RALPH_REPORTis enabledPRD.md— feature checklist with[ ],[TODO],[DONE], optional[P0]priority,(depends: …), andAcceptance:lines
Memory model
Ralph attaches shared GraphMemory to the agent via setupRalphGraphMemory() and:
- Initializes with
MALDA_MEMORY_EMBED(hash,bow, orllama) and loads.ralph-memory.*from the workdir - Scopes memory with
agent.setMemoryScope("ralph:{project}")(override viaMALDA_RALPH_MEMORY_SCOPE) - Reindexes
PRD.mdat startup and seeds facts from.ralph-interview-brief.jsonwhen present - Runs
maintainRalphMemory()after each iteration:consolidate/reflect,prune,enforceLimits(default on whenMAX_ITER > 5) - Disables auto-remember of the full prompt by default (
setAutoRememberOnThink(false)); setMALDA_RALPH_AUTO_REMEMBER=trueto restore legacy behavior - Registers
remember_progress/recall_progressviaaddMemoryProgressTools() - After each iteration, saves a structured summary (phase, validation, files, tool count) with metadata
- Indexes modified files with
memory.analyzeFile()where applicable agent.think()injects hybrid memory (vector + lexical + synapse + MMR). Optional phase-scoped block:MALDA_RALPH_MEMORY_PHASE_QUERY=true- Includes
memory.stats()inralph-run-report.jsonwhen reporting is enabled
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
MALDA_RALPH_WORKDIR,MALDA_RALPH_WORKTREE,MALDA_RALPH_PROJECT_REL,MALDA_RALPH_PRD,MALDA_RALPH_MAX_ITER,MALDA_RALPH_TASKMALDA_RALPH_RESET_EACH—true,false, orphase(reset conversation when PRD phase changes; defaulttrue)MALDA_RALPH_RESUME,MALDA_RALPH_RESUME_POLICY(all|success-only),MALDA_RALPH_VALIDATE,MALDA_RALPH_VALIDATE_DEPTH,MALDA_RALPH_VALIDATE_CMDMALDA_RALPH_PRD_STRICT,MALDA_RALPH_REQUIRE_SIGNAL,MALDA_RALPH_PLAN_ONLY,MALDA_RALPH_PREFLIGHTMALDA_RALPH_REPORT,MALDA_RALPH_NOTIFY,MALDA_RALPH_MAX_PHASE_RETRIES,MALDA_RALPH_ABORT_ON_STALLMALDA_RALPH_GIT_COMMIT,MALDA_RALPH_AUTO_REMEMBER(defaultfalse)MALDA_AGENT_THINK_TIMEOUT_MS/MALDA_RALPH_ITER_TIMEOUT_MS— per-iteration think deadline- Verbose CLI:
MALDA_AGENT_VERBOSE(defaulttruein Ralph),MALDA_AGENT_RICH,MALDA_AGENT_LLM_THINKING,MALDA_AGENT_LLM_STREAM— see 19.5 verbose logging OPENROUTER_API_KEYorproviders.openrouterin~/.malda/config.json
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);