# How to Create Custom Agents Using the OpenDerisk Agent Development Framework

> Learn to create custom agents in OpenDerisk by subclassing ConversableAgent or ManagerAgent. Define profiles, prompts, and register actions to build powerful AI tools with our framework.

- Repository: [derisk-ai/openderisk](https://github.com/derisk-ai/openderisk)
- Tags: how-to-guide
- Published: 2026-02-28

---

**You create custom agents in OpenDerisk by subclassing `ConversableAgent` (for single agents) or `ManagerAgent` (for teams), defining a `ProfileConfig` for identity and prompts, and registering `Action` classes via `self._init_actions()` to expose executable tools.**

The OpenDerisk agent development framework, available in the `derisk-ai/openderisk` repository, provides a modular foundation for building task-oriented AI agents. When you create custom agents using this framework, you leverage pre-built abstractions for message routing, memory persistence, LLM interaction, and parallel action execution while only implementing your specific business logic. This guide demonstrates the exact patterns used in production within the `packages/derisk-core/src/derisk/agent/` directory.

## Core Abstractions of the OpenDerisk Agent Framework

Before writing code, understand the five core classes you will combine to create custom agents:

- **`ConversableAgent`** ([`base_agent.py`](https://github.com/derisk-ai/openderisk/blob/main/base_agent.py)): The base class that implements the complete send/receive/reply loop, GPT-S memory handling, and LLM client interaction. Subclass this for single-agent implementations.
- **`ManagerAgent`** ([`base_team.py`](https://github.com/derisk-ai/openderisk/blob/main/base_team.py)): Extends `ConversableAgent` with team-management capabilities for orchestrating multiple sub-agents and parallel actions.
- **`ProfileConfig`** ([`profile/base.py`](https://github.com/derisk-ai/openderisk/blob/main/profile/base.py)): Declares the agent's name, role, goal, system prompts, and constraints that are injected into the LLM context.
- **`Action`** ([`actions/agent_action.py`](https://github.com/derisk-ai/openderisk/blob/main/actions/agent_action.py)): Encapsulates a single tool or skill that an agent can invoke. Actions are registered in the agent's `_init_actions` list during initialization.
- **`VariableManager`** ([`variable.py`](https://github.com/derisk-ai/openderisk/blob/main/variable.py)): Provides dynamic variable injection (e.g., available tools, knowledge bases) into prompt templates at runtime.

## Step-by-Step Guide to Create Custom Agents

Follow these implementation steps used throughout the OpenDerisk codebase:

1. **Create a subclass** of `ConversableAgent` (for single agents) or `ManagerAgent` (for multi-agent teams).
2. **Define a `ProfileConfig`** class attribute describing the agent's identity, system prompt template, and user prompt template.
3. **Register actions** in `__init__` by calling `self._init_actions([MyActionClass, ...])`.
4. **(Optional) Override `load_resource`** to implement custom resource binding logic.
5. **(Optional) Register dynamic variables** using `self._vm.register` to expose runtime data to prompts.
6. **(Optional) Enable function calling** by setting `enable_function_call = True` and populating `available_system_tools`.

The framework automatically handles resource preloading, prompt generation, LLM invocation via `thinking()`, parallel action execution via `act()`, and memory persistence via `GptsMemory`.

## Minimal Example: EchoAgent

Here is a complete, runnable example from [`packages/derisk-core/src/derisk/agent/expand/echo_agent.py`](https://github.com/derisk-ai/openderisk/blob/main/packages/derisk-core/src/derisk/agent/expand/echo_agent.py) demonstrating the minimal code required to create custom agents:

```python
from derisk.agent.core.base_agent import ConversableAgent
from derisk.agent.core.profile import ProfileConfig, DynConfig
from derisk.agent.expand.actions.agent_action import AgentAction, ActionOutput

class EchoAction(AgentAction):
    """A trivial action that simply echoes the user's message."""
    name = "echo"
    description = "Return the original user input unchanged."

    async def run(self, ai_message: str, **kwargs) -> ActionOutput:
        return ActionOutput(content=ai_message, name=self.name, is_exe_success=True)


class EchoAgent(ConversableAgent):
    """Custom agent that repeats whatever the user says."""
    profile = ProfileConfig(
        name=DynConfig("EchoBot", category="agent", key="custom_echo_name"),
        role=DynConfig("Echo Assistant", category="agent", key="custom_echo_role"),
        goal=DynConfig(
            "Repeat the user's input verbatim",
            category="agent",
            key="custom_echo_goal",
        ),
        system_prompt_template="You are a simple echo bot. Always repeat the user's last message.",
        user_prompt_template="{question}",
    )

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        # Register the only action this agent can perform

        self._init_actions([EchoAction])

    async def load_resource(self, question: str, is_retry_chat: bool = False):
        """EchoAgent does not need external resources."""
        return None, None

```

**Key implementation details:**

- **Profile configuration**: The `ProfileConfig` automatically injects `system_prompt_template` and `user_prompt_template` into the LLM context.
- **Action registration**: `self._init_actions([EchoAction])` tells the engine which tools the LLM may invoke.
- **Resource handling**: Overridden `load_resource` returns `None` because this agent requires no external data.

Deploy the agent by constructing it and calling the lifecycle methods:

```python
from derisk.agent.expand.echo_agent import EchoAgent

agent = EchoAgent()
await agent.build()                 # preload resources, init LLM client

await agent.bind(target=context)    # bind an AgentContext

await agent.send(message, recipient=agent)   # start conversation

```

## Advanced Example: Knowledge-Enhanced Agent

To create custom agents that interact with external knowledge bases, subclass `ConversableAgent` and bind a `RetrieverResource`. This pattern appears in [`packages/derisk-core/src/derisk/agent/expand/kb_agent.py`](https://github.com/derisk-ai/openderisk/blob/main/packages/derisk-core/src/derisk/agent/expand/kb_agent.py):

```python
from derisk.agent.core.base_agent import ConversableAgent
from derisk.agent.core.profile import ProfileConfig, DynConfig
from derisk.agent.expand.actions.agent_action import AgentStart, Terminate
from derisk.agent.expand.actions.knowledge_action import KnowledgeSearch
from derisk.agent.resource import RetrieverResource

class KBAgent(ConversableAgent):
    """Agent that answers questions using a bound knowledge base."""
    profile = ProfileConfig(
        name=DynConfig("KB-Bot", category="agent", key="kb_name"),
        role=DynConfig("Knowledge Assistant", category="agent", key="kb_role"),
        goal=DynConfig("Find the most relevant knowledge and answer the user.", category="agent", key="kb_goal"),
        system_prompt_template=(
            "You are a knowledge-retrieval assistant. Use the available knowledge resources."
        ),
        user_prompt_template="{question}",
    )

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        # Core tools: start, knowledge search, and termination

        self._init_actions([AgentStart, KnowledgeSearch, Terminate])

    async def preload_resource(self) -> None:
        """Load heavy resources before the first request."""
        await super().preload_resource()
        await self.system_tool_injection()   # inject AgentStart/Terminate etc.

    async def load_resource(self, question: str, is_retry_chat: bool = False):
        """If a RetrieverResource is bound, provide its prompt for the LLM."""
        if self.resource and isinstance(self.resource, RetrieverResource):
            return await self.resource.get_prompt(lang=self.language, question=question)
        return None, None

    def register_variables(self):
        """Expose knowledge list to the LLM as <knowledge> tags."""
        super().register_variables()

        @self._vm.register("available_knowledges", "可用知识库")
        async def var_knowledges(instance):
            prompts = ""
            for k, v in self.resource_map.items():
                if isinstance(v[0], RetrieverResource):
                    for item in v:
                        if hasattr(item, "knowledge_spaces"):
                            for ks in item.knowledge_spaces:
                                prompts += (
                                    f"- <knowledge>"
                                    f"<id>{ks.knowledge_id}</id>"
                                    f"<name>{ks.name}</name>"
                                    f"<desc>{ks.desc}</desc>"
                                    f"</knowledge>\n"
                                )
            return prompts

```

**Advanced features demonstrated:**

- **Resource binding**: The agent expects a `RetrieverResource` (vector store) attached via `agent.bind(resource=my_retriever)`.
- **System tool injection**: `system_tool_injection()` registers built-in tools like `AgentStart` and `Terminate`.
- **Dynamic variables**: `available_knowledges` renders as XML tags inside the system prompt, enabling the LLM to see available data sources.
- **Function calling**: Actions like `KnowledgeSearch` enable the LLM to trigger vector searches via tool calls.

## Architecture: How Custom Agents Execute

When you create custom agents using OpenDerisk, the framework orchestrates the following execution flow automatically:

1. **Construction**: Your subclass initializes with `ProfileConfig` and registered actions.
2. **Binding**: `agent.bind(target=...)` attaches `AgentContext`, optional `LLMConfig`, and `Resource` objects.
3. **Preloading**: `build()` triggers `preload_resource()` to load heavy assets and inject system tools.
4. **Prompt generation**: `load_resource()` generates system/user prompt fragments, while `VariableManager` injects registered dynamic variables.
5. **LLM invocation**: `thinking()` sends the composed prompt to `self.llm_client`. If `enable_function_call` is `True`, the LLM can return tool calls.
6. **Tool parsing**: `FunctionCallOutputParser` reads `tool_calls` and instantiates `Action` objects.
7. **Parallel execution**: `act()` runs all parsed actions concurrently via `asyncio.gather`, collecting `ActionOutput` results.
8. **Verification**: `verify()` checks response quality and retries up to `max_retry_count` if needed.
9. **Memory persistence**: All messages and actions are stored via `GptsMemory`.
10. **Response streaming**: The final `AgentMessage` streams back to the sender via `stream_out`.

The entire loop is instrumented with tracing spans (`root_tracer.start_span`) for observability, as implemented in [`base_agent.py`](https://github.com/derisk-ai/openderisk/blob/main/base_agent.py).

## Summary

- **Subclass `ConversableAgent`** (single agent) or **`ManagerAgent`** (team orchestration) to create custom agents with full message handling and memory management.
- **Configure identity** via `ProfileConfig` with `system_prompt_template` and `user_prompt_template` for consistent LLM behavior.
- **Register executable tools** by passing `Action` subclasses to `self._init_actions()` in the `__init__` method.
- **Bind resources** (knowledge retrievers, apps) using `agent.bind()` and implement `load_resource()` to format resource data for prompts.
- **Inject dynamic variables** via `self._vm.register` to expose runtime context like available tools or knowledge bases to the LLM.
- **Lifecycle methods**: Always call `await agent.build()` and `await agent.bind()` before the first `send()` to ensure resources and LLM clients are initialized.

## Frequently Asked Questions

### What is the difference between `ConversableAgent` and `ManagerAgent` when creating custom agents?

**`ConversableAgent`** ([`base_agent.py`](https://github.com/derisk-ai/openderisk/blob/main/base_agent.py)) provides the fundamental send/receive/reply loop suitable for single-agent conversations. **`ManagerAgent`** ([`base_team.py`](https://github.com/derisk-ai/openderisk/blob/main/base_team.py)) extends this with team-management capabilities, allowing you to orchestrate multiple sub-agents, dispatch parallel actions, and coordinate complex multi-step workflows. Choose `ConversableAgent` for simple chatbots or tool-calling agents, and `ManagerAgent` when you need hierarchical agent teams.

### How do I add custom tools to my OpenDerisk agent?

Create an `Action` subclass by inheriting from `AgentAction` in [`actions/agent_action.py`](https://github.com/derisk-ai/openderisk/blob/main/actions/agent_action.py), implement the `async def run(self, ...)` method returning `ActionOutput`, and register it in your agent's `__init__` via `self._init_actions([MyCustomAction])`. If `enable_function_call` is enabled, the framework automatically exposes these actions to the LLM as function-calling tools and handles the `FunctionCallOutputParser` logic.

### Can I bind multiple knowledge bases to a single custom agent?

Yes. The `resource_map` attribute in `ConversableAgent` supports multiple `Resource` objects. In `load_resource()` or `register_variables()`, iterate over `self.resource_map.items()` to access bound resources. Each `RetrieverResource` can expose its `knowledge_spaces` via dynamic variables, allowing the LLM to choose which knowledge base to query during the conversation.

### What hooks are available for customizing the agent's prompt at runtime?

Override `load_resource(self, question, is_retry_chat)` to return dynamic system and user prompt fragments based on the current input. Additionally, use `register_variables()` with `self._vm.register` to inject dynamic content (like available tools or knowledge lists) that renders into the prompt template before the LLM call. Both methods are called automatically during the prompt generation phase in `thinking()`.