From 99041963beaa7b9441051208d32df8a9e2f117e1 Mon Sep 17 00:00:00 2001 From: Kaushik Iska Date: Fri, 1 May 2026 15:19:26 +0900 Subject: [PATCH] chore: simplify langfuse module + multi-step coordinator Langfuse module (`include/ai/langfuse.h`, `src/langfuse/tracer.cpp`): - Single Trace::to_iso8601(time_point) helper replaces three duplicated gmtime_r/snprintf blocks (now_iso8601 plus two inline lambdas in finish_generation / build_trace_event). - Capture timestamp once per record_tool_call_{start,finish} event (previously two clock reads per span event). - Anonymous-namespace constants for event type discriminators (trace-create / span-create / span-update / generation-create) and level / unit strings, replacing scattered magic strings. - Extract make_span_create + wrap_event helpers; the orphan-span fallback in record_tool_call_finish reuses them instead of inlining 20+ lines of duplicated body construction. - Drop the over-engineered ParsedHost/parse_host: httplib::Client's URL constructor handles scheme/host/port already; we only need a base-path extractor for Langfuse instances served under a sub-path. - Cache httplib::Client + Basic-auth headers on the Tracer (lazily constructed under mu_) so repeat send_batch calls reuse the connection / TLS session instead of paying a fresh handshake. - Replace 6-arg `start_trace(name, input, user, session, metadata, tags)` sprawl with `start_trace(name, TraceOptions{})` to match the rest of the SDK's struct-of-options style. - Default Config::error_policy = ErrorPolicy::kStrict (was best_effort=true) so misconfigurations surface at integration time instead of being silently swallowed. - Reject post-end mutations: set_*/instrument/finish_generation/record_* bail out if Trace::end() has fired. - Replace hand-rolled UUID v4 with stduuid (vendored under third_party/stduuid-header-only/). Multi-step coordinator (`src/tools/multi_step_coordinator.cpp`): - Avoid re-copying initial_messages every loop iteration. step_messages is grown in place: erase back to the immutable prefix + insert the running response_messages accumulator, instead of `step_options = initial_options; step_options.messages = initial_messages; step_options.messages.insert(...)` per step. - Remove the dead create_next_step_options stub from header + impl (not part of any external ABI; was only kept as scaffolding during the refactor). - Trim narrating comment that referenced an external repo path. Vendoring: - Add third_party/stduuid-header-only/ (uuid.h + LICENSE) and third_party/stduuid-cmake/ wrapper exposing a stduuid::stduuid INTERFACE target. Wired into ai-sdk-cpp-langfuse via PRIVATE link. Verified: full ctest suite (221/227 pass; 6 failing are ClickHouseIntegrationTest cases that need a local ClickHouse server, unchanged from main). End-to-end Langfuse example produces the expected trace with 1 generation + N tool spans nested correctly. --- CMakeLists.txt | 4 +- examples/langfuse_tracing.cpp | 4 +- include/ai/langfuse.h | 50 +- include/ai/tools.h | 6 - src/langfuse/tracer.cpp | 418 +++++----- src/tools/multi_step_coordinator.cpp | 47 +- third_party/CMakeLists.txt | 3 + third_party/stduuid-cmake/CMakeLists.txt | 24 + third_party/stduuid-header-only/LICENSE | 21 + third_party/stduuid-header-only/uuid.h | 967 +++++++++++++++++++++++ 10 files changed, 1286 insertions(+), 258 deletions(-) create mode 100644 third_party/stduuid-cmake/CMakeLists.txt create mode 100644 third_party/stduuid-header-only/LICENSE create mode 100644 third_party/stduuid-header-only/uuid.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 2eb379b..127f53d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -144,7 +144,8 @@ foreach(provider openai anthropic) ) endforeach() -# Langfuse tracing component links to core and uses httplib + OpenSSL. +# Langfuse tracing component links to core and uses httplib + OpenSSL + +# stduuid (header-only, vendored under third_party/stduuid-header-only). target_link_libraries(ai-sdk-cpp-langfuse PUBLIC ai::core @@ -153,6 +154,7 @@ target_link_libraries(ai-sdk-cpp-langfuse $ $ $ + $ ) target_compile_definitions(ai-sdk-cpp-langfuse PUBLIC diff --git a/examples/langfuse_tracing.cpp b/examples/langfuse_tracing.cpp index 261d268..f772614 100644 --- a/examples/langfuse_tracing.cpp +++ b/examples/langfuse_tracing.cpp @@ -24,9 +24,9 @@ namespace { -const char* env_or(const char* name, const char* fallback) { +const char* getenv_nonempty(const char* name) { const char* v = std::getenv(name); - return (v && *v) ? v : fallback; + return (v && *v) ? v : nullptr; } ai::JsonValue lookup_user(const ai::JsonValue& args, diff --git a/include/ai/langfuse.h b/include/ai/langfuse.h index 99d6e74..f14765f 100644 --- a/include/ai/langfuse.h +++ b/include/ai/langfuse.h @@ -68,19 +68,34 @@ struct Config { int connection_timeout_sec = 10; int read_timeout_sec = 30; - /// If true, suppress HTTP/JSON errors from `Trace::end()` (best-effort - /// tracing). Failures are still logged via the SDK logger. - bool best_effort = true; + /// Error policy for `Trace::end()`. Strict (the default) reports HTTP/JSON + /// failures via the return value so misconfigurations surface immediately; + /// kBestEffort swallows them (failures are still logged). + enum class ErrorPolicy { kStrict, kBestEffort }; + ErrorPolicy error_policy = ErrorPolicy::kStrict; }; class Trace; +/// Optional fields settable at trace-creation time. Mirrors the +/// struct-of-options style used elsewhere in the SDK (GenerateOptions, Config). +struct TraceOptions { + std::optional input; + std::optional output; + std::optional user_id; + std::optional session_id; + std::optional metadata; + std::vector tags; +}; + /// Tracer holds the Langfuse credentials and is the entry point for creating /// traces. Construct one per process; it is safe to create multiple traces /// concurrently from a single Tracer. class Tracer { public: explicit Tracer(Config config); + ~Tracer(); // Out-of-line: HttpState is forward-declared; its destructor + // needs the complete type, which lives in tracer.cpp. /// True iff host, public_key and secret_key are set. bool is_valid() const; @@ -89,13 +104,8 @@ class Tracer { /// Start a new trace. Returns a shared handle so callbacks attached via /// `Trace::instrument()` can keep it alive until generate_text returns. - std::shared_ptr start_trace( - const std::string& name, - std::optional input = std::nullopt, - std::optional user_id = std::nullopt, - std::optional session_id = std::nullopt, - std::optional metadata = std::nullopt, - std::vector tags = {}); + std::shared_ptr start_trace(const std::string& name, + TraceOptions opts = {}); /// POST a batch of ingestion events to Langfuse. Returns true on 2xx. /// Public so advanced callers can build their own event batches; most users @@ -103,7 +113,14 @@ class Tracer { bool send_batch(const JsonValue& events); private: + // Lazily constructed httplib client + base path + Basic-auth header. Cached + // to avoid TLS handshake per send_batch call. Guarded by mu_. + struct HttpState; + HttpState& http_state(); + Config config_; + std::mutex mu_; + std::unique_ptr http_; }; /// A Trace owns a list of pending ingestion events and writes them all to @@ -141,14 +158,20 @@ class Trace : public std::enable_shared_from_this { void finish_generation(const GenerateResult& result); /// Flush all accumulated events to Langfuse. Idempotent; subsequent calls - /// are no-ops. Returns true on success (or when best_effort is enabled and - /// the request failed). + /// are no-ops. After end(), further set_* / instrument / finish_generation + /// calls become no-ops to prevent silently dropping events. Returns true on + /// success (or when Config::error_policy is kBestEffort and the request + /// failed). bool end(); /// Generate a new UUID v4. Exposed so callers (and `Tracer::start_trace`) /// can mint trace ids without re-implementing UUID generation. static std::string new_uuid(); + /// Format a system_clock time_point as RFC 3339 / ISO 8601 with millisecond + /// precision and trailing 'Z'. Public so callers can stamp custom events. + static std::string to_iso8601(std::chrono::system_clock::time_point t); + private: struct PendingGeneration { std::string id; @@ -169,9 +192,6 @@ class Trace : public std::enable_shared_from_this { // input/output/metadata/tags. JsonValue build_trace_event() const; - // Helpers - static std::string now_iso8601(); - Tracer& tracer_; std::string id_; std::string name_; diff --git a/include/ai/tools.h b/include/ai/tools.h index 1e2f0fe..ef073b3 100644 --- a/include/ai/tools.h +++ b/include/ai/tools.h @@ -84,12 +84,6 @@ class MultiStepCoordinator { generate_func); private: - /// Create the next generation options based on previous step - static GenerateOptions create_next_step_options( - const GenerateOptions& base_options, - const GenerateResult& previous_result, - const std::vector& tool_results); - /// Convert tool results to messages for the next step static Messages tool_results_to_messages( const std::vector& tool_calls, diff --git a/src/langfuse/tracer.cpp b/src/langfuse/tracer.cpp index d4a51be..d0e966d 100644 --- a/src/langfuse/tracer.cpp +++ b/src/langfuse/tracer.cpp @@ -5,6 +5,7 @@ #include #include #include +#include namespace ai { namespace langfuse { @@ -13,43 +14,52 @@ namespace { constexpr const char* kIngestionPath = "/api/public/ingestion"; -struct ParsedHost { - std::string host; - int port = -1; // -1 means "default for scheme" - bool use_ssl = true; - std::string base_path; // e.g. "/langfuse" if hosted under a sub-path -}; +// Langfuse ingestion event type discriminators +// (packages/shared/src/server/ingestion/types.ts::eventTypes). +constexpr const char* kEventTraceCreate = "trace-create"; +constexpr const char* kEventSpanCreate = "span-create"; +constexpr const char* kEventSpanUpdate = "span-update"; +constexpr const char* kEventGenerationCreate = "generation-create"; + +constexpr const char* kLevelDefault = "DEFAULT"; +constexpr const char* kLevelError = "ERROR"; +constexpr const char* kUsageUnitTokens = "TOKENS"; + +// Strip the scheme prefix off a Langfuse host URL and return the leading +// sub-path (everything after the host[:port], e.g. "/langfuse" if hosted under +// a sub-path). httplib::Client's URL constructor handles host/port/TLS itself, +// so we only need to extract the optional base path. +std::string extract_base_path(const std::string& url) { + std::string s = url; + if (s.starts_with("https://")) + s = s.substr(8); + else if (s.starts_with("http://")) + s = s.substr(7); + auto slash = s.find('/'); + if (slash == std::string::npos) + return {}; + std::string p = s.substr(slash); + if (!p.empty() && p.back() == '/') + p.pop_back(); + return p; +} -ParsedHost parse_host(const std::string& url) { - ParsedHost out; +// Strip any trailing slash from a scheme+host[:port] URL so httplib's URL ctor +// gets exactly what it expects. +std::string strip_path_suffix(const std::string& url) { std::string s = url; + std::string scheme; if (s.starts_with("https://")) { + scheme = "https://"; s = s.substr(8); - out.use_ssl = true; } else if (s.starts_with("http://")) { + scheme = "http://"; s = s.substr(7); - out.use_ssl = false; } - auto slash = s.find('/'); - std::string host_port = (slash == std::string::npos) ? s : s.substr(0, slash); - out.base_path = (slash == std::string::npos) ? "" : s.substr(slash); - if (!out.base_path.empty() && out.base_path.back() == '/') - out.base_path.pop_back(); - - auto colon = host_port.find(':'); - if (colon != std::string::npos) { - out.host = host_port.substr(0, colon); - try { - out.port = std::stoi(host_port.substr(colon + 1)); - } catch (...) { - out.port = -1; - } - } else { - out.host = host_port; - } - - return out; + if (slash != std::string::npos) + s = s.substr(0, slash); + return scheme + s; } std::string base64_encode(const std::string& in) { @@ -70,9 +80,8 @@ std::string base64_encode(const std::string& in) { if (valb > -6) { out.push_back(kAlphabet[((val << 8) >> (valb + 8)) & 0x3F]); } - while (out.size() % 4) { + while (out.size() % 4) out.push_back('='); - } return out; } @@ -96,7 +105,6 @@ JsonValue model_parameters_from(const GenerateOptions& options) { } JsonValue messages_input_from(const GenerateOptions& options) { - // Prefer explicit messages; otherwise synthesise from system+prompt. JsonValue arr = JsonValue::array(); if (!options.system.empty()) { arr.push_back({{"role", "system"}, {"content", options.system}}); @@ -115,14 +123,10 @@ JsonValue messages_input_from(const GenerateOptions& options) { } JsonValue usage_to_langfuse(const Usage& u) { - // Langfuse accepts both legacy `usage` and `usageDetails`. Send legacy form - // for broad compatibility. - return { - {"input", u.prompt_tokens}, - {"output", u.completion_tokens}, - {"total", u.total_tokens}, - {"unit", "TOKENS"}, - }; + return {{"input", u.prompt_tokens}, + {"output", u.completion_tokens}, + {"total", u.total_tokens}, + {"unit", kUsageUnitTokens}}; } } // namespace @@ -131,30 +135,64 @@ JsonValue usage_to_langfuse(const Usage& u) { // Tracer // --------------------------------------------------------------------------- +struct Tracer::HttpState { + httplib::Client client; + std::string base_path; // e.g. "/langfuse" or "" for the standard host + httplib::Headers headers; + + HttpState(const std::string& host_url, + const std::string& public_key, + const std::string& secret_key, + int connection_timeout_sec, + int read_timeout_sec) + : client(strip_path_suffix(host_url)), + base_path(extract_base_path(host_url)) { + client.enable_server_certificate_verification(true); + client.set_connection_timeout(connection_timeout_sec, 0); + client.set_read_timeout(read_timeout_sec, 0); + headers = { + {"Authorization", + "Basic " + base64_encode(public_key + ":" + secret_key)}, + {"User-Agent", "ai-sdk-cpp-langfuse/0.1"}, + {"X-Langfuse-Sdk-Name", "ai-sdk-cpp"}, + {"X-Langfuse-Sdk-Variant", "ai-sdk-cpp"}, + }; + } +}; + Tracer::Tracer(Config config) : config_(std::move(config)) {} +Tracer::~Tracer() = default; + bool Tracer::is_valid() const { return !config_.host.empty() && !config_.public_key.empty() && !config_.secret_key.empty(); } -std::shared_ptr Tracer::start_trace( - const std::string& name, - std::optional input, - std::optional user_id, - std::optional session_id, - std::optional metadata, - std::vector tags) { +Tracer::HttpState& Tracer::http_state() { + // Caller must hold mu_. + if (!http_) { + http_ = std::make_unique( + config_.host, config_.public_key, config_.secret_key, + config_.connection_timeout_sec, config_.read_timeout_sec); + } + return *http_; +} + +std::shared_ptr Tracer::start_trace(const std::string& name, + TraceOptions opts) { auto trace = std::make_shared(*this, Trace::new_uuid(), name); - if (input) - trace->set_input(std::move(*input)); - if (user_id) - trace->set_user_id(std::move(*user_id)); - if (session_id) - trace->set_session_id(std::move(*session_id)); - if (metadata) - trace->set_metadata(std::move(*metadata)); - for (auto& t : tags) + if (opts.input) + trace->set_input(std::move(*opts.input)); + if (opts.output) + trace->set_output(std::move(*opts.output)); + if (opts.user_id) + trace->set_user_id(std::move(*opts.user_id)); + if (opts.session_id) + trace->set_session_id(std::move(*opts.session_id)); + if (opts.metadata) + trace->set_metadata(std::move(*opts.metadata)); + for (auto& t : opts.tags) trace->add_tag(std::move(t)); return trace; } @@ -168,33 +206,16 @@ bool Tracer::send_batch(const JsonValue& events) { return false; } - ParsedHost p = parse_host(config_.host); - // httplib::Client(scheme_host_port) handles http/https + port automatically. - std::string scheme_host_port = - std::string(p.use_ssl ? "https://" : "http://") + p.host; - if (p.port > 0) - scheme_host_port += ":" + std::to_string(p.port); - httplib::Client client(scheme_host_port); - client.enable_server_certificate_verification(true); - client.set_connection_timeout(config_.connection_timeout_sec, 0); - client.set_read_timeout(config_.read_timeout_sec, 0); - - std::string auth = - "Basic " + base64_encode(config_.public_key + ":" + config_.secret_key); - JsonValue body; body["batch"] = events; std::string serialized = body.dump(); - std::string path = p.base_path + kIngestionPath; - httplib::Headers headers = { - {"Authorization", auth}, - {"User-Agent", "ai-sdk-cpp-langfuse/0.1"}, - {"X-Langfuse-Sdk-Name", "ai-sdk-cpp"}, - {"X-Langfuse-Sdk-Variant", "ai-sdk-cpp"}, - }; + std::lock_guard lock(mu_); + auto& s = http_state(); + std::string path = s.base_path + kIngestionPath; + auto res = + s.client.Post(path.c_str(), s.headers, serialized, "application/json"); - auto res = client.Post(path.c_str(), headers, serialized, "application/json"); if (!res) { ai::logger::log_error("Langfuse ingestion failed: {}", httplib::to_string(res.error())); @@ -221,37 +242,52 @@ Trace::Trace(Tracer& tracer, std::string id, std::string name) trace_start_(std::chrono::system_clock::now()) {} void Trace::set_input(JsonValue input) { + if (ended_.load()) + return; std::lock_guard lock(mu_); input_ = std::move(input); } void Trace::set_output(JsonValue output) { + if (ended_.load()) + return; std::lock_guard lock(mu_); output_ = std::move(output); } void Trace::set_user_id(std::string user_id) { + if (ended_.load()) + return; std::lock_guard lock(mu_); user_id_ = std::move(user_id); } void Trace::set_session_id(std::string session_id) { + if (ended_.load()) + return; std::lock_guard lock(mu_); session_id_ = std::move(session_id); } void Trace::set_metadata(JsonValue metadata) { + if (ended_.load()) + return; std::lock_guard lock(mu_); metadata_ = std::move(metadata); } void Trace::add_tag(std::string tag) { + if (ended_.load()) + return; std::lock_guard lock(mu_); tags_.push_back(std::move(tag)); } void Trace::instrument(GenerateOptions& options, const std::string& generation_name) { + if (ended_.load()) + return; + PendingGeneration gen; gen.id = new_uuid(); gen.name = generation_name; @@ -266,8 +302,7 @@ void Trace::instrument(GenerateOptions& options, } // Chain tool callbacks so we can record per-tool spans, preserving any - // user-installed callbacks. Capture a weak_ptr so we do not extend the - // Trace's lifetime beyond the caller's intent. + // user-installed callbacks. std::weak_ptr self = shared_from_this(); auto user_tool_start = options.on_tool_call_start; @@ -288,90 +323,115 @@ void Trace::instrument(GenerateOptions& options, }; } -void Trace::record_tool_call_start(const ToolCall& call) { +namespace { + +// Build a span-create event body. Used for both tool-call starts (open span) +// and the synthetic span emitted in record_tool_call_finish's fallback path. +JsonValue make_span_create(const std::string& span_id, + const std::string& trace_id, + const std::string& parent_id, + const std::string& name, + const std::string& start_iso, + const JsonValue& input, + const std::string& environment) { JsonValue body; - body["id"] = new_uuid(); - body["traceId"] = id_; - body["name"] = call.tool_name; - body["startTime"] = now_iso8601(); - body["input"] = call.arguments; - body["environment"] = tracer_.config().environment; + body["id"] = span_id; + body["traceId"] = trace_id; + body["name"] = name; + body["startTime"] = start_iso; + body["input"] = input; + body["environment"] = environment; + if (!parent_id.empty()) + body["parentObservationId"] = parent_id; + return body; +} +JsonValue wrap_event(const char* type, + const std::string& timestamp, + JsonValue body) { JsonValue event; - event["id"] = new_uuid(); - event["timestamp"] = now_iso8601(); - event["type"] = "span-create"; - event["body"] = body; + event["id"] = Trace::new_uuid(); + event["timestamp"] = timestamp; + event["type"] = type; + event["body"] = std::move(body); + return event; +} + +} // namespace + +void Trace::record_tool_call_start(const ToolCall& call) { + if (ended_.load()) + return; + + // One clock read per event: startTime and the envelope timestamp share it. + std::string ts = to_iso8601(std::chrono::system_clock::now()); std::lock_guard lock(mu_); - if (active_generation_) { - event["body"]["parentObservationId"] = active_generation_->id; - } + std::string parent_id = + active_generation_ ? active_generation_->id : std::string(); + JsonValue body = + make_span_create(new_uuid(), id_, parent_id, call.tool_name, ts, + call.arguments, tracer_.config().environment); + size_t idx = events_.size(); - events_.push_back(std::move(event)); + events_.push_back(wrap_event(kEventSpanCreate, ts, std::move(body))); open_tool_spans_[call.id] = idx; } void Trace::record_tool_call_finish(const ToolResult& result) { + if (ended_.load()) + return; + + std::string ts = to_iso8601(std::chrono::system_clock::now()); + std::lock_guard lock(mu_); auto it = open_tool_spans_.find(result.tool_call_id); + JsonValue output = + result.is_success() ? result.result : JsonValue(result.error_message()); + if (it == open_tool_spans_.end()) { - // No matching start (shouldn't happen, but be defensive). - JsonValue body; - body["id"] = new_uuid(); - body["traceId"] = id_; - body["name"] = result.tool_name; - body["startTime"] = now_iso8601(); - body["endTime"] = now_iso8601(); - body["input"] = result.arguments; - body["output"] = - result.is_success() ? result.result : JsonValue(result.error_message()); - body["level"] = result.is_success() ? "DEFAULT" : "ERROR"; - if (active_generation_) - body["parentObservationId"] = active_generation_->id; - body["environment"] = tracer_.config().environment; - JsonValue event; - event["id"] = new_uuid(); - event["timestamp"] = now_iso8601(); - event["type"] = "span-create"; - event["body"] = body; - events_.push_back(std::move(event)); + // No matching start (defensive). Emit a closed span with start==end. + std::string parent_id = + active_generation_ ? active_generation_->id : std::string(); + JsonValue body = + make_span_create(new_uuid(), id_, parent_id, result.tool_name, ts, + result.arguments, tracer_.config().environment); + body["endTime"] = ts; + body["output"] = output; + body["level"] = result.is_success() ? kLevelDefault : kLevelError; + if (!result.is_success()) + body["statusMessage"] = result.error_message(); + events_.push_back(wrap_event(kEventSpanCreate, ts, std::move(body))); return; } - // Close the open span by emitting a span-update event referencing the same - // id. - JsonValue& open = events_[it->second]; - std::string span_id = open["body"]["id"].get(); - + // Close the open span by emitting span-update referencing the same id. + const std::string span_id = + events_[it->second]["body"]["id"].get(); JsonValue body; body["id"] = span_id; body["traceId"] = id_; - body["endTime"] = now_iso8601(); - body["output"] = - result.is_success() ? result.result : JsonValue(result.error_message()); + body["endTime"] = ts; + body["output"] = output; if (!result.is_success()) { - body["level"] = "ERROR"; + body["level"] = kLevelError; body["statusMessage"] = result.error_message(); } - - JsonValue update; - update["id"] = new_uuid(); - update["timestamp"] = now_iso8601(); - update["type"] = "span-update"; - update["body"] = body; - events_.push_back(std::move(update)); + events_.push_back(wrap_event(kEventSpanUpdate, ts, std::move(body))); open_tool_spans_.erase(it); } void Trace::finish_generation(const GenerateResult& result) { + if (ended_.load()) + return; + std::lock_guard lock(mu_); if (!active_generation_ || active_generation_->finalized) return; auto& gen = *active_generation_; gen.finalized = true; - // Aggregate usage across multi-step results when present. + // Aggregate per-step usage when the multi-step coordinator didn't roll it up. Usage total = result.usage; if (!result.steps.empty() && total.total_tokens == 0) { int p = 0, c = 0; @@ -382,31 +442,14 @@ void Trace::finish_generation(const GenerateResult& result) { total = Usage(p, c); } - auto fmt = [](std::chrono::system_clock::time_point t) { - auto tt = std::chrono::system_clock::to_time_t(t); - auto ms = std::chrono::duration_cast( - t.time_since_epoch()) - .count() % - 1000; - std::tm tm{}; -#if defined(_WIN32) - gmtime_s(&tm, &tt); -#else - gmtime_r(&tt, &tm); -#endif - char buf[40]; - std::snprintf(buf, sizeof(buf), "%04d-%02d-%02dT%02d:%02d:%02d.%03lldZ", - tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, - tm.tm_min, tm.tm_sec, static_cast(ms)); - return std::string(buf); - }; + std::string end_iso = to_iso8601(std::chrono::system_clock::now()); JsonValue body; body["id"] = gen.id; body["traceId"] = id_; body["name"] = gen.name; - body["startTime"] = fmt(gen.start_time); - body["endTime"] = fmt(std::chrono::system_clock::now()); + body["startTime"] = to_iso8601(gen.start_time); + body["endTime"] = end_iso; body["model"] = gen.model; if (!gen.model_parameters.empty()) body["modelParameters"] = gen.model_parameters; @@ -424,41 +467,22 @@ void Trace::finish_generation(const GenerateResult& result) { body["metadata"] = std::move(meta); if (!result.is_success() && result.error) { - body["level"] = "ERROR"; + body["level"] = kLevelError; body["statusMessage"] = *result.error; } - JsonValue event; - event["id"] = new_uuid(); - event["timestamp"] = now_iso8601(); - event["type"] = "generation-create"; - event["body"] = std::move(body); - events_.push_back(std::move(event)); + events_.push_back( + wrap_event(kEventGenerationCreate, end_iso, std::move(body))); } JsonValue Trace::build_trace_event() const { // Caller must hold mu_. + std::string start_iso = to_iso8601(trace_start_); + JsonValue body; body["id"] = id_; body["name"] = name_; - body["timestamp"] = [this] { - auto tt = std::chrono::system_clock::to_time_t(trace_start_); - auto ms = std::chrono::duration_cast( - trace_start_.time_since_epoch()) - .count() % - 1000; - std::tm tm{}; -#if defined(_WIN32) - gmtime_s(&tm, &tt); -#else - gmtime_r(&tt, &tm); -#endif - char buf[40]; - std::snprintf(buf, sizeof(buf), "%04d-%02d-%02dT%02d:%02d:%02d.%03lldZ", - tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, - tm.tm_min, tm.tm_sec, static_cast(ms)); - return std::string(buf); - }(); + body["timestamp"] = start_iso; body["environment"] = tracer_.config().environment; if (!tracer_.config().release.empty()) body["release"] = tracer_.config().release; @@ -475,12 +499,7 @@ JsonValue Trace::build_trace_event() const { if (!tags_.empty()) body["tags"] = tags_; - JsonValue event; - event["id"] = new_uuid(); - event["timestamp"] = now_iso8601(); - event["type"] = "trace-create"; - event["body"] = std::move(body); - return event; + return wrap_event(kEventTraceCreate, start_iso, std::move(body)); } bool Trace::end() { @@ -497,14 +516,14 @@ bool Trace::end() { } bool ok = tracer_.send_batch(batch); - return ok || tracer_.config().best_effort; + return ok || + tracer_.config().error_policy == Config::ErrorPolicy::kBestEffort; } -std::string Trace::now_iso8601() { - auto now = std::chrono::system_clock::now(); - auto tt = std::chrono::system_clock::to_time_t(now); +std::string Trace::to_iso8601(std::chrono::system_clock::time_point t) { + auto tt = std::chrono::system_clock::to_time_t(t); auto ms = std::chrono::duration_cast( - now.time_since_epoch()) + t.time_since_epoch()) .count() % 1000; std::tm tm{}; @@ -521,24 +540,11 @@ std::string Trace::now_iso8601() { } std::string Trace::new_uuid() { - // RFC 4122 v4-compatible UUID. - static thread_local std::mt19937_64 rng{std::random_device{}()}; - std::uniform_int_distribution dist; - uint64_t a = dist(rng); - uint64_t b = dist(rng); - - // Version 4 - a = (a & 0xFFFFFFFFFFFF0FFFULL) | 0x0000000000004000ULL; - // Variant 10xx - b = (b & 0x3FFFFFFFFFFFFFFFULL) | 0x8000000000000000ULL; - - char buf[37]; - std::snprintf( - buf, sizeof(buf), "%08x-%04x-%04x-%04x-%012llx", - static_cast(a >> 32), static_cast((a >> 16) & 0xFFFF), - static_cast(a & 0xFFFF), static_cast(b >> 48), - static_cast(b & 0xFFFFFFFFFFFFULL)); - return std::string(buf); + // RFC 4122 v4 UUID via stduuid. The generator is thread-local to avoid + // contention; each thread seeds its own mt19937 from std::random_device. + static thread_local std::mt19937 engine{std::random_device{}()}; + static thread_local uuids::uuid_random_generator gen{engine}; + return uuids::to_string(gen()); } // --------------------------------------------------------------------------- diff --git a/src/tools/multi_step_coordinator.cpp b/src/tools/multi_step_coordinator.cpp index 328d676..713babf 100644 --- a/src/tools/multi_step_coordinator.cpp +++ b/src/tools/multi_step_coordinator.cpp @@ -16,32 +16,34 @@ GenerateResult MultiStepCoordinator::execute_multi_step( return generate_func(initial_options); } - // Mirror the Vercel AI SDK `generateText` pattern - // (packages/ai/src/generate-text/generate-text.ts): - // - `initial_messages` is the user's original input, kept immutable. - // - `response_messages` is the running accumulator of assistant turns and - // tool result messages produced across steps. - // - Each step's input messages are `[initial_messages..., - // response_messages...]`. - // - `system` and other top-level options stay on `initial_options` and the - // provider request builder threads them through unchanged. + // initial_messages is the user's original input, kept immutable across + // steps; response_messages accumulates assistant turns and tool-result + // turns. Each step's input is [initial_messages..., response_messages...]. + // system and other top-level options stay on initial_options. Messages initial_messages = initial_options.messages; if (initial_messages.empty() && !initial_options.prompt.empty()) { initial_messages.push_back(Message::user(initial_options.prompt)); } + const size_t initial_count = initial_messages.size(); Messages response_messages; + // step_messages is grown in place each iteration (truncate to the immutable + // prefix, then append response_messages) so we don't re-copy + // initial_messages every step. + Messages step_messages = std::move(initial_messages); + GenerateOptions step_options = initial_options; + step_options.prompt.clear(); + GenerateResult final_result; for (int step = 0; step < initial_options.max_steps; ++step) { - GenerateOptions step_options = initial_options; - // Once we move to messages-based stepping, clear `prompt` so the request - // builder doesn't double-send it. - step_options.prompt = ""; - step_options.messages = initial_messages; - step_options.messages.insert(step_options.messages.end(), - response_messages.begin(), - response_messages.end()); + // Truncate to the immutable prefix and re-append the running accumulator. + // (vector::resize would require Message to be default-constructible.) + step_messages.erase(std::next(step_messages.begin(), initial_count), + step_messages.end()); + step_messages.insert(step_messages.end(), response_messages.begin(), + response_messages.end()); + step_options.messages = step_messages; ai::logger::log_debug("Executing step {} of {}, messages={}", step + 1, initial_options.max_steps, @@ -156,17 +158,6 @@ GenerateResult MultiStepCoordinator::execute_multi_step( return final_result; } -GenerateOptions MultiStepCoordinator::create_next_step_options( - const GenerateOptions& base_options, - const GenerateResult& /*previous_result*/, - const std::vector& /*tool_results*/) { - // Retained for ABI compatibility with the public header. The new - // execute_multi_step builds step inputs inline; this helper is no longer on - // the hot path. Returning base_options is harmless since callers within - // this translation unit no longer invoke it. - return base_options; -} - Messages MultiStepCoordinator::tool_results_to_messages( const std::vector& tool_calls, const std::vector& tool_results) { diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt index 85c4427..a4adc22 100644 --- a/third_party/CMakeLists.txt +++ b/third_party/CMakeLists.txt @@ -20,6 +20,9 @@ add_subdirectory(nlohmann_json-cmake) # Add concurrentqueue add_subdirectory(concurrentqueue-cmake) +# Add stduuid (header-only) +add_subdirectory(stduuid-cmake) + # Add googletest (only if BUILD_TESTS is ON) if(BUILD_TESTS) add_subdirectory(googletest-cmake) diff --git a/third_party/stduuid-cmake/CMakeLists.txt b/third_party/stduuid-cmake/CMakeLists.txt new file mode 100644 index 0000000..0f3d3fa --- /dev/null +++ b/third_party/stduuid-cmake/CMakeLists.txt @@ -0,0 +1,24 @@ +cmake_minimum_required(VERSION 3.16) +project(stduuid_wrapper) + +# stduuid is a header-only library; expose it via an INTERFACE target. +add_library(stduuid INTERFACE) +add_library(stduuid::stduuid ALIAS stduuid) + +set(STDUUID_BUILD_INCLUDE_DIR ${CMAKE_CURRENT_BINARY_DIR}/include) +file(MAKE_DIRECTORY ${STDUUID_BUILD_INCLUDE_DIR}) + +# Copy uuid.h from the vendored header-only directory. +configure_file( + ${AI_SDK_THIRD_PARTY_DIR}/stduuid-header-only/uuid.h + ${STDUUID_BUILD_INCLUDE_DIR}/uuid.h + COPYONLY +) + +target_include_directories(stduuid INTERFACE + $ + $ +) + +# stduuid requires C++17 minimum (uses std::optional, std::string_view). +target_compile_features(stduuid INTERFACE cxx_std_17) diff --git a/third_party/stduuid-header-only/LICENSE b/third_party/stduuid-header-only/LICENSE new file mode 100644 index 0000000..8864d4a --- /dev/null +++ b/third_party/stduuid-header-only/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/third_party/stduuid-header-only/uuid.h b/third_party/stduuid-header-only/uuid.h new file mode 100644 index 0000000..d48059d --- /dev/null +++ b/third_party/stduuid-header-only/uuid.h @@ -0,0 +1,967 @@ +#ifndef STDUUID_H +#define STDUUID_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __cplusplus + +# if (__cplusplus >= 202002L) || (defined(_MSVC_LANG) && _MSVC_LANG >= 202002L) +# define LIBUUID_CPP20_OR_GREATER +# endif + +#endif + + +#ifdef LIBUUID_CPP20_OR_GREATER +#include +#else +#include +#endif + +#ifdef _WIN32 + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#ifdef UUID_SYSTEM_GENERATOR +#include +#endif + +#ifdef UUID_TIME_GENERATOR +#include +#pragma comment(lib, "IPHLPAPI.lib") +#endif + +#elif defined(__linux__) || defined(__unix__) + +#ifdef UUID_SYSTEM_GENERATOR +#include +#endif + +#elif defined(__APPLE__) + +#ifdef UUID_SYSTEM_GENERATOR +#include +#endif + +#endif + +namespace uuids +{ +#ifdef __cpp_lib_span + template + using span = std::span; +#else + template + using span = gsl::span; +#endif + + namespace detail + { + template + [[nodiscard]] constexpr inline unsigned char hex2char(TChar const ch) noexcept + { + if (ch >= static_cast('0') && ch <= static_cast('9')) + return static_cast(ch - static_cast('0')); + if (ch >= static_cast('a') && ch <= static_cast('f')) + return static_cast(10 + ch - static_cast('a')); + if (ch >= static_cast('A') && ch <= static_cast('F')) + return static_cast(10 + ch - static_cast('A')); + return 0; + } + + template + [[nodiscard]] constexpr inline bool is_hex(TChar const ch) noexcept + { + return + (ch >= static_cast('0') && ch <= static_cast('9')) || + (ch >= static_cast('a') && ch <= static_cast('f')) || + (ch >= static_cast('A') && ch <= static_cast('F')); + } + + template + [[nodiscard]] constexpr std::basic_string_view to_string_view(TChar const * str) noexcept + { + if (str) return str; + return {}; + } + + template + [[nodiscard]] + constexpr std::basic_string_view< + typename StringType::value_type, + typename StringType::traits_type> + to_string_view(StringType const & str) noexcept + { + return str; + } + + class sha1 + { + public: + using digest32_t = uint32_t[5]; + using digest8_t = uint8_t[20]; + + static constexpr unsigned int block_bytes = 64; + + [[nodiscard]] inline static uint32_t left_rotate(uint32_t value, size_t const count) noexcept + { + return (value << count) ^ (value >> (32 - count)); + } + + sha1() { reset(); } + + void reset() noexcept + { + m_digest[0] = 0x67452301; + m_digest[1] = 0xEFCDAB89; + m_digest[2] = 0x98BADCFE; + m_digest[3] = 0x10325476; + m_digest[4] = 0xC3D2E1F0; + m_blockByteIndex = 0; + m_byteCount = 0; + } + + void process_byte(uint8_t octet) + { + this->m_block[this->m_blockByteIndex++] = octet; + ++this->m_byteCount; + if (m_blockByteIndex == block_bytes) + { + this->m_blockByteIndex = 0; + process_block(); + } + } + + void process_block(void const * const start, void const * const end) + { + const uint8_t* begin = static_cast(start); + const uint8_t* finish = static_cast(end); + while (begin != finish) + { + process_byte(*begin); + begin++; + } + } + + void process_bytes(void const * const data, size_t const len) + { + const uint8_t* block = static_cast(data); + process_block(block, block + len); + } + + uint32_t const * get_digest(digest32_t digest) + { + size_t const bitCount = this->m_byteCount * 8; + process_byte(0x80); + if (this->m_blockByteIndex > 56) { + while (m_blockByteIndex != 0) { + process_byte(0); + } + while (m_blockByteIndex < 56) { + process_byte(0); + } + } + else { + while (m_blockByteIndex < 56) { + process_byte(0); + } + } + process_byte(0); + process_byte(0); + process_byte(0); + process_byte(0); + process_byte(static_cast((bitCount >> 24) & 0xFF)); + process_byte(static_cast((bitCount >> 16) & 0xFF)); + process_byte(static_cast((bitCount >> 8) & 0xFF)); + process_byte(static_cast((bitCount) & 0xFF)); + + memcpy(digest, m_digest, 5 * sizeof(uint32_t)); + return digest; + } + + uint8_t const * get_digest_bytes(digest8_t digest) + { + digest32_t d32; + get_digest(d32); + size_t di = 0; + digest[di++] = static_cast(d32[0] >> 24); + digest[di++] = static_cast(d32[0] >> 16); + digest[di++] = static_cast(d32[0] >> 8); + digest[di++] = static_cast(d32[0] >> 0); + + digest[di++] = static_cast(d32[1] >> 24); + digest[di++] = static_cast(d32[1] >> 16); + digest[di++] = static_cast(d32[1] >> 8); + digest[di++] = static_cast(d32[1] >> 0); + + digest[di++] = static_cast(d32[2] >> 24); + digest[di++] = static_cast(d32[2] >> 16); + digest[di++] = static_cast(d32[2] >> 8); + digest[di++] = static_cast(d32[2] >> 0); + + digest[di++] = static_cast(d32[3] >> 24); + digest[di++] = static_cast(d32[3] >> 16); + digest[di++] = static_cast(d32[3] >> 8); + digest[di++] = static_cast(d32[3] >> 0); + + digest[di++] = static_cast(d32[4] >> 24); + digest[di++] = static_cast(d32[4] >> 16); + digest[di++] = static_cast(d32[4] >> 8); + digest[di++] = static_cast(d32[4] >> 0); + + return digest; + } + + private: + void process_block() + { + uint32_t w[80]; + for (size_t i = 0; i < 16; i++) { + w[i] = static_cast(m_block[i * 4 + 0] << 24); + w[i] |= static_cast(m_block[i * 4 + 1] << 16); + w[i] |= static_cast(m_block[i * 4 + 2] << 8); + w[i] |= static_cast(m_block[i * 4 + 3]); + } + for (size_t i = 16; i < 80; i++) { + w[i] = left_rotate((w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]), 1); + } + + uint32_t a = m_digest[0]; + uint32_t b = m_digest[1]; + uint32_t c = m_digest[2]; + uint32_t d = m_digest[3]; + uint32_t e = m_digest[4]; + + for (std::size_t i = 0; i < 80; ++i) + { + uint32_t f = 0; + uint32_t k = 0; + + if (i < 20) { + f = (b & c) | (~b & d); + k = 0x5A827999; + } + else if (i < 40) { + f = b ^ c ^ d; + k = 0x6ED9EBA1; + } + else if (i < 60) { + f = (b & c) | (b & d) | (c & d); + k = 0x8F1BBCDC; + } + else { + f = b ^ c ^ d; + k = 0xCA62C1D6; + } + uint32_t temp = left_rotate(a, 5) + f + e + k + w[i]; + e = d; + d = c; + c = left_rotate(b, 30); + b = a; + a = temp; + } + + m_digest[0] += a; + m_digest[1] += b; + m_digest[2] += c; + m_digest[3] += d; + m_digest[4] += e; + } + + private: + digest32_t m_digest; + uint8_t m_block[64]; + size_t m_blockByteIndex; + size_t m_byteCount; + }; + + template + inline constexpr CharT empty_guid[37] = "00000000-0000-0000-0000-000000000000"; + + template <> + inline constexpr wchar_t empty_guid[37] = L"00000000-0000-0000-0000-000000000000"; + + template + inline constexpr CharT guid_encoder[17] = "0123456789abcdef"; + + template <> + inline constexpr wchar_t guid_encoder[17] = L"0123456789abcdef"; + } + + // -------------------------------------------------------------------------------------------------------------------------- + // UUID format https://tools.ietf.org/html/rfc4122 + // -------------------------------------------------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------------------------------------------------- + // Field NDR Data Type Octet # Note + // -------------------------------------------------------------------------------------------------------------------------- + // time_low unsigned long 0 - 3 The low field of the timestamp. + // time_mid unsigned short 4 - 5 The middle field of the timestamp. + // time_hi_and_version unsigned short 6 - 7 The high field of the timestamp multiplexed with the version number. + // clock_seq_hi_and_reserved unsigned small 8 The high field of the clock sequence multiplexed with the variant. + // clock_seq_low unsigned small 9 The low field of the clock sequence. + // node character 10 - 15 The spatially unique node identifier. + // -------------------------------------------------------------------------------------------------------------------------- + // 0 1 2 3 + // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // | time_low | + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // | time_mid | time_hi_and_version | + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // |clk_seq_hi_res | clk_seq_low | node (0-1) | + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // | node (2-5) | + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + // -------------------------------------------------------------------------------------------------------------------------- + // enumerations + // -------------------------------------------------------------------------------------------------------------------------- + + // indicated by a bit pattern in octet 8, marked with N in xxxxxxxx-xxxx-xxxx-Nxxx-xxxxxxxxxxxx + enum class uuid_variant + { + // NCS backward compatibility (with the obsolete Apollo Network Computing System 1.5 UUID format) + // N bit pattern: 0xxx + // > the first 6 octets of the UUID are a 48-bit timestamp (the number of 4 microsecond units of time since 1 Jan 1980 UTC); + // > the next 2 octets are reserved; + // > the next octet is the "address family"; + // > the final 7 octets are a 56-bit host ID in the form specified by the address family + ncs, + + // RFC 4122/DCE 1.1 + // N bit pattern: 10xx + // > big-endian byte order + rfc, + + // Microsoft Corporation backward compatibility + // N bit pattern: 110x + // > little endian byte order + // > formely used in the Component Object Model (COM) library + microsoft, + + // reserved for possible future definition + // N bit pattern: 111x + reserved + }; + + // indicated by a bit pattern in octet 6, marked with M in xxxxxxxx-xxxx-Mxxx-xxxx-xxxxxxxxxxxx + enum class uuid_version + { + none = 0, // only possible for nil or invalid uuids + time_based = 1, // The time-based version specified in RFC 4122 + dce_security = 2, // DCE Security version, with embedded POSIX UIDs. + name_based_md5 = 3, // The name-based version specified in RFS 4122 with MD5 hashing + random_number_based = 4, // The randomly or pseudo-randomly generated version specified in RFS 4122 + name_based_sha1 = 5 // The name-based version specified in RFS 4122 with SHA1 hashing + }; + + // Forward declare uuid & to_string so that we can declare to_string as a friend later. + class uuid; + template , + class Allocator = std::allocator> + std::basic_string to_string(uuid const &id); + + // -------------------------------------------------------------------------------------------------------------------------- + // uuid class + // -------------------------------------------------------------------------------------------------------------------------- + class uuid + { + public: + using value_type = uint8_t; + + constexpr uuid() noexcept = default; + + uuid(value_type(&arr)[16]) noexcept + { + std::copy(std::cbegin(arr), std::cend(arr), std::begin(data)); + } + + constexpr uuid(std::array const & arr) noexcept : data{arr} {} + + explicit uuid(span bytes) + { + std::copy(std::cbegin(bytes), std::cend(bytes), std::begin(data)); + } + + template + explicit uuid(ForwardIterator first, ForwardIterator last) + { + if (std::distance(first, last) == 16) + std::copy(first, last, std::begin(data)); + } + + [[nodiscard]] constexpr uuid_variant variant() const noexcept + { + if ((data[8] & 0x80) == 0x00) + return uuid_variant::ncs; + else if ((data[8] & 0xC0) == 0x80) + return uuid_variant::rfc; + else if ((data[8] & 0xE0) == 0xC0) + return uuid_variant::microsoft; + else + return uuid_variant::reserved; + } + + [[nodiscard]] constexpr uuid_version version() const noexcept + { + if ((data[6] & 0xF0) == 0x10) + return uuid_version::time_based; + else if ((data[6] & 0xF0) == 0x20) + return uuid_version::dce_security; + else if ((data[6] & 0xF0) == 0x30) + return uuid_version::name_based_md5; + else if ((data[6] & 0xF0) == 0x40) + return uuid_version::random_number_based; + else if ((data[6] & 0xF0) == 0x50) + return uuid_version::name_based_sha1; + else + return uuid_version::none; + } + + [[nodiscard]] constexpr bool is_nil() const noexcept + { + for (size_t i = 0; i < data.size(); ++i) if (data[i] != 0) return false; + return true; + } + + void swap(uuid & other) noexcept + { + data.swap(other.data); + } + + [[nodiscard]] inline span as_bytes() const + { + return span(reinterpret_cast(data.data()), 16); + } + + template + [[nodiscard]] constexpr static bool is_valid_uuid(StringType const & in_str) noexcept + { + auto str = detail::to_string_view(in_str); + bool firstDigit = true; + size_t hasBraces = 0; + size_t index = 0; + + if (str.empty()) + return false; + + if (str.front() == '{') + hasBraces = 1; + if (hasBraces && str.back() != '}') + return false; + + for (size_t i = hasBraces; i < str.size() - hasBraces; ++i) + { + if (str[i] == '-') continue; + + if (index >= 16 || !detail::is_hex(str[i])) + { + return false; + } + + if (firstDigit) + { + firstDigit = false; + } + else + { + index++; + firstDigit = true; + } + } + + if (index < 16) + { + return false; + } + + return true; + } + + template + [[nodiscard]] constexpr static std::optional from_string(StringType const & in_str) noexcept + { + auto str = detail::to_string_view(in_str); + bool firstDigit = true; + size_t hasBraces = 0; + size_t index = 0; + + std::array data{ { 0 } }; + + if (str.empty()) return {}; + + if (str.front() == '{') + hasBraces = 1; + if (hasBraces && str.back() != '}') + return {}; + + for (size_t i = hasBraces; i < str.size() - hasBraces; ++i) + { + if (str[i] == '-') continue; + + if (index >= 16 || !detail::is_hex(str[i])) + { + return {}; + } + + if (firstDigit) + { + data[index] = static_cast(detail::hex2char(str[i]) << 4); + firstDigit = false; + } + else + { + data[index] = static_cast(data[index] | detail::hex2char(str[i])); + index++; + firstDigit = true; + } + } + + if (index < 16) + { + return {}; + } + + return uuid{ data }; + } + + private: + std::array data{ { 0 } }; + + friend bool operator==(uuid const & lhs, uuid const & rhs) noexcept; + friend bool operator<(uuid const & lhs, uuid const & rhs) noexcept; + + template + friend std::basic_ostream & operator<<(std::basic_ostream &s, uuid const & id); + + template + friend std::basic_string to_string(uuid const& id); + + friend std::hash; + }; + + // -------------------------------------------------------------------------------------------------------------------------- + // operators and non-member functions + // -------------------------------------------------------------------------------------------------------------------------- + + [[nodiscard]] inline bool operator== (uuid const& lhs, uuid const& rhs) noexcept + { + return lhs.data == rhs.data; + } + + [[nodiscard]] inline bool operator!= (uuid const& lhs, uuid const& rhs) noexcept + { + return !(lhs == rhs); + } + + [[nodiscard]] inline bool operator< (uuid const& lhs, uuid const& rhs) noexcept + { + return lhs.data < rhs.data; + } + + template + [[nodiscard]] inline std::basic_string to_string(uuid const & id) + { + std::basic_string uustr{detail::empty_guid}; + + for (size_t i = 0, index = 0; i < 36; ++i) + { + if (i == 8 || i == 13 || i == 18 || i == 23) + { + continue; + } + uustr[i] = detail::guid_encoder[id.data[index] >> 4 & 0x0f]; + uustr[++i] = detail::guid_encoder[id.data[index] & 0x0f]; + index++; + } + + return uustr; + } + + template + std::basic_ostream& operator<<(std::basic_ostream& s, uuid const& id) + { + s << to_string(id); + return s; + } + + inline void swap(uuids::uuid & lhs, uuids::uuid & rhs) noexcept + { + lhs.swap(rhs); + } + + // -------------------------------------------------------------------------------------------------------------------------- + // namespace IDs that could be used for generating name-based uuids + // -------------------------------------------------------------------------------------------------------------------------- + + // Name string is a fully-qualified domain name + static uuid uuid_namespace_dns{ {0x6b, 0xa7, 0xb8, 0x10, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8} }; + + // Name string is a URL + static uuid uuid_namespace_url{ {0x6b, 0xa7, 0xb8, 0x11, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8} }; + + // Name string is an ISO OID (See https://oidref.com/, https://en.wikipedia.org/wiki/Object_identifier) + static uuid uuid_namespace_oid{ {0x6b, 0xa7, 0xb8, 0x12, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8} }; + + // Name string is an X.500 DN, in DER or a text output format (See https://en.wikipedia.org/wiki/X.500, https://en.wikipedia.org/wiki/Abstract_Syntax_Notation_One) + static uuid uuid_namespace_x500{ {0x6b, 0xa7, 0xb8, 0x14, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8} }; + + // -------------------------------------------------------------------------------------------------------------------------- + // uuid generators + // -------------------------------------------------------------------------------------------------------------------------- + +#ifdef UUID_SYSTEM_GENERATOR + class uuid_system_generator + { + public: + using result_type = uuid; + + uuid operator()() + { +#ifdef _WIN32 + + GUID newId; + HRESULT hr = ::CoCreateGuid(&newId); + + if (FAILED(hr)) + { + throw std::system_error(hr, std::system_category(), "CoCreateGuid failed"); + } + + std::array bytes = + { { + static_cast((newId.Data1 >> 24) & 0xFF), + static_cast((newId.Data1 >> 16) & 0xFF), + static_cast((newId.Data1 >> 8) & 0xFF), + static_cast((newId.Data1) & 0xFF), + + (unsigned char)((newId.Data2 >> 8) & 0xFF), + (unsigned char)((newId.Data2) & 0xFF), + + (unsigned char)((newId.Data3 >> 8) & 0xFF), + (unsigned char)((newId.Data3) & 0xFF), + + newId.Data4[0], + newId.Data4[1], + newId.Data4[2], + newId.Data4[3], + newId.Data4[4], + newId.Data4[5], + newId.Data4[6], + newId.Data4[7] + } }; + + return uuid{ std::begin(bytes), std::end(bytes) }; + +#elif defined(__linux__) || defined(__unix__) + + uuid_t id; + uuid_generate(id); + + std::array bytes = + { { + id[0], + id[1], + id[2], + id[3], + id[4], + id[5], + id[6], + id[7], + id[8], + id[9], + id[10], + id[11], + id[12], + id[13], + id[14], + id[15] + } }; + + return uuid{ std::begin(bytes), std::end(bytes) }; + +#elif defined(__APPLE__) + auto newId = CFUUIDCreate(NULL); + auto bytes = CFUUIDGetUUIDBytes(newId); + CFRelease(newId); + + std::array arrbytes = + { { + bytes.byte0, + bytes.byte1, + bytes.byte2, + bytes.byte3, + bytes.byte4, + bytes.byte5, + bytes.byte6, + bytes.byte7, + bytes.byte8, + bytes.byte9, + bytes.byte10, + bytes.byte11, + bytes.byte12, + bytes.byte13, + bytes.byte14, + bytes.byte15 + } }; + return uuid{ std::begin(arrbytes), std::end(arrbytes) }; +#else + return uuid{}; +#endif + } + }; +#endif + + template + class basic_uuid_random_generator + { + public: + using engine_type = UniformRandomNumberGenerator; + + explicit basic_uuid_random_generator(engine_type& gen) : + generator(&gen, [](auto) {}) {} + explicit basic_uuid_random_generator(engine_type* gen) : + generator(gen, [](auto) {}) {} + + [[nodiscard]] uuid operator()() + { + alignas(uint32_t) uint8_t bytes[16]; + for (int i = 0; i < 16; i += 4) + *reinterpret_cast(bytes + i) = distribution(*generator); + + // variant must be 10xxxxxx + bytes[8] &= 0xBF; + bytes[8] |= 0x80; + + // version must be 0100xxxx + bytes[6] &= 0x4F; + bytes[6] |= 0x40; + + return uuid{std::begin(bytes), std::end(bytes)}; + } + + private: + std::uniform_int_distribution distribution; + std::shared_ptr generator; + }; + + using uuid_random_generator = basic_uuid_random_generator; + + class uuid_name_generator + { + public: + explicit uuid_name_generator(uuid const& namespace_uuid) noexcept + : nsuuid(namespace_uuid) + {} + + template + [[nodiscard]] uuid operator()(StringType const & name) + { + reset(); + process_characters(detail::to_string_view(name)); + return make_uuid(); + } + + private: + void reset() + { + hasher.reset(); + std::byte bytes[16]; + auto nsbytes = nsuuid.as_bytes(); + std::copy(std::cbegin(nsbytes), std::cend(nsbytes), bytes); + hasher.process_bytes(bytes, 16); + } + + template + void process_characters(std::basic_string_view const str) + { + for (uint32_t c : str) + { + hasher.process_byte(static_cast(c & 0xFF)); + if constexpr (!std::is_same_v) + { + hasher.process_byte(static_cast((c >> 8) & 0xFF)); + hasher.process_byte(static_cast((c >> 16) & 0xFF)); + hasher.process_byte(static_cast((c >> 24) & 0xFF)); + } + } + } + + [[nodiscard]] uuid make_uuid() + { + detail::sha1::digest8_t digest; + hasher.get_digest_bytes(digest); + + // variant must be 0b10xxxxxx + digest[8] &= 0xBF; + digest[8] |= 0x80; + + // version must be 0b0101xxxx + digest[6] &= 0x5F; + digest[6] |= 0x50; + + return uuid{ digest, digest + 16 }; + } + + private: + uuid nsuuid; + detail::sha1 hasher; + }; + +#ifdef UUID_TIME_GENERATOR + // !!! DO NOT USE THIS IN PRODUCTION + // this implementation is unreliable for good uuids + class uuid_time_generator + { + using mac_address = std::array; + + std::optional device_address; + + [[nodiscard]] bool get_mac_address() + { + if (device_address.has_value()) + { + return true; + } + +#ifdef _WIN32 + DWORD len = 0; + auto ret = GetAdaptersInfo(nullptr, &len); + if (ret != ERROR_BUFFER_OVERFLOW) return false; + std::vector buf(len); + auto pips = reinterpret_cast(&buf.front()); + ret = GetAdaptersInfo(pips, &len); + if (ret != ERROR_SUCCESS) return false; + mac_address addr; + std::copy(pips->Address, pips->Address + 6, std::begin(addr)); + device_address = addr; +#endif + + return device_address.has_value(); + } + + [[nodiscard]] long long get_time_intervals() + { + auto start = std::chrono::system_clock::from_time_t(time_t(-12219292800)); + auto diff = std::chrono::system_clock::now() - start; + auto ns = std::chrono::duration_cast(diff).count(); + return ns / 100; + } + + [[nodiscard]] static unsigned short get_clock_sequence() + { + static std::mt19937 clock_gen(std::random_device{}()); + static std::uniform_int_distribution clock_dis; + static std::atomic_ushort clock_sequence = clock_dis(clock_gen); + return clock_sequence++; + } + + public: + [[nodiscard]] uuid operator()() + { + if (get_mac_address()) + { + std::array data; + + auto tm = get_time_intervals(); + + auto clock_seq = get_clock_sequence(); + + auto ptm = reinterpret_cast(&tm); + + memcpy(&data[0], ptm + 4, 4); + memcpy(&data[4], ptm + 2, 2); + memcpy(&data[6], ptm, 2); + + memcpy(&data[8], &clock_seq, 2); + + // variant must be 0b10xxxxxx + data[8] &= 0xBF; + data[8] |= 0x80; + + // version must be 0b0001xxxx + data[6] &= 0x1F; + data[6] |= 0x10; + + memcpy(&data[10], &device_address.value()[0], 6); + + return uuids::uuid{std::cbegin(data), std::cend(data)}; + } + + return {}; + } + }; +#endif +} + +namespace std +{ + template <> + struct hash + { + using argument_type = uuids::uuid; + using result_type = std::size_t; + + [[nodiscard]] result_type operator()(argument_type const &uuid) const + { +#ifdef UUID_HASH_STRING_BASED + std::hash hasher; + return static_cast(hasher(uuids::to_string(uuid))); +#else + uint64_t l = + static_cast(uuid.data[0]) << 56 | + static_cast(uuid.data[1]) << 48 | + static_cast(uuid.data[2]) << 40 | + static_cast(uuid.data[3]) << 32 | + static_cast(uuid.data[4]) << 24 | + static_cast(uuid.data[5]) << 16 | + static_cast(uuid.data[6]) << 8 | + static_cast(uuid.data[7]); + uint64_t h = + static_cast(uuid.data[8]) << 56 | + static_cast(uuid.data[9]) << 48 | + static_cast(uuid.data[10]) << 40 | + static_cast(uuid.data[11]) << 32 | + static_cast(uuid.data[12]) << 24 | + static_cast(uuid.data[13]) << 16 | + static_cast(uuid.data[14]) << 8 | + static_cast(uuid.data[15]); + + if constexpr (sizeof(result_type) > 4) + { + return result_type(l ^ h); + } + else + { + uint64_t hash64 = l ^ h; + return result_type(uint32_t(hash64 >> 32) ^ uint32_t(hash64)); + } +#endif + } + }; +} + +#endif /* STDUUID_H */