#include <gaia/agent.h>class WeatherAgent : public gaia::Agent {public: WeatherAgent() : Agent(makeConfig()) { init(); }protected: std::string getSystemPrompt() const override { return "You are a meteorology assistant. " "Always answer in concise bullet points. " "Never speculate — if you do not have data, say so explicitly. " "When reporting temperatures always use both Celsius and Fahrenheit."; } // ... registerTools() below ...private: static gaia::AgentConfig makeConfig() { gaia::AgentConfig cfg; cfg.modelId = "Qwen3-4B-GGUF"; return cfg; }};
The framework automatically appends the tool list and response-format schema after your system prompt. You do not need to describe tools in getSystemPrompt() — they are injected automatically.
#include <gaia/agent.h>#include <gaia/types.h>#include <stdexcept>class WeatherAgent : public gaia::Agent {protected: void registerTools() override { // Tool 1: no parameters toolRegistry().registerTool( "get_supported_cities", "Return the list of cities for which weather data is available.", [](const gaia::json& /*args*/) -> gaia::json { return {{"cities", {"Seattle", "Austin", "London", "Tokyo"}}}; }, {} // no parameters ); // Tool 2: one required STRING parameter toolRegistry().registerTool( "get_current_weather", "Return the current weather conditions for a given city.", [](const gaia::json& args) -> gaia::json { std::string city = args.value("city", ""); if (city.empty()) throw std::runtime_error("city is required"); // Replace with real API call in production return { {"city", city}, {"temperature", "18°C / 64°F"}, {"conditions", "Partly cloudy"}, {"humidity", "72%"} }; }, { {"city", gaia::ToolParamType::STRING, /*required=*/true, "The city name to get weather for (e.g. 'Seattle')"} } ); // Tool 3: mixed required + optional parameters toolRegistry().registerTool( "get_weather_forecast", "Return a multi-day weather forecast for a city.", [](const gaia::json& args) -> gaia::json { std::string city = args.value("city", ""); int days = args.value("days", 3); // optional, defaults to 3 if (city.empty()) throw std::runtime_error("city is required"); if (days < 1 || days > 7) throw std::runtime_error("days must be 1-7"); gaia::json forecast = gaia::json::array(); for (int i = 1; i <= days; ++i) { forecast.push_back({ {"day", "Day " + std::to_string(i)}, {"high", "20°C / 68°F"}, {"low", "12°C / 54°F"}, {"conditions", "Mostly sunny"} }); } return {{"city", city}, {"forecast", forecast}}; }, { {"city", gaia::ToolParamType::STRING, /*required=*/true, "The city name to forecast"}, {"days", gaia::ToolParamType::INTEGER, /*required=*/false, "Number of forecast days (1-7, default 3)"} } ); }};
The ToolParameter aggregate is {name, type, required, description}. Optional parameters should have a matching default in your callback (use args.value("key", default_value) from nlohmann/json).
Call connectMcpServer()afterinit() to register tools from an external MCP server. This works with any stdio-based MCP server — your own, a third-party package, or a local script.
class WeatherAgent : public gaia::Agent {public: WeatherAgent() : Agent(makeConfig()) { init(); // registers native tools first // Connect a custom MCP server for extended data sources connectMcpServer("weather_data", { {"command", "uvx"}, {"args", {"my-weather-mcp-server", "--api-key", "YOUR_KEY"}} }); } // ...};
All tools from weather_data are automatically prefixed as mcp_weather_data_<tool_name> and injected into the LLM system prompt alongside your native tools.
MCP connections use stdio transport (JSON-RPC 2.0 over stdin/stdout). The server subprocess is spawned at connectMcpServer() time and cleaned up when the agent is destroyed.
Here is a complete agent that uses both native C++ tools and an MCP server in one class:
combined_agent.cpp
#include <gaia/agent.h>#include <gaia/types.h>#include <ctime>#include <iostream>#include <stdexcept>#include <string>class WeatherAgent : public gaia::Agent {public: WeatherAgent() : Agent(makeConfig()) { init(); // Native tools are already registered by init(). // Now attach an MCP server for additional data. connectMcpServer("weather_data", { {"command", "uvx"}, {"args", {"my-weather-mcp-server"}} }); }protected: std::string getSystemPrompt() const override { return "You are a professional meteorology assistant. " "Answer in clear, structured bullet points. " "Always check current conditions before giving a forecast. " "If a city is not supported, say so rather than guessing."; } void registerTools() override { // Native: fast, no subprocess needed toolRegistry().registerTool( "get_supported_cities", "Return the list of cities for which weather data is available.", [](const gaia::json&) -> gaia::json { return {{"cities", {"Seattle", "Austin", "London", "Tokyo"}}}; }, {} ); toolRegistry().registerTool( "get_current_weather", "Return current weather for a city.", [](const gaia::json& args) -> gaia::json { std::string city = args.value("city", ""); if (city.empty()) throw std::runtime_error("city parameter required"); return {{"city", city}, {"temperature", "18°C / 64°F"}, {"conditions", "Partly cloudy"}}; }, {{"city", gaia::ToolParamType::STRING, true, "City name"}} ); // MCP tools from "weather_data" will be added automatically by // connectMcpServer() after init() completes. }private: static gaia::AgentConfig makeConfig() { gaia::AgentConfig cfg; cfg.maxSteps = 15; cfg.debug = false; return cfg; }};int main() { try { WeatherAgent agent; auto result = agent.processQuery( "What is the current weather in Seattle, and what is the 3-day forecast?" ); std::cout << result["result"].get<std::string>() << "\n"; } catch (const std::exception& e) { std::cerr << "Error: " << e.what() << "\n"; return 1; }}
By default the agent prints to the terminal using TerminalConsole. You can replace it with SilentConsole (built-in) or a fully custom OutputHandler subclass.
// Option A: set in config (suppresses all output including final answer)cfg.silentMode = true;// Option B: SilentConsole — suppresses progress but still prints the final answeragent.setOutputHandler(std::make_unique<gaia::SilentConsole>(/*silenceFinalAnswer=*/false));// Option C: full silence including the final answeragent.setOutputHandler(std::make_unique<gaia::SilentConsole>(/*silenceFinalAnswer=*/true));
auto console = std::make_unique<CapturingConsole>();CapturingConsole* consolePtr = console.get(); // keep raw pointer before moveagent.setOutputHandler(std::move(console));auto result = agent.processQuery("What is the weather in Seattle?");// Inspect all captured linesfor (const auto& line : consolePtr->lines()) { std::cout << line << "\n";}// Or just the final answerstd::cout << consolePtr->finalAnswer() << "\n";
When integrating the agent into a desktop application (WPF, Qt, Electron), you need to run it headless, capture its output programmatically, and keep your UI responsive.
auto ui = std::make_unique<UIOutputHandler>();ui->onStepStart = [](int step, int total) { // Update progress bar: step / total};ui->onThought = [](const std::string& thought) { // Display agent reasoning in a text panel};ui->onAnswer = [](const std::string& answer) { // Show final answer in result area};agent.setOutputHandler(std::move(ui));
Thread safety: The OutputHandler methods are called from the thread running processQuery(). If your callbacks update a GUI, you must post to the UI thread (e.g., QMetaObject::invokeMethod in Qt, Dispatcher.Invoke in WPF, PostMessage in Win32).
Each Agent instance is independent. You can run multiple agents concurrently on separate threads — each with its own tools, MCP connections, and output handler:
// Each agent gets its own config, tools, and output handlerMyDiagnosticAgent diagAgent;MyReportAgent reportAgent;// Safe to run in parallel — no shared stateauto f1 = std::async(std::launch::async, [&] { return diagAgent.processQuery("check health"); });auto f2 = std::async(std::launch::async, [&] { return reportAgent.processQuery("generate report"); });