#pragma once
#include <chrono>
#include <concepts>
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <unordered_map>
#include <utility>
#include <vector>
#include <nlohmann/json.hpp>
#include <spdlog/spdlog.h>
#include "cancellation.hpp"
#include "runtime.hpp"
namespace agent_smith
{
namespace internal
{
struct ToolAccess;
}
/// OpenAI-compatible schema and presentation metadata for one tool.
struct ToolSchema
{
/// Unique function name sent to the LLM provider.
std::string name;
/// Human-readable explanation of the tool's purpose.
std::string description;
/// JSON-Schema object accepted as the tool's argument shape.
nlohmann::json parameters;
};
/// Per-invocation resources and constraints supplied to a tool.
struct ToolContext
{
/// Cooperative cancellation signal for this invocation.
CancellationToken cancel;
/// Remaining execution-time budget for this invocation.
std::chrono::milliseconds budget{0};
/// Identifier that matches the LLM-generated tool call.
std::string call_id;
/// Optional logger scoped to tool execution.
spdlog::logger* logger = nullptr;
/// Runtime used by tools that need framework services.
Runtime* runtime = nullptr;
};
/// A normalized result returned by one tool invocation.
struct ToolResult
{
/// Text content supplied to the LLM as the tool result.
std::string content;
/// Whether the tool result represents an expected tool-level failure.
bool is_error = false;
/// Optional structured representation of the result.
nlohmann::json structured = nullptr;
};
/// Per-tool invocation options stored by the registry.
struct ToolOptions
{
/// Optional maximum execution duration for the tool.
std::optional<std::chrono::milliseconds> timeout;
};
/// Interface implemented by all callable tools.
class Tool
{
public:
/// Destroy a tool instance through the interface.
virtual ~Tool() = default;
/// Return the immutable schema advertised for this tool.
virtual const ToolSchema& schema() const = 0;
/// Invoke the tool with parsed JSON arguments and invocation context.
virtual mw::Task<mw::E<ToolResult>> invoke(nlohmann::json arguments,
ToolContext context) = 0;
};
/// Normalize a text result from a tool callable.
inline mw::E<ToolResult> normalizeToolResult(std::string value)
{
return ToolResult{
.content = std::move(value),
};
}
/// Normalize a C-string result from a tool callable.
inline mw::E<ToolResult> normalizeToolResult(const char* value)
{
return normalizeToolResult(value == nullptr ? std::string()
: std::string(value));
}
/// Normalize structured JSON into both text and structured tool output.
inline mw::E<ToolResult> normalizeToolResult(nlohmann::json value)
{
const std::string content = value.dump();
return ToolResult{
.content = content,
.structured = std::move(value),
};
}
/// Forward a fully normalized tool result unchanged.
inline mw::E<ToolResult> normalizeToolResult(ToolResult value)
{
return value;
}
/// Forward a fallible normalized tool result unchanged.
inline mw::E<ToolResult> normalizeToolResult(mw::E<ToolResult> value)
{
return value;
}
/// A callable that finishes synchronously with a normalizable result.
template <typename F>
concept SyncToolCallable = requires(F& function, nlohmann::json arguments,
ToolContext context) {
{
normalizeToolResult(function(std::move(arguments), std::move(context)))
} -> std::same_as<mw::E<ToolResult>>;
};
/// A callable that already exposes Agent Smith's asynchronous tool result.
template <typename F>
concept AsyncToolCallable =
requires(F& function, nlohmann::json arguments, ToolContext context) {
{
function(std::move(arguments), std::move(context))
} -> std::same_as<mw::Task<mw::E<ToolResult>>>;
};
namespace internal
{
inline void validateToolSchema(const ToolSchema& schema)
{
if(!schema.parameters.is_object())
{
spdlog::warn("tool '{}' has non-object JSON Schema parameters",
schema.name.c_str());
return;
}
const auto type = schema.parameters.find("type");
if(type == schema.parameters.end() || !type->is_string() ||
*type != "object")
{
spdlog::warn("tool '{}' JSON Schema parameters should have type object",
schema.name.c_str());
}
const auto properties = schema.parameters.find("properties");
if(properties == schema.parameters.end() || !properties->is_object())
{
spdlog::warn(
"tool '{}' JSON Schema parameters should have object properties",
schema.name.c_str());
}
}
template <SyncToolCallable F> class SyncToolAdapter final : public Tool
{
public:
SyncToolAdapter(ToolSchema schema, F function)
: schema_(std::move(schema)), function_(std::move(function))
{
validateToolSchema(schema_);
}
const ToolSchema& schema() const override
{
return schema_;
}
mw::Task<mw::E<ToolResult>> invoke(nlohmann::json arguments,
ToolContext context) override
{
if(context.runtime == nullptr)
{
co_return std::unexpected(
mw::runtimeError("synchronous tool requires a runtime"));
}
std::function<mw::E<ToolResult>()> work =
[function = function_, arguments = std::move(arguments),
context = std::move(context)]() mutable
{
if(context.cancel.cancelled())
{
return mw::E<ToolResult>(std::unexpected(
mw::runtimeError("tool cancelled before execution")));
}
return normalizeToolResult(
function(std::move(arguments), std::move(context)));
};
co_return co_await context.runtime->runOnWorker(std::move(work));
}
private:
ToolSchema schema_;
F function_;
};
template <AsyncToolCallable F> class AsyncToolAdapter final : public Tool
{
public:
AsyncToolAdapter(ToolSchema schema, F function)
: schema_(std::move(schema)), function_(std::move(function))
{
validateToolSchema(schema_);
}
const ToolSchema& schema() const override
{
return schema_;
}
mw::Task<mw::E<ToolResult>> invoke(nlohmann::json arguments,
ToolContext context) override
{
co_return co_await function_(std::move(arguments), std::move(context));
}
private:
ToolSchema schema_;
F function_;
};
} // namespace internal
/// Adapt a synchronous callable so it runs on the runtime's worker pool.
template <SyncToolCallable F>
std::unique_ptr<Tool> makeTool(ToolSchema schema, F function)
{
return std::make_unique<internal::SyncToolAdapter<F>>(std::move(schema),
std::move(function));
}
/// Adapt an asynchronous callable without moving it to a worker thread.
template <AsyncToolCallable F>
std::unique_ptr<Tool> makeTool(ToolSchema schema, F function)
{
return std::make_unique<internal::AsyncToolAdapter<F>>(std::move(schema),
std::move(function));
}
/// Owns named tools and their options in deterministic insertion order.
/// Registry access is not thread-safe. Running calls keep replaced tools alive.
class ToolRegistry
{
public:
/// Add a tool unless its name is already registered.
bool add(std::unique_ptr<Tool> tool, ToolOptions options = {});
/// Add a tool or replace the entry already registered under its name.
void replace(std::unique_ptr<Tool> tool, ToolOptions options = {});
/// Find a registered tool by name, or return null when it is absent.
Tool* find(std::string_view name);
/// Return registered options, or default options when the tool is absent.
ToolOptions options(std::string_view name) const;
/// Return schemas in deterministic insertion order.
std::vector<ToolSchema> schemas() const;
/// Return the number of registered tools.
std::size_t size() const;
private:
friend struct internal::ToolAccess;
struct Entry
{
// In-flight invocations retain ownership after timeout or replacement.
std::shared_ptr<Tool> tool;
ToolOptions options;
};
std::unordered_map<std::string, Entry> by_name_;
std::vector<std::string> insertion_order_;
};
} // namespace agent_smith