How to Implement Custom Agent Nodes in ChatDev Workflow Graphs
To implement custom agent nodes in ChatDev, create a configuration dataclass inheriting from AgentConfig, subclass NodeExecutor (or AgentNodeExecutor) for the execution logic, and register both via register_node_type in runtime/node/registry.py.
ChatDev's workflow engine orchestrates multi-agent collaboration through a graph-based execution model where each step is a node. The OpenBMB/ChatDev repository provides a pluggable architecture that allows developers to extend the framework with custom agent nodes tailored to specific LLM behaviors or tool interactions. Implementing custom agent nodes in ChatDev requires understanding the strict separation between static configuration and dynamic execution logic.
Architecture of ChatDev Nodes
ChatDev treats every workflow step as a node composed of two distinct components: a Configuration dataclass that declares static parameters, and an Executor class that implements the runtime behavior.
Configuration Layer
Configuration classes live in entity/configs/node/ and inherit from BaseConfig or specialized subclasses like AgentConfig. These dataclasses define fields such as model name, temperature, retry policies, and tool lists. In entity/configs/node/agent.py, the standard AgentConfig provides the base schema that custom nodes typically extend.
Execution Layer
Executor classes reside in runtime/node/executor/ and inherit from the abstract NodeExecutor defined in runtime/node/executor/base.py. The built-in AgentNodeExecutor in runtime/node/executor/agent_executor.py serves as the primary reference implementation, handling LLM calls, tool execution, and message formatting. Custom executors override specific methods like _prepare_prompt_messages to inject bespoke behavior while reusing the platform's infrastructure.
Step-by-Step Implementation
Adding a custom agent node requires four concrete steps: defining the configuration, implementing the executor, registering the type, and referencing it in workflow YAML.
Step 1: Define the Configuration Dataclass
Create a dataclass inheriting from AgentConfig to declare node-specific parameters. This class must implement a from_dict classmethod for deserialization and optionally expose FIELD_SPECS for UI validation.
# custom_nodes/my_agent_config.py
from dataclasses import dataclass
from typing import Any, Mapping
from entity.configs.base import ConfigFieldSpec, optional_str, optional_float
from entity.configs.node.agent import AgentConfig
@dataclass
class MyAgentConfig(AgentConfig):
"""Custom agent with configurable temperature and system prompt injection."""
temperature: float = 0.7
system_prompt: str | None = None
@classmethod
def from_dict(cls, data: Mapping[str, Any], *, path: str) -> "MyAgentConfig":
base = super().from_dict(data, path=path) # type: ignore[arg-type]
mapping = dict(data)
temperature = optional_float(
mapping.get("temperature", 0.7),
field_path=f"{path}.temperature"
)
system_prompt = optional_str(mapping, "system_prompt", path)
return cls(
**base.__dict__,
temperature=temperature,
system_prompt=system_prompt,
)
FIELD_SPECS = {
**AgentConfig.FIELD_SPECS,
"temperature": ConfigFieldSpec(
name="temperature",
display_name="Temperature",
type_hint="float",
default=0.7,
description="Sampling temperature for the LLM",
),
"system_prompt": ConfigFieldSpec(
name="system_prompt",
display_name="System Prompt",
type_hint="text",
description="Additional system prompt injected before user input",
),
}
Step 2: Implement the Node Executor
Subclass AgentNodeExecutor and override methods like _prepare_prompt_messages to modify input formatting or _prepare_call_options to adjust LLM parameters. The executor receives an ExecutionContext containing shared services.
# custom_nodes/my_agent_executor.py
from typing import List
from entity.messages import Message, MessageRole
from runtime.node.executor.agent_executor import AgentNodeExecutor
class MyAgentExecutor(AgentNodeExecutor):
"""Executor that prepends a custom system prompt and uses configurable temperature."""
def _prepare_prompt_messages(self, node, input_data: str, skill_manager) -> List[Message]:
messages = super()._prepare_prompt_messages(node, input_data, skill_manager)
cfg = node.as_config(MyAgentConfig)
if cfg.system_prompt:
messages.insert(0, Message(role=MessageRole.SYSTEM, content=cfg.system_prompt))
return messages
def _prepare_call_options(self, node):
"""Override to inject custom temperature from config."""
options = super()._prepare_call_options(node)
cfg = node.as_config(MyAgentConfig)
options["temperature"] = cfg.temperature
return options
Step 3: Register the Node Type
Registration bridges the YAML type name to your Python classes. Import register_node_type from runtime/node/registry.py and invoke it with your configuration class, executor class, and capabilities.
# custom_nodes/__init__.py
from runtime.node.registry import register_node_type, NodeCapabilities
from .my_agent_config import MyAgentConfig
from .my_agent_executor import MyAgentExecutor
register_node_type(
name="my_agent",
config_cls=MyAgentConfig,
executor_cls=MyAgentExecutor,
capabilities=NodeCapabilities(
default_role_field="role",
exposes_tools=True,
),
summary="Custom agent node with configurable temperature and system prompt injection.",
)
Place this package under main/custom_nodes/ or any location imported during startup. The registration executes at import time, populating the global node_registry used by the runtime.
Step 4: Reference in Workflow YAML
Once registered, reference the node by its registered name in any workflow graph definition:
# workflow.yaml
graph:
- id: greeting_agent
type: my_agent
config:
provider: openai
name: gpt-4o-mini
temperature: 0.9
system_prompt: |
You are a friendly assistant that always greets users with a joke.
tools:
- calculator
The GraphBuilder resolves type: my_agent via runtime/node/registry.py, instantiates MyAgentExecutor with the shared ExecutionContext, and executes it within the graph.
Key Source Files
Understanding these specific files in the OpenBMB/ChatDev repository is essential for implementation:
runtime/node/registry.py: Containsregister_node_typeand the global registry dictionary that maps node type strings toNodeRegistrationobjects.runtime/node/executor/base.py: Defines the abstractNodeExecutorclass andExecutionContextpassed to all executors.runtime/node/executor/agent_executor.py: Reference implementation showing how to handle LLM calls, tool invocation, and message history.entity/configs/node/agent.py: Base configuration schema for agent nodes including retry policies and tool specifications.runtime/node/builtin_nodes.py: Examples of how built-in nodes likeagentandliteralare registered usingregister_node_type.
Summary
- Configuration: Inherit from
AgentConfiginentity/configs/node/to define static parameters like temperature and custom prompts. - Execution: Subclass
AgentNodeExecutorinruntime/node/executor/to override message preparation or call options while reusing base LLM logic. - Registration: Call
register_node_typefromruntime/node/registry.pyto bind your config and executor to a YAML type name. - Workflow: Reference the registered name in graph YAML files; the engine automatically resolves and executes your custom node during runtime.
Frequently Asked Questions
What is the difference between AgentConfig and BaseConfig?
AgentConfig in entity/configs/node/agent.py extends BaseConfig with fields specific to LLM agents such as model provider, tool lists, and retry configurations. For custom agent nodes that interact with language models, inherit from AgentConfig to retain these standard fields. Use BaseConfig directly only when building non-LLM node types that require minimal configuration.
Can I override tool execution behavior in my custom agent?
Yes. While AgentNodeExecutor handles standard tool calling, you can override the _execute_tool or _handle_tool_calls methods in your executor subclass to implement custom tool invocation logic, filtering, or pre/post-processing. The method signatures are defined in runtime/node/executor/agent_executor.py.
How does the engine locate my custom node if I place it outside the main package?
The node_registry only contains nodes imported during runtime. Place your custom package under main/ and ensure custom_nodes/__init__.py executes the register_node_type call. Import the module in main/server_main.py or add it to runtime/node/__init__.py to guarantee registration before the workflow engine initializes.
What capabilities should I declare when registering a custom agent?
Pass NodeCapabilities to register_node_type with default_role_field="role" if your node produces messages with roles (user, assistant, system), and set exposes_tools=True if it supports tool use. These flags inform the UI and validation systems about node behavior without affecting execution logic.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →