Programming Neam
📖 21 min read

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:

💠 Why This Matters

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.

neam
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.

neam
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.

neam
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:

Does your agent need conversation history?
Use: agent
(stateless)
Use: claw
agent
(session)
Use: forge
agent
(loop)
💡 Tip

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 #

neam
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:

neam
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:

📝 Note

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).

Here is a claw agent that uses every available configuration field:

neam
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.

🌎 Real-World Analogy

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 #

• Check recursion depth (max 25)
• Check kill switch (emergency stop)
• Run input guard chains
• Verify budget not exhausted
• get_or_create(session_key)
• Check idle timeout (reset if exceeded)
• Check daily reset hour
• append_message(role: "user", content)
• Load system prompt
• Load conversation history from session
• Query semantic memory (if configured)
• Trim to token budget (max_tokens)
• Assemble messages array
• Run output guard chains
• append_message(role: "assistant", text)
• Record metrics (tokens, latency, cost)
• Check if compaction is needed
• Return response string to caller

Let us walk through each step.

Step 1: Security Checks #

Before any LLM inference occurs, the runtime performs a battery of safety checks:

Step 2: Session Management #

The runtime resolves the session for this conversation:

Step 3: Context Building #

The runtime assembles the messages array that will be sent to the LLM:

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() #

neam
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 #

neam
{
  // 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";
}
Common Mistake: Treating Claw Agents Like Stateless Agents

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:

First .ask() call for this session key
→ New session ID generated (UUID)
→ Empty message history initialized
→ Session file created on disk
Each .ask() call:
→ Append user message to history
→ Append assistant response to history
→ Flush to disk (JSONL format)
→ Trigger compaction if needed
Triggered by:
→ Explicit .reset() call
→ Idle timeout (idle_reset_minutes)
→ Daily reset (daily_reset_hour)
→ Manual session clear
Effect:
→ History cleared
→ New session ID assigned
→ Old session archived (not deleted)

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:

jsonl
{"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:

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"
neam
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:

📁.neam/
📁sessions/
📁SupportBot/
📁user_alice/
📄current.jsonl ← Active session
📁archive/
📄2026-02-17_a1b2c3.jsonl
📄2026-02-16_d4e5f6.jsonl
📁memory/
📄semantic.idx ← Semantic memory index
📁user_bob/
📄current.jsonl
📁archive/
📄_meta.json ← Agent-level metadata

Each user (identified by the session key) gets a separate directory containing:

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:

text
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:

  1. Different users never share a session.
  2. Different channels for the same user maintain independent histories (unless you explicitly configure shared sessions).
  3. Different claw agents never share session storage.
🎯 Try It Yourself

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:

  1. 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.

  2. 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.

  3. 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 #

BEFORE COMPACTION (120 turns, exceeds 80% token budget)

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.

Common Mistake: Setting max_history_turns Too Low

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:

neam
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 #

Claw Agent
.ask()
CLI Channel
┌────────────┐
│ stdin │
│ → agent │
│ → stdout │
└────────────┘
stdin
→ agent
→ stdout
HTTP Channel
┌────────────┐
│ POST /chat │
│ → agent │
│ → JSON │
└────────────┘
POST /chat
→ agent
→ JSON

CLI Channel #

The CLI channel reads user input from stdin and writes agent responses to stdout. It is ideal for local development and testing:

neam
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:

neam
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:

json
{
  "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:

json
{
  "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.
neam
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.

📝 Note

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:

neam
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 #

Request Router
VIP
Lane
Queue:
■ ■
Workers
[W][W]
max: 2
pri:hi
Default
Lane
Queue:
■ ■ ■
Workers
[W][W]
[W][W]
max: 4
pri:norm

Default Lane Values #

If you do not specify a lanes block, the claw agent uses a single default lane:

neam
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:

json
{
  "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.

💡 Tip

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 #

neam
// 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
}
bash
$ 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:

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.

💡 Tip

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 #

neam
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:

After: Claw Agent with Built-In Session Management #

neam
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:

  1. Replace agent with claw agent.
  2. Add at least one channel declaration and reference it in channels.
  3. Add a session block with your desired configuration.
  4. Remove all manual history list management code.
  5. Remove all manual context building code.
  6. Replace any custom compaction logic with compaction: "auto".
  7. 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 #

neam
// =====================================================================
// 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).

🎯 Try It Yourself

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:


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:

neam
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.

Start typing to search...