Chapter 27: NeamClaw Traits -- Composable Agent Behaviors #
"The measure of intelligence is the ability to change." -- Albert Einstein
In the preceding chapters, you built claw agents with persistent sessions, forge agents
with build-verify loops, and wired them into multi-agent pipelines. Each of those agents
had a fixed set of capabilities determined at declaration time. But production agents
need more: they need to run periodic health checks, route messages across platforms,
sandbox untrusted code, detect behavioral anomalies, hook into spawn and delegate
events, and configure their own search strategies. NeamClaw provides six built-in traits
that add these behaviors through the same impl Trait for Type syntax you learned in
Chapter 9b. This chapter is a deep dive into every one of them.
By the end of this chapter, you will be able to:
- Explain the connection between OOP traits (Chapter 9b) and NeamClaw agent traits
- Read the trait compatibility matrix and predict whether a given trait will compile for a given agent type
- Implement
Schedulablefor periodic heartbeat and cron-based scheduling - Implement
Channelablefor multi-channel message routing - Implement
Sandboxablefor per-execution Docker sandbox configuration - Implement
Monitorablefor behavioral monitoring and anomaly detection - Implement
Orchestrablefor multi-agent spawn and delegate callbacks - Implement
Searchablefor RAG and search configuration - Compose multiple traits on a single agent and understand trait interaction rules
A chat assistant that cannot detect when it is stuck in a loop will burn through your API budget overnight. A coding agent that executes user-submitted code without a sandbox is a security incident waiting to happen. A multi-agent system that spawns child agents with no logging produces invisible failures. NeamClaw traits give you compile-time guarantees that these capabilities are wired correctly. If you try to schedule a stateless agent that has no session infrastructure, the compiler stops you before you deploy. That is the difference between "it works on my laptop" and "it works in production."
27.1 From OOP Traits to Agent Traits #
In Chapter 9b, you learned that a trait defines a set of methods that types can
implement. You used impl Describable for Dog to give a Dog struct a .describe()
method. The trait system provided polymorphism without inheritance: different types
could share the same interface while providing their own implementations.
NeamClaw agent traits work the same way syntactically but serve a different purpose. OOP traits define shared interfaces for data types. Agent traits define composable behaviors for agent types. The syntax is identical:
// OOP trait (Chapter 9b)
trait Describable {
fn describe(self);
}
impl Describable for Dog {
fn describe(self) {
return f"Dog: {self.name}";
}
}
// Agent trait (this chapter)
impl Schedulable for my_claw_agent {
fn on_heartbeat(self) {
return HeartbeatAction.Silent;
}
fn heartbeat_interval(self) {
return 30000;
}
}
The key differences are:
-
You do not declare agent traits yourself. The six NeamClaw traits (
Schedulable,Channelable,Sandboxable,Monitorable,Orchestrable,Searchable) are built into the language. You only writeimplblocks for them. -
The compiler enforces compatibility. Not every trait works with every agent type. Implementing
Schedulableon a statelessagentproduces a compile error because stateless agents have no session infrastructure to support periodic heartbeats. The compatibility matrix (Section 27.2) defines exactly which combinations are valid. -
The runtime hooks into your implementations. When you implement
Monitorable, the runtime automatically calls yourbaseline()method to establish expected metrics and invokes youron_anomaly()callback whenever those metrics deviate. You do not need to wire anything manually.
┌─────────────────────────────────────────────────────────────────────┐
│ OOP Traits vs Agent Traits │
│ │
│ OOP Trait (Chapter 9b) Agent Trait (Chapter 27) │
│ ───────────────────── ──────────────────────── │
│ │
│ trait Describable { Schedulable (built-in) │
│ fn describe(self); fn on_heartbeat(self); │
│ } fn heartbeat_interval(self); │
│ │
│ impl Describable for Dog { impl Schedulable for my_bot { │
│ fn describe(self) { fn on_heartbeat(self) { │
│ return "woof"; return HeartbeatAction │
│ } .Silent; │
│ } } │
│ fn heartbeat_interval(self) { │
│ Purpose: return 30000; │
│ Shared interface for } │
│ data types } │
│ │
│ Enforced by: Purpose: │
│ Compiler (method exists) Composable behavior for │
│ agent types │
│ │
│ Enforced by: │
│ Compiler (method exists │
│ AND agent type compatible) │
└─────────────────────────────────────────────────────────────────────┘
Think of agent traits as optional certifications for an employee. A "First Aid
Certified" badge means the employee can handle medical emergencies. A "Forklift
Licensed" badge means they can operate heavy equipment. Not every role qualifies
for every certification -- an office receptionist cannot get forklift certification
because they do not work in a warehouse. Similarly, a stateless agent cannot
implement Schedulable because it has no session infrastructure to schedule against.
The compiler is the HR department that checks qualifications before issuing badges.
27.2 Trait Compatibility Matrix #
Not every trait is compatible with every agent type. The following matrix shows which combinations are valid:
| Trait | agent (stateless) |
claw agent |
forge agent |
|---|---|---|---|
| Schedulable | Error | Yes | Error |
| Channelable | Error | Yes | Error |
| Sandboxable | Warning | Yes | Yes |
| Monitorable | Warning | Yes | Yes |
| Orchestrable | Error | Yes | Yes |
| Searchable | Warning | Yes | Yes |
Legend:
- Yes -- Fully supported. The compiler accepts the impl block and the runtime
hooks into your methods.
- Warning -- The compiler accepts the impl block but emits a warning. The trait
may have reduced functionality on this agent type.
- Error -- The compiler rejects the impl block with an error. The trait requires
capabilities that this agent type does not have.
Why Some Combinations Produce Errors #
Schedulable requires session infrastructure for heartbeat timers and cron scheduling. Stateless agents and forge agents lack this infrastructure. A forge agent runs a finite loop and exits; there is no long-running process to attach a heartbeat to.
Channelable requires multi-channel I/O, which only claw agents provide. Stateless agents have no channel system, and forge agents receive work through their plan file, not through message channels.
Orchestrable requires the spawn and dag_execute runtime to fire callbacks.
Stateless agents do not participate in orchestration flows that need lifecycle callbacks.
Compile Error Messages #
When you attempt an invalid combination, the compiler produces a clear error:
# Attempting to implement Schedulable on a stateless agent
neamc my_agent.neam
error[E0271]: trait `Schedulable` is not compatible with `agent`
--> my_agent.neam:15:1
|
15 | impl Schedulable for my_agent {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: `Schedulable` requires a `claw agent` declaration
= help: change `agent my_agent` to `claw agent my_agent`
# Attempting to implement Channelable on a forge agent
neamc my_forge.neam
error[E0271]: trait `Channelable` is not compatible with `forge agent`
--> my_forge.neam:22:1
|
22 | impl Channelable for code_builder {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: `Channelable` requires a `claw agent` declaration
= help: only `claw agent` supports multi-channel message routing
Compiler Warning Example #
# Implementing Sandboxable on a stateless agent
neamc my_agent.neam
warning[W0150]: trait `Sandboxable` has limited support on `agent`
--> my_agent.neam:18:1
|
18 | impl Sandboxable for my_agent {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: sandbox configuration will apply but cannot enforce
per-session isolation without a `claw agent` or `forge agent`
= help: consider using `claw agent` or `forge agent` for full
sandbox enforcement
When in doubt, use a claw agent. Claw agents support all six traits with full
functionality. If your agent does not need persistent sessions, you can set
session: { compaction: "disabled" } and treat it as a lightweight session agent
that still benefits from the full trait system.
27.3 Schedulable #
Purpose #
The Schedulable trait enables periodic heartbeat and cron-based scheduling for claw
agents. A scheduled agent can perform background tasks at regular intervals: checking
for pending work, sending daily digests, running periodic cleanup, or proactively
notifying users of important events.
Required Methods #
| Method | Arity | Required | Returns |
|---|---|---|---|
on_heartbeat(self) |
1 | Yes | HeartbeatAction |
heartbeat_interval(self) |
1 | Yes | Number (milliseconds) |
on_cron(self, cron_id) |
2 | No | any (default: nil) |
The HeartbeatAction Sealed Type #
The on_heartbeat method returns a HeartbeatAction, a built-in sealed type with
two variants:
// Built-in -- you do not declare this yourself
sealed HeartbeatAction {
Notify(message: string),
Silent
}
HeartbeatAction.Notify(message)-- The runtime delivers the message as a proactive notification through the agent's active channel. Use this when the heartbeat detects something worth reporting.HeartbeatAction.Silent-- The runtime takes no action. Use this when the heartbeat check finds nothing noteworthy.
Full Implementation Example #
channel cli_chan {
type: "cli"
}
claw agent task_monitor {
provider: "openai"
model: "gpt-4o"
channels: [cli_chan]
system: "You monitor pending tasks and notify users of overdue items."
workspace: "./task_data"
}
impl Schedulable for task_monitor {
fn on_heartbeat(self) {
// Check for overdue tasks in the workspace
let pending = workspace_read("pending_tasks.json");
if (pending == nil) {
return HeartbeatAction.Silent;
}
// Parse and check for overdue items
let tasks = parse_json(pending);
let overdue_count = 0;
for (task in tasks) {
if (task["status"] == "overdue") {
overdue_count = overdue_count + 1;
}
}
if (overdue_count > 0) {
return HeartbeatAction.Notify(
f"You have {overdue_count} overdue task(s). Type 'show overdue' to review."
);
}
return HeartbeatAction.Silent;
}
fn heartbeat_interval(self) {
return 60000; // Check every 60 seconds
}
fn on_cron(self, cron_id) {
if (cron_id == "daily_digest") {
let summary = self.ask("Generate a daily task summary from the workspace.");
return HeartbeatAction.Notify(summary);
}
return nil;
}
}
How the Runtime Uses Schedulable #
┌─────────────────────────────────────────────────────────────────────┐
│ Schedulable Runtime Flow │
│ │
│ Agent starts │
│ │ │
│ ▼ │
│ Read heartbeat_interval() ──▶ 60000 ms │
│ │ │
│ ▼ │
│ Start timer (every 60s) │
│ │ │
│ ▼ │
│ ┌─────────────────────────────┐ │
│ │ Timer fires │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Call on_heartbeat(self) │ │
│ │ │ │ │
│ │ ├── Silent ──▶ no-op │ │
│ │ │ │ │
│ │ └── Notify(msg) ──▶ deliver to active channel │
│ │ │ │
│ │ Wait 60s, repeat │ │
│ └─────────────────────────────┘ │
│ │
│ Cron events fire on_cron(self, cron_id) at configured times │
└─────────────────────────────────────────────────────────────────────┘
Use Cases #
- Pending task checks -- Periodically scan a workspace or database for items that need attention and proactively notify the user.
- Daily digests -- Use
on_cronto generate and deliver a daily summary at a fixed time each day. - Periodic cleanup -- Remove stale session files, archive old conversations, or prune expired cache entries on a schedule.
- Health monitoring -- Check external service availability and warn the user if a dependency is unreachable.
27.4 Channelable #
Purpose #
The Channelable trait enables multi-channel message routing for claw agents. A
channelable agent can receive messages from multiple platforms (Slack, Discord, email,
HTTP webhooks) and route each message through a unified handler. This is the trait you
implement when your agent needs to operate across communication platforms simultaneously.
Required Methods #
| Method | Arity | Required | Returns |
|---|---|---|---|
channels(self) |
1 | Yes | List of strings |
on_message(self, msg) |
2 | Yes | any |
Full Implementation Example #
channel slack_chan {
type: "http"
}
channel discord_chan {
type: "http"
}
channel email_chan {
type: "http"
}
claw agent omni_bot {
provider: "openai"
model: "gpt-4o"
channels: [slack_chan, discord_chan, email_chan]
system: "You are a support agent. Adapt your response style to the channel."
temperature: 0.5
}
impl Channelable for omni_bot {
fn channels(self) {
return ["slack", "discord", "email"];
}
fn on_message(self, msg) {
let channel = msg["channel_type"];
let content = msg["content"];
let sender = msg["sender"];
// Log the incoming message
emit f"[{channel}] {sender}: {content}";
// Route based on channel type
if (channel == "email") {
// Email messages get formal responses
let response = self.ask(
f"Respond formally to this email from {sender}: {content}"
);
return response;
}
if (channel == "slack") {
// Slack messages get concise responses
let response = self.ask(
f"Respond concisely to this Slack message from {sender}: {content}"
);
return response;
}
// Default: standard response for all other channels
let response = self.ask(content);
return response;
}
}
How the Runtime Uses Channelable #
When a message arrives on any registered channel, the runtime:
- Identifies the channel type from the inbound message metadata.
- Calls
on_message(self, msg)with a map containingchannel_type,scope,sender,content,session_key, andmetadata_json. - Delivers the return value back through the originating channel.
The channels(self) method is called at startup to register the list of channel
names the agent expects to handle. This allows the runtime to validate that all
declared channels match the agent's channel configuration.
Use Cases #
- Cross-platform support bots -- A single agent that handles customer inquiries from Slack, Discord, email, and a web chat widget, adapting tone and format to each platform.
- Notification routing -- Route high-priority alerts to Slack while sending detailed reports via email.
- Message transformation -- Normalize messages from different platforms into a uniform format before processing.
27.5 Sandboxable #
Purpose #
The Sandboxable trait configures per-execution Docker sandbox parameters for an
agent. When your agent executes untrusted code, calls external tools, or processes
user-uploaded content, the sandbox isolates those operations from the host system.
Implementing Sandboxable lets you declare exactly what the sandbox allows: network
access, filesystem permissions, allowed commands, and memory limits.
Required Methods #
| Method | Arity | Required | Returns |
|---|---|---|---|
sandbox_config(self) |
1 | Yes | Map |
Config Map Keys #
The map returned by sandbox_config supports the following keys:
| Key | Type | Default | Description |
|---|---|---|---|
mode |
string | "strict" |
Sandbox mode: "strict", "permissive", "disabled" |
network |
bool | false |
Allow network access from within the sandbox |
filesystem |
string | "readonly" |
Filesystem access: "readonly", "readwrite", "none" |
allowed_commands |
list | [] |
Shell commands the agent may execute |
max_memory_mb |
number | 512 |
Maximum memory allocation in megabytes |
Full Implementation Example #
forge agent code_runner {
provider: "openai"
model: "gpt-4o"
system: "You execute Python code in a sandbox. Write tests, run them, iterate."
verify: verify_tests
workspace: "./sandbox_workspace"
checkpoint: "git"
loop: {
max_iterations: 10
max_cost: 5.0
}
}
impl Sandboxable for code_runner {
fn sandbox_config(self) {
return {
"mode": "strict",
"network": false,
"filesystem": "readwrite",
"allowed_commands": ["python3", "pip", "pytest"],
"max_memory_mb": 1024
};
}
}
fun verify_tests(ctx) {
let test_output = workspace_read("test_results.txt");
if (test_output == nil) {
return "No test results found. Run pytest and save output to test_results.txt.";
}
if (test_output.contains("FAILED")) {
return f"Tests failed. Fix the failures:\n{test_output}";
}
return true;
}
In this example, the forge agent executes Python code inside a strict sandbox that:
- Blocks all network access (
network: false) - Allows read-write filesystem access within the workspace (
filesystem: "readwrite") - Permits only
python3,pip, andpytestcommands (allowed_commands) - Limits memory to 1024 MB (
max_memory_mb: 1024)
Sandbox Modes #
Do not set mode: "disabled" in production. An agent executing LLM-generated code
without a sandbox can delete files, exfiltrate data, or consume unbounded resources.
Even for trusted internal agents, mode: "permissive" with a max_memory_mb limit
is safer than no sandbox at all.
Use Cases #
- Code execution safety -- Run LLM-generated Python, JavaScript, or Bash scripts inside a memory-limited, network-isolated sandbox.
- Untrusted tool isolation -- When an agent calls external tools or plugins, the sandbox prevents those tools from accessing the broader system.
- Compliance requirements -- Enforce that agent workloads cannot access the network or write to unauthorized filesystem paths, meeting regulatory isolation requirements.
27.6 Monitorable #
Purpose #
The Monitorable trait enables behavioral monitoring and anomaly detection for agents.
A monitorable agent declares a baseline of expected metrics (average tool calls, average
response time, maximum cost per turn). When the runtime detects that actual behavior
deviates from the baseline, it invokes your on_anomaly callback, giving you the
opportunity to alert, silence, or disable the agent.
Required Methods #
| Method | Arity | Required | Returns |
|---|---|---|---|
baseline(self) |
1 | Yes | Map |
on_anomaly(self, event) |
2 | Yes | AnomalyAction |
The AnomalyAction Sealed Type #
The on_anomaly method returns an AnomalyAction, a built-in sealed type:
// Built-in -- you do not declare this yourself
sealed AnomalyAction {
Alert(message: string),
Silence,
Disable
}
AnomalyAction.Alert(message)-- Log the message and continue. Use this for anomalies that warrant attention but do not require stopping the agent.AnomalyAction.Silence-- Suppress the anomaly. Use this when the deviation is expected (for example, during a known high-traffic period).AnomalyAction.Disable-- Immediately disable the agent via the kill switch. Use this for critical anomalies that indicate the agent is compromised or stuck in an infinite loop.
Baseline Metrics #
The baseline() method returns a map of expected metric values. The runtime compares
actual behavior against these values on every turn:
| Metric Key | Type | Description |
|---|---|---|
avg_tool_calls |
number | Expected average number of tool calls per turn |
avg_response_time_ms |
number | Expected average response time in milliseconds |
max_tool_calls |
number | Maximum tool calls before anomaly fires |
max_response_time_ms |
number | Maximum response time before anomaly fires |
max_cost_per_turn |
number | Maximum cost (dollars) per single turn |
max_consecutive_errors |
number | Maximum sequential errors before anomaly fires |
Full Implementation Example #
channel support_chan {
type: "http"
}
claw agent monitored_support {
provider: "openai"
model: "gpt-4o"
channels: [support_chan]
system: "You are a customer support agent. Be helpful and concise."
skills: [lookup_order, process_refund]
}
impl Monitorable for monitored_support {
fn baseline(self) {
return {
"avg_tool_calls": 2,
"avg_response_time_ms": 800,
"max_tool_calls": 10,
"max_response_time_ms": 5000,
"max_cost_per_turn": 0.05,
"max_consecutive_errors": 3
};
}
fn on_anomaly(self, event) {
let metric = event["metric"];
let actual = event["actual"];
let threshold = event["threshold"];
emit f"ANOMALY on {self.name}: {metric} = {actual} (threshold: {threshold})";
// Critical anomalies: disable the agent
if (metric == "max_tool_calls" & actual > 20) {
return AnomalyAction.Disable;
}
// Cost anomalies: alert but continue
if (metric == "max_cost_per_turn") {
return AnomalyAction.Alert(
f"Cost anomaly: ${actual} exceeds ${threshold} per turn"
);
}
// Everything else: alert
return AnomalyAction.Alert(
f"Behavioral anomaly: {metric} at {actual}"
);
}
}
How the Runtime Uses Monitorable #
┌─────────────────────────────────────────────────────────────────────┐
│ Monitorable Runtime Flow │
│ │
│ Agent registers │
│ │ │
│ ▼ │
│ Call baseline(self) ──▶ Store expected metrics │
│ │ │
│ ▼ │
│ ┌─────────────────────────────┐ │
│ │ Agent processes a turn │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Record actual metrics: │ │
│ │ tool_calls, time_ms, │ │
│ │ cost, errors │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Compare against baseline │ │
│ │ │ │ │
│ │ ├── Within bounds ──▶ continue │
│ │ │ │ │
│ │ └── Exceeded ──▶ call on_anomaly(self, event) │
│ │ │ │ │
│ │ ├── Alert(msg) ──▶ log + continue │
│ │ ├── Silence ──▶ suppress + continue │
│ │ └── Disable ──▶ kill switch activated │
│ │ │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
Use Cases #
- Production monitoring -- Detect when an agent starts making an unusual number of tool calls, which may indicate a prompt injection or an infinite tool loop.
- Cost anomaly detection -- Alert when a single turn exceeds the expected cost threshold, catching runaway API usage before it becomes expensive.
- Reliability tracking -- Monitor consecutive errors and disable an agent that repeatedly fails, preventing cascading failures in multi-agent systems.
Start with generous thresholds and tighten them after observing real traffic patterns.
Setting max_tool_calls: 3 on day one will produce constant false alarms. Let the
agent run for a week, measure the actual distribution, and set thresholds at the
99th percentile of observed behavior.
27.7 Orchestrable #
Purpose #
The Orchestrable trait provides callbacks for multi-agent spawn and delegate events.
When a claw or forge agent spawns a child agent (via spawn) or receives a delegation
result, the runtime invokes your on_spawn and on_delegate methods. This gives you
hooks for logging, metrics collection, access control, and auditing of sub-agent
invocations.
Methods #
| Method | Arity | Required | Returns |
|---|---|---|---|
on_spawn(self, child_name) |
2 | No (default: nil) |
any |
on_delegate(self, result) |
2 | No (default: nil) |
any |
Both methods are optional. If you do not implement one, the runtime uses a default
implementation that returns nil (no-op). This means you can implement only the
callbacks you need.
Full Implementation Example #
channel cli_chan {
type: "cli"
}
claw agent coordinator {
provider: "openai"
model: "gpt-4o"
channels: [cli_chan]
system: "You coordinate research tasks by spawning specialist agents."
skills: [web_search]
}
claw agent researcher {
provider: "openai"
model: "gpt-4o"
channels: [cli_chan]
system: "You are a research specialist. Provide thorough, cited answers."
}
claw agent summarizer {
provider: "openai"
model: "gpt-4o"
channels: [cli_chan]
system: "You summarize research findings into concise bullet points."
}
impl Orchestrable for coordinator {
fn on_spawn(self, child_name) {
emit f"[orchestration] Spawning child agent: {child_name}";
// Log the spawn event to workspace
workspace_append(
"orchestration.log",
f"SPAWN: {child_name}\n"
);
return nil;
}
fn on_delegate(self, result) {
let preview = result;
if (len(result) > 100) {
preview = result[0:100] + "...";
}
emit f"[orchestration] Delegate result: {preview}";
// Log the delegation result
workspace_append(
"orchestration.log",
f"DELEGATE_RESULT: {preview}\n"
);
return nil;
}
}
// Usage: spawn calls trigger the Orchestrable callbacks
{
// This triggers on_spawn("researcher") before the LLM call,
// then on_delegate(result) after the result arrives.
let research = spawn researcher("Summarize recent advances in RAG systems");
let summary = spawn summarizer(
f"Summarize the following research:\n{research}"
);
emit summary;
}
Spawn-Delegate Lifecycle #
Use Cases #
- Audit logging -- Record every spawn and delegate event to a persistent log for compliance and debugging.
- Metrics collection -- Track how many child agents are spawned per session and measure delegation latency.
- Access control -- In
on_spawn, check whether the current user has permission to invoke a particular child agent. Return an error string to deny access. - Result transformation -- In
on_delegate, transform or filter the child agent's result before it reaches the parent.
27.8 Searchable #
Purpose #
The Searchable trait configures RAG (Retrieval-Augmented Generation) and search
behavior for an agent. Instead of relying on the default search settings from the
agent's declaration, Searchable lets you programmatically control the search
strategy, the number of results retrieved, the minimum relevance score, and hook
into the indexing pipeline.
Required Methods #
| Method | Arity | Required | Returns |
|---|---|---|---|
search_config(self) |
1 | Yes | Map |
on_index(self, doc) |
2 | No (default: nil) |
any |
Config Map Keys #
The map returned by search_config supports the following keys:
| Key | Type | Default | Description |
|---|---|---|---|
strategy |
string | "hybrid" |
Search strategy: "hybrid", "vector", "keyword" |
top_k |
number | 5 |
Number of results to retrieve per query |
min_score |
number | 0.0 |
Minimum relevance score (0.0 to 1.0) to include a result |
Full Implementation Example #
channel cli_chan {
type: "cli"
}
claw agent doc_assistant {
provider: "openai"
model: "gpt-4o"
channels: [cli_chan]
system: "You answer questions based on indexed documentation."
workspace: "./docs"
semantic_memory: {
backend: "local"
search: "hybrid"
}
}
impl Searchable for doc_assistant {
fn search_config(self) {
return {
"strategy": "hybrid",
"top_k": 8,
"min_score": 0.65
};
}
fn on_index(self, doc) {
let path = doc["file_path"];
let size = doc["size_bytes"];
emit f"[index] Indexed {path} ({size} bytes)";
// Skip very large files from indexing
if (size > 1000000) {
emit f"[index] Warning: {path} exceeds 1MB, chunking may be coarse";
}
return nil;
}
}
Search Strategies #
Use Cases #
- Custom search tuning -- Override the default
top_kandmin_scoreto control retrieval precision. A legal document assistant might usemin_score: 0.8to avoid surfacing marginally relevant precedents. - Indexing hooks -- Use
on_indexto log, filter, or transform documents as they enter the search index. For example, skip binary files or emit warnings for unusually large documents. - Strategy selection -- Use
"keyword"for codebases where exact function names matter,"vector"for natural-language documentation, or"hybrid"for mixed content.
27.9 Composing Multiple Traits #
A single agent can implement multiple traits. Each trait is implemented in its own
impl block, and the order of impl blocks does not matter. The compiler processes
all impl blocks for a given agent and registers the corresponding behaviors with the
runtime.
Example: Three Traits on One Agent #
channel cli_chan {
type: "cli"
}
claw agent smart_assistant {
provider: "openai"
model: "gpt-4o"
channels: [cli_chan]
system: "You are an intelligent assistant with monitoring, scheduling,
and search capabilities."
workspace: "./assistant_data"
semantic_memory: {
backend: "local"
search: "hybrid"
}
}
// Trait 1: Schedulable -- daily health check
impl Schedulable for smart_assistant {
fn on_heartbeat(self) {
let status = workspace_read("health_status.txt");
if (status != nil & status.contains("degraded")) {
return HeartbeatAction.Notify("System health is degraded. Run diagnostics.");
}
return HeartbeatAction.Silent;
}
fn heartbeat_interval(self) {
return 120000; // Every 2 minutes
}
}
// Trait 2: Monitorable -- anomaly detection
impl Monitorable for smart_assistant {
fn baseline(self) {
return {
"avg_tool_calls": 3,
"avg_response_time_ms": 1000,
"max_tool_calls": 15,
"max_cost_per_turn": 0.10
};
}
fn on_anomaly(self, event) {
emit f"[monitor] Anomaly: {event['metric']} = {event['actual']}";
return AnomalyAction.Alert(
f"Anomaly detected: {event['metric']}"
);
}
}
// Trait 3: Searchable -- custom RAG config
impl Searchable for smart_assistant {
fn search_config(self) {
return {
"strategy": "hybrid",
"top_k": 10,
"min_score": 0.6
};
}
}
Trait Interaction Considerations #
When composing multiple traits, keep the following in mind:
-
Each trait operates independently. The
Schedulableheartbeat timer does not interact with theMonitorablebaseline. Each trait's methods are invoked by different parts of the runtime at different times. -
The order of
implblocks does not matter. You can defineMonitorablebefore or afterSchedulable. The compiler and runtime process them in the same way regardless of source order. -
Traits share the
selfreference. All trait methods receive the sameselfparameter, which is the agent instance. This means every trait can callself.ask(), accessself.name, or invoke workspace functions in the same way. -
A heartbeat can trigger an anomaly. If your
on_heartbeatimplementation callsself.ask(), that ask operation is subject to monitoring. If the ask exceeds the baseline metrics,on_anomalywill fire. Design your heartbeat implementations to be lightweight to avoid triggering false anomalies. -
Searchable affects all queries. Once
Searchableis implemented, everyself.ask()call uses the search configuration you defined. This includes calls from within other trait methods (such ason_heartbeatoron_anomaly).
There is no limit to the number of traits a single agent can implement, but each
trait can only be implemented once per agent. Attempting to write two impl
Schedulable for my_agent blocks produces a compile error.
27.10 Real-World Example: Production-Ready Monitored Agent #
This section brings together everything from the chapter into a single, production-ready claw agent that implements four traits. The agent is a customer support bot that:
- Runs a periodic health check heartbeat (Schedulable)
- Detects behavioral anomalies with baseline metrics (Monitorable)
- Uses hybrid search with custom configuration (Searchable)
- Logs all spawn and delegate events (Orchestrable)
// ── Skills ──────────────────────────────────────────────────────────
skill lookup_order {
description: "Look up order status by order ID"
params: { order_id: string }
impl(order_id) {
return "Order " + order_id + ": Shipped, arriving Tuesday";
}
}
skill process_refund {
description: "Process a refund for an order"
params: { order_id: string, reason: string }
impl(order_id, reason) {
workspace_append("refunds.log", f"{order_id}: {reason}\n");
return f"Refund initiated for order {order_id}. Reason: {reason}";
}
}
skill escalate_to_human {
description: "Escalate the conversation to a human agent"
params: { reason: string }
impl(reason) {
workspace_append("escalations.log", f"ESCALATE: {reason}\n");
return "A human agent has been notified and will join shortly.";
}
}
// ── Guards ──────────────────────────────────────────────────────────
guard input_safety {
description: "Filters harmful input"
on_observation(text) { return text; }
}
guardchain safety_chain = [input_safety];
// ── Channel ─────────────────────────────────────────────────────────
channel http_chan {
type: "http"
}
// ── Agent Declaration ───────────────────────────────────────────────
claw agent support_pro {
provider: "openai"
model: "gpt-4o"
channels: [http_chan]
system: "You are a professional customer support agent for Acme Corp.
Be empathetic, concise, and solution-oriented. Use tools to
look up orders, process refunds, and escalate when needed."
temperature: 0.3
skills: [lookup_order, process_refund, escalate_to_human]
guards: [safety_chain]
workspace: "./support_pro_data"
session: {
idle_reset_minutes: 30
max_history_turns: 50
compaction: "auto"
}
semantic_memory: {
backend: "local"
search: "hybrid"
}
}
// ── Trait 1: Schedulable ────────────────────────────────────────────
impl Schedulable for support_pro {
fn on_heartbeat(self) {
// Check for unresolved escalations
let escalations = workspace_read("escalations.log");
if (escalations == nil) {
return HeartbeatAction.Silent;
}
let lines = split(escalations, "\n");
let unresolved = 0;
for (line in lines) {
if (len(line) > 0 & !line.contains("RESOLVED")) {
unresolved = unresolved + 1;
}
}
if (unresolved > 0) {
return HeartbeatAction.Notify(
f"There are {unresolved} unresolved escalation(s) awaiting human review."
);
}
return HeartbeatAction.Silent;
}
fn heartbeat_interval(self) {
return 300000; // Every 5 minutes
}
fn on_cron(self, cron_id) {
if (cron_id == "daily_summary") {
let refunds = workspace_read("refunds.log");
let escalations = workspace_read("escalations.log");
let summary = f"Daily Support Summary\n";
summary = summary + f"Refunds: {refunds}\n";
summary = summary + f"Escalations: {escalations}\n";
workspace_write("daily_summary.txt", summary);
return HeartbeatAction.Notify("Daily summary generated. Check daily_summary.txt.");
}
return nil;
}
}
// ── Trait 2: Monitorable ────────────────────────────────────────────
impl Monitorable for support_pro {
fn baseline(self) {
return {
"avg_tool_calls": 2,
"avg_response_time_ms": 1200,
"max_tool_calls": 8,
"max_response_time_ms": 10000,
"max_cost_per_turn": 0.08,
"max_consecutive_errors": 5
};
}
fn on_anomaly(self, event) {
let metric = event["metric"];
let actual = event["actual"];
let threshold = event["threshold"];
// Log all anomalies to workspace
workspace_append(
"anomalies.log",
f"ANOMALY: {metric} = {actual} (threshold: {threshold})\n"
);
// Disable on runaway tool usage (possible infinite loop)
if (metric == "max_tool_calls" & actual > 20) {
workspace_append("anomalies.log", "ACTION: DISABLED\n");
return AnomalyAction.Disable;
}
// Disable on excessive consecutive errors
if (metric == "max_consecutive_errors") {
workspace_append("anomalies.log", "ACTION: DISABLED\n");
return AnomalyAction.Disable;
}
// Alert on cost overruns
if (metric == "max_cost_per_turn") {
return AnomalyAction.Alert(
f"Cost alert: ${actual} per turn exceeds ${threshold} threshold"
);
}
// Default: alert and continue
return AnomalyAction.Alert(
f"Behavioral anomaly: {metric} = {actual}"
);
}
}
// ── Trait 3: Searchable ─────────────────────────────────────────────
impl Searchable for support_pro {
fn search_config(self) {
return {
"strategy": "hybrid",
"top_k": 5,
"min_score": 0.7
};
}
fn on_index(self, doc) {
emit f"[search] Indexed: {doc['file_path']}";
return nil;
}
}
// ── Trait 4: Orchestrable ───────────────────────────────────────────
impl Orchestrable for support_pro {
fn on_spawn(self, child_name) {
workspace_append(
"orchestration.log",
f"SPAWN: {child_name}\n"
);
emit f"[orchestration] Spawned: {child_name}";
return nil;
}
fn on_delegate(self, result) {
let preview = result;
if (len(result) > 200) {
preview = result[0:200] + "...";
}
workspace_append(
"orchestration.log",
f"RESULT: {preview}\n"
);
return nil;
}
}
// ── Main Execution ──────────────────────────────────────────────────
{
emit "=== Support Pro Agent ===";
emit "";
// Test a support interaction
let r1 = support_pro.ask("What is the status of order ORD-55921?");
emit f"Response: {r1}";
emit "";
// The agent remembers the previous context
let r2 = support_pro.ask("I want a refund for that order. The product was damaged.");
emit f"Response: {r2}";
emit "";
// Test escalation
let r3 = support_pro.ask("This is unacceptable. I want to speak to a manager.");
emit f"Response: {r3}";
}
Walkthrough #
Let us walk through each trait's contribution to this production agent:
Schedulable provides a heartbeat that runs every 5 minutes. Each heartbeat checks
the escalations.log file for unresolved escalations and notifies the active channel
if any are found. The on_cron handler generates a daily summary of refunds and
escalations, writing it to daily_summary.txt and notifying the operator.
Monitorable establishes baseline metrics for normal operation: an average of 2 tool
calls per turn, 1200ms average response time, and a maximum cost of $0.08 per turn.
When the runtime detects that actual behavior exceeds these thresholds, on_anomaly
fires. The handler logs every anomaly to anomalies.log, disables the agent on
runaway tool usage or consecutive errors, and alerts on cost overruns.
Searchable configures hybrid search (70% vector + 30% BM25) with 5 results per
query and a minimum relevance score of 0.7. This means the agent only retrieves
documentation chunks that are at least 70% relevant to the user's question, reducing
noise in the LLM context. The on_index hook logs each file as it enters the search
index.
Orchestrable logs every spawn and delegate event to orchestration.log. When
support_pro spawns a child agent (for example, to perform deep research on a
customer's issue), the spawn event is recorded. When the child agent returns its
result, the delegation result is logged with a truncated preview. This creates a
complete audit trail of multi-agent interactions.
Together, these four traits transform a basic chat agent into a production-ready system with proactive monitoring, cost controls, searchable documentation, periodic health checks, and full audit logging -- all enforced by the compiler and wired by the runtime.
Summary #
- NeamClaw traits are built-in composable behaviors that extend agent capabilities
using the same
impl Trait for Typesyntax from Chapter 9b. - The trait compatibility matrix determines which traits work with which agent types. The compiler enforces these rules with clear error or warning messages.
- Schedulable enables periodic heartbeat and cron-based scheduling. Required methods:
on_heartbeat(self)(returnsHeartbeatAction) andheartbeat_interval(self)(returns milliseconds). Optional:on_cron(self, cron_id). - Channelable enables multi-channel message routing for claw agents. Required
methods:
channels(self)(returns a list of channel names) andon_message(self, msg)(handles incoming messages). - Sandboxable configures per-execution Docker sandbox parameters. Required method:
sandbox_config(self)(returns a map withmode,network,filesystem,allowed_commands,max_memory_mb). - Monitorable enables behavioral monitoring and anomaly detection. Required methods:
baseline(self)(returns expected metrics) andon_anomaly(self, event)(returnsAnomalyAction). - Orchestrable provides callbacks for multi-agent spawn and delegate events.
Optional methods:
on_spawn(self, child_name)andon_delegate(self, result). - Searchable configures RAG and search behavior. Required method:
search_config(self)(returns a map withstrategy,top_k,min_score). Optional:on_index(self, doc). - Multiple traits can be composed on a single agent through separate
implblocks. The order ofimplblocks does not matter, and each trait operates independently. HeartbeatAction(sealed type withNotifyandSilent) andAnomalyAction(sealed type withAlert,Silence, andDisable) are built-in types that control runtime behavior from within trait methods.
Exercises #
Exercise 27.1: Schedulable Heartbeat
Create a claw agent called health_checker that implements Schedulable with a
30-second heartbeat interval. The on_heartbeat method should read a file called
system_status.txt from the workspace. If the file contains the word "critical",
return HeartbeatAction.Notify with an alert message. Otherwise, return
HeartbeatAction.Silent. Test by writing different status values to the file and
observing the heartbeat output.
Exercise 27.2: Channelable Message Router
Create a claw agent that implements Channelable with three channels: "web",
"mobile", and "api". In on_message, route messages differently based on the
channel type: web messages get verbose responses, mobile messages get brief responses
(under 100 characters), and API messages get JSON-formatted responses. Test each
channel path.
Exercise 27.3: Sandboxable Forge Agent
Create a forge agent called script_runner that implements Sandboxable. Configure
the sandbox to allow only node and npm commands, block network access, allow
read-write filesystem access, and limit memory to 256 MB. Write a verify function
that checks whether an output.json file was created in the workspace.
Exercise 27.4: Monitorable with AnomalyAction
Create a claw agent that implements Monitorable with the following baseline:
avg_tool_calls: 2, max_tool_calls: 5, max_cost_per_turn: 0.03. In
on_anomaly, return AnomalyAction.Disable if max_tool_calls is exceeded by
more than double the threshold, AnomalyAction.Alert for cost anomalies, and
AnomalyAction.Silence for response time anomalies during off-peak hours. Write
the logic for determining off-peak hours.
Exercise 27.5: Orchestrable Audit Trail
Create a claw agent called team_lead that implements Orchestrable. In on_spawn,
log the child agent name, the current timestamp, and the spawn count to a workspace
file. In on_delegate, log the result length and a 50-character preview. Declare two
additional claw agents (worker_a and worker_b) and use spawn to invoke both.
Verify that the audit trail in the workspace file contains all expected entries.
Exercise 27.6: Searchable Strategy Comparison
Create three claw agents -- keyword_bot, vector_bot, and hybrid_bot -- each
implementing Searchable with a different strategy ("keyword", "vector", and
"hybrid" respectively). Give all three the same top_k: 5 and min_score: 0.5.
Connect all three to the same workspace containing several text files. Ask each agent
the same question and compare the quality of their responses. Document your findings
as comments in the code.
Exercise 27.7: Multi-Trait Composition
Create a single claw agent called super_agent that implements all six NeamClaw
traits: Schedulable, Channelable, Sandboxable, Monitorable, Orchestrable,
and Searchable. For each trait, provide a minimal but functional implementation.
Verify that the program compiles without errors. Then remove the Channelable
implementation and confirm that the agent still compiles (since Channelable is not
the only trait). Finally, change the agent to a forge agent and observe which
impl blocks produce compile errors.
Exercise 27.8: Production Monitoring Dashboard
Extend the production-ready example from Section 27.10 by adding a dashboard skill
that reads the anomalies.log, escalations.log, and orchestration.log files from
the workspace and returns a formatted summary. The summary should include: total
anomalies (grouped by metric), total escalations (resolved vs. unresolved), and total
spawn/delegate events. Connect the skill to the agent and ask it: "Show me the
dashboard." Verify that the agent uses the skill and returns a coherent summary.