Normandy.DSL.Agent
(normandy v2.2.0)
View Source
DSL for defining AI agents with a clean, declarative syntax.
Provides macros to simplify agent creation with sensible defaults and expressive configuration options.
Examples
defmodule MyResearchAgent do
use Normandy.DSL.Agent
agent do
name "Research Agent"
description "Conducts research and analysis"
model "claude-opus-5-5"
max_tokens 16_000
system_prompt """
You are a helpful research assistant.
Provide thorough, well-researched answers.
"""
# Optional: Define tools
tool CalculatorTool
tool SearchTool
end
end
# Create an instance
agent = MyResearchAgent.new(client: my_client)
# Run the agent
{:ok, updated_agent, response} = MyResearchAgent.run(agent, "Research quantum computing")Without DSL (for comparison)
config = %{
client: my_client,
model: "claude-opus-5-5",
max_tokens: 16_000
}
agent = Normandy.Agents.BaseAgent.init(config)
# ... configure system prompt, tools, etc.Features
- Clean, declarative syntax
- Sensible defaults
- Configuration checked when the agent is built (
new/1raises without a model) - Checked when the module compiles: an
agentblock withsystem_promptbesidebackground,stepsoroutput_instructionsraisesArgumentError - Generated helper functions
- Tool registration support
- Memory management helpers
hooks- lifecycle hook modules (Normandy.Behaviours.Hooks),[{Mod, opts}]
Name clashes
use Normandy.DSL.Agent imports every macro of this module, hooks/1 included. A
module that defines its own hooks/1 compiles, but an unqualified local call
hooks(...) inside it is a compile error (it matches both the local function and the
import). Keep the DSL's hooks by calling it
qualified, and drop it from the import:
defmodule MyAgent do
use Normandy.DSL.Agent
import Normandy.DSL.Agent, except: [hooks: 1]
def hooks(x), do: x
agent do
model "claude-opus-5-5"
Normandy.DSL.Agent.hooks [{MyApp.Hooks.Audit, []}]
end
endA second import of the same module replaces the first, so use stays as it is.
Summary
Functions
Defines an agent configuration block.
Sets the background context.
Sets the agent description.
Configures guardrails for the agent.
Declares lifecycle hooks (Normandy.Behaviours.Hooks): an ordered list of
{module, opts} entries, once per agent. They run before any hooks in a
behaviours: given to new/1. A literal list is checked at compile time (shape,
:on_error, plain-data opts); modules and their validate_opts!/1 are checked when
new/1 builds the agent.
Sets the maximum number of messages in memory.
Sets the max tokens.
Sets the maximum number of tool calls executed concurrently inside a single
tool-loop iteration. Defaults to 1 (sequential — matches pre-0.5.0
observable behaviour). Values > 1 opt the agent into bounded parallel tool
execution with up to N tools running at once. The final result list passed
back to the LLM stays in the LLM's tool-call order (Task.async_stream is
invoked with ordered: true).
Declares persistent memory (Normandy.Tools.Memory). Takes Memory.new/1's options
except :store, which a handle such as a Redis connection cannot supply at compile
time: pass memory_store: {module, handle} to new/1. The namespace comes from
tool_context (new/1's :tool_context).
Sets the LLM model.
Sets the agent name.
Sets the output instructions.
Sets the internal steps.
Sets the chunk size (bytes of accumulated text) at which incremental output
guardrails run. Only takes effect when streaming_mode :incremental is set.
Default: 200.
Selects the output-guardrail streaming mode for this agent.
Sets the system prompt.
Sets the temperature.
Registers a tool: new/1 registers %module{} with opts
(Normandy.Tools.Toolset.register/3's options, e.g. timeout: 60_000). A tool whose
struct needs configuration is registered at runtime with BaseAgent.register_tool/3.
Functions
Defines an agent configuration block.
Available Options
name- Agent name (optional)description- Agent description (optional)model- LLM model to use (required)temperature- Sampling temperature, or omit it to send none (default: none; Opus 4.7+ reject sampling parameters)max_tokens- Maximum tokens in response (default: 16_000)system_prompt- System prompt text (optional). Not together withbackground,stepsoroutput_instructions: the module does not compilebackground- Background context for prompt (optional)steps- Internal reasoning steps (optional)output_instructions- Output formatting instructions (optional)tool- Register a tool:tool MyToolregisters%MyTool{},tool MyTool, timeout: 60_000passes registration options (Normandy.Tools.Toolset.register/3); can be called multiple timesmax_messages- Maximum messages in memory (optional)
Examples
agent do
name "My Agent"
model "claude-opus-5-5"
system_prompt "You are helpful."
end
Sets the background context.
Sets the agent description.
Configures guardrails for the agent.
stage is either :input or :output. specs is a list of guard
specifications — either a module atom or {module, opts} tuple. See
Normandy.Guardrails for the full contract.
Calling this macro twice for the same stage replaces the previous list.
An input violation always fails the run. An output violation blocks the answer
by default: run returns {:error, %Normandy.Guardrails.ViolationError{}}
(run! raises it) and the answer is not saved to memory. Pass
on_violation: :warn (output only) to log the violation and return the answer
instead. Telemetry is emitted either way.
Streaming output (stream/3) reports violations through the
:guardrail_violation stream event and telemetry instead; see Normandy.Guardrails.
Example
agent do
model "claude-opus-5-5"
guardrails :input, [
{Normandy.Guardrails.Builtins.MaxLength, limit: 10_000},
{Normandy.Guardrails.Builtins.ForbiddenSubstrings, terms: ["ignore previous"]}
]
guardrails :output, [
{Normandy.Guardrails.Builtins.RegexGuard,
patterns: [~r/\b\d{3}-\d{2}-\d{4}\b/], mode: :deny}
]
end
Declares lifecycle hooks (Normandy.Behaviours.Hooks): an ordered list of
{module, opts} entries, once per agent. They run before any hooks in a
behaviours: given to new/1. A literal list is checked at compile time (shape,
:on_error, plain-data opts); modules and their validate_opts!/1 are checked when
new/1 builds the agent.
hooks [{MyApp.Hooks.Redact, []}, {MyApp.Hooks.Audit, on_error: :open}]
Sets the maximum number of messages in memory.
Sets the max tokens.
Sets the maximum number of tool calls executed concurrently inside a single
tool-loop iteration. Defaults to 1 (sequential — matches pre-0.5.0
observable behaviour). Values > 1 opt the agent into bounded parallel tool
execution with up to N tools running at once. The final result list passed
back to the LLM stays in the LLM's tool-call order (Task.async_stream is
invoked with ordered: true).
Streaming callback ordering: BaseAgent.stream/3 delivers
%StreamEvent{type: :tool_result} from inside each worker as soon as that
tool finishes. At max_tool_concurrency > 1, callers therefore observe
:tool_result events in completion order, not LLM-call order — a
100ms tool that fires after a 500ms tool will still emit its :tool_result
first. If you need LLM-order callback handling, set max_tool_concurrency: 1 or buffer events and reorder them yourself after the stream completes.
Process-semantics note: tool invocations run inside Task.async_stream
workers, even at max_tool_concurrency: 1. Streaming callbacks fired while
a tool is executing therefore run in those worker processes — capture
parent = self() outside the callback before sending messages to the
test/owner process. Streaming callbacks fired outside tool execution (e.g.
:text_delta before any tool_use) still run in the caller's process.
Declares persistent memory (Normandy.Tools.Memory). Takes Memory.new/1's options
except :store, which a handle such as a Redis connection cannot supply at compile
time: pass memory_store: {module, handle} to new/1. The namespace comes from
tool_context (new/1's :tool_context).
Sets the LLM model.
Sets the agent name.
Sets the output instructions.
Sets the internal steps.
Sets the chunk size (bytes of accumulated text) at which incremental output
guardrails run. Only takes effect when streaming_mode :incremental is set.
Default: 200.
Selects the output-guardrail streaming mode for this agent.
:accumulate (default) runs guards on the full message after the stream
completes; :incremental runs guards at chunk boundaries and halts the
stream on violation. See Normandy.Guardrails for the full semantics.
Sets the system prompt.
Sets the temperature.
Registers a tool: new/1 registers %module{} with opts
(Normandy.Tools.Toolset.register/3's options, e.g. timeout: 60_000). A tool whose
struct needs configuration is registered at runtime with BaseAgent.register_tool/3.