Agentic AIC++verifiedVerified
Tool Use Agent Pattern in C++
Augment an LLM with callable external tools — APIs, code interpreters, databases — so it can take actions and retrieve real-time information beyond its training data.
How to Implement the Tool Use Agent Pattern in C++
1Step 1: Define tool types and a simple registry
struct ToolResult {
std::string toolName;
std::string output;
bool isError;
};
using ToolHandler = std::function<std::string(const std::map<std::string, std::string>&)>;
class ToolRegistry {
std::map<std::string, ToolHandler> tools_;
public:
void registerTool(const std::string& name, ToolHandler handler) {
tools_[name] = std::move(handler);
}
ToolResult execute(const std::string& name,
const std::map<std::string, std::string>& params) {
auto it = tools_.find(name);
if (it == tools_.end())
return {name, "Unknown tool: " + name, true};
try {
return {name, it->second(params), false};
} catch (const std::exception& e) {
return {name, e.what(), true};
}
}
std::vector<std::string> listTools() const {
std::vector<std::string> names;
for (const auto& [k, _] : tools_) names.push_back(k);
return names;
}
};2Step 2: Implement the tool-use loop
struct LLMResponse {
std::string content;
bool wantsToolCall;
std::string toolName;
std::map<std::string, std::string> params;
};
using LLMFn = std::function<LLMResponse(const std::string&, const std::vector<std::string>&)>;
std::string toolUseLoop(const std::string& query,
ToolRegistry& registry,
LLMFn llm, int maxSteps = 10) {
std::string context = query;
for (int i = 0; i < maxSteps; ++i) {
auto response = llm(context, registry.listTools());
if (!response.wantsToolCall) return response.content;
auto result = registry.execute(response.toolName, response.params);
context += "\nTool(" + result.toolName + "): " + result.output;
}
return "Max iterations reached";
}3Step 3: Demonstrate with a calculator tool
int main() {
ToolRegistry registry;
registry.registerTool("add", [](const std::map<std::string, std::string>& p) {
int a = std::stoi(p.at("a")), b = std::stoi(p.at("b"));
return std::to_string(a + b);
});
auto answer = toolUseLoop(
"What is 3 + 5?", registry,
[n = 0](const std::string&, const std::vector<std::string>&) mutable -> LLMResponse {
if (++n > 1) return {"The answer is 8", false, "", {}};
return {"", true, "add", {{"a", "3"}, {"b", "5"}}};
}
);
std::cout << answer << "\n";
}#include <iostream>
#include <string>
#include <map>
#include <vector>
#include <functional>
#include <memory>
#include <format>
#include <chrono>
#include <stdexcept>
#include <optional>
// [step] Define structured tool schema and result types
struct ToolParam {
std::string name;
std::string type;
std::string description;
bool required = true;
};
struct ToolDefinition {
std::string name;
std::string description;
std::vector<ToolParam> params;
};
struct ToolCallResult {
std::string callId;
std::string toolName;
std::string output;
bool isError;
std::chrono::milliseconds duration;
};
// [step] Build the ITool interface and typed registry with validation
class ITool {
public:
virtual ~ITool() = default;
virtual ToolDefinition definition() const = 0;
virtual std::string execute(const std::map<std::string, std::string>& params) = 0;
};
class ToolRegistry {
std::map<std::string, std::unique_ptr<ITool>> tools_;
int nextCallId_ = 0;
public:
void registerTool(std::unique_ptr<ITool> tool) {
auto def = tool->definition();
tools_.emplace(def.name, std::move(tool));
}
std::vector<ToolDefinition> definitions() const {
std::vector<ToolDefinition> defs;
for (const auto& [_, t] : tools_) defs.push_back(t->definition());
return defs;
}
ToolCallResult execute(const std::string& name,
const std::map<std::string, std::string>& params) {
auto id = std::format("call_{}", nextCallId_++);
auto start = std::chrono::steady_clock::now();
auto it = tools_.find(name);
if (it == tools_.end()) {
return {id, name, std::format("Tool '{}' not found", name),
true, std::chrono::milliseconds{0}};
}
// Validate required params
for (const auto& p : it->second->definition().params) {
if (p.required && !params.contains(p.name)) {
return {id, name,
std::format("Missing required param: '{}'", p.name),
true, std::chrono::milliseconds{0}};
}
}
try {
auto output = it->second->execute(params);
auto dur = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start);
return {id, name, std::move(output), false, dur};
} catch (const std::exception& e) {
auto dur = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start);
return {id, name, e.what(), true, dur};
}
}
};
// [step] Implement a Tool-Use agent with conversation history
struct Message {
std::string role; // "user", "assistant", "tool"
std::string content;
std::optional<std::string> toolCallId;
};
struct LLMResponse {
std::string content;
struct ToolCall {
std::string name;
std::map<std::string, std::string> params;
};
std::optional<ToolCall> toolCall;
};
using LLMFn = std::function<LLMResponse(const std::vector<Message>&,
const std::vector<ToolDefinition>&)>;
class ToolUseAgent {
ToolRegistry registry_;
LLMFn llm_;
std::vector<Message> history_;
int maxIterations_;
public:
ToolUseAgent(LLMFn llm, int maxIter = 15)
: llm_(std::move(llm)), maxIterations_(maxIter) {}
void registerTool(std::unique_ptr<ITool> tool) {
registry_.registerTool(std::move(tool));
}
std::string run(const std::string& userMessage) {
history_.push_back({"user", userMessage, std::nullopt});
for (int i = 0; i < maxIterations_; ++i) {
auto response = llm_(history_, registry_.definitions());
if (!response.toolCall) {
history_.push_back({"assistant", response.content, std::nullopt});
return response.content;
}
auto result = registry_.execute(response.toolCall->name,
response.toolCall->params);
history_.push_back({"assistant",
std::format("Calling {}", response.toolCall->name),
result.callId});
history_.push_back({"tool",
result.isError ? "Error: " + result.output : result.output,
result.callId});
}
throw std::runtime_error("Exceeded max tool-use iterations");
}
};
// [step] Demo with a concrete calculator tool
class CalculatorTool : public ITool {
public:
ToolDefinition definition() const override {
return {"calculator", "Evaluates arithmetic",
{{"expr", "string", "Expression", true}}};
}
std::string execute(const std::map<std::string, std::string>& params) override {
// Simplified: only handles "a+b"
auto expr = params.at("expr");
auto pos = expr.find('+');
if (pos == std::string::npos) throw std::runtime_error("Unsupported expr");
int a = std::stoi(expr.substr(0, pos));
int b = std::stoi(expr.substr(pos + 1));
return std::to_string(a + b);
}
};
int main() {
ToolUseAgent agent(
[n = 0](const std::vector<Message>& hist,
const std::vector<ToolDefinition>&) mutable -> LLMResponse {
if (++n > 1) return {"The answer is 8", std::nullopt};
return {"", LLMResponse::ToolCall{"calculator", {{"expr", "3+5"}}}};
}
);
agent.registerTool(std::make_unique<CalculatorTool>());
std::cout << agent.run("What is 3+5?") << "\n";
}Tool Use Agent Pattern Architecture
hourglass_empty
Rendering diagram...
lightbulb
Tool Use Agent Pattern in the Real World
“A lawyer (the LLM) in a courtroom knows the law but needs a paralegal team (the tools) to pull case files, run searches, and retrieve exhibits. The lawyer directs which file to fetch, the paralegal returns it, and the lawyer integrates that information into their argument — the lawyer's intelligence is amplified by the support staff's ability to reach into the real world.”