Skip to content

AgentConfig and create_agent()

from lionagi.agent import AgentConfig, create_agent, PermissionPolicy
from lionagi.agent.hooks import guard_destructive, guard_paths, log_tool_use

AgentConfig captures what a coding agent needs — model, tools, hooks, permissions, system prompt — in a single serializable object. create_agent() wires it into a ready-to-use Branch.


AgentConfig

@dataclass
class AgentConfig

Source: lionagi/agent/config.py

Fields

Field Type Default Notes
name str "agent" Human label for the agent
model str \| None None Model spec: "provider/model" or bare alias
effort str \| None None Override effort level (e.g. "high", "xhigh")
system_prompt str "" System prompt text
tools list[str] [] Tool presets to register: "coding", "reader", "editor", "bash", "search"
hook_handlers dict[str, list[Callable]] {} Phase-keyed hooks ("pre:bash", "post:*", "error:editor")
permissions dict \| PermissionPolicy {} Permission rules; see PermissionPolicy
mcp_servers list[str] \| None None MCP server names to load from .mcp.json
mcp_config_path str \| None None Explicit path to .mcp.json (overrides auto-discovery)
max_extensions int 20 Max tool-use rounds per agent invocation
yolo bool False Auto-approve all tool calls (pass-through to provider kwargs)
lion_system bool True Prepend lionagi system preamble to system_prompt
cwd str \| None None Working directory for tools and MCP discovery
extra dict {} Additional YAML fields preserved on round-trip

Hook methods

config.pre("bash", handler)       # register a pre-hook for the bash tool
config.post("editor", handler)    # register a post-hook for the editor tool
config.on_error("*", handler)     # register an error hook for all tools
  • pre hooks: async (tool_name: str, action: str, args: dict) -> dict | None Return a modified args dict to rewrite the call, or raise PermissionError to block.
  • post hooks: async (tool_name: str, action: str, args: dict, result: dict) -> dict | None Return a modified result dict, or None to pass through unchanged.
  • Tool name "*" matches all tools.

Preset methods

AgentConfig.coding()

@classmethod
def coding(
    cls,
    name: str = "coder",
    model: str | None = None,
    effort: str | None = "high",
    system_prompt: str | None = None,
    cwd: str | None = None,
    **kwargs,
) -> AgentConfig

Preset for a coding agent. Registers tools=["coding"] (reader, editor, bash, search, context, subagent) and uses the built-in coding system prompt when system_prompt is not provided.

config = AgentConfig.coding(model="openai/gpt-4.1", cwd="/Users/me/project")

AgentConfig.from_yaml()

@classmethod
def from_yaml(cls, path: str | Path) -> AgentConfig

Load config from a YAML file. Hook callables are code-only and are not serialized.

# example .lionagi/agents/coder/coder.yaml
name: coder
model: openai/gpt-4.1
effort: high
tools: [coding]
system_prompt: |
  You are a coding agent...
permissions:
  mode: rules
  allow:
    reader: ["*"]
    search: ["*"]
    bash: ["git *", "cargo *", "uv *"]
  deny:
    bash: ["rm -rf *", "sudo *"]

AgentConfig.to_yaml()

def to_yaml(self, path: str | Path) -> None

Save config fields to YAML. hook_handlers (callables) are omitted.


create_agent()

async def create_agent(
    config: AgentConfig,
    *,
    load_settings: bool = True,
    project_dir: str | None = None,
    trust_project_settings: bool = False,
    trusted_hook_modules: set[str] | frozenset[str] | None = None,
) -> Branch

Source: lionagi/agent/factory.py

Creates a fully configured Branch from an AgentConfig. Wires: settings → hooks → system prompt → model → tools → MCP.

Param Type Default Notes
config AgentConfig Agent configuration
load_settings bool True Load hooks from ~/.lionagi/settings.yaml
project_dir str \| None None Project root for settings resolution; auto-detected if None
trust_project_settings bool False Also load .lionagi/settings.yaml from the project dir
trusted_hook_modules set[str] \| None None Python modules allowed for import-based hooks; defaults to {"lionagi.agent.hooks"}

Returns a Branch ready for use with all tools registered and hooks attached.

