Chapter 24: Claw Agents -- Persistent Conversational Agents #
"The art of conversation is the art of hearing as well as of being heard." -- William Hazlitt
In the preceding chapters, you built stateless agents that forget everything between calls, orchestrated multi-agent pipelines with runners, and deployed them across clouds. These patterns are powerful for single-turn tasks -- classification, data extraction, one-shot Q&A. But many real-world applications demand something more: a conversation. A customer support bot that remembers your order number across messages. A coding assistant that recalls the architecture discussion from earlier in the session. A personal tutor that tracks which concepts the student has mastered and which still need work. Stateless agents cannot do this. They start with a blank slate on every call.
This chapter introduces the claw agent -- Neam's first-class construct for persistent conversational agents. A claw agent maintains sessions across interactions, automatically compacts long conversation histories, routes messages through channels, and partitions concurrent work into lanes. By the end of this chapter, you will understand how to build agents that remember.
By the end of this chapter, you will be able to:
- Distinguish between Neam's three agent types and choose the right one for your use case
- Declare a claw agent with session management, channels, and semantic memory
- Trace the five-step
.ask()pipeline from security checks through response delivery - Configure session persistence with idle resets, daily resets, and compaction strategies
- Understand how auto-compaction summarizes old conversation turns to stay within token budgets
- Declare channels for CLI and HTTP message routing
- Partition concurrent conversations using lanes
- Avoid forbidden fields that cause compile errors on claw agents
- Migrate legacy stateless agents with manual session management to claw agents
- Build a complete, production-ready customer support bot
Think of the difference between a receptionist who greets every visitor as if meeting them for the first time and one who says, "Welcome back, Ms. Chen -- I have your badge ready and Dr. Patel is expecting you in Room 302." The first receptionist is functional. The second is useful. Every stateless agent you have built so far is the first receptionist. A claw agent is the second. It remembers who you are, what you discussed, and where you left off. In customer service, this means fewer repeated explanations. In education, it means personalized learning paths. In software development, it means an assistant that understands your codebase and your preferences. Session persistence is not a luxury feature -- it is what separates a demo from a product.
24.1 The Three Agent Types #
Neam provides three distinct agent constructs, each designed for a fundamentally different execution model. Understanding the differences is essential for choosing the right tool.
Agent (Stateless) #
The agent keyword, which you learned in Chapter 10, creates a stateless agent. Each
.ask() call is independent. There is no conversation history, no session management, and
no lifecycle beyond the single request-response cycle. Stateless agents are the simplest
and most common type.
agent Classifier {
provider: "openai"
model: "gpt-4o-mini"
system: "Classify input as BILLING, TECHNICAL, or GENERAL."
}
Claw Agent (Session) #
The claw agent keyword creates a session-based agent. It persists conversation
history across multiple .ask() calls, manages session lifecycle (creation, compaction,
reset), and supports multi-channel I/O. Claw agents are designed for assistants, chatbots,
and any application where continuity matters.
claw agent Assistant {
provider: "openai"
model: "gpt-4o-mini"
system: "You are a helpful assistant."
channels: [cli_channel]
session: {
idle_reset_minutes: 60
}
}
Forge Agent (Loop) #
The forge agent keyword, covered in Chapter 25, creates an iterative build-verify
agent. It runs a loop of build, verify, and revise steps with fresh context per
iteration, git checkpoints, and plan tracking. Forge agents are designed for TDD coding
workflows, document generation, and long-horizon tasks where drift prevention matters.
forge agent Builder {
provider: "openai"
model: "gpt-4o"
system: "You are a code generator."
verify: check_tests
checkpoint: git
loop: { max_iterations: 10 }
}
Comparison Table #
| Feature | agent |
claw agent |
forge agent |
|---|---|---|---|
| Conversation history | None | Persistent sessions | Fresh per iteration |
| Session management | Manual (if any) | Built-in | Not applicable |
| Auto-compaction | No | Yes | No |
| Channels (CLI/HTTP) | No | Yes | No |
| Lanes (concurrency) | No | Yes | No |
| Semantic memory | No | Yes | No |
| Verification loop | No | No | Yes |
| Git checkpoints | No | No | Yes |
| Plan tracking | No | No | Yes |
| Workspace isolation | No | Optional | Yes |
| Skills and guards | Yes | Yes | Yes |
| Budget constraints | Yes | Yes | Yes |
| Ideal for | Single-turn tasks, RAG, classification | Assistants, chatbots, support bots | TDD, code gen, long tasks |
When-to-Use Decision Tree #
Use the following decision tree to choose the right agent type:
When in doubt, start with a stateless agent. Upgrade to claw agent only
when you need session persistence. Upgrade to forge agent only when you need an
iterative build-verify workflow. The simplest construct that meets your requirements
is always the best choice.
24.2 Claw Agent Declaration #
A claw agent is declared with the claw agent keyword followed by a name and a
configuration block. The syntax mirrors the stateless agent declaration but adds
session-specific fields.
Syntax #
claw agent <Name> {
provider: "<provider>"
model: "<model>"
channels: [<channel_refs>]
// Optional fields
system: "<system_prompt>"
temperature: <float>
skills: [<skill_refs>]
guards: [<guardchain_refs>]
workspace: "<path>"
session: { <session_config> }
lanes: { <lane_config> }
semantic_memory: { <memory_config> }
}
Required Fields #
| Field | Type | Description |
|---|---|---|
provider |
string | LLM provider: "openai", "anthropic", "ollama", etc. |
model |
string | Model identifier (e.g., "gpt-4o-mini", "claude-sonnet-4") |
channels |
list | One or more channel references for message routing |
Optional Fields #
| Field | Type | Default | Description |
|---|---|---|---|
system |
string | "" |
System prompt defining agent behavior |
temperature |
float | 0.7 |
Sampling temperature (0.0 to 2.0) |
skills |
list | [] |
Skills (tools) available to the agent |
guards |
list | [] |
Guard chains for input/output validation |
workspace |
string | "./.neam/workspace" |
Directory for file operations |
session |
map | See 24.4 | Session configuration |
lanes |
map | See 24.7 | Concurrency lane configuration |
semantic_memory |
map | nil |
Semantic memory configuration |
budget |
identifier | nil |
Budget declaration for resource limits |
policy |
identifier | nil |
Security policy |
api_key_env |
string | Provider default | Environment variable for API key |
endpoint |
string | Provider default | Custom API endpoint URL |
Minimal Claw Agent #
Here is the simplest possible claw agent. It requires only a provider, model, and at least one channel:
channel cli_ch {
type: "cli"
}
claw agent MinimalBot {
provider: "ollama"
model: "llama3.2:3b"
channels: [cli_ch]
}
This agent will persist conversation history using default session settings: a 60-minute idle reset, a 4:00 AM daily reset, a maximum of 100 history turns, and automatic compaction.
Let us break this down:
channel cli_ch-- Declares a CLI channel namedcli_chthat reads from stdin and writes to stdout. Channels are covered in detail in Section 24.6.claw agent MinimalBot-- Declares a claw agent namedMinimalBot.channels: [cli_ch]-- Attaches the CLI channel. Every claw agent must have at least one channel.
Unlike stateless agents, claw agents require at least one channel. This is
because claw agents are designed for ongoing conversations that arrive through defined
I/O adapters, not ad-hoc .ask() calls from within a main block (though .ask() is
still supported for programmatic use).
Full-Featured Claw Agent #
Here is a claw agent that uses every available configuration field:
channel support_cli {
type: "cli"
prompt: "customer> "
}
channel support_http {
type: "http"
port: 8080
path: "/chat"
}
skill lookup_order {
description: "Look up an order by ID"
params: { order_id: string }
impl(order_id) {
// In production, query a database
return {
"order_id": order_id,
"status": "shipped",
"eta": "2026-02-20"
};
}
}
skill escalate {
description: "Escalate to a human agent"
params: { reason: string }
impl(reason) {
emit "[ESCALATION] Reason: " + reason;
return "Escalated to human support. A representative will contact you shortly.";
}
}
guard input_filter {
description: "Filters prohibited input patterns"
on_observation(input) {
if (input.contains("ignore previous instructions")) {
return "block";
}
return input;
}
}
guardchain safety = [input_filter];
budget SupportBudget {
api_calls: 500
tokens: 1000000
cost_usd: 25.0
reset: "daily"
}
claw agent SupportBot {
provider: "openai"
model: "gpt-4o-mini"
temperature: 0.5
system: "You are a customer support assistant for Acme Corp.
Be helpful, empathetic, and concise. If you cannot
resolve an issue, use the escalate skill."
channels: [support_cli, support_http]
skills: [lookup_order, escalate]
guards: [safety]
budget: SupportBudget
workspace: "./.neam/support_workspace"
session: {
idle_reset_minutes: 30
daily_reset_hour: 4
max_history_turns: 80
compaction: "auto"
}
lanes: {
default: { concurrency: 4, priority: "normal" }
vip: { concurrency: 2, priority: "high" }
}
semantic_memory: {
backend: "sqlite"
embedding_model: "nomic-embed-text"
search: "hybrid"
top_k: 5
}
}
This declaration configures a production-grade support bot with two channels (CLI for development, HTTP for production), two skills, a guard chain, a budget, session management, lane-based concurrency, and semantic memory for recalling facts from past conversations.
Declaring a claw agent is like setting up a new customer service desk. The provider
and model are the employee and their qualifications. The system prompt is the
training manual. The channels are the phone lines and chat windows where customers
reach the desk. The session config is the filing cabinet where conversation records
are stored. The skills are the procedures the employee can perform. The guards are
the compliance checks. The lanes are the separate queues for regular and VIP customers.
And semantic_memory is the employee's notebook where they jot down important facts for
later recall.
24.3 The .ask() Pipeline #
When you call .ask() on a claw agent, the request passes through a five-step pipeline
before a response is returned. Understanding this pipeline is essential for debugging,
performance tuning, and security analysis.
Pipeline Diagram #
Let us walk through each step.
Step 1: Security Checks #
Before any LLM inference occurs, the runtime performs a battery of safety checks:
- Recursion depth -- If a claw agent's skill triggers another
.ask()call (directly or indirectly), the runtime tracks the recursion depth. If it exceeds 25, the call is rejected with an error. This prevents infinite loops. - Kill switch -- If an administrator has activated the emergency kill switch (via the
/admin/killendpoint or a runtime flag), all.ask()calls return an error immediately. - Input guards -- The guard chains attached to the agent run on the inbound message.
If any guard returns
"block", the pipeline halts and returns an error. - Budget check -- If the agent's budget (API calls, tokens, or cost) is exhausted, the call is rejected.
Step 2: Session Management #
The runtime resolves the session for this conversation:
get_or_create(session_key)-- Looks up an existing session by key. If none exists, creates a new one. The session key is derived from the channel and user identity.- Idle timeout -- If the session has been idle longer than
idle_reset_minutes, the session is reset (history cleared, new session ID assigned). - Daily reset -- If the current hour matches
daily_reset_hourand the session has not been reset today, the session is reset. - Append message -- The user's message is appended to the session history with
role: "user".
Step 3: Context Building #
The runtime assembles the messages array that will be sent to the LLM:
- System prompt -- The
systemfield from the agent declaration. - Conversation history -- All messages from the session, oldest first.
- Semantic memory -- If
semantic_memoryis configured, the runtime queries the memory store with the current user message and injects relevant past facts as additional context. - Token budget -- If the assembled context exceeds 80% of the model's maximum token limit, auto-compaction triggers (see Section 24.5).
Step 4: Tool-Calling Loop #
The assembled messages are sent to the LLM provider. If the LLM responds with a tool call (requesting a skill), the runtime executes the skill locally, appends the result, and re-sends to the LLM. This loop repeats up to 25 times. If the LLM responds with plain text (no tool calls), the loop exits.
Step 5: Response #
The LLM's final text response passes through output guard chains. The response is then
appended to the session history as role: "assistant". Metrics (token count, latency,
cost) are recorded. If the session now exceeds the compaction threshold, a compaction
job is scheduled. The response string is returned to the caller.
Code Example: Calling .ask() #
channel cli_ch {
type: "cli"
}
claw agent Tutor {
provider: "ollama"
model: "llama3.2:3b"
system: "You are a patient programming tutor. Remember what the student
has learned and build on previous topics."
channels: [cli_ch]
session: {
idle_reset_minutes: 120
max_history_turns: 50
}
}
{
// First message -- session is created
let r1 = Tutor.ask("What is a variable?");
emit "Tutor: " + r1;
// Second message -- session persists, history includes first exchange
let r2 = Tutor.ask("Can you give me an example using what you just explained?");
emit "Tutor: " + r2;
// Third message -- the tutor remembers both previous exchanges
let r3 = Tutor.ask("Now explain functions, building on what I already know.");
emit "Tutor: " + r3;
}
In this example, each .ask() call automatically appends the message to the session and
includes the full conversation history in the LLM context. The tutor remembers the
student's previous questions and builds on them.
Methods Reference #
| Method | Signature | Description |
|---|---|---|
.ask() |
ask(prompt: string) -> string |
Send a message and get a response (full pipeline) |
.reset() |
reset() -> void |
Clear session history and start fresh |
.history() |
history() -> list |
Return the current session's message history |
Property Access #
{
// Ask a question
let response = Tutor.ask("What is recursion?");
// Access session history
let messages = Tutor.history();
emit "History length: " + str(len(messages));
// Reset the session
Tutor.reset();
let fresh = Tutor.history();
emit "After reset: " + str(len(fresh)) + " messages";
}
Treating Claw Agents Like Stateless Agents
Beginners sometimes call .ask() on a claw agent and expect each call to be
independent. It is not. Every .ask() appends to the session history, and the full
history is sent to the LLM on the next call. This means: (1) token usage grows with
each turn, (2) older context can influence newer responses in unexpected ways, and
(3) if you want a fresh start, you must call .reset() explicitly. If you need
independent calls, use a stateless agent instead.
24.4 Sessions #
The session is the core abstraction that makes claw agents persistent. A session is a chronologically ordered sequence of messages tied to a unique key.
Session Lifecycle #
Every session goes through three phases:
JSONL Storage Format #
Sessions are persisted to disk in JSONL (JSON Lines) format. Each line in the file represents a single message in the conversation:
{"role":"user","content":"What is a variable?","ts":"2026-02-18T10:00:01Z"}
{"role":"assistant","content":"A variable is a named container...","ts":"2026-02-18T10:00:03Z"}
{"role":"user","content":"Can you give me an example?","ts":"2026-02-18T10:01:15Z"}
{"role":"assistant","content":"Sure! Here is a simple example...","ts":"2026-02-18T10:01:18Z"}
Each line is a self-contained JSON object with three fields:
| Field | Type | Description |
|---|---|---|
role |
string | "user", "assistant", "system", or "tool" |
content |
string | The message text |
ts |
string | ISO 8601 timestamp of when the message was recorded |
The JSONL format has several advantages:
- Append-only -- New messages are appended to the end of the file without rewriting the entire file. This is fast and crash-safe.
- Streamable -- You can read and process messages line by line without loading the entire history into memory.
- Human-readable -- You can inspect session files with standard Unix tools.
- Compactable -- Old messages can be summarized and replaced without complex data structure manipulation (see Section 24.5).
Session Configuration Fields #
The session block inside a claw agent declaration accepts the following fields:
| Field | Type | Default | Description |
|---|---|---|---|
idle_reset_minutes |
integer | 60 |
Minutes of inactivity before session auto-resets |
daily_reset_hour |
integer | 4 |
Hour (0-23) at which sessions reset daily |
max_history_turns |
integer | 100 |
Maximum number of turns (user + assistant pairs) to retain |
compaction |
string | "auto" |
Compaction strategy: "auto", "manual", or "disabled" |
claw agent ChatBot {
provider: "openai"
model: "gpt-4o-mini"
channels: [cli_ch]
session: {
idle_reset_minutes: 30 // Reset after 30 minutes of silence
daily_reset_hour: 3 // Reset at 3:00 AM daily
max_history_turns: 50 // Keep at most 50 turns
compaction: "auto" // Let the runtime decide when to compact
}
}
Session Directory Structure #
Sessions are stored in a structured directory hierarchy under the .neam/sessions/
directory:
Each user (identified by the session key) gets a separate directory containing:
current.jsonl-- The active session file, updated on every.ask()call.archive/-- Completed sessions that were reset, named with date and session ID.memory/-- Semantic memory index files (ifsemantic_memoryis configured).
Session Key and Isolation #
The session key determines which session file a message belongs to. By default, the key is derived from the channel and user identity:
session_key = "<agent_name>/<channel_type>_<user_id>"
For example, if user_alice sends a message through support_cli, the session key is
SupportBot/cli_user_alice. If the same user sends a message through support_http,
it creates a separate session: SupportBot/http_user_alice.
This isolation ensures that:
- Different users never share a session.
- Different channels for the same user maintain independent histories (unless you explicitly configure shared sessions).
- Different claw agents never share session storage.
Declare a claw agent with idle_reset_minutes: 2 (two minutes). Send two messages with
a one-minute pause between them -- the second message should reference the first, and the
agent should remember. Then wait three minutes and send a third message. The agent should
have reset and no longer remember the previous conversation. This demonstrates idle
timeout in action.
24.5 Auto-Compaction #
Conversation history grows with every turn. A claw agent that has been running for hours can accumulate hundreds of messages, easily exceeding the model's context window. Auto- compaction solves this by summarizing old turns into a concise digest.
When Compaction Triggers #
Auto-compaction triggers when the assembled context (system prompt + history + semantic memory) exceeds 80% of the model's maximum token limit. This threshold provides a buffer so that the current user message and the assistant's response still have room.
Compaction Strategy #
The compaction process follows a three-step strategy:
-
Select -- The oldest two-thirds of conversation turns are selected for compaction. The most recent turns (controlled by
keep_recent, default 20 turns) are always preserved verbatim. -
Summarize -- The selected turns are sent to the LLM with a special compaction prompt: "Summarize the following conversation, preserving all key facts, decisions, and context that would be needed to continue the conversation coherently." The LLM produces a concise summary.
-
Replace -- The selected turns in the session file are replaced with a single message of
role: "system"containing the summary. The recent turns remain unchanged.
Before and After Compaction #
Configuration #
The keep_recent parameter is not directly configurable in the session block -- it
defaults to 20 turns. The compaction strategy itself is controlled by the compaction
field:
| Value | Behavior |
|---|---|
"auto" |
Runtime triggers compaction when context exceeds 80% of max tokens |
"manual" |
Compaction only runs when you explicitly call .compact() |
"disabled" |
No compaction; oldest messages are simply dropped when max_history_turns is exceeded |
Pre-Compaction Flush #
Before compaction begins, the runtime flushes all pending session writes to disk. This ensures that no messages are lost if the compaction process involves writing a new summary message and removing old entries. The flush is atomic -- either all pending writes succeed, or none do.
Setting max_history_turns Too Low
If you set max_history_turns to a very low value (such as 10) with compaction:
"disabled", the agent will lose important context after just 10 exchanges. This leads
to the agent "forgetting" critical information mid-conversation. If you need a short
history window, use compaction: "auto" so that old context is summarized rather than
discarded.
Tip: Auto-compaction uses one additional LLM call to generate the summary. This
adds latency and cost to the turn that triggers compaction. For latency-sensitive
applications, consider setting max_history_turns high enough that compaction rarely
triggers during a single session.
24.6 Channels #
A channel is the I/O adapter through which messages enter and leave a claw agent. Channels decouple the agent's logic from the transport mechanism, allowing the same agent to serve users over CLI during development and HTTP in production.
Channel Declaration #
Channels are declared at the top level of a Neam file using the channel keyword:
channel <name> {
type: "<channel_type>"
// Type-specific configuration
}
Channel Types #
Neam supports two channel types:
| Type | Transport | Use Case |
|---|---|---|
"cli" |
stdin/stdout | Development, testing, local interactive use |
"http" |
HTTP REST API | Production deployments, web integrations |
Channel Type Diagram #
CLI Channel #
The CLI channel reads user input from stdin and writes agent responses to stdout. It is ideal for local development and testing:
channel dev_cli {
type: "cli"
prompt: "you> " // Custom input prompt (default: "> ")
greeting: "Hello! How can I help you today?"
}
| Field | Type | Default | Description |
|---|---|---|---|
type |
string | Required | Must be "cli" |
prompt |
string | "> " |
Input prompt displayed to the user |
greeting |
string | "" |
Message displayed when the channel starts |
HTTP Channel #
The HTTP channel exposes a REST endpoint that accepts JSON messages and returns JSON responses:
channel api_channel {
type: "http"
port: 8080 // Port to listen on
path: "/v1/chat" // URL path for the endpoint
cors: true // Enable CORS headers
}
| Field | Type | Default | Description |
|---|---|---|---|
type |
string | Required | Must be "http" |
port |
integer | 8080 |
Port number |
path |
string | "/chat" |
URL path for the chat endpoint |
cors |
bool | false |
Enable Cross-Origin Resource Sharing headers |
Message Format #
All channels use a standardized message format internally. Inbound messages are
normalized to InboundMessage, and responses are returned as OutboundMessage:
InboundMessage:
{
"channel": "api_channel",
"user_id": "user_alice",
"session_key": "SupportBot/http_user_alice",
"content": "Where is my order #12345?",
"timestamp": "2026-02-18T14:30:00Z",
"metadata": {
"source_ip": "192.168.1.100"
}
}
OutboundMessage:
{
"channel": "api_channel",
"session_key": "SupportBot/http_user_alice",
"content": "Your order #12345 was shipped on Feb 17 and is expected to arrive by Feb 20.",
"timestamp": "2026-02-18T14:30:02Z",
"metadata": {
"tokens_used": 342,
"latency_ms": 1847
}
}
DM and Group Policies #
Channels support two messaging policies:
| Policy | Behavior | Configuration |
|---|---|---|
| DM (Direct Message) | One user per session. Each user gets an isolated conversation. | Default behavior. |
| Group | Multiple users share a single session. All messages from the group are in one history. | Set policy: "group" in the channel block. |
channel team_chat {
type: "http"
port: 8080
path: "/team"
policy: "group" // All users in this channel share one session
}
Group policy is useful for team assistants where multiple people contribute to the same conversation -- for example, a shared support queue or a collaborative brainstorming session.
In DM policy (the default), each user's user_id is included in the session
key, ensuring complete isolation. In group policy, the session key is derived from the
channel name alone, so all users share the same history.
24.7 Lanes #
When a claw agent serves many users concurrently, you need control over how requests are queued and processed. Lanes provide concurrency partitioning within a single claw agent.
What Lanes Solve #
Without lanes, all incoming requests are processed in a single queue. A burst of low- priority requests can starve high-priority users. Lanes let you define separate queues with independent concurrency limits and priority levels.
Lane Configuration Syntax #
Lanes are configured in the lanes block of a claw agent declaration:
claw agent PriorityBot {
provider: "openai"
model: "gpt-4o-mini"
channels: [cli_ch]
lanes: {
default: { concurrency: 4, priority: "normal" }
vip: { concurrency: 2, priority: "high" }
batch: { concurrency: 1, priority: "low" }
}
}
Each lane is a named entry with two fields:
| Field | Type | Default | Description |
|---|---|---|---|
concurrency |
integer | 4 |
Maximum number of simultaneous requests in this lane |
priority |
string | "normal" |
Priority level: "low", "normal", or "high" |
LaneQueueEngine Diagram #
Default Lane Values #
If you do not specify a lanes block, the claw agent uses a single default lane:
lanes: {
default: { concurrency: 4, priority: "normal" }
}
This means up to four requests can be processed simultaneously, all at normal priority.
Assigning Requests to Lanes #
Requests are assigned to lanes based on metadata in the InboundMessage. The lane
field in the message metadata determines which lane processes the request:
{
"content": "Where is my order?",
"metadata": {
"lane": "vip"
}
}
If no lane field is present, the request goes to the default lane. Your application
logic (or a middleware in the HTTP channel) is responsible for setting the lane based on
user attributes such as subscription tier, account priority, or request type.
Lanes do not affect session isolation. Two requests in different lanes for the same user still share the same session. Lanes control throughput and priority, not data isolation.
24.8 Forbidden Fields #
Claw agents enforce strict type safety by forbidding certain fields that belong
exclusively to forge agents. If you include any of these fields in a claw agent
declaration, the compiler rejects the program with a clear error message.
Forbidden Fields Table #
| Field | Belongs To | Compile Error Message |
|---|---|---|
verify |
forge agent |
E4001: "verify" is not valid on claw agents. Use "forge agent" for verification loops. |
checkpoint |
forge agent |
E4002: "checkpoint" is not valid on claw agents. Use "forge agent" for git checkpoints. |
loop |
forge agent |
E4003: "loop" is not valid on claw agents. Use "forge agent" for iterative build loops. |
Example #
// This will NOT compile
claw agent BadBot {
provider: "openai"
model: "gpt-4o-mini"
channels: [cli_ch]
verify: check_tests // E4001: compile error
loop: { max_iterations: 5 } // E4003: compile error
}
$ neamc bad_bot.neam -o bad_bot.neamb
error[E4001]: "verify" is not valid on claw agents.
--> bad_bot.neam:6:3
|
6 | verify: check_tests
| ^^^^^^ Use "forge agent" for verification loops.
error[E4003]: "loop" is not valid on claw agents.
--> bad_bot.neam:7:3
|
7 | loop: { max_iterations: 5 }
| ^^^^ Use "forge agent" for iterative build loops.
Why These Restrictions Exist #
The three agent types -- agent, claw agent, and forge agent -- have fundamentally
different execution models. A claw agent is built around persistent sessions and
multi-turn conversations. A forge agent is built around iterative build-verify loops
with fresh context per iteration. These models are incompatible:
verifyimplies an external verification callback that determines success or failure of each iteration. Claw agents do not have iterations.checkpointimplies git-based snapshotting between iterations. Claw agents persist conversation history, not code artifacts.loopimplies an iteration-based execution model withmax_iterationsand fresh context per cycle. Claw agents accumulate context, not reset it.
By making these restrictions compile-time errors rather than runtime warnings, Neam ensures that you cannot accidentally create an agent with contradictory semantics. The type system guides you to the correct agent type for your use case.
If you find yourself wanting both session persistence and build-verify loops, consider using a claw agent that delegates to a forge agent via handoff. The claw agent manages the conversation, and the forge agent handles the iterative build task.
24.9 Migration from Legacy Agents #
If you have an existing codebase that uses stateless agent declarations with manual
session management (storing history in a list and passing it to each .ask() call), you
can migrate to claw agent for cleaner code, automatic compaction, and built-in session
lifecycle management.
Before: Stateless Agent with Manual Session Management #
agent OldAssistant {
provider: "openai"
model: "gpt-4o-mini"
system: "You are a helpful assistant."
}
{
// Manual session management
let history = [];
let q1 = "What is a closure?";
push(history, {"role": "user", "content": q1});
let r1 = OldAssistant.ask(q1);
push(history, {"role": "assistant", "content": r1});
emit "A: " + r1;
let q2 = "Can you give me an example?";
push(history, {"role": "user", "content": q2});
// Manually build context with history
let context = "";
for (msg in history) {
context = context + msg["role"] + ": " + msg["content"] + "\n";
}
let r2 = OldAssistant.ask(context + "\n" + q2);
push(history, {"role": "assistant", "content": r2});
emit "A: " + r2;
}
This approach has several problems:
- You must manually maintain the history list.
- Context building is error-prone and does not respect token limits.
- There is no automatic compaction, idle reset, or session persistence across runs.
- Session isolation between users must be implemented manually.
After: Claw Agent with Built-In Session Management #
channel cli_ch {
type: "cli"
}
claw agent NewAssistant {
provider: "openai"
model: "gpt-4o-mini"
system: "You are a helpful assistant."
channels: [cli_ch]
session: {
idle_reset_minutes: 60
max_history_turns: 100
compaction: "auto"
}
}
{
let r1 = NewAssistant.ask("What is a closure?");
emit "A: " + r1;
// History is automatically maintained and included
let r2 = NewAssistant.ask("Can you give me an example?");
emit "A: " + r2;
}
The claw agent version is shorter, safer, and more capable:
| Concern | Manual (Before) | Claw Agent (After) |
|---|---|---|
| History tracking | Manual list manipulation | Automatic |
| Context building | Manual string concatenation | Automatic with token budgeting |
| Token budget | Not handled | Auto-compaction at 80% |
| Session persistence | Not implemented | Built-in JSONL on disk |
| Idle reset | Not implemented | Configurable timeout |
| Multi-user isolation | Not implemented | Automatic via session keys |
| Lines of code | ~20 | ~8 |
Migration Checklist #
When migrating from manual session management to a claw agent:
- Replace
agentwithclaw agent. - Add at least one
channeldeclaration and reference it inchannels. - Add a
sessionblock with your desired configuration. - Remove all manual history list management code.
- Remove all manual context building code.
- Replace any custom compaction logic with
compaction: "auto". - Test that the agent remembers context across multiple
.ask()calls.
24.10 Real-World Example: Customer Support Bot #
Let us build a complete, production-ready customer support bot that demonstrates every claw agent feature covered in this chapter. This example is intentionally comprehensive -- it uses skills, guards, channels, sessions, semantic memory, a Monitorable trait implementation, and multi-turn conversation.
Complete Source Code #
// =====================================================================
// File: support_bot.neam
// A production-ready customer support claw agent
// =====================================================================
// === Channel ===
channel support_cli {
type: "cli"
prompt: "customer> "
greeting: "Welcome to Acme Support! How can I help you today?"
}
// === Skills ===
skill lookup_order {
description: "Look up order status by order ID"
params: { order_id: string }
impl(order_id) {
// In production, this would query a database or API
let orders = {
"ORD-1001": {"status": "shipped", "eta": "2026-02-20", "carrier": "FedEx"},
"ORD-1002": {"status": "processing", "eta": "2026-02-25", "carrier": "pending"},
"ORD-1003": {"status": "delivered", "eta": "2026-02-15", "carrier": "UPS"}
};
if (orders[order_id] != nil) {
let o = orders[order_id];
return f"Order {order_id}: Status={o['status']}, ETA={o['eta']}, Carrier={o['carrier']}";
}
return f"Order {order_id} not found. Please verify the order ID.";
}
}
skill escalate {
description: "Escalate the conversation to a human support agent"
params: { reason: string, priority: string }
impl(reason, priority) {
emit f"[ESCALATION] Priority: {priority} | Reason: {reason}";
return f"I have escalated your case to our support team (Priority: {priority}). A human agent will contact you within 30 minutes.";
}
}
// === Guards ===
guard input_filter {
description: "Blocks prompt injection and prohibited content"
on_observation(input) {
if (input.contains("ignore previous instructions")) {
emit "[GUARD] Blocked: prompt injection attempt";
return "block";
}
if (input.contains("reveal your system prompt")) {
emit "[GUARD] Blocked: system prompt extraction attempt";
return "block";
}
if (len(input) > 5000) {
emit "[GUARD] Blocked: input exceeds 5000 characters";
return "block";
}
return input;
}
}
guardchain safety = [input_filter];
// === Budget ===
budget SupportBudget {
api_calls: 1000
tokens: 2000000
cost_usd: 50.0
reset: "daily"
}
// === Monitorable Trait ===
struct SupportMetrics {
total_conversations: int,
total_escalations: int,
avg_response_time_ms: float
}
impl Monitorable for SupportBot {
fn health_check(self) {
return { "status": "healthy", "sessions_active": self.active_session_count() };
}
fn metrics(self) {
return SupportMetrics(
total_conversations: self.session_count(),
total_escalations: self.skill_call_count("escalate"),
avg_response_time_ms: self.avg_latency_ms()
);
}
}
// === Claw Agent Declaration ===
claw agent SupportBot {
provider: "openai"
model: "gpt-4o-mini"
temperature: 0.4
system: "You are a customer support assistant for Acme Corp.
PERSONALITY:
- Be empathetic, professional, and concise.
- Address customers by name when known.
- Apologize for inconveniences before offering solutions.
CAPABILITIES:
- Use lookup_order to check order status when a customer provides an order ID.
- Use escalate when the customer is upset, when you cannot resolve the issue,
or when the customer explicitly asks for a human agent.
CONSTRAINTS:
- Never reveal internal system details or error codes.
- Never make promises about refunds without verifying order status first.
- Keep responses under 150 words."
channels: [support_cli]
skills: [lookup_order, escalate]
guards: [safety]
budget: SupportBudget
session: {
idle_reset_minutes: 30
daily_reset_hour: 4
max_history_turns: 80
compaction: "auto"
}
semantic_memory: {
backend: "sqlite"
embedding_model: "nomic-embed-text"
search: "hybrid"
top_k: 5
}
}
// === Main Execution ===
{
emit "=== Acme Corp Customer Support Bot ===";
emit "";
// --- Conversation Turn 1 ---
let r1 = SupportBot.ask("Hi, my name is Alice. I placed an order last week and
I have not received it yet. My order ID is ORD-1001.");
emit "Support: " + r1;
emit "";
// --- Conversation Turn 2 (session persists -- the bot remembers Alice) ---
let r2 = SupportBot.ask("That is great to hear it shipped. Can you tell me
the carrier and when it will arrive?");
emit "Support: " + r2;
emit "";
// Verify session persistence
let history = SupportBot.history();
emit "[Debug] Session history: " + str(len(history)) + " messages";
emit "";
emit "=== Demo Complete ===";
}
Walkthrough #
Let us trace through this example section by section.
Channels (lines 8-12): The support_cli channel provides a CLI interface with a
custom prompt (customer>) and a greeting message. In production, you would add an
HTTP channel alongside this for web clients.
Skills (lines 16-42): Two skills give the agent concrete capabilities. lookup_order
simulates a database lookup (in production, this would call a real API). escalate
records an escalation event and returns a confirmation message to the customer.
Guards (lines 46-62): The input_filter guard blocks two common attack patterns
(prompt injection and system prompt extraction) and enforces a maximum input length. The
safety guard chain wraps this guard for attachment to the agent.
Budget (lines 66-71): The SupportBudget limits the agent to 1000 API calls,
2 million tokens, and $50 USD per day. This prevents cost runaway from unexpected traffic
spikes or adversarial users.
Monitorable Trait (lines 75-92): The impl Monitorable for SupportBot block
implements the Monitorable trait from Neam's standard library. This exposes
health_check() and metrics() methods that monitoring systems (Prometheus, Grafana)
can scrape. The metrics include total conversations, escalation count, and average
response latency.
Claw Agent Declaration (lines 96-135): The SupportBot declaration ties everything
together. The system prompt defines the agent's personality, capabilities, and
constraints in structured sections. The session block configures a 30-minute idle
timeout, 4:00 AM daily reset, 80-turn history limit, and automatic compaction. The
semantic_memory block enables hybrid search (combining keyword and vector similarity)
over past conversation facts.
Main Execution (lines 139-158): Two .ask() calls demonstrate session persistence.
The first message introduces Alice and asks about her order. The agent uses the
lookup_order skill to check the status. The second message references the first
("That is great to hear it shipped") -- the agent can answer because the full
conversation history is in the session. Finally, the .history() call verifies that the
session contains all four messages (two user, two assistant).
Extend this example by adding a third .ask() call where the customer says they are
unhappy and wants to speak to a human. The agent should use the escalate skill
automatically based on the customer's tone. After the escalation, call .history()
again and verify that the escalation exchange is included in the session.
Summary #
In this chapter, you learned:
-
Neam provides three agent types:
agent(stateless, single-turn),claw agent(session-based, persistent conversations), andforge agent(iterative build-verify loops). Each type is a first-class language construct with distinct semantics. -
A claw agent is declared with the
claw agentkeyword and requires at least one channel. Optional fields includesession,lanes,semantic_memory,skills,guards, andbudget. -
The
.ask()pipeline follows five steps: security checks (recursion depth, kill switch, guards, budget), session management (get or create, timeout checks, append), context building (system prompt, history, semantic memory, token budget), the tool-calling loop (up to 25 iterations), and response (output guards, session append, metrics). -
Sessions are the persistence mechanism for claw agents. They are stored in JSONL format on disk, organized by agent name and session key. Sessions support idle resets, daily resets, and configurable turn limits.
-
Auto-compaction triggers when context exceeds 80% of the model's maximum tokens. The oldest two-thirds of turns are summarized by the LLM into a single system message, preserving the most recent 20 turns verbatim. Compaction can be
"auto","manual", or"disabled". -
Channels are I/O adapters (CLI or HTTP) that decouple message transport from agent logic. Each channel normalizes messages into
InboundMessage/OutboundMessageformat. Channels support DM (default, user-isolated) and group (shared session) policies. -
Lanes partition concurrent requests into separate queues with independent concurrency limits and priority levels (
"low","normal","high"). High-priority lanes are scheduled before lower-priority ones. -
Forbidden fields (
verify,checkpoint,loop) cause compile-time errors on claw agents. These fields belong exclusively to forge agents. This restriction is enforced by the type system. -
Migration from stateless agents with manual session management to claw agents eliminates manual history tracking, context building, compaction, and session lifecycle code.
-
A production claw agent combines skills, guards, budgets, channels, session configuration, semantic memory, and trait implementations into a single, cohesive declaration.
Exercises #
Exercise 24.1: Minimal Claw Agent Declare a claw agent with an Ollama provider, a CLI channel, and no optional fields. Ask it two questions where the second question references the first. Verify that the agent remembers the first exchange by inspecting the response.
Exercise 24.2: Session Reset Experiment
Create a claw agent with idle_reset_minutes: 1. Send a message, wait 90 seconds
(use a shell sleep command between runs), and send another message that references the
first. Verify that the session was reset and the agent does not remember the first
message. Then repeat with idle_reset_minutes: 5 and verify that the session persists.
Exercise 24.3: Multi-Channel Agent
Declare a claw agent with both a CLI channel and an HTTP channel. Write a main block
that sends a message through the CLI channel using .ask(). Then, using curl or a
similar tool, send a message to the HTTP endpoint. Verify that the two channels maintain
separate sessions (the HTTP message should not reference the CLI conversation).
Exercise 24.4: Custom Compaction Threshold
Create a claw agent with max_history_turns: 10 and compaction: "auto". Send 15
messages in a loop, each asking the agent to remember a specific number. After all 15
messages, ask the agent to recall the numbers. Inspect which numbers are remembered
(recent ones) and which are only referenced in the compaction summary.
Exercise 24.5: Lane Priority Testing
Declare a claw agent with three lanes: urgent (concurrency 1, priority high), normal
(concurrency 2, priority normal), and background (concurrency 1, priority low). Write
a test that sends requests to each lane and measures response times. Verify that urgent
requests are served before background requests when the system is under load.
Exercise 24.6: Guard Integration
Write a claw agent with an input guard that blocks messages containing the word
"password" and an output guard that redacts any string matching the pattern "sk-". Test
with three inputs: a normal question, a message containing "password" (should be blocked),
and a prompt that might cause the agent to mention an API key (the output should be
redacted).
Exercise 24.7: Migration Exercise Take the following stateless agent with manual session management and migrate it to a claw agent. Verify that the behavior is identical but the code is shorter:
agent LegacyBot {
provider: "ollama"
model: "llama3.2:3b"
system: "You are a trivia assistant."
}
{
let history = [];
let q1 = "What is the tallest mountain?";
push(history, {"role": "user", "content": q1});
let r1 = LegacyBot.ask(q1);
push(history, {"role": "assistant", "content": r1});
emit r1;
let q2 = "How tall is it in meters?";
push(history, {"role": "user", "content": q2});
let ctx = "";
for (msg in history) {
ctx = ctx + msg["role"] + ": " + msg["content"] + "\n";
}
let r2 = LegacyBot.ask(ctx + q2);
emit r2;
}
Exercise 24.8: Full Production Bot
Build a complete claw agent for a restaurant reservation system. The agent should:
1. Have a skill check_availability that checks table availability for a given date,
time, and party size.
2. Have a skill make_reservation that books a table and returns a confirmation number.
3. Have a guard that blocks messages longer than 500 characters.
4. Use session persistence with idle_reset_minutes: 15.
5. Use semantic memory to recall returning customers' preferences (e.g., "I usually sit
by the window").
6. Handle a three-turn conversation: customer asks about availability, confirms the
booking, and then asks to change the time. Verify that the agent remembers the
original reservation details when processing the change request.