How to Create Custom Agents Using the OpenDerisk Agent Development Framework

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): 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): Extends ConversableAgent with team-management capabilities for orchestrating multiple sub-agents and parallel actions.
  • ProfileConfig (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): 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): 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 demonstrating the minimal code required to create custom agents:

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:

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:

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.

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) provides the fundamental send/receive/reply loop suitable for single-agent conversations. ManagerAgent (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, 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().

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →