AgentSpec and create_agent()¶
from lionagi.agent import AgentSpec, create_agent, PermissionPolicy
from lionagi.agent.hooks import guard_destructive, guard_paths, log_tool_use
AgentSpec captures what an agent needs — role/identity, model, tools, hooks, permissions, emission grants — in a single serializable object. create_agent() wires it into a ready-to-use Branch.
AgentSpec¶
@dataclass
class AgentSpec(HooksMixin)
Source: lionagi/agent/spec.py
An AgentSpec pairs a Profile (role + modes identity) with runtime concerns. Build one with AgentSpec.compose(role, ...) or the AgentSpec.coding() preset rather than constructing the dataclass directly.
Fields¶
| Field | Type | Default | Notes |
|---|---|---|---|
profile | Profile | — | Role + modes identity (set by compose()) |
model | str \| None | None | Model spec: "provider/model" or bare alias |
effort | str \| None | None | Override effort level (e.g. "high", "xhigh") |
tools | tuple[str, ...] | () | Tool presets to register: "coding", "reader", "editor", "bash", "search" |
permissions | PermissionPolicy \| None | None | Permission rules; see PermissionPolicy |
grant_emissions | bool | True | Grant the role's declared capability-emission models |
emits | tuple \| None | None | Override what is granted; None uses the role contract |
pack | str \| Pack \| None | "default" | Policy pack for the role-policy prompt block |
lion_system | bool | True | Prepend the lionagi system preamble to the system prompt |
extra_prompt | str \| None | None | Extra literal prompt text (set via system_prompt=) |
hook_handlers | dict[str, list[Callable]] | {} | Phase-keyed hooks ("pre:bash", "post:*", "error:editor") |
cwd | str \| None | None | Working directory for tools and MCP discovery |
yolo | bool | False | Auto-approve all tool calls (pass-through to provider kwargs) |
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) |
mcp_servers and mcp_config_path are not compose() keyword arguments — set them as attributes after building the spec:
spec = AgentSpec.coding()
spec.mcp_servers = ["khive"]
spec.mcp_config_path = "/Users/me/project/.mcp.json"
Hook methods¶
Inherited from HooksMixin:
spec.pre("bash", handler) # register a pre-hook for the bash tool
spec.post("editor", handler) # register a post-hook for the editor tool
spec.on_error("*", handler) # register an error hook for all tools
prehooks:async (tool_name: str, action: str, args: dict) -> dict | NoneReturn a modifiedargsdict to rewrite the call, or raisePermissionErrorto block.posthooks:async (tool_name: str, action: str, args: dict, result: dict) -> dict | NoneReturn a modifiedresultdict, orNoneto pass through unchanged.- Tool name
"*"matches all tools.
AgentSpec.compose()¶
@classmethod
def compose(
cls,
role: Any,
*,
modes: list[Any] | None = None,
model: str | None = None,
effort: str | None = None,
tools: tuple[str, ...] | list[str] = (),
permissions: Any = None,
pack: str | Pack | None = "default",
grant_emissions: bool = True,
emits: tuple | None = None,
system_prompt: str | None = None,
cwd: str | None = None,
yolo: bool = False,
) -> AgentSpec
Build an AgentSpec from a role name/object plus optional overrides. permissions accepts a PermissionPolicy, a plain dict (YAML-shaped), or a preset name ("safe", "read_only", "allow_all", "deny_all"). system_prompt becomes the spec's extra_prompt.
spec = AgentSpec.compose("implementer", tools=["coding"], model="openai/gpt-4.1")
AgentSpec.coding()¶
@classmethod
def coding(
cls,
*,
model: str | None = None,
effort: str | None = "high",
system_prompt: str | None = None,
cwd: str | None = None,
secure: bool = True,
**kwargs,
) -> AgentSpec
Preset for a coding agent — implementer role + tools=["coding"] (reader, editor, bash, search, context, subagent). Extra **kwargs flow through to compose().
By default (secure=True) it wires two guards:
guard_destructiveas a pre-hook onbash— blocks destructive shell commands (rm -rf, force-push, etc.).guard_pathsas a pre-hook onreaderandeditor— restricts file access to the workspace root (cwdif provided, elsePath.cwd()at call time).
Set secure=False to disable these defaults and manage hooks manually.
spec = AgentSpec.coding(model="openai/gpt-4.1", cwd="/Users/me/project")
AgentSpec.from_yaml()¶
@classmethod
def from_yaml(cls, path: str | Path) -> AgentSpec
Load a spec from a YAML file. Hook callables are code-only and are not serialized.
# example .lionagi/agents/coder/coder.yaml
role: implementer
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 *"]
AgentSpec.to_yaml()¶
def to_yaml(self, path: str | Path) -> None
Save spec fields to YAML. hook_handlers (callables) are omitted.
create_agent()¶
async def create_agent(
config: AgentSpec,
*,
load_settings: bool = True,
project_dir: str | None = None,
trust_project_settings: bool = False,
trusted_hook_modules: set[str] | frozenset[str] | None = None,
chat_model: Any = None,
log_config: Any = None,
) -> Branch
Source: lionagi/agent/factory.py
Creates a fully configured Branch from an AgentSpec. Wires: settings → hooks → system prompt → model → tools → MCP → emissions.
| Param | Type | Default | Notes |
|---|---|---|---|
config | AgentSpec | — | Agent specification |
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"} |
chat_model | iModel \| None | None | Prebuilt model to use verbatim; skips spec.model parsing |
log_config | DataLoggerConfig \| dict \| None | None | Logging config forwarded to the Branch |
Returns a Branch ready for use with all tools registered and hooks attached.
spec = AgentSpec.coding(model="openai/gpt-4.1")
branch = await create_agent(spec)
response = await branch.chat("Refactor the auth module")
Settings loading order (project-local wins):
~/.lionagi/settings.yaml— always loaded whenload_settings=True.lionagi/settings.yaml— loaded only whentrust_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 AgentSpec¶
# Preset name (resolved by compose())
spec = AgentSpec.compose("implementer", tools=["coding"], permissions="safe")
# Dict form (round-trips through YAML)
spec.permissions = PermissionPolicy.from_dict({
"mode": "rules",
"allow": {"reader": ["*"], "bash": ["git *"]},
"deny": {"bash": ["rm *"]},
})
# Object form (code-only)
spec.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*.
spec.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 raisesPermissionError.denied_paths: patterns (absolute paths, filenames, or substrings) that are always blocked.
spec.pre("reader", guard_paths(allowed_paths=["/Users/me/project/"]))
spec.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).
spec.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.
spec.post("editor", auto_format_python)
Next: SandboxSession — isolated worktree execution