config = AgentConfig.coding(model="openai/gpt-4.1")
branch = await create_agent(config)
response = await branch.chat("Refactor the auth module")

Settings loading order (project-local wins):

  1. ~/.lionagi/settings.yaml — always loaded when load_settings=True
  2. .lionagi/settings.yaml — loaded only when trust_project_settings=True

PermissionPolicy

@dataclass
class PermissionPolicy

Source: lionagi/agent/permissions.py

Per-tool allow/deny/escalate rules evaluated before each tool call. Three modes:

Mode Behavior
"allow_all" All tool calls permitted (default)
"deny_all" All tool calls blocked
"rules" Check deny → allow → escalate lists; default deny if no rule matches

Fields

Field Type Default Notes
mode str "allow_all" "allow_all" | "deny_all" | "rules"
allow dict[str, list[str]] {} Tool → list of fnmatch patterns that permit the call
deny dict[str, list[str]] {} Tool → list of fnmatch patterns that block the call
escalate dict[str, list[str]] {} Tool → list of patterns that trigger on_escalate
on_escalate Callable \| None None Async callable invoked on escalation; return True to allow, a dict to rewrite args

Tool names in allow/deny/escalate are normalized: "bash_tool""bash", etc. "*" as a tool key applies to all tools.

Preset class methods

PermissionPolicy.allow_all()   # mode="allow_all"
PermissionPolicy.deny_all()    # mode="deny_all"

# reader + search allowed; editor + bash denied
PermissionPolicy.read_only()

# reader + editor + search allowed; dangerous bash commands denied; other bash → escalate
PermissionPolicy.safe()

from_dict()

@classmethod
def from_dict(cls, data: dict) -> PermissionPolicy

Build from a plain dict (e.g. loaded from YAML):

policy = PermissionPolicy.from_dict({
    "mode": "rules",
    "allow": {"reader": ["*"], "bash": ["git *", "uv *"]},
    "deny": {"bash": ["rm *", "sudo *"]},
})

Pattern matching

For the bash tool, patterns are matched against the command string. For editor and reader, patterns are matched against the file path. Shell control operators (;, &&, ||, |, backticks, $(), redirects) in bash commands are blocked unconditionally before pattern matching — they cannot be allow-listed.

Using with AgentConfig

# Dict form (round-trips through YAML)
config.permissions = {
    "mode": "rules",
    "allow": {"reader": ["*"], "bash": ["git *"]},
    "deny": {"bash": ["rm *"]},
}

# Object form (code-only)
config.permissions = PermissionPolicy.safe()

Built-in hooks

Source: lionagi/agent/hooks.py

guard_destructive

async def guard_destructive(tool_name: str, action: str, args: dict) -> dict | None

Pre-hook for bash. Raises PermissionError when the command matches a destructive pattern: rm -rf, git push --force, git reset --hard, git clean -fd, DROP TABLE, DROP DATABASE, TRUNCATE TABLE, mkfs, dd if=, writes to /dev/sd*.

config.pre("bash", guard_destructive)

guard_paths()

def guard_paths(
    allowed_paths: list[str] | None = None,
    denied_paths: list[str] | None = None,
) -> Callable

Factory that returns a pre-hook restricting file access by path. Applied to reader and editor.

  • allowed_paths: if set, any path outside these roots raises PermissionError.
  • denied_paths: patterns (absolute paths, filenames, or substrings) that are always blocked.
config.pre("reader", guard_paths(allowed_paths=["/Users/me/project/"]))
config.pre("editor", guard_paths(denied_paths=[".env", "*.key"]))

log_tool_use

async def log_tool_use(tool_name: str, action: str, args: dict, result: dict) -> dict | None

Post-hook for any tool. Logs tool=<name> action=<action> success=<bool> at INFO level via the standard logging module. Returns None (does not modify result).

config.post("*", log_tool_use)

auto_format_python

async def auto_format_python(tool_name: str, action: str, args: dict, result: dict) -> dict | None

Post-hook for editor. Runs ruff format <file_path> on successfully edited .py files.

config.post("editor", auto_format_python)

Next: SandboxSession — isolated worktree execution