Files
Aethera/mcp/core/Control_Service.cpp
2026-09-04 21:55:18 +08:00

583 lines
32 KiB
C++

#include "Control_Service.hpp"
#include "Control_Service.ipp"
#include "Control_Requests.hpp"
#include "Protocol_Type.hpp"
#include "runtime/Datoviz_Observation_Json.hpp"
#include "runtime/Gallery_Plots.hpp"
#include "runtime/Input_Event.hpp"
#include "runtime/Taskflow_Trace_Json.hpp"
#include <magic_enum/magic_enum.hpp>
#include <render_2D/module/plottable/Plottables.hpp>
#include <render_3D/Render_3D.hpp>
#include <algorithm>
#include <array>
#include <cmath>
#include <stdexcept>
#include <utility>
#include <vector>
namespace aethera::mcp {
namespace {
using Invoke = Tool_Call_Output (*)(Control_Service&, const nlohmann::json&);
using Schema = nlohmann::json (*)();
struct Operation {
std::string_view name; /* Stable MCP tool name. */
std::string_view description; /* Business description used for tool selection. */
Schema schema; /* Protocol input schema producer. */
Invoke invoke; /* Protocol-independent business entry point. */
};
struct Control_Stop_State final {
explicit Control_Stop_State(std::size_t count, std::function<void()> value_completion) : remaining(count), completion(std::move(value_completion)) {}
void retired() noexcept {
if (remaining.fetch_sub(1, std::memory_order_acq_rel) != 1) return;
try {
completion();
}
catch (...) {
std::terminate();
}
}
std::atomic_size_t remaining;
std::function<void()> completion;
};
template <typename Request>
[[nodiscard]] nlohmann::json request_schema() {
return describe_protocol_type<Request>();
}
template <typename Request, typename Callback>
[[nodiscard]] Tool_Call_Output decode_and_call(const nlohmann::json& arguments, Callback&& callback) {
try {
Request request{};
decode_protocol_value(request, arguments);
return std::forward<Callback>(callback)(request);
}
catch (const nlohmann::json::exception& failure) {
return {Tool_Call_Result::invalid_arguments, {}, failure.what()};
}
catch (const std::invalid_argument& failure) {
return {Tool_Call_Result::invalid_arguments, {}, failure.what()};
}
}
[[nodiscard]] nlohmann::json statistics_json(const Statistics_Summary& summary) {
return {{"sample_count", summary.sample_count}, {"average_ns", summary.average}, {"p95_ns", summary.p95}, {"standard_deviation_ns", summary.standard_deviation}};
}
[[nodiscard]] std::string render_error_name(const scene::Render_Error& error) {
return std::visit([](auto value) { return std::string{magic_enum::enum_name(value)}; }, error);
}
[[nodiscard]] nlohmann::json frame_policy_state_json(const Frame_Policy::State& state) {
return {
{"timer_ticks", state.timer_ticks},
{"dropped_timer_ticks", state.dropped_timer_ticks},
{"completed_frames", state.completed_frames},
{"effective_frames_per_second", state.effective_frames_per_second},
{"render_capacity_fps", state.render_capacity_fps ? nlohmann::json{*state.render_capacity_fps} : nlohmann::json{nullptr}},
{"send_capacity_fps", state.send_capacity_fps ? nlohmann::json{*state.send_capacity_fps} : nlohmann::json{nullptr}},
{"render_time", statistics_json(state.render_time_ns)},
{"send_time", statistics_json(state.send_time_ns)},
{"end_to_end_time", statistics_json(state.end_to_end_time_ns)},
{"configuration_error", state.configuration_error ? nlohmann::json{magic_enum::enum_name(*state.configuration_error)} : nlohmann::json{nullptr}},
{"render_submission_error", state.render_submission_error ? nlohmann::json{render_error_name(*state.render_submission_error)} : nlohmann::json{nullptr}}
};
}
[[nodiscard]] nlohmann::json frame_policy_schema(const Throttled_Latest_only& policy) {
const auto user_rate = policy.get<Prop_Tag>(&Frame_Policy::Prop::user_frames_per_second);
return {
{"id", "frame-analysis"},
{"label", "Frame pipeline"},
{"kind", "analysis"},
{"fields", nlohmann::json::array({
{{"key", "user_frames_per_second"}, {"editor", "number"}, {"editable", true}, {"minimum", 0.1}, {"value", user_rate ? nlohmann::json{*user_rate} : nlohmann::json{nullptr}}},
{{"key", "render_rate_limit_enabled"}, {"editor", "boolean"}, {"editable", true}, {"value", policy.get<Prop_Tag>(&Frame_Policy::Prop::render_rate_limit_enabled)}},
{{"key", "send_rate_limit_enabled"}, {"editor", "boolean"}, {"editable", true}, {"value", policy.get<Prop_Tag>(&Frame_Policy::Prop::send_rate_limit_enabled)}},
{{"key", "statistics_enabled"}, {"editor", "boolean"}, {"editable", true}, {"value", policy.get<Prop_Tag>(&Frame_Policy::Prop::statistics_enabled)}},
{{"key", "statistics_window_size"}, {"editor", "integer"}, {"editable", true}, {"minimum", 1}, {"value", policy.get<Prop_Tag>(&Frame_Policy::Prop::statistics_window_size)}}
})}
};
}
[[nodiscard]] Tool_Call_Output list_plots(Control_Service& service, const nlohmann::json& arguments) {
return decode_and_call<Empty_Request>(arguments, [&service](const auto&) { return Tool_Call_Output{Tool_Call_Result::ok, service.plot_catalog(), {}}; });
}
[[nodiscard]] Tool_Call_Output plot_schema(Control_Service& service, const nlohmann::json& arguments) {
return decode_and_call<Plot_Request>(arguments, [&service](const auto& request) {
if (!service.contains_plot(request.plot)) return Tool_Call_Output{Tool_Call_Result::unknown_plot, {}, "unknown plot"};
return Tool_Call_Output{Tool_Call_Result::ok, service.plot_schema(request.plot), {}};
});
}
[[nodiscard]] Tool_Call_Output plot_diagnostics(Control_Service& service, const nlohmann::json& arguments) {
return decode_and_call<Plot_Request>(arguments, [&service](const auto& request) {
if (!service.contains_plot(request.plot)) return Tool_Call_Output{Tool_Call_Result::unknown_plot, {}, "unknown plot"};
return Tool_Call_Output{Tool_Call_Result::ok, service.plot_diagnostics(request.plot), {}};
});
}
[[nodiscard]] Tool_Call_Output component_state(Control_Service& service, const nlohmann::json& arguments) {
return decode_and_call<Component_Request>(arguments, [&service](const auto& request) {
if (!service.contains_plot(request.plot)) return Tool_Call_Output{Tool_Call_Result::unknown_plot, {}, "unknown plot"};
auto result = service.plot_component_state(request.plot, request.component);
if (!result.value("success", true)) return Tool_Call_Output{Tool_Call_Result::rejected, std::move(result), "unknown component"};
return Tool_Call_Output{Tool_Call_Result::ok, std::move(result), {}};
});
}
[[nodiscard]] Tool_Call_Output write_property(Control_Service& service, const nlohmann::json& arguments) {
return decode_and_call<Write_Property_Request>(arguments, [&service](const auto& request) {
if (!service.contains_plot(request.plot)) return Tool_Call_Output{Tool_Call_Result::unknown_plot, {}, "unknown plot"};
auto result = service.write_plot_property(request.plot, request.component, request.property, request.value);
const bool success = result.value("success", false);
return Tool_Call_Output{success ? Tool_Call_Result::ok : Tool_Call_Result::rejected, std::move(result), success ? "" : "property write rejected"};
});
}
[[nodiscard]] Tool_Call_Output generate_data(Control_Service& service, const nlohmann::json& arguments) {
return decode_and_call<Generate_Data_Request>(arguments, [&service](const auto& request) {
if (!service.contains_plot(request.plot)) return Tool_Call_Output{Tool_Call_Result::unknown_plot, {}, "unknown plot"};
auto result = service.generate_plot_data(request.plot, request.input);
const bool success = result.value("success", false);
return Tool_Call_Output{success ? Tool_Call_Result::ok : Tool_Call_Result::rejected, std::move(result), success ? "" : "data generation rejected"};
});
}
[[nodiscard]] Tool_Call_Output submit_plot_input(Control_Service& service, const nlohmann::json& arguments) {
return decode_and_call<Plot_Input_Request>(arguments, [&service](auto request) {
if (!service.contains_plot(request.plot)) return Tool_Call_Output{Tool_Call_Result::unknown_plot, {}, "unknown plot"};
const auto time_milliseconds = request.event.at("time_milliseconds").get<double>();
service.submit_input(request.plot, web::make_input_event(request.event));
return Tool_Call_Output{Tool_Call_Result::ok, {{"accepted", true}, {"plot", request.plot}, {"time_milliseconds", time_milliseconds}}, {}};
});
}
[[nodiscard]] Tool_Call_Output render_plot_at(Control_Service& service, const nlohmann::json& arguments) {
return decode_and_call<Plot_Render_Request>(arguments, [&service](const auto& request) {
if (!service.contains_plot(request.plot)) return Tool_Call_Output{Tool_Call_Result::unknown_plot, {}, "unknown plot"};
service.request_frame(request.plot, {.issued_at = std::chrono::steady_clock::now(), .time_milliseconds = request.time_milliseconds, .width = request.width, .height = request.height});
return Tool_Call_Output{Tool_Call_Result::ok, {{"accepted", true}, {"plot", request.plot}, {"time_milliseconds", request.time_milliseconds}, {"width", request.width}, {"height", request.height}}, {}};
});
}
constexpr std::array operations{
Operation{"aethera_plot_list", "List every 2D and 3D gallery plot.", &request_schema<Empty_Request>, &list_plots},
Operation{"aethera_plot_schema", "Describe a plot and its editable render components.", &request_schema<Plot_Request>, &plot_schema},
Operation{"aethera_plot_diagnostics", "Read the current adaptive Frame Policy and backend diagnostics.", &request_schema<Plot_Request>, &plot_diagnostics},
Operation{"aethera_component_state", "Read one render component's published state.", &request_schema<Component_Request>, &component_state},
Operation{"aethera_component_write", "Write one editable render component property.", &request_schema<Write_Property_Request>, &write_property},
Operation{"aethera_data_generate", "Generate input data for a gallery plot.", &request_schema<Generate_Data_Request>, &generate_data},
Operation{"aethera_plot_input", "Submit one timestamped input event without waiting for rendering.", &request_schema<Plot_Input_Request>, &submit_plot_input},
Operation{"aethera_plot_render_at", "Replace the latest pending plot request consumed by a subsequent policy tick.", &request_schema<Plot_Render_Request>, &render_plot_at}
};
scene::Render_Result map_render_result(const render_2d::Render_Scene_2D_Result& result) {
if (result) return {};
return std::visit([](auto error) -> scene::Render_Result {
using Error = decltype(error);
if constexpr (std::same_as<Error, render_2d::Render_Scene_2D_State_Result>) {
switch (error) {
case render_2d::Render_Scene_2D_State_Result::frame_missing: return std::unexpected(scene::Render_Error{scene::Render_State_Result::frame_missing});
case render_2d::Render_Scene_2D_State_Result::completion_missing: return std::unexpected(scene::Render_Error{scene::Render_State_Result::completion_missing});
case render_2d::Render_Scene_2D_State_Result::view_inactive: return std::unexpected(scene::Render_Error{scene::Render_State_Result::view_inactive});
case render_2d::Render_Scene_2D_State_Result::empty_viewport: return std::unexpected(scene::Render_Error{scene::Render_State_Result::empty_viewport});
}
std::terminate();
}
else {
return std::unexpected(scene::Render_Error{error});
}
}, result.error());
}
scene::Render_Result map_render_result(render_3d::Render_Scene_3D::Render_Result result) {
using Result = render_3d::Render_Scene_3D::Render_Result;
switch (result) {
case Result::submitted: return {};
case Result::frame_missing: return std::unexpected(scene::Render_Error{scene::Render_State_Result::frame_missing});
case Result::completion_missing: return std::unexpected(scene::Render_Error{scene::Render_State_Result::completion_missing});
case Result::view_inactive: return std::unexpected(scene::Render_Error{scene::Render_State_Result::view_inactive});
case Result::empty_viewport: return std::unexpected(scene::Render_Error{scene::Render_State_Result::empty_viewport});
case Result::backend_unavailable: return std::unexpected(scene::Render_Error{scene::Render_State_Result::backend_unavailable});
}
std::terminate();
}
} // namespace
proxy<scene::Frame> Control_Service::Private::Policy_Scene::Private::create_frame() {
return entry->create_frame();
}
scene::Render_Result Control_Service::Private::Policy_Scene::Private::render(proxy<scene::Frame>& frame, scene::Render_Completion completion) {
return entry->render(frame, std::move(completion));
}
Control_Service::Private::Entry::Entry(std::string value_id, web::Gallery_Build build, proxy<frame_policy::Sink> value_sink, std::uint32_t width, std::uint32_t height) : components(std::move(build.first)), scene(std::move(build.second)), sink(std::move(value_sink)), id(std::move(value_id)), output_width(width), output_height(height) {}
Control_Service::Private::Entry::~Entry() = default;
Control_Service::Private::Entry_Retirement::~Entry_Retirement() noexcept {
entry.reset();
if (!completion) return;
try {
completion();
}
catch (...) {
std::terminate();
}
}
std::shared_ptr<Throttled_Latest_only> Control_Service::Private::Entry::current_policy() const noexcept {
return policy.load(std::memory_order_acquire);
}
void Control_Service::Private::Entry::start() {
auto timer = frame_policy::make_timer_service();
auto scene_endpoint = make_model_proxy<frame_policy::Scene, Policy_Scene>(this);
Throttled_Latest_only::Builder builder(std::move(timer), std::move(scene_endpoint), std::move(sink));
builder
.set<Prop_Tag>(&Frame_Policy::Prop::user_frames_per_second, std::optional<double>{100.0})
.set<Prop_Tag>(&Frame_Policy::Prop::render_rate_limit_enabled, false)
.set<Prop_Tag>(&Frame_Policy::Prop::send_rate_limit_enabled, false)
.set<Prop_Tag>(&Frame_Policy::Prop::statistics_enabled, true)
.set<Prop_Tag>(&Frame_Policy::Prop::statistics_window_size, std::size_t{120});
auto next = std::shared_ptr<Throttled_Latest_only>{builder.build().release()};
const auto started = next->start();
if (!started) throw std::logic_error("Gallery Frame Policy rejected start");
policy.store(std::move(next), std::memory_order_release);
}
void Control_Service::Private::Entry::stop(std::shared_ptr<Entry_Retirement> retirement) noexcept {
auto current = policy.exchange({}, std::memory_order_acq_rel);
if (!current) return;
auto lifetime = std::make_shared<Entry_Stop_Lifetime>();
lifetime->retirement = std::move(retirement);
lifetime->policy = std::move(current);
const auto stopped = lifetime->policy->stop([lifetime] {});
if (!stopped) std::terminate();
}
void Control_Service::Private::Entry::request_frame(web::Gallery_Frame_Request request) {
if (!std::isfinite(request.time_milliseconds) || request.width == 0 || request.height == 0) throw std::invalid_argument("Gallery frame request is invalid");
if (request.issued_at == std::chrono::steady_clock::time_point{}) request.issued_at = std::chrono::steady_clock::now();
requested_frame.store(std::make_shared<const web::Gallery_Frame_Request>(std::move(request)), std::memory_order_release);
}
bool Control_Service::Private::Entry::reserve_trace() noexcept {
auto remaining = taskflow_trace_remaining.load(std::memory_order_acquire);
while (remaining != 0) if (taskflow_trace_remaining.compare_exchange_weak(remaining, remaining - 1, std::memory_order_acq_rel, std::memory_order_acquire)) return true;
return false;
}
void Control_Service::Private::Entry::restore_trace() noexcept {
taskflow_trace_remaining.fetch_add(1, std::memory_order_release);
}
void Control_Service::Private::Entry::store_trace(const Taskflow_Execution_Trace& trace, const web::Gallery_Frame_Request& request, const nlohmann::json& backend) {
auto value = web::taskflow_trace_json(trace, request.sequence, request.correlation_id, {}, backend);
if (const auto current = current_policy()) current->get<State_Tag>([&](const Frame_Policy::State& state) { value["frame_policy"] = frame_policy_state_json(state); });
auto encoded = std::make_shared<const nlohmann::json>(std::move(value));
auto control = taskflow_trace_control.load(std::memory_order_acquire);
for (;;) {
const auto requested = static_cast<std::uint32_t>(control >> 32U);
const auto captured = static_cast<std::uint32_t>(control);
if (captured >= requested) return;
taskflow_trace_slots[captured].store(encoded, std::memory_order_release);
const auto next = (static_cast<std::uint64_t>(requested) << 32U) | static_cast<std::uint64_t>(captured + 1U);
if (taskflow_trace_control.compare_exchange_weak(control, next, std::memory_order_release, std::memory_order_acquire)) return;
}
}
proxy<scene::Frame> Control_Service::Private::Entry::create_frame() {
const bool use_3d = std::holds_alternative<std::unique_ptr<render_3d::Render_Scene_3D>>(scene);
return pro::make_proxy<scene::Frame, web::Gallery_Frame_Slot>(use_3d);
}
scene::Render_Result Control_Service::Private::Entry::render(proxy<scene::Frame>& frame, scene::Render_Completion completion) {
auto* slot = frame ? proxy_cast<web::Gallery_Frame_Slot>(&*frame) : nullptr;
if (!slot) return std::unexpected(scene::Render_Error{scene::Render_State_Result::frame_missing});
if (!completion) return std::unexpected(scene::Render_Error{scene::Render_State_Result::completion_missing});
auto request_owner = requested_frame.exchange({}, std::memory_order_acq_rel);
web::Gallery_Frame_Request request = request_owner ? *request_owner : web::Gallery_Frame_Request{};
if (!request_owner) {
request.issued_at = std::chrono::steady_clock::now();
request.time_milliseconds = std::chrono::duration<double, std::milli>(request.issued_at - started_at).count();
request.width = output_width;
request.height = output_height;
}
request.sequence = next_sequence.fetch_add(1, std::memory_order_relaxed) + 1;
if (request.correlation_id == 0) request.correlation_id = request.sequence;
slot->request = request;
++slot->generation;
slot->taskflow_trace_reserved = reserve_trace();
components->update(request);
auto* policy_frame = std::addressof(frame); /* Non-owning pointer to a stable policy slot; Frame Policy keeps it alive until completion returns. */
if (auto* native = std::get_if<std::unique_ptr<render_2d::Frame_2D>>(&slot->native)) {
auto& scene_2d = *std::get<std::unique_ptr<render_2d::Render_Scene_2D>>(scene);
scene_2d.set<Prop_Tag>(&render_2d::Render_Scene_2D::Prop::viewport, render_2d::Size{static_cast<int>(request.width), static_cast<int>(request.height)});
(*native)->begin({request.sequence, slot->generation}, render_2d::Frame_2D::native_pixel_format);
if (slot->taskflow_trace_reserved) (*native)->request_taskflow_trace();
auto result = map_render_result(scene_2d.render(native->get(), [this, slot, policy_frame, completion = std::move(completion)](render_2d::Frame_2D*, std::exception_ptr failure) mutable {
if (std::exchange(slot->taskflow_trace_reserved, false)) {
if (!failure) {
if (auto trace = std::get<std::unique_ptr<render_2d::Frame_2D>>(slot->native)->take_taskflow_trace()) store_trace(*trace, slot->request);
else restore_trace();
}
else restore_trace();
}
completion(*policy_frame, std::move(failure));
}));
if (!result && std::exchange(slot->taskflow_trace_reserved, false)) restore_trace();
return result;
}
auto& scene_3d = *std::get<std::unique_ptr<render_3d::Render_Scene_3D>>(scene);
auto& native = std::get<std::unique_ptr<render_3d::Frame_3D>>(slot->native);
scene_3d.set<Prop_Tag>(&render_3d::Render_Scene_3D::Prop::viewport, render_3d::Extent{request.width, request.height});
native->begin({request.sequence, slot->generation}, render_3d::Frame_3D_Output::pixels, render_3d::Frame_3D::native_pixel_format);
if (slot->taskflow_trace_reserved) native->request_taskflow_trace();
auto result = map_render_result(scene_3d.render(native.get(), [this, slot, policy_frame, completion = std::move(completion)](render_3d::Frame_3D*, std::exception_ptr failure) mutable {
auto& completed = std::get<std::unique_ptr<render_3d::Frame_3D>>(slot->native);
nlohmann::json backend;
if (!failure) {
if (auto observation = completed->take_datoviz_observation()) {
backend = datoviz_observation_json(*observation);
datoviz_observation.store(std::make_shared<const nlohmann::json>(backend), std::memory_order_release);
}
}
if (std::exchange(slot->taskflow_trace_reserved, false)) {
if (!failure) {
if (auto trace = completed->take_taskflow_trace()) store_trace(*trace, slot->request, backend);
else restore_trace();
}
else restore_trace();
}
completion(*policy_frame, std::move(failure));
}));
if (!result && std::exchange(slot->taskflow_trace_reserved, false)) restore_trace();
return result;
}
nlohmann::json Control_Service::Private::Entry::schema() const {
auto result = components->schema();
if (const auto current = current_policy()) {
auto analysis = frame_policy_schema(*current);
const auto generator = components->data_generator_schema();
if (!generator.is_null()) analysis["data_generator"] = generator;
result["frame_analysis"] = std::move(analysis);
}
return result;
}
nlohmann::json Control_Service::Private::Entry::diagnostics() const {
const auto current = current_policy();
if (!current) return {{"protocol", "aethera.plot.diagnostics"}, {"version", 6}, {"available", false}, {"plot", id}};
nlohmann::json result;
current->get<State_Tag>([&](const Frame_Policy::State& state) {
result = {
{"protocol", "aethera.plot.diagnostics"},
{"version", 6},
{"available", true},
{"plot", id},
{"dimension", std::holds_alternative<std::unique_ptr<render_3d::Render_Scene_3D>>(scene) ? "3D" : "2D"},
{"frame_policy", frame_policy_state_json(state)}
};
});
if (const auto observation = datoviz_observation.load(std::memory_order_acquire)) result["datoviz"] = *observation;
return result;
}
void Control_Service::Private::Entry::reset_diagnostics() noexcept {
datoviz_observation.store({}, std::memory_order_release);
}
void Control_Service::Private::Entry::request_trace(std::size_t frame_count) {
if (frame_count == 0 || frame_count > maximum_taskflow_trace_frames) throw std::invalid_argument("Plot Taskflow trace frame_count must be between 1 and 120");
auto control = taskflow_trace_control.load(std::memory_order_acquire);
for (;;) {
const auto requested = static_cast<std::uint32_t>(control >> 32U);
const auto captured = static_cast<std::uint32_t>(control);
if (requested != captured) throw std::logic_error("A Plot Taskflow trace request is already active");
const auto next = static_cast<std::uint64_t>(frame_count) << 32U;
if (taskflow_trace_control.compare_exchange_weak(control, next, std::memory_order_release, std::memory_order_acquire)) break;
}
for (auto& slot : taskflow_trace_slots) slot.store({}, std::memory_order_release);
taskflow_trace_remaining.store(frame_count, std::memory_order_release);
}
nlohmann::json Control_Service::Private::Entry::trace() const {
nlohmann::json frames = nlohmann::json::array();
const auto control = taskflow_trace_control.load(std::memory_order_acquire);
const auto requested = static_cast<std::uint32_t>(control >> 32U);
const auto captured = static_cast<std::uint32_t>(control);
for (std::uint32_t index = 0; index < captured; ++index) if (const auto frame = taskflow_trace_slots[index].load(std::memory_order_acquire)) frames.push_back(*frame);
return {{"protocol", "aethera.taskflow.frames"}, {"version", 1}, {"requested", requested}, {"remaining", taskflow_trace_remaining.load(std::memory_order_acquire)}, {"captured", frames.size()}, {"complete", requested != 0 && frames.size() == requested}, {"frames", std::move(frames)}};
}
Control_Service::Control_Service() : d(std::make_unique<Private>()) {}
Control_Service::~Control_Service() {
if (!d || d->stop_started) return;
try {
stop([] {});
}
catch (...) {
std::terminate();
}
}
std::shared_ptr<Control_Service> Control_Service::create(Gallery_Output output) {
auto service = std::shared_ptr<Control_Service>(new Control_Service);
service->d->plots.reserve(web::gallery_plot_definitions().size());
for (const auto& definition : web::gallery_plot_definitions()) {
auto sink = output.make_sink ? output.make_sink(definition.id) : proxy<frame_policy::Sink>{};
/* Headless MCP and partial test catalogs intentionally complete frames without media. */
if (!sink) sink = make_model_proxy<frame_policy::Sink, Private::Headless_Sink>();
auto entry = std::make_shared<Private::Entry>(std::string{definition.id}, definition.create(), std::move(sink), output.width, output.height);
service->d->plots.emplace(entry->id, entry);
entry->start();
}
return service;
}
bool Control_Service::contains_plot(std::string_view id) const noexcept {
return d->plots.contains(std::string{id});
}
void Control_Service::stop(std::function<void()> completion) {
if (!completion) throw std::invalid_argument("Control Service stop completion is empty");
if (d->stop_started) throw std::logic_error("Control Service stop already started");
d->stop_started = true;
if (d->plots.empty()) {
completion();
return;
}
auto state = std::make_shared<Control_Stop_State>(d->plots.size(), std::move(completion));
std::vector<std::shared_ptr<Private::Entry_Retirement>> retiring;
retiring.reserve(d->plots.size());
for (auto& [id, entry] : d->plots) {
(void)id;
auto retirement = std::make_shared<Private::Entry_Retirement>();
retirement->entry = std::move(entry);
retirement->completion = [state] { state->retired(); };
retiring.push_back(std::move(retirement));
}
d->plots.clear();
for (const auto& retirement : retiring) retirement->entry->stop(retirement);
}
void Control_Service::submit_input(std::string_view id, std::unique_ptr<Event> event) {
if (!event) throw std::invalid_argument("Gallery input event is null");
const auto found = d->plots.find(std::string{id});
if (found == d->plots.end()) throw std::invalid_argument("unknown plot");
std::visit([event = std::move(event)](auto& scene) mutable { scene->submit_event(std::move(event)); }, found->second->scene);
}
void Control_Service::request_frame(std::string_view id, web::Gallery_Frame_Request request) {
const auto found = d->plots.find(std::string{id});
if (found == d->plots.end()) throw std::invalid_argument("unknown plot");
found->second->request_frame(std::move(request));
}
nlohmann::json Control_Service::plot_schema(std::string_view id) const {
const auto found = d->plots.find(std::string{id});
if (found == d->plots.end()) throw std::invalid_argument("unknown plot");
return found->second->schema();
}
nlohmann::json Control_Service::write_plot_property(std::string_view id, std::string_view component, std::string_view key, const nlohmann::json& value) {
const auto found = d->plots.find(std::string{id});
if (found == d->plots.end()) throw std::invalid_argument("unknown plot");
auto& entry = *found->second;
if (component != "frame-analysis") return entry.components->write_prop(component, key, value);
const auto current = entry.current_policy();
if (!current) return {{"success", false}, {"error", "Frame Policy is stopping"}};
if (key == "user_frames_per_second") {
std::optional<double> rate;
if (!value.is_null()) {
if (!value.is_number()) return {{"success", false}, {"error", "number or null required"}};
rate = value.get<double>();
if (!std::isfinite(*rate) || *rate <= 0.0) return {{"success", false}, {"error", "positive finite frame rate required"}};
}
current->set<Prop_Tag>(&Frame_Policy::Prop::user_frames_per_second, rate);
return {{"success", true}, {"component", component}, {"key", key}, {"value", rate ? nlohmann::json{*rate} : nlohmann::json{nullptr}}};
}
if (key == "statistics_window_size") {
if (!value.is_number_unsigned()) return {{"success", false}, {"error", "positive integer required"}};
const auto window = value.get<std::size_t>();
if (window == 0) return {{"success", false}, {"error", "positive integer required"}};
current->set<Prop_Tag>(&Frame_Policy::Prop::statistics_window_size, window);
return {{"success", true}, {"component", component}, {"key", key}, {"value", window}};
}
if (key == "render_rate_limit_enabled" || key == "send_rate_limit_enabled" || key == "statistics_enabled") {
if (!value.is_boolean()) return {{"success", false}, {"error", "boolean required"}};
const bool enabled = value.get<bool>();
if (key == "render_rate_limit_enabled") current->set<Prop_Tag>(&Frame_Policy::Prop::render_rate_limit_enabled, enabled);
else if (key == "send_rate_limit_enabled") current->set<Prop_Tag>(&Frame_Policy::Prop::send_rate_limit_enabled, enabled);
else current->set<Prop_Tag>(&Frame_Policy::Prop::statistics_enabled, enabled);
return {{"success", true}, {"component", component}, {"key", key}, {"value", enabled}};
}
return {{"success", false}, {"error", "unknown frame property"}};
}
nlohmann::json Control_Service::plot_component_state(std::string_view id, std::string_view component) const {
const auto found = d->plots.find(std::string{id});
if (found == d->plots.end()) throw std::invalid_argument("unknown plot");
return found->second->components->component_state(component);
}
nlohmann::json Control_Service::generate_plot_data(std::string_view id, const nlohmann::json& input) {
const auto found = d->plots.find(std::string{id});
if (found == d->plots.end()) throw std::invalid_argument("unknown plot");
return found->second->components->generate_data(input);
}
nlohmann::json Control_Service::plot_diagnostics(std::string_view id) const {
const auto found = d->plots.find(std::string{id});
if (found == d->plots.end()) throw std::invalid_argument("unknown plot");
return found->second->diagnostics();
}
void Control_Service::reset_plot_diagnostics(std::string_view id) {
const auto found = d->plots.find(std::string{id});
if (found == d->plots.end()) throw std::invalid_argument("unknown plot");
found->second->reset_diagnostics();
}
void Control_Service::request_plot_taskflow_trace(std::string_view id, std::size_t frame_count) {
const auto found = d->plots.find(std::string{id});
if (found == d->plots.end()) throw std::invalid_argument("unknown plot");
found->second->request_trace(frame_count);
}
nlohmann::json Control_Service::plot_taskflow_trace(std::string_view id) const {
const auto found = d->plots.find(std::string{id});
if (found == d->plots.end()) throw std::invalid_argument("unknown plot");
return found->second->trace();
}
nlohmann::json Control_Service::plot_catalog() const {
nlohmann::json result = nlohmann::json::array();
for (const auto& definition : web::gallery_plot_definitions()) result.push_back({{"id", definition.id}, {"title", definition.title}, {"category", definition.category}, {"description", definition.description}, {"dimension", web::plot_dimension_name(definition.dimension)}});
return result;
}
nlohmann::json Control_Service::tool_catalog() const {
nlohmann::json result = nlohmann::json::array();
for (const auto& operation : operations) result.push_back({{"name", operation.name}, {"description", operation.description}, {"inputSchema", operation.schema()}});
return result;
}
Tool_Call_Output Control_Service::call_tool(std::string_view name, const nlohmann::json& arguments) {
for (const auto& operation : operations) if (operation.name == name) return operation.invoke(*this, arguments);
return {Tool_Call_Result::unknown_tool, {}, "unknown tool"};
}
} // namespace aethera::mcp