Custom Agent Development
The built-in agent handles the vast majority of use cases through JSON configuration and skills. Modern LLMs are capable enough to follow detailed instructions for complex workflows - summarization strategies, multi-step analysis, domain-specific protocols - when those instructions are delivered through skills, system prompts, and tool configuration. Building an agent harness from scratch is a significant engineering effort, so starting from the built-in agent and customizing through configuration is almost always the faster path.
This section documents how to build a custom agent class - for cases where the composable architecture genuinely doesn't fit.
When to Build a Custom Agent
You need a custom agent class when you need to override core agent loop behavior, i.e. the built-in agent's streaming/tool-execution loop is the wrong shape for your use case.
If your need is "I want different tools," "I want different behavior," or "I want a multi-step workflow" - configure the built-in agent instead. If you need a new type of tool, context, or skill that the built-in sources don't cover, you can create a custom source — that plugs into the built-in agent without writing a custom agent class.
See How the Agent Harness Works for the full capability menu, Built-in Agent Provider Configuration for the built-in agent's capabilities, and Configuration Reference for the full JSON schema.
Custom Agent Class
If you need full control of the agent loop, implement StreamableChatAgent:
from typing import AsyncGenerator, List
from zav.agents_sdk import ChatAgentClassRegistry, ChatMessage, StreamableChatAgent
from zav.agents_sdk.adapters import ZAVChatCompletionClient
@ChatAgentClassRegistry.register()
class MyCustomAgent(StreamableChatAgent):
agent_name = "my_custom_agent"
def __init__(self, client: ZAVChatCompletionClient, system_prompt: str = ""):
self.client = client
self.system_prompt = system_prompt
async def execute_streaming(
self, conversation: List[ChatMessage]
) -> AsyncGenerator[ChatMessage, None]:
response = await self.client.complete(
bot_setup_description=self.system_prompt,
messages=conversation,
stream=True,
)
async for chat_client_response in response:
if chat_client_response.error is not None:
raise chat_client_response.error
if chat_client_response.chat_completion is None:
raise Exception("No response from chat completion client")
yield ChatMessage.from_orm(chat_client_response.chat_completion)
Then add a setup entry in agent_setups.json:
{
"agent_identifier": "my-custom-agent",
"agent_name": "my_custom_agent",
"llm_client_configuration": {
"vendor": "openai",
"vendor_configuration": {},
"model_configuration": { "name": "gpt-5.4-mini", "type": "chat" }
},
"agent_configuration": {
"system_prompt": "You are a specialized assistant."
}
}
And the credentials in env/agent_setups.json:
[
{
"agent_identifier": "my-custom-agent",
"llm_client_configuration": {
"vendor_configuration": {
"openai": { "openai_api_key": "sk-...", "openai_org": "org-..." }
}
}
}
]
Advanced Articles in This Section
- Creating Agents that Use Tools - custom tool registration
- Custom RAG Agent with the Completion Client - custom retrieval and prompt construction with the SDK completion client
- Custom RAG Agent with LangChain - custom retrieval pipeline with LangChain