From 203bbb2fd9c3873425775f580d496796023469e6 Mon Sep 17 00:00:00 2001 From: Alex Holmberg Date: Fri, 20 Feb 2026 06:10:14 +0100 Subject: [PATCH 1/3] feat: updated cli with newest dependencies an updated syncable-ag-ui-sdk setup --- CHANGELOG.md | 100 - Cargo.lock | 1781 ++++++------ Cargo.toml | 8 +- crates/ag-ui-core/Cargo.toml | 16 - crates/ag-ui-core/src/error.rs | 30 - crates/ag-ui-core/src/event.rs | 2481 ----------------- crates/ag-ui-core/src/lib.rs | 64 - crates/ag-ui-core/src/patch.rs | 622 ----- crates/ag-ui-core/src/state.rs | 645 ----- crates/ag-ui-core/src/types/content.rs | 451 --- crates/ag-ui-core/src/types/ids.rs | 156 -- crates/ag-ui-core/src/types/input.rs | 289 -- crates/ag-ui-core/src/types/message.rs | 714 ----- crates/ag-ui-core/src/types/mod.rs | 20 - crates/ag-ui-core/src/types/tool.rs | 78 - crates/ag-ui-server/Cargo.toml | 28 - crates/ag-ui-server/src/error.rs | 31 - crates/ag-ui-server/src/lib.rs | 43 - crates/ag-ui-server/src/producer.rs | 1055 ------- crates/ag-ui-server/src/transport/mod.rs | 64 - crates/ag-ui-server/src/transport/sse.rs | 291 -- crates/ag-ui-server/src/transport/ws.rs | 443 --- src/agent/mod.rs | 6 +- src/agent/session/mod.rs | 5 +- src/agent/session/ui.rs | 6 +- src/agent/tools/mod.rs | 11 +- src/agent/tools/platform/analyze_codebase.rs | 28 +- .../platform/check_provider_connection.rs | 25 +- .../platform/create_deployment_config.rs | 10 +- src/agent/tools/platform/deploy_service.rs | 479 ++-- .../tools/platform/get_deployment_status.rs | 55 +- .../platform/list_deployment_capabilities.rs | 32 +- .../platform/list_hetzner_availability.rs | 113 +- .../tools/platform/provision_registry.rs | 17 +- src/agent/tools/platform/select_project.rs | 2 +- src/agent/tools/platform/set_secrets.rs | 4 +- src/agent/ui/hooks.rs | 6 +- src/analyzer/context/health_detector.rs | 197 +- src/analyzer/context/infra_detector.rs | 24 +- src/analyzer/context/language_analyzers/go.rs | 3 +- .../context/language_analyzers/python.rs | 3 +- .../context/language_analyzers/rust.rs | 3 +- src/analyzer/docker_analyzer.rs | 8 +- src/analyzer/mod.rs | 29 +- src/lib.rs | 73 +- src/main.rs | 297 +- src/platform/api/client.rs | 94 +- src/platform/api/error.rs | 8 +- src/platform/api/types.rs | 113 +- src/server/bridge.rs | 46 +- src/server/mod.rs | 145 +- src/server/processor.rs | 198 +- src/server/routes.rs | 114 +- src/wizard/cloud_provider_data.rs | 418 ++- src/wizard/config_form.rs | 51 +- src/wizard/dockerfile_selection.rs | 12 +- src/wizard/environment_creation.rs | 11 +- src/wizard/environment_selection.rs | 2 +- src/wizard/infrastructure_selection.rs | 84 +- src/wizard/mod.rs | 55 +- src/wizard/orchestrator.rs | 77 +- src/wizard/provider_selection.rs | 22 +- src/wizard/recommendations.rs | 211 +- src/wizard/registry_provisioning.rs | 15 +- src/wizard/registry_selection.rs | 8 +- src/wizard/render.rs | 16 +- src/wizard/repository_selection.rs | 31 +- src/wizard/service_endpoints.rs | 122 +- src/wizard/target_selection.rs | 38 +- 69 files changed, 3119 insertions(+), 9618 deletions(-) delete mode 100644 crates/ag-ui-core/Cargo.toml delete mode 100644 crates/ag-ui-core/src/error.rs delete mode 100644 crates/ag-ui-core/src/event.rs delete mode 100644 crates/ag-ui-core/src/lib.rs delete mode 100644 crates/ag-ui-core/src/patch.rs delete mode 100644 crates/ag-ui-core/src/state.rs delete mode 100644 crates/ag-ui-core/src/types/content.rs delete mode 100644 crates/ag-ui-core/src/types/ids.rs delete mode 100644 crates/ag-ui-core/src/types/input.rs delete mode 100644 crates/ag-ui-core/src/types/message.rs delete mode 100644 crates/ag-ui-core/src/types/mod.rs delete mode 100644 crates/ag-ui-core/src/types/tool.rs delete mode 100644 crates/ag-ui-server/Cargo.toml delete mode 100644 crates/ag-ui-server/src/error.rs delete mode 100644 crates/ag-ui-server/src/lib.rs delete mode 100644 crates/ag-ui-server/src/producer.rs delete mode 100644 crates/ag-ui-server/src/transport/mod.rs delete mode 100644 crates/ag-ui-server/src/transport/sse.rs delete mode 100644 crates/ag-ui-server/src/transport/ws.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 3065e3e6..8a9433fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,106 +2,6 @@ All notable changes to this project will be documented in this file. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [Unreleased] - -## [0.16.0](https://github.com/syncable-dev/syncable-cli/compare/v0.15.0...v0.16.0) - 2025-09-10 - -### Added - -- open-telemtry added and improved techonology scannings - -### Other - -- removed telemtry for start/complete phases - -## [0.15.0](https://github.com/syncable-dev/syncable-cli/compare/v0.14.0...v0.15.0) - 2025-09-10 - -### Added - -- fixed errors -- removed warnings - -### Other - -- fixed vulnerabilities report - -## [0.14.0](https://github.com/syncable-dev/syncable-cli/compare/v0.13.6...v0.14.0) - 2025-09-09 - -### Added - -- added further refactor -- improved vulnerablity scanner for more that just npm audit but also bun, yarn & pnpm - -### Other - -- Merge branch 'main' of github.com:syncable-dev/syncable-cli into develop -- Merge branch 'develop' of github.com:syncable-dev/syncable-cli into develop - -### Added -- 🧄 **Bun Runtime Integration**: Complete support for Bun JavaScript runtime and package manager - - Automatic Bun project detection via `bun.lockb`, `bunfig.toml`, and package.json configuration - - Multi-runtime vulnerability scanning with priority-based package manager detection (Bun > pnpm > yarn > npm) - - Cross-platform Bun installation support (Windows PowerShell, Unix curl/bash) - - Runtime detection with confidence levels and fallback mechanisms - - Comprehensive unit and integration tests (34+ tests covering all scenarios) - - Enhanced ToolDetector with caching and alternative command support - - Updated documentation with Bun examples and migration guides - -## [0.13.6](https://github.com/syncable-dev/syncable-cli/compare/v0.13.5...v0.13.6) - 2025-09-03 - -### Other - -- update Cargo.lock dependencies - -## [0.13.5](https://github.com/syncable-dev/syncable-cli/compare/v0.13.4...v0.13.5) - 2025-08-13 - -### Other - -- update Cargo.lock dependencies - -## [0.13.4](https://github.com/syncable-dev/syncable-cli/compare/v0.13.3...v0.13.4) - 2025-08-08 - -### Other - -- update Cargo.lock dependencies - -## [0.13.3](https://github.com/syncable-dev/syncable-cli/compare/v0.13.2...v0.13.3) - 2025-08-05 - -### Other - -- update Cargo.lock dependencies - -## [0.13.2](https://github.com/syncable-dev/syncable-cli/compare/v0.13.1...v0.13.2) - 2025-08-04 - -### Other - -- update Cargo.lock dependencies - -## [0.13.1](https://github.com/syncable-dev/syncable-cli/compare/v0.13.0...v0.13.1) - 2025-08-01 - -### Added - -- updated color mode discovery - -### Other - -- Merge branch 'main' into develop -- *(deps)* bump toml from 0.8.23 to 0.9.3 -- *(deps)* bump tokio from 1.46.1 to 1.47.0 -- Merge pull request #114 from syncable-dev/dependabot/cargo/develop/tokio-1.46.1 - -## [0.13.0](https://github.com/syncable-dev/syncable-cli/compare/v0.12.1...v0.13.0) - 2025-07-30 - -### Added - -- updated color mode discovery -# Changelog - -All notable changes to this project will be documented in this file. - The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] diff --git a/Cargo.lock b/Cargo.lock index c85c7a73..963d5659 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,36 +4,9 @@ version = 4 [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" - -[[package]] -name = "ag-ui-core" -version = "0.1.0" -dependencies = [ - "json-patch 3.0.1", - "jsonptr 0.6.3", - "serde", - "serde_json", - "thiserror 2.0.17", - "uuid", -] - -[[package]] -name = "ag-ui-server" -version = "0.1.0" -dependencies = [ - "ag-ui-core", - "async-trait", - "axum", - "futures", - "serde", - "serde_json", - "thiserror 2.0.17", - "tokio", - "tokio-stream", -] +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "ahash" @@ -42,7 +15,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", - "getrandom 0.3.3", + "getrandom 0.3.4", "once_cell", "version_check", "zerocopy", @@ -50,9 +23,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -74,9 +47,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.19" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -89,9 +62,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.11" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" @@ -104,35 +77,38 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.9" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arc-swap" -version = "1.7.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +checksum = "f9f3647c145568cec02c42054e07bdf9a5a698e15b466fb2341bfc393cd24aa5" +dependencies = [ + "rustversion", +] [[package]] name = "arraydeque" @@ -160,13 +136,12 @@ checksum = "b0f477b951e452a0b6b4a10b53ccd569042d1d01729b519e02074a9c0958a063" [[package]] name = "assert_cmd" -version = "2.0.17" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bd389a4b2970a01282ee455294913c0a43724daedcd1a24c3eb0ec1c1320b66" +checksum = "9c5bcfa8749ac45dd12cb11055aeeb6b27a3895560d60d71e3c23bf979e60514" dependencies = [ "anstyle", "bstr", - "doc-comment", "libc", "predicates", "predicates-core", @@ -188,13 +163,12 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.23" +version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b37fc50485c4f3f736a4fb14199f6d5f5ba008d7f28fe710306c92780f004c07" +checksum = "7d67d43201f4d20c78bcda740c142ca52482d81da80681533d33bf3f0596c8e2" dependencies = [ - "flate2", - "futures-core", - "memchr", + "compression-codecs", + "compression-core", "pin-project-lite", "tokio", ] @@ -240,15 +214,15 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-config" -version = "1.8.5" +version = "1.8.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c478f5b10ce55c9a33f87ca3404ca92768b144fc1bfdede7c0121214a8283a25" +checksum = "c456581cb3c77fafcc8c67204a70680d40b61112d6da78c77bd31d945b65f1b5" dependencies = [ "aws-credential-types", "aws-runtime", @@ -265,7 +239,7 @@ dependencies = [ "bytes", "fastrand", "hex", - "http 1.3.1", + "http 1.4.0", "ring", "time", "tokio", @@ -288,9 +262,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.15.2" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88aab2464f1f25453baa7a07c84c5b7684e274054ba06817f382357f77a288" +checksum = "d9a7b350e3bb1767102698302bc37256cbd48422809984b98d292c40e2579aa9" dependencies = [ "aws-lc-sys", "zeroize", @@ -298,9 +272,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.35.0" +version = "0.37.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45afffdee1e7c9126814751f88dddc747f41d91da16c9551a0f1e8a11e788a1" +checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549" dependencies = [ "cc", "cmake", @@ -310,9 +284,9 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.5.10" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c034a1bc1d70e16e7f4e4caf7e9f7693e4c9c24cd91cf17c2a0b21abaebc7c8b" +checksum = "c635c2dc792cb4a11ce1a4f392a925340d1bdf499289b5ec1ec6810954eb43f5" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -325,8 +299,8 @@ dependencies = [ "aws-types", "bytes", "fastrand", - "http 0.2.12", - "http-body 0.4.6", + "http 1.4.0", + "http-body 1.0.1", "percent-encoding", "pin-project-lite", "tracing", @@ -335,9 +309,9 @@ dependencies = [ [[package]] name = "aws-sdk-bedrockruntime" -version = "1.104.0" +version = "1.124.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1574d1fad8f4bbf71aeb5dbb16653e7db48463f031ae77fdc161621019364d4a" +checksum = "135b4983ee5cef449a1442a91779959ba3f8a7b9ab95b4cc8b081f026567eab3" dependencies = [ "aws-credential-types", "aws-runtime", @@ -346,6 +320,7 @@ dependencies = [ "aws-smithy-eventstream", "aws-smithy-http", "aws-smithy-json", + "aws-smithy-observability", "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", @@ -353,22 +328,24 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", - "hyper 0.14.32", + "http 1.4.0", + "http-body-util", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-sso" -version = "1.82.0" +version = "1.93.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b069e4973dc25875bbd54e4c6658bdb4086a846ee9ed50f328d4d4c33ebf9857" +checksum = "9dcb38bb33fc0a11f1ffc3e3e85669e0a11a37690b86f77e75306d8f369146a0" dependencies = [ "aws-credential-types", "aws-runtime", "aws-smithy-async", "aws-smithy-http", "aws-smithy-json", + "aws-smithy-observability", "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", @@ -376,21 +353,23 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", + "http 1.4.0", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-ssooidc" -version = "1.83.0" +version = "1.95.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b49e8fe57ff100a2f717abfa65bdd94e39702fa5ab3f60cddc6ac7784010c68" +checksum = "2ada8ffbea7bd1be1f53df1dadb0f8fdb04badb13185b3321b929d1ee3caad09" dependencies = [ "aws-credential-types", "aws-runtime", "aws-smithy-async", "aws-smithy-http", "aws-smithy-json", + "aws-smithy-observability", "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", @@ -398,21 +377,23 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", + "http 1.4.0", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-sts" -version = "1.84.0" +version = "1.97.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91abcdbfb48c38a0419eb75e0eac772a4783a96750392680e4f3c25a8a0535b9" +checksum = "e6443ccadc777095d5ed13e21f5c364878c9f5bad4e35187a6cdbd863b0afcad" dependencies = [ "aws-credential-types", "aws-runtime", "aws-smithy-async", "aws-smithy-http", "aws-smithy-json", + "aws-smithy-observability", "aws-smithy-query", "aws-smithy-runtime", "aws-smithy-runtime-api", @@ -421,15 +402,16 @@ dependencies = [ "aws-types", "fastrand", "http 0.2.12", + "http 1.4.0", "regex-lite", "tracing", ] [[package]] name = "aws-sigv4" -version = "1.3.7" +version = "1.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e523e1c4e8e7e8ff219d732988e22bfeae8a1cafdbe6d9eca1546fa080be7c" +checksum = "efa49f3c607b92daae0c078d48a4571f599f966dce3caee5f1ea55c4d9073f99" dependencies = [ "aws-credential-types", "aws-smithy-eventstream", @@ -441,7 +423,7 @@ dependencies = [ "hex", "hmac", "http 0.2.12", - "http 1.3.1", + "http 1.4.0", "percent-encoding", "sha2", "time", @@ -450,9 +432,9 @@ dependencies = [ [[package]] name = "aws-smithy-async" -version = "1.2.7" +version = "1.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ee19095c7c4dda59f1697d028ce704c24b2d33c6718790c7f1d5a3015b4107c" +checksum = "52eec3db979d18cb807fc1070961cc51d87d069abe9ab57917769687368a8c6c" dependencies = [ "futures-util", "pin-project-lite", @@ -461,9 +443,9 @@ dependencies = [ [[package]] name = "aws-smithy-eventstream" -version = "0.60.14" +version = "0.60.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc12f8b310e38cad85cf3bef45ad236f470717393c613266ce0a89512286b650" +checksum = "35b9c7354a3b13c66f60fe4616d6d1969c9fd36b1b5333a5dfb3ee716b33c588" dependencies = [ "aws-smithy-types", "bytes", @@ -472,9 +454,9 @@ dependencies = [ [[package]] name = "aws-smithy-http" -version = "0.62.6" +version = "0.63.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "826141069295752372f8203c17f28e30c464d22899a43a0c9fd9c458d469c88b" +checksum = "630e67f2a31094ffa51b210ae030855cb8f3b7ee1329bdd8d085aaf61e8b97fc" dependencies = [ "aws-smithy-eventstream", "aws-smithy-runtime-api", @@ -483,9 +465,9 @@ dependencies = [ "bytes-utils", "futures-core", "futures-util", - "http 0.2.12", - "http 1.3.1", - "http-body 0.4.6", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", "percent-encoding", "pin-project-lite", "pin-utils", @@ -494,56 +476,57 @@ dependencies = [ [[package]] name = "aws-smithy-http-client" -version = "1.0.6" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f108f1ca850f3feef3009bdcc977be201bca9a91058864d9de0684e64514bee0" +checksum = "12fb0abf49ff0cab20fd31ac1215ed7ce0ea92286ba09e2854b42ba5cabe7525" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", "aws-smithy-types", "h2 0.3.27", - "h2 0.4.10", + "h2 0.4.13", "http 0.2.12", - "http 1.3.1", + "http 1.4.0", "http-body 0.4.6", "hyper 0.14.32", "hyper 1.8.1", "hyper-rustls 0.24.2", - "hyper-rustls 0.27.6", + "hyper-rustls 0.27.7", "hyper-util", "pin-project-lite", "rustls 0.21.12", - "rustls 0.23.27", - "rustls-native-certs 0.8.1", + "rustls 0.23.36", + "rustls-native-certs", "rustls-pki-types", "tokio", + "tokio-rustls 0.26.4", "tower", "tracing", ] [[package]] name = "aws-smithy-json" -version = "0.61.9" +version = "0.62.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49fa1213db31ac95288d981476f78d05d9cbb0353d22cdf3472cc05bb02f6551" +checksum = "3cb96aa208d62ee94104645f7b2ecaf77bf27edf161590b6224bfbac2832f979" dependencies = [ "aws-smithy-types", ] [[package]] name = "aws-smithy-observability" -version = "0.1.5" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f616c3f2260612fe44cede278bafa18e73e6479c4e393e2c4518cf2a9a228a" +checksum = "c0a46543fbc94621080b3cf553eb4cbbdc41dd9780a30c4756400f0139440a1d" dependencies = [ "aws-smithy-runtime-api", ] [[package]] name = "aws-smithy-query" -version = "0.60.9" +version = "0.60.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae5d689cf437eae90460e944a58b5668530d433b4ff85789e69d2f2a556e057d" +checksum = "0cebbddb6f3a5bd81553643e9c7daf3cc3dc5b0b5f398ac668630e8a84e6fff0" dependencies = [ "aws-smithy-types", "urlencoding", @@ -551,9 +534,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.8.6" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e107ce0783019dbff59b3a244aa0c114e4a8c9d93498af9162608cd5474e796" +checksum = "f3df87c14f0127a0d77eb261c3bc45d5b4833e2a1f63583ebfb728e4852134ee" dependencies = [ "aws-smithy-async", "aws-smithy-http", @@ -564,9 +547,10 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", - "http 1.3.1", + "http 1.4.0", "http-body 0.4.6", "http-body 1.0.1", + "http-body-util", "pin-project-lite", "pin-utils", "tokio", @@ -575,15 +559,15 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.9.3" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab0d43d899f9e508300e587bf582ba54c27a452dd0a9ea294690669138ae14a2" +checksum = "49952c52f7eebb72ce2a754d3866cc0f87b97d2a46146b79f80f3a93fb2b3716" dependencies = [ "aws-smithy-async", "aws-smithy-types", "bytes", "http 0.2.12", - "http 1.3.1", + "http 1.4.0", "pin-project-lite", "tokio", "tracing", @@ -592,16 +576,16 @@ dependencies = [ [[package]] name = "aws-smithy-types" -version = "1.3.5" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "905cb13a9895626d49cf2ced759b062d913834c7482c38e49557eac4e6193f01" +checksum = "3b3a26048eeab0ddeba4b4f9d51654c79af8c3b32357dc5f336cee85ab331c33" dependencies = [ "base64-simd", "bytes", "bytes-utils", "futures-core", "http 0.2.12", - "http 1.3.1", + "http 1.4.0", "http-body 0.4.6", "http-body 1.0.1", "http-body-util", @@ -646,11 +630,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" dependencies = [ "axum-core", - "base64 0.22.1", + "base64", "bytes", "form_urlencoded", "futures-util", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "http-body-util", "hyper 1.8.1", @@ -683,7 +667,7 @@ checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "http-body-util", "mime", @@ -705,12 +689,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.1" @@ -753,21 +731,22 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "blake3" -version = "1.8.2" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" +checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" dependencies = [ "arrayref", "arrayvec", "cc", "cfg-if", "constant_time_eq", + "cpufeatures", ] [[package]] @@ -781,18 +760,18 @@ dependencies = [ [[package]] name = "borsh" -version = "1.5.7" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" +checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" dependencies = [ "cfg_aliases", ] [[package]] name = "bstr" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" dependencies = [ "memchr", "regex-automata", @@ -801,9 +780,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.18.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "byteorder" @@ -813,9 +792,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "bytes-utils" @@ -829,15 +808,15 @@ dependencies = [ [[package]] name = "camino" -version = "1.1.10" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0da45bc31171d8d6960122e222a67740df867c1dd53b4d51caa297084c185cab" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" [[package]] name = "cargo-lock" -version = "11.0.0" +version = "11.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf53e0ebbbc6e45357b199f3b213f3eb330792c8b370e548499f5685470ecb11" +checksum = "63585cdf8572aa7adf0e30a253f988f2b77233bfac1973d52efb6dd53a75920e" dependencies = [ "semver", "serde", @@ -847,9 +826,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.50" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f50d563227a1c37cc0a263f64eca3334388c01c5e4c4861a9def205c614383c" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", "jobserver", @@ -865,9 +844,9 @@ checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -877,16 +856,16 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ "iana-time-zone", "js-sys", "num-traits", "serde", "wasm-bindgen", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -913,9 +892,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.44" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c1f056bae57e3e54c3375c41ff79619ddd13460a17d7438712bd0d83fda4ff8" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" dependencies = [ "clap_builder", "clap_derive", @@ -923,9 +902,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.44" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" dependencies = [ "anstream", "anstyle", @@ -935,9 +914,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.41" +version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -947,9 +926,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.4" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "clipboard-win" @@ -983,11 +962,11 @@ checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "colored" -version = "3.0.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1000,6 +979,23 @@ dependencies = [ "memchr", ] +[[package]] +name = "compression-codecs" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7" +dependencies = [ + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -1011,22 +1007,22 @@ dependencies = [ [[package]] name = "console" -version = "0.16.0" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e09ced7ebbccb63b4c65413d821f2e00ce54c5ca4514ddc6b3c892fdbcbc69d" +checksum = "03e45a4a8926227e4197636ba97a9fc9b00477e9f4bd711395687c5f0734bec4" dependencies = [ "encode_unicode", "libc", "once_cell", - "unicode-width 0.2.0", - "windows-sys 0.60.2", + "unicode-width 0.2.2", + "windows-sys 0.61.2", ] [[package]] name = "constant_time_eq" -version = "0.3.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" [[package]] name = "convert_case" @@ -1101,9 +1097,9 @@ dependencies = [ [[package]] name = "crokey" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51360853ebbeb3df20c76c82aecf43d387a62860f1a59ba65ab51f00eea85aad" +checksum = "04a63daf06a168535c74ab97cdba3ed4fa5d4f32cb36e437dcceb83d66854b7c" dependencies = [ "crokey-proc_macros", "crossterm", @@ -1114,9 +1110,9 @@ dependencies = [ [[package]] name = "crokey-proc_macros" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bf1a727caeb5ee5e0a0826a97f205a9cf84ee964b0b48239fef5214a00ae439" +checksum = "847f11a14855fc490bd5d059821895c53e77eeb3c2b73ee3dded7ce77c93b231" dependencies = [ "crossterm", "proc-macro2", @@ -1210,9 +1206,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -1220,21 +1216,21 @@ dependencies = [ [[package]] name = "csv" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" dependencies = [ "csv-core", "itoa", "ryu", - "serde", + "serde_core", ] [[package]] name = "csv-core" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" dependencies = [ "memchr", ] @@ -1346,12 +1342,12 @@ dependencies = [ [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "cc3dc5ad92c2e2d1c193bbbbdf2ea477cb81331de4f3103f267ca18368b988c4" dependencies = [ "powerfmt", - "serde", + "serde_core", ] [[package]] @@ -1426,7 +1422,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users 0.5.0", + "redox_users 0.5.2", "windows-sys 0.61.2", ] @@ -1452,12 +1448,6 @@ dependencies = [ "syn", ] -[[package]] -name = "doc-comment" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" - [[package]] name = "document-features" version = "0.2.12" @@ -1540,9 +1530,9 @@ dependencies = [ [[package]] name = "env_filter" -version = "0.1.3" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" dependencies = [ "log", "regex", @@ -1550,9 +1540,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" dependencies = [ "anstream", "anstyle", @@ -1569,12 +1559,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.12" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1644,27 +1634,26 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.25" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" dependencies = [ "cfg-if", "libc", "libredox", - "windows-sys 0.59.0", ] [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" -version = "1.1.1" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -1714,18 +1703,18 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] [[package]] name = "fs-err" -version = "3.1.1" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d7be93788013f265201256d58f04936a8079ad5dc898743aa20525f503b683" +checksum = "73fde052dbfc920003cfd2c8e2c6e6d4cc7c1091538c3a24226cec0665ab08c0" dependencies = [ "autocfg", ] @@ -1738,9 +1727,9 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -1753,9 +1742,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -1763,15 +1752,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -1780,15 +1769,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -1797,15 +1786,15 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-timer" @@ -1815,9 +1804,9 @@ checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -1827,7 +1816,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -1852,31 +1840,44 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "js-sys", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasip2", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + [[package]] name = "gix" version = "0.74.1" @@ -1926,7 +1927,7 @@ dependencies = [ "gix-worktree", "gix-worktree-state", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -1939,7 +1940,7 @@ dependencies = [ "gix-date", "gix-utils", "itoa", - "thiserror 2.0.17", + "thiserror 2.0.18", "winnow 0.7.14", ] @@ -1956,17 +1957,17 @@ dependencies = [ "gix-trace", "kstring", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", "unicode-bom", ] [[package]] name = "gix-bitmap" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e150161b8a75b5860521cb876b506879a3376d3adc857ec7a9d35e7c6a5e531" +checksum = "d982fc7ef0608e669851d0d2a6141dae74c60d5a27e8daa451f2a4857bbf41e2" dependencies = [ - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -1975,14 +1976,14 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c356b3825677cb6ff579551bb8311a81821e184453cbd105e2fc5311b288eeb" dependencies = [ - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] name = "gix-command" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395c94093f4a79645a2b9119b65e5605044ba68a27f9da36e4e618da9ffe2190" +checksum = "46f9c425730a654835351e6da8c3c69ba1804f8b8d4e96d027254151138d5c64" dependencies = [ "bstr", "gix-path", @@ -2001,7 +2002,7 @@ dependencies = [ "gix-chunk", "gix-hash", "memmap2", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -2019,7 +2020,7 @@ dependencies = [ "gix-sec", "memchr", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", "unicode-bom", "winnow 0.7.14", ] @@ -2034,7 +2035,7 @@ dependencies = [ "bstr", "gix-path", "libc", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -2052,7 +2053,7 @@ dependencies = [ "gix-sec", "gix-trace", "gix-url", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -2065,7 +2066,7 @@ dependencies = [ "itoa", "jiff", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -2077,7 +2078,7 @@ dependencies = [ "bstr", "gix-hash", "gix-object", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -2093,7 +2094,7 @@ dependencies = [ "gix-path", "gix-ref", "gix-sec", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -2113,7 +2114,7 @@ dependencies = [ "once_cell", "parking_lot", "prodash", - "thiserror 2.0.17", + "thiserror 2.0.18", "walkdir", ] @@ -2135,7 +2136,7 @@ dependencies = [ "gix-trace", "gix-utils", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -2149,7 +2150,7 @@ dependencies = [ "gix-features", "gix-path", "gix-utils", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -2173,7 +2174,7 @@ dependencies = [ "faster-hex", "gix-features", "sha1-checked", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -2225,7 +2226,7 @@ dependencies = [ "memmap2", "rustix", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -2236,7 +2237,7 @@ checksum = "729d7857429a66023bc0c29d60fa21d0d6ae8862f33c1937ba89e0f74dd5c67f" dependencies = [ "gix-tempfile", "gix-utils", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -2252,7 +2253,7 @@ dependencies = [ "gix-object", "gix-revwalk", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -2272,7 +2273,7 @@ dependencies = [ "gix-validate", "itoa", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", "winnow 0.7.14", ] @@ -2294,7 +2295,7 @@ dependencies = [ "gix-quote", "parking_lot", "tempfile", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -2314,7 +2315,7 @@ dependencies = [ "memmap2", "parking_lot", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", "uluru", ] @@ -2327,7 +2328,7 @@ dependencies = [ "bstr", "faster-hex", "gix-trace", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -2339,7 +2340,7 @@ dependencies = [ "bstr", "faster-hex", "gix-trace", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -2351,7 +2352,7 @@ dependencies = [ "bstr", "gix-trace", "gix-validate", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -2366,7 +2367,7 @@ dependencies = [ "gix-config-value", "gix-glob", "gix-path", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -2379,7 +2380,7 @@ dependencies = [ "gix-config-value", "parking_lot", "rustix", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -2404,19 +2405,19 @@ dependencies = [ "gix-transport", "gix-utils", "maybe-async", - "thiserror 2.0.17", + "thiserror 2.0.18", "winnow 0.7.14", ] [[package]] name = "gix-quote" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e912ec04b7b1566a85ad486db0cab6b9955e3e32bcd3c3a734542ab3af084c5b" +checksum = "96fc2ff2ec8cc0c92807f02eab1f00eb02619fc2810d13dc42679492fcc36757" dependencies = [ "bstr", "gix-utils", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -2436,7 +2437,7 @@ dependencies = [ "gix-utils", "gix-validate", "memmap2", - "thiserror 2.0.17", + "thiserror 2.0.18", "winnow 0.7.14", ] @@ -2451,7 +2452,7 @@ dependencies = [ "gix-revision", "gix-validate", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -2469,7 +2470,7 @@ dependencies = [ "gix-object", "gix-revwalk", "gix-trace", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -2484,7 +2485,7 @@ dependencies = [ "gix-hashtable", "gix-object", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -2508,7 +2509,7 @@ dependencies = [ "bstr", "gix-hash", "gix-lock", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -2523,7 +2524,7 @@ dependencies = [ "gix-pathspec", "gix-refspec", "gix-url", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -2540,9 +2541,9 @@ dependencies = [ [[package]] name = "gix-trace" -version = "0.1.16" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd971cd6961fb1ebb29a0052a4ab04d8498dbf363c122e137b04753a3bbb5c3" +checksum = "f69a13643b8437d4ca6845e08143e847a36ca82903eed13303475d0ae8b162e0" [[package]] name = "gix-transport" @@ -2550,7 +2551,7 @@ version = "0.49.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8da4a77922accb1e26e610c7a84ef7e6b34fd07112e6a84afd68d7f3e795957" dependencies = [ - "base64 0.22.1", + "base64", "bstr", "gix-command", "gix-credentials", @@ -2560,7 +2561,7 @@ dependencies = [ "gix-sec", "gix-url", "reqwest 0.12.28", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -2577,7 +2578,7 @@ dependencies = [ "gix-object", "gix-revwalk", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -2590,7 +2591,7 @@ dependencies = [ "gix-features", "gix-path", "percent-encoding", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -2610,7 +2611,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b1e63a5b516e970a594f870ed4571a8fdcb8a344e7bd407a20db8bd61dbfde4" dependencies = [ "bstr", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -2649,7 +2650,7 @@ dependencies = [ "gix-path", "gix-worktree", "io-close", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -2660,9 +2661,9 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "globset" -version = "0.4.16" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" dependencies = [ "aho-corasick", "bstr", @@ -2715,16 +2716,16 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.10" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "http 1.3.1", + "http 1.4.0", "indexmap", "slab", "tokio", @@ -2749,9 +2750,9 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hashbrown" -version = "0.15.3" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", @@ -2780,9 +2781,9 @@ dependencies = [ [[package]] name = "hcl-edit" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab35d988dc879e293759e29b430a4ba9e6125965eec6fd0dfab0cb349e172d7" +checksum = "562c25584610e7fdaa26824eb2aa81386e17ffbf19e397c514b0a2b9855f41dc" dependencies = [ "fnv", "hcl-primitives", @@ -2806,9 +2807,9 @@ dependencies = [ [[package]] name = "hcl-rs" -version = "0.19.4" +version = "0.19.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5914e8caacb6e224944a8181bebd79bf81ad4999a36689f0a3158e555b49040d" +checksum = "704d9a4edf414209ae86f11589d44393bf5d19d2f2e37e57fe3bb0e726a332c2" dependencies = [ "hcl-edit", "hcl-primitives", @@ -2842,9 +2843,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hex" @@ -2863,11 +2864,11 @@ dependencies = [ [[package]] name = "home" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2878,7 +2879,7 @@ checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd" dependencies = [ "cfg-if", "libc", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -2894,12 +2895,11 @@ dependencies = [ [[package]] name = "http" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] @@ -2921,7 +2921,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.3.1", + "http 1.4.0", ] [[package]] @@ -2932,7 +2932,7 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "pin-project-lite", ] @@ -2992,8 +2992,8 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "h2 0.4.10", - "http 1.3.1", + "h2 0.4.13", + "http 1.4.0", "http-body 1.0.1", "httparse", "httpdate", @@ -3016,26 +3016,25 @@ dependencies = [ "hyper 0.14.32", "log", "rustls 0.21.12", - "rustls-native-certs 0.6.3", "tokio", "tokio-rustls 0.24.1", ] [[package]] name = "hyper-rustls" -version = "0.27.6" +version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a01595e11bdcec50946522c32dde3fc6914743000a68b93000965f2f02406d" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "http 1.3.1", + "http 1.4.0", "hyper 1.8.1", "hyper-util", "log", - "rustls 0.23.27", - "rustls-native-certs 0.8.1", + "rustls 0.23.36", + "rustls-native-certs", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.2", + "tokio-rustls 0.26.4", "tower-service", ] @@ -3070,23 +3069,22 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "futures-channel", - "futures-core", "futures-util", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "hyper 1.8.1", "ipnet", "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.0", + "socket2 0.6.2", "system-configuration", "tokio", "tower-service", @@ -3096,9 +3094,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -3120,9 +3118,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", @@ -3133,9 +3131,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -3146,11 +3144,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -3161,42 +3158,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -3204,6 +3197,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -3212,9 +3211,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -3239,9 +3238,9 @@ checksum = "cd62e6b5e86ea8eeeb8db1de02880a6abc01a397b2ebb64b5d74ac255318f5cb" [[package]] name = "ignore" -version = "0.4.23" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" dependencies = [ "crossbeam-deque", "globset", @@ -3255,24 +3254,25 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.9.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown 0.15.3", + "hashbrown 0.16.1", "serde", + "serde_core", ] [[package]] name = "indicatif" -version = "0.18.3" +version = "0.18.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9375e112e4b463ec1b1c6c011953545c65a30164fbab5b581df32b3abf0dcb88" +checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" dependencies = [ "console", "portable-atomic", - "unicode-width 0.2.0", + "unicode-width 0.2.2", "unit-prefix", "web-time", ] @@ -3288,16 +3288,16 @@ dependencies = [ [[package]] name = "inquire" -version = "0.9.1" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2628910d0114e9139056161d8644a2026be7b117f8498943f9437748b04c9e0a" +checksum = "979f5ab9760427ada4fa5762b2d905e5b12704fb1fada07b6bfa66aeaa586f87" dependencies = [ "bitflags", "crossterm", "dyn-clone", "fuzzy-matcher", "unicode-segmentation", - "unicode-width 0.2.0", + "unicode-width 0.2.2", ] [[package]] @@ -3318,9 +3318,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "iri-string" -version = "0.7.8" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" dependencies = [ "memchr", "serde", @@ -3337,13 +3337,13 @@ dependencies = [ [[package]] name = "is-terminal" -version = "0.4.16" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3358,21 +3358,21 @@ dependencies = [ [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jiff" -version = "0.2.17" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a87d9b8105c23642f50cbbae03d1f75d8422c5cb98ce7ee9271f7ff7505be6b8" +checksum = "c867c356cc096b33f4981825ab281ecba3db0acefe60329f044c1789d94c6543" dependencies = [ "jiff-static", "jiff-tzdb-platform", @@ -3385,9 +3385,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.17" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b787bebb543f8969132630c51fd0afab173a86c6abae56ff3b9e5e3e3f9f6e58" +checksum = "f7946b4325269738f270bb55b3c19ab5c5040525f83fd625259422a9d25d9be5" dependencies = [ "proc-macro2", "quote", @@ -3396,9 +3396,9 @@ dependencies = [ [[package]] name = "jiff-tzdb" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524" +checksum = "68971ebff725b9e2ca27a601c5eb38a4c5d64422c4cbab0c535f248087eda5c2" [[package]] name = "jiff-tzdb-platform" @@ -3437,15 +3437,15 @@ version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "libc", ] [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ "once_cell", "wasm-bindgen", @@ -3485,7 +3485,7 @@ dependencies = [ "pest_derive", "regex", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -3514,7 +3514,7 @@ version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06d9e5e61dd037cdc51da0d7e2b2be10f497478ea7e120d85dad632adb99882b" dependencies = [ - "base64 0.22.1", + "base64", "chrono", "serde", "serde_json", @@ -3549,29 +3549,29 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4987d57a184d2b5294fdad3d7fc7f278899469d21a4da39a8f6ca16426567a36" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "chrono", "either", "futures", "home", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "http-body-util", "hyper 1.8.1", - "hyper-rustls 0.27.6", + "hyper-rustls 0.27.7", "hyper-timeout", "hyper-util", "jsonpath-rust", "k8s-openapi", "kube-core", "pem", - "rustls 0.23.27", + "rustls 0.23.36", "secrecy", "serde", "serde_json", "serde_yaml", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-util", "tower", @@ -3588,14 +3588,14 @@ dependencies = [ "chrono", "derive_more", "form_urlencoded", - "http 1.3.1", + "http 1.4.0", "json-patch 4.1.0", "k8s-openapi", "schemars", "serde", "serde-value", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -3624,7 +3624,7 @@ dependencies = [ "backon", "educe", "futures", - "hashbrown 0.15.3", + "hashbrown 0.15.5", "hostname", "json-patch 4.1.0", "k8s-openapi", @@ -3633,7 +3633,7 @@ dependencies = [ "pin-project", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-util", "tracing", @@ -3641,9 +3641,9 @@ dependencies = [ [[package]] name = "lazy-regex" -version = "3.4.2" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "191898e17ddee19e60bccb3945aa02339e81edd4a8c50e21fd4d48cdecda7b29" +checksum = "6bae91019476d3ec7147de9aa291cadb6d870abf2f3015d2da73a90325ac1496" dependencies = [ "lazy-regex-proc_macros", "once_cell", @@ -3652,9 +3652,9 @@ dependencies = [ [[package]] name = "lazy-regex-proc_macros" -version = "3.4.2" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c35dc8b0da83d1a9507e12122c80dea71a9c7c613014347392483a83ea593e04" +checksum = "4de9c1e1439d8b7b3061b2d209809f447ca33241733d9a3c01eabf2dc8d94358" dependencies = [ "proc-macro2", "quote", @@ -3668,27 +3668,33 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" -version = "0.2.179" +version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "libm" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.3" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ "bitflags", "libc", - "redox_syscall", + "redox_syscall 0.7.1", ] [[package]] @@ -3714,9 +3720,9 @@ checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "litrs" @@ -3735,9 +3741,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.27" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lru-slab" @@ -3764,15 +3770,15 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.4" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memmap2" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" dependencies = [ "libc", ] @@ -3810,30 +3816,31 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] name = "mio" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "log", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "wasi", + "windows-sys 0.61.2", ] [[package]] name = "native-tls" -version = "0.2.14" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" dependencies = [ "libc", "log", @@ -3841,7 +3848,7 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "security-framework 2.11.1", + "security-framework", "security-framework-sys", "tempfile", ] @@ -3891,9 +3898,9 @@ checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-integer" @@ -3956,9 +3963,9 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "once_cell_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "onig" @@ -3995,9 +4002,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.73" +version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ "bitflags", "cfg-if", @@ -4021,15 +4028,15 @@ dependencies = [ [[package]] name = "openssl-probe" -version = "0.1.6" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" -version = "0.9.109" +version = "0.9.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" dependencies = [ "cc", "libc", @@ -4091,9 +4098,9 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -4117,32 +4124,31 @@ version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" dependencies = [ - "base64 0.22.1", + "base64", "serde_core", ] [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.0" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" dependencies = [ "memchr", - "thiserror 2.0.17", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.8.0" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" dependencies = [ "pest", "pest_generator", @@ -4150,9 +4156,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.0" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" dependencies = [ "pest", "pest_meta", @@ -4163,11 +4169,10 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.8.0" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" dependencies = [ - "once_cell", "pest", "sha2", ] @@ -4250,9 +4255,9 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "platforms" -version = "3.6.0" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b02ffed1bc8c2234bb6f8e760e34613776c5102a041f25330b869a78153a68c" +checksum = "a546fc83c436ffbef8e7e639df8498bbc5122e0bd19cf8db208720c2cc85290e" dependencies = [ "serde", ] @@ -4263,7 +4268,7 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ - "base64 0.22.1", + "base64", "indexmap", "quick-xml", "serde", @@ -4272,24 +4277,24 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.11.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" dependencies = [ "portable-atomic", ] [[package]] name = "potential_utf" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] @@ -4317,9 +4322,9 @@ checksum = "17e0a4425d076f0718b820673a38fbf3747080c61017eeb0dd79bc7e472b8bb8" [[package]] name = "predicates" -version = "3.1.3" +version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" dependencies = [ "anstyle", "difflib", @@ -4331,20 +4336,30 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" [[package]] name = "predicates-tree" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" dependencies = [ "predicates-core", "termtree", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "prettytable" version = "0.10.0" @@ -4371,9 +4386,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -4389,15 +4404,15 @@ dependencies = [ [[package]] name = "proptest" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40" +checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532" dependencies = [ "bit-set", "bit-vec", "bitflags", "num-traits", - "rand 0.9.1", + "rand 0.9.2", "rand_chacha 0.9.0", "rand_xorshift", "regex-syntax", @@ -4423,9 +4438,9 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ "bytes", "cfg_aliases", @@ -4433,9 +4448,9 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.27", - "socket2 0.5.10", - "thiserror 2.0.17", + "rustls 0.23.36", + "socket2 0.6.2", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -4443,21 +4458,21 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.12" +version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ "aws-lc-rs", "bytes", - "getrandom 0.3.3", + "getrandom 0.3.4", "lru-slab", - "rand 0.9.1", + "rand 0.9.2", "ring", "rustc-hash", - "rustls 0.23.27", + "rustls 0.23.36", "rustls-pki-types", "slab", - "thiserror 2.0.17", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -4465,32 +4480,32 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.12" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee4e529991f949c5e25755532370b8af5d114acae52326361d68d47af64aa842" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "radix_trie" @@ -4515,12 +4530,12 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -4540,7 +4555,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -4549,16 +4564,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", ] [[package]] @@ -4567,7 +4582,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" dependencies = [ - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -4592,33 +4607,42 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.12" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags", ] [[package]] -name = "redox_users" -version = "0.4.6" +name = "redox_syscall" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b" dependencies = [ - "getrandom 0.2.16", + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", "libredox", "thiserror 1.0.69", ] [[package]] name = "redox_users" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "libredox", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -4643,9 +4667,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -4655,9 +4679,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -4666,15 +4690,15 @@ dependencies = [ [[package]] name = "regex-lite" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" [[package]] name = "reqwest" @@ -4682,18 +4706,18 @@ version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "encoding_rs", "futures-channel", "futures-core", "futures-util", - "h2 0.4.10", - "http 1.3.1", + "h2 0.4.13", + "http 1.4.0", "http-body 1.0.1", "http-body-util", "hyper 1.8.1", - "hyper-rustls 0.27.6", + "hyper-rustls 0.27.7", "hyper-tls", "hyper-util", "js-sys", @@ -4704,8 +4728,8 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.27", - "rustls-native-certs 0.8.1", + "rustls 0.23.36", + "rustls-native-certs", "rustls-pki-types", "serde", "serde_json", @@ -4713,7 +4737,7 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-native-tls", - "tokio-rustls 0.26.2", + "tokio-rustls 0.26.4", "tokio-util", "tower", "tower-http", @@ -4727,22 +4751,22 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04e9018c9d814e5f30cc16a0f03271aeab3571e609612d9fe78c1aa8d11c2f62" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "encoding_rs", "futures-channel", "futures-core", "futures-util", - "h2 0.4.10", - "http 1.3.1", + "h2 0.4.13", + "http 1.4.0", "http-body 1.0.1", "http-body-util", "hyper 1.8.1", - "hyper-rustls 0.27.6", + "hyper-rustls 0.27.7", "hyper-util", "js-sys", "log", @@ -4750,14 +4774,14 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.27", + "rustls 0.23.36", "rustls-pki-types", "rustls-platform-verifier", "serde", "serde_json", "sync_wrapper", "tokio", - "tokio-rustls 0.26.2", + "tokio-rustls 0.26.4", "tower", "tower-http", "tower-service", @@ -4775,14 +4799,14 @@ checksum = "5b1a48121c1ecd6f6ce59d64ec353c791aac6fc07bf4aa353380e8185659e6eb" dependencies = [ "as-any", "async-stream", - "base64 0.22.1", + "base64", "bytes", "eventsource-stream", "fastrand", "futures", "futures-timer", "glob", - "http 1.3.1", + "http 1.4.0", "mime", "mime_guess", "ordered-float 5.1.0", @@ -4792,7 +4816,7 @@ dependencies = [ "schemars", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "tracing-futures", @@ -4822,7 +4846,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -4876,58 +4900,37 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.27" +version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ "aws-lc-rs", "log", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.3", + "rustls-webpki 0.103.9", "subtle", "zeroize", ] [[package]] name = "rustls-native-certs" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" -dependencies = [ - "openssl-probe", - "rustls-pemfile", - "schannel", - "security-framework 2.11.1", -] - -[[package]] -name = "rustls-native-certs" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.5.1", -] - -[[package]] -name = "rustls-pemfile" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" -dependencies = [ - "base64 0.21.7", + "security-framework", ] [[package]] name = "rustls-pki-types" -version = "1.12.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "web-time", "zeroize", @@ -4944,11 +4947,11 @@ dependencies = [ "jni", "log", "once_cell", - "rustls 0.23.27", - "rustls-native-certs 0.8.1", + "rustls 0.23.36", + "rustls-native-certs", "rustls-platform-verifier-android", - "rustls-webpki 0.103.3", - "security-framework 3.5.1", + "rustls-webpki 0.103.9", + "security-framework", "security-framework-sys", "webpki-root-certs", "windows-sys 0.61.2", @@ -4972,9 +4975,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.3" +version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ "aws-lc-rs", "ring", @@ -4997,7 +5000,7 @@ dependencies = [ "semver", "serde", "tame-index", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "toml", "url", @@ -5005,15 +5008,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rusty-fork" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" dependencies = [ "fnv", "quick-error", @@ -5038,16 +5041,16 @@ dependencies = [ "nix", "radix_trie", "unicode-segmentation", - "unicode-width 0.2.0", + "unicode-width 0.2.2", "utf8parse", "windows-sys 0.60.2", ] [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "same-file" @@ -5060,18 +5063,18 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "schemars" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" dependencies = [ "dyn-clone", "ref-cast", @@ -5082,9 +5085,9 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301858a4023d78debd2353c7426dc486001bddc91ae31a76fb1f55132f7e2633" +checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" dependencies = [ "proc-macro2", "quote", @@ -5119,22 +5122,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.11.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags", - "core-foundation 0.9.4", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework" -version = "3.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags", "core-foundation 0.10.1", @@ -5145,9 +5135,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -5155,11 +5145,12 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" dependencies = [ "serde", + "serde_core", ] [[package]] @@ -5215,15 +5206,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.146" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "217ca874ae0207aac254aa02c957ded05585a90892cc8d87f9e5fa49669dadd8" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -5239,9 +5230,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.1" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2789234a13a53fc4be1b51ea1bab45a3c338bdb884862a257d10e5a74ae009e6" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" dependencies = [ "serde_core", ] @@ -5305,9 +5296,9 @@ dependencies = [ [[package]] name = "shell-words" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" [[package]] name = "shlex" @@ -5338,13 +5329,20 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.7" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + [[package]] name = "simdutf8" version = "0.1.5" @@ -5359,18 +5357,15 @@ checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" [[package]] name = "siphasher" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" [[package]] name = "slab" -version = "0.4.9" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "slug" @@ -5416,19 +5411,19 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.0" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "static_assertions" @@ -5471,9 +5466,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.111" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -5489,12 +5484,41 @@ dependencies = [ "futures-core", ] +[[package]] +name = "syncable-ag-ui-core" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f375f960383f428782c4422592c24657d5ca42be9eb2bd806e4b2dd503bd15" +dependencies = [ + "json-patch 3.0.1", + "jsonptr 0.6.3", + "serde", + "serde_json", + "thiserror 2.0.18", + "uuid", +] + +[[package]] +name = "syncable-ag-ui-server" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dabc1fcab30fd99994c6aa7afd8e2c8bad513063683f24e2f4a4b80c88175553" +dependencies = [ + "async-trait", + "axum", + "futures", + "serde", + "serde_json", + "syncable-ag-ui-core", + "thiserror 2.0.18", + "tokio", + "tokio-stream", +] + [[package]] name = "syncable-cli" version = "0.26.1" dependencies = [ - "ag-ui-core", - "ag-ui-server", "ahash", "aho-corasick", "anyhow", @@ -5504,7 +5528,7 @@ dependencies = [ "aws-sdk-bedrockruntime", "aws-smithy-types", "axum", - "base64 0.22.1", + "base64", "blake3", "bstr", "chrono", @@ -5518,7 +5542,7 @@ dependencies = [ "futures-util", "glob", "hcl-rs", - "http 1.3.1", + "http 1.4.0", "indicatif", "inquire", "k8s-openapi", @@ -5533,13 +5557,13 @@ dependencies = [ "predicates", "prettytable", "proptest", - "rand 0.9.1", + "rand 0.9.2", "rayon", "regex", "regex-automata", - "reqwest 0.13.1", + "reqwest 0.13.2", "rig-core", - "rustls 0.23.27", + "rustls 0.23.36", "rustsec", "rustyline", "schemars", @@ -5549,6 +5573,8 @@ dependencies = [ "simdutf8", "similar", "strip-ansi-escapes", + "syncable-ag-ui-core", + "syncable-ag-ui-server", "syntect", "tempfile", "tera", @@ -5556,7 +5582,7 @@ dependencies = [ "termcolor", "termimad", "textwrap", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-stream", "toml", @@ -5596,16 +5622,16 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", "walkdir", "yaml-rust", ] [[package]] name = "system-configuration" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ "bitflags", "core-foundation 0.9.4", @@ -5632,7 +5658,7 @@ dependencies = [ "crossbeam-channel", "gix", "home", - "http 1.3.1", + "http 1.4.0", "libc", "memchr", "rayon", @@ -5642,7 +5668,7 @@ dependencies = [ "serde", "serde_json", "smol_str", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "toml-span", "twox-hash", @@ -5650,12 +5676,12 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.24.0" +version = "3.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom 0.4.1", "once_cell", "rustix", "windows-sys 0.61.2", @@ -5663,9 +5689,9 @@ dependencies = [ [[package]] name = "tera" -version = "1.20.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab9d851b45e865f178319da0abdbfe6acbc4328759ff18dafc3a41c16b4cd2ee" +checksum = "e8004bca281f2d32df3bacd59bc67b312cb4c70cea46cbd79dbe8ac5ed206722" dependencies = [ "chrono", "chrono-tz", @@ -5680,7 +5706,7 @@ dependencies = [ "serde", "serde_json", "slug", - "unic-segment", + "unicode-segmentation", ] [[package]] @@ -5725,7 +5751,7 @@ dependencies = [ "lazy-regex", "minimad", "serde", - "thiserror 2.0.17", + "thiserror 2.0.18", "unicode-width 0.1.14", ] @@ -5743,7 +5769,7 @@ checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" dependencies = [ "smawk", "unicode-linebreak", - "unicode-width 0.2.0", + "unicode-width 0.2.2", ] [[package]] @@ -5757,11 +5783,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -5777,9 +5803,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -5797,30 +5823,30 @@ dependencies = [ [[package]] name = "time" -version = "0.3.41" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -5828,9 +5854,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -5838,9 +5864,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -5853,16 +5879,16 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.48.0" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ "bytes", "libc", "mio", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.0", + "socket2 0.6.2", "tokio-macros", "windows-sys 0.61.2", ] @@ -5900,11 +5926,11 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.27", + "rustls 0.23.36", "tokio", ] @@ -5934,9 +5960,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.15" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -5948,14 +5974,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.6" +version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae2a4cf385da23d1d53bc15cdfa5c2109e93d8d362393c801e87da2f72f0e201" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ "indexmap", "serde_core", "serde_spanned", - "toml_datetime 0.7.1", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", "winnow 0.7.14", @@ -5978,9 +6004,9 @@ checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" [[package]] name = "toml_datetime" -version = "0.7.1" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a197c0ec7d131bfc6f7e82c8442ba1595aeab35da7adbf05b6b73cd06a16b6be" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ "serde_core", ] @@ -5998,24 +6024,24 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.2" +version = "1.0.9+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" dependencies = [ "winnow 0.7.14", ] [[package]] name = "toml_writer" -version = "1.0.2" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -6035,12 +6061,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "async-compression", - "base64 0.22.1", + "base64", "bitflags", "bytes", "futures-core", "futures-util", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "http-body-util", "iri-string", @@ -6124,26 +6150,26 @@ checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" dependencies = [ "bytes", "data-encoding", - "http 1.3.1", + "http 1.4.0", "httparse", "log", - "rand 0.9.1", + "rand 0.9.2", "sha1", - "thiserror 2.0.17", + "thiserror 2.0.18", "utf-8", ] [[package]] name = "twox-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b907da542cbced5261bd3256de1b3a1bf340a3d37f93425a07362a1d687de56" +checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "ucd-trie" @@ -6166,61 +6192,11 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" -[[package]] -name = "unic-char-property" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" -dependencies = [ - "unic-char-range", -] - -[[package]] -name = "unic-char-range" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" - -[[package]] -name = "unic-common" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" - -[[package]] -name = "unic-segment" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23" -dependencies = [ - "unic-ucd-segment", -] - -[[package]] -name = "unic-ucd-segment" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700" -dependencies = [ - "unic-char-property", - "unic-char-range", - "unic-ucd-version", -] - -[[package]] -name = "unic-ucd-version" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" -dependencies = [ - "unic-common", -] - [[package]] name = "unicase" -version = "2.8.1" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" [[package]] name = "unicode-bom" @@ -6230,9 +6206,9 @@ checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-linebreak" @@ -6242,9 +6218,9 @@ checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-normalization" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" dependencies = [ "tinyvec", ] @@ -6263,15 +6239,21 @@ checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-width" -version = "0.2.0" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "unit-prefix" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "323402cff2dd658f39ca17c789b502021b3f18707c91cdf22e3838e1b4023817" +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" [[package]] name = "unsafe-libyaml" @@ -6287,14 +6269,15 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.4" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", + "serde_derive", ] [[package]] @@ -6323,13 +6306,13 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.18.1" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.4.1", "js-sys", - "serde", + "serde_core", "wasm-bindgen", ] @@ -6399,52 +6382,49 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" +name = "wasip2" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] -name = "wasm-bindgen" -version = "0.2.100" +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", + "wit-bindgen", ] [[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" +name = "wasm-bindgen" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -6453,9 +6433,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6463,26 +6443,48 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + [[package]] name = "wasm-streams" version = "0.4.2" @@ -6496,11 +6498,23 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" dependencies = [ "js-sys", "wasm-bindgen", @@ -6518,9 +6532,9 @@ dependencies = [ [[package]] name = "webbrowser" -version = "1.0.6" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00f1243ef785213e3a32fa0396093424a3a6ea566f9948497e5a2309261a4c97" +checksum = "3f00bb839c1cf1e3036066614cbdcd035ecf215206691ea646aa3c60a24f68f2" dependencies = [ "core-foundation 0.10.1", "jni", @@ -6534,9 +6548,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36a29fc0408b113f68cf32637857ab740edfafdf460c326cd2afaa2d84cc05dc" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" dependencies = [ "rustls-pki-types", ] @@ -6559,11 +6573,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6574,22 +6588,22 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.61.2" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.1.1", + "windows-link", "windows-result", "windows-strings", ] [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", @@ -6598,21 +6612,15 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", "syn", ] -[[package]] -name = "windows-link" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" - [[package]] name = "windows-link" version = "0.2.1" @@ -6621,31 +6629,31 @@ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-registry" -version = "0.5.2" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3bab093bdd303a1240bb99b8aba8ea8a69ee19d34c9e2ef9594e708a4878820" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ - "windows-link 0.1.1", + "windows-link", "windows-result", "windows-strings", ] [[package]] name = "windows-result" -version = "0.3.4" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link 0.1.1", + "windows-link", ] [[package]] name = "windows-strings" -version = "0.4.2" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link 0.1.1", + "windows-link", ] [[package]] @@ -6681,7 +6689,7 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.2", + "windows-targets 0.53.5", ] [[package]] @@ -6690,7 +6698,7 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -6726,18 +6734,19 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.2" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -6754,9 +6763,9 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -6772,9 +6781,9 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -6790,9 +6799,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" @@ -6802,9 +6811,9 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -6820,9 +6829,9 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -6838,9 +6847,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -6856,9 +6865,9 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -6874,9 +6883,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" @@ -6897,19 +6906,98 @@ dependencies = [ ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", ] [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "xmlparser" @@ -6939,11 +7027,10 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -6951,9 +7038,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", @@ -6963,18 +7050,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.25" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.25" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" dependencies = [ "proc-macro2", "quote", @@ -7004,15 +7091,15 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", @@ -7021,9 +7108,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.2" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -7032,9 +7119,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", @@ -7046,3 +7133,9 @@ name = "zlib-rs" version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 5f76557b..f5f64a60 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,16 +77,16 @@ bstr = "1.9" # Byte string utilities simdutf8 = "0.1" # SIMD UTF-8 validation # Telemetry dependencies -uuid = { version = "1.0", features = ["v4"] } +uuid = { version = "1.0", features = ["v4", "serde"] } rand = "0.9" futures-util = "0.3" # Agent dependencies (using Rig - LLM application framework) rig-core = { version = "0.28", features = ["derive", "image"] } -# AG-UI Protocol - enables frontend connectivity for agent mode -ag-ui-core = { path = "crates/ag-ui-core" } -ag-ui-server = { path = "crates/ag-ui-server" } +# AG-UI Protocol - Syncable SDK for frontend connectivity +syncable-ag-ui-core = "0.2" +syncable-ag-ui-server = "0.2" axum = { version = "0.8", features = ["ws"] } tower-http = { version = "0.6", features = ["cors"] } tokio-stream = { version = "0.1", features = ["sync"] } diff --git a/crates/ag-ui-core/Cargo.toml b/crates/ag-ui-core/Cargo.toml deleted file mode 100644 index aa26d058..00000000 --- a/crates/ag-ui-core/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "ag-ui-core" -version = "0.1.0" -edition = "2024" -rust-version = "1.88" -license = "MIT" -description = "Core type library for AG-UI protocol - Syncable SDK" -readme = "README.md" - -[dependencies] -thiserror = "2" -serde = { version = "1", features = ["derive"] } -serde_json = "1" -json-patch = "3" -jsonptr = "0.6" -uuid = { version = "1", features = ["v4", "serde"] } diff --git a/crates/ag-ui-core/src/error.rs b/crates/ag-ui-core/src/error.rs deleted file mode 100644 index d7af543e..00000000 --- a/crates/ag-ui-core/src/error.rs +++ /dev/null @@ -1,30 +0,0 @@ -//! Error types for AG-UI core operations. - -use thiserror::Error; - -/// Errors that can occur in AG-UI core operations. -#[derive(Debug, Error)] -pub enum AgUiError { - /// Error during JSON serialization/deserialization - #[error("Serialization error: {0}")] - Serialization(#[from] serde_json::Error), - - /// Validation error for event or message data - #[error("Validation error: {0}")] - Validation(String), - - /// Invalid event format or structure - #[error("Invalid event: {0}")] - InvalidEvent(String), - - /// Invalid message format or content - #[error("Invalid message: {0}")] - InvalidMessage(String), - - /// State operation error - #[error("State error: {0}")] - State(String), -} - -/// Result type alias using AgUiError -pub type Result = std::result::Result; diff --git a/crates/ag-ui-core/src/event.rs b/crates/ag-ui-core/src/event.rs deleted file mode 100644 index 1522f757..00000000 --- a/crates/ag-ui-core/src/event.rs +++ /dev/null @@ -1,2481 +0,0 @@ -//! AG-UI Event Types -//! -//! This module defines all AG-UI protocol event types including: -//! - Text message events (start, content, end, chunk) -//! - Thinking text message events -//! - Tool call events (start, args, end, result) -//! - State events (snapshot, delta) -//! - Run lifecycle events (started, finished, error) -//! - Step events (started, finished) -//! - Custom and raw events - -use crate::state::AgentState; -use crate::types::{Message, MessageId, Role, RunId, ThreadId, ToolCallId}; -use crate::JsonValue; -use serde::{Deserialize, Serialize}; - -/// Event types for the AG-UI protocol. -/// -/// This enum enumerates all possible event types in the protocol. -/// Event types are serialized using SCREAMING_SNAKE_CASE (e.g., `TEXT_MESSAGE_START`). -/// -/// # Event Categories -/// -/// - **Text Messages**: `TextMessageStart`, `TextMessageContent`, `TextMessageEnd`, `TextMessageChunk` -/// - **Thinking Messages**: `ThinkingTextMessageStart`, `ThinkingTextMessageContent`, `ThinkingTextMessageEnd` -/// - **Tool Calls**: `ToolCallStart`, `ToolCallArgs`, `ToolCallEnd`, `ToolCallChunk`, `ToolCallResult` -/// - **Thinking Steps**: `ThinkingStart`, `ThinkingEnd` -/// - **State**: `StateSnapshot`, `StateDelta` -/// - **Messages**: `MessagesSnapshot` -/// - **Run Lifecycle**: `RunStarted`, `RunFinished`, `RunError` -/// - **Step Lifecycle**: `StepStarted`, `StepFinished` -/// - **Other**: `Raw`, `Custom` -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum EventType { - /// Start of a text message from the assistant. - TextMessageStart, - /// Content chunk of a text message (streaming delta). - TextMessageContent, - /// End of a text message. - TextMessageEnd, - /// Complete text message chunk (non-streaming alternative). - TextMessageChunk, - /// Start of a thinking text message (extended thinking). - ThinkingTextMessageStart, - /// Content chunk of a thinking text message. - ThinkingTextMessageContent, - /// End of a thinking text message. - ThinkingTextMessageEnd, - /// Start of a tool call. - ToolCallStart, - /// Arguments chunk for a tool call (streaming). - ToolCallArgs, - /// End of a tool call. - ToolCallEnd, - /// Complete tool call chunk (non-streaming alternative). - ToolCallChunk, - /// Result of a tool call execution. - ToolCallResult, - /// Start of a thinking step (chain-of-thought). - ThinkingStart, - /// End of a thinking step. - ThinkingEnd, - /// Complete state snapshot. - StateSnapshot, - /// Incremental state update (JSON Patch RFC 6902). - StateDelta, - /// Complete messages snapshot. - MessagesSnapshot, - /// Complete activity snapshot. - ActivitySnapshot, - /// Incremental activity update (JSON Patch RFC 6902). - ActivityDelta, - /// Raw event from the underlying provider. - Raw, - /// Custom application-specific event. - Custom, - /// Agent run has started. - RunStarted, - /// Agent run has finished successfully. - RunFinished, - /// Agent run encountered an error. - RunError, - /// A step within a run has started. - StepStarted, - /// A step within a run has finished. - StepFinished, -} - -impl EventType { - /// Returns the string representation of the event type. - pub fn as_str(&self) -> &'static str { - match self { - EventType::TextMessageStart => "TEXT_MESSAGE_START", - EventType::TextMessageContent => "TEXT_MESSAGE_CONTENT", - EventType::TextMessageEnd => "TEXT_MESSAGE_END", - EventType::TextMessageChunk => "TEXT_MESSAGE_CHUNK", - EventType::ThinkingTextMessageStart => "THINKING_TEXT_MESSAGE_START", - EventType::ThinkingTextMessageContent => "THINKING_TEXT_MESSAGE_CONTENT", - EventType::ThinkingTextMessageEnd => "THINKING_TEXT_MESSAGE_END", - EventType::ToolCallStart => "TOOL_CALL_START", - EventType::ToolCallArgs => "TOOL_CALL_ARGS", - EventType::ToolCallEnd => "TOOL_CALL_END", - EventType::ToolCallChunk => "TOOL_CALL_CHUNK", - EventType::ToolCallResult => "TOOL_CALL_RESULT", - EventType::ThinkingStart => "THINKING_START", - EventType::ThinkingEnd => "THINKING_END", - EventType::StateSnapshot => "STATE_SNAPSHOT", - EventType::StateDelta => "STATE_DELTA", - EventType::MessagesSnapshot => "MESSAGES_SNAPSHOT", - EventType::ActivitySnapshot => "ACTIVITY_SNAPSHOT", - EventType::ActivityDelta => "ACTIVITY_DELTA", - EventType::Raw => "RAW", - EventType::Custom => "CUSTOM", - EventType::RunStarted => "RUN_STARTED", - EventType::RunFinished => "RUN_FINISHED", - EventType::RunError => "RUN_ERROR", - EventType::StepStarted => "STEP_STARTED", - EventType::StepFinished => "STEP_FINISHED", - } - } -} - -impl std::fmt::Display for EventType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.as_str()) - } -} - -/// Base event structure for all AG-UI protocol events. -/// -/// Contains common fields that are present in all event types. -/// Individual event structs flatten this into their structure. -/// -/// # Fields -/// -/// - `timestamp`: Optional Unix timestamp in milliseconds since epoch -/// - `raw_event`: Optional raw event from the underlying provider (for debugging/passthrough) -/// -/// # Example -/// -/// ```rust -/// use ag_ui_core::event::BaseEvent; -/// -/// let base = BaseEvent { -/// timestamp: Some(1706123456789.0), -/// raw_event: None, -/// }; -/// ``` -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] -pub struct BaseEvent { - /// Unix timestamp in milliseconds since epoch. - #[serde(skip_serializing_if = "Option::is_none")] - pub timestamp: Option, - /// Raw event from the underlying provider (for debugging/passthrough). - #[serde(rename = "rawEvent", skip_serializing_if = "Option::is_none")] - pub raw_event: Option, -} - -impl BaseEvent { - /// Creates a new empty BaseEvent. - pub fn new() -> Self { - Self::default() - } - - /// Creates a BaseEvent with the current timestamp. - pub fn with_current_timestamp() -> Self { - Self { - timestamp: Some( - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_millis() as f64) - .unwrap_or(0.0), - ), - raw_event: None, - } - } - - /// Sets the timestamp for this event. - pub fn timestamp(mut self, timestamp: f64) -> Self { - self.timestamp = Some(timestamp); - self - } - - /// Sets the raw event for this event. - pub fn raw_event(mut self, raw_event: JsonValue) -> Self { - self.raw_event = Some(raw_event); - self - } -} - -/// Validation errors for AG-UI protocol events. -/// -/// These errors occur when event data fails validation rules. -#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] -pub enum EventValidationError { - /// Delta content must not be empty. - #[error("Delta must not be an empty string")] - EmptyDelta, - /// Event format is invalid. - #[error("Invalid event format: {0}")] - InvalidFormat(String), - /// Required field is missing. - #[error("Missing required field: {0}")] - MissingField(String), - /// Event type mismatch. - #[error("Event type mismatch: expected {expected}, got {actual}")] - TypeMismatch { - /// Expected event type. - expected: String, - /// Actual event type. - actual: String, - }, -} - -// ============================================================================= -// Text Message Events -// ============================================================================= - -/// Event indicating the start of a text message. -/// -/// This event is sent when the agent begins generating a text message. -/// The message_id identifies this message throughout the streaming process. -/// -/// # Example -/// -/// ```rust -/// use ag_ui_core::{MessageId, Role}; -/// use ag_ui_core::event::TextMessageStartEvent; -/// -/// let event = TextMessageStartEvent::new(MessageId::random()); -/// assert_eq!(event.role, Role::Assistant); -/// ``` -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct TextMessageStartEvent { - /// Common event fields (timestamp, rawEvent). - #[serde(flatten)] - pub base: BaseEvent, - /// Unique identifier for this message. - #[serde(rename = "messageId")] - pub message_id: MessageId, - /// The role of the message sender (always Assistant for new messages). - pub role: Role, -} - -impl TextMessageStartEvent { - /// Creates a new TextMessageStartEvent with the given message ID. - pub fn new(message_id: impl Into) -> Self { - Self { - base: BaseEvent::default(), - message_id: message_id.into(), - role: Role::Assistant, - } - } - - /// Sets the timestamp for this event. - pub fn with_timestamp(mut self, timestamp: f64) -> Self { - self.base.timestamp = Some(timestamp); - self - } - - /// Sets the raw event for this event. - pub fn with_raw_event(mut self, raw_event: JsonValue) -> Self { - self.base.raw_event = Some(raw_event); - self - } -} - -/// Event containing a piece of text message content. -/// -/// This event is sent for each chunk of content as the agent generates a message. -/// The delta field contains the new text to append to the message. -/// -/// # Validation -/// -/// The delta must not be empty. Use `new()` which returns a Result to validate, -/// or `new_unchecked()` if you've already validated the input. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct TextMessageContentEvent { - /// Common event fields (timestamp, rawEvent). - #[serde(flatten)] - pub base: BaseEvent, - /// The message ID this content belongs to. - #[serde(rename = "messageId")] - pub message_id: MessageId, - /// The text content delta to append. - pub delta: String, -} - -impl TextMessageContentEvent { - /// Creates a new TextMessageContentEvent with validation. - /// - /// Returns an error if delta is empty. - pub fn new( - message_id: impl Into, - delta: impl Into, - ) -> Result { - let delta = delta.into(); - if delta.is_empty() { - return Err(EventValidationError::EmptyDelta); - } - Ok(Self { - base: BaseEvent::default(), - message_id: message_id.into(), - delta, - }) - } - - /// Creates a new TextMessageContentEvent without validation. - /// - /// Use this only if you've already validated the delta is not empty. - pub fn new_unchecked(message_id: impl Into, delta: impl Into) -> Self { - Self { - base: BaseEvent::default(), - message_id: message_id.into(), - delta: delta.into(), - } - } - - /// Validates this event's data. - pub fn validate(&self) -> Result<(), EventValidationError> { - if self.delta.is_empty() { - return Err(EventValidationError::EmptyDelta); - } - Ok(()) - } - - /// Sets the timestamp for this event. - pub fn with_timestamp(mut self, timestamp: f64) -> Self { - self.base.timestamp = Some(timestamp); - self - } -} - -/// Event indicating the end of a text message. -/// -/// This event is sent when the agent completes a text message. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct TextMessageEndEvent { - /// Common event fields (timestamp, rawEvent). - #[serde(flatten)] - pub base: BaseEvent, - /// The message ID that has completed. - #[serde(rename = "messageId")] - pub message_id: MessageId, -} - -impl TextMessageEndEvent { - /// Creates a new TextMessageEndEvent. - pub fn new(message_id: impl Into) -> Self { - Self { - base: BaseEvent::default(), - message_id: message_id.into(), - } - } - - /// Sets the timestamp for this event. - pub fn with_timestamp(mut self, timestamp: f64) -> Self { - self.base.timestamp = Some(timestamp); - self - } -} - -/// Event containing a chunk of text message content. -/// -/// This event combines start, content, and potentially end information in a single event. -/// Used as a non-streaming alternative where all fields may be optional. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct TextMessageChunkEvent { - /// Common event fields (timestamp, rawEvent). - #[serde(flatten)] - pub base: BaseEvent, - /// Optional message ID (may be omitted for continuation chunks). - #[serde(rename = "messageId", skip_serializing_if = "Option::is_none")] - pub message_id: Option, - /// The role of the message sender. - pub role: Role, - /// Optional text content delta. - #[serde(skip_serializing_if = "Option::is_none")] - pub delta: Option, -} - -impl TextMessageChunkEvent { - /// Creates a new TextMessageChunkEvent with the given role. - pub fn new(role: Role) -> Self { - Self { - base: BaseEvent::default(), - message_id: None, - role, - delta: None, - } - } - - /// Sets the message ID for this event. - pub fn with_message_id(mut self, message_id: impl Into) -> Self { - self.message_id = Some(message_id.into()); - self - } - - /// Sets the delta for this event. - pub fn with_delta(mut self, delta: impl Into) -> Self { - self.delta = Some(delta.into()); - self - } - - /// Sets the timestamp for this event. - pub fn with_timestamp(mut self, timestamp: f64) -> Self { - self.base.timestamp = Some(timestamp); - self - } -} - -// ============================================================================= -// Thinking Text Message Events -// ============================================================================= - -/// Event indicating the start of a thinking text message. -/// -/// This event is sent when the agent begins generating internal thinking content -/// (extended thinking / chain-of-thought). Unlike regular messages, thinking -/// messages don't have a message ID as they're ephemeral. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct ThinkingTextMessageStartEvent { - /// Common event fields (timestamp, rawEvent). - #[serde(flatten)] - pub base: BaseEvent, -} - -impl ThinkingTextMessageStartEvent { - /// Creates a new ThinkingTextMessageStartEvent. - pub fn new() -> Self { - Self { - base: BaseEvent::default(), - } - } - - /// Sets the timestamp for this event. - pub fn with_timestamp(mut self, timestamp: f64) -> Self { - self.base.timestamp = Some(timestamp); - self - } -} - -impl Default for ThinkingTextMessageStartEvent { - fn default() -> Self { - Self::new() - } -} - -/// Event containing a piece of thinking text message content. -/// -/// This event contains chunks of the agent's internal thinking process. -/// Unlike regular content events, thinking content doesn't validate for -/// empty delta as it may represent the start of a stream. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct ThinkingTextMessageContentEvent { - /// Common event fields (timestamp, rawEvent). - #[serde(flatten)] - pub base: BaseEvent, - /// The thinking content delta. - pub delta: String, -} - -impl ThinkingTextMessageContentEvent { - /// Creates a new ThinkingTextMessageContentEvent. - pub fn new(delta: impl Into) -> Self { - Self { - base: BaseEvent::default(), - delta: delta.into(), - } - } - - /// Sets the timestamp for this event. - pub fn with_timestamp(mut self, timestamp: f64) -> Self { - self.base.timestamp = Some(timestamp); - self - } -} - -/// Event indicating the end of a thinking text message. -/// -/// This event is sent when the agent completes its internal thinking process. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct ThinkingTextMessageEndEvent { - /// Common event fields (timestamp, rawEvent). - #[serde(flatten)] - pub base: BaseEvent, -} - -impl ThinkingTextMessageEndEvent { - /// Creates a new ThinkingTextMessageEndEvent. - pub fn new() -> Self { - Self { - base: BaseEvent::default(), - } - } - - /// Sets the timestamp for this event. - pub fn with_timestamp(mut self, timestamp: f64) -> Self { - self.base.timestamp = Some(timestamp); - self - } -} - -impl Default for ThinkingTextMessageEndEvent { - fn default() -> Self { - Self::new() - } -} - -// ============================================================================= -// Tool Call Events -// ============================================================================= - -/// Event indicating the start of a tool call. -/// -/// This event is sent when the agent begins calling a tool with specific parameters. -/// The tool_call_id identifies this call throughout the streaming process. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct ToolCallStartEvent { - /// Common event fields (timestamp, rawEvent). - #[serde(flatten)] - pub base: BaseEvent, - /// Unique identifier for this tool call. - #[serde(rename = "toolCallId")] - pub tool_call_id: ToolCallId, - /// Name of the tool being called. - #[serde(rename = "toolCallName")] - pub tool_call_name: String, - /// Optional parent message ID if this call is part of a message. - #[serde(rename = "parentMessageId", skip_serializing_if = "Option::is_none")] - pub parent_message_id: Option, -} - -impl ToolCallStartEvent { - /// Creates a new ToolCallStartEvent. - pub fn new(tool_call_id: impl Into, tool_call_name: impl Into) -> Self { - Self { - base: BaseEvent::default(), - tool_call_id: tool_call_id.into(), - tool_call_name: tool_call_name.into(), - parent_message_id: None, - } - } - - /// Sets the parent message ID. - pub fn with_parent_message_id(mut self, message_id: impl Into) -> Self { - self.parent_message_id = Some(message_id.into()); - self - } - - /// Sets the timestamp for this event. - pub fn with_timestamp(mut self, timestamp: f64) -> Self { - self.base.timestamp = Some(timestamp); - self - } -} - -/// Event containing tool call arguments. -/// -/// This event contains chunks of the arguments being passed to a tool. -/// Arguments are streamed as JSON string deltas. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct ToolCallArgsEvent { - /// Common event fields (timestamp, rawEvent). - #[serde(flatten)] - pub base: BaseEvent, - /// The tool call ID this argument chunk belongs to. - #[serde(rename = "toolCallId")] - pub tool_call_id: ToolCallId, - /// The argument delta (JSON string chunk). - pub delta: String, -} - -impl ToolCallArgsEvent { - /// Creates a new ToolCallArgsEvent. - pub fn new(tool_call_id: impl Into, delta: impl Into) -> Self { - Self { - base: BaseEvent::default(), - tool_call_id: tool_call_id.into(), - delta: delta.into(), - } - } - - /// Sets the timestamp for this event. - pub fn with_timestamp(mut self, timestamp: f64) -> Self { - self.base.timestamp = Some(timestamp); - self - } -} - -/// Event indicating the end of a tool call. -/// -/// This event is sent when the agent completes sending arguments to a tool. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct ToolCallEndEvent { - /// Common event fields (timestamp, rawEvent). - #[serde(flatten)] - pub base: BaseEvent, - /// The tool call ID that has completed. - #[serde(rename = "toolCallId")] - pub tool_call_id: ToolCallId, -} - -impl ToolCallEndEvent { - /// Creates a new ToolCallEndEvent. - pub fn new(tool_call_id: impl Into) -> Self { - Self { - base: BaseEvent::default(), - tool_call_id: tool_call_id.into(), - } - } - - /// Sets the timestamp for this event. - pub fn with_timestamp(mut self, timestamp: f64) -> Self { - self.base.timestamp = Some(timestamp); - self - } -} - -/// Event containing a chunk of tool call content. -/// -/// This event combines start, args, and potentially end information in a single event. -/// Used as a non-streaming alternative where all fields may be optional. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct ToolCallChunkEvent { - /// Common event fields (timestamp, rawEvent). - #[serde(flatten)] - pub base: BaseEvent, - /// Optional tool call ID (may be omitted for continuation chunks). - #[serde(rename = "toolCallId", skip_serializing_if = "Option::is_none")] - pub tool_call_id: Option, - /// Optional tool name. - #[serde(rename = "toolCallName", skip_serializing_if = "Option::is_none")] - pub tool_call_name: Option, - /// Optional parent message ID. - #[serde(rename = "parentMessageId", skip_serializing_if = "Option::is_none")] - pub parent_message_id: Option, - /// Optional argument delta. - #[serde(skip_serializing_if = "Option::is_none")] - pub delta: Option, -} - -impl ToolCallChunkEvent { - /// Creates a new empty ToolCallChunkEvent. - pub fn new() -> Self { - Self { - base: BaseEvent::default(), - tool_call_id: None, - tool_call_name: None, - parent_message_id: None, - delta: None, - } - } - - /// Sets the tool call ID. - pub fn with_tool_call_id(mut self, tool_call_id: impl Into) -> Self { - self.tool_call_id = Some(tool_call_id.into()); - self - } - - /// Sets the tool call name. - pub fn with_tool_call_name(mut self, name: impl Into) -> Self { - self.tool_call_name = Some(name.into()); - self - } - - /// Sets the parent message ID. - pub fn with_parent_message_id(mut self, message_id: impl Into) -> Self { - self.parent_message_id = Some(message_id.into()); - self - } - - /// Sets the delta. - pub fn with_delta(mut self, delta: impl Into) -> Self { - self.delta = Some(delta.into()); - self - } - - /// Sets the timestamp for this event. - pub fn with_timestamp(mut self, timestamp: f64) -> Self { - self.base.timestamp = Some(timestamp); - self - } -} - -impl Default for ToolCallChunkEvent { - fn default() -> Self { - Self::new() - } -} - -/// Event containing the result of a tool call. -/// -/// This event is sent when a tool has completed execution and returns its result. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct ToolCallResultEvent { - /// Common event fields (timestamp, rawEvent). - #[serde(flatten)] - pub base: BaseEvent, - /// Message ID for the result message. - #[serde(rename = "messageId")] - pub message_id: MessageId, - /// The tool call ID this result corresponds to. - #[serde(rename = "toolCallId")] - pub tool_call_id: ToolCallId, - /// The result content. - pub content: String, - /// Role (always Tool). - #[serde(default = "Role::tool")] - pub role: Role, -} - -impl ToolCallResultEvent { - /// Creates a new ToolCallResultEvent. - pub fn new( - message_id: impl Into, - tool_call_id: impl Into, - content: impl Into, - ) -> Self { - Self { - base: BaseEvent::default(), - message_id: message_id.into(), - tool_call_id: tool_call_id.into(), - content: content.into(), - role: Role::Tool, - } - } - - /// Sets the timestamp for this event. - pub fn with_timestamp(mut self, timestamp: f64) -> Self { - self.base.timestamp = Some(timestamp); - self - } -} - -// ============================================================================= -// Run Lifecycle Events -// ============================================================================= - -/// Event indicating that a run has started. -/// -/// This event is sent when an agent run begins execution within a specific thread. -/// A run represents a single agent execution that may produce multiple events. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct RunStartedEvent { - /// Common event fields (timestamp, rawEvent). - #[serde(flatten)] - pub base: BaseEvent, - /// The thread ID this run belongs to. - #[serde(rename = "threadId")] - pub thread_id: ThreadId, - /// Unique identifier for this run. - #[serde(rename = "runId")] - pub run_id: RunId, -} - -impl RunStartedEvent { - /// Creates a new RunStartedEvent. - pub fn new(thread_id: impl Into, run_id: impl Into) -> Self { - Self { - base: BaseEvent::default(), - thread_id: thread_id.into(), - run_id: run_id.into(), - } - } - - /// Sets the timestamp for this event. - pub fn with_timestamp(mut self, timestamp: f64) -> Self { - self.base.timestamp = Some(timestamp); - self - } -} - -/// Outcome of a run finishing. -/// -/// Used to indicate whether a run completed successfully or was interrupted -/// for human-in-the-loop interaction. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum RunFinishedOutcome { - /// Run completed successfully. - Success, - /// Run was interrupted and requires human input to continue. - Interrupt, -} - -impl Default for RunFinishedOutcome { - fn default() -> Self { - Self::Success - } -} - -/// Information about a run interrupt. -/// -/// When a run finishes with `outcome == Interrupt`, this struct contains -/// information about why the interrupt occurred and what input is needed. -/// -/// # Example -/// -/// ```rust -/// use ag_ui_core::InterruptInfo; -/// -/// let info = InterruptInfo::new() -/// .with_id("approval-001") -/// .with_reason("human_approval") -/// .with_payload(serde_json::json!({ -/// "action": "DELETE", -/// "table": "users", -/// "affectedRows": 42 -/// })); -/// ``` -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] -pub struct InterruptInfo { - /// Optional identifier for tracking this interrupt across resume. - #[serde(skip_serializing_if = "Option::is_none")] - pub id: Option, - /// Optional reason describing why the interrupt occurred. - /// Common values: "human_approval", "upload_required", "policy_hold" - #[serde(skip_serializing_if = "Option::is_none")] - pub reason: Option, - /// Optional payload with context for the interrupt UI. - /// Contains arbitrary JSON data for rendering approval forms, proposals, etc. - #[serde(skip_serializing_if = "Option::is_none")] - pub payload: Option, -} - -impl InterruptInfo { - /// Creates a new empty InterruptInfo. - pub fn new() -> Self { - Self::default() - } - - /// Sets the interrupt ID. - pub fn with_id(mut self, id: impl Into) -> Self { - self.id = Some(id.into()); - self - } - - /// Sets the interrupt reason. - pub fn with_reason(mut self, reason: impl Into) -> Self { - self.reason = Some(reason.into()); - self - } - - /// Sets the interrupt payload. - pub fn with_payload(mut self, payload: JsonValue) -> Self { - self.payload = Some(payload); - self - } -} - -/// Event indicating that a run has finished. -/// -/// This event is sent when an agent run completes, either successfully or -/// with an interrupt requiring human input. -/// -/// # Interrupt Flow -/// -/// When `outcome == Interrupt`, the agent indicates that on the next run, -/// a value needs to be provided via `RunAgentInput.resume` to continue. -/// -/// # Example -/// -/// ```rust -/// use ag_ui_core::{RunFinishedEvent, RunFinishedOutcome, InterruptInfo, ThreadId, RunId}; -/// -/// // Success case -/// let success = RunFinishedEvent::new(ThreadId::random(), RunId::random()) -/// .with_result(serde_json::json!({"status": "done"})); -/// -/// // Interrupt case -/// let interrupt = RunFinishedEvent::new(ThreadId::random(), RunId::random()) -/// .with_interrupt( -/// InterruptInfo::new() -/// .with_reason("human_approval") -/// .with_payload(serde_json::json!({"action": "send_email"})) -/// ); -/// ``` -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct RunFinishedEvent { - /// Common event fields (timestamp, rawEvent). - #[serde(flatten)] - pub base: BaseEvent, - /// The thread ID this run belongs to. - #[serde(rename = "threadId")] - pub thread_id: ThreadId, - /// The run ID that finished. - #[serde(rename = "runId")] - pub run_id: RunId, - /// Outcome of the run. Optional for backward compatibility. - /// When omitted, outcome is inferred: if interrupt is present, it's Interrupt; otherwise Success. - #[serde(skip_serializing_if = "Option::is_none")] - pub outcome: Option, - /// Optional result value from the run. - /// Present when outcome is Success (or omitted with no interrupt). - #[serde(skip_serializing_if = "Option::is_none")] - pub result: Option, - /// Optional interrupt information. - /// Present when outcome is Interrupt (or omitted with interrupt present). - #[serde(skip_serializing_if = "Option::is_none")] - pub interrupt: Option, -} - -impl RunFinishedEvent { - /// Creates a new RunFinishedEvent with Success outcome. - pub fn new(thread_id: impl Into, run_id: impl Into) -> Self { - Self { - base: BaseEvent::default(), - thread_id: thread_id.into(), - run_id: run_id.into(), - outcome: None, - result: None, - interrupt: None, - } - } - - /// Sets the outcome explicitly. - pub fn with_outcome(mut self, outcome: RunFinishedOutcome) -> Self { - self.outcome = Some(outcome); - self - } - - /// Sets the result for this event (implies Success outcome). - pub fn with_result(mut self, result: JsonValue) -> Self { - self.result = Some(result); - self - } - - /// Sets the interrupt info (implies Interrupt outcome). - pub fn with_interrupt(mut self, interrupt: InterruptInfo) -> Self { - self.outcome = Some(RunFinishedOutcome::Interrupt); - self.interrupt = Some(interrupt); - self - } - - /// Sets the timestamp for this event. - pub fn with_timestamp(mut self, timestamp: f64) -> Self { - self.base.timestamp = Some(timestamp); - self - } - - /// Returns the effective outcome of this event. - /// - /// If outcome is explicitly set, returns that. Otherwise: - /// - If interrupt is present, returns Interrupt - /// - Otherwise, returns Success - pub fn effective_outcome(&self) -> RunFinishedOutcome { - self.outcome.unwrap_or_else(|| { - if self.interrupt.is_some() { - RunFinishedOutcome::Interrupt - } else { - RunFinishedOutcome::Success - } - }) - } -} - -/// Event indicating that a run has encountered an error. -/// -/// This event is sent when an agent run fails with an error. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct RunErrorEvent { - /// Common event fields (timestamp, rawEvent). - #[serde(flatten)] - pub base: BaseEvent, - /// Error message describing what went wrong. - pub message: String, - /// Optional error code for programmatic handling. - #[serde(skip_serializing_if = "Option::is_none")] - pub code: Option, -} - -impl RunErrorEvent { - /// Creates a new RunErrorEvent. - pub fn new(message: impl Into) -> Self { - Self { - base: BaseEvent::default(), - message: message.into(), - code: None, - } - } - - /// Sets the error code. - pub fn with_code(mut self, code: impl Into) -> Self { - self.code = Some(code.into()); - self - } - - /// Sets the timestamp for this event. - pub fn with_timestamp(mut self, timestamp: f64) -> Self { - self.base.timestamp = Some(timestamp); - self - } -} - -// ============================================================================= -// Step Events -// ============================================================================= - -/// Event indicating that a step has started. -/// -/// This event is sent when a specific named step within a run begins execution. -/// Steps allow tracking progress through multi-stage agent workflows. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct StepStartedEvent { - /// Common event fields (timestamp, rawEvent). - #[serde(flatten)] - pub base: BaseEvent, - /// Name of the step that started. - #[serde(rename = "stepName")] - pub step_name: String, -} - -impl StepStartedEvent { - /// Creates a new StepStartedEvent. - pub fn new(step_name: impl Into) -> Self { - Self { - base: BaseEvent::default(), - step_name: step_name.into(), - } - } - - /// Sets the timestamp for this event. - pub fn with_timestamp(mut self, timestamp: f64) -> Self { - self.base.timestamp = Some(timestamp); - self - } -} - -/// Event indicating that a step has finished. -/// -/// This event is sent when a specific named step within a run completes execution. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct StepFinishedEvent { - /// Common event fields (timestamp, rawEvent). - #[serde(flatten)] - pub base: BaseEvent, - /// Name of the step that finished. - #[serde(rename = "stepName")] - pub step_name: String, -} - -impl StepFinishedEvent { - /// Creates a new StepFinishedEvent. - pub fn new(step_name: impl Into) -> Self { - Self { - base: BaseEvent::default(), - step_name: step_name.into(), - } - } - - /// Sets the timestamp for this event. - pub fn with_timestamp(mut self, timestamp: f64) -> Self { - self.base.timestamp = Some(timestamp); - self - } -} - -// ============================================================================= -// State Events -// ============================================================================= - -/// Event containing a complete state snapshot. -/// -/// This event is sent to provide the full current state of the agent. -/// The state is generic over `StateT` which must implement `AgentState`. -/// -/// # Type Parameter -/// -/// - `StateT`: The type of state, defaults to `JsonValue` for flexibility. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(bound(deserialize = ""))] -pub struct StateSnapshotEvent { - /// Common event fields (timestamp, rawEvent). - #[serde(flatten)] - pub base: BaseEvent, - /// The complete state snapshot. - pub snapshot: StateT, -} - -impl StateSnapshotEvent { - /// Creates a new StateSnapshotEvent with the given state. - pub fn new(snapshot: StateT) -> Self { - Self { - base: BaseEvent::default(), - snapshot, - } - } - - /// Sets the timestamp for this event. - pub fn with_timestamp(mut self, timestamp: f64) -> Self { - self.base.timestamp = Some(timestamp); - self - } -} - -impl Default for StateSnapshotEvent { - fn default() -> Self { - Self { - base: BaseEvent::default(), - snapshot: StateT::default(), - } - } -} - -/// Event containing incremental state updates as JSON Patch operations. -/// -/// This event is sent to update state incrementally using RFC 6902 JSON Patch format. -/// The delta is a vector of patch operations (add, remove, replace, move, copy, test). -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct StateDeltaEvent { - /// Common event fields (timestamp, rawEvent). - #[serde(flatten)] - pub base: BaseEvent, - /// JSON Patch operations per RFC 6902. - pub delta: Vec, -} - -impl StateDeltaEvent { - /// Creates a new StateDeltaEvent with the given patch operations. - pub fn new(delta: Vec) -> Self { - Self { - base: BaseEvent::default(), - delta, - } - } - - /// Sets the timestamp for this event. - pub fn with_timestamp(mut self, timestamp: f64) -> Self { - self.base.timestamp = Some(timestamp); - self - } -} - -impl Default for StateDeltaEvent { - fn default() -> Self { - Self { - base: BaseEvent::default(), - delta: Vec::new(), - } - } -} - -/// Event containing a complete snapshot of all messages. -/// -/// This event is sent to provide the full message history to the client. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct MessagesSnapshotEvent { - /// Common event fields (timestamp, rawEvent). - #[serde(flatten)] - pub base: BaseEvent, - /// Complete list of messages. - pub messages: Vec, -} - -impl MessagesSnapshotEvent { - /// Creates a new MessagesSnapshotEvent with the given messages. - pub fn new(messages: Vec) -> Self { - Self { - base: BaseEvent::default(), - messages, - } - } - - /// Sets the timestamp for this event. - pub fn with_timestamp(mut self, timestamp: f64) -> Self { - self.base.timestamp = Some(timestamp); - self - } -} - -impl Default for MessagesSnapshotEvent { - fn default() -> Self { - Self { - base: BaseEvent::default(), - messages: Vec::new(), - } - } -} - -// ============================================================================= -// Activity Events -// ============================================================================= - -/// Event containing a complete activity snapshot. -/// -/// This event creates a new activity message or replaces an existing one. -/// Activity messages track structured agent activities like planning or research. -/// -/// # Example -/// -/// ```rust -/// use ag_ui_core::event::ActivitySnapshotEvent; -/// use ag_ui_core::MessageId; -/// use serde_json::json; -/// -/// let event = ActivitySnapshotEvent::new( -/// MessageId::random(), -/// "PLAN", -/// json!({"steps": ["research", "implement", "test"]}), -/// ); -/// ``` -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct ActivitySnapshotEvent { - /// Common event fields (timestamp, rawEvent). - #[serde(flatten)] - pub base: BaseEvent, - /// The message ID for this activity. - #[serde(rename = "messageId")] - pub message_id: MessageId, - /// The type of activity (e.g., "PLAN", "RESEARCH"). - #[serde(rename = "activityType")] - pub activity_type: String, - /// The activity content as a flexible JSON object. - pub content: JsonValue, - /// Whether to replace the existing activity content (default: true). - #[serde(skip_serializing_if = "Option::is_none")] - pub replace: Option, -} - -impl ActivitySnapshotEvent { - /// Creates a new ActivitySnapshotEvent with the given message ID, type, and content. - pub fn new( - message_id: impl Into, - activity_type: impl Into, - content: JsonValue, - ) -> Self { - Self { - base: BaseEvent::default(), - message_id: message_id.into(), - activity_type: activity_type.into(), - content, - replace: None, - } - } - - /// Sets whether to replace the existing activity content. - pub fn with_replace(mut self, replace: bool) -> Self { - self.replace = Some(replace); - self - } - - /// Sets the timestamp for this event. - pub fn with_timestamp(mut self, timestamp: f64) -> Self { - self.base.timestamp = Some(timestamp); - self - } -} - -/// Event containing an incremental activity update. -/// -/// This event applies a JSON Patch (RFC 6902) to an existing activity's content. -/// Use this for efficient partial updates to activity content. -/// -/// # Example -/// -/// ```rust -/// use ag_ui_core::event::ActivityDeltaEvent; -/// use ag_ui_core::MessageId; -/// use serde_json::json; -/// -/// let event = ActivityDeltaEvent::new( -/// MessageId::random(), -/// "PLAN", -/// vec![json!({"op": "add", "path": "/steps/-", "value": "deploy"})], -/// ); -/// ``` -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct ActivityDeltaEvent { - /// Common event fields (timestamp, rawEvent). - #[serde(flatten)] - pub base: BaseEvent, - /// The message ID for this activity. - #[serde(rename = "messageId")] - pub message_id: MessageId, - /// The type of activity (e.g., "PLAN", "RESEARCH"). - #[serde(rename = "activityType")] - pub activity_type: String, - /// JSON Patch operations (RFC 6902) to apply to the content. - pub patch: Vec, -} - -impl ActivityDeltaEvent { - /// Creates a new ActivityDeltaEvent with the given message ID, type, and patch. - pub fn new( - message_id: impl Into, - activity_type: impl Into, - patch: Vec, - ) -> Self { - Self { - base: BaseEvent::default(), - message_id: message_id.into(), - activity_type: activity_type.into(), - patch, - } - } - - /// Sets the timestamp for this event. - pub fn with_timestamp(mut self, timestamp: f64) -> Self { - self.base.timestamp = Some(timestamp); - self - } -} - -// ============================================================================= -// Thinking Step Events -// ============================================================================= - -/// Event indicating that a thinking step has started. -/// -/// This event is sent when the agent begins a chain-of-thought reasoning step. -/// Unlike ThinkingTextMessage events (which contain actual thinking content), -/// this event marks the boundary of a thinking block. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct ThinkingStartEvent { - /// Common event fields (timestamp, rawEvent). - #[serde(flatten)] - pub base: BaseEvent, - /// Optional title for the thinking step. - #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option, -} - -impl ThinkingStartEvent { - /// Creates a new ThinkingStartEvent. - pub fn new() -> Self { - Self { - base: BaseEvent::default(), - title: None, - } - } - - /// Sets the title for this thinking step. - pub fn with_title(mut self, title: impl Into) -> Self { - self.title = Some(title.into()); - self - } - - /// Sets the timestamp for this event. - pub fn with_timestamp(mut self, timestamp: f64) -> Self { - self.base.timestamp = Some(timestamp); - self - } -} - -impl Default for ThinkingStartEvent { - fn default() -> Self { - Self::new() - } -} - -/// Event indicating that a thinking step has ended. -/// -/// This event is sent when the agent completes a chain-of-thought reasoning step. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct ThinkingEndEvent { - /// Common event fields (timestamp, rawEvent). - #[serde(flatten)] - pub base: BaseEvent, -} - -impl ThinkingEndEvent { - /// Creates a new ThinkingEndEvent. - pub fn new() -> Self { - Self { - base: BaseEvent::default(), - } - } - - /// Sets the timestamp for this event. - pub fn with_timestamp(mut self, timestamp: f64) -> Self { - self.base.timestamp = Some(timestamp); - self - } -} - -impl Default for ThinkingEndEvent { - fn default() -> Self { - Self::new() - } -} - -// ============================================================================= -// Special Events -// ============================================================================= - -/// Event containing raw data from the underlying provider. -/// -/// This event is sent to pass through raw provider-specific data that -/// doesn't fit into other event types. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct RawEvent { - /// Common event fields (timestamp, rawEvent). - #[serde(flatten)] - pub base: BaseEvent, - /// The raw event data. - pub event: JsonValue, - /// Optional source identifier for the raw event. - #[serde(skip_serializing_if = "Option::is_none")] - pub source: Option, -} - -impl RawEvent { - /// Creates a new RawEvent with the given event data. - pub fn new(event: JsonValue) -> Self { - Self { - base: BaseEvent::default(), - event, - source: None, - } - } - - /// Sets the source identifier. - pub fn with_source(mut self, source: impl Into) -> Self { - self.source = Some(source.into()); - self - } - - /// Sets the timestamp for this event. - pub fn with_timestamp(mut self, timestamp: f64) -> Self { - self.base.timestamp = Some(timestamp); - self - } -} - -/// Event for custom application-specific data. -/// -/// This event allows sending arbitrary named events with custom payloads. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct CustomEvent { - /// Common event fields (timestamp, rawEvent). - #[serde(flatten)] - pub base: BaseEvent, - /// Name of the custom event. - pub name: String, - /// Custom event payload. - pub value: JsonValue, -} - -impl CustomEvent { - /// Creates a new CustomEvent with the given name and value. - pub fn new(name: impl Into, value: JsonValue) -> Self { - Self { - base: BaseEvent::default(), - name: name.into(), - value, - } - } - - /// Sets the timestamp for this event. - pub fn with_timestamp(mut self, timestamp: f64) -> Self { - self.base.timestamp = Some(timestamp); - self - } -} - -// ============================================================================= -// Event Union -// ============================================================================= - -/// Union of all possible events in the Agent User Interaction Protocol. -/// -/// This enum represents any event that can be sent or received in the AG-UI protocol. -/// Events are serialized with a `type` discriminant in SCREAMING_SNAKE_CASE format. -/// -/// # Type Parameter -/// -/// - `StateT`: The type of state for `StateSnapshot` events, defaults to `JsonValue`. -/// -/// # Serialization -/// -/// Events are serialized as JSON objects with a `type` field indicating the variant: -/// ```json -/// {"type": "TEXT_MESSAGE_START", "messageId": "...", "role": "assistant"} -/// ``` -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(tag = "type", rename_all = "SCREAMING_SNAKE_CASE", bound(deserialize = ""))] -pub enum Event { - /// Start of a text message from the assistant. - TextMessageStart(TextMessageStartEvent), - /// Content chunk of a text message (streaming delta). - TextMessageContent(TextMessageContentEvent), - /// End of a text message. - TextMessageEnd(TextMessageEndEvent), - /// Complete text message chunk (non-streaming alternative). - TextMessageChunk(TextMessageChunkEvent), - /// Start of a thinking text message (extended thinking). - ThinkingTextMessageStart(ThinkingTextMessageStartEvent), - /// Content chunk of a thinking text message. - ThinkingTextMessageContent(ThinkingTextMessageContentEvent), - /// End of a thinking text message. - ThinkingTextMessageEnd(ThinkingTextMessageEndEvent), - /// Start of a tool call. - ToolCallStart(ToolCallStartEvent), - /// Arguments chunk for a tool call (streaming). - ToolCallArgs(ToolCallArgsEvent), - /// End of a tool call. - ToolCallEnd(ToolCallEndEvent), - /// Complete tool call chunk (non-streaming alternative). - ToolCallChunk(ToolCallChunkEvent), - /// Result of a tool call execution. - ToolCallResult(ToolCallResultEvent), - /// Start of a thinking step (chain-of-thought boundary). - ThinkingStart(ThinkingStartEvent), - /// End of a thinking step. - ThinkingEnd(ThinkingEndEvent), - /// Complete state snapshot. - StateSnapshot(StateSnapshotEvent), - /// Incremental state update (JSON Patch). - StateDelta(StateDeltaEvent), - /// Complete messages snapshot. - MessagesSnapshot(MessagesSnapshotEvent), - /// Complete activity snapshot. - ActivitySnapshot(ActivitySnapshotEvent), - /// Incremental activity update (JSON Patch). - ActivityDelta(ActivityDeltaEvent), - /// Raw event from the underlying provider. - Raw(RawEvent), - /// Custom application-specific event. - Custom(CustomEvent), - /// Agent run has started. - RunStarted(RunStartedEvent), - /// Agent run has finished successfully. - RunFinished(RunFinishedEvent), - /// Agent run encountered an error. - RunError(RunErrorEvent), - /// A step within a run has started. - StepStarted(StepStartedEvent), - /// A step within a run has finished. - StepFinished(StepFinishedEvent), -} - -impl Event { - /// Returns the event type for this event. - pub fn event_type(&self) -> EventType { - match self { - Event::TextMessageStart(_) => EventType::TextMessageStart, - Event::TextMessageContent(_) => EventType::TextMessageContent, - Event::TextMessageEnd(_) => EventType::TextMessageEnd, - Event::TextMessageChunk(_) => EventType::TextMessageChunk, - Event::ThinkingTextMessageStart(_) => EventType::ThinkingTextMessageStart, - Event::ThinkingTextMessageContent(_) => EventType::ThinkingTextMessageContent, - Event::ThinkingTextMessageEnd(_) => EventType::ThinkingTextMessageEnd, - Event::ToolCallStart(_) => EventType::ToolCallStart, - Event::ToolCallArgs(_) => EventType::ToolCallArgs, - Event::ToolCallEnd(_) => EventType::ToolCallEnd, - Event::ToolCallChunk(_) => EventType::ToolCallChunk, - Event::ToolCallResult(_) => EventType::ToolCallResult, - Event::ThinkingStart(_) => EventType::ThinkingStart, - Event::ThinkingEnd(_) => EventType::ThinkingEnd, - Event::StateSnapshot(_) => EventType::StateSnapshot, - Event::StateDelta(_) => EventType::StateDelta, - Event::MessagesSnapshot(_) => EventType::MessagesSnapshot, - Event::ActivitySnapshot(_) => EventType::ActivitySnapshot, - Event::ActivityDelta(_) => EventType::ActivityDelta, - Event::Raw(_) => EventType::Raw, - Event::Custom(_) => EventType::Custom, - Event::RunStarted(_) => EventType::RunStarted, - Event::RunFinished(_) => EventType::RunFinished, - Event::RunError(_) => EventType::RunError, - Event::StepStarted(_) => EventType::StepStarted, - Event::StepFinished(_) => EventType::StepFinished, - } - } - - /// Returns the timestamp of this event if available. - pub fn timestamp(&self) -> Option { - match self { - Event::TextMessageStart(e) => e.base.timestamp, - Event::TextMessageContent(e) => e.base.timestamp, - Event::TextMessageEnd(e) => e.base.timestamp, - Event::TextMessageChunk(e) => e.base.timestamp, - Event::ThinkingTextMessageStart(e) => e.base.timestamp, - Event::ThinkingTextMessageContent(e) => e.base.timestamp, - Event::ThinkingTextMessageEnd(e) => e.base.timestamp, - Event::ToolCallStart(e) => e.base.timestamp, - Event::ToolCallArgs(e) => e.base.timestamp, - Event::ToolCallEnd(e) => e.base.timestamp, - Event::ToolCallChunk(e) => e.base.timestamp, - Event::ToolCallResult(e) => e.base.timestamp, - Event::ThinkingStart(e) => e.base.timestamp, - Event::ThinkingEnd(e) => e.base.timestamp, - Event::StateSnapshot(e) => e.base.timestamp, - Event::StateDelta(e) => e.base.timestamp, - Event::MessagesSnapshot(e) => e.base.timestamp, - Event::ActivitySnapshot(e) => e.base.timestamp, - Event::ActivityDelta(e) => e.base.timestamp, - Event::Raw(e) => e.base.timestamp, - Event::Custom(e) => e.base.timestamp, - Event::RunStarted(e) => e.base.timestamp, - Event::RunFinished(e) => e.base.timestamp, - Event::RunError(e) => e.base.timestamp, - Event::StepStarted(e) => e.base.timestamp, - Event::StepFinished(e) => e.base.timestamp, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_event_type_serialization() { - let event_type = EventType::TextMessageStart; - let json = serde_json::to_string(&event_type).unwrap(); - assert_eq!(json, "\"TEXT_MESSAGE_START\""); - - let event_type = EventType::ToolCallArgs; - let json = serde_json::to_string(&event_type).unwrap(); - assert_eq!(json, "\"TOOL_CALL_ARGS\""); - - let event_type = EventType::StateSnapshot; - let json = serde_json::to_string(&event_type).unwrap(); - assert_eq!(json, "\"STATE_SNAPSHOT\""); - } - - #[test] - fn test_event_type_deserialization() { - let event_type: EventType = serde_json::from_str("\"RUN_STARTED\"").unwrap(); - assert_eq!(event_type, EventType::RunStarted); - - let event_type: EventType = serde_json::from_str("\"THINKING_TEXT_MESSAGE_CONTENT\"").unwrap(); - assert_eq!(event_type, EventType::ThinkingTextMessageContent); - } - - #[test] - fn test_event_type_as_str() { - assert_eq!(EventType::TextMessageStart.as_str(), "TEXT_MESSAGE_START"); - assert_eq!(EventType::RunFinished.as_str(), "RUN_FINISHED"); - assert_eq!(EventType::Custom.as_str(), "CUSTOM"); - } - - #[test] - fn test_event_type_display() { - assert_eq!(format!("{}", EventType::TextMessageStart), "TEXT_MESSAGE_START"); - assert_eq!(format!("{}", EventType::StateDelta), "STATE_DELTA"); - } - - #[test] - fn test_base_event_serialization() { - let event = BaseEvent { - timestamp: Some(1706123456789.0), - raw_event: None, - }; - let json = serde_json::to_string(&event).unwrap(); - assert!(json.contains("\"timestamp\":1706123456789.0")); - assert!(!json.contains("rawEvent")); // skipped when None - } - - #[test] - fn test_base_event_with_raw_event() { - let event = BaseEvent { - timestamp: None, - raw_event: Some(serde_json::json!({"provider": "openai"})), - }; - let json = serde_json::to_string(&event).unwrap(); - assert!(json.contains("\"rawEvent\"")); - assert!(json.contains("\"provider\":\"openai\"")); - } - - #[test] - fn test_base_event_builder() { - let event = BaseEvent::new() - .timestamp(1234567890.0) - .raw_event(serde_json::json!({"test": true})); - - assert_eq!(event.timestamp, Some(1234567890.0)); - assert!(event.raw_event.is_some()); - } - - #[test] - fn test_event_validation_error_display() { - let error = EventValidationError::EmptyDelta; - assert_eq!(error.to_string(), "Delta must not be an empty string"); - - let error = EventValidationError::InvalidFormat("bad json".to_string()); - assert_eq!(error.to_string(), "Invalid event format: bad json"); - - let error = EventValidationError::MissingField("message_id".to_string()); - assert_eq!(error.to_string(), "Missing required field: message_id"); - - let error = EventValidationError::TypeMismatch { - expected: "TEXT_MESSAGE_START".to_string(), - actual: "RUN_STARTED".to_string(), - }; - assert_eq!( - error.to_string(), - "Event type mismatch: expected TEXT_MESSAGE_START, got RUN_STARTED" - ); - } - - #[test] - fn test_event_validation_error_is_std_error() { - fn requires_error(_: E) {} - requires_error(EventValidationError::EmptyDelta); - } - - #[test] - fn test_all_event_types_roundtrip() { - let all_types = [ - EventType::TextMessageStart, - EventType::TextMessageContent, - EventType::TextMessageEnd, - EventType::TextMessageChunk, - EventType::ThinkingTextMessageStart, - EventType::ThinkingTextMessageContent, - EventType::ThinkingTextMessageEnd, - EventType::ToolCallStart, - EventType::ToolCallArgs, - EventType::ToolCallEnd, - EventType::ToolCallChunk, - EventType::ToolCallResult, - EventType::ThinkingStart, - EventType::ThinkingEnd, - EventType::StateSnapshot, - EventType::StateDelta, - EventType::MessagesSnapshot, - EventType::ActivitySnapshot, - EventType::ActivityDelta, - EventType::Raw, - EventType::Custom, - EventType::RunStarted, - EventType::RunFinished, - EventType::RunError, - EventType::StepStarted, - EventType::StepFinished, - ]; - - for event_type in all_types { - let json = serde_json::to_string(&event_type).unwrap(); - let parsed: EventType = serde_json::from_str(&json).unwrap(); - assert_eq!(event_type, parsed); - } - } - - // ========================================================================= - // Text Message Event Tests - // ========================================================================= - - #[test] - fn test_text_message_start_event() { - use crate::types::{MessageId, Role}; - - let event = TextMessageStartEvent::new(MessageId::random()); - assert_eq!(event.role, Role::Assistant); - - let json = serde_json::to_string(&event).unwrap(); - assert!(json.contains("\"messageId\"")); - assert!(json.contains("\"role\":\"assistant\"")); - } - - #[test] - fn test_text_message_start_event_with_timestamp() { - use crate::types::MessageId; - - let event = TextMessageStartEvent::new(MessageId::random()).with_timestamp(1234567890.0); - assert_eq!(event.base.timestamp, Some(1234567890.0)); - } - - #[test] - fn test_text_message_content_event_validation() { - use crate::types::MessageId; - - // Valid delta - let result = TextMessageContentEvent::new(MessageId::random(), "Hello"); - assert!(result.is_ok()); - - // Empty delta should fail - let result = TextMessageContentEvent::new(MessageId::random(), ""); - assert!(matches!(result, Err(EventValidationError::EmptyDelta))); - } - - #[test] - fn test_text_message_content_event_validate_method() { - use crate::types::MessageId; - - let event = TextMessageContentEvent::new_unchecked(MessageId::random(), ""); - assert!(matches!(event.validate(), Err(EventValidationError::EmptyDelta))); - - let event = TextMessageContentEvent::new_unchecked(MessageId::random(), "Hello"); - assert!(event.validate().is_ok()); - } - - #[test] - fn test_text_message_content_event_serialization() { - use crate::types::MessageId; - - let event = TextMessageContentEvent::new(MessageId::random(), "Hello, world!").unwrap(); - let json = serde_json::to_string(&event).unwrap(); - - assert!(json.contains("\"messageId\"")); - assert!(json.contains("\"delta\":\"Hello, world!\"")); - } - - #[test] - fn test_text_message_end_event() { - use crate::types::MessageId; - - let msg_id = MessageId::random(); - let event = TextMessageEndEvent::new(msg_id.clone()); - - let json = serde_json::to_string(&event).unwrap(); - assert!(json.contains("\"messageId\"")); - } - - #[test] - fn test_text_message_chunk_event() { - use crate::types::{MessageId, Role}; - - let event = TextMessageChunkEvent::new(Role::Assistant) - .with_message_id(MessageId::random()) - .with_delta("chunk content"); - - assert!(event.message_id.is_some()); - assert_eq!(event.delta, Some("chunk content".to_string())); - - let json = serde_json::to_string(&event).unwrap(); - assert!(json.contains("\"messageId\"")); - assert!(json.contains("\"delta\":\"chunk content\"")); - } - - #[test] - fn test_text_message_chunk_event_skips_none() { - use crate::types::Role; - - let event = TextMessageChunkEvent::new(Role::Assistant); - let json = serde_json::to_string(&event).unwrap(); - - // Should not contain optional fields when None - assert!(!json.contains("\"messageId\"")); - assert!(!json.contains("\"delta\"")); - assert!(json.contains("\"role\":\"assistant\"")); - } - - // ========================================================================= - // Thinking Text Message Event Tests - // ========================================================================= - - #[test] - fn test_thinking_text_message_start_event() { - let event = ThinkingTextMessageStartEvent::new(); - let json = serde_json::to_string(&event).unwrap(); - - // Should be minimal - just empty object or with timestamp if set - assert_eq!(json, "{}"); - } - - #[test] - fn test_thinking_text_message_start_event_with_timestamp() { - let event = ThinkingTextMessageStartEvent::new().with_timestamp(1234567890.0); - let json = serde_json::to_string(&event).unwrap(); - - assert!(json.contains("\"timestamp\":1234567890.0")); - } - - #[test] - fn test_thinking_text_message_content_event() { - let event = ThinkingTextMessageContentEvent::new("Let me think about this..."); - - assert_eq!(event.delta, "Let me think about this..."); - - let json = serde_json::to_string(&event).unwrap(); - assert!(json.contains("\"delta\":\"Let me think about this...\"")); - } - - #[test] - fn test_thinking_text_message_content_event_allows_empty() { - // Unlike TextMessageContentEvent, ThinkingTextMessageContentEvent allows empty delta - let event = ThinkingTextMessageContentEvent::new(""); - assert_eq!(event.delta, ""); - } - - #[test] - fn test_thinking_text_message_end_event() { - let event = ThinkingTextMessageEndEvent::new(); - let json = serde_json::to_string(&event).unwrap(); - - // Should be minimal - assert_eq!(json, "{}"); - } - - #[test] - fn test_thinking_text_message_events_default() { - let start = ThinkingTextMessageStartEvent::default(); - let end = ThinkingTextMessageEndEvent::default(); - - assert!(start.base.timestamp.is_none()); - assert!(end.base.timestamp.is_none()); - } - - // ========================================================================= - // Tool Call Event Tests - // ========================================================================= - - #[test] - fn test_tool_call_start_event() { - use crate::types::ToolCallId; - - let event = ToolCallStartEvent::new(ToolCallId::random(), "get_weather"); - - assert_eq!(event.tool_call_name, "get_weather"); - assert!(event.parent_message_id.is_none()); - - let json = serde_json::to_string(&event).unwrap(); - assert!(json.contains("\"toolCallId\"")); - assert!(json.contains("\"toolCallName\":\"get_weather\"")); - assert!(!json.contains("parentMessageId")); // skipped when None - } - - #[test] - fn test_tool_call_start_event_with_parent() { - use crate::types::{MessageId, ToolCallId}; - - let event = ToolCallStartEvent::new(ToolCallId::random(), "get_weather") - .with_parent_message_id(MessageId::random()); - - assert!(event.parent_message_id.is_some()); - - let json = serde_json::to_string(&event).unwrap(); - assert!(json.contains("\"parentMessageId\"")); - } - - #[test] - fn test_tool_call_args_event() { - use crate::types::ToolCallId; - - let event = ToolCallArgsEvent::new(ToolCallId::random(), r#"{"location":"#); - - assert_eq!(event.delta, r#"{"location":"#); - - let json = serde_json::to_string(&event).unwrap(); - assert!(json.contains("\"toolCallId\"")); - assert!(json.contains("\"delta\"")); - } - - #[test] - fn test_tool_call_end_event() { - use crate::types::ToolCallId; - - let event = ToolCallEndEvent::new(ToolCallId::random()); - - let json = serde_json::to_string(&event).unwrap(); - assert!(json.contains("\"toolCallId\"")); - } - - #[test] - fn test_tool_call_chunk_event() { - use crate::types::ToolCallId; - - let event = ToolCallChunkEvent::new() - .with_tool_call_id(ToolCallId::random()) - .with_tool_call_name("search") - .with_delta(r#"{"query": "rust"}"#); - - assert!(event.tool_call_id.is_some()); - assert_eq!(event.tool_call_name, Some("search".to_string())); - assert!(event.delta.is_some()); - - let json = serde_json::to_string(&event).unwrap(); - assert!(json.contains("\"toolCallId\"")); - assert!(json.contains("\"toolCallName\":\"search\"")); - assert!(json.contains("\"delta\"")); - } - - #[test] - fn test_tool_call_chunk_event_skips_none() { - let event = ToolCallChunkEvent::new(); - let json = serde_json::to_string(&event).unwrap(); - - // Should not contain optional fields when None - assert_eq!(json, "{}"); - } - - #[test] - fn test_tool_call_result_event() { - use crate::types::{MessageId, Role, ToolCallId}; - - let event = ToolCallResultEvent::new( - MessageId::random(), - ToolCallId::random(), - r#"{"weather": "sunny", "temp": 72}"#, - ); - - assert_eq!(event.role, Role::Tool); - assert!(event.content.contains("sunny")); - - let json = serde_json::to_string(&event).unwrap(); - assert!(json.contains("\"messageId\"")); - assert!(json.contains("\"toolCallId\"")); - assert!(json.contains("\"content\"")); - assert!(json.contains("\"role\":\"tool\"")); - } - - #[test] - fn test_tool_call_result_event_deserialize_default_role() { - // Test that role defaults to "tool" when not present in JSON - let json = r#"{"messageId":"550e8400-e29b-41d4-a716-446655440000","toolCallId":"6ba7b810-9dad-11d1-80b4-00c04fd430c8","content":"result"}"#; - let event: ToolCallResultEvent = serde_json::from_str(json).unwrap(); - - assert_eq!(event.role, Role::Tool); - } - - // ========================================================================= - // Run Lifecycle Event Tests - // ========================================================================= - - #[test] - fn test_run_started_event() { - use crate::types::{RunId, ThreadId}; - - let event = RunStartedEvent::new(ThreadId::random(), RunId::random()); - - let json = serde_json::to_string(&event).unwrap(); - assert!(json.contains("\"threadId\"")); - assert!(json.contains("\"runId\"")); - } - - #[test] - fn test_run_finished_event() { - use crate::types::{RunId, ThreadId}; - - let event = RunFinishedEvent::new(ThreadId::random(), RunId::random()); - - assert!(event.result.is_none()); - - let json = serde_json::to_string(&event).unwrap(); - assert!(json.contains("\"threadId\"")); - assert!(json.contains("\"runId\"")); - assert!(!json.contains("\"result\"")); // skipped when None - } - - #[test] - fn test_run_finished_event_with_result() { - use crate::types::{RunId, ThreadId}; - - let event = RunFinishedEvent::new(ThreadId::random(), RunId::random()) - .with_result(serde_json::json!({"success": true})); - - assert!(event.result.is_some()); - - let json = serde_json::to_string(&event).unwrap(); - assert!(json.contains("\"result\"")); - assert!(json.contains("\"success\":true")); - } - - #[test] - fn test_run_error_event() { - let event = RunErrorEvent::new("Connection timeout"); - - assert_eq!(event.message, "Connection timeout"); - assert!(event.code.is_none()); - - let json = serde_json::to_string(&event).unwrap(); - assert!(json.contains("\"message\":\"Connection timeout\"")); - assert!(!json.contains("\"code\"")); // skipped when None - } - - #[test] - fn test_run_error_event_with_code() { - let event = RunErrorEvent::new("Rate limit exceeded").with_code("RATE_LIMITED"); - - assert_eq!(event.code, Some("RATE_LIMITED".to_string())); - - let json = serde_json::to_string(&event).unwrap(); - assert!(json.contains("\"code\":\"RATE_LIMITED\"")); - } - - // ========================================================================= - // Interrupt Tests - // ========================================================================= - - #[test] - fn test_run_finished_outcome_serialization() { - // Test SCREAMING_SNAKE_CASE serialization - let success = RunFinishedOutcome::Success; - let interrupt = RunFinishedOutcome::Interrupt; - - let success_json = serde_json::to_string(&success).unwrap(); - let interrupt_json = serde_json::to_string(&interrupt).unwrap(); - - assert_eq!(success_json, "\"SUCCESS\""); - assert_eq!(interrupt_json, "\"INTERRUPT\""); - - // Test deserialization - let deserialized: RunFinishedOutcome = serde_json::from_str("\"SUCCESS\"").unwrap(); - assert_eq!(deserialized, RunFinishedOutcome::Success); - - let deserialized: RunFinishedOutcome = serde_json::from_str("\"INTERRUPT\"").unwrap(); - assert_eq!(deserialized, RunFinishedOutcome::Interrupt); - } - - #[test] - fn test_run_finished_outcome_default() { - let outcome = RunFinishedOutcome::default(); - assert_eq!(outcome, RunFinishedOutcome::Success); - } - - #[test] - fn test_interrupt_info_empty() { - let info = InterruptInfo::new(); - - assert!(info.id.is_none()); - assert!(info.reason.is_none()); - assert!(info.payload.is_none()); - - // Empty struct should serialize to {} - let json = serde_json::to_string(&info).unwrap(); - assert_eq!(json, "{}"); - } - - #[test] - fn test_interrupt_info_with_all_fields() { - let info = InterruptInfo::new() - .with_id("approval-001") - .with_reason("human_approval") - .with_payload(serde_json::json!({"action": "delete", "rows": 42})); - - assert_eq!(info.id, Some("approval-001".to_string())); - assert_eq!(info.reason, Some("human_approval".to_string())); - assert!(info.payload.is_some()); - - let json = serde_json::to_string(&info).unwrap(); - assert!(json.contains("\"id\":\"approval-001\"")); - assert!(json.contains("\"reason\":\"human_approval\"")); - assert!(json.contains("\"action\":\"delete\"")); - } - - #[test] - fn test_run_finished_event_with_interrupt() { - use crate::types::{RunId, ThreadId}; - - let event = RunFinishedEvent::new(ThreadId::random(), RunId::random()) - .with_interrupt( - InterruptInfo::new() - .with_reason("human_approval") - .with_payload(serde_json::json!({"proposal": "send email"})) - ); - - // with_interrupt sets outcome to Interrupt - assert_eq!(event.outcome, Some(RunFinishedOutcome::Interrupt)); - assert!(event.interrupt.is_some()); - assert!(event.result.is_none()); - - let json = serde_json::to_string(&event).unwrap(); - assert!(json.contains("\"outcome\":\"INTERRUPT\"")); - assert!(json.contains("\"interrupt\"")); - assert!(json.contains("\"reason\":\"human_approval\"")); - } - - #[test] - fn test_run_finished_event_backward_compatibility() { - use crate::types::{RunId, ThreadId}; - - // Old-style event without outcome field - let event = RunFinishedEvent::new(ThreadId::random(), RunId::random()) - .with_result(serde_json::json!({"done": true})); - - // outcome is None (backward compat) - assert!(event.outcome.is_none()); - assert!(event.interrupt.is_none()); - - // effective_outcome should infer Success - assert_eq!(event.effective_outcome(), RunFinishedOutcome::Success); - - let json = serde_json::to_string(&event).unwrap(); - assert!(!json.contains("\"outcome\"")); // skipped when None - } - - #[test] - fn test_run_finished_event_effective_outcome() { - use crate::types::{RunId, ThreadId}; - - // No outcome, no interrupt → Success - let event1 = RunFinishedEvent::new(ThreadId::random(), RunId::random()); - assert_eq!(event1.effective_outcome(), RunFinishedOutcome::Success); - - // No outcome, has interrupt → Interrupt - let mut event2 = RunFinishedEvent::new(ThreadId::random(), RunId::random()); - event2.interrupt = Some(InterruptInfo::new()); - assert_eq!(event2.effective_outcome(), RunFinishedOutcome::Interrupt); - - // Explicit outcome overrides - let event3 = RunFinishedEvent::new(ThreadId::random(), RunId::random()) - .with_outcome(RunFinishedOutcome::Interrupt); - assert_eq!(event3.effective_outcome(), RunFinishedOutcome::Interrupt); - } - - // ========================================================================= - // Step Event Tests - // ========================================================================= - - #[test] - fn test_step_started_event() { - let event = StepStartedEvent::new("process_input"); - - assert_eq!(event.step_name, "process_input"); - - let json = serde_json::to_string(&event).unwrap(); - assert!(json.contains("\"stepName\":\"process_input\"")); - } - - #[test] - fn test_step_finished_event() { - let event = StepFinishedEvent::new("generate_response"); - - assert_eq!(event.step_name, "generate_response"); - - let json = serde_json::to_string(&event).unwrap(); - assert!(json.contains("\"stepName\":\"generate_response\"")); - } - - #[test] - fn test_step_events_with_timestamp() { - let start = StepStartedEvent::new("step1").with_timestamp(1234567890.0); - let end = StepFinishedEvent::new("step1").with_timestamp(1234567891.0); - - assert_eq!(start.base.timestamp, Some(1234567890.0)); - assert_eq!(end.base.timestamp, Some(1234567891.0)); - } - - // ========================================================================= - // State Event Tests - // ========================================================================= - - #[test] - fn test_state_snapshot_event() { - let event = StateSnapshotEvent::new(serde_json::json!({"count": 42})); - - let json = serde_json::to_string(&event).unwrap(); - assert!(json.contains("\"snapshot\"")); - assert!(json.contains("\"count\":42")); - } - - #[test] - fn test_state_snapshot_event_default() { - let event: StateSnapshotEvent<()> = StateSnapshotEvent::default(); - assert!(event.base.timestamp.is_none()); - } - - #[test] - fn test_state_delta_event() { - let patches = vec![ - serde_json::json!({"op": "replace", "path": "/count", "value": 43}), - serde_json::json!({"op": "add", "path": "/new_field", "value": "hello"}), - ]; - let event = StateDeltaEvent::new(patches); - - assert_eq!(event.delta.len(), 2); - - let json = serde_json::to_string(&event).unwrap(); - assert!(json.contains("\"delta\"")); - assert!(json.contains("\"op\":\"replace\"")); - } - - #[test] - fn test_state_delta_event_default() { - let event = StateDeltaEvent::default(); - assert!(event.delta.is_empty()); - } - - #[test] - fn test_messages_snapshot_event() { - use crate::types::{Message, MessageId}; - - let messages = vec![ - Message::User { - id: MessageId::random(), - content: "Hello".to_string(), - name: None, - }, - Message::Assistant { - id: MessageId::random(), - content: Some("Hi there!".to_string()), - name: None, - tool_calls: None, - }, - ]; - let event = MessagesSnapshotEvent::new(messages); - - assert_eq!(event.messages.len(), 2); - - let json = serde_json::to_string(&event).unwrap(); - assert!(json.contains("\"messages\"")); - } - - #[test] - fn test_messages_snapshot_event_default() { - let event = MessagesSnapshotEvent::default(); - assert!(event.messages.is_empty()); - } - - // ========================================================================= - // Thinking Step Event Tests - // ========================================================================= - - #[test] - fn test_thinking_start_event() { - let event = ThinkingStartEvent::new(); - - assert!(event.title.is_none()); - - let json = serde_json::to_string(&event).unwrap(); - assert!(!json.contains("\"title\"")); // skipped when None - } - - #[test] - fn test_thinking_start_event_with_title() { - let event = ThinkingStartEvent::new().with_title("Analyzing query"); - - assert_eq!(event.title, Some("Analyzing query".to_string())); - - let json = serde_json::to_string(&event).unwrap(); - assert!(json.contains("\"title\":\"Analyzing query\"")); - } - - #[test] - fn test_thinking_end_event() { - let event = ThinkingEndEvent::new(); - - let json = serde_json::to_string(&event).unwrap(); - assert_eq!(json, "{}"); - } - - #[test] - fn test_thinking_step_events_default() { - let start = ThinkingStartEvent::default(); - let end = ThinkingEndEvent::default(); - - assert!(start.title.is_none()); - assert!(end.base.timestamp.is_none()); - } - - // ========================================================================= - // Special Event Tests - // ========================================================================= - - #[test] - fn test_raw_event() { - let event = RawEvent::new(serde_json::json!({"provider_data": "openai"})); - - assert!(event.source.is_none()); - - let json = serde_json::to_string(&event).unwrap(); - assert!(json.contains("\"event\"")); - assert!(json.contains("\"provider_data\":\"openai\"")); - assert!(!json.contains("\"source\"")); // skipped when None - } - - #[test] - fn test_raw_event_with_source() { - let event = RawEvent::new(serde_json::json!({})).with_source("anthropic"); - - assert_eq!(event.source, Some("anthropic".to_string())); - - let json = serde_json::to_string(&event).unwrap(); - assert!(json.contains("\"source\":\"anthropic\"")); - } - - #[test] - fn test_custom_event() { - let event = CustomEvent::new("user_action", serde_json::json!({"clicked": "button"})); - - assert_eq!(event.name, "user_action"); - - let json = serde_json::to_string(&event).unwrap(); - assert!(json.contains("\"name\":\"user_action\"")); - assert!(json.contains("\"value\"")); - assert!(json.contains("\"clicked\":\"button\"")); - } - - // ========================================================================= - // Event Enum Tests - // ========================================================================= - - #[test] - fn test_event_enum_serialization() { - use crate::types::MessageId; - - let event: Event = Event::TextMessageStart(TextMessageStartEvent::new( - MessageId::random(), - )); - - let json = serde_json::to_string(&event).unwrap(); - assert!(json.contains("\"type\":\"TEXT_MESSAGE_START\"")); - assert!(json.contains("\"messageId\"")); - assert!(json.contains("\"role\":\"assistant\"")); - } - - #[test] - fn test_event_enum_deserialization() { - let json = r#"{"type":"RUN_ERROR","message":"Test error"}"#; - let event: Event = serde_json::from_str(json).unwrap(); - - match event { - Event::RunError(e) => assert_eq!(e.message, "Test error"), - _ => panic!("Expected RunError variant"), - } - } - - #[test] - fn test_event_type_method() { - use crate::types::MessageId; - - let event: Event = Event::TextMessageEnd(TextMessageEndEvent::new(MessageId::random())); - assert_eq!(event.event_type(), EventType::TextMessageEnd); - - let event: Event = Event::RunStarted(RunStartedEvent::new( - crate::types::ThreadId::random(), - crate::types::RunId::random(), - )); - assert_eq!(event.event_type(), EventType::RunStarted); - - let event: Event = Event::Custom(CustomEvent::new("test", serde_json::json!({}))); - assert_eq!(event.event_type(), EventType::Custom); - } - - #[test] - fn test_event_timestamp_method() { - use crate::types::MessageId; - - let event: Event = Event::TextMessageStart( - TextMessageStartEvent::new(MessageId::random()) - .with_timestamp(1234567890.0), - ); - assert_eq!(event.timestamp(), Some(1234567890.0)); - - let event: Event = Event::ThinkingEnd(ThinkingEndEvent::new()); - assert_eq!(event.timestamp(), None); - } - - #[test] - fn test_event_all_variants_serialize() { - use crate::types::{Message, MessageId, RunId, ThreadId, ToolCallId}; - - // Test that all event variants can be serialized - let events: Vec = vec![ - Event::TextMessageStart(TextMessageStartEvent::new(MessageId::random())), - Event::TextMessageContent(TextMessageContentEvent::new_unchecked(MessageId::random(), "Hello")), - Event::TextMessageEnd(TextMessageEndEvent::new(MessageId::random())), - Event::TextMessageChunk(TextMessageChunkEvent::new(Role::Assistant).with_delta("Hi")), - Event::ThinkingTextMessageStart(ThinkingTextMessageStartEvent::new()), - Event::ThinkingTextMessageContent(ThinkingTextMessageContentEvent::new("thinking...")), - Event::ThinkingTextMessageEnd(ThinkingTextMessageEndEvent::new()), - Event::ToolCallStart(ToolCallStartEvent::new(ToolCallId::random(), "test_tool")), - Event::ToolCallArgs(ToolCallArgsEvent::new(ToolCallId::random(), "{}")), - Event::ToolCallEnd(ToolCallEndEvent::new(ToolCallId::random())), - Event::ToolCallChunk(ToolCallChunkEvent::new()), - Event::ToolCallResult(ToolCallResultEvent::new(MessageId::random(), ToolCallId::random(), "result")), - Event::ThinkingStart(ThinkingStartEvent::new()), - Event::ThinkingEnd(ThinkingEndEvent::new()), - Event::StateSnapshot(StateSnapshotEvent::new(serde_json::json!({}))), - Event::StateDelta(StateDeltaEvent::new(vec![])), - Event::MessagesSnapshot(MessagesSnapshotEvent::new(vec![Message::Assistant { - id: MessageId::random(), - content: Some("Hi".to_string()), - name: None, - tool_calls: None, - }])), - Event::ActivitySnapshot(ActivitySnapshotEvent::new(MessageId::random(), "PLAN", serde_json::json!({"steps": []}))), - Event::ActivityDelta(ActivityDeltaEvent::new(MessageId::random(), "PLAN", vec![serde_json::json!({"op": "add", "path": "/steps/-", "value": "test"})])), - Event::Raw(RawEvent::new(serde_json::json!({}))), - Event::Custom(CustomEvent::new("test", serde_json::json!({}))), - Event::RunStarted(RunStartedEvent::new(ThreadId::random(), RunId::random())), - Event::RunFinished(RunFinishedEvent::new(ThreadId::random(), RunId::random())), - Event::RunError(RunErrorEvent::new("error")), - Event::StepStarted(StepStartedEvent::new("step")), - Event::StepFinished(StepFinishedEvent::new("step")), - ]; - - for event in events { - let json = serde_json::to_string(&event).unwrap(); - assert!(json.contains("\"type\":")); - - // Roundtrip test - let deserialized: Event = serde_json::from_str(&json).unwrap(); - assert_eq!(event.event_type(), deserialized.event_type()); - } - } -} diff --git a/crates/ag-ui-core/src/lib.rs b/crates/ag-ui-core/src/lib.rs deleted file mode 100644 index 6b9da45b..00000000 --- a/crates/ag-ui-core/src/lib.rs +++ /dev/null @@ -1,64 +0,0 @@ -//! AG-UI Core Types -//! -//! This crate provides the core type definitions for the AG-UI (Agent-User Interaction) -//! protocol. It includes event types, message structures, state management primitives, -//! and error handling for building AG-UI compatible agents. -//! -//! # Overview -//! -//! AG-UI is an event-based protocol that standardizes how AI agents communicate with -//! user-facing applications. This crate provides: -//! -//! - **Event types**: All ~25 AG-UI protocol event types (text messages, tool calls, state, etc.) -//! - **Message types**: Structured message formats for agent-user communication -//! - **State management**: State snapshots and JSON Patch delta operations -//! - **Error handling**: Comprehensive error types for protocol operations -//! -//! # Usage -//! -//! ```rust,ignore -//! use ag_ui_core::{Event, Result}; -//! ``` - -pub mod error; -pub mod event; -pub mod patch; -pub mod state; -pub mod types; - -// Re-export key types for convenience -pub use error::{AgUiError, Result}; - -/// Re-export serde_json::Value for consistent JSON handling across the crate -pub use serde_json::Value as JsonValue; - -// Re-export all types at crate root for convenient access -pub use types::*; - -// Re-export state traits and helpers -pub use state::{diff_states, AgentState, FwdProps, StateManager, TypedStateManager}; - -// Re-export event types -pub use event::{ - // Foundation types - BaseEvent, Event, EventType, EventValidationError, - // Text message events - TextMessageChunkEvent, TextMessageContentEvent, TextMessageEndEvent, TextMessageStartEvent, - // Thinking text message events - ThinkingTextMessageContentEvent, ThinkingTextMessageEndEvent, ThinkingTextMessageStartEvent, - // Tool call events - ToolCallArgsEvent, ToolCallChunkEvent, ToolCallEndEvent, ToolCallResultEvent, - ToolCallStartEvent, - // Thinking step events - ThinkingEndEvent, ThinkingStartEvent, - // State events - MessagesSnapshotEvent, StateDeltaEvent, StateSnapshotEvent, - // Activity events - ActivityDeltaEvent, ActivitySnapshotEvent, - // Special events - CustomEvent, RawEvent, - // Run lifecycle events - InterruptInfo, RunErrorEvent, RunFinishedEvent, RunFinishedOutcome, RunStartedEvent, - // Step events - StepFinishedEvent, StepStartedEvent, -}; diff --git a/crates/ag-ui-core/src/patch.rs b/crates/ag-ui-core/src/patch.rs deleted file mode 100644 index e43a8f64..00000000 --- a/crates/ag-ui-core/src/patch.rs +++ /dev/null @@ -1,622 +0,0 @@ -//! JSON Patch utilities for AG-UI state delta generation. -//! -//! This module provides utilities for working with JSON Patch (RFC 6902) -//! operations, enabling efficient state synchronization between agents and -//! frontends through delta updates. -//! -//! # Overview -//! -//! JSON Patch is a format for describing changes to a JSON document. Instead -//! of sending the entire state on every update, you can send just the changes -//! (patches) which is more efficient for large state objects. -//! -//! # Example -//! -//! ```rust -//! use ag_ui_core::patch::{create_patch, apply_patch}; -//! use serde_json::json; -//! -//! // Create a patch from two states -//! let old_state = json!({"count": 0, "items": []}); -//! let new_state = json!({"count": 1, "items": ["apple"]}); -//! -//! let patch = create_patch(&old_state, &new_state); -//! -//! // Apply patch to recreate the new state -//! let mut state = old_state.clone(); -//! apply_patch(&mut state, &patch).unwrap(); -//! assert_eq!(state, new_state); -//! ``` - -use serde_json::Value as JsonValue; -use std::error::Error; -use std::fmt; - -// Re-export json_patch types for convenience -pub use json_patch::{ - AddOperation, CopyOperation, MoveOperation, Patch, PatchOperation, RemoveOperation, - ReplaceOperation, TestOperation, -}; -use jsonptr::PointerBuf; - -/// Error type for patch operations. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct PatchError { - message: String, -} - -impl PatchError { - /// Creates a new patch error with the given message. - pub fn new(message: impl Into) -> Self { - Self { - message: message.into(), - } - } -} - -impl fmt::Display for PatchError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Patch error: {}", self.message) - } -} - -impl Error for PatchError {} - -impl From for PatchError { - fn from(err: json_patch::PatchError) -> Self { - Self::new(format!("{}", err)) - } -} - -/// Creates a JSON Patch representing the difference between two JSON values. -/// -/// The patch, when applied to `from`, will produce `to`. -/// -/// # Example -/// -/// ```rust -/// use ag_ui_core::patch::create_patch; -/// use serde_json::json; -/// -/// let from = json!({"name": "Alice", "age": 30}); -/// let to = json!({"name": "Alice", "age": 31}); -/// -/// let patch = create_patch(&from, &to); -/// -/// // The patch contains a "replace" operation for the age field -/// assert!(!patch.0.is_empty()); -/// ``` -pub fn create_patch(from: &JsonValue, to: &JsonValue) -> Patch { - json_patch::diff(from, to) -} - -/// Applies a JSON Patch to a JSON value in place. -/// -/// # Errors -/// -/// Returns an error if any patch operation fails (e.g., path doesn't exist -/// for a remove operation, or test operation fails). -/// -/// # Example -/// -/// ```rust -/// use ag_ui_core::patch::{create_patch, apply_patch}; -/// use serde_json::json; -/// -/// let mut state = json!({"count": 0}); -/// let patch = create_patch(&json!({"count": 0}), &json!({"count": 5})); -/// -/// apply_patch(&mut state, &patch).unwrap(); -/// assert_eq!(state["count"], 5); -/// ``` -pub fn apply_patch(target: &mut JsonValue, patch: &Patch) -> Result<(), PatchError> { - json_patch::patch(target, patch.0.as_slice()).map_err(PatchError::from) -} - -/// Applies a JSON Patch from a JSON array representation. -/// -/// This is useful when you receive patches as raw JSON values (e.g., from -/// network events). -/// -/// # Errors -/// -/// Returns an error if the patch is not a valid JSON Patch array or if -/// any operation fails. -/// -/// # Example -/// -/// ```rust -/// use ag_ui_core::patch::apply_patch_from_value; -/// use serde_json::json; -/// -/// let mut state = json!({"count": 0}); -/// let patch_json = json!([ -/// {"op": "replace", "path": "/count", "value": 10} -/// ]); -/// -/// apply_patch_from_value(&mut state, &patch_json).unwrap(); -/// assert_eq!(state["count"], 10); -/// ``` -pub fn apply_patch_from_value(target: &mut JsonValue, patch: &JsonValue) -> Result<(), PatchError> { - let patch: Patch = serde_json::from_value(patch.clone()) - .map_err(|e| PatchError::new(format!("Invalid patch format: {}", e)))?; - apply_patch(target, &patch) -} - -/// Converts a Patch to a JSON value for serialization. -/// -/// This is useful when you need to send patches over the network or -/// store them as JSON. -/// -/// # Example -/// -/// ```rust -/// use ag_ui_core::patch::{create_patch, patch_to_value}; -/// use serde_json::json; -/// -/// let patch = create_patch( -/// &json!({"x": 1}), -/// &json!({"x": 2}), -/// ); -/// -/// let json = patch_to_value(&patch); -/// assert!(json.is_array()); -/// ``` -pub fn patch_to_value(patch: &Patch) -> JsonValue { - serde_json::to_value(patch).unwrap_or(JsonValue::Array(vec![])) -} - -/// Converts a Patch to a vector of JSON values. -/// -/// This is the format expected by StateDeltaEvent and ActivityDeltaEvent. -/// -/// # Example -/// -/// ```rust -/// use ag_ui_core::patch::{create_patch, patch_to_vec}; -/// use serde_json::json; -/// -/// let patch = create_patch( -/// &json!({"items": []}), -/// &json!({"items": ["a"]}), -/// ); -/// -/// let ops = patch_to_vec(&patch); -/// // Each operation is a separate JSON object -/// assert!(!ops.is_empty()); -/// ``` -pub fn patch_to_vec(patch: &Patch) -> Vec { - patch - .0 - .iter() - .filter_map(|op| serde_json::to_value(op).ok()) - .collect() -} - -/// A builder for constructing JSON Patches programmatically. -/// -/// This provides a more ergonomic way to create patches when you know -/// exactly what operations you want to perform. -/// -/// # Example -/// -/// ```rust -/// use ag_ui_core::patch::PatchBuilder; -/// use serde_json::json; -/// -/// let patch = PatchBuilder::new() -/// .add("/name", json!("Alice")) -/// .replace("/age", json!(31)) -/// .remove("/temp") -/// .build(); -/// -/// assert_eq!(patch.0.len(), 3); -/// ``` -#[derive(Debug, Clone, Default)] -pub struct PatchBuilder { - operations: Vec, -} - -impl PatchBuilder { - /// Creates a new empty patch builder. - pub fn new() -> Self { - Self::default() - } - - /// Adds an "add" operation to the patch. - /// - /// The add operation adds a value at the target location. If the target - /// location specifies an array index, the value is inserted at that index. - pub fn add(mut self, path: impl AsRef, value: JsonValue) -> Self { - self.operations.push(PatchOperation::Add(AddOperation { - path: PointerBuf::parse(path.as_ref()).unwrap_or_default(), - value, - })); - self - } - - /// Adds a "remove" operation to the patch. - /// - /// The remove operation removes the value at the target location. - pub fn remove(mut self, path: impl AsRef) -> Self { - self.operations - .push(PatchOperation::Remove(RemoveOperation { - path: PointerBuf::parse(path.as_ref()).unwrap_or_default(), - })); - self - } - - /// Adds a "replace" operation to the patch. - /// - /// The replace operation replaces the value at the target location with - /// the new value. - pub fn replace(mut self, path: impl AsRef, value: JsonValue) -> Self { - self.operations - .push(PatchOperation::Replace(ReplaceOperation { - path: PointerBuf::parse(path.as_ref()).unwrap_or_default(), - value, - })); - self - } - - /// Adds a "move" operation to the patch. - /// - /// The move operation removes the value at a specified location and - /// adds it to the target location. - pub fn move_value(mut self, from: impl AsRef, path: impl AsRef) -> Self { - self.operations.push(PatchOperation::Move(MoveOperation { - from: PointerBuf::parse(from.as_ref()).unwrap_or_default(), - path: PointerBuf::parse(path.as_ref()).unwrap_or_default(), - })); - self - } - - /// Adds a "copy" operation to the patch. - /// - /// The copy operation copies the value at a specified location to the - /// target location. - pub fn copy(mut self, from: impl AsRef, path: impl AsRef) -> Self { - self.operations.push(PatchOperation::Copy(CopyOperation { - from: PointerBuf::parse(from.as_ref()).unwrap_or_default(), - path: PointerBuf::parse(path.as_ref()).unwrap_or_default(), - })); - self - } - - /// Adds a "test" operation to the patch. - /// - /// The test operation tests that a value at the target location is equal - /// to a specified value. If the test fails, the entire patch fails. - pub fn test(mut self, path: impl AsRef, value: JsonValue) -> Self { - self.operations.push(PatchOperation::Test(TestOperation { - path: PointerBuf::parse(path.as_ref()).unwrap_or_default(), - value, - })); - self - } - - /// Builds the patch from the accumulated operations. - pub fn build(self) -> Patch { - Patch(self.operations) - } - - /// Builds the patch and returns it as a vector of JSON values. - /// - /// This is the format expected by StateDeltaEvent and ActivityDeltaEvent. - pub fn build_vec(self) -> Vec { - patch_to_vec(&self.build()) - } -} - -/// Checks if applying a patch would succeed without actually modifying the target. -/// -/// This is useful for validation before committing to a patch operation. -/// -/// # Example -/// -/// ```rust -/// use ag_ui_core::patch::{can_apply_patch, PatchBuilder}; -/// use serde_json::json; -/// -/// let state = json!({"count": 0}); -/// let valid_patch = PatchBuilder::new().replace("/count", json!(1)).build(); -/// let invalid_patch = PatchBuilder::new().remove("/nonexistent").build(); -/// -/// assert!(can_apply_patch(&state, &valid_patch)); -/// assert!(!can_apply_patch(&state, &invalid_patch)); -/// ``` -pub fn can_apply_patch(target: &JsonValue, patch: &Patch) -> bool { - let mut test_target = target.clone(); - apply_patch(&mut test_target, patch).is_ok() -} - -/// Merges two patches into one. -/// -/// The resulting patch applies the operations from the first patch followed -/// by operations from the second patch. -/// -/// Note: This is a simple concatenation and does not optimize or simplify -/// the resulting patch. -/// -/// # Example -/// -/// ```rust -/// use ag_ui_core::patch::{merge_patches, PatchBuilder}; -/// use serde_json::json; -/// -/// let patch1 = PatchBuilder::new().add("/a", json!(1)).build(); -/// let patch2 = PatchBuilder::new().add("/b", json!(2)).build(); -/// -/// let merged = merge_patches(&patch1, &patch2); -/// assert_eq!(merged.0.len(), 2); -/// ``` -pub fn merge_patches(first: &Patch, second: &Patch) -> Patch { - let mut operations = first.0.clone(); - operations.extend(second.0.clone()); - Patch(operations) -} - -#[cfg(test)] -mod tests { - use super::*; - use serde_json::json; - - #[test] - fn test_create_patch_simple() { - let from = json!({"count": 0}); - let to = json!({"count": 5}); - - let patch = create_patch(&from, &to); - assert!(!patch.0.is_empty()); - - // Apply patch and verify - let mut result = from.clone(); - apply_patch(&mut result, &patch).unwrap(); - assert_eq!(result, to); - } - - #[test] - fn test_create_patch_add_field() { - let from = json!({"name": "Alice"}); - let to = json!({"name": "Alice", "age": 30}); - - let patch = create_patch(&from, &to); - - let mut result = from.clone(); - apply_patch(&mut result, &patch).unwrap(); - assert_eq!(result, to); - } - - #[test] - fn test_create_patch_remove_field() { - let from = json!({"name": "Alice", "temp": "value"}); - let to = json!({"name": "Alice"}); - - let patch = create_patch(&from, &to); - - let mut result = from.clone(); - apply_patch(&mut result, &patch).unwrap(); - assert_eq!(result, to); - } - - #[test] - fn test_create_patch_array_operations() { - let from = json!({"items": ["a", "b"]}); - let to = json!({"items": ["a", "b", "c"]}); - - let patch = create_patch(&from, &to); - - let mut result = from.clone(); - apply_patch(&mut result, &patch).unwrap(); - assert_eq!(result, to); - } - - #[test] - fn test_apply_patch_from_value() { - let mut state = json!({"count": 0}); - let patch_json = json!([ - {"op": "replace", "path": "/count", "value": 42} - ]); - - apply_patch_from_value(&mut state, &patch_json).unwrap(); - assert_eq!(state["count"], 42); - } - - #[test] - fn test_apply_patch_from_value_invalid() { - let mut state = json!({"count": 0}); - let invalid_patch = json!("not an array"); - - let result = apply_patch_from_value(&mut state, &invalid_patch); - assert!(result.is_err()); - } - - #[test] - fn test_patch_to_value() { - let patch = create_patch(&json!({"x": 1}), &json!({"x": 2})); - let value = patch_to_value(&patch); - - assert!(value.is_array()); - } - - #[test] - fn test_patch_to_vec() { - let patch = create_patch(&json!({"a": 1, "b": 2}), &json!({"a": 1, "b": 3, "c": 4})); - let ops = patch_to_vec(&patch); - - // Should have operations for changing b and adding c - assert!(!ops.is_empty()); - for op in &ops { - assert!(op.is_object()); - assert!(op.get("op").is_some()); - } - } - - #[test] - fn test_patch_builder_add() { - let patch = PatchBuilder::new() - .add("/name", json!("Alice")) - .build(); - - let mut state = json!({}); - apply_patch(&mut state, &patch).unwrap(); - assert_eq!(state["name"], "Alice"); - } - - #[test] - fn test_patch_builder_replace() { - let patch = PatchBuilder::new() - .replace("/count", json!(10)) - .build(); - - let mut state = json!({"count": 0}); - apply_patch(&mut state, &patch).unwrap(); - assert_eq!(state["count"], 10); - } - - #[test] - fn test_patch_builder_remove() { - let patch = PatchBuilder::new().remove("/temp").build(); - - let mut state = json!({"name": "Alice", "temp": "value"}); - apply_patch(&mut state, &patch).unwrap(); - assert!(state.get("temp").is_none()); - assert_eq!(state["name"], "Alice"); - } - - #[test] - fn test_patch_builder_move() { - let patch = PatchBuilder::new() - .move_value("/old", "/new") - .build(); - - let mut state = json!({"old": "value"}); - apply_patch(&mut state, &patch).unwrap(); - assert!(state.get("old").is_none()); - assert_eq!(state["new"], "value"); - } - - #[test] - fn test_patch_builder_copy() { - let patch = PatchBuilder::new() - .copy("/source", "/dest") - .build(); - - let mut state = json!({"source": "value"}); - apply_patch(&mut state, &patch).unwrap(); - assert_eq!(state["source"], "value"); - assert_eq!(state["dest"], "value"); - } - - #[test] - fn test_patch_builder_test() { - // Test operation succeeds - let patch = PatchBuilder::new() - .test("/count", json!(0)) - .replace("/count", json!(1)) - .build(); - - let mut state = json!({"count": 0}); - apply_patch(&mut state, &patch).unwrap(); - assert_eq!(state["count"], 1); - } - - #[test] - fn test_patch_builder_test_fails() { - let patch = PatchBuilder::new() - .test("/count", json!(999)) // Wrong value - .replace("/count", json!(1)) - .build(); - - let mut state = json!({"count": 0}); - let result = apply_patch(&mut state, &patch); - assert!(result.is_err()); - } - - #[test] - fn test_patch_builder_build_vec() { - let ops = PatchBuilder::new() - .add("/a", json!(1)) - .replace("/b", json!(2)) - .build_vec(); - - assert_eq!(ops.len(), 2); - } - - #[test] - fn test_can_apply_patch() { - let state = json!({"count": 0}); - - let valid_patch = PatchBuilder::new().replace("/count", json!(1)).build(); - assert!(can_apply_patch(&state, &valid_patch)); - - let invalid_patch = PatchBuilder::new().remove("/nonexistent").build(); - assert!(!can_apply_patch(&state, &invalid_patch)); - } - - #[test] - fn test_merge_patches() { - let patch1 = PatchBuilder::new().add("/a", json!(1)).build(); - let patch2 = PatchBuilder::new().add("/b", json!(2)).build(); - - let merged = merge_patches(&patch1, &patch2); - assert_eq!(merged.0.len(), 2); - - let mut state = json!({}); - apply_patch(&mut state, &merged).unwrap(); - assert_eq!(state["a"], 1); - assert_eq!(state["b"], 2); - } - - #[test] - fn test_patch_error_display() { - let err = PatchError::new("test error"); - assert!(err.to_string().contains("test error")); - } - - #[test] - fn test_complex_nested_patch() { - let from = json!({ - "user": { - "profile": { - "name": "Alice", - "settings": { - "theme": "light" - } - } - } - }); - - let to = json!({ - "user": { - "profile": { - "name": "Alice", - "settings": { - "theme": "dark", - "notifications": true - } - } - } - }); - - let patch = create_patch(&from, &to); - - let mut result = from.clone(); - apply_patch(&mut result, &patch).unwrap(); - assert_eq!(result, to); - } - - #[test] - fn test_empty_patch() { - let state = json!({"count": 0}); - let patch = create_patch(&state, &state); - - // Patch of identical values should be empty - assert!(patch.0.is_empty()); - - // Applying empty patch should be no-op - let mut result = state.clone(); - apply_patch(&mut result, &patch).unwrap(); - assert_eq!(result, state); - } -} diff --git a/crates/ag-ui-core/src/state.rs b/crates/ag-ui-core/src/state.rs deleted file mode 100644 index b2df1f01..00000000 --- a/crates/ag-ui-core/src/state.rs +++ /dev/null @@ -1,645 +0,0 @@ -//! AG-UI State Management -//! -//! This module provides state management traits and utilities for AG-UI: -//! - `AgentState`: Marker trait for types that can represent agent state -//! - `FwdProps`: Marker trait for types that can be forwarded as props to UI -//! - `StateManager`: Helper for managing state and generating deltas -//! -//! These traits enable generic state handling in events while ensuring -//! the necessary bounds for serialization and async operations. -//! -//! # State Synchronization -//! -//! AG-UI supports two modes of state synchronization: -//! - **Snapshots**: Send the complete state (simpler but less efficient) -//! - **Deltas**: Send JSON Patch operations (more efficient for large states) -//! -//! The `StateManager` helper makes it easy to track state changes and -//! generate appropriate events. -//! -//! # Example -//! -//! ```rust -//! use ag_ui_core::state::StateManager; -//! use serde_json::json; -//! -//! let mut manager = StateManager::new(json!({"count": 0})); -//! -//! // Update state and get the delta -//! let delta = manager.update(json!({"count": 1})); -//! assert!(delta.is_some()); -//! -//! // Get current state -//! assert_eq!(manager.current()["count"], 1); -//! ``` - -use crate::patch::{create_patch, Patch}; -use serde::{Deserialize, Serialize}; -use serde_json::Value as JsonValue; -use std::fmt::Debug; - -/// Marker trait for types that can represent agent state. -/// -/// Types implementing this trait can be used as the state type in -/// state-related events (StateSnapshot, StateDelta, etc.). -/// -/// # Bounds -/// -/// - `'static`: Required for async operations -/// - `Debug`: For debugging and logging -/// - `Clone`: State may need to be copied -/// - `Send + Sync`: For thread-safe async operations -/// - `Serialize + Deserialize`: For JSON serialization -/// - `Default`: For initializing empty state -/// -/// # Example -/// -/// ```rust -/// use ag_ui_core::AgentState; -/// use serde::{Deserialize, Serialize}; -/// -/// #[derive(Debug, Clone, Default, Serialize, Deserialize)] -/// struct MyState { -/// counter: u32, -/// messages: Vec, -/// } -/// -/// impl AgentState for MyState {} -/// ``` -pub trait AgentState: - 'static + Debug + Clone + Send + Sync + for<'de> Deserialize<'de> + Serialize + Default -{ -} - -/// Marker trait for types that can be forwarded as props to UI components. -/// -/// Types implementing this trait can be passed through the AG-UI protocol -/// to frontend components as properties. -/// -/// # Bounds -/// -/// - `'static`: Required for async operations -/// - `Clone`: Props may need to be copied -/// - `Send + Sync`: For thread-safe async operations -/// - `Serialize + Deserialize`: For JSON serialization -/// - `Default`: For initializing empty props -/// -/// # Example -/// -/// ```rust -/// use ag_ui_core::FwdProps; -/// use serde::{Deserialize, Serialize}; -/// -/// #[derive(Clone, Default, Serialize, Deserialize)] -/// struct MyProps { -/// theme: String, -/// locale: String, -/// } -/// -/// impl FwdProps for MyProps {} -/// ``` -pub trait FwdProps: - 'static + Clone + Send + Sync + for<'de> Deserialize<'de> + Serialize + Default -{ -} - -// Implement AgentState for common types - -impl AgentState for JsonValue {} -impl AgentState for () {} - -// Implement FwdProps for common types - -impl FwdProps for JsonValue {} -impl FwdProps for () {} - -// ============================================================================= -// State Helper Utilities -// ============================================================================= - -/// Computes the difference between two JSON states as a JSON Patch. -/// -/// Returns `None` if the states are identical. -/// -/// # Example -/// -/// ```rust -/// use ag_ui_core::state::diff_states; -/// use serde_json::json; -/// -/// let old = json!({"count": 0}); -/// let new = json!({"count": 5}); -/// -/// let patch = diff_states(&old, &new); -/// assert!(patch.is_some()); -/// ``` -pub fn diff_states(old: &JsonValue, new: &JsonValue) -> Option { - let patch = create_patch(old, new); - if patch.0.is_empty() { - None - } else { - Some(patch) - } -} - -/// A helper for managing state and generating deltas. -/// -/// `StateManager` tracks the current state and provides methods to update -/// it while automatically computing the JSON Patch delta between states. -/// This is useful for efficiently synchronizing state with frontends. -/// -/// # Example -/// -/// ```rust -/// use ag_ui_core::state::StateManager; -/// use serde_json::json; -/// -/// let mut manager = StateManager::new(json!({"count": 0, "items": []})); -/// -/// // Update state - returns delta patch -/// let delta = manager.update(json!({"count": 1, "items": []})); -/// assert!(delta.is_some()); -/// -/// // No change - returns None -/// let delta = manager.update(json!({"count": 1, "items": []})); -/// assert!(delta.is_none()); -/// -/// // Check current state -/// assert_eq!(manager.current()["count"], 1); -/// ``` -#[derive(Debug, Clone)] -pub struct StateManager { - current: JsonValue, - version: u64, -} - -impl StateManager { - /// Creates a new state manager with the given initial state. - pub fn new(initial: JsonValue) -> Self { - Self { - current: initial, - version: 0, - } - } - - /// Returns a reference to the current state. - pub fn current(&self) -> &JsonValue { - &self.current - } - - /// Returns the current state version (increments on each update). - pub fn version(&self) -> u64 { - self.version - } - - /// Updates the state and returns the delta patch if there were changes. - /// - /// Returns `None` if the new state is identical to the current state. - pub fn update(&mut self, new_state: JsonValue) -> Option { - let patch = diff_states(&self.current, &new_state); - if patch.is_some() { - self.current = new_state; - self.version += 1; - } - patch - } - - /// Updates the state using a closure and returns the delta patch. - /// - /// The closure receives a mutable reference to the current state. - /// After the closure completes, the delta is computed. - /// - /// # Example - /// - /// ```rust - /// use ag_ui_core::state::StateManager; - /// use serde_json::json; - /// - /// let mut manager = StateManager::new(json!({"count": 0})); - /// - /// let delta = manager.update_with(|state| { - /// state["count"] = json!(10); - /// }); - /// - /// assert!(delta.is_some()); - /// assert_eq!(manager.current()["count"], 10); - /// ``` - pub fn update_with(&mut self, f: F) -> Option - where - F: FnOnce(&mut JsonValue), - { - let old_state = self.current.clone(); - f(&mut self.current); - let patch = diff_states(&old_state, &self.current); - if patch.is_some() { - self.version += 1; - } - patch - } - - /// Resets the state to a new value without computing a delta. - /// - /// Use this when you want to replace the entire state (e.g., on reconnection) - /// and will send a snapshot instead of a delta. - pub fn reset(&mut self, new_state: JsonValue) { - self.current = new_state; - self.version += 1; - } - - /// Takes a snapshot of the current state. - /// - /// Returns a clone of the current state value. - pub fn snapshot(&self) -> JsonValue { - self.current.clone() - } -} - -impl Default for StateManager { - fn default() -> Self { - Self::new(JsonValue::Object(serde_json::Map::new())) - } -} - -/// A typed state manager for custom state types. -/// -/// This provides the same functionality as `StateManager` but works with -/// strongly-typed state objects that implement `AgentState`. -/// -/// # Example -/// -/// ```rust -/// use ag_ui_core::state::TypedStateManager; -/// use ag_ui_core::AgentState; -/// use serde::{Deserialize, Serialize}; -/// -/// #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] -/// struct AppState { -/// count: u32, -/// user: Option, -/// } -/// -/// impl AgentState for AppState {} -/// -/// let mut manager = TypedStateManager::new(AppState { count: 0, user: None }); -/// -/// let delta = manager.update(AppState { count: 1, user: None }); -/// assert!(delta.is_some()); -/// -/// assert_eq!(manager.current().count, 1); -/// ``` -#[derive(Debug, Clone)] -pub struct TypedStateManager { - current: S, - version: u64, -} - -impl TypedStateManager { - /// Creates a new typed state manager with the given initial state. - pub fn new(initial: S) -> Self { - Self { - current: initial, - version: 0, - } - } - - /// Returns a reference to the current state. - pub fn current(&self) -> &S { - &self.current - } - - /// Returns the current state version (increments on each update). - pub fn version(&self) -> u64 { - self.version - } - - /// Updates the state and returns the delta patch if there were changes. - /// - /// Returns `None` if the new state is identical to the current state. - pub fn update(&mut self, new_state: S) -> Option { - if self.current == new_state { - return None; - } - - let old_json = serde_json::to_value(&self.current).ok()?; - let new_json = serde_json::to_value(&new_state).ok()?; - let patch = diff_states(&old_json, &new_json); - - self.current = new_state; - self.version += 1; - patch - } - - /// Resets the state to a new value without computing a delta. - pub fn reset(&mut self, new_state: S) { - self.current = new_state; - self.version += 1; - } - - /// Takes a snapshot of the current state as JSON. - pub fn snapshot(&self) -> JsonValue { - serde_json::to_value(&self.current).unwrap_or(JsonValue::Null) - } - - /// Returns the current state as a JSON value. - pub fn as_json(&self) -> JsonValue { - serde_json::to_value(&self.current).unwrap_or(JsonValue::Null) - } -} - -impl Default for TypedStateManager { - fn default() -> Self { - Self::new(S::default()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[derive(Debug, Clone, Default, Serialize, Deserialize)] - struct TestState { - value: i32, - } - - impl AgentState for TestState {} - - #[derive(Clone, Default, Serialize, Deserialize)] - struct TestProps { - name: String, - } - - impl FwdProps for TestProps {} - - #[test] - fn test_json_value_implements_agent_state() { - fn requires_agent_state(_: T) {} - requires_agent_state(JsonValue::Null); - } - - #[test] - fn test_unit_implements_agent_state() { - fn requires_agent_state(_: T) {} - requires_agent_state(()); - } - - #[test] - fn test_json_value_implements_fwd_props() { - fn requires_fwd_props(_: T) {} - requires_fwd_props(JsonValue::Null); - } - - #[test] - fn test_unit_implements_fwd_props() { - fn requires_fwd_props(_: T) {} - requires_fwd_props(()); - } - - #[test] - fn test_custom_state_type() { - fn requires_agent_state(_: T) {} - requires_agent_state(TestState { value: 42 }); - } - - #[test] - fn test_custom_props_type() { - fn requires_fwd_props(_: T) {} - requires_fwd_props(TestProps { - name: "test".to_string(), - }); - } - - // ========================================================================= - // State Helper Tests - // ========================================================================= - - #[test] - fn test_diff_states_with_changes() { - use serde_json::json; - - let old = json!({"count": 0}); - let new = json!({"count": 5}); - - let patch = diff_states(&old, &new); - assert!(patch.is_some()); - } - - #[test] - fn test_diff_states_no_changes() { - use serde_json::json; - - let state = json!({"count": 0}); - - let patch = diff_states(&state, &state); - assert!(patch.is_none()); - } - - #[test] - fn test_state_manager_new() { - use serde_json::json; - - let manager = StateManager::new(json!({"count": 0})); - assert_eq!(manager.current()["count"], 0); - assert_eq!(manager.version(), 0); - } - - #[test] - fn test_state_manager_update_with_changes() { - use serde_json::json; - - let mut manager = StateManager::new(json!({"count": 0})); - - let delta = manager.update(json!({"count": 5})); - assert!(delta.is_some()); - assert_eq!(manager.current()["count"], 5); - assert_eq!(manager.version(), 1); - } - - #[test] - fn test_state_manager_update_no_changes() { - use serde_json::json; - - let mut manager = StateManager::new(json!({"count": 0})); - - let delta = manager.update(json!({"count": 0})); - assert!(delta.is_none()); - assert_eq!(manager.version(), 0); // Version shouldn't increment - } - - #[test] - fn test_state_manager_update_with_closure() { - use serde_json::json; - - let mut manager = StateManager::new(json!({"count": 0})); - - let delta = manager.update_with(|state| { - state["count"] = json!(10); - }); - - assert!(delta.is_some()); - assert_eq!(manager.current()["count"], 10); - assert_eq!(manager.version(), 1); - } - - #[test] - fn test_state_manager_update_with_no_changes() { - use serde_json::json; - - let mut manager = StateManager::new(json!({"count": 0})); - - let delta = manager.update_with(|_state| { - // No changes - }); - - assert!(delta.is_none()); - assert_eq!(manager.version(), 0); - } - - #[test] - fn test_state_manager_reset() { - use serde_json::json; - - let mut manager = StateManager::new(json!({"count": 0})); - manager.reset(json!({"count": 100, "new_field": true})); - - assert_eq!(manager.current()["count"], 100); - assert_eq!(manager.current()["new_field"], true); - assert_eq!(manager.version(), 1); - } - - #[test] - fn test_state_manager_snapshot() { - use serde_json::json; - - let manager = StateManager::new(json!({"count": 42})); - let snapshot = manager.snapshot(); - - assert_eq!(snapshot, json!({"count": 42})); - } - - #[test] - fn test_state_manager_default() { - let manager = StateManager::default(); - assert!(manager.current().is_object()); - assert_eq!(manager.version(), 0); - } - - #[test] - fn test_state_manager_multiple_updates() { - use serde_json::json; - - let mut manager = StateManager::new(json!({"count": 0})); - - manager.update(json!({"count": 1})); - manager.update(json!({"count": 2})); - manager.update(json!({"count": 3})); - - assert_eq!(manager.current()["count"], 3); - assert_eq!(manager.version(), 3); - } - - // ========================================================================= - // TypedStateManager Tests - // ========================================================================= - - #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] - struct AppState { - count: u32, - name: String, - } - - impl AgentState for AppState {} - - #[test] - fn test_typed_state_manager_new() { - let manager = TypedStateManager::new(AppState { - count: 0, - name: "test".to_string(), - }); - - assert_eq!(manager.current().count, 0); - assert_eq!(manager.current().name, "test"); - assert_eq!(manager.version(), 0); - } - - #[test] - fn test_typed_state_manager_update() { - let mut manager = TypedStateManager::new(AppState { - count: 0, - name: "test".to_string(), - }); - - let delta = manager.update(AppState { - count: 5, - name: "test".to_string(), - }); - - assert!(delta.is_some()); - assert_eq!(manager.current().count, 5); - assert_eq!(manager.version(), 1); - } - - #[test] - fn test_typed_state_manager_update_no_changes() { - let mut manager = TypedStateManager::new(AppState { - count: 0, - name: "test".to_string(), - }); - - let delta = manager.update(AppState { - count: 0, - name: "test".to_string(), - }); - - assert!(delta.is_none()); - assert_eq!(manager.version(), 0); - } - - #[test] - fn test_typed_state_manager_reset() { - let mut manager = TypedStateManager::new(AppState { - count: 0, - name: "old".to_string(), - }); - - manager.reset(AppState { - count: 100, - name: "new".to_string(), - }); - - assert_eq!(manager.current().count, 100); - assert_eq!(manager.current().name, "new"); - assert_eq!(manager.version(), 1); - } - - #[test] - fn test_typed_state_manager_snapshot() { - let manager = TypedStateManager::new(AppState { - count: 42, - name: "test".to_string(), - }); - - let snapshot = manager.snapshot(); - assert_eq!(snapshot["count"], 42); - assert_eq!(snapshot["name"], "test"); - } - - #[test] - fn test_typed_state_manager_as_json() { - let manager = TypedStateManager::new(AppState { - count: 10, - name: "hello".to_string(), - }); - - let json = manager.as_json(); - assert_eq!(json["count"], 10); - assert_eq!(json["name"], "hello"); - } - - #[test] - fn test_typed_state_manager_default() { - let manager: TypedStateManager = TypedStateManager::default(); - assert_eq!(manager.current().count, 0); - assert_eq!(manager.current().name, ""); - assert_eq!(manager.version(), 0); - } -} diff --git a/crates/ag-ui-core/src/types/content.rs b/crates/ag-ui-core/src/types/content.rs deleted file mode 100644 index e76b9d4e..00000000 --- a/crates/ag-ui-core/src/types/content.rs +++ /dev/null @@ -1,451 +0,0 @@ -//! Content types for AG-UI protocol multimodal messages. -//! -//! This module defines input content types for handling text and binary -//! content in messages, enabling multimodal agent interactions. - -use serde::{Deserialize, Serialize}; -use std::error::Error; -use std::fmt; - -/// Error type for content validation failures. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ContentValidationError { - message: String, -} - -impl ContentValidationError { - /// Creates a new validation error with the given message. - pub fn new(message: impl Into) -> Self { - Self { - message: message.into(), - } - } -} - -impl fmt::Display for ContentValidationError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.message) - } -} - -impl Error for ContentValidationError {} - -/// Text input content for messages. -/// -/// Represents plain text content in a message. -/// -/// # Example -/// -/// ``` -/// use ag_ui_core::TextInputContent; -/// -/// let content = TextInputContent::new("Hello, world!"); -/// assert_eq!(content.text, "Hello, world!"); -/// ``` -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct TextInputContent { - /// The content type discriminator, always "text". - #[serde(rename = "type")] - pub type_tag: String, - /// The text content. - pub text: String, -} - -impl TextInputContent { - /// Creates a new text input content. - pub fn new(text: impl Into) -> Self { - Self { - type_tag: "text".to_string(), - text: text.into(), - } - } -} - -/// Binary input content for multimodal messages. -/// -/// Represents binary content such as images, files, or other media. -/// At least one of `id`, `url`, or `data` must be provided. -/// -/// # Example -/// -/// ``` -/// use ag_ui_core::BinaryInputContent; -/// -/// let content = BinaryInputContent::new("image/png") -/// .with_url("https://example.com/image.png") -/// .with_filename("screenshot.png"); -/// -/// assert_eq!(content.mime_type, "image/png"); -/// assert_eq!(content.url, Some("https://example.com/image.png".to_string())); -/// ``` -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct BinaryInputContent { - /// The content type discriminator, always "binary". - #[serde(rename = "type")] - pub type_tag: String, - /// The MIME type of the binary content. - #[serde(rename = "mimeType")] - pub mime_type: String, - /// Optional identifier for the content. - #[serde(skip_serializing_if = "Option::is_none")] - pub id: Option, - /// Optional URL where the content can be fetched. - #[serde(skip_serializing_if = "Option::is_none")] - pub url: Option, - /// Optional base64-encoded data. - #[serde(skip_serializing_if = "Option::is_none")] - pub data: Option, - /// Optional filename for the content. - #[serde(skip_serializing_if = "Option::is_none")] - pub filename: Option, -} - -impl BinaryInputContent { - /// Creates a new binary input content with the given MIME type. - pub fn new(mime_type: impl Into) -> Self { - Self { - type_tag: "binary".to_string(), - mime_type: mime_type.into(), - id: None, - url: None, - data: None, - filename: None, - } - } - - /// Sets the content identifier. - pub fn with_id(mut self, id: impl Into) -> Self { - self.id = Some(id.into()); - self - } - - /// Sets the content URL. - pub fn with_url(mut self, url: impl Into) -> Self { - self.url = Some(url.into()); - self - } - - /// Sets the base64-encoded data. - pub fn with_data(mut self, data: impl Into) -> Self { - self.data = Some(data.into()); - self - } - - /// Sets the filename. - pub fn with_filename(mut self, filename: impl Into) -> Self { - self.filename = Some(filename.into()); - self - } - - /// Validates that at least one of id, url, or data is present. - pub fn validate(&self) -> Result<(), ContentValidationError> { - if self.id.is_none() && self.url.is_none() && self.data.is_none() { - return Err(ContentValidationError::new( - "BinaryInputContent requires at least one of: id, url, or data", - )); - } - Ok(()) - } -} - -/// Input content union type for multimodal messages. -/// -/// This is a discriminated union that can hold either text or binary content. -/// The `type` field in JSON determines which variant is used. -/// -/// # Example -/// -/// ``` -/// use ag_ui_core::InputContent; -/// -/// // Create text content -/// let text = InputContent::text("Hello!"); -/// assert!(text.is_text()); -/// -/// // Create binary content with URL -/// let binary = InputContent::binary_with_url("image/jpeg", "https://example.com/img.jpg"); -/// assert!(binary.is_binary()); -/// ``` -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(tag = "type", rename_all = "lowercase")] -pub enum InputContent { - /// Text content variant. - Text { - /// The text content. - text: String, - }, - /// Binary content variant for images, files, etc. - Binary { - /// The MIME type of the binary content. - #[serde(rename = "mimeType")] - mime_type: String, - /// Optional identifier for the content. - #[serde(skip_serializing_if = "Option::is_none")] - id: Option, - /// Optional URL where the content can be fetched. - #[serde(skip_serializing_if = "Option::is_none")] - url: Option, - /// Optional base64-encoded data. - #[serde(skip_serializing_if = "Option::is_none")] - data: Option, - /// Optional filename for the content. - #[serde(skip_serializing_if = "Option::is_none")] - filename: Option, - }, -} - -impl InputContent { - /// Creates a text content variant. - pub fn text(text: impl Into) -> Self { - Self::Text { text: text.into() } - } - - /// Creates a minimal binary content variant. - pub fn binary(mime_type: impl Into) -> Self { - Self::Binary { - mime_type: mime_type.into(), - id: None, - url: None, - data: None, - filename: None, - } - } - - /// Creates a binary content variant with a URL. - pub fn binary_with_url(mime_type: impl Into, url: impl Into) -> Self { - Self::Binary { - mime_type: mime_type.into(), - id: None, - url: Some(url.into()), - data: None, - filename: None, - } - } - - /// Creates a binary content variant with base64-encoded data. - pub fn binary_with_data(mime_type: impl Into, data: impl Into) -> Self { - Self::Binary { - mime_type: mime_type.into(), - id: None, - url: None, - data: Some(data.into()), - filename: None, - } - } - - /// Returns true if this is text content. - pub fn is_text(&self) -> bool { - matches!(self, Self::Text { .. }) - } - - /// Returns true if this is binary content. - pub fn is_binary(&self) -> bool { - matches!(self, Self::Binary { .. }) - } - - /// Returns the text content if this is a text variant. - pub fn as_text(&self) -> Option<&str> { - match self { - Self::Text { text } => Some(text), - Self::Binary { .. } => None, - } - } - - /// Validates the content. - /// - /// For text content, always succeeds. - /// For binary content, validates that at least one of id, url, or data is present. - pub fn validate(&self) -> Result<(), ContentValidationError> { - match self { - Self::Text { .. } => Ok(()), - Self::Binary { - id, url, data, .. - } => { - if id.is_none() && url.is_none() && data.is_none() { - Err(ContentValidationError::new( - "Binary content requires at least one of: id, url, or data", - )) - } else { - Ok(()) - } - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - // Test 1: TextInputContent serialization - #[test] - fn test_text_input_content_serialization() { - let content = TextInputContent::new("Hello, world!"); - let json = serde_json::to_string(&content).unwrap(); - - assert!(json.contains("\"type\":\"text\"")); - assert!(json.contains("\"text\":\"Hello, world!\"")); - } - - // Test 2: TextInputContent deserialization - #[test] - fn test_text_input_content_deserialization() { - let json = r#"{"type":"text","text":"Hello!"}"#; - let content: TextInputContent = serde_json::from_str(json).unwrap(); - - assert_eq!(content.type_tag, "text"); - assert_eq!(content.text, "Hello!"); - } - - // Test 3: BinaryInputContent serialization - #[test] - fn test_binary_input_content_serialization() { - let content = BinaryInputContent::new("image/png") - .with_url("https://example.com/img.png") - .with_filename("test.png"); - - let json = serde_json::to_string(&content).unwrap(); - - assert!(json.contains("\"type\":\"binary\"")); - assert!(json.contains("\"mimeType\":\"image/png\"")); - assert!(json.contains("\"url\":\"https://example.com/img.png\"")); - assert!(json.contains("\"filename\":\"test.png\"")); - // Optional fields should be omitted when None - assert!(!json.contains("\"id\"")); - assert!(!json.contains("\"data\"")); - } - - // Test 4: BinaryInputContent builder pattern - #[test] - fn test_binary_input_content_builder() { - let content = BinaryInputContent::new("application/pdf") - .with_id("file-123") - .with_url("https://example.com/doc.pdf") - .with_data("base64data") - .with_filename("document.pdf"); - - assert_eq!(content.mime_type, "application/pdf"); - assert_eq!(content.id, Some("file-123".to_string())); - assert_eq!(content.url, Some("https://example.com/doc.pdf".to_string())); - assert_eq!(content.data, Some("base64data".to_string())); - assert_eq!(content.filename, Some("document.pdf".to_string())); - } - - // Test 5: InputContent text variant - #[test] - fn test_input_content_text_variant() { - let content = InputContent::text("Hello!"); - - assert!(content.is_text()); - assert!(!content.is_binary()); - assert_eq!(content.as_text(), Some("Hello!")); - } - - // Test 6: InputContent binary variant - #[test] - fn test_input_content_binary_variant() { - let content = InputContent::binary_with_url("image/jpeg", "https://example.com/img.jpg"); - - assert!(!content.is_text()); - assert!(content.is_binary()); - assert_eq!(content.as_text(), None); - } - - // Test 7: InputContent discriminated union serialization - #[test] - fn test_input_content_discriminated_union() { - // Text variant - let text = InputContent::text("Hello"); - let text_json = serde_json::to_string(&text).unwrap(); - assert!(text_json.contains("\"type\":\"text\"")); - - // Binary variant - let binary = InputContent::binary_with_url("image/png", "https://example.com/img.png"); - let binary_json = serde_json::to_string(&binary).unwrap(); - assert!(binary_json.contains("\"type\":\"binary\"")); - - // Deserialize text - let parsed_text: InputContent = serde_json::from_str(&text_json).unwrap(); - assert!(parsed_text.is_text()); - - // Deserialize binary - let parsed_binary: InputContent = serde_json::from_str(&binary_json).unwrap(); - assert!(parsed_binary.is_binary()); - } - - // Test 8: Binary validation success - #[test] - fn test_binary_validation_success() { - let with_url = BinaryInputContent::new("image/png").with_url("https://example.com/img.png"); - assert!(with_url.validate().is_ok()); - - let with_data = BinaryInputContent::new("image/png").with_data("base64data"); - assert!(with_data.validate().is_ok()); - - let with_id = BinaryInputContent::new("image/png").with_id("file-123"); - assert!(with_id.validate().is_ok()); - } - - // Test 9: Binary validation failure - #[test] - fn test_binary_validation_failure() { - let empty = BinaryInputContent::new("image/png"); - let result = empty.validate(); - - assert!(result.is_err()); - let err = result.unwrap_err(); - assert!(err.to_string().contains("at least one of")); - } - - // Test 10: InputContent roundtrip - #[test] - fn test_input_content_roundtrip() { - // Text roundtrip - let text = InputContent::text("Hello, world!"); - let text_json = serde_json::to_string(&text).unwrap(); - let text_parsed: InputContent = serde_json::from_str(&text_json).unwrap(); - assert_eq!(text, text_parsed); - - // Binary roundtrip - let binary = InputContent::Binary { - mime_type: "image/png".to_string(), - id: Some("img-123".to_string()), - url: Some("https://example.com/img.png".to_string()), - data: Some("iVBORw0KGgo=".to_string()), - filename: Some("screenshot.png".to_string()), - }; - let binary_json = serde_json::to_string(&binary).unwrap(); - let binary_parsed: InputContent = serde_json::from_str(&binary_json).unwrap(); - assert_eq!(binary, binary_parsed); - } - - // Test 11: InputContent validation - #[test] - fn test_input_content_validation() { - // Text always valid - let text = InputContent::text("Hello"); - assert!(text.validate().is_ok()); - - // Binary with url is valid - let binary_valid = InputContent::binary_with_url("image/png", "https://example.com/img.png"); - assert!(binary_valid.validate().is_ok()); - - // Binary without id/url/data is invalid - let binary_invalid = InputContent::binary("image/png"); - assert!(binary_invalid.validate().is_err()); - } - - // Test 12: BinaryInputContent deserialization - #[test] - fn test_binary_input_content_deserialization() { - let json = r#"{"type":"binary","mimeType":"image/jpeg","url":"https://example.com/img.jpg"}"#; - let content: BinaryInputContent = serde_json::from_str(json).unwrap(); - - assert_eq!(content.type_tag, "binary"); - assert_eq!(content.mime_type, "image/jpeg"); - assert_eq!(content.url, Some("https://example.com/img.jpg".to_string())); - assert_eq!(content.id, None); - assert_eq!(content.data, None); - assert_eq!(content.filename, None); - } -} diff --git a/crates/ag-ui-core/src/types/ids.rs b/crates/ag-ui-core/src/types/ids.rs deleted file mode 100644 index 7cd14812..00000000 --- a/crates/ag-ui-core/src/types/ids.rs +++ /dev/null @@ -1,156 +0,0 @@ -//! ID types for the AG-UI protocol. -//! -//! This module provides strongly-typed ID newtypes to prevent mixing up -//! different ID types (e.g., passing a MessageId where a ThreadId is expected). - -use serde::{Deserialize, Serialize}; -use std::ops::Deref; -use uuid::Uuid; - -/// Macro to define a newtype ID based on Uuid. -macro_rules! define_id_type { - // This arm of the macro handles calls that don't specify extra derives. - ($name:ident) => { - define_id_type!($name,); - }; - // This arm handles calls that do specify extra derives (like Eq). - ($name:ident, $($extra_derive:ident),*) => { - #[doc = concat!(stringify!($name), ": A newtype used to prevent mixing it with other ID values.")] - #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Eq, Hash, $($extra_derive),*)] - pub struct $name(Uuid); - - impl $name { - /// Creates a new random ID. - pub fn random() -> Self { - Self(Uuid::new_v4()) - } - } - - /// Allows creating an ID from a Uuid. - impl From for $name { - fn from(uuid: Uuid) -> Self { - Self(uuid) - } - } - - /// Allows converting an ID back into a Uuid. - impl From<$name> for Uuid { - fn from(id: $name) -> Self { - id.0 - } - } - - /// Allows getting a reference to the inner Uuid. - impl AsRef for $name { - fn as_ref(&self) -> &Uuid { - &self.0 - } - } - - /// Allows printing the ID. - impl std::fmt::Display for $name { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } - } - - /// Allows parsing an ID from a string slice. - impl std::str::FromStr for $name { - type Err = uuid::Error; - - fn from_str(s: &str) -> Result { - Ok(Self(Uuid::parse_str(s)?)) - } - } - - /// Allows comparing the ID with a Uuid. - impl PartialEq for $name { - fn eq(&self, other: &Uuid) -> bool { - self.0 == *other - } - } - - /// Allows comparing the ID with a string slice. - impl PartialEq for $name { - fn eq(&self, other: &str) -> bool { - if let Ok(uuid) = Uuid::parse_str(other) { - self.0 == uuid - } else { - false - } - } - } - }; -} - -// Define UUID-based ID types using the macro -define_id_type!(AgentId); -define_id_type!(ThreadId); -define_id_type!(RunId); -define_id_type!(MessageId); - -/// A tool call ID. -/// -/// Used by some providers to denote a specific ID for a tool call generation, -/// where the result of the tool call must also use this ID. -/// -/// Does not follow UUID format, instead uses "call_xxxxxxxx" format. -#[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone)] -pub struct ToolCallId(String); - -impl ToolCallId { - /// Creates a new random tool call ID in the format "call_xxxxxxxx". - pub fn random() -> Self { - let uuid = &Uuid::new_v4().to_string()[..8]; - let id = format!("call_{uuid}"); - Self(id) - } -} - -impl Deref for ToolCallId { - type Target = str; - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl> From for ToolCallId { - fn from(s: S) -> Self { - Self(s.into()) - } -} - -impl std::fmt::Display for ToolCallId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Test whether tool call ID has the expected format - #[test] - fn test_tool_call_random() { - let id = ToolCallId::random(); - assert_eq!(id.0.len(), 5 + 8); // "call_" + 8 hex chars - assert!(id.0.starts_with("call_")); - } - - /// Test UUID-based ID creation and conversion - #[test] - fn test_message_id_random() { - let id = MessageId::random(); - let uuid: Uuid = id.clone().into(); - assert_eq!(id, uuid); - } - - /// Test ID parsing from string - #[test] - fn test_id_from_str() { - let uuid_str = "550e8400-e29b-41d4-a716-446655440000"; - let id: MessageId = uuid_str.parse().unwrap(); - assert_eq!(id, *uuid_str); // Dereference &str to str for PartialEq - } -} diff --git a/crates/ag-ui-core/src/types/input.rs b/crates/ag-ui-core/src/types/input.rs deleted file mode 100644 index 64593c39..00000000 --- a/crates/ag-ui-core/src/types/input.rs +++ /dev/null @@ -1,289 +0,0 @@ -//! Input types for AG-UI protocol requests. -//! -//! This module defines types for handling client requests to AG-UI agents, -//! including the main `RunAgentInput` request type and supporting types. - -use crate::types::ids::{RunId, ThreadId}; -use crate::types::message::Message; -use crate::types::tool::Tool; -use serde::{Deserialize, Serialize}; -use serde_json::Value as JsonValue; - -/// Context information provided to an agent. -/// -/// Context items provide additional information to help the agent -/// understand the user's request or environment. -/// -/// # Example -/// -/// ``` -/// use ag_ui_core::Context; -/// -/// let ctx = Context::new( -/// "current_page".to_string(), -/// "https://example.com/dashboard".to_string(), -/// ); -/// -/// assert_eq!(ctx.description, "current_page"); -/// assert_eq!(ctx.value, "https://example.com/dashboard"); -/// ``` -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct Context { - /// A description of what this context represents. - pub description: String, - /// The value of the context. - pub value: String, -} - -impl Context { - /// Creates a new context with the given description and value. - pub fn new(description: String, value: String) -> Self { - Self { description, value } - } -} - -/// Input for running an agent. -/// -/// This is the primary request type sent by clients to start or continue -/// an agent run. It contains the thread and run identifiers, conversation -/// messages, available tools, context, and any custom state. -/// -/// # Example -/// -/// ``` -/// use ag_ui_core::{RunAgentInput, Context, Message, ThreadId, RunId}; -/// -/// let input = RunAgentInput::new(ThreadId::random(), RunId::random()) -/// .with_messages(vec![Message::new_user("Hello!")]) -/// .with_context(vec![ -/// Context::new("timezone".to_string(), "UTC".to_string()), -/// ]); -/// -/// assert!(input.messages.len() == 1); -/// assert!(input.context.len() == 1); -/// ``` -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct RunAgentInput { - /// The thread identifier for this conversation. - #[serde(rename = "threadId")] - pub thread_id: ThreadId, - - /// The run identifier for this specific agent invocation. - #[serde(rename = "runId")] - pub run_id: RunId, - - /// Optional parent run ID for nested or sub-agent runs. - #[serde(rename = "parentRunId", skip_serializing_if = "Option::is_none")] - pub parent_run_id: Option, - - /// The current state, can be any JSON value. - pub state: JsonValue, - - /// The conversation messages. - pub messages: Vec, - - /// The tools available to the agent. - pub tools: Vec, - - /// Additional context provided to the agent. - pub context: Vec, - - /// Forwarded properties from the client. - #[serde(rename = "forwardedProps")] - pub forwarded_props: JsonValue, -} - -impl RunAgentInput { - /// Creates a new RunAgentInput with the given thread and run IDs. - /// - /// Initializes with empty messages, tools, context, null state, - /// and null forwarded props. - pub fn new(thread_id: impl Into, run_id: impl Into) -> Self { - Self { - thread_id: thread_id.into(), - run_id: run_id.into(), - parent_run_id: None, - state: JsonValue::Null, - messages: Vec::new(), - tools: Vec::new(), - context: Vec::new(), - forwarded_props: JsonValue::Null, - } - } - - /// Sets the parent run ID for nested runs. - pub fn with_parent_run_id(mut self, parent_id: impl Into) -> Self { - self.parent_run_id = Some(parent_id.into()); - self - } - - /// Sets the state. - pub fn with_state(mut self, state: JsonValue) -> Self { - self.state = state; - self - } - - /// Sets the messages. - pub fn with_messages(mut self, messages: Vec) -> Self { - self.messages = messages; - self - } - - /// Sets the available tools. - pub fn with_tools(mut self, tools: Vec) -> Self { - self.tools = tools; - self - } - - /// Sets the context items. - pub fn with_context(mut self, context: Vec) -> Self { - self.context = context; - self - } - - /// Sets the forwarded props. - pub fn with_forwarded_props(mut self, props: JsonValue) -> Self { - self.forwarded_props = props; - self - } -} - -#[cfg(test)] -mod tests { - use super::*; - use serde_json::json; - - #[test] - fn test_context_serialization() { - let ctx = Context::new("current_page".to_string(), "/dashboard".to_string()); - let json = serde_json::to_string(&ctx).unwrap(); - - assert!(json.contains("\"description\":\"current_page\"")); - assert!(json.contains("\"value\":\"/dashboard\"")); - } - - #[test] - fn test_context_deserialization() { - let json = r#"{"description":"timezone","value":"UTC"}"#; - let ctx: Context = serde_json::from_str(json).unwrap(); - - assert_eq!(ctx.description, "timezone"); - assert_eq!(ctx.value, "UTC"); - } - - #[test] - fn test_run_agent_input_minimal() { - let thread_id = ThreadId::random(); - let run_id = RunId::random(); - let input = RunAgentInput::new(thread_id.clone(), run_id.clone()); - - assert_eq!(input.thread_id, thread_id); - assert_eq!(input.run_id, run_id); - assert!(input.parent_run_id.is_none()); - assert_eq!(input.state, JsonValue::Null); - assert!(input.messages.is_empty()); - assert!(input.tools.is_empty()); - assert!(input.context.is_empty()); - assert_eq!(input.forwarded_props, JsonValue::Null); - } - - #[test] - fn test_run_agent_input_full() { - let thread_id = ThreadId::random(); - let run_id = RunId::random(); - let parent_id = RunId::random(); - - let input = RunAgentInput::new(thread_id.clone(), run_id.clone()) - .with_parent_run_id(parent_id.clone()) - .with_state(json!({"count": 42})) - .with_messages(vec![Message::new_user("Hello")]) - .with_tools(vec![Tool::new( - "get_weather".to_string(), - "Get weather".to_string(), - json!({"type": "object"}), - )]) - .with_context(vec![Context::new("tz".to_string(), "UTC".to_string())]) - .with_forwarded_props(json!({"custom": true})); - - assert_eq!(input.thread_id, thread_id); - assert_eq!(input.run_id, run_id); - assert_eq!(input.parent_run_id, Some(parent_id)); - assert_eq!(input.state, json!({"count": 42})); - assert_eq!(input.messages.len(), 1); - assert_eq!(input.tools.len(), 1); - assert_eq!(input.context.len(), 1); - assert_eq!(input.forwarded_props, json!({"custom": true})); - } - - #[test] - fn test_run_agent_input_builder() { - let input = RunAgentInput::new(ThreadId::random(), RunId::random()) - .with_state(json!(null)) - .with_messages(vec![]) - .with_tools(vec![]) - .with_context(vec![]) - .with_forwarded_props(json!({})); - - assert_eq!(input.state, JsonValue::Null); - assert!(input.messages.is_empty()); - assert_eq!(input.forwarded_props, json!({})); - } - - #[test] - fn test_run_agent_input_serialization() { - let thread_id = ThreadId::random(); - let run_id = RunId::random(); - let input = RunAgentInput::new(thread_id, run_id); - - let json = serde_json::to_string(&input).unwrap(); - - // Check camelCase field names - assert!(json.contains("\"threadId\"")); - assert!(json.contains("\"runId\"")); - assert!(json.contains("\"forwardedProps\"")); - // parentRunId should be skipped when None - assert!(!json.contains("\"parentRunId\"")); - } - - #[test] - fn test_run_agent_input_serialization_with_parent() { - let input = RunAgentInput::new(ThreadId::random(), RunId::random()) - .with_parent_run_id(RunId::random()); - - let json = serde_json::to_string(&input).unwrap(); - - // parentRunId should be present when Some - assert!(json.contains("\"parentRunId\"")); - } - - #[test] - fn test_run_agent_input_roundtrip() { - let thread_id = ThreadId::random(); - let run_id = RunId::random(); - let parent_id = RunId::random(); - - let original = RunAgentInput::new(thread_id, run_id) - .with_parent_run_id(parent_id) - .with_state(json!({"nested": {"value": 123}})) - .with_messages(vec![ - Message::new_user("Hello"), - Message::new_assistant("Hi there!"), - ]) - .with_context(vec![ - Context::new("key1".to_string(), "value1".to_string()), - Context::new("key2".to_string(), "value2".to_string()), - ]) - .with_forwarded_props(json!({"prop": "value"})); - - let json = serde_json::to_string(&original).unwrap(); - let deserialized: RunAgentInput = serde_json::from_str(&json).unwrap(); - - assert_eq!(original.thread_id, deserialized.thread_id); - assert_eq!(original.run_id, deserialized.run_id); - assert_eq!(original.parent_run_id, deserialized.parent_run_id); - assert_eq!(original.state, deserialized.state); - assert_eq!(original.messages.len(), deserialized.messages.len()); - assert_eq!(original.context.len(), deserialized.context.len()); - assert_eq!(original.forwarded_props, deserialized.forwarded_props); - } -} diff --git a/crates/ag-ui-core/src/types/message.rs b/crates/ag-ui-core/src/types/message.rs deleted file mode 100644 index 5cbdefd8..00000000 --- a/crates/ag-ui-core/src/types/message.rs +++ /dev/null @@ -1,714 +0,0 @@ -//! Message types for the AG-UI protocol. -//! -//! This module defines message structures for agent-user communication, -//! including role definitions and various message type variants. - -use crate::types::ids::{MessageId, ToolCallId}; -use crate::types::tool::ToolCall; -use serde::{Deserialize, Serialize}; -use serde_json::Value as JsonValue; - -/// A generated function call from a model. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct FunctionCall { - /// The name of the function to call. - pub name: String, - /// The arguments to pass to the function (JSON-encoded string). - pub arguments: String, -} - -/// Message role indicating the sender type. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum Role { - /// Developer messages, typically for debugging. - Developer, - /// System messages, usually containing system prompts. - System, - /// Assistant messages from the AI model. - Assistant, - /// User messages from the human user. - User, - /// Tool messages containing tool/function call results. - Tool, - /// Activity messages for tracking agent activities. - Activity, -} - -// Utility methods for serde defaults -impl Role { - pub(crate) fn developer() -> Self { - Self::Developer - } - pub(crate) fn system() -> Self { - Self::System - } - pub(crate) fn assistant() -> Self { - Self::Assistant - } - pub(crate) fn user() -> Self { - Self::User - } - pub(crate) fn tool() -> Self { - Self::Tool - } - pub(crate) fn activity() -> Self { - Self::Activity - } -} - -/// A basic message with optional string content. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct BaseMessage { - /// Unique identifier for this message. - pub id: MessageId, - /// The role of the message sender. - pub role: Role, - /// The text content of the message. - #[serde(skip_serializing_if = "Option::is_none")] - pub content: Option, - /// Optional name for the sender. - #[serde(skip_serializing_if = "Option::is_none")] - pub name: Option, -} - -/// A developer message, typically for debugging purposes. -/// Not to be confused with system messages. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct DeveloperMessage { - /// Unique identifier for this message. - pub id: MessageId, - /// The role (always Developer). - #[serde(default = "Role::developer")] - pub role: Role, - /// The text content of the message. - pub content: String, - /// Optional name for the sender. - #[serde(skip_serializing_if = "Option::is_none")] - pub name: Option, -} - -impl DeveloperMessage { - /// Creates a new developer message with the given ID and content. - pub fn new(id: impl Into, content: String) -> Self { - Self { - id: id.into(), - role: Role::Developer, - content, - name: None, - } - } - - /// Sets the name for this message. - pub fn with_name(mut self, name: String) -> Self { - self.name = Some(name); - self - } -} - -/// A system message, usually containing the system prompt. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SystemMessage { - /// Unique identifier for this message. - pub id: MessageId, - /// The role (always System). - #[serde(default = "Role::system")] - pub role: Role, - /// The text content of the message. - pub content: String, - /// Optional name for the sender. - #[serde(skip_serializing_if = "Option::is_none")] - pub name: Option, -} - -impl SystemMessage { - /// Creates a new system message with the given ID and content. - pub fn new(id: impl Into, content: String) -> Self { - Self { - id: id.into(), - role: Role::System, - content, - name: None, - } - } - - /// Sets the name for this message. - pub fn with_name(mut self, name: String) -> Self { - self.name = Some(name); - self - } -} - -/// An assistant message (from the AI model). -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct AssistantMessage { - /// Unique identifier for this message. - pub id: MessageId, - /// The role (always Assistant). - #[serde(default = "Role::assistant")] - pub role: Role, - /// The text content of the message. - #[serde(skip_serializing_if = "Option::is_none")] - pub content: Option, - /// Optional name for the sender. - #[serde(skip_serializing_if = "Option::is_none")] - pub name: Option, - /// Tool calls made by the assistant. - #[serde(rename = "toolCalls", skip_serializing_if = "Option::is_none")] - pub tool_calls: Option>, -} - -impl AssistantMessage { - /// Creates a new assistant message with the given ID. - pub fn new(id: impl Into) -> Self { - Self { - id: id.into(), - role: Role::Assistant, - content: None, - name: None, - tool_calls: None, - } - } - - /// Sets the content for this message. - pub fn with_content(mut self, content: String) -> Self { - self.content = Some(content); - self - } - - /// Sets the name for this message. - pub fn with_name(mut self, name: String) -> Self { - self.name = Some(name); - self - } - - /// Sets the tool calls for this message. - pub fn with_tool_calls(mut self, tool_calls: Vec) -> Self { - self.tool_calls = Some(tool_calls); - self - } -} - -/// A user message from the human user. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct UserMessage { - /// Unique identifier for this message. - pub id: MessageId, - /// The role (always User). - #[serde(default = "Role::user")] - pub role: Role, - /// The text content of the message. - pub content: String, - /// Optional name for the sender. - #[serde(skip_serializing_if = "Option::is_none")] - pub name: Option, -} - -impl UserMessage { - /// Creates a new user message with the given ID and content. - pub fn new(id: impl Into, content: String) -> Self { - Self { - id: id.into(), - role: Role::User, - content, - name: None, - } - } - - /// Sets the name for this message. - pub fn with_name(mut self, name: String) -> Self { - self.name = Some(name); - self - } -} - -/// A tool message containing the result of a tool/function call. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct ToolMessage { - /// Unique identifier for this message. - pub id: MessageId, - /// The text content (tool result). - pub content: String, - /// The role (always Tool). - #[serde(default = "Role::tool")] - pub role: Role, - /// The ID of the tool call this result corresponds to. - #[serde(rename = "toolCallId")] - pub tool_call_id: ToolCallId, - /// Optional error message if the tool call failed. - #[serde(skip_serializing_if = "Option::is_none")] - pub error: Option, -} - -impl ToolMessage { - /// Creates a new tool message with the given ID, content, and tool call ID. - pub fn new( - id: impl Into, - content: String, - tool_call_id: impl Into, - ) -> Self { - Self { - id: id.into(), - content, - role: Role::Tool, - tool_call_id: tool_call_id.into(), - error: None, - } - } - - /// Sets the error for this message. - pub fn with_error(mut self, error: String) -> Self { - self.error = Some(error); - self - } -} - -/// An activity message for tracking agent activities. -/// -/// Activity messages represent structured agent activities like planning, -/// research, or other non-text operations. The content is a flexible JSON -/// object that can hold activity-specific data. -/// -/// # Example -/// -/// ``` -/// use ag_ui_core::{ActivityMessage, MessageId}; -/// use serde_json::json; -/// -/// let activity = ActivityMessage::new( -/// MessageId::random(), -/// "PLAN".to_string(), -/// json!({"steps": ["research", "implement", "test"]}), -/// ); -/// -/// assert_eq!(activity.activity_type, "PLAN"); -/// ``` -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct ActivityMessage { - /// Unique identifier for this message. - pub id: MessageId, - /// The role (always Activity). - #[serde(default = "Role::activity")] - pub role: Role, - /// The type of activity (e.g., "PLAN", "RESEARCH"). - #[serde(rename = "activityType")] - pub activity_type: String, - /// The activity content as a flexible JSON object. - pub content: JsonValue, -} - -impl ActivityMessage { - /// Creates a new activity message with the given ID, type, and content. - pub fn new( - id: impl Into, - activity_type: impl Into, - content: JsonValue, - ) -> Self { - Self { - id: id.into(), - role: Role::Activity, - activity_type: activity_type.into(), - content, - } - } - - /// Sets the content for this activity message. - pub fn with_content(mut self, content: JsonValue) -> Self { - self.content = content; - self - } -} - -/// Represents the different types of messages in a conversation. -/// -/// This enum provides a unified type for all message variants, using the -/// role field as the discriminant for JSON serialization. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(tag = "role", rename_all = "lowercase")] -pub enum Message { - /// A developer message for debugging. - Developer { - id: MessageId, - content: String, - #[serde(skip_serializing_if = "Option::is_none")] - name: Option, - }, - /// A system message (usually the system prompt). - System { - id: MessageId, - content: String, - #[serde(skip_serializing_if = "Option::is_none")] - name: Option, - }, - /// An assistant message from the AI model. - Assistant { - id: MessageId, - #[serde(skip_serializing_if = "Option::is_none")] - content: Option, - #[serde(skip_serializing_if = "Option::is_none")] - name: Option, - #[serde(rename = "toolCalls", skip_serializing_if = "Option::is_none")] - tool_calls: Option>, - }, - /// A user message from the human user. - User { - id: MessageId, - content: String, - #[serde(skip_serializing_if = "Option::is_none")] - name: Option, - }, - /// A tool message containing tool call results. - Tool { - id: MessageId, - content: String, - #[serde(rename = "toolCallId")] - tool_call_id: ToolCallId, - #[serde(skip_serializing_if = "Option::is_none")] - error: Option, - }, - /// An activity message for tracking agent activities. - Activity { - id: MessageId, - #[serde(rename = "activityType")] - activity_type: String, - content: JsonValue, - }, -} - -impl Message { - /// Creates a new message with the given role, ID, and content. - pub fn new>(role: Role, id: impl Into, content: S) -> Self { - match role { - Role::Developer => Self::Developer { - id: id.into(), - content: content.as_ref().to_string(), - name: None, - }, - Role::System => Self::System { - id: id.into(), - content: content.as_ref().to_string(), - name: None, - }, - Role::Assistant => Self::Assistant { - id: id.into(), - content: Some(content.as_ref().to_string()), - name: None, - tool_calls: None, - }, - Role::User => Self::User { - id: id.into(), - content: content.as_ref().to_string(), - name: None, - }, - Role::Tool => Self::Tool { - id: id.into(), - content: content.as_ref().to_string(), - tool_call_id: ToolCallId::random(), - error: None, - }, - Role::Activity => Self::Activity { - id: id.into(), - activity_type: "custom".to_string(), - content: JsonValue::String(content.as_ref().to_string()), - }, - } - } - - /// Creates a new user message with a random ID. - pub fn new_user>(content: S) -> Self { - Self::new(Role::User, MessageId::random(), content) - } - - /// Creates a new tool message with a random ID. - pub fn new_tool>(content: S) -> Self { - Self::new(Role::Tool, MessageId::random(), content) - } - - /// Creates a new system message with a random ID. - pub fn new_system>(content: S) -> Self { - Self::new(Role::System, MessageId::random(), content) - } - - /// Creates a new assistant message with a random ID. - pub fn new_assistant>(content: S) -> Self { - Self::new(Role::Assistant, MessageId::random(), content) - } - - /// Creates a new developer message with a random ID. - pub fn new_developer>(content: S) -> Self { - Self::new(Role::Developer, MessageId::random(), content) - } - - /// Creates a new activity message with a random ID. - pub fn new_activity(activity_type: impl Into, content: JsonValue) -> Self { - Self::Activity { - id: MessageId::random(), - activity_type: activity_type.into(), - content, - } - } - - /// Returns a reference to the message ID. - pub fn id(&self) -> &MessageId { - match self { - Message::Developer { id, .. } => id, - Message::System { id, .. } => id, - Message::Assistant { id, .. } => id, - Message::User { id, .. } => id, - Message::Tool { id, .. } => id, - Message::Activity { id, .. } => id, - } - } - - /// Returns a mutable reference to the message ID. - pub fn id_mut(&mut self) -> &mut MessageId { - match self { - Message::Developer { id, .. } => id, - Message::System { id, .. } => id, - Message::Assistant { id, .. } => id, - Message::User { id, .. } => id, - Message::Tool { id, .. } => id, - Message::Activity { id, .. } => id, - } - } - - /// Returns the role of this message. - pub fn role(&self) -> Role { - match self { - Message::Developer { .. } => Role::Developer, - Message::System { .. } => Role::System, - Message::Assistant { .. } => Role::Assistant, - Message::User { .. } => Role::User, - Message::Tool { .. } => Role::Tool, - Message::Activity { .. } => Role::Activity, - } - } - - /// Returns the content of this message, if any. - /// - /// Note: Activity messages have JSON content, not string content. - /// Use `activity_content()` to access their content. - pub fn content(&self) -> Option<&str> { - match self { - Message::Developer { content, .. } => Some(content), - Message::System { content, .. } => Some(content), - Message::User { content, .. } => Some(content), - Message::Tool { content, .. } => Some(content), - Message::Assistant { content, .. } => content.as_deref(), - Message::Activity { .. } => None, - } - } - - /// Returns a mutable reference to the content of this message. - /// - /// Note: Activity messages have JSON content, not string content. - /// Use `activity_content_mut()` to modify their content. - pub fn content_mut(&mut self) -> Option<&mut String> { - match self { - Message::Developer { content, .. } - | Message::System { content, .. } - | Message::User { content, .. } - | Message::Tool { content, .. } => Some(content), - Message::Assistant { content, .. } => { - if content.is_none() { - *content = Some(String::new()); - } - content.as_mut() - } - Message::Activity { .. } => None, - } - } - - /// Returns the activity content of this message, if it's an activity message. - pub fn activity_content(&self) -> Option<&JsonValue> { - match self { - Message::Activity { content, .. } => Some(content), - _ => None, - } - } - - /// Returns a mutable reference to the activity content, if it's an activity message. - pub fn activity_content_mut(&mut self) -> Option<&mut JsonValue> { - match self { - Message::Activity { content, .. } => Some(content), - _ => None, - } - } - - /// Returns the activity type, if this is an activity message. - pub fn activity_type(&self) -> Option<&str> { - match self { - Message::Activity { activity_type, .. } => Some(activity_type), - _ => None, - } - } - - /// Returns the tool calls for this message, if any. - pub fn tool_calls(&self) -> Option<&[ToolCall]> { - match self { - Message::Assistant { tool_calls, .. } => tool_calls.as_deref(), - _ => None, - } - } - - /// Returns a mutable reference to the tool calls for this message. - pub fn tool_calls_mut(&mut self) -> Option<&mut Vec> { - match self { - Message::Assistant { tool_calls, .. } => { - if tool_calls.is_none() { - *tool_calls = Some(Vec::new()); - } - tool_calls.as_mut() - } - _ => None, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_role_serialization() { - let role = Role::Assistant; - let json = serde_json::to_string(&role).unwrap(); - assert_eq!(json, "\"assistant\""); - - let role = Role::User; - let json = serde_json::to_string(&role).unwrap(); - assert_eq!(json, "\"user\""); - } - - #[test] - fn test_developer_message_builder() { - let msg = DeveloperMessage::new(MessageId::random(), "debug info".to_string()) - .with_name("debugger".to_string()); - - assert_eq!(msg.role, Role::Developer); - assert_eq!(msg.content, "debug info"); - assert_eq!(msg.name, Some("debugger".to_string())); - } - - #[test] - fn test_assistant_message_builder() { - let msg = AssistantMessage::new(MessageId::random()) - .with_content("Hello!".to_string()) - .with_name("Claude".to_string()); - - assert_eq!(msg.role, Role::Assistant); - assert_eq!(msg.content, Some("Hello!".to_string())); - assert_eq!(msg.name, Some("Claude".to_string())); - } - - #[test] - fn test_message_enum_serialization() { - let msg = Message::new_user("Hello, world!"); - let json = serde_json::to_string(&msg).unwrap(); - - // Should contain "role": "user" - assert!(json.contains("\"role\":\"user\"")); - assert!(json.contains("\"content\":\"Hello, world!\"")); - } - - #[test] - fn test_message_accessors() { - let msg = Message::new_assistant("I can help with that."); - - assert_eq!(msg.role(), Role::Assistant); - assert_eq!(msg.content(), Some("I can help with that.")); - assert!(msg.tool_calls().is_none()); - } - - #[test] - fn test_activity_role_serialization() { - let role = Role::Activity; - let json = serde_json::to_string(&role).unwrap(); - assert_eq!(json, "\"activity\""); - - let parsed: Role = serde_json::from_str(&json).unwrap(); - assert_eq!(parsed, Role::Activity); - } - - #[test] - fn test_activity_message_struct() { - use serde_json::json; - - let activity = ActivityMessage::new( - MessageId::random(), - "PLAN", - json!({"steps": ["research", "implement"]}), - ); - - assert_eq!(activity.role, Role::Activity); - assert_eq!(activity.activity_type, "PLAN"); - assert_eq!(activity.content["steps"][0], "research"); - } - - #[test] - fn test_activity_message_serialization() { - use serde_json::json; - - let activity = ActivityMessage::new( - MessageId::random(), - "RESEARCH", - json!({"query": "rust async"}), - ); - - let json_str = serde_json::to_string(&activity).unwrap(); - assert!(json_str.contains("\"role\":\"activity\"")); - assert!(json_str.contains("\"activityType\":\"RESEARCH\"")); - assert!(json_str.contains("\"query\":\"rust async\"")); - } - - #[test] - fn test_activity_message_enum() { - use serde_json::json; - - let msg = Message::new_activity("PLAN", json!({"steps": ["a", "b"]})); - - assert_eq!(msg.role(), Role::Activity); - assert!(msg.content().is_none()); // Activity has JSON content, not string - assert!(msg.activity_content().is_some()); - assert_eq!(msg.activity_type(), Some("PLAN")); - } - - #[test] - fn test_activity_message_enum_serialization() { - use serde_json::json; - - let msg = Message::new_activity("DEPLOY", json!({"target": "production"})); - let json_str = serde_json::to_string(&msg).unwrap(); - - assert!(json_str.contains("\"role\":\"activity\"")); - assert!(json_str.contains("\"activityType\":\"DEPLOY\"")); - assert!(json_str.contains("\"target\":\"production\"")); - - // Roundtrip - let parsed: Message = serde_json::from_str(&json_str).unwrap(); - assert_eq!(parsed.role(), Role::Activity); - assert_eq!(parsed.activity_type(), Some("DEPLOY")); - } - - #[test] - fn test_activity_content_accessors() { - use serde_json::json; - - let mut msg = Message::new_activity("TEST", json!({"status": "pending"})); - - // Test immutable accessor - assert!(msg.activity_content().is_some()); - assert_eq!(msg.activity_content().unwrap()["status"], "pending"); - - // Test mutable accessor - if let Some(content) = msg.activity_content_mut() { - content["status"] = json!("complete"); - } - assert_eq!(msg.activity_content().unwrap()["status"], "complete"); - - // Non-activity messages should return None - let user_msg = Message::new_user("hello"); - assert!(user_msg.activity_content().is_none()); - assert!(user_msg.activity_type().is_none()); - } -} diff --git a/crates/ag-ui-core/src/types/mod.rs b/crates/ag-ui-core/src/types/mod.rs deleted file mode 100644 index df69afbd..00000000 --- a/crates/ag-ui-core/src/types/mod.rs +++ /dev/null @@ -1,20 +0,0 @@ -//! AG-UI Protocol Types -//! -//! This module defines core protocol types including: -//! - Message types (user, assistant, system, tool) -//! - Role definitions -//! - ID types (MessageId, RunId, ThreadId, ToolCallId) -//! - Context and input types -//! - Content types (text, binary) for multimodal messages - -mod content; -mod ids; -mod input; -mod message; -mod tool; - -pub use content::*; -pub use ids::*; -pub use input::*; -pub use message::*; -pub use tool::*; diff --git a/crates/ag-ui-core/src/types/tool.rs b/crates/ag-ui-core/src/types/tool.rs deleted file mode 100644 index 9af554a8..00000000 --- a/crates/ag-ui-core/src/types/tool.rs +++ /dev/null @@ -1,78 +0,0 @@ -//! Tool types for the AG-UI protocol. -//! -//! This module defines structures for tool/function calling, -//! including tool definitions and tool call representations. - -use crate::types::ids::ToolCallId; -use crate::types::message::FunctionCall; -use serde::{Deserialize, Serialize}; -use serde_json::Value as JsonValue; - -/// A tool call made by an assistant. -/// -/// Represents a specific invocation of a tool/function by the model, -/// including the tool call ID, type, and function details. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct ToolCall { - /// Unique identifier for this tool call. - pub id: ToolCallId, - /// The type of call (always "function" for now). - #[serde(rename = "type")] - pub call_type: String, - /// The function being called with its arguments. - pub function: FunctionCall, -} - -impl ToolCall { - /// Creates a new tool call with the given ID and function. - pub fn new(id: impl Into, function: FunctionCall) -> Self { - Self { - id: id.into(), - call_type: "function".to_string(), - function, - } - } -} - -/// A tool definition describing a function the model can call. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct Tool { - /// The name of the tool. - pub name: String, - /// A description of what the tool does. - pub description: String, - /// JSON Schema describing the tool's parameters. - pub parameters: JsonValue, -} - -impl Tool { - /// Creates a new tool definition. - pub fn new(name: String, description: String, parameters: JsonValue) -> Self { - Self { - name, - description, - parameters, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_tool_call_serialization() { - let tool_call = ToolCall::new( - ToolCallId::random(), - FunctionCall { - name: "get_weather".to_string(), - arguments: r#"{"location": "NYC"}"#.to_string(), - }, - ); - - let json = serde_json::to_string(&tool_call).unwrap(); - // Should have "type": "function", not "call_type" - assert!(json.contains("\"type\":\"function\"")); - assert!(json.contains("\"name\":\"get_weather\"")); - } -} diff --git a/crates/ag-ui-server/Cargo.toml b/crates/ag-ui-server/Cargo.toml deleted file mode 100644 index b03cf872..00000000 --- a/crates/ag-ui-server/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "ag-ui-server" -version = "0.1.0" -edition = "2024" -rust-version = "1.88" -license = "MIT" -description = "Server-side AG-UI event producer for streaming to frontends" -readme = "README.md" - -[dependencies] -# Internal -ag-ui-core = { version = "0.1.0", path = "../ag-ui-core" } - -# Error handling -thiserror = "2" - -# Serialization -serde = { version = "1", features = ["derive"] } -serde_json = "1" - -# Async runtime -tokio = { version = "1", features = ["rt", "macros", "sync"] } -tokio-stream = "0.1" -async-trait = "0.1" -futures = "0.3" - -# Web framework -axum = { version = "0.8", features = ["ws"] } diff --git a/crates/ag-ui-server/src/error.rs b/crates/ag-ui-server/src/error.rs deleted file mode 100644 index 9a8e4c91..00000000 --- a/crates/ag-ui-server/src/error.rs +++ /dev/null @@ -1,31 +0,0 @@ -//! Error types for AG-UI server operations. - -use ag_ui_core::AgUiError; -use thiserror::Error; - -/// Errors that can occur in AG-UI server operations. -#[derive(Debug, Error)] -pub enum ServerError { - /// Core AG-UI error - #[error("Core error: {0}")] - Core(#[from] AgUiError), - - /// Transport layer error (SSE, WebSocket, etc.) - #[error("Transport error: {0}")] - Transport(String), - - /// Serialization error during event emission - #[error("Serialization error: {0}")] - Serialization(String), - - /// Channel or stream error - #[error("Channel error: {0}")] - Channel(String), - - /// Connection error - #[error("Connection error: {0}")] - Connection(String), -} - -/// Result type alias using ServerError -pub type Result = std::result::Result; diff --git a/crates/ag-ui-server/src/lib.rs b/crates/ag-ui-server/src/lib.rs deleted file mode 100644 index 93898b1c..00000000 --- a/crates/ag-ui-server/src/lib.rs +++ /dev/null @@ -1,43 +0,0 @@ -//! AG-UI Server SDK -//! -//! This crate provides server-side functionality for producing AG-UI protocol events. -//! It enables Rust agents to stream events to frontend applications via various -//! transports (SSE, WebSocket, etc.). -//! -//! # Overview -//! -//! The AG-UI Server SDK includes: -//! -//! - **Event Producer**: High-level API for emitting AG-UI events from agent code -//! - **Transport Layer**: SSE and WebSocket implementations for streaming events -//! - **Error Handling**: Server-specific error types -//! -//! # Usage -//! -//! ```rust,ignore -//! use ag_ui_server::{EventProducer, Result}; -//! ``` -//! -//! # Integration -//! -//! This crate is designed to integrate with the Syncable CLI agent, enabling -//! any frontend to connect and receive real-time agent events. - -pub mod error; -pub mod producer; -pub mod transport; - -// Re-export ag-ui-core types for convenience -pub use ag_ui_core::*; - -// Re-export server-specific types -pub use error::{Result, ServerError}; - -// Re-export transport types -pub use transport::{SseHandler, SseSender}; - -// Re-export producer types -pub use producer::{ - AgentSession, EventProducer, MessageStream, ThinkingMessageStream, ThinkingStep, - ToolCallStream, -}; diff --git a/crates/ag-ui-server/src/producer.rs b/crates/ag-ui-server/src/producer.rs deleted file mode 100644 index 0796cfbe..00000000 --- a/crates/ag-ui-server/src/producer.rs +++ /dev/null @@ -1,1055 +0,0 @@ -//! Event producer API for emitting AG-UI events. -//! -//! This module provides the high-level API for agents to emit events to connected -//! frontends. It includes: -//! -//! - [`EventProducer`] trait - Core abstraction for event emission -//! - [`MessageStream`] - Helper for streaming text messages -//! - [`ToolCallStream`] - Helper for streaming tool calls -//! - [`ThinkingMessageStream`] - Helper for streaming thinking content -//! - [`ThinkingStep`] - Helper for thinking block boundaries (chain-of-thought) -//! - [`AgentSession`] - Manages run lifecycle and state -//! -//! # Example -//! -//! ```rust,ignore -//! use ag_ui_server::{transport::sse, AgentSession, MessageStream}; -//! -//! async fn handle_request() -> impl IntoResponse { -//! let (sender, handler) = sse::channel(32); -//! -//! tokio::spawn(async move { -//! let mut session = AgentSession::new(sender); -//! session.start_run().await.unwrap(); -//! -//! // Stream a message -//! let msg = MessageStream::start(session.producer()).await.unwrap(); -//! msg.content("Hello, ").await.unwrap(); -//! msg.content("world!").await.unwrap(); -//! msg.end().await.unwrap(); -//! -//! session.finish_run(None).await.unwrap(); -//! }); -//! -//! handler.into_response() -//! } -//! ``` - -use std::marker::PhantomData; - -use ag_ui_core::{ - AgentState, Event, InterruptInfo, JsonValue, MessageId, RunErrorEvent, RunFinishedEvent, - RunId, RunStartedEvent, TextMessageContentEvent, TextMessageEndEvent, TextMessageStartEvent, - ThinkingEndEvent, ThinkingStartEvent, ThinkingTextMessageContentEvent, - ThinkingTextMessageEndEvent, ThinkingTextMessageStartEvent, ThreadId, ToolCallArgsEvent, - ToolCallEndEvent, ToolCallId, ToolCallStartEvent, -}; -use async_trait::async_trait; - -use crate::error::ServerError; -use crate::transport::SseSender; - -/// Trait for producing AG-UI events. -/// -/// Implementors of this trait can emit events to connected frontends -/// through various transport mechanisms (SSE, WebSocket, etc.). -/// -/// # Example -/// -/// ```rust,ignore -/// use ag_ui_server::EventProducer; -/// use ag_ui_core::{Event, RunErrorEvent}; -/// -/// async fn emit_error(producer: &P) -> Result<(), ServerError> { -/// producer.emit(Event::RunError(RunErrorEvent::new("Something went wrong"))).await -/// } -/// ``` -#[async_trait] -pub trait EventProducer: Send + Sync { - /// Emit a single event to connected clients. - /// - /// Returns an error if the connection is closed or the event cannot be sent. - async fn emit(&self, event: Event) -> Result<(), ServerError>; - - /// Emit multiple events to connected clients. - /// - /// Events are sent in order. Stops and returns an error on the first failure. - async fn emit_many(&self, events: Vec>) -> Result<(), ServerError> { - for event in events { - self.emit(event).await?; - } - Ok(()) - } - - /// Check if the connection is still open. - /// - /// Returns `false` if the client has disconnected. - fn is_connected(&self) -> bool; -} - -// Implement EventProducer for SseSender -#[async_trait] -impl EventProducer for SseSender { - async fn emit(&self, event: Event) -> Result<(), ServerError> { - self.send(event) - .await - .map_err(|_| ServerError::Channel("SSE channel closed".into())) - } - - fn is_connected(&self) -> bool { - !self.is_closed() - } -} - -/// Helper for streaming a text message piece by piece. -/// -/// This struct manages the lifecycle of a streaming text message, automatically -/// generating message IDs and emitting the appropriate events. -/// -/// # Example -/// -/// ```rust,ignore -/// let msg = MessageStream::start(&producer).await?; -/// msg.content("Hello, ").await?; -/// msg.content("world!").await?; -/// let message_id = msg.end().await?; -/// ``` -pub struct MessageStream<'a, P: EventProducer, StateT: AgentState = JsonValue> { - producer: &'a P, - message_id: MessageId, - _state: PhantomData, -} - -impl<'a, P: EventProducer, StateT: AgentState> MessageStream<'a, P, StateT> { - /// Start a new message stream. - /// - /// Emits a `TextMessageStart` event with a randomly generated message ID. - pub async fn start(producer: &'a P) -> Result { - let message_id = MessageId::random(); - producer - .emit(Event::TextMessageStart(TextMessageStartEvent::new( - message_id.clone(), - ))) - .await?; - Ok(Self { - producer, - message_id, - _state: PhantomData, - }) - } - - /// Start a new message stream with a specific message ID. - pub async fn start_with_id( - producer: &'a P, - message_id: MessageId, - ) -> Result { - producer - .emit(Event::TextMessageStart(TextMessageStartEvent::new( - message_id.clone(), - ))) - .await?; - Ok(Self { - producer, - message_id, - _state: PhantomData, - }) - } - - /// Append content to the message. - /// - /// Emits a `TextMessageContent` event with the given delta. - /// Empty deltas are silently ignored. - pub async fn content(&self, delta: impl Into) -> Result<(), ServerError> { - let delta = delta.into(); - if delta.is_empty() { - return Ok(()); - } - self.producer - .emit(Event::TextMessageContent( - TextMessageContentEvent::new_unchecked(self.message_id.clone(), delta), - )) - .await - } - - /// End the message stream. - /// - /// Emits a `TextMessageEnd` event and returns the message ID. - /// Consumes the stream to prevent further content being added. - pub async fn end(self) -> Result { - self.producer - .emit(Event::TextMessageEnd(TextMessageEndEvent::new( - self.message_id.clone(), - ))) - .await?; - Ok(self.message_id) - } - - /// Get the message ID for this stream. - pub fn message_id(&self) -> &MessageId { - &self.message_id - } -} - -/// Helper for streaming a tool call with arguments. -/// -/// This struct manages the lifecycle of a streaming tool call, automatically -/// generating tool call IDs and emitting the appropriate events. -/// -/// # Example -/// -/// ```rust,ignore -/// let call = ToolCallStream::start(&producer, "get_weather").await?; -/// call.args(r#"{"location": "#).await?; -/// call.args(r#""New York"}"#).await?; -/// let tool_call_id = call.end().await?; -/// ``` -pub struct ToolCallStream<'a, P: EventProducer, StateT: AgentState = JsonValue> { - producer: &'a P, - tool_call_id: ToolCallId, - _state: PhantomData, -} - -impl<'a, P: EventProducer, StateT: AgentState> ToolCallStream<'a, P, StateT> { - /// Start a new tool call stream. - /// - /// Emits a `ToolCallStart` event with the given tool name and a randomly - /// generated tool call ID. - pub async fn start(producer: &'a P, name: impl Into) -> Result { - let tool_call_id = ToolCallId::random(); - producer - .emit(Event::ToolCallStart(ToolCallStartEvent::new( - tool_call_id.clone(), - name, - ))) - .await?; - Ok(Self { - producer, - tool_call_id, - _state: PhantomData, - }) - } - - /// Start a new tool call stream with a specific tool call ID. - pub async fn start_with_id( - producer: &'a P, - tool_call_id: ToolCallId, - name: impl Into, - ) -> Result { - producer - .emit(Event::ToolCallStart(ToolCallStartEvent::new( - tool_call_id.clone(), - name, - ))) - .await?; - Ok(Self { - producer, - tool_call_id, - _state: PhantomData, - }) - } - - /// Stream an argument chunk. - /// - /// Emits a `ToolCallArgs` event with the given delta. - pub async fn args(&self, delta: impl Into) -> Result<(), ServerError> { - self.producer - .emit(Event::ToolCallArgs(ToolCallArgsEvent::new( - self.tool_call_id.clone(), - delta, - ))) - .await - } - - /// End the tool call stream. - /// - /// Emits a `ToolCallEnd` event and returns the tool call ID. - /// Consumes the stream to prevent further args being added. - pub async fn end(self) -> Result { - self.producer - .emit(Event::ToolCallEnd(ToolCallEndEvent::new( - self.tool_call_id.clone(), - ))) - .await?; - Ok(self.tool_call_id) - } - - /// Get the tool call ID for this stream. - pub fn tool_call_id(&self) -> &ToolCallId { - &self.tool_call_id - } -} - -/// Helper for streaming thinking content (extended thinking / chain-of-thought). -/// -/// This struct manages the lifecycle of streaming thinking content. Unlike -/// [`MessageStream`], thinking messages don't have IDs as they're ephemeral. -/// -/// # Example -/// -/// ```rust,ignore -/// let thinking = ThinkingMessageStream::start(&producer).await?; -/// thinking.content("Let me analyze this...").await?; -/// thinking.content("The key factors are...").await?; -/// thinking.end().await?; -/// ``` -pub struct ThinkingMessageStream<'a, P: EventProducer, StateT: AgentState = JsonValue> { - producer: &'a P, - _state: PhantomData, -} - -impl<'a, P: EventProducer, StateT: AgentState> ThinkingMessageStream<'a, P, StateT> { - /// Start a new thinking message stream. - /// - /// Emits a `ThinkingTextMessageStart` event. - pub async fn start(producer: &'a P) -> Result { - producer - .emit(Event::ThinkingTextMessageStart( - ThinkingTextMessageStartEvent::new(), - )) - .await?; - Ok(Self { - producer, - _state: PhantomData, - }) - } - - /// Append content to the thinking message. - /// - /// Emits a `ThinkingTextMessageContent` event with the given delta. - /// Unlike regular messages, empty deltas are allowed for thinking content. - pub async fn content(&self, delta: impl Into) -> Result<(), ServerError> { - self.producer - .emit(Event::ThinkingTextMessageContent( - ThinkingTextMessageContentEvent::new(delta), - )) - .await - } - - /// End the thinking message stream. - /// - /// Emits a `ThinkingTextMessageEnd` event. - /// Consumes the stream to prevent further content being added. - pub async fn end(self) -> Result<(), ServerError> { - self.producer - .emit(Event::ThinkingTextMessageEnd( - ThinkingTextMessageEndEvent::new(), - )) - .await - } -} - -/// Helper for managing thinking block boundaries (chain-of-thought steps). -/// -/// This struct wraps a thinking block with `ThinkingStart` and `ThinkingEnd` events. -/// Inside a thinking step, you can emit thinking content using [`ThinkingMessageStream`]. -/// -/// # Example -/// -/// ```rust,ignore -/// // Start a thinking step with optional title -/// let step = ThinkingStep::start(&producer, Some("Analyzing user query")).await?; -/// -/// // Emit thinking content inside the step -/// let thinking = ThinkingMessageStream::start(step.producer()).await?; -/// thinking.content("First, let me consider...").await?; -/// thinking.end().await?; -/// -/// // End the thinking step -/// step.end().await?; -/// ``` -pub struct ThinkingStep<'a, P: EventProducer, StateT: AgentState = JsonValue> { - producer: &'a P, - _state: PhantomData, -} - -impl<'a, P: EventProducer, StateT: AgentState> ThinkingStep<'a, P, StateT> { - /// Start a new thinking step. - /// - /// Emits a `ThinkingStart` event with an optional title. - pub async fn start( - producer: &'a P, - title: Option>, - ) -> Result { - let event = if let Some(t) = title { - ThinkingStartEvent::new().with_title(t) - } else { - ThinkingStartEvent::new() - }; - producer.emit(Event::ThinkingStart(event)).await?; - Ok(Self { - producer, - _state: PhantomData, - }) - } - - /// End the thinking step. - /// - /// Emits a `ThinkingEnd` event. - /// Consumes the step to prevent reuse. - pub async fn end(self) -> Result<(), ServerError> { - self.producer - .emit(Event::ThinkingEnd(ThinkingEndEvent::new())) - .await - } - - /// Get a reference to the underlying producer. - /// - /// Use this to create [`ThinkingMessageStream`] instances inside the step. - pub fn producer(&self) -> &'a P { - self.producer - } -} - -/// Manages an agent session with run lifecycle events. -/// -/// This struct provides high-level management of agent runs, including -/// starting, finishing, and error handling. -/// -/// # Example -/// -/// ```rust,ignore -/// let mut session = AgentSession::new(sender); -/// -/// // Start a run -/// let run_id = session.start_run().await?; -/// -/// // Do work... -/// -/// // Finish the run -/// session.finish_run(Some(json!({"result": "success"}))).await?; -/// ``` -pub struct AgentSession, StateT: AgentState = JsonValue> { - producer: P, - thread_id: ThreadId, - current_run: Option, - _state: PhantomData, -} - -impl, StateT: AgentState> AgentSession { - /// Create a new session with the given producer. - /// - /// Generates a random thread ID for the session. - pub fn new(producer: P) -> Self { - Self { - producer, - thread_id: ThreadId::random(), - current_run: None, - _state: PhantomData, - } - } - - /// Create a new session with a specific thread ID. - pub fn with_thread_id(producer: P, thread_id: ThreadId) -> Self { - Self { - producer, - thread_id, - current_run: None, - _state: PhantomData, - } - } - - /// Start a new run. - /// - /// Emits a `RunStarted` event and stores the run ID. - /// Returns an error if a run is already in progress. - pub async fn start_run(&mut self) -> Result { - if self.current_run.is_some() { - return Err(ServerError::Channel("Run already in progress".into())); - } - let run_id = RunId::random(); - self.producer - .emit(Event::RunStarted(RunStartedEvent::new( - self.thread_id.clone(), - run_id.clone(), - ))) - .await?; - self.current_run = Some(run_id.clone()); - Ok(run_id) - } - - /// Finish the current run. - /// - /// Emits a `RunFinished` event with an optional result. - /// Does nothing if no run is in progress. - pub async fn finish_run(&mut self, result: Option) -> Result<(), ServerError> { - if let Some(run_id) = self.current_run.take() { - let mut event = RunFinishedEvent::new(self.thread_id.clone(), run_id); - if let Some(r) = result { - event = event.with_result(r); - } - self.producer.emit(Event::RunFinished(event)).await?; - } - Ok(()) - } - - /// Signal a run error. - /// - /// Emits a `RunError` event and clears the current run. - pub async fn run_error(&mut self, message: impl Into) -> Result<(), ServerError> { - self.current_run = None; - self.producer - .emit(Event::RunError(RunErrorEvent::new(message))) - .await - } - - /// Signal a run error with an error code. - pub async fn run_error_with_code( - &mut self, - message: impl Into, - code: impl Into, - ) -> Result<(), ServerError> { - self.current_run = None; - self.producer - .emit(Event::RunError( - RunErrorEvent::new(message).with_code(code), - )) - .await - } - - /// Get a reference to the underlying producer. - pub fn producer(&self) -> &P { - &self.producer - } - - /// Get the thread ID for this session. - pub fn thread_id(&self) -> &ThreadId { - &self.thread_id - } - - /// Get the current run ID, if any. - pub fn run_id(&self) -> Option<&RunId> { - self.current_run.as_ref() - } - - /// Check if a run is currently in progress. - pub fn is_running(&self) -> bool { - self.current_run.is_some() - } - - /// Check if the connection is still open. - pub fn is_connected(&self) -> bool { - self.producer.is_connected() - } - - /// Start a thinking step. - /// - /// Convenience method that creates a [`ThinkingStep`] using this session's producer. - /// - /// # Example - /// - /// ```rust,ignore - /// let step = session.start_thinking(Some("Planning response")).await?; - /// // ... emit thinking content ... - /// step.end().await?; - /// ``` - pub async fn start_thinking( - &self, - title: Option>, - ) -> Result, ServerError> { - ThinkingStep::start(&self.producer, title).await - } - - /// Interrupt the current run for human-in-the-loop interaction. - /// - /// Finishes the run with an interrupt outcome, signaling that human input - /// is required before the agent can continue. The client should display - /// appropriate UI based on the interrupt info and resume with user input. - /// - /// # Example - /// - /// ```rust,ignore - /// session.start_run().await?; - /// - /// // Request human approval - /// session.interrupt( - /// Some("human_approval"), - /// Some(serde_json::json!({"action": "send_email", "to": "user@example.com"})) - /// ).await?; - /// ``` - pub async fn interrupt( - &mut self, - reason: Option>, - payload: Option, - ) -> Result<(), ServerError> { - let run_id = self.current_run.take(); - if let Some(run_id) = run_id { - let mut info = InterruptInfo::new(); - if let Some(r) = reason { - info = info.with_reason(r); - } - if let Some(p) = payload { - info = info.with_payload(p); - } - - let event = RunFinishedEvent::new(self.thread_id.clone(), run_id).with_interrupt(info); - self.producer.emit(Event::RunFinished(event)).await?; - } - Ok(()) - } - - /// Interrupt with a specific interrupt ID for tracking. - /// - /// The interrupt ID can be used by the client to correlate the resume - /// request with the original interrupt. - /// - /// # Example - /// - /// ```rust,ignore - /// session.start_run().await?; - /// - /// // Request approval with tracking ID - /// session.interrupt_with_id( - /// "approval-001", - /// Some("database_modification"), - /// Some(serde_json::json!({"query": "DELETE FROM users WHERE inactive"})) - /// ).await?; - /// ``` - pub async fn interrupt_with_id( - &mut self, - id: impl Into, - reason: Option>, - payload: Option, - ) -> Result<(), ServerError> { - let run_id = self.current_run.take(); - if let Some(run_id) = run_id { - let mut info = InterruptInfo::new().with_id(id); - if let Some(r) = reason { - info = info.with_reason(r); - } - if let Some(p) = payload { - info = info.with_payload(p); - } - - let event = RunFinishedEvent::new(self.thread_id.clone(), run_id).with_interrupt(info); - self.producer.emit(Event::RunFinished(event)).await?; - } - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::sync::{Arc, Mutex}; - - /// Mock producer for testing - struct MockProducer { - events: Arc>>, - connected: bool, - } - - impl MockProducer { - fn new() -> Self { - Self { - events: Arc::new(Mutex::new(Vec::new())), - connected: true, - } - } - - fn events(&self) -> Vec { - self.events.lock().unwrap().clone() - } - } - - #[async_trait] - impl EventProducer for MockProducer { - async fn emit(&self, event: Event) -> Result<(), ServerError> { - if !self.connected { - return Err(ServerError::Channel("disconnected".into())); - } - self.events.lock().unwrap().push(event); - Ok(()) - } - - fn is_connected(&self) -> bool { - self.connected - } - } - - #[tokio::test] - async fn test_event_producer_emit() { - let producer = MockProducer::new(); - - producer - .emit(Event::RunError(RunErrorEvent::new("test"))) - .await - .unwrap(); - - let events = producer.events(); - assert_eq!(events.len(), 1); - assert!(matches!(events[0], Event::RunError(_))); - } - - #[tokio::test] - async fn test_event_producer_emit_many() { - let producer = MockProducer::new(); - - producer - .emit_many(vec![ - Event::RunError(RunErrorEvent::new("error1")), - Event::RunError(RunErrorEvent::new("error2")), - ]) - .await - .unwrap(); - - let events = producer.events(); - assert_eq!(events.len(), 2); - } - - #[tokio::test] - async fn test_message_stream() { - let producer = MockProducer::new(); - - let msg = MessageStream::start(&producer).await.unwrap(); - msg.content("Hello, ").await.unwrap(); - msg.content("world!").await.unwrap(); - let _message_id = msg.end().await.unwrap(); - - let events = producer.events(); - assert_eq!(events.len(), 4); // start + 2 content + end - - assert!(matches!(events[0], Event::TextMessageStart(_))); - assert!(matches!(events[1], Event::TextMessageContent(_))); - assert!(matches!(events[2], Event::TextMessageContent(_))); - assert!(matches!(events[3], Event::TextMessageEnd(_))); - } - - #[tokio::test] - async fn test_message_stream_empty_content_ignored() { - let producer = MockProducer::new(); - - let msg = MessageStream::start(&producer).await.unwrap(); - msg.content("").await.unwrap(); // Should be ignored - msg.content("Hello").await.unwrap(); - msg.end().await.unwrap(); - - let events = producer.events(); - assert_eq!(events.len(), 3); // start + 1 content + end (empty ignored) - } - - #[tokio::test] - async fn test_tool_call_stream() { - let producer = MockProducer::new(); - - let call = ToolCallStream::start(&producer, "get_weather").await.unwrap(); - call.args(r#"{"location": "#).await.unwrap(); - call.args(r#""NYC"}"#).await.unwrap(); - let _tool_call_id = call.end().await.unwrap(); - - let events = producer.events(); - assert_eq!(events.len(), 4); // start + 2 args + end - - assert!(matches!(events[0], Event::ToolCallStart(_))); - assert!(matches!(events[1], Event::ToolCallArgs(_))); - assert!(matches!(events[2], Event::ToolCallArgs(_))); - assert!(matches!(events[3], Event::ToolCallEnd(_))); - } - - #[tokio::test] - async fn test_agent_session_run_lifecycle() { - let producer = MockProducer::new(); - let mut session = AgentSession::new(producer); - - assert!(!session.is_running()); - - // Start run - let run_id = session.start_run().await.unwrap(); - assert!(session.is_running()); - assert_eq!(session.run_id(), Some(&run_id)); - - // Finish run - session.finish_run(None).await.unwrap(); - assert!(!session.is_running()); - assert_eq!(session.run_id(), None); - - let events = session.producer().events(); - assert_eq!(events.len(), 2); - assert!(matches!(events[0], Event::RunStarted(_))); - assert!(matches!(events[1], Event::RunFinished(_))); - } - - #[tokio::test] - async fn test_agent_session_run_error() { - let producer = MockProducer::new(); - let mut session = AgentSession::new(producer); - - session.start_run().await.unwrap(); - session.run_error("Something went wrong").await.unwrap(); - - assert!(!session.is_running()); - - let events = session.producer().events(); - assert_eq!(events.len(), 2); - assert!(matches!(events[0], Event::RunStarted(_))); - assert!(matches!(events[1], Event::RunError(_))); - } - - #[tokio::test] - async fn test_agent_session_double_start_error() { - let producer = MockProducer::new(); - let mut session = AgentSession::new(producer); - - session.start_run().await.unwrap(); - let result = session.start_run().await; - - assert!(result.is_err()); - } - - #[tokio::test] - async fn test_agent_session_finish_without_run() { - let producer = MockProducer::new(); - let mut session = AgentSession::new(producer); - - // Should not error, just do nothing - session.finish_run(None).await.unwrap(); - - let events = session.producer().events(); - assert!(events.is_empty()); - } - - // ========================================================================= - // Thinking Message Stream Tests - // ========================================================================= - - #[tokio::test] - async fn test_thinking_message_stream() { - let producer = MockProducer::new(); - - let thinking = ThinkingMessageStream::start(&producer).await.unwrap(); - thinking.content("Let me analyze...").await.unwrap(); - thinking.content("The answer is...").await.unwrap(); - thinking.end().await.unwrap(); - - let events = producer.events(); - assert_eq!(events.len(), 4); // start + 2 content + end - - assert!(matches!(events[0], Event::ThinkingTextMessageStart(_))); - assert!(matches!(events[1], Event::ThinkingTextMessageContent(_))); - assert!(matches!(events[2], Event::ThinkingTextMessageContent(_))); - assert!(matches!(events[3], Event::ThinkingTextMessageEnd(_))); - } - - #[tokio::test] - async fn test_thinking_message_stream_empty_content_allowed() { - let producer = MockProducer::new(); - - let thinking = ThinkingMessageStream::start(&producer).await.unwrap(); - thinking.content("").await.unwrap(); // Empty is allowed for thinking - thinking.content("Thinking...").await.unwrap(); - thinking.end().await.unwrap(); - - let events = producer.events(); - // Empty content is emitted (unlike regular MessageStream) - assert_eq!(events.len(), 4); // start + empty + content + end - } - - // ========================================================================= - // Thinking Step Tests - // ========================================================================= - - #[tokio::test] - async fn test_thinking_step() { - let producer = MockProducer::new(); - - let step = ThinkingStep::start(&producer, None::).await.unwrap(); - step.end().await.unwrap(); - - let events = producer.events(); - assert_eq!(events.len(), 2); // start + end - - assert!(matches!(events[0], Event::ThinkingStart(_))); - assert!(matches!(events[1], Event::ThinkingEnd(_))); - } - - #[tokio::test] - async fn test_thinking_step_with_title() { - let producer = MockProducer::new(); - - let step = ThinkingStep::start(&producer, Some("Analyzing query")) - .await - .unwrap(); - step.end().await.unwrap(); - - let events = producer.events(); - assert_eq!(events.len(), 2); - - if let Event::ThinkingStart(start) = &events[0] { - assert_eq!(start.title, Some("Analyzing query".to_string())); - } else { - panic!("Expected ThinkingStart event"); - } - } - - #[tokio::test] - async fn test_thinking_step_with_content() { - let producer = MockProducer::new(); - - let step = ThinkingStep::start(&producer, Some("Planning")) - .await - .unwrap(); - - // Emit thinking content inside the step - let thinking = ThinkingMessageStream::start(step.producer()).await.unwrap(); - thinking.content("First, consider...").await.unwrap(); - thinking.end().await.unwrap(); - - step.end().await.unwrap(); - - let events = producer.events(); - assert_eq!(events.len(), 5); // ThinkingStart + TextStart + content + TextEnd + ThinkingEnd - - assert!(matches!(events[0], Event::ThinkingStart(_))); - assert!(matches!(events[1], Event::ThinkingTextMessageStart(_))); - assert!(matches!(events[2], Event::ThinkingTextMessageContent(_))); - assert!(matches!(events[3], Event::ThinkingTextMessageEnd(_))); - assert!(matches!(events[4], Event::ThinkingEnd(_))); - } - - // ========================================================================= - // AgentSession Thinking Tests - // ========================================================================= - - #[tokio::test] - async fn test_agent_session_start_thinking() { - let producer = MockProducer::new(); - let session = AgentSession::new(producer); - - let step = session.start_thinking(Some("Reasoning")).await.unwrap(); - step.end().await.unwrap(); - - let events = session.producer().events(); - assert_eq!(events.len(), 2); - assert!(matches!(events[0], Event::ThinkingStart(_))); - assert!(matches!(events[1], Event::ThinkingEnd(_))); - } - - #[tokio::test] - async fn test_agent_session_start_thinking_no_title() { - let producer = MockProducer::new(); - let session = AgentSession::new(producer); - - let step = session.start_thinking(None::).await.unwrap(); - step.end().await.unwrap(); - - let events = session.producer().events(); - assert_eq!(events.len(), 2); - - if let Event::ThinkingStart(start) = &events[0] { - assert!(start.title.is_none()); - } else { - panic!("Expected ThinkingStart event"); - } - } - - // ========================================================================= - // AgentSession Interrupt Tests - // ========================================================================= - - #[tokio::test] - async fn test_agent_session_interrupt() { - use ag_ui_core::RunFinishedOutcome; - - let producer = MockProducer::new(); - let mut session = AgentSession::new(producer); - - session.start_run().await.unwrap(); - session - .interrupt( - Some("human_approval"), - Some(serde_json::json!({"action": "send_email"})), - ) - .await - .unwrap(); - - // Run should be cleared after interrupt - assert!(!session.is_running()); - - let events = session.producer().events(); - assert_eq!(events.len(), 2); // RunStarted + RunFinished(interrupt) - - assert!(matches!(events[0], Event::RunStarted(_))); - - if let Event::RunFinished(finished) = &events[1] { - assert_eq!(finished.outcome, Some(RunFinishedOutcome::Interrupt)); - assert!(finished.interrupt.is_some()); - let info = finished.interrupt.as_ref().unwrap(); - assert_eq!(info.reason, Some("human_approval".to_string())); - assert!(info.payload.is_some()); - } else { - panic!("Expected RunFinished event"); - } - } - - #[tokio::test] - async fn test_agent_session_interrupt_with_id() { - use ag_ui_core::RunFinishedOutcome; - - let producer = MockProducer::new(); - let mut session = AgentSession::new(producer); - - session.start_run().await.unwrap(); - session - .interrupt_with_id( - "approval-001", - Some("database_modification"), - Some(serde_json::json!({"query": "DELETE FROM users"})), - ) - .await - .unwrap(); - - assert!(!session.is_running()); - - let events = session.producer().events(); - assert_eq!(events.len(), 2); - - if let Event::RunFinished(finished) = &events[1] { - assert_eq!(finished.outcome, Some(RunFinishedOutcome::Interrupt)); - let info = finished.interrupt.as_ref().unwrap(); - assert_eq!(info.id, Some("approval-001".to_string())); - assert_eq!(info.reason, Some("database_modification".to_string())); - } else { - panic!("Expected RunFinished event"); - } - } - - #[tokio::test] - async fn test_agent_session_interrupt_without_run() { - let producer = MockProducer::new(); - let mut session = AgentSession::new(producer); - - // Interrupt without an active run should do nothing - session - .interrupt(Some("test"), None) - .await - .unwrap(); - - let events = session.producer().events(); - assert!(events.is_empty()); - } - - #[tokio::test] - async fn test_agent_session_interrupt_minimal() { - let producer = MockProducer::new(); - let mut session = AgentSession::new(producer); - - session.start_run().await.unwrap(); - - // Interrupt with no reason or payload - session - .interrupt(None::, None) - .await - .unwrap(); - - let events = session.producer().events(); - assert_eq!(events.len(), 2); - - if let Event::RunFinished(finished) = &events[1] { - let info = finished.interrupt.as_ref().unwrap(); - assert!(info.id.is_none()); - assert!(info.reason.is_none()); - assert!(info.payload.is_none()); - } else { - panic!("Expected RunFinished event"); - } - } -} diff --git a/crates/ag-ui-server/src/transport/mod.rs b/crates/ag-ui-server/src/transport/mod.rs deleted file mode 100644 index d896fad0..00000000 --- a/crates/ag-ui-server/src/transport/mod.rs +++ /dev/null @@ -1,64 +0,0 @@ -//! Transport Layer for AG-UI Events -//! -//! This module provides transport implementations for streaming AG-UI events -//! to frontend clients: -//! -//! - **SSE (Server-Sent Events)**: HTTP-based unidirectional streaming via [`sse`] -//! - **WebSocket**: Bidirectional WebSocket transport via [`ws`] -//! -//! # SSE Example -//! -//! ```rust,ignore -//! use ag_ui_server::transport::sse; -//! use ag_ui_core::{Event, RunErrorEvent}; -//! -//! // Create channel pair -//! let (sender, handler) = sse::channel::(32); -//! -//! // Send events from agent code -//! sender.send(Event::RunError(RunErrorEvent::new("error"))).await?; -//! -//! // Return handler as axum response -//! handler.into_response() -//! ``` -//! -//! # WebSocket Example -//! -//! ```rust,ignore -//! use ag_ui_server::transport::ws; -//! use ag_ui_core::{Event, RunErrorEvent}; -//! use axum::extract::ws::WebSocketUpgrade; -//! -//! async fn ws_endpoint(upgrade: WebSocketUpgrade) -> impl IntoResponse { -//! let (sender, handler) = ws::channel::(32); -//! -//! tokio::spawn(async move { -//! sender.send(Event::RunError(RunErrorEvent::new("error"))).await.ok(); -//! }); -//! -//! handler.into_response(upgrade) -//! } -//! ``` -//! -//! # Choosing Between SSE and WebSocket -//! -//! | Feature | SSE | WebSocket | -//! |---------|-----|-----------| -//! | Direction | Server → Client | Bidirectional | -//! | Auto-reconnect | Built-in (EventSource) | Manual | -//! | HTTP/2 multiplexing | Yes | No | -//! | Binary data | No (text only) | Yes | -//! | Browser connection limit | Per-domain | Per-domain | - -pub mod sse; -pub mod ws; - -// Re-export SSE types (default transport) -pub use sse::{channel, format_sse_event, SendError, SseHandler, SseSender}; - -// Re-export WebSocket types with ws_ prefix to avoid conflicts -pub use ws::{ - channel as ws_channel, channel_with_config as ws_channel_with_config, - format_ws_message, SendError as WsSendError, WsConfig, WsHandler, WsSender, - DEFAULT_PING_INTERVAL, -}; diff --git a/crates/ag-ui-server/src/transport/sse.rs b/crates/ag-ui-server/src/transport/sse.rs deleted file mode 100644 index 73d69962..00000000 --- a/crates/ag-ui-server/src/transport/sse.rs +++ /dev/null @@ -1,291 +0,0 @@ -//! Server-Sent Events (SSE) Transport -//! -//! This module provides SSE transport for streaming AG-UI events to frontend clients. -//! It integrates with axum to provide HTTP SSE endpoints. -//! -//! # Architecture -//! -//! The SSE transport uses a channel-based design: -//! - [`SseSender`] - Used by agent code to send events into the stream -//! - [`SseHandler`] - Converted to an axum SSE response for the HTTP endpoint -//! -//! # Example -//! -//! ```rust,ignore -//! use ag_ui_server::transport::sse; -//! use ag_ui_core::{Event, TextMessageStartEvent, MessageId}; -//! -//! // Create a channel pair -//! let (sender, handler) = sse::channel::(32); -//! -//! // In your axum handler, return the SSE response -//! async fn events_endpoint() -> impl IntoResponse { -//! let (sender, handler) = sse::channel::(32); -//! -//! // Spawn task to send events -//! tokio::spawn(async move { -//! let event = Event::TextMessageStart( -//! TextMessageStartEvent::new(MessageId::random()) -//! ); -//! sender.send(event).await.ok(); -//! }); -//! -//! handler.into_response() -//! } -//! ``` - -use std::convert::Infallible; -use std::pin::Pin; -use std::task::{Context, Poll}; - -use ag_ui_core::{AgentState, Event, JsonValue}; -use axum::response::sse::{Event as AxumSseEvent, KeepAlive, Sse}; -use axum::response::IntoResponse; -use futures::Stream; -use tokio::sync::mpsc; -use tokio_stream::wrappers::ReceiverStream; - -use crate::error::ServerError; - -/// Error type for SSE send operations. -#[derive(Debug, Clone)] -pub struct SendError(pub T); - -impl std::fmt::Display for SendError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "channel closed") - } -} - -impl std::error::Error for SendError {} - -/// Sender side of an SSE channel. -/// -/// Use this to send AG-UI events that will be streamed to connected clients. -/// Events are serialized to JSON and formatted as SSE data frames. -#[derive(Debug, Clone)] -pub struct SseSender { - sender: mpsc::Sender>, -} - -impl SseSender { - /// Sends an event to the SSE stream. - /// - /// Returns an error if the receiver has been dropped (client disconnected). - pub async fn send(&self, event: Event) -> Result<(), SendError>> { - self.sender.send(event).await.map_err(|e| SendError(e.0)) - } - - /// Sends multiple events to the SSE stream. - /// - /// Stops and returns an error on the first failed send. - pub async fn send_many( - &self, - events: impl IntoIterator>, - ) -> Result<(), SendError>> { - for event in events { - self.send(event).await?; - } - Ok(()) - } - - /// Tries to send an event without waiting. - /// - /// Returns an error if the channel is full or closed. - pub fn try_send(&self, event: Event) -> Result<(), SendError>> { - self.sender.try_send(event).map_err(|e| SendError(e.into_inner())) - } - - /// Checks if the receiver is still connected. - pub fn is_closed(&self) -> bool { - self.sender.is_closed() - } -} - -/// Handler side of an SSE channel. -/// -/// This is converted to an axum SSE response that streams events to the client. -/// Each event is serialized to JSON and sent as an SSE data frame. -pub struct SseHandler { - receiver: mpsc::Receiver>, -} - -impl SseHandler { - /// Converts this handler into an axum SSE response. - /// - /// The response will stream events as they are sent through the corresponding - /// [`SseSender`]. The stream ends when the sender is dropped. - pub fn into_response(self) -> impl IntoResponse { - let stream = SseEventStream { - inner: ReceiverStream::new(self.receiver), - }; - - Sse::new(stream).keep_alive(KeepAlive::default()) - } -} - -/// Internal stream wrapper that converts Events to axum SSE events. -struct SseEventStream { - inner: ReceiverStream>, -} - -impl Stream for SseEventStream { - type Item = Result; - - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - match Pin::new(&mut self.inner).poll_next(cx) { - Poll::Ready(Some(event)) => { - // Serialize event to JSON - let json = match serde_json::to_string(&event) { - Ok(json) => json, - Err(e) => { - // Log error and send error event - eprintln!("SSE serialization error: {}", e); - format!(r#"{{"type":"RUN_ERROR","message":"Serialization error: {}"}}"#, e) - } - }; - - // Create SSE event with the event type as the SSE event name - let sse_event = AxumSseEvent::default() - .event(event.event_type().as_str()) - .data(json); - - Poll::Ready(Some(Ok(sse_event))) - } - Poll::Ready(None) => Poll::Ready(None), - Poll::Pending => Poll::Pending, - } - } -} - -/// Creates a new SSE channel pair. -/// -/// The `buffer` parameter controls how many events can be queued before -/// sends will block (or fail for `try_send`). -/// -/// # Arguments -/// -/// * `buffer` - The capacity of the internal channel buffer -/// -/// # Returns -/// -/// A tuple of (`SseSender`, `SseHandler`) that are connected. -/// -/// # Example -/// -/// ```rust,ignore -/// let (sender, handler) = sse::channel::(32); -/// ``` -pub fn channel(buffer: usize) -> (SseSender, SseHandler) { - let (tx, rx) = mpsc::channel(buffer); - (SseSender { sender: tx }, SseHandler { receiver: rx }) -} - -/// Serializes an event to SSE format. -/// -/// Returns the event formatted as `data: {json}\n\n`. -pub fn format_sse_event(event: &Event) -> Result { - let json = serde_json::to_string(event) - .map_err(|e| ServerError::Serialization(e.to_string()))?; - Ok(format!("data: {}\n\n", json)) -} - -#[cfg(test)] -mod tests { - use super::*; - use ag_ui_core::{ - MessageId, RunErrorEvent, TextMessageContentEvent, TextMessageStartEvent, - }; - - #[tokio::test] - async fn test_channel_creation() { - let (sender, _handler) = channel::(10); - assert!(!sender.is_closed()); - } - - #[tokio::test] - async fn test_send_event() { - let (sender, mut handler) = channel::(10); - - let event: Event = Event::TextMessageStart(TextMessageStartEvent::new(MessageId::random())); - - sender.send(event.clone()).await.unwrap(); - - // Receive from the handler's receiver directly for testing - let received = handler.receiver.recv().await.unwrap(); - assert_eq!(received.event_type(), event.event_type()); - } - - #[tokio::test] - async fn test_send_many_events() { - let (sender, mut handler) = channel::(10); - - let events: Vec = vec![ - Event::TextMessageStart(TextMessageStartEvent::new(MessageId::random())), - Event::TextMessageContent(TextMessageContentEvent::new_unchecked( - MessageId::random(), - "Hello", - )), - Event::RunError(RunErrorEvent::new("test error")), - ]; - - sender.send_many(events.clone()).await.unwrap(); - - // Verify all events received - for expected in &events { - let received = handler.receiver.recv().await.unwrap(); - assert_eq!(received.event_type(), expected.event_type()); - } - } - - #[tokio::test] - async fn test_channel_close_detection() { - let (sender, handler) = channel::(10); - - // Drop the handler - drop(handler); - - // Sender should detect closure - assert!(sender.is_closed()); - - // Send should fail - let event: Event = Event::RunError(RunErrorEvent::new("test")); - let result = sender.send(event).await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn test_try_send() { - let (sender, _handler) = channel::(2); - - let event: Event = Event::RunError(RunErrorEvent::new("test")); - - // First two should succeed (buffer size is 2) - assert!(sender.try_send(event.clone()).is_ok()); - assert!(sender.try_send(event.clone()).is_ok()); - - // Third should fail (buffer full) - assert!(sender.try_send(event).is_err()); - } - - #[test] - fn test_format_sse_event() { - let event: Event = Event::RunError(RunErrorEvent::new("test error")); - let formatted = format_sse_event(&event).unwrap(); - - assert!(formatted.starts_with("data: ")); - assert!(formatted.ends_with("\n\n")); - assert!(formatted.contains("\"type\":\"RUN_ERROR\"")); - assert!(formatted.contains("\"message\":\"test error\"")); - } - - #[test] - fn test_format_sse_event_with_complex_event() { - let event: Event = Event::TextMessageStart(TextMessageStartEvent::new(MessageId::random())); - let formatted = format_sse_event(&event).unwrap(); - - assert!(formatted.contains("\"type\":\"TEXT_MESSAGE_START\"")); - assert!(formatted.contains("\"messageId\":")); - assert!(formatted.contains("\"role\":\"assistant\"")); - } -} diff --git a/crates/ag-ui-server/src/transport/ws.rs b/crates/ag-ui-server/src/transport/ws.rs deleted file mode 100644 index 333e8c68..00000000 --- a/crates/ag-ui-server/src/transport/ws.rs +++ /dev/null @@ -1,443 +0,0 @@ -//! WebSocket Transport for AG-UI Events -//! -//! This module provides WebSocket transport for streaming AG-UI events to frontend clients. -//! It integrates with axum to provide WebSocket endpoints as an alternative to SSE. -//! -//! # Architecture -//! -//! The WebSocket transport uses a channel-based design similar to SSE: -//! - [`WsSender`] - Used by agent code to send events into the WebSocket stream -//! - [`WsHandler`] - Handles the WebSocket connection and streams events -//! -//! # Example -//! -//! ```rust,ignore -//! use ag_ui_server::transport::ws; -//! use ag_ui_core::{Event, TextMessageStartEvent, MessageId}; -//! use axum::extract::ws::WebSocketUpgrade; -//! -//! async fn ws_endpoint(upgrade: WebSocketUpgrade) -> impl IntoResponse { -//! let (sender, handler) = ws::channel::(32); -//! -//! // Spawn task to send events -//! tokio::spawn(async move { -//! let event = Event::TextMessageStart( -//! TextMessageStartEvent::new(MessageId::random()) -//! ); -//! sender.send(event).await.ok(); -//! }); -//! -//! handler.into_response(upgrade) -//! } -//! ``` -//! -//! # SSE vs WebSocket -//! -//! Choose WebSocket when: -//! - You need bidirectional communication (future AG-UI extensions) -//! - You want lower latency for high-frequency updates -//! - You need to work around SSE connection limits in browsers -//! -//! Choose SSE when: -//! - You only need server-to-client streaming (current AG-UI) -//! - You want automatic reconnection (built into EventSource) -//! - You need HTTP/2 multiplexing benefits - -use std::time::Duration; - -use ag_ui_core::{AgentState, Event, JsonValue}; -use axum::extract::ws::{Message, WebSocket, WebSocketUpgrade}; -use axum::response::IntoResponse; -use futures::{SinkExt, StreamExt}; -use tokio::sync::mpsc; -use tokio::time::interval; - -use crate::error::ServerError; - -/// Default ping interval for WebSocket keep-alive (30 seconds). -pub const DEFAULT_PING_INTERVAL: Duration = Duration::from_secs(30); - -/// Error type for WebSocket send operations. -#[derive(Debug, Clone)] -pub struct SendError(pub T); - -impl std::fmt::Display for SendError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "WebSocket channel closed") - } -} - -impl std::error::Error for SendError {} - -/// Configuration for WebSocket connections. -#[derive(Debug, Clone)] -pub struct WsConfig { - /// Interval between ping messages for keep-alive. - pub ping_interval: Duration, - /// Whether to send ping messages. - pub enable_ping: bool, -} - -impl Default for WsConfig { - fn default() -> Self { - Self { - ping_interval: DEFAULT_PING_INTERVAL, - enable_ping: true, - } - } -} - -impl WsConfig { - /// Creates a new configuration with default values. - pub fn new() -> Self { - Self::default() - } - - /// Sets the ping interval. - pub fn ping_interval(mut self, interval: Duration) -> Self { - self.ping_interval = interval; - self - } - - /// Disables ping messages. - pub fn disable_ping(mut self) -> Self { - self.enable_ping = false; - self - } -} - -/// Sender side of a WebSocket channel. -/// -/// Use this to send AG-UI events that will be streamed to connected clients. -/// Events are serialized to JSON and sent as WebSocket text messages. -#[derive(Debug, Clone)] -pub struct WsSender { - sender: mpsc::Sender>, -} - -impl WsSender { - /// Sends an event to the WebSocket stream. - /// - /// Returns an error if the receiver has been dropped (client disconnected). - pub async fn send(&self, event: Event) -> Result<(), SendError>> { - self.sender.send(event).await.map_err(|e| SendError(e.0)) - } - - /// Sends multiple events to the WebSocket stream. - /// - /// Stops and returns an error on the first failed send. - pub async fn send_many( - &self, - events: impl IntoIterator>, - ) -> Result<(), SendError>> { - for event in events { - self.send(event).await?; - } - Ok(()) - } - - /// Tries to send an event without waiting. - /// - /// Returns an error if the channel is full or closed. - pub fn try_send(&self, event: Event) -> Result<(), SendError>> { - self.sender - .try_send(event) - .map_err(|e| SendError(e.into_inner())) - } - - /// Checks if the receiver is still connected. - pub fn is_closed(&self) -> bool { - self.sender.is_closed() - } -} - -/// Handler side of a WebSocket channel. -/// -/// This handles the WebSocket connection and streams events from the sender. -pub struct WsHandler { - receiver: mpsc::Receiver>, - config: WsConfig, -} - -impl WsHandler { - /// Converts a WebSocket upgrade into an axum response. - /// - /// The response will upgrade to WebSocket and stream events as they are - /// sent through the corresponding [`WsSender`]. - pub fn into_response(self, upgrade: WebSocketUpgrade) -> impl IntoResponse { - upgrade.on_upgrade(move |socket| self.handle_socket(socket)) - } - - /// Handles the WebSocket connection. - async fn handle_socket(self, socket: WebSocket) { - let (mut ws_sender, mut ws_receiver) = socket.split(); - let mut event_receiver = self.receiver; - - // Create ping interval if enabled - let mut ping_interval = if self.config.enable_ping { - Some(interval(self.config.ping_interval)) - } else { - None - }; - - loop { - tokio::select! { - // Handle incoming events to send - event = event_receiver.recv() => { - match event { - Some(event) => { - // Serialize event to JSON - let json = match serde_json::to_string(&event) { - Ok(json) => json, - Err(e) => { - eprintln!("WebSocket serialization error: {}", e); - continue; - } - }; - - // Send as text message - if ws_sender.send(Message::Text(json.into())).await.is_err() { - // Client disconnected - break; - } - } - None => { - // Event channel closed, send close frame and exit - let _ = ws_sender.send(Message::Close(None)).await; - break; - } - } - } - - // Handle ping interval - _ = async { - if let Some(ref mut interval) = ping_interval { - interval.tick().await; - } else { - // Never completes if ping disabled - std::future::pending::<()>().await; - } - } => { - if ws_sender.send(Message::Ping(vec![].into())).await.is_err() { - break; - } - } - - // Handle incoming WebSocket messages (for close/pong) - msg = ws_receiver.next() => { - match msg { - Some(Ok(Message::Pong(_))) => { - // Pong received, connection is alive - } - Some(Ok(Message::Close(_))) | None => { - // Client closed connection - break; - } - Some(Ok(_)) => { - // Ignore other message types (Text, Binary) - // AG-UI is unidirectional server->client - } - Some(Err(_)) => { - // WebSocket error - break; - } - } - } - } - } - } -} - -/// Creates a new WebSocket channel pair with default configuration. -/// -/// The `buffer` parameter controls how many events can be queued before -/// sends will block (or fail for `try_send`). -/// -/// # Arguments -/// -/// * `buffer` - The capacity of the internal channel buffer -/// -/// # Returns -/// -/// A tuple of (`WsSender`, `WsHandler`) that are connected. -/// -/// # Example -/// -/// ```rust,ignore -/// let (sender, handler) = ws::channel::(32); -/// ``` -pub fn channel(buffer: usize) -> (WsSender, WsHandler) { - channel_with_config(buffer, WsConfig::default()) -} - -/// Creates a new WebSocket channel pair with custom configuration. -/// -/// # Arguments -/// -/// * `buffer` - The capacity of the internal channel buffer -/// * `config` - WebSocket configuration options -/// -/// # Returns -/// -/// A tuple of (`WsSender`, `WsHandler`) that are connected. -/// -/// # Example -/// -/// ```rust,ignore -/// let config = WsConfig::new() -/// .ping_interval(Duration::from_secs(15)) -/// .disable_ping(); -/// let (sender, handler) = ws::channel_with_config::(32, config); -/// ``` -pub fn channel_with_config( - buffer: usize, - config: WsConfig, -) -> (WsSender, WsHandler) { - let (tx, rx) = mpsc::channel(buffer); - ( - WsSender { sender: tx }, - WsHandler { - receiver: rx, - config, - }, - ) -} - -/// Serializes an event to a WebSocket text message. -/// -/// Returns the JSON string suitable for sending as a WebSocket text frame. -pub fn format_ws_message(event: &Event) -> Result { - serde_json::to_string(event).map_err(|e| ServerError::Serialization(e.to_string())) -} - -#[cfg(test)] -mod tests { - use super::*; - use ag_ui_core::{MessageId, RunErrorEvent, TextMessageContentEvent, TextMessageStartEvent}; - - #[tokio::test] - async fn test_channel_creation() { - let (sender, _handler) = channel::(10); - assert!(!sender.is_closed()); - } - - #[tokio::test] - async fn test_channel_with_config() { - let config = WsConfig::new() - .ping_interval(Duration::from_secs(10)) - .disable_ping(); - - let (sender, handler) = channel_with_config::(10, config); - assert!(!sender.is_closed()); - assert!(!handler.config.enable_ping); - assert_eq!(handler.config.ping_interval, Duration::from_secs(10)); - } - - #[tokio::test] - async fn test_send_event() { - let (sender, mut handler) = channel::(10); - - let event: Event = Event::TextMessageStart(TextMessageStartEvent::new(MessageId::random())); - - sender.send(event.clone()).await.unwrap(); - - // Receive from the handler's receiver directly for testing - let received = handler.receiver.recv().await.unwrap(); - assert_eq!(received.event_type(), event.event_type()); - } - - #[tokio::test] - async fn test_send_many_events() { - let (sender, mut handler) = channel::(10); - - let events: Vec = vec![ - Event::TextMessageStart(TextMessageStartEvent::new(MessageId::random())), - Event::TextMessageContent(TextMessageContentEvent::new_unchecked( - MessageId::random(), - "Hello", - )), - Event::RunError(RunErrorEvent::new("test error")), - ]; - - sender.send_many(events.clone()).await.unwrap(); - - // Verify all events received - for expected in &events { - let received = handler.receiver.recv().await.unwrap(); - assert_eq!(received.event_type(), expected.event_type()); - } - } - - #[tokio::test] - async fn test_channel_close_detection() { - let (sender, handler) = channel::(10); - - // Drop the handler - drop(handler); - - // Sender should detect closure - assert!(sender.is_closed()); - - // Send should fail - let event: Event = Event::RunError(RunErrorEvent::new("test")); - let result = sender.send(event).await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn test_try_send() { - let (sender, _handler) = channel::(2); - - let event: Event = Event::RunError(RunErrorEvent::new("test")); - - // First two should succeed (buffer size is 2) - assert!(sender.try_send(event.clone()).is_ok()); - assert!(sender.try_send(event.clone()).is_ok()); - - // Third should fail (buffer full) - assert!(sender.try_send(event).is_err()); - } - - #[test] - fn test_format_ws_message() { - let event: Event = Event::RunError(RunErrorEvent::new("test error")); - let message = format_ws_message(&event).unwrap(); - - assert!(message.contains("\"type\":\"RUN_ERROR\"")); - assert!(message.contains("\"message\":\"test error\"")); - } - - #[test] - fn test_format_ws_message_complex() { - let event: Event = - Event::TextMessageStart(TextMessageStartEvent::new(MessageId::random())); - let message = format_ws_message(&event).unwrap(); - - assert!(message.contains("\"type\":\"TEXT_MESSAGE_START\"")); - assert!(message.contains("\"messageId\":")); - assert!(message.contains("\"role\":\"assistant\"")); - } - - #[test] - fn test_ws_config_default() { - let config = WsConfig::default(); - assert!(config.enable_ping); - assert_eq!(config.ping_interval, DEFAULT_PING_INTERVAL); - } - - #[test] - fn test_ws_config_builder() { - let config = WsConfig::new() - .ping_interval(Duration::from_secs(60)) - .disable_ping(); - - assert!(!config.enable_ping); - assert_eq!(config.ping_interval, Duration::from_secs(60)); - } - - #[test] - fn test_send_error_display() { - let error: SendError = SendError(42); - assert_eq!(format!("{}", error), "WebSocket channel closed"); - } -} diff --git a/src/agent/mod.rs b/src/agent/mod.rs index 6569b383..d46250ec 100644 --- a/src/agent/mod.rs +++ b/src/agent/mod.rs @@ -310,7 +310,11 @@ pub async fn run_interactive( if session.platform_session.is_project_selected() { println!( "{}", - format!("Platform context: {}", session.platform_session.display_context()).dimmed() + format!( + "Platform context: {}", + session.platform_session.display_context() + ) + .dimmed() ); } diff --git a/src/agent/session/mod.rs b/src/agent/session/mod.rs index d8e8c7db..8b60caff 100644 --- a/src/agent/session/mod.rs +++ b/src/agent/session/mod.rs @@ -262,10 +262,7 @@ impl ChatSession { // Build prompt with platform context if project is selected let prompt = if self.platform_session.is_project_selected() { - format!( - "{} >", - self.platform_session.display_context() - ) + format!("{} >", self.platform_session.display_context()) } else { ">".to_string() }; diff --git a/src/agent/session/ui.rs b/src/agent/session/ui.rs index 0aa852b7..206eeaee 100644 --- a/src/agent/session/ui.rs +++ b/src/agent/session/ui.rs @@ -229,11 +229,7 @@ pub fn print_banner(session: &ChatSession) { "Project:".white(), "(none selected)".dimmed() ); - println!( - " {} {}", - "→".cyan(), - "sync-ctl org list".dimmed() - ); + println!(" {} {}", "→".cyan(), "sync-ctl org list".dimmed()); } // Check for incomplete plans and show a hint diff --git a/src/agent/tools/mod.rs b/src/agent/tools/mod.rs index 9457f122..115642ef 100644 --- a/src/agent/tools/mod.rs +++ b/src/agent/tools/mod.rs @@ -186,12 +186,11 @@ pub use k8s_optimize::K8sOptimizeTool; pub use kubelint::KubelintTool; pub use plan::{PlanCreateTool, PlanListTool, PlanNextTool, PlanUpdateTool}; pub use platform::{ - CheckProviderConnectionTool, CreateDeploymentConfigTool, CurrentContextTool, - DeployServiceTool, GetDeploymentStatusTool, GetServiceLogsTool, - ListDeploymentCapabilitiesTool, ListDeploymentConfigsTool, ListDeploymentsTool, - ListHetznerAvailabilityTool, ListOrganizationsTool, ListProjectsTool, - OpenProviderSettingsTool, SelectProjectTool, SetDeploymentSecretsTool, - TriggerDeploymentTool, + CheckProviderConnectionTool, CreateDeploymentConfigTool, CurrentContextTool, DeployServiceTool, + GetDeploymentStatusTool, GetServiceLogsTool, ListDeploymentCapabilitiesTool, + ListDeploymentConfigsTool, ListDeploymentsTool, ListHetznerAvailabilityTool, + ListOrganizationsTool, ListProjectsTool, OpenProviderSettingsTool, SelectProjectTool, + SetDeploymentSecretsTool, TriggerDeploymentTool, }; pub use prometheus_connect::PrometheusConnectTool; pub use prometheus_discover::PrometheusDiscoverTool; diff --git a/src/agent/tools/platform/analyze_codebase.rs b/src/agent/tools/platform/analyze_codebase.rs index 4cdbdc06..78e62b55 100644 --- a/src/agent/tools/platform/analyze_codebase.rs +++ b/src/agent/tools/platform/analyze_codebase.rs @@ -12,8 +12,7 @@ use std::path::Path; use crate::agent::tools::error::{ErrorCategory, format_error_for_llm}; use crate::analyzer::{ - AnalysisConfig, ProjectAnalysis, ProjectType, TechnologyCategory, - analyze_project_with_config, + AnalysisConfig, ProjectAnalysis, ProjectType, TechnologyCategory, analyze_project_with_config, }; /// Arguments for the analyze codebase tool @@ -373,7 +372,11 @@ fn infer_dockerfile_base(analysis: &ProjectAnalysis) -> Option { match lang.name.to_lowercase().as_str() { "javascript" | "typescript" => { // Check for Bun - if analysis.technologies.iter().any(|t| t.name.to_lowercase() == "bun") { + if analysis + .technologies + .iter() + .any(|t| t.name.to_lowercase() == "bun") + { return Some("oven/bun:1-alpine".to_string()); } return Some("node:20-alpine".to_string()); @@ -402,15 +405,26 @@ fn determine_next_steps(analysis: &ProjectAnalysis) -> Vec { if has_dockerfile { steps.push("Use analyze_project to get specific Dockerfile details".to_string()); - steps.push("Use list_deployment_capabilities to see available deployment targets".to_string()); + steps.push( + "Use list_deployment_capabilities to see available deployment targets".to_string(), + ); steps.push("Use create_deployment_config to create a deployment configuration".to_string()); } else { - steps.push("Create a Dockerfile for your application (recommended base image in deployment_hints)".to_string()); - steps.push("After creating Dockerfile, use analyze_project to verify it's detected".to_string()); + steps.push( + "Create a Dockerfile for your application (recommended base image in deployment_hints)" + .to_string(), + ); + steps.push( + "After creating Dockerfile, use analyze_project to verify it's detected".to_string(), + ); } if !analysis.environment_variables.is_empty() { - let required_count = analysis.environment_variables.iter().filter(|e| e.required).count(); + let required_count = analysis + .environment_variables + .iter() + .filter(|e| e.required) + .count(); if required_count > 0 { steps.push(format!( "Configure {} required environment variable{} before deployment", diff --git a/src/agent/tools/platform/check_provider_connection.rs b/src/agent/tools/platform/check_provider_connection.rs index 3f3eee92..1b9efdba 100644 --- a/src/agent/tools/platform/check_provider_connection.rs +++ b/src/agent/tools/platform/check_provider_connection.rs @@ -108,7 +108,10 @@ This tool NEVER returns actual credentials - only connection status. return Ok(format_error_for_llm( "check_provider_connection", ErrorCategory::ValidationFailed, - &format!("Invalid provider: '{}'. Must be one of: gcp, aws, azure, hetzner", args.provider), + &format!( + "Invalid provider: '{}'. Must be one of: gcp, aws, azure, hetzner", + args.provider + ), Some(vec![ "Use 'gcp' for Google Cloud Platform", "Use 'aws' for Amazon Web Services", @@ -128,7 +131,10 @@ This tool NEVER returns actual credentials - only connection status. }; // Check the connection status - match client.check_provider_connection(&provider, &args.project_id).await { + match client + .check_provider_connection(&provider, &args.project_id) + .await + { Ok(Some(status)) => { // Provider is connected let result = json!({ @@ -141,8 +147,9 @@ This tool NEVER returns actual credentials - only connection status. // NOTE: We intentionally do NOT include any credential values here }); - serde_json::to_string_pretty(&result) - .map_err(|e| CheckProviderConnectionError(format!("Failed to serialize: {}", e))) + serde_json::to_string_pretty(&result).map_err(|e| { + CheckProviderConnectionError(format!("Failed to serialize: {}", e)) + }) } Ok(None) => { // Provider is NOT connected @@ -159,8 +166,9 @@ This tool NEVER returns actual credentials - only connection status. ] }); - serde_json::to_string_pretty(&result) - .map_err(|e| CheckProviderConnectionError(format!("Failed to serialize: {}", e))) + serde_json::to_string_pretty(&result).map_err(|e| { + CheckProviderConnectionError(format!("Failed to serialize: {}", e)) + }) } Err(e) => Ok(format_api_error("check_provider_connection", e)), } @@ -251,7 +259,10 @@ mod tests { #[test] fn test_tool_name() { - assert_eq!(CheckProviderConnectionTool::NAME, "check_provider_connection"); + assert_eq!( + CheckProviderConnectionTool::NAME, + "check_provider_connection" + ); } #[test] diff --git a/src/agent/tools/platform/create_deployment_config.rs b/src/agent/tools/platform/create_deployment_config.rs index ff42a5e2..790e44f3 100644 --- a/src/agent/tools/platform/create_deployment_config.rs +++ b/src/agent/tools/platform/create_deployment_config.rs @@ -8,7 +8,10 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use crate::agent::tools::error::{ErrorCategory, format_error_for_llm}; -use crate::platform::api::types::{CloudProvider, CloudRunnerConfigInput, CreateDeploymentConfigRequest, build_cloud_runner_config_v2}; +use crate::platform::api::types::{ + CloudProvider, CloudRunnerConfigInput, CreateDeploymentConfigRequest, + build_cloud_runner_config_v2, +}; use crate::platform::api::{PlatformApiClient, PlatformApiError}; use std::str::FromStr; @@ -312,7 +315,10 @@ A deployment config defines how to build and deploy a service, including: let mut subscription_id = None; if let Some(ref provider) = provider_enum { if matches!(provider, CloudProvider::Gcp | CloudProvider::Azure) { - if let Ok(credential) = client.check_provider_connection(provider, &args.project_id).await { + if let Ok(credential) = client + .check_provider_connection(provider, &args.project_id) + .await + { if let Some(cred) = credential { match provider { CloudProvider::Gcp => gcp_project_id = cred.provider_account_id, diff --git a/src/agent/tools/platform/deploy_service.rs b/src/agent/tools/platform/deploy_service.rs index 9647cc99..65a647d6 100644 --- a/src/agent/tools/platform/deploy_service.rs +++ b/src/agent/tools/platform/deploy_service.rs @@ -19,14 +19,14 @@ use crate::platform::api::types::{ }; use super::set_secrets::{SecretPromptResult, default_true, prompt_secret_value}; -use crate::platform::api::{PlatformApiClient, PlatformApiError, TriggerDeploymentRequest}; use crate::platform::PlatformSession; +use crate::platform::api::{PlatformApiClient, PlatformApiError, TriggerDeploymentRequest}; use crate::wizard::{ - RecommendationInput, recommend_deployment, get_provider_deployment_statuses, - get_hetzner_regions_dynamic, get_hetzner_server_types_dynamic, HetznerFetchResult, - DynamicCloudRegion, DynamicMachineType, discover_env_files, parse_env_file, - get_available_endpoints, filter_endpoints_for_provider, match_env_vars_to_services, - extract_network_endpoints, + DynamicCloudRegion, DynamicMachineType, HetznerFetchResult, RecommendationInput, + discover_env_files, extract_network_endpoints, filter_endpoints_for_provider, + get_available_endpoints, get_hetzner_regions_dynamic, get_hetzner_server_types_dynamic, + get_provider_deployment_statuses, match_env_vars_to_services, parse_env_file, + recommend_deployment, }; use std::process::Command; @@ -297,7 +297,10 @@ User: "deploy this service" "deploy_service", ErrorCategory::FileNotFound, &format!("Path not found: {}", analysis_path.display()), - Some(vec!["Check if the path exists", "Use list_directory to explore"]), + Some(vec![ + "Check if the path exists", + "Use list_directory to explore", + ]), )); } @@ -382,23 +385,30 @@ User: "deploy this service" }; // Resolve environment name for display - let (resolved_env_id, resolved_env_name, is_production) = if let Some(ref env_id) = environment_id { - let env = environments.iter().find(|e| e.id == *env_id); - let name = env.map(|e| e.name.clone()).unwrap_or_else(|| "Unknown".to_string()); - let is_prod = name.to_lowercase().contains("prod"); - (env_id.clone(), name, is_prod) - } else if let Some(existing) = &existing_config { - // Use the environment from existing config - let env = environments.iter().find(|e| e.id == existing.environment_id); - let name = env.map(|e| e.name.clone()).unwrap_or_else(|| "Unknown".to_string()); - let is_prod = name.to_lowercase().contains("prod"); - (existing.environment_id.clone(), name, is_prod) - } else if let Some(first_env) = environments.first() { - let is_prod = first_env.name.to_lowercase().contains("prod"); - (first_env.id.clone(), first_env.name.clone(), is_prod) - } else { - ("".to_string(), "No environment".to_string(), false) - }; + let (resolved_env_id, resolved_env_name, is_production) = + if let Some(ref env_id) = environment_id { + let env = environments.iter().find(|e| e.id == *env_id); + let name = env + .map(|e| e.name.clone()) + .unwrap_or_else(|| "Unknown".to_string()); + let is_prod = name.to_lowercase().contains("prod"); + (env_id.clone(), name, is_prod) + } else if let Some(existing) = &existing_config { + // Use the environment from existing config + let env = environments + .iter() + .find(|e| e.id == existing.environment_id); + let name = env + .map(|e| e.name.clone()) + .unwrap_or_else(|| "Unknown".to_string()); + let is_prod = name.to_lowercase().contains("prod"); + (existing.environment_id.clone(), name, is_prod) + } else if let Some(first_env) = environments.first() { + let is_prod = first_env.name.to_lowercase().contains("prod"); + (first_env.id.clone(), first_env.name.clone(), is_prod) + } else { + ("".to_string(), "No environment".to_string(), false) + }; // 6. Get available providers let capabilities = match get_provider_deployment_statuses(&client, &project_id).await { @@ -447,7 +457,8 @@ User: "deploy this service" // 6.5. For Hetzner deployments, fetch real-time availability and update recommendations // We require real-time data - no static fallback allowed - let final_provider_for_check = args.provider + let final_provider_for_check = args + .provider .as_ref() .and_then(|p| CloudProvider::from_str(p).ok()) .unwrap_or(recommendation.provider.clone()); @@ -486,7 +497,10 @@ User: "deploy this service" return Ok(format_error_for_llm( "deploy_service", ErrorCategory::NetworkError, - &format!("Cannot recommend Hetzner deployment: Failed to fetch availability - {}", err), + &format!( + "Cannot recommend Hetzner deployment: Failed to fetch availability - {}", + err + ), Some(vec![ "Use list_hetzner_availability to check current status", "Or specify a different provider (e.g., provider='gcp')", @@ -496,7 +510,13 @@ User: "deploy this service" }; // Fetch server types with optional location filter - let server_types = match get_hetzner_server_types_dynamic(&client, &project_id, args.region.as_deref()).await { + let server_types = match get_hetzner_server_types_dynamic( + &client, + &project_id, + args.region.as_deref(), + ) + .await + { HetznerFetchResult::Success(s) if !s.is_empty() => s, HetznerFetchResult::Success(_) => { return Ok(format_error_for_llm( @@ -524,32 +544,51 @@ User: "deploy this service" return Ok(format_error_for_llm( "deploy_service", ErrorCategory::NetworkError, - &format!("Cannot recommend Hetzner deployment: Failed to fetch server types - {}", err), - Some(vec!["Use list_hetzner_availability to check current status"]), + &format!( + "Cannot recommend Hetzner deployment: Failed to fetch server types - {}", + err + ), + Some(vec![ + "Use list_hetzner_availability to check current status", + ]), )); } }; // Store for later use in recommendations - hetzner_availability = Some(HetznerAvailabilityData { regions, server_types }); + hetzner_availability = Some(HetznerAvailabilityData { + regions, + server_types, + }); } // 7. Extract analysis summary - let primary_language = analysis.languages.first() + let primary_language = analysis + .languages + .first() .map(|l| l.name.clone()) .unwrap_or_else(|| "Unknown".to_string()); - let primary_framework = analysis.technologies.iter() - .find(|t| matches!(t.category, TechnologyCategory::BackendFramework | TechnologyCategory::MetaFramework)) + let primary_framework = analysis + .technologies + .iter() + .find(|t| { + matches!( + t.category, + TechnologyCategory::BackendFramework | TechnologyCategory::MetaFramework + ) + }) .map(|t| t.name.clone()) .unwrap_or_else(|| "None detected".to_string()); - let has_dockerfile = analysis.docker_analysis + let has_dockerfile = analysis + .docker_analysis .as_ref() .map(|d| !d.dockerfiles.is_empty()) .unwrap_or(false); - let has_k8s = analysis.infrastructure + let has_k8s = analysis + .infrastructure .as_ref() .map(|i| i.has_kubernetes) .unwrap_or(false); @@ -557,7 +596,9 @@ User: "deploy this service" // 10. If preview_only, return recommendation if args.preview_only { // Build the deployment mode info - let (deployment_mode, mode_explanation, next_steps) = if let Some(existing) = &existing_config { + let (deployment_mode, mode_explanation, next_steps) = if let Some(existing) = + &existing_config + { ( "REDEPLOY", format!( @@ -588,105 +629,145 @@ User: "deploy this service" // Production warning let production_warning = if is_production { - Some("⚠️ WARNING: This will deploy to PRODUCTION environment. Please confirm you intend to deploy to production.") + Some( + "⚠️ WARNING: This will deploy to PRODUCTION environment. Please confirm you intend to deploy to production.", + ) } else { None }; // For Hetzner, use real-time availability to select best options - let (final_machine_type, final_region, machine_reasoning, region_reasoning, price_monthly) = - if let Some(ref hetzner) = hetzner_availability { - // SMART SELECTION: Find the best region + machine combination - // Strategy: Find cheapest machine with 4GB+ that's actually available somewhere - - // First, find all server types that are actually available (non-empty available_in) - let available_types: Vec<_> = hetzner.server_types.iter() - .filter(|st| !st.available_in.is_empty()) - .collect(); - - // If user specified a region, check if anything is available there - let user_region = args.region.as_deref(); - - // Find best machine: cheapest with 4GB+ that's available - let best_machine_with_region = if let Some(region) = user_region { - // User specified region - find best machine for that region - available_types.iter() - .filter(|st| st.memory_gb >= 4.0 && st.available_in.contains(®ion.to_string())) - .min_by(|a, b| a.price_monthly.partial_cmp(&b.price_monthly).unwrap()) - .map(|st| (*st, region.to_string())) - .or_else(|| { - // No 4GB+ available in that region, try any machine - available_types.iter() - .filter(|st| st.available_in.contains(®ion.to_string())) - .min_by(|a, b| a.price_monthly.partial_cmp(&b.price_monthly).unwrap()) - .map(|st| (*st, region.to_string())) - }) - } else { - // No region specified - find globally cheapest 4GB+ machine and use its best region - available_types.iter() - .filter(|st| st.memory_gb >= 4.0) - .min_by(|a, b| a.price_monthly.partial_cmp(&b.price_monthly).unwrap()) - .map(|st| { - // Pick the first available region for this machine - let region = st.available_in.first() - .cloned() - .unwrap_or_else(|| "nbg1".to_string()); - (*st, region) - }) - .or_else(|| { - // No 4GB+ available anywhere, find any cheapest machine - available_types.iter() - .min_by(|a, b| a.price_monthly.partial_cmp(&b.price_monthly).unwrap()) - .map(|st| { - let region = st.available_in.first() - .cloned() - .unwrap_or_else(|| "nbg1".to_string()); - (*st, region) - }) - }) - }; - - if let Some((machine, region_id)) = best_machine_with_region { - let region_name = hetzner.regions.iter() - .find(|r| r.id == region_id) - .map(|r| format!("{}, {}", r.name, r.location)) - .unwrap_or_else(|| region_id.clone()); + let ( + final_machine_type, + final_region, + machine_reasoning, + region_reasoning, + price_monthly, + ) = if let Some(ref hetzner) = hetzner_availability { + // SMART SELECTION: Find the best region + machine combination + // Strategy: Find cheapest machine with 4GB+ that's actually available somewhere + + // First, find all server types that are actually available (non-empty available_in) + let available_types: Vec<_> = hetzner + .server_types + .iter() + .filter(|st| !st.available_in.is_empty()) + .collect(); + + // If user specified a region, check if anything is available there + let user_region = args.region.as_deref(); + + // Find best machine: cheapest with 4GB+ that's available + let best_machine_with_region = if let Some(region) = user_region { + // User specified region - find best machine for that region + available_types + .iter() + .filter(|st| { + st.memory_gb >= 4.0 && st.available_in.contains(®ion.to_string()) + }) + .min_by(|a, b| a.price_monthly.partial_cmp(&b.price_monthly).unwrap()) + .map(|st| (*st, region.to_string())) + .or_else(|| { + // No 4GB+ available in that region, try any machine + available_types + .iter() + .filter(|st| st.available_in.contains(®ion.to_string())) + .min_by(|a, b| { + a.price_monthly.partial_cmp(&b.price_monthly).unwrap() + }) + .map(|st| (*st, region.to_string())) + }) + } else { + // No region specified - find globally cheapest 4GB+ machine and use its best region + available_types + .iter() + .filter(|st| st.memory_gb >= 4.0) + .min_by(|a, b| a.price_monthly.partial_cmp(&b.price_monthly).unwrap()) + .map(|st| { + // Pick the first available region for this machine + let region = st + .available_in + .first() + .cloned() + .unwrap_or_else(|| "nbg1".to_string()); + (*st, region) + }) + .or_else(|| { + // No 4GB+ available anywhere, find any cheapest machine + available_types + .iter() + .min_by(|a, b| { + a.price_monthly.partial_cmp(&b.price_monthly).unwrap() + }) + .map(|st| { + let region = st + .available_in + .first() + .cloned() + .unwrap_or_else(|| "nbg1".to_string()); + (*st, region) + }) + }) + }; - let available_count = hetzner.regions.iter() - .find(|r| r.id == region_id) - .map(|r| r.available_server_types.len()) - .unwrap_or(0); + if let Some((machine, region_id)) = best_machine_with_region { + let region_name = hetzner + .regions + .iter() + .find(|r| r.id == region_id) + .map(|r| format!("{}, {}", r.name, r.location)) + .unwrap_or_else(|| region_id.clone()); + + let available_count = hetzner + .regions + .iter() + .find(|r| r.id == region_id) + .map(|r| r.available_server_types.len()) + .unwrap_or(0); - ( - args.machine_type.clone().unwrap_or_else(|| machine.id.clone()), - region_id.clone(), - format!( - "Selected {} ({} vCPU, {:.0} GB RAM) - cheapest AVAILABLE option at €{:.2}/mo", - machine.id, machine.cores, machine.memory_gb, machine.price_monthly - ), - format!("Selected {} ({}) - {} server types available", region_id, region_name, available_count), - Some(machine.price_monthly), - ) - } else { - // No server types available anywhere - this shouldn't happen if we passed validation - ( - args.machine_type.clone().unwrap_or_else(|| recommendation.machine_type.clone()), - args.region.clone().unwrap_or_else(|| recommendation.region.clone()), - "WARNING: No server types currently available - using fallback".to_string(), - "Using fallback region".to_string(), - None, - ) - } + ( + args.machine_type + .clone() + .unwrap_or_else(|| machine.id.clone()), + region_id.clone(), + format!( + "Selected {} ({} vCPU, {:.0} GB RAM) - cheapest AVAILABLE option at €{:.2}/mo", + machine.id, machine.cores, machine.memory_gb, machine.price_monthly + ), + format!( + "Selected {} ({}) - {} server types available", + region_id, region_name, available_count + ), + Some(machine.price_monthly), + ) } else { - // Non-Hetzner provider - use static recommendation + // No server types available anywhere - this shouldn't happen if we passed validation ( - args.machine_type.clone().unwrap_or_else(|| recommendation.machine_type.clone()), - args.region.clone().unwrap_or_else(|| recommendation.region.clone()), - recommendation.machine_reasoning.clone(), - recommendation.region_reasoning.clone(), + args.machine_type + .clone() + .unwrap_or_else(|| recommendation.machine_type.clone()), + args.region + .clone() + .unwrap_or_else(|| recommendation.region.clone()), + "WARNING: No server types currently available - using fallback".to_string(), + "Using fallback region".to_string(), None, ) - }; + } + } else { + // Non-Hetzner provider - use static recommendation + ( + args.machine_type + .clone() + .unwrap_or_else(|| recommendation.machine_type.clone()), + args.region + .clone() + .unwrap_or_else(|| recommendation.region.clone()), + recommendation.machine_reasoning.clone(), + recommendation.region_reasoning.clone(), + None, + ) + }; // Build availability info for response let hetzner_availability_info = hetzner_availability.as_ref().map(|h| { @@ -994,7 +1075,8 @@ User: "deploy this service" } // NEW DEPLOYMENT PATH - no existing config found - let final_provider = args.provider + let final_provider = args + .provider .as_ref() .and_then(|p| CloudProvider::from_str(p).ok()) .unwrap_or(recommendation.provider.clone()); @@ -1004,7 +1086,9 @@ User: "deploy this service" // SMART SELECTION: Same logic as preview // Find all server types that are actually available (non-empty available_in) - let available_types: Vec<_> = hetzner.server_types.iter() + let available_types: Vec<_> = hetzner + .server_types + .iter() .filter(|st| !st.available_in.is_empty()) .collect(); @@ -1013,32 +1097,42 @@ User: "deploy this service" // Find best machine: cheapest with 4GB+ that's available let best_machine_with_region = if let Some(region) = user_region { // User specified region - find best machine for that region - available_types.iter() - .filter(|st| st.memory_gb >= 4.0 && st.available_in.contains(®ion.to_string())) + available_types + .iter() + .filter(|st| { + st.memory_gb >= 4.0 && st.available_in.contains(®ion.to_string()) + }) .min_by(|a, b| a.price_monthly.partial_cmp(&b.price_monthly).unwrap()) .map(|st| (st.id.clone(), region.to_string())) .or_else(|| { - available_types.iter() + available_types + .iter() .filter(|st| st.available_in.contains(®ion.to_string())) .min_by(|a, b| a.price_monthly.partial_cmp(&b.price_monthly).unwrap()) .map(|st| (st.id.clone(), region.to_string())) }) } else { // No region specified - find globally cheapest 4GB+ machine - available_types.iter() + available_types + .iter() .filter(|st| st.memory_gb >= 4.0) .min_by(|a, b| a.price_monthly.partial_cmp(&b.price_monthly).unwrap()) .map(|st| { - let region = st.available_in.first() + let region = st + .available_in + .first() .cloned() .unwrap_or_else(|| "nbg1".to_string()); (st.id.clone(), region) }) .or_else(|| { - available_types.iter() + available_types + .iter() .min_by(|a, b| a.price_monthly.partial_cmp(&b.price_monthly).unwrap()) .map(|st| { - let region = st.available_in.first() + let region = st + .available_in + .first() .cloned() .unwrap_or_else(|| "nbg1".to_string()); (st.id.clone(), region) @@ -1054,21 +1148,28 @@ User: "deploy this service" } else { // Fallback to static defaults ( - args.machine_type.clone().unwrap_or_else(|| recommendation.machine_type.clone()), - args.region.clone().unwrap_or_else(|| recommendation.region.clone()), + args.machine_type + .clone() + .unwrap_or_else(|| recommendation.machine_type.clone()), + args.region + .clone() + .unwrap_or_else(|| recommendation.region.clone()), ) } } else { // Non-Hetzner or no availability data - use static defaults - let machine = args.machine_type.clone() + let machine = args + .machine_type + .clone() .unwrap_or_else(|| recommendation.machine_type.clone()); - let region = args.region.clone() + let region = args + .region + .clone() .unwrap_or_else(|| recommendation.region.clone()); (machine, region) }; - let final_port = args.port - .unwrap_or(recommendation.port); + let final_port = args.port.unwrap_or(recommendation.port); // Get repository info let repositories = match client.list_project_repositories(&project_id).await { @@ -1135,17 +1236,22 @@ User: "deploy this service" // // NOT: // dockerfile: "Dockerfile", context: "." (would look at repo root) - let (dockerfile_path, build_context) = analysis.docker_analysis + let (dockerfile_path, build_context) = analysis + .docker_analysis .as_ref() .and_then(|d| d.dockerfiles.first()) .map(|df| { // Get dockerfile filename (e.g., "Dockerfile" or "Dockerfile.prod") - let dockerfile_name = df.path.file_name() + let dockerfile_name = df + .path + .file_name() .map(|n| n.to_string_lossy().to_string()) .unwrap_or_else(|| "Dockerfile".to_string()); // Derive dockerfile's directory relative to analysis_path - let analysis_relative_dir = df.path.parent() + let analysis_relative_dir = df + .path + .parent() .and_then(|p| p.strip_prefix(&analysis_path).ok()) .map(|p| p.to_string_lossy().to_string()) .unwrap_or_default(); @@ -1159,19 +1265,28 @@ User: "deploy this service" if analysis_relative_dir.is_empty() { (dockerfile_name, ".".to_string()) } else { - (format!("{}/{}", analysis_relative_dir, dockerfile_name), analysis_relative_dir) + ( + format!("{}/{}", analysis_relative_dir, dockerfile_name), + analysis_relative_dir, + ) } } else { // Analyzing a subdirectory - prepend subpath to make repo-root-relative if analysis_relative_dir.is_empty() { // Dockerfile at root of analyzed subdir // e.g., subpath="services/contact-intelligence" -> dockerfile="services/contact-intelligence/Dockerfile" - (format!("{}/{}", subpath, dockerfile_name), subpath.to_string()) + ( + format!("{}/{}", subpath, dockerfile_name), + subpath.to_string(), + ) } else { // Dockerfile in nested dir within analyzed subdir // e.g., subpath="services", analysis_relative_dir="contact-intelligence" let full_context = format!("{}/{}", subpath, analysis_relative_dir); - (format!("{}/{}", full_context, dockerfile_name), full_context) + ( + format!("{}/{}", full_context, dockerfile_name), + full_context, + ) } } }) @@ -1196,7 +1311,10 @@ User: "deploy this service" let mut gcp_project_id = None; let mut subscription_id = None; if matches!(final_provider, CloudProvider::Gcp | CloudProvider::Azure) { - if let Ok(Some(cred)) = client.check_provider_connection(&final_provider, &project_id).await { + if let Ok(Some(cred)) = client + .check_provider_connection(&final_provider, &project_id) + .await + { match final_provider { CloudProvider::Gcp => gcp_project_id = cred.provider_account_id, CloudProvider::Azure => subscription_id = cred.provider_account_id, @@ -1206,15 +1324,20 @@ User: "deploy this service" } // Determine CPU/memory from args or recommendation - let final_cpu = args.cpu.clone() - .or_else(|| recommendation.cpu.clone()); - let final_memory = args.memory.clone() + let final_cpu = args.cpu.clone().or_else(|| recommendation.cpu.clone()); + let final_memory = args + .memory + .clone() .or_else(|| recommendation.memory.clone()); let config_input = CloudRunnerConfigInput { provider: Some(final_provider.clone()), region: Some(final_region.clone()), - server_type: if final_provider == CloudProvider::Hetzner { Some(final_machine.clone()) } else { None }, + server_type: if final_provider == CloudProvider::Hetzner { + Some(final_machine.clone()) + } else { + None + }, gcp_project_id, cpu: final_cpu.clone(), memory: final_memory.clone(), @@ -1243,7 +1366,9 @@ User: "deploy this service" "deploy_service", ErrorCategory::ValidationFailed, "Secret entry cancelled by user", - Some(vec!["The user cancelled secret input. Try again when ready."]), + Some(vec![ + "The user cancelled secret input. Try again when ready.", + ]), )); } } @@ -1256,7 +1381,11 @@ User: "deploy this service" is_secret: sk.is_secret, }); } - if resolved.is_empty() { None } else { Some(resolved) } + if resolved.is_empty() { + None + } else { + Some(resolved) + } } else { None }; @@ -1278,11 +1407,14 @@ User: "deploy this service" build_context: Some(build_context.clone()), context: Some(build_context.clone()), port: final_port as i32, - branch: repo.default_branch.clone().unwrap_or_else(|| "main".to_string()), + branch: repo + .default_branch + .clone() + .unwrap_or_else(|| "main".to_string()), target_type: recommendation.target.as_str().to_string(), cloud_provider: final_provider.as_str().to_string(), environment_id: resolved_env_id.clone(), - cluster_id: None, // Cloud Runner doesn't need cluster + cluster_id: None, // Cloud Runner doesn't need cluster registry_id: None, // Auto-provision auto_deploy_enabled: true, is_public: Some(args.is_public), @@ -1385,7 +1517,13 @@ fn parse_repo_from_url(url: &str) -> Option { // HTTPS format: https://github.com/owner/repo.git if url.starts_with("https://") || url.starts_with("http://") { - if let Some(path) = url.split('/').skip(3).collect::>().join("/").strip_suffix(".git") { + if let Some(path) = url + .split('/') + .skip(3) + .collect::>() + .join("/") + .strip_suffix(".git") + { return Some(path.to_string()); } // Without .git suffix @@ -1404,12 +1542,15 @@ fn find_matching_repository<'a>( project_path: &PathBuf, ) -> Option<&'a ProjectRepository> { // First, try to detect from local git remote - if let Some(detected_name) = detect_git_remote(project_path).and_then(|url| parse_repo_from_url(&url)) { + if let Some(detected_name) = + detect_git_remote(project_path).and_then(|url| parse_repo_from_url(&url)) + { tracing::debug!("Detected local git remote: {}", detected_name); - if let Some(repo) = repositories.iter().find(|r| { - r.repository_full_name.eq_ignore_ascii_case(&detected_name) - }) { + if let Some(repo) = repositories + .iter() + .find(|r| r.repository_full_name.eq_ignore_ascii_case(&detected_name)) + { tracing::debug!("Matched detected repo: {}", repo.repository_full_name); return Some(repo); } @@ -1418,9 +1559,12 @@ fn find_matching_repository<'a>( // Fall back: find first non-GitOps repository // GitOps repos are typically infrastructure/config repos, not application repos if let Some(repo) = repositories.iter().find(|r| { - r.is_primary_git_ops != Some(true) && - !r.repository_full_name.to_lowercase().contains("infrastructure") && - !r.repository_full_name.to_lowercase().contains("gitops") + r.is_primary_git_ops != Some(true) + && !r + .repository_full_name + .to_lowercase() + .contains("infrastructure") + && !r.repository_full_name.to_lowercase().contains("gitops") }) { tracing::debug!("Using non-gitops repo: {}", repo.repository_full_name); return Some(repo); @@ -1516,10 +1660,7 @@ mod tests { get_service_name(&PathBuf::from("/path/to/my_service")), "my-service" ); - assert_eq!( - get_service_name(&PathBuf::from("/path/to/MyApp")), - "myapp" - ); + assert_eq!(get_service_name(&PathBuf::from("/path/to/MyApp")), "myapp"); assert_eq!( get_service_name(&PathBuf::from("/path/to/api-service")), "api-service" @@ -1551,6 +1692,10 @@ mod tests { }; let result = tool.call(args).await.unwrap(); - assert!(result.contains("error") || result.contains("not found") || result.contains("Path not found")); + assert!( + result.contains("error") + || result.contains("not found") + || result.contains("Path not found") + ); } } diff --git a/src/agent/tools/platform/get_deployment_status.rs b/src/agent/tools/platform/get_deployment_status.rs index 834674f9..bd4109eb 100644 --- a/src/agent/tools/platform/get_deployment_status.rs +++ b/src/agent/tools/platform/get_deployment_status.rs @@ -133,27 +133,32 @@ also check if the service has a public URL (meaning it's actually ready). // Also check actual deployment if project_id and service_name provided // This is crucial for Cloud Runner where task completes but service takes longer - let (service_status, public_url, service_ready) = if let (Some(project_id), Some(service_name)) = (&args.project_id, &args.service_name) { - match client.list_deployments(project_id, Some(10)).await { - Ok(paginated) => { - // Find the deployment for this service - let deployment = paginated.data.iter() - .find(|d| d.service_name.eq_ignore_ascii_case(service_name)); + let (service_status, public_url, service_ready) = + if let (Some(project_id), Some(service_name)) = + (&args.project_id, &args.service_name) + { + match client.list_deployments(project_id, Some(10)).await { + Ok(paginated) => { + // Find the deployment for this service + let deployment = paginated + .data + .iter() + .find(|d| d.service_name.eq_ignore_ascii_case(service_name)); - match deployment { - Some(d) => ( - Some(d.status.clone()), - d.public_url.clone(), - d.public_url.is_some() && d.status == "running" - ), - None => (None, None, false) + match deployment { + Some(d) => ( + Some(d.status.clone()), + d.public_url.clone(), + d.public_url.is_some() && d.status == "running", + ), + None => (None, None, false), + } } + Err(_) => (None, None, false), } - Err(_) => (None, None, false) - } - } else { - (None, None, false) - }; + } else { + (None, None, false) + }; // True completion = task done AND (service has URL or no service check requested) let truly_ready = if args.project_id.is_some() { @@ -200,7 +205,10 @@ also check if the service has a public URL (meaning it's actually ready). result["action"] = json!("STOP_POLLING"); } else if truly_ready && public_url.is_some() { result["next_steps"] = json!([ - format!("STOP - Service is live at: {}", public_url.as_ref().unwrap()), + format!( + "STOP - Service is live at: {}", + public_url.as_ref().unwrap() + ), "Deployment completed successfully!", "Inform the user their service is ready" ]); @@ -214,10 +222,15 @@ also check if the service has a public URL (meaning it's actually ready). ]); result["action"] = json!("INFORM_USER_AND_WAIT"); result["estimated_wait"] = json!("1-2 minutes"); - result["note"] = json!("Task shows 100% but container is still being built/deployed. This is normal. DO NOT poll repeatedly - inform the user and wait for them to ask for status."); + result["note"] = json!( + "Task shows 100% but container is still being built/deployed. This is normal. DO NOT poll repeatedly - inform the user and wait for them to ask for status." + ); } else if !task_complete { result["next_steps"] = json!([ - format!("STOP POLLING - Deployment is {} ({}% complete)", status.overall_status, status.progress), + format!( + "STOP POLLING - Deployment is {} ({}% complete)", + status.overall_status, status.progress + ), "Inform the user of current progress", "Tell them to wait and ask again in 30 seconds if they want an update", "DO NOT call get_deployment_status again automatically" diff --git a/src/agent/tools/platform/list_deployment_capabilities.rs b/src/agent/tools/platform/list_deployment_capabilities.rs index b46a8808..d62880d2 100644 --- a/src/agent/tools/platform/list_deployment_capabilities.rs +++ b/src/agent/tools/platform/list_deployment_capabilities.rs @@ -175,12 +175,28 @@ targets are available (clusters, registries, Cloud Run). let summary = if available_connected_count == 0 { "No available providers connected. Connect GCP, Hetzner, or Azure in platform settings.".to_string() } else { - let mut parts = vec![format!("{} provider{} ready", available_connected_count, if available_connected_count == 1 { "" } else { "s" })]; + let mut parts = vec![format!( + "{} provider{} ready", + available_connected_count, + if available_connected_count == 1 { + "" + } else { + "s" + } + )]; if total_clusters > 0 { - parts.push(format!("{} cluster{}", total_clusters, if total_clusters == 1 { "" } else { "s" })); + parts.push(format!( + "{} cluster{}", + total_clusters, + if total_clusters == 1 { "" } else { "s" } + )); } if total_registries > 0 { - parts.push(format!("{} registr{}", total_registries, if total_registries == 1 { "y" } else { "ies" })); + parts.push(format!( + "{} registr{}", + total_registries, + if total_registries == 1 { "y" } else { "ies" } + )); } parts.join(", ") }; @@ -210,8 +226,9 @@ targets are available (clusters, registries, Cloud Run). } }); - serde_json::to_string_pretty(&result) - .map_err(|e| ListDeploymentCapabilitiesError(format!("Failed to serialize: {}", e))) + serde_json::to_string_pretty(&result).map_err(|e| { + ListDeploymentCapabilitiesError(format!("Failed to serialize: {}", e)) + }) } Err(e) => Ok(format_api_error("list_deployment_capabilities", e)), } @@ -302,7 +319,10 @@ mod tests { #[test] fn test_tool_name() { - assert_eq!(ListDeploymentCapabilitiesTool::NAME, "list_deployment_capabilities"); + assert_eq!( + ListDeploymentCapabilitiesTool::NAME, + "list_deployment_capabilities" + ); } #[test] diff --git a/src/agent/tools/platform/list_hetzner_availability.rs b/src/agent/tools/platform/list_hetzner_availability.rs index 38aca56e..2245d17a 100644 --- a/src/agent/tools/platform/list_hetzner_availability.rs +++ b/src/agent/tools/platform/list_hetzner_availability.rs @@ -9,11 +9,11 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use crate::agent::tools::error::{ErrorCategory, format_error_for_llm}; -use crate::platform::api::PlatformApiClient; use crate::platform::PlatformSession; +use crate::platform::api::PlatformApiClient; use crate::wizard::{ - get_hetzner_regions_dynamic, get_hetzner_server_types_dynamic, - HetznerFetchResult, DynamicCloudRegion, DynamicMachineType, + DynamicCloudRegion, DynamicMachineType, HetznerFetchResult, get_hetzner_regions_dynamic, + get_hetzner_server_types_dynamic, }; /// Arguments for the list_hetzner_availability tool @@ -142,65 +142,69 @@ This provides current data directly from Hetzner API - never use hardcoded/stati let project_id = session.project_id.clone().unwrap_or_default(); // Fetch regions - let regions: Vec = match get_hetzner_regions_dynamic(&client, &project_id).await { - HetznerFetchResult::Success(r) => r, - HetznerFetchResult::NoCredentials => { - return Ok(format_error_for_llm( - "list_hetzner_availability", - ErrorCategory::PermissionDenied, - "Hetzner credentials not configured for this project", - Some(vec![ - "Add Hetzner API token in project settings", - "Use open_provider_settings to configure Hetzner", - ]), - )); - } - HetznerFetchResult::ApiError(err) => { - return Ok(format_error_for_llm( - "list_hetzner_availability", - ErrorCategory::NetworkError, - &format!("Failed to fetch Hetzner regions: {}", err), - None, - )); - } - }; + let regions: Vec = + match get_hetzner_regions_dynamic(&client, &project_id).await { + HetznerFetchResult::Success(r) => r, + HetznerFetchResult::NoCredentials => { + return Ok(format_error_for_llm( + "list_hetzner_availability", + ErrorCategory::PermissionDenied, + "Hetzner credentials not configured for this project", + Some(vec![ + "Add Hetzner API token in project settings", + "Use open_provider_settings to configure Hetzner", + ]), + )); + } + HetznerFetchResult::ApiError(err) => { + return Ok(format_error_for_llm( + "list_hetzner_availability", + ErrorCategory::NetworkError, + &format!("Failed to fetch Hetzner regions: {}", err), + None, + )); + } + }; // Fetch server types - let server_types: Vec = match get_hetzner_server_types_dynamic( - &client, - &project_id, - args.location.as_deref(), - ).await { - HetznerFetchResult::Success(s) => s, - HetznerFetchResult::NoCredentials => Vec::new(), // Already handled above - HetznerFetchResult::ApiError(_) => Vec::new(), // Non-fatal, continue with regions - }; + let server_types: Vec = + match get_hetzner_server_types_dynamic(&client, &project_id, args.location.as_deref()) + .await + { + HetznerFetchResult::Success(s) => s, + HetznerFetchResult::NoCredentials => Vec::new(), // Already handled above + HetznerFetchResult::ApiError(_) => Vec::new(), // Non-fatal, continue with regions + }; // Format response let regions_json: Vec = regions .iter() - .map(|r| json!({ - "id": r.id, - "name": r.name, - "country": r.location, - "network_zone": r.network_zone, - "available_server_types_count": r.available_server_types.len(), - "available_server_types": r.available_server_types, - })) + .map(|r| { + json!({ + "id": r.id, + "name": r.name, + "country": r.location, + "network_zone": r.network_zone, + "available_server_types_count": r.available_server_types.len(), + "available_server_types": r.available_server_types, + }) + }) .collect(); let server_types_json: Vec = server_types .iter() - .map(|s| json!({ - "id": s.id, - "name": s.name, - "cores": s.cores, - "memory_gb": s.memory_gb, - "disk_gb": s.disk_gb, - "price_hourly_eur": s.price_hourly, - "price_monthly_eur": s.price_monthly, - "available_in": s.available_in, - })) + .map(|s| { + json!({ + "id": s.id, + "name": s.name, + "cores": s.cores, + "memory_gb": s.memory_gb, + "disk_gb": s.disk_gb, + "price_hourly_eur": s.price_hourly, + "price_monthly_eur": s.price_monthly, + "available_in": s.available_in, + }) + }) .collect(); // Group server types by category for easier reading @@ -278,7 +282,10 @@ mod tests { #[test] fn test_tool_name() { - assert_eq!(ListHetznerAvailabilityTool::NAME, "list_hetzner_availability"); + assert_eq!( + ListHetznerAvailabilityTool::NAME, + "list_hetzner_availability" + ); } #[test] diff --git a/src/agent/tools/platform/provision_registry.rs b/src/agent/tools/platform/provision_registry.rs index 65c0dccf..92ef0031 100644 --- a/src/agent/tools/platform/provision_registry.rs +++ b/src/agent/tools/platform/provision_registry.rs @@ -149,7 +149,9 @@ and polls until completion (may take 1-3 minutes). "provision_registry", ErrorCategory::ValidationFailed, "cluster_id cannot be empty", - Some(vec!["Use list_deployment_capabilities to find available clusters"]), + Some(vec![ + "Use list_deployment_capabilities to find available clusters", + ]), )); } @@ -284,8 +286,9 @@ and polls until completion (may take 1-3 minutes). ] }); - return serde_json::to_string_pretty(&result) - .map_err(|e| ProvisionRegistryError(format!("Failed to serialize: {}", e))); + return serde_json::to_string_pretty(&result).map_err(|e| { + ProvisionRegistryError(format!("Failed to serialize: {}", e)) + }); } RegistryTaskState::Failed => { let error_msg = status @@ -324,7 +327,13 @@ and polls until completion (may take 1-3 minutes). fn sanitize_registry_name(name: &str) -> String { name.to_lowercase() .chars() - .map(|c| if c.is_alphanumeric() || c == '-' { c } else { '-' }) + .map(|c| { + if c.is_alphanumeric() || c == '-' { + c + } else { + '-' + } + }) .collect::() .trim_matches('-') .to_string() diff --git a/src/agent/tools/platform/select_project.rs b/src/agent/tools/platform/select_project.rs index ccd8374c..c5390da2 100644 --- a/src/agent/tools/platform/select_project.rs +++ b/src/agent/tools/platform/select_project.rs @@ -8,8 +8,8 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use crate::agent::tools::error::{ErrorCategory, format_error_for_llm}; -use crate::platform::api::{PlatformApiClient, PlatformApiError}; use crate::platform::PlatformSession; +use crate::platform::api::{PlatformApiClient, PlatformApiError}; /// Arguments for the select project tool #[derive(Debug, Deserialize)] diff --git a/src/agent/tools/platform/set_secrets.rs b/src/agent/tools/platform/set_secrets.rs index 1d1da009..40ee9965 100644 --- a/src/agent/tools/platform/set_secrets.rs +++ b/src/agent/tools/platform/set_secrets.rs @@ -258,7 +258,9 @@ Set DATABASE_URL as a secret (value omitted — prompted in terminal) and NODE_E "set_deployment_secrets", ErrorCategory::ValidationFailed, "Secret entry cancelled by user", - Some(vec!["The user cancelled secret input. Try again when ready."]), + Some(vec![ + "The user cancelled secret input. Try again when ready.", + ]), )); } } diff --git a/src/agent/ui/hooks.rs b/src/agent/ui/hooks.rs index f1b54ea1..a2a615c3 100644 --- a/src/agent/ui/hooks.rs +++ b/src/agent/ui/hooks.rs @@ -44,7 +44,7 @@ pub struct ToolCallState { pub is_collapsible: bool, pub status_ok: bool, /// AG-UI tool call ID for event correlation - pub ag_ui_tool_call_id: Option, + pub ag_ui_tool_call_id: Option, } /// Accumulated usage from API responses @@ -286,7 +286,9 @@ where let mut s = state.lock().await; if let Some(idx) = s.current_tool_index { // Get tool call ID before mutating - let ag_ui_tool_call_id = s.tool_calls.get(idx) + let ag_ui_tool_call_id = s + .tool_calls + .get(idx) .and_then(|t| t.ag_ui_tool_call_id.clone()); if let Some(tool) = s.tool_calls.get_mut(idx) { diff --git a/src/analyzer/context/health_detector.rs b/src/analyzer/context/health_detector.rs index 722c985f..d8a2e29a 100644 --- a/src/analyzer/context/health_detector.rs +++ b/src/analyzer/context/health_detector.rs @@ -5,7 +5,9 @@ //! - Framework conventions (Spring Actuator, etc.) //! - Configuration files (K8s manifests) -use crate::analyzer::{DetectedTechnology, HealthEndpoint, HealthEndpointSource, TechnologyCategory}; +use crate::analyzer::{ + DetectedTechnology, HealthEndpoint, HealthEndpointSource, TechnologyCategory, +}; use crate::common::file_utils::{is_readable_file, read_file_safe}; use crate::error::Result; use regex::Regex; @@ -58,7 +60,11 @@ pub fn detect_health_endpoints( } // Sort by confidence (highest first) - endpoints.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap_or(std::cmp::Ordering::Equal)); + endpoints.sort_by(|a, b| { + b.confidence + .partial_cmp(&a.confidence) + .unwrap_or(std::cmp::Ordering::Equal) + }); endpoints } @@ -67,8 +73,14 @@ pub fn detect_health_endpoints( fn get_framework_health_endpoint(tech: &DetectedTechnology) -> Option { match tech.name.as_str() { // Java frameworks - "Spring Boot" => Some(HealthEndpoint::from_framework("/actuator/health", "Spring Boot Actuator")), - "Quarkus" => Some(HealthEndpoint::from_framework("/q/health", "Quarkus SmallRye Health")), + "Spring Boot" => Some(HealthEndpoint::from_framework( + "/actuator/health", + "Spring Boot Actuator", + )), + "Quarkus" => Some(HealthEndpoint::from_framework( + "/q/health", + "Quarkus SmallRye Health", + )), "Micronaut" => Some(HealthEndpoint::from_framework("/health", "Micronaut")), // Node.js frameworks - no standard, but common patterns @@ -127,10 +139,17 @@ fn scan_for_health_routes( // Determine which file types to scan based on detected technologies let has_js = technologies.iter().any(|t| { - matches!(t.category, TechnologyCategory::BackendFramework | TechnologyCategory::MetaFramework) - && (t.name.contains("Express") || t.name.contains("Fastify") || t.name.contains("Koa") - || t.name.contains("Hono") || t.name.contains("Elysia") || t.name.contains("NestJS") - || t.name.contains("Next") || t.name.contains("Nuxt")) + matches!( + t.category, + TechnologyCategory::BackendFramework | TechnologyCategory::MetaFramework + ) && (t.name.contains("Express") + || t.name.contains("Fastify") + || t.name.contains("Koa") + || t.name.contains("Hono") + || t.name.contains("Elysia") + || t.name.contains("NestJS") + || t.name.contains("Next") + || t.name.contains("Nuxt")) }); let has_python = technologies.iter().any(|t| { @@ -140,7 +159,10 @@ fn scan_for_health_routes( let has_go = technologies.iter().any(|t| { matches!(t.category, TechnologyCategory::BackendFramework) - && (t.name.contains("Gin") || t.name.contains("Echo") || t.name.contains("Fiber") || t.name.contains("Chi")) + && (t.name.contains("Gin") + || t.name.contains("Echo") + || t.name.contains("Fiber") + || t.name.contains("Chi")) }); let has_rust = technologies.iter().any(|t| { @@ -150,7 +172,9 @@ fn scan_for_health_routes( let has_java = technologies.iter().any(|t| { matches!(t.category, TechnologyCategory::BackendFramework) - && (t.name.contains("Spring") || t.name.contains("Quarkus") || t.name.contains("Micronaut")) + && (t.name.contains("Spring") + || t.name.contains("Quarkus") + || t.name.contains("Micronaut")) }); // Common locations to check @@ -169,26 +193,65 @@ fn scan_for_health_routes( let dir = project_root.join(location); if dir.is_dir() { if has_js { - scan_directory_for_patterns(&dir, &["js", "ts", "mjs"], &js_health_patterns(), max_file_size, &mut endpoints); + scan_directory_for_patterns( + &dir, + &["js", "ts", "mjs"], + &js_health_patterns(), + max_file_size, + &mut endpoints, + ); } if has_python { - scan_directory_for_patterns(&dir, &["py"], &python_health_patterns(), max_file_size, &mut endpoints); + scan_directory_for_patterns( + &dir, + &["py"], + &python_health_patterns(), + max_file_size, + &mut endpoints, + ); } if has_go { - scan_directory_for_patterns(&dir, &["go"], &go_health_patterns(), max_file_size, &mut endpoints); + scan_directory_for_patterns( + &dir, + &["go"], + &go_health_patterns(), + max_file_size, + &mut endpoints, + ); } if has_rust { - scan_directory_for_patterns(&dir, &["rs"], &rust_health_patterns(), max_file_size, &mut endpoints); + scan_directory_for_patterns( + &dir, + &["rs"], + &rust_health_patterns(), + max_file_size, + &mut endpoints, + ); } if has_java { - scan_directory_for_patterns(&dir, &["java", "kt"], &java_health_patterns(), max_file_size, &mut endpoints); + scan_directory_for_patterns( + &dir, + &["java", "kt"], + &java_health_patterns(), + max_file_size, + &mut endpoints, + ); } } } // Also check root-level files if has_js { - for entry in ["index.js", "index.ts", "app.js", "app.ts", "server.js", "server.ts", "main.js", "main.ts"] { + for entry in [ + "index.js", + "index.ts", + "app.js", + "app.ts", + "server.js", + "server.ts", + "main.js", + "main.ts", + ] { let path = project_root.join(entry); if is_readable_file(&path) { scan_file_for_patterns(&path, &js_health_patterns(), max_file_size, &mut endpoints); @@ -199,20 +262,35 @@ fn scan_for_health_routes( for entry in ["main.py", "app.py", "wsgi.py", "asgi.py"] { let path = project_root.join(entry); if is_readable_file(&path) { - scan_file_for_patterns(&path, &python_health_patterns(), max_file_size, &mut endpoints); + scan_file_for_patterns( + &path, + &python_health_patterns(), + max_file_size, + &mut endpoints, + ); } } } if has_go { let main_go = project_root.join("main.go"); if is_readable_file(&main_go) { - scan_file_for_patterns(&main_go, &go_health_patterns(), max_file_size, &mut endpoints); + scan_file_for_patterns( + &main_go, + &go_health_patterns(), + max_file_size, + &mut endpoints, + ); } } if has_rust { let main_rs = project_root.join("src/main.rs"); if is_readable_file(&main_rs) { - scan_file_for_patterns(&main_rs, &rust_health_patterns(), max_file_size, &mut endpoints); + scan_file_for_patterns( + &main_rs, + &rust_health_patterns(), + max_file_size, + &mut endpoints, + ); } } @@ -238,9 +316,29 @@ fn scan_directory_for_patterns( } } else if path.is_dir() { // Skip common non-source directories - let dir_name = path.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default(); - if !["node_modules", ".git", "target", "build", "dist", "__pycache__", ".next", "vendor"].contains(&dir_name.as_str()) { - scan_directory_for_patterns(&path, extensions, patterns, max_file_size, endpoints); + let dir_name = path + .file_name() + .map(|n| n.to_string_lossy().to_string()) + .unwrap_or_default(); + if ![ + "node_modules", + ".git", + "target", + "build", + "dist", + "__pycache__", + ".next", + "vendor", + ] + .contains(&dir_name.as_str()) + { + scan_directory_for_patterns( + &path, + extensions, + patterns, + max_file_size, + endpoints, + ); } } } @@ -261,7 +359,10 @@ fn scan_file_for_patterns( if let Some(path_match) = cap.get(1) { let health_path = path_match.as_str().to_string(); // Only add if it looks like a health endpoint - if COMMON_HEALTH_PATHS.iter().any(|p| health_path.contains(p) || p.contains(&health_path)) { + if COMMON_HEALTH_PATHS + .iter() + .any(|p| health_path.contains(p) || p.contains(&health_path)) + { if !endpoints.iter().any(|e| e.path == health_path) { endpoints.push(HealthEndpoint { path: health_path, @@ -282,11 +383,20 @@ fn scan_file_for_patterns( fn js_health_patterns() -> Vec<(&'static str, f32)> { vec![ // Express/Fastify/Koa style: app.get('/health', ...) - (r#"\.(?:get|route)\s*\(\s*['"]([^'"]*(?:health|ready|live|status|ping)[^'"]*)['"]"#, 0.9), + ( + r#"\.(?:get|route)\s*\(\s*['"]([^'"]*(?:health|ready|live|status|ping)[^'"]*)['"]"#, + 0.9, + ), // NestJS style: @Get('health') - (r#"@Get\s*\(\s*['"]([^'"]*(?:health|ready|live|status|ping)[^'"]*)['"]"#, 0.9), + ( + r#"@Get\s*\(\s*['"]([^'"]*(?:health|ready|live|status|ping)[^'"]*)['"]"#, + 0.9, + ), // Hono/Elysia style: .get('/health', ...) - (r#"\.get\s*\(\s*['"]([^'"]*(?:health|ready|live|status|ping)[^'"]*)['"]"#, 0.9), + ( + r#"\.get\s*\(\s*['"]([^'"]*(?:health|ready|live|status|ping)[^'"]*)['"]"#, + 0.9, + ), ] } @@ -294,9 +404,15 @@ fn js_health_patterns() -> Vec<(&'static str, f32)> { fn python_health_patterns() -> Vec<(&'static str, f32)> { vec![ // FastAPI/Flask style: @app.get("/health") - (r#"@\w+\.(?:get|route)\s*\(\s*['"]([^'"]*(?:health|ready|live|status|ping)[^'"]*)['"]"#, 0.9), + ( + r#"@\w+\.(?:get|route)\s*\(\s*['"]([^'"]*(?:health|ready|live|status|ping)[^'"]*)['"]"#, + 0.9, + ), // Django URL patterns: path('health/', ...) - (r#"path\s*\(\s*['"]([^'"]*(?:health|ready|live|status|ping)[^'"]*)['"]"#, 0.85), + ( + r#"path\s*\(\s*['"]([^'"]*(?:health|ready|live|status|ping)[^'"]*)['"]"#, + 0.85, + ), ] } @@ -304,9 +420,15 @@ fn python_health_patterns() -> Vec<(&'static str, f32)> { fn go_health_patterns() -> Vec<(&'static str, f32)> { vec![ // http.HandleFunc("/health", ...) - (r#"HandleFunc\s*\(\s*"([^"]*(?:health|ready|live|status|ping)[^"]*)"#, 0.9), + ( + r#"HandleFunc\s*\(\s*"([^"]*(?:health|ready|live|status|ping)[^"]*)"#, + 0.9, + ), // Gin/Echo: r.GET("/health", ...) - (r#"\.(?:GET|Handle)\s*\(\s*"([^"]*(?:health|ready|live|status|ping)[^"]*)"#, 0.9), + ( + r#"\.(?:GET|Handle)\s*\(\s*"([^"]*(?:health|ready|live|status|ping)[^"]*)"#, + 0.9, + ), ] } @@ -314,9 +436,15 @@ fn go_health_patterns() -> Vec<(&'static str, f32)> { fn rust_health_patterns() -> Vec<(&'static str, f32)> { vec![ // Actix: .route("/health", ...) - (r#"\.route\s*\(\s*"([^"]*(?:health|ready|live|status|ping)[^"]*)"#, 0.9), + ( + r#"\.route\s*\(\s*"([^"]*(?:health|ready|live|status|ping)[^"]*)"#, + 0.9, + ), // Axum: .route("/health", get(...)) - (r#"\.route\s*\(\s*"([^"]*(?:health|ready|live|status|ping)[^"]*)"#, 0.9), + ( + r#"\.route\s*\(\s*"([^"]*(?:health|ready|live|status|ping)[^"]*)"#, + 0.9, + ), ] } @@ -324,7 +452,10 @@ fn rust_health_patterns() -> Vec<(&'static str, f32)> { fn java_health_patterns() -> Vec<(&'static str, f32)> { vec![ // Spring: @GetMapping("/health") - (r#"@(?:Get|Request)Mapping\s*\(\s*(?:value\s*=\s*)?["']([^"']*(?:health|ready|live|status|ping)[^"']*)["']"#, 0.9), + ( + r#"@(?:Get|Request)Mapping\s*\(\s*(?:value\s*=\s*)?["']([^"']*(?:health|ready|live|status|ping)[^"']*)["']"#, + 0.9, + ), ] } diff --git a/src/analyzer/context/infra_detector.rs b/src/analyzer/context/infra_detector.rs index c8a5a6df..5292da8d 100644 --- a/src/analyzer/context/infra_detector.rs +++ b/src/analyzer/context/infra_detector.rs @@ -262,7 +262,11 @@ mod tests { #[test] fn test_detect_docker_compose() { let temp_dir = TempDir::new().unwrap(); - fs::write(temp_dir.path().join("docker-compose.yml"), "version: '3'\nservices:\n app:\n build: .").unwrap(); + fs::write( + temp_dir.path().join("docker-compose.yml"), + "version: '3'\nservices:\n app:\n build: .", + ) + .unwrap(); let infra = detect_infrastructure(temp_dir.path()); assert!(infra.has_docker_compose); @@ -274,7 +278,11 @@ mod tests { let temp_dir = TempDir::new().unwrap(); let k8s_dir = temp_dir.path().join("k8s"); fs::create_dir(&k8s_dir).unwrap(); - fs::write(k8s_dir.join("deployment.yaml"), "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: test").unwrap(); + fs::write( + k8s_dir.join("deployment.yaml"), + "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: test", + ) + .unwrap(); let infra = detect_infrastructure(temp_dir.path()); assert!(infra.has_kubernetes); @@ -286,7 +294,11 @@ mod tests { let temp_dir = TempDir::new().unwrap(); let helm_dir = temp_dir.path().join("charts").join("myapp"); fs::create_dir_all(&helm_dir).unwrap(); - fs::write(helm_dir.join("Chart.yaml"), "apiVersion: v2\nname: myapp\nversion: 1.0.0").unwrap(); + fs::write( + helm_dir.join("Chart.yaml"), + "apiVersion: v2\nname: myapp\nversion: 1.0.0", + ) + .unwrap(); let infra = detect_infrastructure(temp_dir.path()); assert!(infra.has_helm); @@ -298,7 +310,11 @@ mod tests { let temp_dir = TempDir::new().unwrap(); let tf_dir = temp_dir.path().join("terraform"); fs::create_dir(&tf_dir).unwrap(); - fs::write(tf_dir.join("main.tf"), "provider \"aws\" {\n region = \"us-east-1\"\n}").unwrap(); + fs::write( + tf_dir.join("main.tf"), + "provider \"aws\" {\n region = \"us-east-1\"\n}", + ) + .unwrap(); let infra = detect_infrastructure(temp_dir.path()); assert!(infra.has_terraform); diff --git a/src/analyzer/context/language_analyzers/go.rs b/src/analyzer/context/language_analyzers/go.rs index c1a5b200..5899b4d4 100644 --- a/src/analyzer/context/language_analyzers/go.rs +++ b/src/analyzer/context/language_analyzers/go.rs @@ -1,5 +1,6 @@ use crate::analyzer::{ - AnalysisConfig, BuildScript, EntryPoint, Port, PortSource, Protocol, context::helpers::create_regex, + AnalysisConfig, BuildScript, EntryPoint, Port, PortSource, Protocol, + context::helpers::create_regex, }; use crate::common::file_utils::{is_readable_file, read_file_safe}; use crate::error::Result; diff --git a/src/analyzer/context/language_analyzers/python.rs b/src/analyzer/context/language_analyzers/python.rs index 7d797016..ab8efc8e 100644 --- a/src/analyzer/context/language_analyzers/python.rs +++ b/src/analyzer/context/language_analyzers/python.rs @@ -1,5 +1,6 @@ use crate::analyzer::{ - AnalysisConfig, BuildScript, EntryPoint, Port, PortSource, Protocol, context::helpers::create_regex, + AnalysisConfig, BuildScript, EntryPoint, Port, PortSource, Protocol, + context::helpers::create_regex, }; use crate::common::file_utils::{is_readable_file, read_file_safe}; use crate::error::Result; diff --git a/src/analyzer/context/language_analyzers/rust.rs b/src/analyzer/context/language_analyzers/rust.rs index dc5a49ef..71b4349b 100644 --- a/src/analyzer/context/language_analyzers/rust.rs +++ b/src/analyzer/context/language_analyzers/rust.rs @@ -1,5 +1,6 @@ use crate::analyzer::{ - AnalysisConfig, BuildScript, EntryPoint, Port, PortSource, Protocol, context::helpers::create_regex, + AnalysisConfig, BuildScript, EntryPoint, Port, PortSource, Protocol, + context::helpers::create_regex, }; use crate::common::file_utils::{is_readable_file, read_file_safe}; use crate::error::Result; diff --git a/src/analyzer/docker_analyzer.rs b/src/analyzer/docker_analyzer.rs index f603dea9..64484d37 100644 --- a/src/analyzer/docker_analyzer.rs +++ b/src/analyzer/docker_analyzer.rs @@ -1297,13 +1297,7 @@ fn sanitize_service_name(name: &str) -> String { let sanitized: String = name .to_lowercase() .chars() - .map(|c| { - if c.is_ascii_alphanumeric() { - c - } else { - '-' - } - }) + .map(|c| if c.is_ascii_alphanumeric() { c } else { '-' }) .collect(); // Remove consecutive hyphens and trim hyphens from ends diff --git a/src/analyzer/mod.rs b/src/analyzer/mod.rs index a1660b52..116b32e8 100644 --- a/src/analyzer/mod.rs +++ b/src/analyzer/mod.rs @@ -363,17 +363,31 @@ pub struct InfrastructurePresence { impl InfrastructurePresence { /// Returns true if any infrastructure was detected pub fn has_any(&self) -> bool { - self.has_kubernetes || self.has_helm || self.has_docker_compose || self.has_terraform || self.has_deployment_config + self.has_kubernetes + || self.has_helm + || self.has_docker_compose + || self.has_terraform + || self.has_deployment_config } /// Returns a list of detected infrastructure types pub fn detected_types(&self) -> Vec<&'static str> { let mut types = Vec::new(); - if self.has_kubernetes { types.push("Kubernetes"); } - if self.has_helm { types.push("Helm"); } - if self.has_docker_compose { types.push("Docker Compose"); } - if self.has_terraform { types.push("Terraform"); } - if self.has_deployment_config { types.push("Syncable Config"); } + if self.has_kubernetes { + types.push("Kubernetes"); + } + if self.has_helm { + types.push("Helm"); + } + if self.has_docker_compose { + types.push("Docker Compose"); + } + if self.has_terraform { + types.push("Terraform"); + } + if self.has_deployment_config { + types.push("Syncable Config"); + } types } } @@ -578,7 +592,8 @@ pub fn analyze_project_with_config( let context = context::analyze_context(&project_root, &languages, &frameworks, config)?; // Detect health check endpoints - let health_endpoints = context::detect_health_endpoints(&project_root, &frameworks, config.max_file_size); + let health_endpoints = + context::detect_health_endpoints(&project_root, &frameworks, config.max_file_size); // Detect infrastructure presence (K8s, Helm, Terraform, etc.) let infrastructure = context::detect_infrastructure(&project_root); diff --git a/src/lib.rs b/src/lib.rs index 9594d5d3..a52ed76d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -276,12 +276,19 @@ pub async fn run_command( agent::session::ChatSession::load_api_key_to_env(provider_type); if let Some(q) = query { - let response = - agent::run_query(&project_path, &q, provider_type, effective_model, event_bridge).await?; + let response = agent::run_query( + &project_path, + &q, + provider_type, + effective_model, + event_bridge, + ) + .await?; println!("{}", response); Ok(()) } else { - agent::run_interactive(&project_path, provider_type, effective_model, event_bridge).await?; + agent::run_interactive(&project_path, provider_type, effective_model, event_bridge) + .await?; Ok(()) } } @@ -324,19 +331,29 @@ pub async fn run_command( match format { OutputFormat::Json => { - println!("{}", serde_json::to_string_pretty(&projects).unwrap_or_default()); + println!( + "{}", + serde_json::to_string_pretty(&projects).unwrap_or_default() + ); } OutputFormat::Table => { println!("\n{:<40} {:<30} {}", "ID", "NAME", "DESCRIPTION"); println!("{}", "-".repeat(90)); for project in projects { - let desc = if project.description.is_empty() { "-" } else { &project.description }; + let desc = if project.description.is_empty() { + "-" + } else { + &project.description + }; let desc_truncated = if desc.len() > 30 { format!("{}...", &desc[..27]) } else { desc.to_string() }; - println!("{:<40} {:<30} {}", project.id, project.name, desc_truncated); + println!( + "{:<40} {:<30} {}", + project.id, project.name, desc_truncated + ); } println!(); } @@ -362,7 +379,10 @@ pub async fn run_command( Ok(project) => { // Get org info let org = client.get_organization(&project.organization_id).await.ok(); - let org_name = org.as_ref().map(|o| o.name.clone()).unwrap_or_else(|| "Unknown".to_string()); + let org_name = org + .as_ref() + .map(|o| o.name.clone()) + .unwrap_or_else(|| "Unknown".to_string()); let session = PlatformSession::with_project( project.id.clone(), @@ -408,10 +428,14 @@ pub async fn run_command( if let (Some(org_name), Some(org_id)) = (&session.org_name, &session.org_id) { println!(" Organization: {} ({})", org_name, org_id); } - if let (Some(project_name), Some(project_id)) = (&session.project_name, &session.project_id) { + if let (Some(project_name), Some(project_id)) = + (&session.project_name, &session.project_id) + { println!(" Project: {} ({})", project_name, project_id); } - if let (Some(env_name), Some(env_id)) = (&session.environment_name, &session.environment_id) { + if let (Some(env_name), Some(env_id)) = + (&session.environment_name, &session.environment_id) + { println!(" Environment: {} ({})", env_name, env_id); } else { println!(" Environment: (none selected)"); @@ -420,7 +444,10 @@ pub async fn run_command( println!(" sync-ctl env select "); } if let Some(updated) = session.last_updated { - println!(" Last updated: {}", updated.format("%Y-%m-%d %H:%M:%S UTC")); + println!( + " Last updated: {}", + updated.format("%Y-%m-%d %H:%M:%S UTC") + ); } println!(); Ok(()) @@ -452,15 +479,25 @@ pub async fn run_command( Ok(project) => { // Get org info let org = client.get_organization(&project.organization_id).await.ok(); - let org_name = org.as_ref().map(|o| o.name.clone()).unwrap_or_else(|| "Unknown".to_string()); + let org_name = org + .as_ref() + .map(|o| o.name.clone()) + .unwrap_or_else(|| "Unknown".to_string()); println!("\nProject Details:"); println!(" ID: {}", project.id); println!(" Name: {}", project.name); - let desc = if project.description.is_empty() { "-" } else { &project.description }; + let desc = if project.description.is_empty() { + "-" + } else { + &project.description + }; println!(" Description: {}", desc); println!(" Organization: {} ({})", org_name, project.organization_id); - println!(" Created: {}", project.created_at.format("%Y-%m-%d %H:%M:%S UTC")); + println!( + " Created: {}", + project.created_at.format("%Y-%m-%d %H:%M:%S UTC") + ); println!(); } Err(platform::api::error::PlatformApiError::Unauthorized) => { @@ -478,7 +515,7 @@ pub async fn run_command( } } Commands::Org { command } => { - use cli::{OutputFormat, OrgCommand}; + use cli::{OrgCommand, OutputFormat}; use platform::api::client::PlatformApiClient; use platform::session::PlatformSession; @@ -499,13 +536,17 @@ pub async fn run_command( match format { OutputFormat::Json => { - println!("{}", serde_json::to_string_pretty(&orgs).unwrap_or_default()); + println!( + "{}", + serde_json::to_string_pretty(&orgs).unwrap_or_default() + ); } OutputFormat::Table => { println!("\n{:<40} {:<30} {}", "ID", "NAME", "SLUG"); println!("{}", "-".repeat(90)); for org in orgs { - let slug = if org.slug.is_empty() { "-" } else { &org.slug }; + let slug = + if org.slug.is_empty() { "-" } else { &org.slug }; println!("{:<40} {:<30} {}", org.id, org.name, slug); } println!(); diff --git a/src/main.rs b/src/main.rs index c61fef5c..e4304a7a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -682,7 +682,7 @@ async fn run() -> syncable_cli::Result<()> { // Start AG-UI server if requested and get the event bridge let event_bridge = if ag_ui { - use syncable_cli::server::{AgUiServer, AgUiConfig}; + use syncable_cli::server::{AgUiConfig, AgUiServer}; let config = AgUiConfig::new().port(ag_ui_port); let server = AgUiServer::new(config); @@ -770,56 +770,57 @@ async fn run() -> syncable_cli::Result<()> { }; match command { - EnvCommand::List { format } => { - match client.list_environments(&project_id).await { - Ok(environments) => { - if environments.is_empty() { - println!("No environments found in project."); - println!( - "\nCreate one with: {}", - "sync-ctl deploy new-env".bright_cyan() - ); - } else { - match format { - OutputFormat::Json => { - println!( - "{}", - serde_json::to_string_pretty(&environments).unwrap() - ); - } - OutputFormat::Table => { - println!("\nEnvironments in project:\n"); - for env in &environments { - let selected = session - .environment_id - .as_ref() - .map(|id| id == &env.id) - .unwrap_or(false); - let marker = - if selected { "→ ".green() } else { " ".normal() }; - println!( - "{}{} ({}) - {}", - marker, - env.name.bold(), - env.id.dimmed(), - env.environment_type - ); - } + EnvCommand::List { format } => match client.list_environments(&project_id).await { + Ok(environments) => { + if environments.is_empty() { + println!("No environments found in project."); + println!( + "\nCreate one with: {}", + "sync-ctl deploy new-env".bright_cyan() + ); + } else { + match format { + OutputFormat::Json => { + println!( + "{}", + serde_json::to_string_pretty(&environments).unwrap() + ); + } + OutputFormat::Table => { + println!("\nEnvironments in project:\n"); + for env in &environments { + let selected = session + .environment_id + .as_ref() + .map(|id| id == &env.id) + .unwrap_or(false); + let marker = if selected { + "→ ".green() + } else { + " ".normal() + }; println!( - "\nSelect with: {}", - "sync-ctl env select ".bright_cyan() + "{}{} ({}) - {}", + marker, + env.name.bold(), + env.id.dimmed(), + env.environment_type ); } + println!( + "\nSelect with: {}", + "sync-ctl env select ".bright_cyan() + ); } } - Ok(()) - } - Err(e) => { - eprintln!("Failed to list environments: {}", e); - process::exit(1); } + Ok(()) } - } + Err(e) => { + eprintln!("Failed to list environments: {}", e); + process::exit(1); + } + }, EnvCommand::Select { id } => { // Verify environment exists (match by ID or name) match client.list_environments(&project_id).await { @@ -890,8 +891,8 @@ async fn run() -> syncable_cli::Result<()> { use syncable_cli::platform::api::PlatformApiClient; use syncable_cli::platform::session::PlatformSession; use syncable_cli::wizard::{ - create_environment_wizard, run_wizard, select_environment, EnvironmentCreationResult, EnvironmentSelectionResult, WizardResult, + create_environment_wizard, run_wizard, select_environment, }; // Check authentication @@ -984,10 +985,7 @@ async fn run() -> syncable_cli::Result<()> { "═══════════════════════════════════════════════════════════════" .bright_blue() ); - println!( - "{}", - format!(" Deployment Status: {}", task_id).bold() - ); + println!("{}", format!(" Deployment Status: {}", task_id).bold()); println!( "{}", "═══════════════════════════════════════════════════════════════" @@ -1049,10 +1047,7 @@ async fn run() -> syncable_cli::Result<()> { } // Wait before next poll - println!( - " {}", - "Watching... (Ctrl+C to stop)".dimmed() - ); + println!(" {}", "Watching... (Ctrl+C to stop)".dimmed()); sleep(Duration::from_secs(5)).await; } Err(e) => { @@ -1065,54 +1060,55 @@ async fn run() -> syncable_cli::Result<()> { } Some(DeployCommand::Wizard { path: wizard_path }) => { // Always ask for environment selection - let (environment_id, _session) = match select_environment(&client, &project_id).await { - EnvironmentSelectionResult::Selected(env) => { - // Update session with selected environment - let new_session = PlatformSession::with_environment( - session.project_id.clone().unwrap(), - session.project_name.clone().unwrap_or_default(), - session.org_id.clone().unwrap_or_default(), - session.org_name.clone().unwrap_or_default(), - env.id.clone(), - env.name.clone(), - ); - let _ = new_session.save(); - (env.id, new_session) - } - EnvironmentSelectionResult::CreateNew => { - // Run environment creation wizard - match create_environment_wizard(&client, &project_id).await { - EnvironmentCreationResult::Created(env) => { - let new_session = PlatformSession::with_environment( - session.project_id.clone().unwrap(), - session.project_name.clone().unwrap_or_default(), - session.org_id.clone().unwrap_or_default(), - session.org_name.clone().unwrap_or_default(), - env.id.clone(), - env.name.clone(), - ); - let _ = new_session.save(); - (env.id, new_session) - } - EnvironmentCreationResult::Cancelled => { - println!("{}", "Environment creation cancelled.".dimmed()); - return Ok(()); - } - EnvironmentCreationResult::Error(e) => { - eprintln!("Error creating environment: {}", e); - process::exit(1); + let (environment_id, _session) = + match select_environment(&client, &project_id).await { + EnvironmentSelectionResult::Selected(env) => { + // Update session with selected environment + let new_session = PlatformSession::with_environment( + session.project_id.clone().unwrap(), + session.project_name.clone().unwrap_or_default(), + session.org_id.clone().unwrap_or_default(), + session.org_name.clone().unwrap_or_default(), + env.id.clone(), + env.name.clone(), + ); + let _ = new_session.save(); + (env.id, new_session) + } + EnvironmentSelectionResult::CreateNew => { + // Run environment creation wizard + match create_environment_wizard(&client, &project_id).await { + EnvironmentCreationResult::Created(env) => { + let new_session = PlatformSession::with_environment( + session.project_id.clone().unwrap(), + session.project_name.clone().unwrap_or_default(), + session.org_id.clone().unwrap_or_default(), + session.org_name.clone().unwrap_or_default(), + env.id.clone(), + env.name.clone(), + ); + let _ = new_session.save(); + (env.id, new_session) + } + EnvironmentCreationResult::Cancelled => { + println!("{}", "Environment creation cancelled.".dimmed()); + return Ok(()); + } + EnvironmentCreationResult::Error(e) => { + eprintln!("Error creating environment: {}", e); + process::exit(1); + } } } - } - EnvironmentSelectionResult::Cancelled => { - println!("{}", "Wizard cancelled.".dimmed()); - return Ok(()); - } - EnvironmentSelectionResult::Error(e) => { - eprintln!("Error: {}", e); - process::exit(1); - } - }; + EnvironmentSelectionResult::Cancelled => { + println!("{}", "Wizard cancelled.".dimmed()); + return Ok(()); + } + EnvironmentSelectionResult::Error(e) => { + eprintln!("Error: {}", e); + process::exit(1); + } + }; // Run deployment wizard match run_wizard(&client, &project_id, &environment_id, &wizard_path).await { @@ -1130,10 +1126,7 @@ async fn run() -> syncable_cli::Result<()> { .yellow() ); } - println!( - "\n{}", - "Next: Run deployment with created config".dimmed() - ); + println!("\n{}", "Next: Run deployment with created config".dimmed()); Ok(()) } WizardResult::StartAgent(prompt) => { @@ -1169,54 +1162,55 @@ async fn run() -> syncable_cli::Result<()> { } None => { // Always ask for environment selection - let (environment_id, _session) = match select_environment(&client, &project_id).await { - EnvironmentSelectionResult::Selected(env) => { - // Update session with selected environment - let new_session = PlatformSession::with_environment( - session.project_id.clone().unwrap(), - session.project_name.clone().unwrap_or_default(), - session.org_id.clone().unwrap_or_default(), - session.org_name.clone().unwrap_or_default(), - env.id.clone(), - env.name.clone(), - ); - let _ = new_session.save(); - (env.id, new_session) - } - EnvironmentSelectionResult::CreateNew => { - // Run environment creation wizard - match create_environment_wizard(&client, &project_id).await { - EnvironmentCreationResult::Created(env) => { - let new_session = PlatformSession::with_environment( - session.project_id.clone().unwrap(), - session.project_name.clone().unwrap_or_default(), - session.org_id.clone().unwrap_or_default(), - session.org_name.clone().unwrap_or_default(), - env.id.clone(), - env.name.clone(), - ); - let _ = new_session.save(); - (env.id, new_session) - } - EnvironmentCreationResult::Cancelled => { - println!("{}", "Environment creation cancelled.".dimmed()); - return Ok(()); - } - EnvironmentCreationResult::Error(e) => { - eprintln!("Error creating environment: {}", e); - process::exit(1); + let (environment_id, _session) = + match select_environment(&client, &project_id).await { + EnvironmentSelectionResult::Selected(env) => { + // Update session with selected environment + let new_session = PlatformSession::with_environment( + session.project_id.clone().unwrap(), + session.project_name.clone().unwrap_or_default(), + session.org_id.clone().unwrap_or_default(), + session.org_name.clone().unwrap_or_default(), + env.id.clone(), + env.name.clone(), + ); + let _ = new_session.save(); + (env.id, new_session) + } + EnvironmentSelectionResult::CreateNew => { + // Run environment creation wizard + match create_environment_wizard(&client, &project_id).await { + EnvironmentCreationResult::Created(env) => { + let new_session = PlatformSession::with_environment( + session.project_id.clone().unwrap(), + session.project_name.clone().unwrap_or_default(), + session.org_id.clone().unwrap_or_default(), + session.org_name.clone().unwrap_or_default(), + env.id.clone(), + env.name.clone(), + ); + let _ = new_session.save(); + (env.id, new_session) + } + EnvironmentCreationResult::Cancelled => { + println!("{}", "Environment creation cancelled.".dimmed()); + return Ok(()); + } + EnvironmentCreationResult::Error(e) => { + eprintln!("Error creating environment: {}", e); + process::exit(1); + } } } - } - EnvironmentSelectionResult::Cancelled => { - println!("{}", "Wizard cancelled.".dimmed()); - return Ok(()); - } - EnvironmentSelectionResult::Error(e) => { - eprintln!("Error: {}", e); - process::exit(1); - } - }; + EnvironmentSelectionResult::Cancelled => { + println!("{}", "Wizard cancelled.".dimmed()); + return Ok(()); + } + EnvironmentSelectionResult::Error(e) => { + eprintln!("Error: {}", e); + process::exit(1); + } + }; // Run deployment wizard with top-level path match run_wizard(&client, &project_id, &environment_id, &path).await { @@ -1234,10 +1228,7 @@ async fn run() -> syncable_cli::Result<()> { .yellow() ); } - println!( - "\n{}", - "Next: Run deployment with created config".dimmed() - ); + println!("\n{}", "Next: Run deployment with created config".dimmed()); Ok(()) } WizardResult::StartAgent(prompt) => { diff --git a/src/platform/api/client.rs b/src/platform/api/client.rs index eb6df118..6d09fc73 100644 --- a/src/platform/api/client.rs +++ b/src/platform/api/client.rs @@ -17,10 +17,10 @@ use super::types::{ }; use crate::auth::credentials; use reqwest::Client; -use serde::de::DeserializeOwned; use serde::Serialize; -use urlencoding; +use serde::de::DeserializeOwned; use std::time::Duration; +use urlencoding; /// Production API URL const SYNCABLE_API_URL_PROD: &str = "https://syncable.dev"; @@ -98,31 +98,24 @@ impl PlatformApiClient { let mut backoff_ms = INITIAL_BACKOFF_MS; for attempt in 0..=MAX_RETRIES { - let result = self - .http_client - .get(&url) - .bearer_auth(&token) - .send() - .await; + let result = self.http_client.get(&url).bearer_auth(&token).send().await; match result { - Ok(response) => { - match self.handle_response(response).await { - Ok(data) => return Ok(data), - Err(e) if is_retryable_error(&e) && attempt < MAX_RETRIES => { - eprintln!( - "Request failed (attempt {}/{}), retrying in {}ms...", - attempt + 1, - MAX_RETRIES + 1, - backoff_ms - ); - last_error = Some(e); - tokio::time::sleep(Duration::from_millis(backoff_ms)).await; - backoff_ms = (backoff_ms * 2).min(MAX_BACKOFF_MS); - } - Err(e) => return Err(e), + Ok(response) => match self.handle_response(response).await { + Ok(data) => return Ok(data), + Err(e) if is_retryable_error(&e) && attempt < MAX_RETRIES => { + eprintln!( + "Request failed (attempt {}/{}), retrying in {}ms...", + attempt + 1, + MAX_RETRIES + 1, + backoff_ms + ); + last_error = Some(e); + tokio::time::sleep(Duration::from_millis(backoff_ms)).await; + backoff_ms = (backoff_ms * 2).min(MAX_BACKOFF_MS); } - } + Err(e) => return Err(e), + }, Err(e) => { let platform_error = PlatformApiError::HttpError(e); if is_retryable_error(&platform_error) && attempt < MAX_RETRIES { @@ -156,12 +149,7 @@ impl PlatformApiClient { let mut backoff_ms = INITIAL_BACKOFF_MS; for attempt in 0..=MAX_RETRIES { - let result = self - .http_client - .get(&url) - .bearer_auth(&token) - .send() - .await; + let result = self.http_client.get(&url).bearer_auth(&token).send().await; match result { Ok(response) => { @@ -328,10 +316,7 @@ impl PlatformApiClient { } /// Handle the HTTP response, converting errors appropriately - async fn handle_response( - &self, - response: reqwest::Response, - ) -> Result { + async fn handle_response(&self, response: reqwest::Response) -> Result { let status = response.status(); if status.is_success() { @@ -416,8 +401,7 @@ impl PlatformApiClient { /// /// Endpoint: GET /api/projects/:id pub async fn get_project(&self, id: &str) -> Result { - let response: GenericResponse = - self.get(&format!("/api/projects/{}", id)).await?; + let response: GenericResponse = self.get(&format!("/api/projects/{}", id)).await?; Ok(response.data) } @@ -462,10 +446,7 @@ impl PlatformApiClient { project_id: &str, ) -> Result { let response: GenericResponse = self - .get(&format!( - "/api/github/projects/{}/repositories", - project_id - )) + .get(&format!("/api/github/projects/{}/repositories", project_id)) .await?; Ok(response.data) } @@ -722,7 +703,10 @@ impl PlatformApiClient { "secrets": secrets, }); let _response: GenericResponse = self - .put(&format!("/api/deployment-configs/{}/secrets", config_id), &body) + .put( + &format!("/api/deployment-configs/{}/secrets", config_id), + &body, + ) .await?; Ok(()) } @@ -991,7 +975,10 @@ impl PlatformApiClient { urlencoding::encode(project_id) ); if let Some(location) = preferred_location { - path.push_str(&format!("&preferredLocation={}", urlencoding::encode(location))); + path.push_str(&format!( + "&preferredLocation={}", + urlencoding::encode(location) + )); } let response: super::types::ServerTypesResponse = self.get(&path).await?; Ok(response.data) @@ -1032,10 +1019,7 @@ impl PlatformApiClient { /// Use this to discover private networking infrastructure provisioned for the project. /// /// Endpoint: GET /api/v1/cloud-runner/projects/:projectId/networks - pub async fn list_project_networks( - &self, - project_id: &str, - ) -> Result> { + pub async fn list_project_networks(&self, project_id: &str) -> Result> { let response: GenericResponse> = self .get(&format!( "/api/v1/cloud-runner/projects/{}/networks", @@ -1106,7 +1090,10 @@ mod tests { // Test path concatenation logic (implicitly tested through api_url) let expected_path = format!("{}/api/organizations/123", client.api_url()); - assert_eq!(expected_path, "https://api.example.com/api/organizations/123"); + assert_eq!( + expected_path, + "https://api.example.com/api/organizations/123" + ); } #[test] @@ -1125,8 +1112,7 @@ mod tests { assert!(api_error.to_string().contains("400")); assert!(api_error.to_string().contains("Bad request")); - let permission_denied = - PlatformApiError::PermissionDenied("Access denied".to_string()); + let permission_denied = PlatformApiError::PermissionDenied("Access denied".to_string()); assert!(permission_denied.to_string().contains("Permission denied")); let rate_limited = PlatformApiError::RateLimited; @@ -1176,7 +1162,10 @@ mod tests { provider.as_str(), project_id ); - assert_eq!(expected_path, "/api/cloud-credentials/provider/gcp?projectId=proj-123"); + assert_eq!( + expected_path, + "/api/cloud-credentials/provider/gcp?projectId=proj-123" + ); } #[test] @@ -1199,7 +1188,10 @@ mod tests { service_id, query_params.join("&") ); - assert_eq!(path, "/api/deployments/services/svc-123/logs?start=2024-01-01T00:00:00Z&limit=50"); + assert_eq!( + path, + "/api/deployments/services/svc-123/logs?start=2024-01-01T00:00:00Z&limit=50" + ); } #[test] diff --git a/src/platform/api/error.rs b/src/platform/api/error.rs index 99e309f3..c0ad4d1b 100644 --- a/src/platform/api/error.rs +++ b/src/platform/api/error.rs @@ -63,9 +63,7 @@ impl PlatformApiError { Self::Unauthorized => Some("Run `sync-ctl auth login` to authenticate"), Self::RateLimited => Some("Wait a moment and try again"), Self::HttpError(_) => Some("Check your internet connection"), - Self::ServerError { .. } => { - Some("The server is experiencing issues. Try again later") - } + Self::ServerError { .. } => Some("The server is experiencing issues. Try again later"), Self::PermissionDenied(_) => { Some("Check your project permissions in the Syncable dashboard") } @@ -74,9 +72,7 @@ impl PlatformApiError { Self::ApiError { status, .. } if *status >= 400 && *status < 500 => { Some("Check the request parameters") } - Self::ConnectionFailed => { - Some("Check your internet connection and try again") - } + Self::ConnectionFailed => Some("Check your internet connection and try again"), _ => None, } } diff --git a/src/platform/api/types.rs b/src/platform/api/types.rs index afef5b5e..32f03f96 100644 --- a/src/platform/api/types.rs +++ b/src/platform/api/types.rs @@ -161,7 +161,10 @@ impl CloudProvider { /// Returns `true` for GCP, Hetzner, and Azure (currently supported). /// Returns `false` for AWS, Scaleway, Cyso (coming soon). pub fn is_available(&self) -> bool { - matches!(self, CloudProvider::Gcp | CloudProvider::Hetzner | CloudProvider::Azure) + matches!( + self, + CloudProvider::Gcp | CloudProvider::Hetzner | CloudProvider::Azure + ) } } @@ -1047,7 +1050,10 @@ pub fn build_cloud_runner_config_v2(input: &CloudRunnerConfigInput) -> serde_jso if let Some(allow) = input.allow_unauthenticated { gcp_config.insert("allowUnauthenticated".to_string(), serde_json::json!(allow)); } else if let Some(is_pub) = input.is_public { - gcp_config.insert("allowUnauthenticated".to_string(), serde_json::json!(is_pub)); + gcp_config.insert( + "allowUnauthenticated".to_string(), + serde_json::json!(is_pub), + ); } if let Some(boost) = input.cpu_boost { gcp_config.insert("cpuBoost".to_string(), serde_json::json!(boost)); @@ -1070,7 +1076,10 @@ pub fn build_cloud_runner_config_v2(input: &CloudRunnerConfigInput) -> serde_jso if let Some(ref sub_id) = input.subscription_id { azure_config.insert("subscriptionId".to_string(), serde_json::json!(sub_id)); } - azure_config.insert("deploymentType".to_string(), serde_json::json!("azure_container_app")); + azure_config.insert( + "deploymentType".to_string(), + serde_json::json!("azure_container_app"), + ); if let Some(ref cpu) = input.cpu { azure_config.insert("cpu".to_string(), serde_json::json!(cpu)); } @@ -1935,9 +1944,18 @@ mod tests { Some("/health"), ); let gcp = config.get("gcp").expect("should have gcp key"); - assert_eq!(gcp.get("region").and_then(|v| v.as_str()), Some("us-central1")); - assert_eq!(gcp.get("allowUnauthenticated").and_then(|v| v.as_bool()), Some(true)); - assert_eq!(gcp.get("healthCheckPath").and_then(|v| v.as_str()), Some("/health")); + assert_eq!( + gcp.get("region").and_then(|v| v.as_str()), + Some("us-central1") + ); + assert_eq!( + gcp.get("allowUnauthenticated").and_then(|v| v.as_bool()), + Some(true) + ); + assert_eq!( + gcp.get("healthCheckPath").and_then(|v| v.as_str()), + Some("/health") + ); } #[test] @@ -1950,24 +1968,30 @@ mod tests { None, ); let gcp = config.get("gcp").expect("should have gcp key"); - assert_eq!(gcp.get("region").and_then(|v| v.as_str()), Some("europe-west1")); - assert_eq!(gcp.get("allowUnauthenticated").and_then(|v| v.as_bool()), Some(false)); + assert_eq!( + gcp.get("region").and_then(|v| v.as_str()), + Some("europe-west1") + ); + assert_eq!( + gcp.get("allowUnauthenticated").and_then(|v| v.as_bool()), + Some(false) + ); // No health check path when not provided assert!(gcp.get("healthCheckPath").is_none()); } #[test] fn test_build_cloud_runner_config_hetzner() { - let config = build_cloud_runner_config( - &CloudProvider::Hetzner, - "nbg1", - "cx22", - true, - None, - ); + let config = build_cloud_runner_config(&CloudProvider::Hetzner, "nbg1", "cx22", true, None); let hetzner = config.get("hetzner").expect("should have hetzner key"); - assert_eq!(hetzner.get("location").and_then(|v| v.as_str()), Some("nbg1")); - assert_eq!(hetzner.get("serverType").and_then(|v| v.as_str()), Some("cx22")); + assert_eq!( + hetzner.get("location").and_then(|v| v.as_str()), + Some("nbg1") + ); + assert_eq!( + hetzner.get("serverType").and_then(|v| v.as_str()), + Some("cx22") + ); } #[test] @@ -1980,8 +2004,14 @@ mod tests { Some("/healthz"), ); let hetzner = config.get("hetzner").expect("should have hetzner key"); - assert_eq!(hetzner.get("location").and_then(|v| v.as_str()), Some("fsn1")); - assert_eq!(hetzner.get("serverType").and_then(|v| v.as_str()), Some("cx32")); + assert_eq!( + hetzner.get("location").and_then(|v| v.as_str()), + Some("fsn1") + ); + assert_eq!( + hetzner.get("serverType").and_then(|v| v.as_str()), + Some("cx32") + ); // Hetzner config doesn't include health check path in current implementation } @@ -2005,14 +2035,26 @@ mod tests { }; let config = build_cloud_runner_config_v2(&input); let gcp = config.get("gcp").expect("should have gcp key"); - assert_eq!(gcp.get("region").and_then(|v| v.as_str()), Some("us-central1")); - assert_eq!(gcp.get("projectId").and_then(|v| v.as_str()), Some("my-project")); + assert_eq!( + gcp.get("region").and_then(|v| v.as_str()), + Some("us-central1") + ); + assert_eq!( + gcp.get("projectId").and_then(|v| v.as_str()), + Some("my-project") + ); assert_eq!(gcp.get("cpu").and_then(|v| v.as_str()), Some("2")); assert_eq!(gcp.get("memory").and_then(|v| v.as_str()), Some("2Gi")); assert_eq!(gcp.get("minInstances").and_then(|v| v.as_i64()), Some(0)); assert_eq!(gcp.get("maxInstances").and_then(|v| v.as_i64()), Some(10)); - assert_eq!(gcp.get("allowUnauthenticated").and_then(|v| v.as_bool()), Some(true)); - assert_eq!(gcp.get("healthCheckPath").and_then(|v| v.as_str()), Some("/health")); + assert_eq!( + gcp.get("allowUnauthenticated").and_then(|v| v.as_bool()), + Some(true) + ); + assert_eq!( + gcp.get("healthCheckPath").and_then(|v| v.as_str()), + Some("/health") + ); } #[test] @@ -2030,9 +2072,18 @@ mod tests { }; let config = build_cloud_runner_config_v2(&input); let azure = config.get("azure").expect("should have azure key"); - assert_eq!(azure.get("location").and_then(|v| v.as_str()), Some("eastus")); - assert_eq!(azure.get("subscriptionId").and_then(|v| v.as_str()), Some("sub-123")); - assert_eq!(azure.get("deploymentType").and_then(|v| v.as_str()), Some("azure_container_app")); + assert_eq!( + azure.get("location").and_then(|v| v.as_str()), + Some("eastus") + ); + assert_eq!( + azure.get("subscriptionId").and_then(|v| v.as_str()), + Some("sub-123") + ); + assert_eq!( + azure.get("deploymentType").and_then(|v| v.as_str()), + Some("azure_container_app") + ); assert_eq!(azure.get("cpu").and_then(|v| v.as_str()), Some("0.5")); assert_eq!(azure.get("memory").and_then(|v| v.as_str()), Some("1.0Gi")); assert_eq!(azure.get("isPublic").and_then(|v| v.as_bool()), Some(true)); @@ -2053,8 +2104,14 @@ mod tests { }; let config = build_cloud_runner_config_v2(&input); let hetzner = config.get("hetzner").expect("should have hetzner key"); - assert_eq!(hetzner.get("location").and_then(|v| v.as_str()), Some("nbg1")); - assert_eq!(hetzner.get("serverType").and_then(|v| v.as_str()), Some("cx22")); + assert_eq!( + hetzner.get("location").and_then(|v| v.as_str()), + Some("nbg1") + ); + assert_eq!( + hetzner.get("serverType").and_then(|v| v.as_str()), + Some("cx22") + ); } #[test] diff --git a/src/server/bridge.rs b/src/server/bridge.rs index 274141f5..161976c4 100644 --- a/src/server/bridge.rs +++ b/src/server/bridge.rs @@ -28,13 +28,13 @@ use std::sync::Arc; -use ag_ui_core::{ +use syncable_ag_ui_core::{ BaseEvent, Event, InterruptInfo, JsonValue, MessageId, Role, RunFinishedEvent, RunFinishedOutcome, RunId, RunStartedEvent, TextMessageContentEvent, TextMessageEndEvent, TextMessageStartEvent, ThreadId, ToolCallArgsEvent, ToolCallEndEvent, ToolCallId, ToolCallStartEvent, }; -use tokio::sync::{broadcast, RwLock}; +use tokio::sync::{RwLock, broadcast}; /// Bridge between agent code and AG-UI protocol events. /// @@ -115,7 +115,7 @@ impl EventBridge { pub async fn finish_run_with_error(&self, message: &str) { let _run_id = self.run_id.write().await.take(); - self.emit(Event::RunError(ag_ui_core::RunErrorEvent { + self.emit(Event::RunError(syncable_ag_ui_core::RunErrorEvent { base: BaseEvent::with_current_timestamp(), message: message.to_string(), code: None, @@ -310,15 +310,17 @@ impl EventBridge { /// Emits a state snapshot. pub async fn emit_state_snapshot(&self, state: JsonValue) { - self.emit(Event::StateSnapshot(ag_ui_core::StateSnapshotEvent { - base: BaseEvent::with_current_timestamp(), - snapshot: state, - })); + self.emit(Event::StateSnapshot( + syncable_ag_ui_core::StateSnapshotEvent { + base: BaseEvent::with_current_timestamp(), + snapshot: state, + }, + )); } /// Emits a state delta (JSON Patch). pub async fn emit_state_delta(&self, delta: Vec) { - self.emit(Event::StateDelta(ag_ui_core::StateDeltaEvent { + self.emit(Event::StateDelta(syncable_ag_ui_core::StateDeltaEvent { base: BaseEvent::with_current_timestamp(), delta, })); @@ -330,15 +332,17 @@ impl EventBridge { /// Starts a thinking/processing step. pub async fn start_thinking(&self, title: Option<&str>) { - self.emit(Event::ThinkingStart(ag_ui_core::ThinkingStartEvent { - base: BaseEvent::with_current_timestamp(), - title: title.map(|s| s.to_string()), - })); + self.emit(Event::ThinkingStart( + syncable_ag_ui_core::ThinkingStartEvent { + base: BaseEvent::with_current_timestamp(), + title: title.map(|s| s.to_string()), + }, + )); } /// Ends the current thinking step. pub async fn end_thinking(&self) { - self.emit(Event::ThinkingEnd(ag_ui_core::ThinkingEndEvent { + self.emit(Event::ThinkingEnd(syncable_ag_ui_core::ThinkingEndEvent { base: BaseEvent::with_current_timestamp(), })); } @@ -346,7 +350,7 @@ impl EventBridge { /// Starts a step in the agent workflow. pub async fn start_step(&self, name: &str) { *self.current_step_name.write().await = Some(name.to_string()); - self.emit(Event::StepStarted(ag_ui_core::StepStartedEvent { + self.emit(Event::StepStarted(syncable_ag_ui_core::StepStartedEvent { base: BaseEvent::with_current_timestamp(), step_name: name.to_string(), })); @@ -354,9 +358,13 @@ impl EventBridge { /// Ends the current step. pub async fn end_step(&self) { - let step_name = self.current_step_name.write().await.take() + let step_name = self + .current_step_name + .write() + .await + .take() .unwrap_or_else(|| "unknown".to_string()); - self.emit(Event::StepFinished(ag_ui_core::StepFinishedEvent { + self.emit(Event::StepFinished(syncable_ag_ui_core::StepFinishedEvent { base: BaseEvent::with_current_timestamp(), step_name, })); @@ -368,7 +376,7 @@ impl EventBridge { /// Emits a custom event. pub async fn emit_custom(&self, name: &str, value: JsonValue) { - self.emit(Event::Custom(ag_ui_core::CustomEvent { + self.emit(Event::Custom(syncable_ag_ui_core::CustomEvent { base: BaseEvent::with_current_timestamp(), name: name.to_string(), value, @@ -424,7 +432,9 @@ mod tests { async fn test_tool_call() { let bridge = create_bridge(); - let tool_id = bridge.start_tool_call("test", &serde_json::json!({"key": "value"})).await; + let tool_id = bridge + .start_tool_call("test", &serde_json::json!({"key": "value"})) + .await; bridge.emit_tool_args_chunk(&tool_id, "more args").await; bridge.end_tool_call(&tool_id).await; // Should not panic diff --git a/src/server/mod.rs b/src/server/mod.rs index d6f9b129..ef9fce3c 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -45,16 +45,19 @@ pub mod routes; use std::net::SocketAddr; use std::sync::Arc; -use ag_ui_core::{Event, JsonValue, RunId, ThreadId}; -use axum::{routing::{get, post}, Router}; +use syncable_ag_ui_core::{Event, JsonValue, RunId, ThreadId}; +use axum::{ + Router, + routing::{get, post}, +}; +use tokio::sync::{RwLock, broadcast, mpsc}; use tower_http::cors::{Any, CorsLayer}; -use tokio::sync::{broadcast, mpsc, RwLock}; pub use bridge::EventBridge; pub use processor::{AgentProcessor, ProcessorConfig, ThreadSession}; // Re-export types needed for message handling -pub use ag_ui_core::types::{Context, Message as AgUiMessage, RunAgentInput, Tool}; +pub use syncable_ag_ui_core::types::{Context, Message as AgUiMessage, RunAgentInput, Tool}; /// Message from frontend to agent processor. /// Wraps RunAgentInput with optional response channel for acknowledgments. @@ -240,8 +243,7 @@ impl AgUiServer { // Optionally start the agent processor if self.config.enable_processor { - let processor_config = self.config.processor_config.clone() - .unwrap_or_default(); + let processor_config = self.config.processor_config.clone().unwrap_or_default(); if let Some(msg_rx) = self.state.take_message_receiver().await { let event_bridge = self.state.event_sender(); @@ -297,9 +299,7 @@ mod tests { #[test] fn test_config_builder() { - let config = AgUiConfig::new() - .port(8080) - .host("0.0.0.0"); + let config = AgUiConfig::new().port(8080).host("0.0.0.0"); assert_eq!(config.port, 8080); assert_eq!(config.host, "0.0.0.0"); } @@ -334,7 +334,7 @@ mod tests { #[tokio::test] async fn test_server_event_flow() { - use ag_ui_core::Event; + use syncable_ag_ui_core::Event; let state = ServerState::new(); let bridge = state.event_sender(); @@ -350,11 +350,14 @@ mod tests { #[tokio::test] async fn test_message_channel() { - use ag_ui_core::types::{RunAgentInput, Message}; + use syncable_ag_ui_core::types::{Message, RunAgentInput}; let state = ServerState::new(); let msg_tx = state.message_sender(); - let mut msg_rx = state.take_message_receiver().await.expect("Should get receiver"); + let mut msg_rx = state + .take_message_receiver() + .await + .expect("Should get receiver"); // Create a RunAgentInput using builder pattern let input = RunAgentInput::new(ThreadId::random(), RunId::random()) @@ -395,8 +398,7 @@ mod tests { .with_provider("anthropic") .with_model("claude-3-sonnet"); - let config = AgUiConfig::new() - .with_processor_config(processor_config); + let config = AgUiConfig::new().with_processor_config(processor_config); assert!(config.enable_processor); let proc_config = config.processor_config.unwrap(); @@ -406,14 +408,17 @@ mod tests { #[tokio::test] async fn test_processor_integration_with_state() { - use ag_ui_core::types::{Message, RunAgentInput}; - use ag_ui_core::Event; + use syncable_ag_ui_core::Event; + use syncable_ag_ui_core::types::{Message, RunAgentInput}; // Create state and get components let state = ServerState::new(); let msg_tx = state.message_sender(); let mut event_rx = state.subscribe(); - let msg_rx = state.take_message_receiver().await.expect("Should get receiver"); + let msg_rx = state + .take_message_receiver() + .await + .expect("Should get receiver"); // Create and spawn processor let event_bridge = state.event_sender(); @@ -429,13 +434,16 @@ mod tests { let input = RunAgentInput::new(thread_id.clone(), run_id.clone()) .with_messages(vec![Message::new_user("Integration test message")]); - msg_tx.send(AgentMessage::new(input)).await.expect("Should send"); + msg_tx + .send(AgentMessage::new(input)) + .await + .expect("Should send"); // Verify events are emitted - let event = tokio::time::timeout( - std::time::Duration::from_millis(200), - event_rx.recv() - ).await.expect("Should receive in time").expect("Should have event"); + let event = tokio::time::timeout(std::time::Duration::from_millis(200), event_rx.recv()) + .await + .expect("Should receive in time") + .expect("Should have event"); assert!(matches!(event, Event::RunStarted(_))); @@ -443,10 +451,7 @@ mod tests { drop(msg_tx); // Wait for processor to finish - let _ = tokio::time::timeout( - std::time::Duration::from_millis(200), - handle - ).await; + let _ = tokio::time::timeout(std::time::Duration::from_millis(200), handle).await; } // ============================================================================= @@ -454,15 +459,19 @@ mod tests { // ============================================================================= /// Helper to collect events until RunFinished or RunError - async fn collect_until_finished(rx: &mut tokio::sync::broadcast::Receiver) -> Vec { - use ag_ui_core::Event; + async fn collect_until_finished( + rx: &mut tokio::sync::broadcast::Receiver, + ) -> Vec { + use syncable_ag_ui_core::Event; let mut events = Vec::new(); loop { match tokio::time::timeout(std::time::Duration::from_secs(5), rx.recv()).await { Ok(Ok(event)) => { let is_finished = matches!(&event, Event::RunFinished(_) | Event::RunError(_)); events.push(event); - if is_finished { break; } + if is_finished { + break; + } } _ => break, } @@ -471,8 +480,10 @@ mod tests { } /// Helper to drain events until run is finished - async fn drain_events_until_run_finished(rx: &mut tokio::sync::broadcast::Receiver) { - use ag_ui_core::Event; + async fn drain_events_until_run_finished( + rx: &mut tokio::sync::broadcast::Receiver, + ) { + use syncable_ag_ui_core::Event; loop { match tokio::time::timeout(std::time::Duration::from_secs(30), rx.recv()).await { Ok(Ok(Event::RunFinished(_))) => break, @@ -485,13 +496,16 @@ mod tests { #[tokio::test] async fn test_multi_turn_conversation() { - use ag_ui_core::types::{Message, RunAgentInput}; + use syncable_ag_ui_core::types::{Message, RunAgentInput}; // Create state and components let state = ServerState::new(); let msg_tx = state.message_sender(); let mut event_rx = state.subscribe(); - let msg_rx = state.take_message_receiver().await.expect("Should get receiver"); + let msg_rx = state + .take_message_receiver() + .await + .expect("Should get receiver"); // Create processor let event_bridge = state.event_sender(); @@ -506,7 +520,10 @@ mod tests { // Send first message let input1 = RunAgentInput::new(thread_id.clone(), RunId::random()) .with_messages(vec![Message::new_user("Hello")]); - msg_tx.send(AgentMessage::new(input1)).await.expect("Should send"); + msg_tx + .send(AgentMessage::new(input1)) + .await + .expect("Should send"); // Wait for first response drain_events_until_run_finished(&mut event_rx).await; @@ -514,7 +531,10 @@ mod tests { // Send follow-up message (same thread) let input2 = RunAgentInput::new(thread_id.clone(), RunId::random()) .with_messages(vec![Message::new_user("Follow up message")]); - msg_tx.send(AgentMessage::new(input2)).await.expect("Should send"); + msg_tx + .send(AgentMessage::new(input2)) + .await + .expect("Should send"); // Verify second run completes drain_events_until_run_finished(&mut event_rx).await; @@ -525,8 +545,8 @@ mod tests { #[tokio::test] async fn test_event_sequence() { - use ag_ui_core::types::{Message, RunAgentInput}; - use ag_ui_core::Event; + use syncable_ag_ui_core::Event; + use syncable_ag_ui_core::types::{Message, RunAgentInput}; // Setup server state let state = ServerState::new(); @@ -536,7 +556,9 @@ mod tests { let event_bridge = state.event_sender(); let mut processor = AgentProcessor::with_defaults(msg_rx, event_bridge); - tokio::spawn(async move { processor.run().await; }); + tokio::spawn(async move { + processor.run().await; + }); // Send message let thread_id = ThreadId::random(); @@ -549,11 +571,17 @@ mod tests { // Verify sequence assert!(!events.is_empty(), "Should receive at least one event"); - assert!(matches!(events[0], Event::RunStarted(_)), "First event should be RunStarted"); + assert!( + matches!(events[0], Event::RunStarted(_)), + "First event should be RunStarted" + ); // Should end with RunFinished or RunError assert!( - matches!(events.last(), Some(Event::RunFinished(_) | Event::RunError(_))), + matches!( + events.last(), + Some(Event::RunFinished(_) | Event::RunError(_)) + ), "Last event should be RunFinished or RunError" ); @@ -561,15 +589,18 @@ mod tests { // RunStarted -> StepStarted -> StepFinished -> TextMessageStart -> TextMessageContent* -> TextMessageEnd -> RunFinished // Without API key, we get: RunStarted -> StepStarted -> StepFinished -> RunError // Either way, verify we have multiple events - assert!(events.len() >= 2, "Should have at least RunStarted and terminal event"); + assert!( + events.len() >= 2, + "Should have at least RunStarted and terminal event" + ); drop(msg_tx); } #[tokio::test] async fn test_empty_message_error() { - use ag_ui_core::types::RunAgentInput; - use ag_ui_core::Event; + use syncable_ag_ui_core::Event; + use syncable_ag_ui_core::types::RunAgentInput; let state = ServerState::new(); let msg_tx = state.message_sender(); @@ -578,7 +609,9 @@ mod tests { let event_bridge = state.event_sender(); let mut processor = AgentProcessor::with_defaults(msg_rx, event_bridge); - tokio::spawn(async move { processor.run().await; }); + tokio::spawn(async move { + processor.run().await; + }); // Send message with no user content let input = RunAgentInput::new(ThreadId::random(), RunId::random()); @@ -588,7 +621,10 @@ mod tests { let events = collect_until_finished(&mut event_rx).await; // Should get RunStarted then RunError - assert!(matches!(events[0], Event::RunStarted(_)), "First should be RunStarted"); + assert!( + matches!(events[0], Event::RunStarted(_)), + "First should be RunStarted" + ); assert!( matches!(events.last(), Some(Event::RunError(_))), "Should end with RunError for empty message" @@ -599,8 +635,8 @@ mod tests { #[tokio::test] async fn test_invalid_provider_error() { - use ag_ui_core::types::{Message, RunAgentInput}; - use ag_ui_core::Event; + use syncable_ag_ui_core::Event; + use syncable_ag_ui_core::types::{Message, RunAgentInput}; let state = ServerState::new(); let msg_tx = state.message_sender(); @@ -612,7 +648,9 @@ mod tests { let config = ProcessorConfig::new().with_provider("invalid_provider_xyz"); let mut processor = AgentProcessor::new(msg_rx, event_bridge, config); - tokio::spawn(async move { processor.run().await; }); + tokio::spawn(async move { + processor.run().await; + }); let input = RunAgentInput::new(ThreadId::random(), RunId::random()) .with_messages(vec![Message::new_user("Test invalid provider")]); @@ -632,7 +670,7 @@ mod tests { #[tokio::test] async fn test_custom_system_prompt() { - use ag_ui_core::types::{Message, RunAgentInput}; + use syncable_ag_ui_core::types::{Message, RunAgentInput}; let state = ServerState::new(); let msg_tx = state.message_sender(); @@ -641,11 +679,14 @@ mod tests { let event_bridge = state.event_sender(); // Configure with custom system prompt - let config = ProcessorConfig::new() - .with_system_prompt("You are a DevOps assistant. Always respond with deployment advice."); + let config = ProcessorConfig::new().with_system_prompt( + "You are a DevOps assistant. Always respond with deployment advice.", + ); let mut processor = AgentProcessor::new(msg_rx, event_bridge, config); - tokio::spawn(async move { processor.run().await; }); + tokio::spawn(async move { + processor.run().await; + }); let input = RunAgentInput::new(ThreadId::random(), RunId::random()) .with_messages(vec![Message::new_user("Hello")]); diff --git a/src/server/processor.rs b/src/server/processor.rs index fc13f688..a843b65c 100644 --- a/src/server/processor.rs +++ b/src/server/processor.rs @@ -18,11 +18,11 @@ use std::collections::HashMap; use std::path::PathBuf; use std::time::Instant; -use ag_ui_core::{Role, RunId, ThreadId}; +use syncable_ag_ui_core::{Role, RunId, ThreadId}; use rig::client::{CompletionClient, ProviderClient}; -use rig::completion::message::{AssistantContent, UserContent}; use rig::completion::Message as RigMessage; use rig::completion::Prompt; +use rig::completion::message::{AssistantContent, UserContent}; use rig::one_or_many::OneOrMany; use rig::providers::{anthropic, openai}; use tokio::sync::mpsc; @@ -32,21 +32,36 @@ use super::{AgentMessage, EventBridge}; use crate::agent::prompts; use crate::agent::tools::{ // Core analysis tools - AnalyzeTool, ListDirectoryTool, ReadFileTool, SecurityScanTool, VulnerabilitiesTool, + AnalyzeTool, + DclintTool, // Linting tools - HadolintTool, DclintTool, KubelintTool, HelmlintTool, + HadolintTool, + HelmlintTool, + K8sCostsTool, + K8sDriftTool, // K8s tools - K8sOptimizeTool, K8sCostsTool, K8sDriftTool, + K8sOptimizeTool, + KubelintTool, + ListDirectoryTool, + ListOutputsTool, + ReadFileTool, + RetrieveOutputTool, + SecurityScanTool, + ShellTool, // Terraform tools - TerraformFmtTool, TerraformValidateTool, TerraformInstallTool, + TerraformFmtTool, + TerraformInstallTool, + TerraformValidateTool, + VulnerabilitiesTool, // Web and retrieval tools - WebFetchTool, RetrieveOutputTool, ListOutputsTool, + WebFetchTool, // Write tools for generation - WriteFileTool, WriteFilesTool, ShellTool, + WriteFileTool, + WriteFilesTool, }; -use ag_ui_core::ToolCallId; -use ag_ui_core::state::StateManager; +use syncable_ag_ui_core::ToolCallId; +use syncable_ag_ui_core::state::StateManager; use rig::agent::CancelSignal; use rig::completion::{CompletionModel, CompletionResponse, Message as RigPromptMessage}; use serde::{Deserialize, Serialize}; @@ -138,7 +153,13 @@ impl AgentUiState { } /// Adds a tool result for rich UI rendering. - pub fn add_tool_result(&mut self, tool_name: String, args: serde_json::Value, result: serde_json::Value, is_error: bool) { + pub fn add_tool_result( + &mut self, + tool_name: String, + args: serde_json::Value, + result: serde_json::Value, + is_error: bool, + ) { self.tool_results.push(ToolResult { tool_name, args, @@ -241,8 +262,20 @@ where let description = match name.as_str() { // Core analysis tools "analyze_project" => "Analyzing project structure...".to_string(), - "read_file" => format!("Reading file: {}", args_json.get("path").and_then(|v| v.as_str()).unwrap_or("...")), - "list_directory" => format!("Listing directory: {}", args_json.get("path").and_then(|v| v.as_str()).unwrap_or("...")), + "read_file" => format!( + "Reading file: {}", + args_json + .get("path") + .and_then(|v| v.as_str()) + .unwrap_or("...") + ), + "list_directory" => format!( + "Listing directory: {}", + args_json + .get("path") + .and_then(|v| v.as_str()) + .unwrap_or("...") + ), // Security tools "security_scan" => "Running security scan...".to_string(), "check_vulnerabilities" => "Checking for vulnerabilities...".to_string(), @@ -260,15 +293,38 @@ where "terraform_validate" => "Validating Terraform configuration...".to_string(), "terraform_install" => "Installing Terraform...".to_string(), // Web tools - "web_fetch" => format!("Fetching: {}", args_json.get("url").and_then(|v| v.as_str()).unwrap_or("...")), + "web_fetch" => format!( + "Fetching: {}", + args_json + .get("url") + .and_then(|v| v.as_str()) + .unwrap_or("...") + ), // Retrieval tools "retrieve_output" => "Retrieving stored output...".to_string(), "list_outputs" => "Listing available outputs...".to_string(), // Write tools - "write_file" => format!("Writing file: {}", args_json.get("path").and_then(|v| v.as_str()).unwrap_or("...")), + "write_file" => format!( + "Writing file: {}", + args_json + .get("path") + .and_then(|v| v.as_str()) + .unwrap_or("...") + ), "write_files" => "Writing multiple files...".to_string(), // Shell tool - "shell" => format!("Running command: {}", args_json.get("command").and_then(|v| v.as_str()).map(|s| if s.len() > 50 { format!("{}...", &s[..50]) } else { s.to_string() }).unwrap_or("...".to_string())), + "shell" => format!( + "Running command: {}", + args_json + .get("command") + .and_then(|v| v.as_str()) + .map(|s| if s.len() > 50 { + format!("{}...", &s[..50]) + } else { + s.to_string() + }) + .unwrap_or("...".to_string()) + ), _ => format!("Running {}...", name.replace('_', " ")), }; s.add_step(description); @@ -579,10 +635,7 @@ impl AgentProcessor { /// Extracts the user message content from RunAgentInput messages. /// /// Returns the last user message content, or None if no user messages. - fn extract_user_input( - &self, - messages: &[ag_ui_core::types::Message], - ) -> Option { + fn extract_user_input(&self, messages: &[syncable_ag_ui_core::types::Message]) -> Option { // Find the last user message and extract its content messages .iter() @@ -662,11 +715,15 @@ impl AgentProcessor { "openai" => { debug!("Setting OPENAI_API_KEY from forwardedProps"); // SAFETY: Single-threaded CLI context - unsafe { std::env::set_var("OPENAI_API_KEY", api_key); } + unsafe { + std::env::set_var("OPENAI_API_KEY", api_key); + } } "anthropic" => { debug!("Setting ANTHROPIC_API_KEY from forwardedProps"); - unsafe { std::env::set_var("ANTHROPIC_API_KEY", api_key); } + unsafe { + std::env::set_var("ANTHROPIC_API_KEY", api_key); + } } _ => {} } @@ -677,7 +734,9 @@ impl AgentProcessor { if let Some(region) = obj.get("awsRegion").and_then(|v| v.as_str()) { if !region.is_empty() { debug!(region = %region, "Setting AWS_REGION from forwardedProps"); - unsafe { std::env::set_var("AWS_REGION", region); } + unsafe { + std::env::set_var("AWS_REGION", region); + } } } } @@ -691,12 +750,7 @@ impl AgentProcessor { /// 3. Emits TextMessage events /// 4. Updates session history /// 5. Emits RunFinished - async fn process_message( - &mut self, - thread_id: ThreadId, - _run_id: RunId, - user_input: String, - ) { + async fn process_message(&mut self, thread_id: ThreadId, _run_id: RunId, user_input: String) { info!( thread_id = %thread_id, input_len = user_input.len(), @@ -753,7 +807,9 @@ impl AgentProcessor { error = %e, "LLM call failed" ); - self.event_bridge.finish_run_with_error(&e.to_string()).await; + self.event_bridge + .finish_run_with_error(&e.to_string()) + .await; } } } @@ -842,7 +898,9 @@ impl AgentProcessor { // Check for API key if std::env::var("ANTHROPIC_API_KEY").is_err() { warn!("ANTHROPIC_API_KEY not set"); - return Err(ProcessorError::MissingApiKey("ANTHROPIC_API_KEY".to_string())); + return Err(ProcessorError::MissingApiKey( + "ANTHROPIC_API_KEY".to_string(), + )); } // Need fresh hook for anthropic (hook may be consumed by openai path) @@ -937,9 +995,7 @@ impl AgentProcessor { .await .map_err(|e| ProcessorError::CompletionFailed(e.to_string())) } - _ => { - Err(ProcessorError::UnsupportedProvider(provider.to_string())) - } + _ => Err(ProcessorError::UnsupportedProvider(provider.to_string())), } } } @@ -947,9 +1003,9 @@ impl AgentProcessor { #[cfg(test)] mod tests { use super::*; - use tokio::sync::broadcast; use std::sync::Arc; use tokio::sync::RwLock; + use tokio::sync::broadcast; fn create_test_processor() -> (AgentProcessor, mpsc::Sender) { let (msg_tx, msg_rx) = mpsc::channel(100); @@ -988,13 +1044,22 @@ mod tests { let config = ProcessorConfig::default(); assert!(config.system_prompt.is_none()); // Analysis prompt contains agent identity section - assert!(config.effective_system_prompt(None).contains("DevOps/Platform Engineer")); + assert!( + config + .effective_system_prompt(None) + .contains("DevOps/Platform Engineer") + ); // Custom system prompt overrides auto-generated - let config = ProcessorConfig::new() - .with_system_prompt("You are a DevOps expert."); - assert_eq!(config.system_prompt, Some("You are a DevOps expert.".to_string())); - assert_eq!(config.effective_system_prompt(None), "You are a DevOps expert."); + let config = ProcessorConfig::new().with_system_prompt("You are a DevOps expert."); + assert_eq!( + config.system_prompt, + Some("You are a DevOps expert.".to_string()) + ); + assert_eq!( + config.effective_system_prompt(None), + "You are a DevOps expert." + ); } #[test] @@ -1072,18 +1137,19 @@ mod tests { let thread_id = ThreadId::random(); let run_id = RunId::random(); - processor.process_message( - thread_id.clone(), - run_id, - "Hello, agent!".to_string(), - ).await; + processor + .process_message(thread_id.clone(), run_id, "Hello, agent!".to_string()) + .await; // Check session was created and user message was added assert_eq!(processor.session_count(), 1); let session = processor.sessions.get(&thread_id).unwrap(); // User message should always be added - assert!(session.history.len() >= 1, "User message should be in history"); + assert!( + session.history.len() >= 1, + "User message should be in history" + ); // If API keys are available, turn_count and history should include assistant response // If not, the LLM call fails gracefully and only user message is present @@ -1100,8 +1166,8 @@ mod tests { #[tokio::test] async fn test_run_processes_messages() { - use ag_ui_core::types::{Message as AgUiProtocolMessage, RunAgentInput}; - use ag_ui_core::Event; + use syncable_ag_ui_core::Event; + use syncable_ag_ui_core::types::{Message as AgUiProtocolMessage, RunAgentInput}; use tokio::sync::broadcast; let (msg_tx, msg_rx) = mpsc::channel(100); @@ -1130,10 +1196,10 @@ mod tests { msg_tx.send(agent_msg).await.expect("Should send"); // Verify we receive RunStarted event - let event = tokio::time::timeout( - std::time::Duration::from_millis(100), - event_rx.recv() - ).await.expect("Should receive event in time").expect("Should have event"); + let event = tokio::time::timeout(std::time::Duration::from_millis(100), event_rx.recv()) + .await + .expect("Should receive event in time") + .expect("Should have event"); assert!(matches!(event, Event::RunStarted(_))); @@ -1141,16 +1207,16 @@ mod tests { drop(msg_tx); // Wait for processor to finish - tokio::time::timeout( - std::time::Duration::from_millis(100), - handle - ).await.expect("Processor should finish").expect("Should not panic"); + tokio::time::timeout(std::time::Duration::from_millis(100), handle) + .await + .expect("Processor should finish") + .expect("Should not panic"); } #[tokio::test] async fn test_run_handles_empty_messages() { - use ag_ui_core::types::RunAgentInput; - use ag_ui_core::Event; + use syncable_ag_ui_core::Event; + use syncable_ag_ui_core::types::RunAgentInput; use tokio::sync::broadcast; let (msg_tx, msg_rx) = mpsc::channel(100); @@ -1179,17 +1245,17 @@ mod tests { msg_tx.send(agent_msg).await.expect("Should send"); // Should receive RunStarted then RunError - let event = tokio::time::timeout( - std::time::Duration::from_millis(100), - event_rx.recv() - ).await.expect("Should receive event").expect("Should have event"); + let event = tokio::time::timeout(std::time::Duration::from_millis(100), event_rx.recv()) + .await + .expect("Should receive event") + .expect("Should have event"); assert!(matches!(event, Event::RunStarted(_))); - let event = tokio::time::timeout( - std::time::Duration::from_millis(100), - event_rx.recv() - ).await.expect("Should receive event").expect("Should have event"); + let event = tokio::time::timeout(std::time::Duration::from_millis(100), event_rx.recv()) + .await + .expect("Should receive event") + .expect("Should have event"); assert!(matches!(event, Event::RunError(_))); diff --git a/src/server/routes.rs b/src/server/routes.rs index cb945fbd..b5331226 100644 --- a/src/server/routes.rs +++ b/src/server/routes.rs @@ -8,15 +8,15 @@ use std::convert::Infallible; use axum::{ + Json, extract::{ - ws::{Message, WebSocket, WebSocketUpgrade}, State, + ws::{Message, WebSocket, WebSocketUpgrade}, }, response::{ - sse::{Event as SseEvent, KeepAlive, Sse}, IntoResponse, Response, + sse::{Event as SseEvent, KeepAlive, Sse}, }, - Json, }; use futures_util::{SinkExt, Stream, StreamExt}; use serde::Deserialize; @@ -109,7 +109,10 @@ pub async fn post_message( State(state): State, Json(raw): Json, ) -> Response { - debug!("Received POST request body: {}", serde_json::to_string_pretty(&raw).unwrap_or_default()); + debug!( + "Received POST request body: {}", + serde_json::to_string_pretty(&raw).unwrap_or_default() + ); // Try to parse as CopilotKit request let copilot_req: Result = serde_json::from_value(raw.clone()); @@ -148,17 +151,21 @@ pub async fn post_message( forwarded_props: None, }); - let thread_id_str = body.thread_id + let thread_id_str = body + .thread_id .or(req.params.as_ref().and_then(|p| p.thread_id.clone())) .unwrap_or_else(|| uuid::Uuid::new_v4().to_string()); - let run_id_str = body.run_id + let run_id_str = body + .run_id .unwrap_or_else(|| uuid::Uuid::new_v4().to_string()); // Parse IDs, falling back to random if invalid UUID - let thread_id: ag_ui_core::ThreadId = thread_id_str.parse() - .unwrap_or_else(|_| ag_ui_core::ThreadId::random()); - let run_id: ag_ui_core::RunId = run_id_str.parse() - .unwrap_or_else(|_| ag_ui_core::RunId::random()); + let thread_id: syncable_ag_ui_core::ThreadId = thread_id_str + .parse() + .unwrap_or_else(|_| syncable_ag_ui_core::ThreadId::random()); + let run_id: syncable_ag_ui_core::RunId = run_id_str + .parse() + .unwrap_or_else(|_| syncable_ag_ui_core::RunId::random()); // Convert messages from JSON to Message type let messages = convert_messages(body.messages.unwrap_or_default()); @@ -177,16 +184,20 @@ pub async fn post_message( // Direct RunAgentInput format debug!("Detected direct RunAgentInput format"); - let thread_id_str = req.thread_id + let thread_id_str = req + .thread_id .unwrap_or_else(|| uuid::Uuid::new_v4().to_string()); - let run_id_str = req.run_id + let run_id_str = req + .run_id .unwrap_or_else(|| uuid::Uuid::new_v4().to_string()); // Parse IDs, falling back to random if invalid UUID - let thread_id: ag_ui_core::ThreadId = thread_id_str.parse() - .unwrap_or_else(|_| ag_ui_core::ThreadId::random()); - let run_id: ag_ui_core::RunId = run_id_str.parse() - .unwrap_or_else(|_| ag_ui_core::RunId::random()); + let thread_id: syncable_ag_ui_core::ThreadId = thread_id_str + .parse() + .unwrap_or_else(|_| syncable_ag_ui_core::ThreadId::random()); + let run_id: syncable_ag_ui_core::RunId = run_id_str + .parse() + .unwrap_or_else(|_| syncable_ag_ui_core::RunId::random()); let messages = convert_messages(req.messages.unwrap_or_default()); let tools = convert_tools(req.tools.unwrap_or_default()); @@ -205,7 +216,8 @@ pub async fn post_message( return Json(json!({ "status": "error", "message": "Invalid request format" - })).into_response(); + })) + .into_response(); } } Err(e) => { @@ -213,7 +225,8 @@ pub async fn post_message( return Json(json!({ "status": "error", "message": format!("Failed to parse request: {}", e) - })).into_response(); + })) + .into_response(); } }; @@ -239,12 +252,13 @@ pub async fn post_message( return Json(json!({ "status": "error", "message": "Failed to route message to agent processor" - })).into_response(); + })) + .into_response(); } // Create SSE stream that filters events and ends on RunFinished/RunError let stream = async_stream::stream! { - use ag_ui_core::Event; + use syncable_ag_ui_core::Event; loop { match event_rx.recv().await { @@ -276,19 +290,24 @@ pub async fn post_message( } }; - Sse::new(stream).keep_alive(KeepAlive::default()).into_response() + Sse::new(stream) + .keep_alive(KeepAlive::default()) + .into_response() } /// Convert JSON messages to AG-UI Message type -fn convert_messages(raw_messages: Vec) -> Vec { - use ag_ui_core::MessageId; +fn convert_messages( + raw_messages: Vec, +) -> Vec { + use syncable_ag_ui_core::MessageId; raw_messages .into_iter() .filter_map(|msg| { let role = msg.get("role")?.as_str()?; let content = msg.get("content").and_then(|c| c.as_str()).unwrap_or(""); - let id_str = msg.get("id") + let id_str = msg + .get("id") .and_then(|i| i.as_str()) .map(|s| s.to_string()) .unwrap_or_else(|| uuid::Uuid::new_v4().to_string()); @@ -297,18 +316,18 @@ fn convert_messages(raw_messages: Vec) -> Vec Some(ag_ui_core::types::Message::User { + "user" => Some(syncable_ag_ui_core::types::Message::User { id, content: content.to_string(), name: msg.get("name").and_then(|n| n.as_str()).map(String::from), }), - "assistant" => Some(ag_ui_core::types::Message::Assistant { + "assistant" => Some(syncable_ag_ui_core::types::Message::Assistant { id, content: Some(content.to_string()), name: msg.get("name").and_then(|n| n.as_str()).map(String::from), tool_calls: None, }), - "system" => Some(ag_ui_core::types::Message::System { + "system" => Some(syncable_ag_ui_core::types::Message::System { id, content: content.to_string(), name: msg.get("name").and_then(|n| n.as_str()).map(String::from), @@ -323,32 +342,38 @@ fn convert_messages(raw_messages: Vec) -> Vec) -> Vec { +fn convert_tools(raw_tools: Vec) -> Vec { raw_tools .into_iter() .filter_map(|tool| { let name = tool.get("name")?.as_str()?.to_string(); - let description = tool.get("description") + let description = tool + .get("description") .and_then(|d| d.as_str()) .unwrap_or("") .to_string(); - let parameters = tool.get("parameters") + let parameters = tool + .get("parameters") .cloned() .unwrap_or(serde_json::json!({})); - Some(ag_ui_core::types::Tool::new(name, description, parameters)) + Some(syncable_ag_ui_core::types::Tool::new( + name, + description, + parameters, + )) }) .collect() } /// Convert JSON context to AG-UI Context type -fn convert_context(raw_context: Vec) -> Vec { +fn convert_context(raw_context: Vec) -> Vec { raw_context .into_iter() .filter_map(|ctx| { let description = ctx.get("description")?.as_str()?.to_string(); let value = ctx.get("value")?.as_str()?.to_string(); - Some(ag_ui_core::types::Context::new(description, value)) + Some(syncable_ag_ui_core::types::Context::new(description, value)) }) .collect() } @@ -367,9 +392,7 @@ pub async fn sse_handler( let json = serde_json::to_string(&event).ok()?; let event_type = event.event_type().as_str().to_string(); - Some(Ok(SseEvent::default() - .event(event_type) - .data(json))) + Some(Ok(SseEvent::default().event(event_type).data(json))) } Err(_) => None, // Lagged, skip this event } @@ -379,10 +402,7 @@ pub async fn sse_handler( } /// WebSocket endpoint for streaming AG-UI events. -pub async fn ws_handler( - ws: WebSocketUpgrade, - State(state): State, -) -> Response { +pub async fn ws_handler(ws: WebSocketUpgrade, State(state): State) -> Response { ws.on_upgrade(move |socket| handle_websocket(socket, state)) } @@ -457,8 +477,8 @@ async fn handle_websocket(socket: WebSocket, state: ServerState) { #[cfg(test)] mod tests { use super::*; - use ag_ui_core::types::Message as AgUiProtocolMessage; - use ag_ui_core::{RunId, ThreadId}; + use syncable_ag_ui_core::types::Message as AgUiProtocolMessage; + use syncable_ag_ui_core::{RunId, ThreadId}; use axum::extract::State; #[tokio::test] @@ -474,7 +494,10 @@ mod tests { use http::StatusCode; let state = ServerState::new(); - let mut msg_rx = state.take_message_receiver().await.expect("Should get receiver"); + let mut msg_rx = state + .take_message_receiver() + .await + .expect("Should get receiver"); // Create RunAgentInput as JSON value let thread_id = ThreadId::random(); @@ -510,7 +533,10 @@ mod tests { use http::StatusCode; let state = ServerState::new(); - let mut msg_rx = state.take_message_receiver().await.expect("Should get receiver"); + let mut msg_rx = state + .take_message_receiver() + .await + .expect("Should get receiver"); // Create CopilotKit envelope format let input_json = json!({ diff --git a/src/wizard/cloud_provider_data.rs b/src/wizard/cloud_provider_data.rs index 643a24cd..597e82f6 100644 --- a/src/wizard/cloud_provider_data.rs +++ b/src/wizard/cloud_provider_data.rs @@ -52,43 +52,163 @@ pub struct AcaResourcePair { /// Azure Container Apps resource pairs (fixed by Azure, 8 combos) pub static ACA_RESOURCE_PAIRS: &[AcaResourcePair] = &[ - AcaResourcePair { cpu: "0.25", memory: "0.5Gi", label: "0.25 vCPU, 0.5 GB" }, - AcaResourcePair { cpu: "0.5", memory: "1.0Gi", label: "0.5 vCPU, 1 GB" }, - AcaResourcePair { cpu: "0.75", memory: "1.5Gi", label: "0.75 vCPU, 1.5 GB" }, - AcaResourcePair { cpu: "1.0", memory: "2.0Gi", label: "1 vCPU, 2 GB" }, - AcaResourcePair { cpu: "1.25", memory: "2.5Gi", label: "1.25 vCPU, 2.5 GB" }, - AcaResourcePair { cpu: "1.5", memory: "3.0Gi", label: "1.5 vCPU, 3 GB" }, - AcaResourcePair { cpu: "1.75", memory: "3.5Gi", label: "1.75 vCPU, 3.5 GB" }, - AcaResourcePair { cpu: "2.0", memory: "4.0Gi", label: "2 vCPU, 4 GB" }, + AcaResourcePair { + cpu: "0.25", + memory: "0.5Gi", + label: "0.25 vCPU, 0.5 GB", + }, + AcaResourcePair { + cpu: "0.5", + memory: "1.0Gi", + label: "0.5 vCPU, 1 GB", + }, + AcaResourcePair { + cpu: "0.75", + memory: "1.5Gi", + label: "0.75 vCPU, 1.5 GB", + }, + AcaResourcePair { + cpu: "1.0", + memory: "2.0Gi", + label: "1 vCPU, 2 GB", + }, + AcaResourcePair { + cpu: "1.25", + memory: "2.5Gi", + label: "1.25 vCPU, 2.5 GB", + }, + AcaResourcePair { + cpu: "1.5", + memory: "3.0Gi", + label: "1.5 vCPU, 3 GB", + }, + AcaResourcePair { + cpu: "1.75", + memory: "3.5Gi", + label: "1.75 vCPU, 3.5 GB", + }, + AcaResourcePair { + cpu: "2.0", + memory: "4.0Gi", + label: "2 vCPU, 4 GB", + }, ]; /// Azure regions (Container Apps supported regions) pub static AZURE_REGIONS: &[CloudRegion] = &[ // Americas - CloudRegion { id: "eastus", name: "East US", location: "Virginia" }, - CloudRegion { id: "eastus2", name: "East US 2", location: "Virginia" }, - CloudRegion { id: "westus", name: "West US", location: "California" }, - CloudRegion { id: "westus2", name: "West US 2", location: "Washington" }, - CloudRegion { id: "westus3", name: "West US 3", location: "Arizona" }, - CloudRegion { id: "centralus", name: "Central US", location: "Iowa" }, - CloudRegion { id: "canadacentral", name: "Canada Central", location: "Toronto" }, - CloudRegion { id: "brazilsouth", name: "Brazil South", location: "São Paulo" }, + CloudRegion { + id: "eastus", + name: "East US", + location: "Virginia", + }, + CloudRegion { + id: "eastus2", + name: "East US 2", + location: "Virginia", + }, + CloudRegion { + id: "westus", + name: "West US", + location: "California", + }, + CloudRegion { + id: "westus2", + name: "West US 2", + location: "Washington", + }, + CloudRegion { + id: "westus3", + name: "West US 3", + location: "Arizona", + }, + CloudRegion { + id: "centralus", + name: "Central US", + location: "Iowa", + }, + CloudRegion { + id: "canadacentral", + name: "Canada Central", + location: "Toronto", + }, + CloudRegion { + id: "brazilsouth", + name: "Brazil South", + location: "São Paulo", + }, // Europe - CloudRegion { id: "westeurope", name: "West Europe", location: "Netherlands" }, - CloudRegion { id: "northeurope", name: "North Europe", location: "Ireland" }, - CloudRegion { id: "uksouth", name: "UK South", location: "London" }, - CloudRegion { id: "ukwest", name: "UK West", location: "Cardiff" }, - CloudRegion { id: "germanywestcentral", name: "Germany West Central", location: "Frankfurt" }, - CloudRegion { id: "francecentral", name: "France Central", location: "Paris" }, - CloudRegion { id: "swedencentral", name: "Sweden Central", location: "Gävle" }, + CloudRegion { + id: "westeurope", + name: "West Europe", + location: "Netherlands", + }, + CloudRegion { + id: "northeurope", + name: "North Europe", + location: "Ireland", + }, + CloudRegion { + id: "uksouth", + name: "UK South", + location: "London", + }, + CloudRegion { + id: "ukwest", + name: "UK West", + location: "Cardiff", + }, + CloudRegion { + id: "germanywestcentral", + name: "Germany West Central", + location: "Frankfurt", + }, + CloudRegion { + id: "francecentral", + name: "France Central", + location: "Paris", + }, + CloudRegion { + id: "swedencentral", + name: "Sweden Central", + location: "Gävle", + }, // Asia Pacific - CloudRegion { id: "eastasia", name: "East Asia", location: "Hong Kong" }, - CloudRegion { id: "southeastasia", name: "Southeast Asia", location: "Singapore" }, - CloudRegion { id: "japaneast", name: "Japan East", location: "Tokyo" }, - CloudRegion { id: "japanwest", name: "Japan West", location: "Osaka" }, - CloudRegion { id: "koreacentral", name: "Korea Central", location: "Seoul" }, - CloudRegion { id: "australiaeast", name: "Australia East", location: "Sydney" }, - CloudRegion { id: "centralindia", name: "Central India", location: "Pune" }, + CloudRegion { + id: "eastasia", + name: "East Asia", + location: "Hong Kong", + }, + CloudRegion { + id: "southeastasia", + name: "Southeast Asia", + location: "Singapore", + }, + CloudRegion { + id: "japaneast", + name: "Japan East", + location: "Tokyo", + }, + CloudRegion { + id: "japanwest", + name: "Japan West", + location: "Osaka", + }, + CloudRegion { + id: "koreacentral", + name: "Korea Central", + location: "Seoul", + }, + CloudRegion { + id: "australiaeast", + name: "Australia East", + location: "Sydney", + }, + CloudRegion { + id: "centralindia", + name: "Central India", + location: "Pune", + }, ]; // ============================================================================= @@ -108,11 +228,31 @@ pub struct CloudRunCpuMemory { /// GCP Cloud Run CPU/memory constraints (matching frontend CLOUD_RUN_MEMORY_CONSTRAINTS) pub static CLOUD_RUN_CPU_MEMORY: &[CloudRunCpuMemory] = &[ - CloudRunCpuMemory { cpu: "1", memory_options: &["128Mi", "256Mi", "512Mi", "1Gi", "2Gi", "4Gi"], default_memory: "512Mi" }, - CloudRunCpuMemory { cpu: "2", memory_options: &["256Mi", "512Mi", "1Gi", "2Gi", "4Gi", "8Gi"], default_memory: "2Gi" }, - CloudRunCpuMemory { cpu: "4", memory_options: &["512Mi", "1Gi", "2Gi", "4Gi", "8Gi", "16Gi"], default_memory: "4Gi" }, - CloudRunCpuMemory { cpu: "6", memory_options: &["1Gi", "2Gi", "4Gi", "8Gi", "16Gi", "24Gi"], default_memory: "8Gi" }, - CloudRunCpuMemory { cpu: "8", memory_options: &["2Gi", "4Gi", "8Gi", "16Gi", "24Gi", "32Gi"], default_memory: "16Gi" }, + CloudRunCpuMemory { + cpu: "1", + memory_options: &["128Mi", "256Mi", "512Mi", "1Gi", "2Gi", "4Gi"], + default_memory: "512Mi", + }, + CloudRunCpuMemory { + cpu: "2", + memory_options: &["256Mi", "512Mi", "1Gi", "2Gi", "4Gi", "8Gi"], + default_memory: "2Gi", + }, + CloudRunCpuMemory { + cpu: "4", + memory_options: &["512Mi", "1Gi", "2Gi", "4Gi", "8Gi", "16Gi"], + default_memory: "4Gi", + }, + CloudRunCpuMemory { + cpu: "6", + memory_options: &["1Gi", "2Gi", "4Gi", "8Gi", "16Gi", "24Gi"], + default_memory: "8Gi", + }, + CloudRunCpuMemory { + cpu: "8", + memory_options: &["2Gi", "4Gi", "8Gi", "16Gi", "24Gi", "32Gi"], + default_memory: "16Gi", + }, ]; // ============================================================================= @@ -121,12 +261,16 @@ pub static CLOUD_RUN_CPU_MEMORY: &[CloudRunCpuMemory] = &[ /// Validate that a CPU/memory pair is valid for Azure Container Apps pub fn validate_aca_cpu_memory(cpu: &str, memory: &str) -> bool { - ACA_RESOURCE_PAIRS.iter().any(|p| p.cpu == cpu && p.memory == memory) + ACA_RESOURCE_PAIRS + .iter() + .any(|p| p.cpu == cpu && p.memory == memory) } /// Validate that a CPU/memory pair is valid for GCP Cloud Run pub fn validate_cloud_run_cpu_memory(cpu: &str, memory: &str) -> bool { - CLOUD_RUN_CPU_MEMORY.iter().any(|c| c.cpu == cpu && c.memory_options.contains(&memory)) + CLOUD_RUN_CPU_MEMORY + .iter() + .any(|c| c.cpu == cpu && c.memory_options.contains(&memory)) } /// Get available memory options for a given GCP Cloud Run CPU level @@ -145,37 +289,147 @@ pub fn get_cloud_run_memory_for_cpu(cpu: &str) -> &'static [&'static str] { /// GCP regions pub static GCP_REGIONS: &[CloudRegion] = &[ // Americas - CloudRegion { id: "us-central1", name: "Iowa", location: "US Central" }, - CloudRegion { id: "us-east1", name: "South Carolina", location: "US East" }, - CloudRegion { id: "us-east4", name: "Virginia", location: "US East" }, - CloudRegion { id: "us-west1", name: "Oregon", location: "US West" }, - CloudRegion { id: "us-west2", name: "Los Angeles", location: "US West" }, + CloudRegion { + id: "us-central1", + name: "Iowa", + location: "US Central", + }, + CloudRegion { + id: "us-east1", + name: "South Carolina", + location: "US East", + }, + CloudRegion { + id: "us-east4", + name: "Virginia", + location: "US East", + }, + CloudRegion { + id: "us-west1", + name: "Oregon", + location: "US West", + }, + CloudRegion { + id: "us-west2", + name: "Los Angeles", + location: "US West", + }, // Europe - CloudRegion { id: "europe-west1", name: "Belgium", location: "Europe" }, - CloudRegion { id: "europe-west2", name: "London", location: "UK" }, - CloudRegion { id: "europe-west3", name: "Frankfurt", location: "Germany" }, - CloudRegion { id: "europe-west4", name: "Netherlands", location: "Europe" }, - CloudRegion { id: "europe-north1", name: "Finland", location: "Europe" }, + CloudRegion { + id: "europe-west1", + name: "Belgium", + location: "Europe", + }, + CloudRegion { + id: "europe-west2", + name: "London", + location: "UK", + }, + CloudRegion { + id: "europe-west3", + name: "Frankfurt", + location: "Germany", + }, + CloudRegion { + id: "europe-west4", + name: "Netherlands", + location: "Europe", + }, + CloudRegion { + id: "europe-north1", + name: "Finland", + location: "Europe", + }, // Asia Pacific - CloudRegion { id: "asia-east1", name: "Taiwan", location: "Asia Pacific" }, - CloudRegion { id: "asia-northeast1", name: "Tokyo", location: "Japan" }, - CloudRegion { id: "asia-southeast1", name: "Singapore", location: "Southeast Asia" }, - CloudRegion { id: "australia-southeast1", name: "Sydney", location: "Australia" }, + CloudRegion { + id: "asia-east1", + name: "Taiwan", + location: "Asia Pacific", + }, + CloudRegion { + id: "asia-northeast1", + name: "Tokyo", + location: "Japan", + }, + CloudRegion { + id: "asia-southeast1", + name: "Singapore", + location: "Southeast Asia", + }, + CloudRegion { + id: "australia-southeast1", + name: "Sydney", + location: "Australia", + }, ]; /// GCP machine types (Compute Engine) pub static GCP_MACHINE_TYPES: &[MachineType] = &[ // E2 Series (Cost-optimized) - MachineType { id: "e2-micro", name: "e2-micro", cpu: "0.25", memory: "1 GB", description: Some("Shared-core") }, - MachineType { id: "e2-small", name: "e2-small", cpu: "0.5", memory: "2 GB", description: Some("Shared-core") }, - MachineType { id: "e2-medium", name: "e2-medium", cpu: "1", memory: "4 GB", description: Some("Shared-core") }, - MachineType { id: "e2-standard-2", name: "e2-standard-2", cpu: "2", memory: "8 GB", description: None }, - MachineType { id: "e2-standard-4", name: "e2-standard-4", cpu: "4", memory: "16 GB", description: None }, - MachineType { id: "e2-standard-8", name: "e2-standard-8", cpu: "8", memory: "32 GB", description: None }, + MachineType { + id: "e2-micro", + name: "e2-micro", + cpu: "0.25", + memory: "1 GB", + description: Some("Shared-core"), + }, + MachineType { + id: "e2-small", + name: "e2-small", + cpu: "0.5", + memory: "2 GB", + description: Some("Shared-core"), + }, + MachineType { + id: "e2-medium", + name: "e2-medium", + cpu: "1", + memory: "4 GB", + description: Some("Shared-core"), + }, + MachineType { + id: "e2-standard-2", + name: "e2-standard-2", + cpu: "2", + memory: "8 GB", + description: None, + }, + MachineType { + id: "e2-standard-4", + name: "e2-standard-4", + cpu: "4", + memory: "16 GB", + description: None, + }, + MachineType { + id: "e2-standard-8", + name: "e2-standard-8", + cpu: "8", + memory: "32 GB", + description: None, + }, // N2 Series (Balanced) - MachineType { id: "n2-standard-2", name: "n2-standard-2", cpu: "2", memory: "8 GB", description: None }, - MachineType { id: "n2-standard-4", name: "n2-standard-4", cpu: "4", memory: "16 GB", description: None }, - MachineType { id: "n2-standard-8", name: "n2-standard-8", cpu: "8", memory: "32 GB", description: None }, + MachineType { + id: "n2-standard-2", + name: "n2-standard-2", + cpu: "2", + memory: "8 GB", + description: None, + }, + MachineType { + id: "n2-standard-4", + name: "n2-standard-4", + cpu: "4", + memory: "16 GB", + description: None, + }, + MachineType { + id: "n2-standard-8", + name: "n2-standard-8", + cpu: "8", + memory: "32 GB", + description: None, + }, ]; // ============================================================================= @@ -324,7 +578,8 @@ pub async fn get_hetzner_regions_dynamic( || error_msg.contains("token") || error_msg.contains("API token") || error_msg.contains("401") - || error_msg.contains("412") // failedPrecondition + || error_msg.contains("412") + // failedPrecondition { HetznerFetchResult::NoCredentials } else { @@ -347,7 +602,10 @@ pub async fn get_hetzner_server_types_dynamic( project_id: &str, preferred_location: Option<&str>, ) -> HetznerFetchResult> { - match client.get_hetzner_server_types(project_id, preferred_location).await { + match client + .get_hetzner_server_types(project_id, preferred_location) + .await + { Ok(server_types) => { HetznerFetchResult::Success(server_types.iter().map(server_type_to_dynamic).collect()) } @@ -359,7 +617,8 @@ pub async fn get_hetzner_server_types_dynamic( || error_msg.contains("token") || error_msg.contains("API token") || error_msg.contains("401") - || error_msg.contains("412") // failedPrecondition + || error_msg.contains("412") + // failedPrecondition { HetznerFetchResult::NoCredentials } else { @@ -383,7 +642,10 @@ pub async fn check_hetzner_availability( location: &str, server_type: &str, ) -> (bool, Option, Vec) { - match client.check_hetzner_availability(project_id, location, server_type).await { + match client + .check_hetzner_availability(project_id, location, server_type) + .await + { Ok(result) => ( result.available, result.reason, @@ -419,10 +681,11 @@ pub async fn get_recommended_server_type( _ => (2, 4.0, false), // Default to standard }; - let server_types = match get_hetzner_server_types_dynamic(client, project_id, preferred_location).await { - HetznerFetchResult::Success(types) => types, - _ => return None, - }; + let server_types = + match get_hetzner_server_types_dynamic(client, project_id, preferred_location).await { + HetznerFetchResult::Success(types) => types, + _ => return None, + }; // Filter by requirements and find cheapest server_types @@ -462,7 +725,10 @@ pub async fn find_best_region( match (a_zone_match, b_zone_match) { (true, false) => std::cmp::Ordering::Less, (false, true) => std::cmp::Ordering::Greater, - _ => b.available_server_types.len().cmp(&a.available_server_types.len()), + _ => b + .available_server_types + .len() + .cmp(&a.available_server_types.len()), } }); @@ -478,10 +744,11 @@ pub async fn find_cheapest_available( project_id: &str, region: &str, ) -> Option { - let server_types = match get_hetzner_server_types_dynamic(client, project_id, Some(region)).await { - HetznerFetchResult::Success(types) => types, - _ => return None, - }; + let server_types = + match get_hetzner_server_types_dynamic(client, project_id, Some(region)).await { + HetznerFetchResult::Success(types) => types, + _ => return None, + }; // Filter to only available types in this region, sort by price server_types @@ -497,7 +764,10 @@ pub async fn find_cheapest_available( /// Format dynamic region for display pub fn format_dynamic_region_display(region: &DynamicCloudRegion) -> String { if region.available_server_types.is_empty() { - format!("{} ({}) - checking availability...", region.name, region.location) + format!( + "{} ({}) - checking availability...", + region.name, region.location + ) } else { format!( "{} ({}) · {} server types available", diff --git a/src/wizard/config_form.rs b/src/wizard/config_form.rs index b45edf2e..a707d9b5 100644 --- a/src/wizard/config_form.rs +++ b/src/wizard/config_form.rs @@ -156,22 +156,19 @@ pub fn collect_config( ); // Show previously selected options - println!( - " {} Dockerfile: {}", - "│".dimmed(), - dockerfile_path.cyan() - ); - println!( - " {} Build context: {}", - "│".dimmed(), - build_context.cyan() - ); + println!(" {} Dockerfile: {}", "│".dimmed(), dockerfile_path.cyan()); + println!(" {} Build context: {}", "│".dimmed(), build_context.cyan()); if let Some(ref r) = region { println!(" {} Region: {}", "│".dimmed(), r.cyan()); } if let Some(ref c) = cpu { if let Some(ref m) = memory { - println!(" {} Resources: {} vCPU / {}", "│".dimmed(), c.cyan(), m.cyan()); + println!( + " {} Resources: {} vCPU / {}", + "│".dimmed(), + c.cyan(), + m.cyan() + ); } } else if let Some(ref m) = machine_type { println!(" {} Machine: {}", "│".dimmed(), m.cyan()); @@ -352,11 +349,7 @@ pub fn collect_env_vars(project_path: &Path) -> Vec { for f in &discovered { let abs = project_path.join(f); let count = count_env_vars_in_file(&abs); - let label = format!( - " {:<30} {} vars", - f.display(), - count.to_string().cyan() - ); + let label = format!(" {:<30} {} vars", f.display(), count.to_string().cyan()); println!(" {}", label); options.push(format!("{:<30} {} vars", f.display(), count)); } @@ -448,10 +441,7 @@ fn collect_env_vars_manually() -> Vec { is_secret, }); - let add_another = match Confirm::new("Add another?") - .with_default(false) - .prompt() - { + let add_another = match Confirm::new("Add another?").with_default(false).prompt() { Ok(v) => v, Err(_) => false, }; @@ -554,7 +544,12 @@ fn collect_env_vars_from_file( ); for s in &secrets { if s.is_secret { - println!(" {} {} {}", "•".dimmed(), s.key.cyan(), "(secret)".dimmed()); + println!( + " {} {} {}", + "•".dimmed(), + s.key.cyan(), + "(secret)".dimmed() + ); } else { println!(" {} {}", "•".dimmed(), s.key.cyan()); } @@ -580,11 +575,7 @@ fn collect_env_vars_from_file( Err(_) => false, }; - if confirm { - secrets - } else { - Vec::new() - } + if confirm { secrets } else { Vec::new() } } /// Check if a key name looks like it should be a secret @@ -631,7 +622,13 @@ fn get_current_branch() -> Option { fn sanitize_service_name(name: &str) -> String { name.to_lowercase() .chars() - .map(|c| if c.is_alphanumeric() || c == '-' { c } else { '-' }) + .map(|c| { + if c.is_alphanumeric() || c == '-' { + c + } else { + '-' + } + }) .collect::() .trim_matches('-') .to_string() diff --git a/src/wizard/dockerfile_selection.rs b/src/wizard/dockerfile_selection.rs index c47961e8..0bd9d2eb 100644 --- a/src/wizard/dockerfile_selection.rs +++ b/src/wizard/dockerfile_selection.rs @@ -144,18 +144,18 @@ fn handle_single_dockerfile( .map(|p| p.to_string_lossy().to_string()) .unwrap_or_else(|_| dockerfile.path.to_string_lossy().to_string()); - println!( - "\n{} Found: {}", - "✓".green(), - relative_path.cyan() - ); + println!("\n{} Found: {}", "✓".green(), relative_path.cyan()); // Show additional info if available if let Some(ref base) = dockerfile.base_image { println!(" {} Base image: {}", "│".dimmed(), base.dimmed()); } if let Some(port) = dockerfile.suggested_port { - println!(" {} Suggested port: {}", "│".dimmed(), port.to_string().dimmed()); + println!( + " {} Suggested port: {}", + "│".dimmed(), + port.to_string().dimmed() + ); } // Proceed to build context selection diff --git a/src/wizard/environment_creation.rs b/src/wizard/environment_creation.rs index 42cfedf1..94c129f3 100644 --- a/src/wizard/environment_creation.rs +++ b/src/wizard/environment_creation.rs @@ -348,13 +348,10 @@ fn select_provider_regions() -> Option> { .position(|r| r.id == default_region) .unwrap_or(0); - let region = match Select::new( - &format!("{} region:", provider_label), - region_labels, - ) - .with_render_config(wizard_render_config()) - .with_starting_cursor(default_idx) - .prompt() + let region = match Select::new(&format!("{} region:", provider_label), region_labels) + .with_render_config(wizard_render_config()) + .with_starting_cursor(default_idx) + .prompt() { Ok(r) => { // Extract region ID from the display string (before first " - ") diff --git a/src/wizard/environment_selection.rs b/src/wizard/environment_selection.rs index 609ef050..e0af890c 100644 --- a/src/wizard/environment_selection.rs +++ b/src/wizard/environment_selection.rs @@ -2,8 +2,8 @@ //! //! Prompts user to select an environment or create a new one. -use crate::platform::api::types::Environment; use crate::platform::api::PlatformApiClient; +use crate::platform::api::types::Environment; use crate::wizard::render::{display_step_header, wizard_render_config}; use colored::Colorize; use inquire::{InquireError, Select}; diff --git a/src/wizard/infrastructure_selection.rs b/src/wizard/infrastructure_selection.rs index 73696493..108dade8 100644 --- a/src/wizard/infrastructure_selection.rs +++ b/src/wizard/infrastructure_selection.rs @@ -10,11 +10,9 @@ use crate::platform::api::client::PlatformApiClient; use crate::platform::api::types::CloudProvider; use crate::wizard::cloud_provider_data::{ - get_default_machine_type, get_default_region, - get_hetzner_regions_dynamic, get_hetzner_server_types_dynamic, - get_machine_types_for_provider, get_regions_for_provider, - DynamicCloudRegion, DynamicMachineType, HetznerFetchResult, - ACA_RESOURCE_PAIRS, CLOUD_RUN_CPU_MEMORY, + ACA_RESOURCE_PAIRS, CLOUD_RUN_CPU_MEMORY, DynamicCloudRegion, DynamicMachineType, + HetznerFetchResult, get_default_machine_type, get_default_region, get_hetzner_regions_dynamic, + get_hetzner_server_types_dynamic, get_machine_types_for_provider, get_regions_for_provider, }; use crate::wizard::render::{display_step_header, wizard_render_config}; use colored::Colorize; @@ -45,7 +43,10 @@ struct DynamicRegionOption { impl fmt::Display for DynamicRegionOption { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let availability = if !self.region.available_server_types.is_empty() { - format!(" · {} types available", self.region.available_server_types.len()) + format!( + " · {} types available", + self.region.available_server_types.len() + ) } else { String::new() }; @@ -66,7 +67,10 @@ struct DynamicMachineTypeOption { impl fmt::Display for DynamicMachineTypeOption { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let specs = format!("{} vCPU · {:.0} GB", self.machine.cores, self.machine.memory_gb); + let specs = format!( + "{} vCPU · {:.0} GB", + self.machine.cores, self.machine.memory_gb + ); let price = if self.machine.price_monthly > 0.0 { format!(" · €{:.2}/mo", self.machine.price_monthly) } else { @@ -178,11 +182,7 @@ async fn select_region( return None; } HetznerFetchResult::ApiError(err) => { - println!( - "\n{} Failed to fetch Hetzner regions: {}", - "✗".red(), - err - ); + println!("\n{} Failed to fetch Hetzner regions: {}", "✗".red(), err); return None; } } @@ -205,10 +205,7 @@ fn select_region_from_dynamic( provider: &CloudProvider, ) -> Option { if regions.is_empty() { - println!( - "\n{} No regions available for this provider.", - "⚠".yellow() - ); + println!("\n{} No regions available for this provider.", "⚠".yellow()); return None; } @@ -249,20 +246,20 @@ fn select_region_from_dynamic( fn select_region_static(provider: &CloudProvider, step_number: u8) -> Option { display_step_header( step_number, - &format!("Select {} Region", match provider { - CloudProvider::Hetzner => "Hetzner", - CloudProvider::Gcp => "GCP", - _ => "Cloud", - }), + &format!( + "Select {} Region", + match provider { + CloudProvider::Hetzner => "Hetzner", + CloudProvider::Gcp => "GCP", + _ => "Cloud", + } + ), "Choose the geographic location for your deployment.", ); let regions = get_regions_for_provider(provider); if regions.is_empty() { - println!( - "\n{} No regions available for this provider.", - "⚠".yellow() - ); + println!("\n{} No regions available for this provider.", "⚠".yellow()); return None; } @@ -322,10 +319,7 @@ async fn select_machine_type( "{}", "─── Machine Type ────────────────────────────".dimmed() ); - println!( - " {}", - "Select the VM size for your deployment.".dimmed() - ); + println!(" {}", "Select the VM size for your deployment.".dimmed()); // For Hetzner: REQUIRE dynamic fetching - no static fallback if *provider == CloudProvider::Hetzner { @@ -373,10 +367,12 @@ async fn select_machine_type( // Non-Hetzner providers: Azure ACA and GCP Cloud Run have custom selection UIs match provider { - CloudProvider::Azure => select_aca_resource_pair() - .map(|(machine, cpu, mem)| (machine, Some(cpu), Some(mem))), - CloudProvider::Gcp => select_cloud_run_resources() - .map(|(machine, cpu, mem)| (machine, Some(cpu), Some(mem))), + CloudProvider::Azure => { + select_aca_resource_pair().map(|(machine, cpu, mem)| (machine, Some(cpu), Some(mem))) + } + CloudProvider::Gcp => { + select_cloud_run_resources().map(|(machine, cpu, mem)| (machine, Some(cpu), Some(mem))) + } _ => select_machine_type_static(provider).map(|m| (m, None, None)), } } @@ -414,7 +410,11 @@ fn select_aca_resource_pair() -> Option<(String, String, String)> { pair.cpu.cyan(), pair.memory.cyan() ); - Some((machine_type_id, pair.cpu.to_string(), pair.memory.to_string())) + Some(( + machine_type_id, + pair.cpu.to_string(), + pair.memory.to_string(), + )) } Err(InquireError::OperationCanceled) => None, Err(InquireError::OperationInterrupted) => None, @@ -428,10 +428,7 @@ fn select_aca_resource_pair() -> Option<(String, String, String)> { fn select_cloud_run_resources() -> Option<(String, String, String)> { let cpu_levels = CLOUD_RUN_CPU_MEMORY; if cpu_levels.is_empty() { - println!( - "\n{} No Cloud Run CPU options available.", - "⚠".yellow() - ); + println!("\n{} No Cloud Run CPU options available.", "⚠".yellow()); return None; } @@ -484,7 +481,11 @@ fn select_cloud_run_resources() -> Option<(String, String, String)> { selected_cpu.cpu.cyan(), selected_memory.cyan() ); - Some((machine_type_id, selected_cpu.cpu.to_string(), selected_memory)) + Some(( + machine_type_id, + selected_cpu.cpu.to_string(), + selected_memory, + )) } Err(InquireError::OperationCanceled) => None, Err(InquireError::OperationInterrupted) => None, @@ -692,7 +693,10 @@ mod tests { cpu: Some("0.5".to_string()), memory: Some("1.0Gi".to_string()), }; - matches!(selected_with_resources, InfrastructureSelectionResult::Selected { .. }); + matches!( + selected_with_resources, + InfrastructureSelectionResult::Selected { .. } + ); let _ = InfrastructureSelectionResult::Back; let _ = InfrastructureSelectionResult::Cancelled; diff --git a/src/wizard/mod.rs b/src/wizard/mod.rs index c28cb5fd..bca06811 100644 --- a/src/wizard/mod.rs +++ b/src/wizard/mod.rs @@ -20,41 +20,50 @@ mod service_endpoints; mod target_selection; pub use cloud_provider_data::{ - get_default_machine_type, get_default_region, get_machine_types_for_provider, - get_regions_for_provider, CloudRegion, MachineType, + CloudRegion, + DynamicCloudRegion, + DynamicMachineType, + HetznerFetchResult, + MachineType, + check_hetzner_availability, + find_best_region, + find_cheapest_available, + get_default_machine_type, + get_default_region, // Dynamic Hetzner availability functions for agent use - get_hetzner_regions_dynamic, get_hetzner_server_types_dynamic, - check_hetzner_availability, get_recommended_server_type, - find_best_region, find_cheapest_available, - DynamicCloudRegion, DynamicMachineType, HetznerFetchResult, + get_hetzner_regions_dynamic, + get_hetzner_server_types_dynamic, + get_machine_types_for_provider, + get_recommended_server_type, + get_regions_for_provider, }; -pub use cluster_selection::{select_cluster, ClusterSelectionResult}; +pub use cluster_selection::{ClusterSelectionResult, select_cluster}; pub use config_form::{ - collect_config, collect_env_vars, discover_env_files, parse_env_file, ConfigFormResult, - EnvFileEntry, + ConfigFormResult, EnvFileEntry, collect_config, collect_env_vars, discover_env_files, + parse_env_file, }; -pub use dockerfile_selection::{select_dockerfile, DockerfileSelectionResult}; -pub use environment_creation::{create_environment_wizard, EnvironmentCreationResult}; -pub use environment_selection::{select_environment, EnvironmentSelectionResult}; +pub use dockerfile_selection::{DockerfileSelectionResult, select_dockerfile}; +pub use environment_creation::{EnvironmentCreationResult, create_environment_wizard}; +pub use environment_selection::{EnvironmentSelectionResult, select_environment}; pub use infrastructure_selection::{ - select_infrastructure, select_infrastructure_sync, InfrastructureSelectionResult, + InfrastructureSelectionResult, select_infrastructure, select_infrastructure_sync, }; -pub use orchestrator::{run_wizard, WizardResult}; +pub use orchestrator::{WizardResult, run_wizard}; pub use provider_selection::{ - get_provider_deployment_statuses, select_provider, ProviderSelectionResult, + ProviderSelectionResult, get_provider_deployment_statuses, select_provider, }; -pub use registry_provisioning::{provision_registry, RegistryProvisioningResult}; -pub use registry_selection::{select_registry, RegistrySelectionResult}; -pub use repository_selection::{select_repository, RepositorySelectionResult}; pub use recommendations::{ - recommend_deployment, DeploymentRecommendation, MachineOption, ProviderOption, - RecommendationAlternatives, RecommendationInput, RegionOption, + DeploymentRecommendation, MachineOption, ProviderOption, RecommendationAlternatives, + RecommendationInput, RegionOption, recommend_deployment, }; +pub use registry_provisioning::{RegistryProvisioningResult, provision_registry}; +pub use registry_selection::{RegistrySelectionResult, select_registry}; pub use render::{count_badge, display_step_header, status_indicator, wizard_render_config}; +pub use repository_selection::{RepositorySelectionResult, select_repository}; pub use service_endpoints::{ + AvailableServiceEndpoint, EndpointSuggestion, MatchConfidence, NetworkEndpointInfo, collect_network_endpoint_env_vars, collect_service_endpoint_env_vars, extract_network_endpoints, filter_endpoints_for_provider, get_available_endpoints, - match_env_vars_to_services, AvailableServiceEndpoint, EndpointSuggestion, MatchConfidence, - NetworkEndpointInfo, + match_env_vars_to_services, }; -pub use target_selection::{select_target, TargetSelectionResult}; +pub use target_selection::{TargetSelectionResult, select_target}; diff --git a/src/wizard/orchestrator.rs b/src/wizard/orchestrator.rs index 9d97ef16..f551259d 100644 --- a/src/wizard/orchestrator.rs +++ b/src/wizard/orchestrator.rs @@ -1,21 +1,20 @@ //! Wizard orchestration - ties all steps together use crate::analyzer::discover_dockerfiles_for_deployment; +use crate::platform::api::PlatformApiClient; use crate::platform::api::types::{ - build_cloud_runner_config_v2, CloudProvider, CloudRunnerConfigInput, - ConnectRepositoryRequest, CreateDeploymentConfigRequest, DeploymentTarget, - ProjectRepository, TriggerDeploymentRequest, WizardDeploymentConfig, + CloudProvider, CloudRunnerConfigInput, ConnectRepositoryRequest, CreateDeploymentConfigRequest, + DeploymentTarget, ProjectRepository, TriggerDeploymentRequest, WizardDeploymentConfig, + build_cloud_runner_config_v2, }; -use crate::platform::api::PlatformApiClient; use crate::wizard::{ - collect_config, collect_env_vars, collect_service_endpoint_env_vars, - filter_endpoints_for_provider, get_available_endpoints, - get_provider_deployment_statuses, provision_registry, - select_cluster, select_dockerfile, select_infrastructure, select_provider, select_registry, - select_repository, select_target, ClusterSelectionResult, ConfigFormResult, - DockerfileSelectionResult, InfrastructureSelectionResult, ProviderSelectionResult, - RegistryProvisioningResult, RegistrySelectionResult, RepositorySelectionResult, - TargetSelectionResult, + ClusterSelectionResult, ConfigFormResult, DockerfileSelectionResult, + InfrastructureSelectionResult, ProviderSelectionResult, RegistryProvisioningResult, + RegistrySelectionResult, RepositorySelectionResult, TargetSelectionResult, collect_config, + collect_env_vars, collect_service_endpoint_env_vars, filter_endpoints_for_provider, + get_available_endpoints, get_provider_deployment_statuses, provision_registry, select_cluster, + select_dockerfile, select_infrastructure, select_provider, select_registry, select_repository, + select_target, }; use colored::Colorize; use inquire::{Confirm, InquireError}; @@ -78,10 +77,14 @@ pub async fn run_wizard( println!("{} Connecting repository...", "→".cyan()); // Extract owner from full_name if not provided - let owner = available - .owner - .clone() - .unwrap_or_else(|| available.full_name.split('/').next().unwrap_or("").to_string()); + let owner = available.owner.clone().unwrap_or_else(|| { + available + .full_name + .split('/') + .next() + .unwrap_or("") + .to_string() + }); let connect_request = ConnectRepositoryRequest { project_id: project_id.to_string(), @@ -90,7 +93,10 @@ pub async fn run_wizard( repository_full_name: available.full_name.clone(), repository_owner: owner.clone(), repository_private: available.private, - default_branch: available.default_branch.clone().or(Some("main".to_string())), + default_branch: available + .default_branch + .clone() + .or(Some("main".to_string())), connection_type: Some("app".to_string()), github_installation_id: available.installation_id, repository_type: Some("application".to_string()), @@ -123,7 +129,10 @@ pub async fn run_wizard( } } } - RepositorySelectionResult::NeedsGitHubApp { installation_url, org_name } => { + RepositorySelectionResult::NeedsGitHubApp { + installation_url, + org_name, + } => { println!( "\n{} Please install the Syncable GitHub App for organization '{}' first.", "⚠".yellow(), @@ -180,7 +189,8 @@ pub async fn run_wizard( }; // Step 3: Infrastructure selection for Cloud Runner OR Cluster selection for K8s - let (cluster_id, region, machine_type, cpu, memory) = if target == DeploymentTarget::CloudRunner { + let (cluster_id, region, machine_type, cpu, memory) = if target == DeploymentTarget::CloudRunner + { // Cloud Runner: Select region and machine type // Pass client and project_id for dynamic Hetzner availability fetching match select_infrastructure(&provider, 3, Some(client), Some(project_id)).await { @@ -192,7 +202,8 @@ pub async fn run_wizard( } => (None, Some(region), Some(machine_type), cpu, memory), InfrastructureSelectionResult::Back => { // Go back (restart wizard for simplicity) - return Box::pin(run_wizard(client, project_id, environment_id, project_path)).await; + return Box::pin(run_wizard(client, project_id, environment_id, project_path)) + .await; } InfrastructureSelectionResult::Cancelled => return WizardResult::Cancelled, } @@ -266,7 +277,8 @@ pub async fn run_wizard( } RegistrySelectionResult::Back => { // Go back (restart wizard for simplicity) - return Box::pin(run_wizard(client, project_id, environment_id, project_path)).await; + return Box::pin(run_wizard(client, project_id, environment_id, project_path)) + .await; } RegistrySelectionResult::Cancelled => return WizardResult::Cancelled, } @@ -362,8 +374,7 @@ pub async fn run_wizard( .filter(|ep| ep.service_name != service_being_deployed) .collect(); // Only show private endpoints from the same cloud provider - let available_endpoints = - filter_endpoints_for_provider(available_endpoints, provider.as_str()); + let available_endpoints = filter_endpoints_for_provider(available_endpoints, provider.as_str()); if !available_endpoints.is_empty() { let endpoint_vars = collect_service_endpoint_env_vars(&available_endpoints); @@ -429,7 +440,10 @@ pub async fn run_wizard( // Fetch provider credential for GCP project ID / Azure subscription ID let (gcp_project_id, subscription_id) = match provider { CloudProvider::Gcp | CloudProvider::Azure => { - match client.check_provider_connection(&provider, project_id).await { + match client + .check_provider_connection(&provider, project_id) + .await + { Ok(Some(cred)) => match provider { CloudProvider::Gcp => (cred.provider_account_id, None), CloudProvider::Azure => (None, cred.provider_account_id), @@ -470,7 +484,10 @@ pub async fn run_wizard( log::debug!(" serviceName: {}", deploy_request.service_name); log::debug!(" environmentId: {}", deploy_request.environment_id); log::debug!(" repositoryId: {}", deploy_request.repository_id); - log::debug!(" repositoryFullName: {}", deploy_request.repository_full_name); + log::debug!( + " repositoryFullName: {}", + deploy_request.repository_full_name + ); log::debug!(" dockerfilePath: {:?}", deploy_request.dockerfile_path); log::debug!(" buildContext: {:?}", deploy_request.build_context); log::debug!(" targetType: {}", deploy_request.target_type); @@ -527,16 +544,16 @@ pub async fn run_wizard( "{}", "═══════════════════════════════════════════════════════════════".bright_green() ); - println!( - "{} Deployment started!", - "✓".bright_green().bold() - ); + println!("{} Deployment started!", "✓".bright_green().bold()); println!( "{}", "═══════════════════════════════════════════════════════════════".bright_green() ); println!(); - println!(" Service: {}", config.service_name.as_deref().unwrap_or("").cyan()); + println!( + " Service: {}", + config.service_name.as_deref().unwrap_or("").cyan() + ); println!(" Task ID: {}", response.backstage_task_id.dimmed()); println!(" Status: {}", response.status.yellow()); println!(); diff --git a/src/wizard/provider_selection.rs b/src/wizard/provider_selection.rs index 0c1bad49..776ba397 100644 --- a/src/wizard/provider_selection.rs +++ b/src/wizard/provider_selection.rs @@ -1,11 +1,11 @@ //! Provider selection step for deployment wizard use crate::platform::api::{ + PlatformApiClient, types::{ CloudProvider, ClusterStatus, ClusterSummary, ProviderDeploymentStatus, RegistryStatus, RegistrySummary, }, - PlatformApiClient, }; use crate::wizard::render::{display_step_header, status_indicator, wizard_render_config}; use colored::Colorize; @@ -92,8 +92,11 @@ pub async fn get_provider_deployment_statuses( let is_connected = connected_providers.contains(provider.as_str()); // Cloud Runner available for GCP, Hetzner, and Azure when connected - let cloud_runner_available = - is_connected && matches!(provider, CloudProvider::Gcp | CloudProvider::Hetzner | CloudProvider::Azure); + let cloud_runner_available = is_connected + && matches!( + provider, + CloudProvider::Gcp | CloudProvider::Hetzner | CloudProvider::Azure + ); let summary = build_status_summary(&clusters, ®istries, cloud_runner_available); @@ -177,7 +180,12 @@ pub fn select_provider(statuses: &[ProviderDeploymentStatus]) -> ProviderSelecti if s.is_connected { format!("{} {} {}", indicator, name, s.summary.dimmed()) } else { - format!("{} {} {}", indicator, name.dimmed(), "Not connected".dimmed()) + format!( + "{} {} {}", + indicator, + name.dimmed(), + "Not connected".dimmed() + ) } } }) @@ -251,11 +259,7 @@ pub fn select_provider(statuses: &[ProviderDeploymentStatus]) -> ProviderSelecti return ProviderSelectionResult::Cancelled; } - println!( - "\n{} Selected: {:?}", - "✓".green(), - selected_status.provider - ); + println!("\n{} Selected: {:?}", "✓".green(), selected_status.provider); ProviderSelectionResult::Selected(selected_status.provider.clone()) } Err(InquireError::OperationCanceled) | Err(InquireError::OperationInterrupted) => { diff --git a/src/wizard/recommendations.rs b/src/wizard/recommendations.rs index e44422b5..892bcc61 100644 --- a/src/wizard/recommendations.rs +++ b/src/wizard/recommendations.rs @@ -116,7 +116,8 @@ pub fn recommend_deployment(input: RecommendationInput) -> DeploymentRecommendat let health_check_path = select_health_endpoint(&input.analysis); // 7. Calculate confidence - let confidence = calculate_confidence(&input.analysis, &port_source, health_check_path.is_some()); + let confidence = + calculate_confidence(&input.analysis, &port_source, health_check_path.is_some()); // 8. Build alternatives let alternatives = build_alternatives(&provider, &input.available_providers); @@ -171,11 +172,7 @@ fn select_provider(input: &RecommendationInput) -> (CloudProvider, String) { let also_available = if connected.len() > 1 { format!( ". Also connected: {}", - connected - .iter() - .copied() - .collect::>() - .join(", ") + connected.iter().copied().collect::>().join(", ") ) } else { String::new() @@ -229,7 +226,8 @@ fn select_target(input: &RecommendationInput) -> (DeploymentTarget, String) { if infra.has_kubernetes && input.has_existing_k8s { return ( DeploymentTarget::Kubernetes, - "Kubernetes recommended: Existing K8s manifests detected and clusters available".to_string(), + "Kubernetes recommended: Existing K8s manifests detected and clusters available" + .to_string(), ); } } @@ -259,33 +257,59 @@ fn select_machine_type(analysis: &ProjectAnalysis, provider: &CloudProvider) -> let (machine_type, reasoning) = match framework_info.memory_requirement { MemoryRequirement::Low => ( "cx23".to_string(), - format!("cx23 (2 vCPU, 4GB) recommended: {} services are memory-efficient", framework_info.name), + format!( + "cx23 (2 vCPU, 4GB) recommended: {} services are memory-efficient", + framework_info.name + ), ), MemoryRequirement::Medium => ( "cx33".to_string(), - format!("cx33 (4 vCPU, 8GB) recommended: {} may benefit from more resources", framework_info.name), + format!( + "cx33 (4 vCPU, 8GB) recommended: {} may benefit from more resources", + framework_info.name + ), ), MemoryRequirement::High => ( "cx43".to_string(), - format!("cx43 (8 vCPU, 16GB) recommended: {} requires significant memory (JVM, ML, etc.)", framework_info.name), + format!( + "cx43 (8 vCPU, 16GB) recommended: {} requires significant memory (JVM, ML, etc.)", + framework_info.name + ), ), }; - MachineTypeResult { machine_type, reasoning, cpu: None, memory: None } + MachineTypeResult { + machine_type, + reasoning, + cpu: None, + memory: None, + } } CloudProvider::Gcp => { // Use Cloud Run CPU/memory instead of Compute Engine machine types let (cpu, mem, reasoning) = match framework_info.memory_requirement { MemoryRequirement::Low => ( - "1", "512Mi", - format!("Cloud Run 1 vCPU / 512Mi recommended: {} services are lightweight", framework_info.name), + "1", + "512Mi", + format!( + "Cloud Run 1 vCPU / 512Mi recommended: {} services are lightweight", + framework_info.name + ), ), MemoryRequirement::Medium => ( - "2", "2Gi", - format!("Cloud Run 2 vCPU / 2Gi recommended: {} may need moderate resources", framework_info.name), + "2", + "2Gi", + format!( + "Cloud Run 2 vCPU / 2Gi recommended: {} may need moderate resources", + framework_info.name + ), ), MemoryRequirement::High => ( - "4", "8Gi", - format!("Cloud Run 4 vCPU / 8Gi recommended: {} requires significant memory", framework_info.name), + "4", + "8Gi", + format!( + "Cloud Run 4 vCPU / 8Gi recommended: {} requires significant memory", + framework_info.name + ), ), }; MachineTypeResult { @@ -299,16 +323,28 @@ fn select_machine_type(analysis: &ProjectAnalysis, provider: &CloudProvider) -> // Use Azure Container Apps resource pairs let (cpu, mem, reasoning) = match framework_info.memory_requirement { MemoryRequirement::Low => ( - "0.5", "1.0Gi", - format!("ACA 0.5 vCPU / 1 GB recommended: {} services are lightweight", framework_info.name), + "0.5", + "1.0Gi", + format!( + "ACA 0.5 vCPU / 1 GB recommended: {} services are lightweight", + framework_info.name + ), ), MemoryRequirement::Medium => ( - "1.0", "2.0Gi", - format!("ACA 1 vCPU / 2 GB recommended: {} may need moderate resources", framework_info.name), + "1.0", + "2.0Gi", + format!( + "ACA 1 vCPU / 2 GB recommended: {} may need moderate resources", + framework_info.name + ), ), MemoryRequirement::High => ( - "2.0", "4.0Gi", - format!("ACA 2 vCPU / 4 GB recommended: {} requires significant memory", framework_info.name), + "2.0", + "4.0Gi", + format!( + "ACA 2 vCPU / 4 GB recommended: {} requires significant memory", + framework_info.name + ), ), }; MachineTypeResult { @@ -352,8 +388,11 @@ fn get_framework_resource_hint(analysis: &ProjectAnalysis) -> FrameworkResourceH let name_lower = tech.name.to_lowercase(); // JVM frameworks - high memory - if name_lower.contains("spring") || name_lower.contains("quarkus") - || name_lower.contains("micronaut") || name_lower.contains("ktor") { + if name_lower.contains("spring") + || name_lower.contains("quarkus") + || name_lower.contains("micronaut") + || name_lower.contains("ktor") + { return FrameworkResourceHint { name: tech.name.clone(), memory_requirement: MemoryRequirement::High, @@ -361,10 +400,14 @@ fn get_framework_resource_hint(analysis: &ProjectAnalysis) -> FrameworkResourceH } // Go, Rust frameworks - low memory - if name_lower.contains("gin") || name_lower.contains("echo") - || name_lower.contains("fiber") || name_lower.contains("chi") - || name_lower.contains("actix") || name_lower.contains("axum") - || name_lower.contains("rocket") { + if name_lower.contains("gin") + || name_lower.contains("echo") + || name_lower.contains("fiber") + || name_lower.contains("chi") + || name_lower.contains("actix") + || name_lower.contains("axum") + || name_lower.contains("rocket") + { return FrameworkResourceHint { name: tech.name.clone(), memory_requirement: MemoryRequirement::Low, @@ -372,9 +415,13 @@ fn get_framework_resource_hint(analysis: &ProjectAnalysis) -> FrameworkResourceH } // Node.js frameworks - low memory - if name_lower.contains("express") || name_lower.contains("fastify") - || name_lower.contains("koa") || name_lower.contains("hono") - || name_lower.contains("elysia") || name_lower.contains("nest") { + if name_lower.contains("express") + || name_lower.contains("fastify") + || name_lower.contains("koa") + || name_lower.contains("hono") + || name_lower.contains("elysia") + || name_lower.contains("nest") + { return FrameworkResourceHint { name: tech.name.clone(), memory_requirement: MemoryRequirement::Low, @@ -382,8 +429,10 @@ fn get_framework_resource_hint(analysis: &ProjectAnalysis) -> FrameworkResourceH } // Python frameworks - medium memory - if name_lower.contains("fastapi") || name_lower.contains("flask") - || name_lower.contains("django") { + if name_lower.contains("fastapi") + || name_lower.contains("flask") + || name_lower.contains("django") + { return FrameworkResourceHint { name: tech.name.clone(), memory_requirement: MemoryRequirement::Medium, @@ -396,7 +445,10 @@ fn get_framework_resource_hint(analysis: &ProjectAnalysis) -> FrameworkResourceH for lang in &analysis.languages { let name_lower = lang.name.to_lowercase(); - if name_lower.contains("java") || name_lower.contains("kotlin") || name_lower.contains("scala") { + if name_lower.contains("java") + || name_lower.contains("kotlin") + || name_lower.contains("scala") + { return FrameworkResourceHint { name: lang.name.clone(), memory_requirement: MemoryRequirement::High, @@ -448,9 +500,18 @@ fn select_region(provider: &CloudProvider, user_hint: Option<&str>) -> (String, let default_region = get_default_region(provider); let reasoning = match provider { - CloudProvider::Hetzner => format!("{} (Nuremberg) selected: Default EU region, low latency for European users", default_region), - CloudProvider::Gcp => format!("{} (Iowa) selected: Default US region, good general-purpose choice", default_region), - CloudProvider::Azure => format!("{} (Virginia) selected: Default US region, broad service availability", default_region), + CloudProvider::Hetzner => format!( + "{} (Nuremberg) selected: Default EU region, low latency for European users", + default_region + ), + CloudProvider::Gcp => format!( + "{} (Iowa) selected: Default US region, good general-purpose choice", + default_region + ), + CloudProvider::Azure => format!( + "{} (Virginia) selected: Default US region, broad service availability", + default_region + ), _ => format!("{} selected: Default region for provider", default_region), }; @@ -474,7 +535,9 @@ fn select_port(analysis: &ProjectAnalysis) -> (u16, String) { }; // Find the highest priority port - let best_port = analysis.ports.iter() + let best_port = analysis + .ports + .iter() .max_by_key(|p| port_priority(&p.source)); if let Some(port) = best_port { @@ -484,11 +547,22 @@ fn select_port(analysis: &ProjectAnalysis) -> (u16, String) { Some(PortSource::ConfigFile) => "Detected from configuration file", Some(PortSource::FrameworkDefault) => { // Try to get framework name - let framework_name = analysis.technologies.iter() - .find(|t| matches!(t.category, TechnologyCategory::BackendFramework | TechnologyCategory::MetaFramework)) + let framework_name = analysis + .technologies + .iter() + .find(|t| { + matches!( + t.category, + TechnologyCategory::BackendFramework + | TechnologyCategory::MetaFramework + ) + }) .map(|t| t.name.as_str()) .unwrap_or("framework"); - return (port.number, format!("Framework default ({}: {})", framework_name, port.number)); + return ( + port.number, + format!("Framework default ({}: {})", framework_name, port.number), + ); } Some(PortSource::Dockerfile) => "Detected from Dockerfile EXPOSE", Some(PortSource::DockerCompose) => "Detected from docker-compose.yml", @@ -499,19 +573,32 @@ fn select_port(analysis: &ProjectAnalysis) -> (u16, String) { } // Fallback to 8080 - (8080, "Default port 8080: No port detected in project".to_string()) + ( + 8080, + "Default port 8080: No port detected in project".to_string(), + ) } /// Select the best health endpoint from analysis fn select_health_endpoint(analysis: &ProjectAnalysis) -> Option { // Find highest confidence health endpoint - analysis.health_endpoints.iter() - .max_by(|a, b| a.confidence.partial_cmp(&b.confidence).unwrap_or(std::cmp::Ordering::Equal)) + analysis + .health_endpoints + .iter() + .max_by(|a, b| { + a.confidence + .partial_cmp(&b.confidence) + .unwrap_or(std::cmp::Ordering::Equal) + }) .map(|e| e.path.clone()) } /// Calculate overall confidence in the recommendation -fn calculate_confidence(analysis: &ProjectAnalysis, port_source: &str, has_health_endpoint: bool) -> f32 { +fn calculate_confidence( + analysis: &ProjectAnalysis, + port_source: &str, + has_health_endpoint: bool, +) -> f32 { let mut confidence: f32 = 0.5; // Base confidence // Boost for detected port from reliable source @@ -522,8 +609,12 @@ fn calculate_confidence(analysis: &ProjectAnalysis, port_source: &str, has_healt } // Boost for detected framework - let has_framework = analysis.technologies.iter() - .any(|t| matches!(t.category, TechnologyCategory::BackendFramework | TechnologyCategory::MetaFramework)); + let has_framework = analysis.technologies.iter().any(|t| { + matches!( + t.category, + TechnologyCategory::BackendFramework | TechnologyCategory::MetaFramework + ) + }); if has_framework { confidence += 0.15; } @@ -542,7 +633,10 @@ fn calculate_confidence(analysis: &ProjectAnalysis, port_source: &str, has_healt } /// Build alternative options for user customization -fn build_alternatives(selected_provider: &CloudProvider, available_providers: &[CloudProvider]) -> RecommendationAlternatives { +fn build_alternatives( + selected_provider: &CloudProvider, + available_providers: &[CloudProvider], +) -> RecommendationAlternatives { // Build provider options let providers: Vec = CloudProvider::all() .iter() @@ -592,8 +686,8 @@ fn build_alternatives(selected_provider: &CloudProvider, available_providers: &[ mod tests { use super::*; use crate::analyzer::{ - AnalysisMetadata, ArchitectureType, DetectedLanguage, DetectedTechnology, - HealthEndpoint, InfrastructurePresence, Port, ProjectType, TechnologyCategory, + AnalysisMetadata, ArchitectureType, DetectedLanguage, DetectedTechnology, HealthEndpoint, + InfrastructurePresence, Port, ProjectType, TechnologyCategory, }; use std::collections::HashMap; use std::path::PathBuf; @@ -665,7 +759,11 @@ mod tests { let rec = recommend_deployment(input); // Express should get a small machine - assert!(rec.machine_type == "cx23" || rec.machine_type.contains("1-cpu") || rec.machine_type == "e2-small"); + assert!( + rec.machine_type == "cx23" + || rec.machine_type.contains("1-cpu") + || rec.machine_type == "e2-small" + ); assert_eq!(rec.port, 3000); assert!(rec.machine_reasoning.contains("Express")); } @@ -769,7 +867,9 @@ mod tests { let rec = recommend_deployment(input); assert_eq!(rec.port, 8080); - assert!(rec.port_source.contains("No port detected") || rec.port_source.contains("Default")); + assert!( + rec.port_source.contains("No port detected") || rec.port_source.contains("Default") + ); } #[test] @@ -853,6 +953,9 @@ mod tests { let rec = recommend_deployment(input); // Go services should get small machine assert_eq!(rec.machine_type, "cx23"); - assert!(rec.machine_reasoning.contains("memory-efficient") || rec.machine_reasoning.contains("Gin")); + assert!( + rec.machine_reasoning.contains("memory-efficient") + || rec.machine_reasoning.contains("Gin") + ); } } diff --git a/src/wizard/registry_provisioning.rs b/src/wizard/registry_provisioning.rs index 8313126c..a2955d83 100644 --- a/src/wizard/registry_provisioning.rs +++ b/src/wizard/registry_provisioning.rs @@ -1,9 +1,9 @@ //! Registry provisioning step for deployment wizard +use crate::platform::api::PlatformApiClient; use crate::platform::api::types::{ CloudProvider, CreateRegistryRequest, RegistrySummary, RegistryTaskState, }; -use crate::platform::api::PlatformApiClient; use crate::wizard::render::{display_step_header, wizard_render_config}; use colored::Colorize; use inquire::{InquireError, Text}; @@ -102,10 +102,7 @@ pub async fn provision_registry( let progress = status.progress.unwrap_or(0); if progress > last_progress { let bar = progress_bar(progress); - let message = status - .overall_message - .as_deref() - .unwrap_or("Processing..."); + let message = status.overall_message.as_deref().unwrap_or("Processing..."); print!( "\r {} {} {}", bar, @@ -163,7 +160,13 @@ fn progress_bar(percent: u8) -> String { fn sanitize_registry_name(name: &str) -> String { name.to_lowercase() .chars() - .map(|c| if c.is_alphanumeric() || c == '-' { c } else { '-' }) + .map(|c| { + if c.is_alphanumeric() || c == '-' { + c + } else { + '-' + } + }) .collect::() .trim_matches('-') .to_string() diff --git a/src/wizard/registry_selection.rs b/src/wizard/registry_selection.rs index 6bad1a32..ebe412ba 100644 --- a/src/wizard/registry_selection.rs +++ b/src/wizard/registry_selection.rs @@ -27,7 +27,8 @@ pub fn select_registry(registries: &[RegistrySummary]) -> RegistrySelectionResul ); // Filter to ready registries - let ready_registries: Vec<&RegistrySummary> = registries.iter().filter(|r| r.is_ready).collect(); + let ready_registries: Vec<&RegistrySummary> = + registries.iter().filter(|r| r.is_ready).collect(); // Build options let mut options: Vec = ready_registries @@ -61,7 +62,10 @@ pub fn select_registry(registries: &[RegistrySummary]) -> RegistrySelectionResul } if answer.contains("Provision new") { - println!("\n{} Will provision new registry during deployment", "→".cyan()); + println!( + "\n{} Will provision new registry during deployment", + "→".cyan() + ); return RegistrySelectionResult::ProvisionNew; } diff --git a/src/wizard/render.rs b/src/wizard/render.rs index b65fcdba..1c6a9728 100644 --- a/src/wizard/render.rs +++ b/src/wizard/render.rs @@ -26,25 +26,19 @@ pub fn display_step_header(step_number: u8, step_name: &str, description: &str) "{}{}{}", "┌".bright_cyan(), header.bright_cyan(), - "─".repeat(inner_width.saturating_sub(header.len())).bright_cyan() + "─" + .repeat(inner_width.saturating_sub(header.len())) + .bright_cyan() ); // Description let desc_lines = textwrap::wrap(description, inner_width - 2); for line in &desc_lines { - println!( - "{} {}", - "│".dimmed(), - line.white() - ); + println!("{} {}", "│".dimmed(), line.white()); } // Bottom border - println!( - "{}{}", - "└".dimmed(), - "─".repeat(box_width - 1).dimmed() - ); + println!("{}{}", "└".dimmed(), "─".repeat(box_width - 1).dimmed()); println!(); } diff --git a/src/wizard/repository_selection.rs b/src/wizard/repository_selection.rs index f1dfc199..c14a4c94 100644 --- a/src/wizard/repository_selection.rs +++ b/src/wizard/repository_selection.rs @@ -2,8 +2,8 @@ //! //! Detects the repository from local git remote or asks user to select. -use crate::platform::api::types::{AvailableRepository, ProjectRepository}; use crate::platform::api::PlatformApiClient; +use crate::platform::api::types::{AvailableRepository, ProjectRepository}; use crate::wizard::render::{display_step_header, wizard_render_config}; use colored::Colorize; use inquire::{Confirm, InquireError, Select}; @@ -88,7 +88,13 @@ fn parse_repo_from_url(url: &str) -> Option { // HTTPS format: https://github.com/owner/repo.git if url.starts_with("https://") || url.starts_with("http://") { - if let Some(path) = url.split('/').skip(3).collect::>().join("/").strip_suffix(".git") { + if let Some(path) = url + .split('/') + .skip(3) + .collect::>() + .join("/") + .strip_suffix(".git") + { return Some(path.to_string()); } // Without .git suffix @@ -221,7 +227,9 @@ async fn prompt_github_app_install( org_name: org_name.to_string(), } } - Err(e) => RepositorySelectionResult::Error(format!("Failed to get installation URL: {}", e)), + Err(e) => { + RepositorySelectionResult::Error(format!("Failed to get installation URL: {}", e)) + } } } @@ -251,10 +259,7 @@ pub async fn select_repository( // If no installations, prompt to install GitHub App if installations.is_empty() { - println!( - "\n{} No GitHub App installations found.", - "⚠".yellow() - ); + println!("\n{} No GitHub App installations found.", "⚠".yellow()); match client.get_github_installation_url().await { Ok(response) => { println!("Install the Syncable GitHub App to connect repositories."); @@ -314,7 +319,8 @@ pub async fn select_repository( let connected_ids = available_response.connected_repositories; // Try to auto-detect from git remote - let detected_repo_name = detect_git_remote(project_path).and_then(|url| parse_repo_from_url(&url)); + let detected_repo_name = + detect_git_remote(project_path).and_then(|url| parse_repo_from_url(&url)); if let Some(ref local_repo_name) = detected_repo_name { // Check if already connected to this project @@ -364,10 +370,7 @@ pub async fn select_repository( // No local repo detected or couldn't match - show selection UI if connected_repos.is_empty() && available_repos.is_empty() { - println!( - "\n{} No repositories available.", - "⚠".yellow() - ); + println!("\n{} No repositories available.", "⚠".yellow()); println!( "{}", "Connect a repository using the GitHub App installation.".dimmed() @@ -423,7 +426,9 @@ pub async fn select_repository( match selection { Ok(selected_name) => { - if let Some(available) = available_repos.iter().find(|r| r.full_name == selected_name) + if let Some(available) = available_repos + .iter() + .find(|r| r.full_name == selected_name) { return RepositorySelectionResult::ConnectNew(available.clone()); } diff --git a/src/wizard/service_endpoints.rs b/src/wizard/service_endpoints.rs index 16a3b1e5..9cd1eda8 100644 --- a/src/wizard/service_endpoints.rs +++ b/src/wizard/service_endpoints.rs @@ -253,9 +253,9 @@ pub fn match_hint_to_service(hint: &str, service_name: &str) -> Option `"SENTIMENT_ANALYSIS_URL"` pub fn suggest_env_var_name(service_name: &str) -> String { - let base = service_name - .to_uppercase() - .replace('-', "_"); + let base = service_name.to_uppercase().replace('-', "_"); format!("{}_URL", base) } @@ -352,7 +350,11 @@ pub fn collect_service_endpoint_env_vars( endpoints.len().to_string().cyan() ); for ep in endpoints { - let access_label = if ep.is_private { " (private network)" } else { "" }; + let access_label = if ep.is_private { + " (private network)" + } else { + "" + }; println!( " {} {:<30} {}{}", "●".green(), @@ -428,7 +430,11 @@ pub fn collect_service_endpoint_env_vars( continue; } - let private_note = if ep.is_private { " (private network)" } else { "" }; + let private_note = if ep.is_private { + " (private network)" + } else { + "" + }; println!( " {} {} = {}{}", "✓".green(), @@ -502,10 +508,7 @@ pub fn extract_network_endpoints( } // Azure-specific if let Some(ref cae_name) = n.container_app_environment_name { - details.push(( - "AZURE_CONTAINER_APP_ENV_NAME".to_string(), - cae_name.clone(), - )); + details.push(("AZURE_CONTAINER_APP_ENV_NAME".to_string(), cae_name.clone())); } if let Some(ref domain) = n.default_domain { details.push(("NETWORK_DEFAULT_DOMAIN".to_string(), domain.clone())); @@ -630,10 +633,7 @@ mod tests { extract_service_hint("SENTIMENT_SERVICE_URL"), Some("sentiment".to_string()) ); - assert_eq!( - extract_service_hint("API_BASE"), - Some("api".to_string()) - ); + assert_eq!(extract_service_hint("API_BASE"), Some("api".to_string())); assert_eq!(extract_service_hint("NODE_ENV"), None); assert_eq!( extract_service_hint("CONTACTS_API_URL"), @@ -727,7 +727,7 @@ mod tests { let env_vars = vec![ "SENTIMENT_SERVICE_URL".to_string(), "CONTACTS_API_URL".to_string(), - "NODE_ENV".to_string(), // not a URL var + "NODE_ENV".to_string(), // not a URL var "DATABASE_URL".to_string(), // no matching service ]; @@ -749,9 +749,7 @@ mod tests { assert_eq!(cont.unwrap().service.service_name, "contact-intelligence"); // NODE_ENV should not be in suggestions (not a URL var) - assert!(suggestions - .iter() - .all(|s| s.env_var_name != "NODE_ENV")); + assert!(suggestions.iter().all(|s| s.env_var_name != "NODE_ENV")); } #[test] @@ -1048,14 +1046,18 @@ mod tests { assert_eq!(endpoints[0].network_id, "n1"); assert_eq!(endpoints[0].cloud_provider, "hetzner"); assert_eq!(endpoints[0].connection_details.len(), 2); - assert!(endpoints[0] - .connection_details - .iter() - .any(|(k, v)| k == "NETWORK_VPC_ID" && v == "vpc-123")); - assert!(endpoints[0] - .connection_details - .iter() - .any(|(k, v)| k == "NETWORK_SUBNET_ID" && v == "subnet-456")); + assert!( + endpoints[0] + .connection_details + .iter() + .any(|(k, v)| k == "NETWORK_VPC_ID" && v == "vpc-123") + ); + assert!( + endpoints[0] + .connection_details + .iter() + .any(|(k, v)| k == "NETWORK_SUBNET_ID" && v == "subnet-456") + ); } #[test] @@ -1070,19 +1072,24 @@ mod tests { let endpoints = extract_network_endpoints(&networks, "azure", Some("env-1")); assert_eq!(endpoints.len(), 1); - assert!(endpoints[0] - .connection_details - .iter() - .any(|(k, v)| k == "AZURE_CONTAINER_APP_ENV_NAME" && v == "my-cae")); - assert!(endpoints[0] - .connection_details - .iter() - .any(|(k, v)| k == "NETWORK_DEFAULT_DOMAIN" - && v == "my-app.azurecontainerapps.io")); - assert!(endpoints[0] - .connection_details - .iter() - .any(|(k, v)| k == "AZURE_RESOURCE_GROUP" && v == "rg-prod")); + assert!( + endpoints[0] + .connection_details + .iter() + .any(|(k, v)| k == "AZURE_CONTAINER_APP_ENV_NAME" && v == "my-cae") + ); + assert!( + endpoints[0] + .connection_details + .iter() + .any(|(k, v)| k == "NETWORK_DEFAULT_DOMAIN" && v == "my-app.azurecontainerapps.io") + ); + assert!( + endpoints[0] + .connection_details + .iter() + .any(|(k, v)| k == "AZURE_RESOURCE_GROUP" && v == "rg-prod") + ); } #[test] @@ -1097,29 +1104,38 @@ mod tests { let endpoints = extract_network_endpoints(&networks, "hetzner", Some("env-1")); // Shared network (no environment_id) should be included assert_eq!(endpoints.len(), 1); - assert!(endpoints[0] - .connection_details - .iter() - .any(|(k, v)| k == "NETWORK_VPC_ID" && v == "hetz-vpc-1")); - assert!(endpoints[0] - .connection_details - .iter() - .any(|(k, v)| k == "NETWORK_SUBNET_ID" && v == "hetz-sub-1")); + assert!( + endpoints[0] + .connection_details + .iter() + .any(|(k, v)| k == "NETWORK_VPC_ID" && v == "hetz-vpc-1") + ); + assert!( + endpoints[0] + .connection_details + .iter() + .any(|(k, v)| k == "NETWORK_SUBNET_ID" && v == "hetz-sub-1") + ); } #[test] fn test_extract_network_endpoints_gcp() { let networks = vec![{ let mut n = make_network("n1", "gcp", "us-central1", "ready", Some("env-1")); - n.vpc_connector_name = Some("projects/my-proj/locations/us-central1/connectors/vpc-conn".to_string()); + n.vpc_connector_name = + Some("projects/my-proj/locations/us-central1/connectors/vpc-conn".to_string()); n }]; let endpoints = extract_network_endpoints(&networks, "gcp", Some("env-1")); assert_eq!(endpoints.len(), 1); - assert!(endpoints[0].connection_details.iter().any(|(k, v)| k - == "GCP_VPC_CONNECTOR" - && v == "projects/my-proj/locations/us-central1/connectors/vpc-conn")); + assert!( + endpoints[0] + .connection_details + .iter() + .any(|(k, v)| k == "GCP_VPC_CONNECTOR" + && v == "projects/my-proj/locations/us-central1/connectors/vpc-conn") + ); } #[test] diff --git a/src/wizard/target_selection.rs b/src/wizard/target_selection.rs index 8bbc9c1a..2d1ce3e6 100644 --- a/src/wizard/target_selection.rs +++ b/src/wizard/target_selection.rs @@ -37,24 +37,26 @@ pub fn select_target(provider_status: &ProviderDeploymentStatus) -> TargetSelect // Build options with descriptions let mut options: Vec = available_targets .iter() - .map(|t| { - match t { - DeploymentTarget::CloudRunner => { - format!( - "{} {}", - "Cloud Runner".cyan(), - "Fully managed, auto-scaling containers".dimmed() - ) - } - DeploymentTarget::Kubernetes => { - let cluster_count = provider_status.clusters.iter().filter(|c| c.is_healthy).count(); - format!( - "{} {} cluster{} available", - "Kubernetes".cyan(), - cluster_count, - if cluster_count == 1 { "" } else { "s" } - ) - } + .map(|t| match t { + DeploymentTarget::CloudRunner => { + format!( + "{} {}", + "Cloud Runner".cyan(), + "Fully managed, auto-scaling containers".dimmed() + ) + } + DeploymentTarget::Kubernetes => { + let cluster_count = provider_status + .clusters + .iter() + .filter(|c| c.is_healthy) + .count(); + format!( + "{} {} cluster{} available", + "Kubernetes".cyan(), + cluster_count, + if cluster_count == 1 { "" } else { "s" } + ) } }) .collect(); From 29fa83a4966524d878d35c03f8c92cb7113d7d83 Mon Sep 17 00:00:00 2001 From: Alex Holmberg Date: Fri, 20 Feb 2026 06:18:11 +0100 Subject: [PATCH 2/3] feat: cargo fmt --- src/server/bridge.rs | 10 ++++++---- src/server/mod.rs | 2 +- src/server/processor.rs | 11 +++++++---- src/server/routes.rs | 6 ++++-- 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/server/bridge.rs b/src/server/bridge.rs index 161976c4..345a60c5 100644 --- a/src/server/bridge.rs +++ b/src/server/bridge.rs @@ -364,10 +364,12 @@ impl EventBridge { .await .take() .unwrap_or_else(|| "unknown".to_string()); - self.emit(Event::StepFinished(syncable_ag_ui_core::StepFinishedEvent { - base: BaseEvent::with_current_timestamp(), - step_name, - })); + self.emit(Event::StepFinished( + syncable_ag_ui_core::StepFinishedEvent { + base: BaseEvent::with_current_timestamp(), + step_name, + }, + )); } // ========================================================================= diff --git a/src/server/mod.rs b/src/server/mod.rs index ef9fce3c..bbfefaed 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -45,11 +45,11 @@ pub mod routes; use std::net::SocketAddr; use std::sync::Arc; -use syncable_ag_ui_core::{Event, JsonValue, RunId, ThreadId}; use axum::{ Router, routing::{get, post}, }; +use syncable_ag_ui_core::{Event, JsonValue, RunId, ThreadId}; use tokio::sync::{RwLock, broadcast, mpsc}; use tower_http::cors::{Any, CorsLayer}; diff --git a/src/server/processor.rs b/src/server/processor.rs index a843b65c..d447a1d3 100644 --- a/src/server/processor.rs +++ b/src/server/processor.rs @@ -18,13 +18,13 @@ use std::collections::HashMap; use std::path::PathBuf; use std::time::Instant; -use syncable_ag_ui_core::{Role, RunId, ThreadId}; use rig::client::{CompletionClient, ProviderClient}; use rig::completion::Message as RigMessage; use rig::completion::Prompt; use rig::completion::message::{AssistantContent, UserContent}; use rig::one_or_many::OneOrMany; use rig::providers::{anthropic, openai}; +use syncable_ag_ui_core::{Role, RunId, ThreadId}; use tokio::sync::mpsc; use tracing::{debug, error, info, warn}; @@ -60,12 +60,12 @@ use crate::agent::tools::{ WriteFilesTool, }; -use syncable_ag_ui_core::ToolCallId; -use syncable_ag_ui_core::state::StateManager; use rig::agent::CancelSignal; use rig::completion::{CompletionModel, CompletionResponse, Message as RigPromptMessage}; use serde::{Deserialize, Serialize}; use std::sync::Arc; +use syncable_ag_ui_core::ToolCallId; +use syncable_ag_ui_core::state::StateManager; use tokio::sync::Mutex; /// Step status for generative UI progress display. @@ -635,7 +635,10 @@ impl AgentProcessor { /// Extracts the user message content from RunAgentInput messages. /// /// Returns the last user message content, or None if no user messages. - fn extract_user_input(&self, messages: &[syncable_ag_ui_core::types::Message]) -> Option { + fn extract_user_input( + &self, + messages: &[syncable_ag_ui_core::types::Message], + ) -> Option { // Find the last user message and extract its content messages .iter() diff --git a/src/server/routes.rs b/src/server/routes.rs index b5331226..bcb86905 100644 --- a/src/server/routes.rs +++ b/src/server/routes.rs @@ -367,7 +367,9 @@ fn convert_tools(raw_tools: Vec) -> Vec) -> Vec { +fn convert_context( + raw_context: Vec, +) -> Vec { raw_context .into_iter() .filter_map(|ctx| { @@ -477,9 +479,9 @@ async fn handle_websocket(socket: WebSocket, state: ServerState) { #[cfg(test)] mod tests { use super::*; + use axum::extract::State; use syncable_ag_ui_core::types::Message as AgUiProtocolMessage; use syncable_ag_ui_core::{RunId, ThreadId}; - use axum::extract::State; #[tokio::test] async fn test_health_endpoint() { From 8462699e24b9af298b079abc94f55e5da70d42af Mon Sep 17 00:00:00 2001 From: Alex Holmberg Date: Fri, 20 Feb 2026 06:54:34 +0100 Subject: [PATCH 3/3] feat: fixed clippy issues --- src/agent/session/ui.rs | 10 ++-------- src/agent/tools/platform/deploy_service.rs | 8 ++++---- .../tools/platform/list_hetzner_availability.rs | 6 +++--- src/analyzer/context/health_detector.rs | 1 - src/analyzer/display/matrix_view.rs | 2 +- src/analyzer/docker_analyzer.rs | 2 +- src/lib.rs | 4 ++-- src/server/processor.rs | 1 - src/wizard/cloud_provider_data.rs | 6 +++--- src/wizard/config_form.rs | 15 ++++++--------- src/wizard/recommendations.rs | 5 +---- src/wizard/service_endpoints.rs | 6 +++--- 12 files changed, 26 insertions(+), 40 deletions(-) diff --git a/src/agent/session/ui.rs b/src/agent/session/ui.rs index 206eeaee..209d6a75 100644 --- a/src/agent/session/ui.rs +++ b/src/agent/session/ui.rs @@ -206,8 +206,7 @@ pub fn print_banner(session: &ChatSession) { // Show platform context (selected project/organization) if session.platform_session.is_project_selected() { println!( - " {} {}: {}/{}", - "📦", + " 📦 {}: {}/{}", "Project".white(), session .platform_session @@ -223,12 +222,7 @@ pub fn print_banner(session: &ChatSession) { .cyan() ); } else { - println!( - " {} {} {}", - "📦", - "Project:".white(), - "(none selected)".dimmed() - ); + println!(" 📦 {} {}", "Project:".white(), "(none selected)".dimmed()); println!(" {} {}", "→".cyan(), "sync-ctl org list".dimmed()); } diff --git a/src/agent/tools/platform/deploy_service.rs b/src/agent/tools/platform/deploy_service.rs index 65a647d6..f917e032 100644 --- a/src/agent/tools/platform/deploy_service.rs +++ b/src/agent/tools/platform/deploy_service.rs @@ -379,10 +379,10 @@ User: "deploy this service" .find(|c| c.service_name.eq_ignore_ascii_case(&service_name)); // 5. Get environment info for display - let environments = match client.list_environments(&project_id).await { - Ok(envs) => envs, - Err(_) => Vec::new(), - }; + let environments: Vec = client + .list_environments(&project_id) + .await + .unwrap_or_default(); // Resolve environment name for display let (resolved_env_id, resolved_env_name, is_production) = diff --git a/src/agent/tools/platform/list_hetzner_availability.rs b/src/agent/tools/platform/list_hetzner_availability.rs index 2245d17a..16f84cb9 100644 --- a/src/agent/tools/platform/list_hetzner_availability.rs +++ b/src/agent/tools/platform/list_hetzner_availability.rs @@ -210,17 +210,17 @@ This provides current data directly from Hetzner API - never use hardcoded/stati // Group server types by category for easier reading let shared_cpu: Vec<&serde_json::Value> = server_types_json .iter() - .filter(|s| s["id"].as_str().map_or(false, |id| id.starts_with("cx"))) + .filter(|s| s["id"].as_str().is_some_and(|id| id.starts_with("cx"))) .collect(); let dedicated_cpu: Vec<&serde_json::Value> = server_types_json .iter() - .filter(|s| s["id"].as_str().map_or(false, |id| id.starts_with("ccx"))) + .filter(|s| s["id"].as_str().is_some_and(|id| id.starts_with("ccx"))) .collect(); let performance: Vec<&serde_json::Value> = server_types_json .iter() - .filter(|s| s["id"].as_str().map_or(false, |id| id.starts_with("cpx"))) + .filter(|s| s["id"].as_str().is_some_and(|id| id.starts_with("cpx"))) .collect(); let response = json!({ diff --git a/src/analyzer/context/health_detector.rs b/src/analyzer/context/health_detector.rs index d8a2e29a..1c995a6f 100644 --- a/src/analyzer/context/health_detector.rs +++ b/src/analyzer/context/health_detector.rs @@ -9,7 +9,6 @@ use crate::analyzer::{ DetectedTechnology, HealthEndpoint, HealthEndpointSource, TechnologyCategory, }; use crate::common::file_utils::{is_readable_file, read_file_safe}; -use crate::error::Result; use regex::Regex; use std::path::Path; diff --git a/src/analyzer/display/matrix_view.rs b/src/analyzer/display/matrix_view.rs index f82941a9..dbb4a8af 100644 --- a/src/analyzer/display/matrix_view.rs +++ b/src/analyzer/display/matrix_view.rs @@ -2,7 +2,7 @@ use crate::analyzer::display::{ BoxDrawer, format_list_smart, format_ports_smart, get_color_adapter, get_terminal_width, - helpers::{add_confidence_bar_to_drawer, format_project_category, get_main_technologies}, + helpers::{add_confidence_bar_to_drawer, format_project_category}, smart_truncate, visual_width, }; use crate::analyzer::{ArchitecturePattern, MonorepoAnalysis}; diff --git a/src/analyzer/docker_analyzer.rs b/src/analyzer/docker_analyzer.rs index 64484d37..5c4307cb 100644 --- a/src/analyzer/docker_analyzer.rs +++ b/src/analyzer/docker_analyzer.rs @@ -1359,7 +1359,7 @@ fn infer_default_port(base_image: &Option) -> Option { // Extract image name without registry/tag let image_name = image_lower .split('/') - .last() + .next_back() .unwrap_or(&image_lower) .split(':') .next() diff --git a/src/lib.rs b/src/lib.rs index a52ed76d..790ecf96 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -337,7 +337,7 @@ pub async fn run_command( ); } OutputFormat::Table => { - println!("\n{:<40} {:<30} {}", "ID", "NAME", "DESCRIPTION"); + println!("\n{:<40} {:<30} DESCRIPTION", "ID", "NAME"); println!("{}", "-".repeat(90)); for project in projects { let desc = if project.description.is_empty() { @@ -542,7 +542,7 @@ pub async fn run_command( ); } OutputFormat::Table => { - println!("\n{:<40} {:<30} {}", "ID", "NAME", "SLUG"); + println!("\n{:<40} {:<30} SLUG", "ID", "NAME"); println!("{}", "-".repeat(90)); for org in orgs { let slug = diff --git a/src/server/processor.rs b/src/server/processor.rs index d447a1d3..175f2b0e 100644 --- a/src/server/processor.rs +++ b/src/server/processor.rs @@ -65,7 +65,6 @@ use rig::completion::{CompletionModel, CompletionResponse, Message as RigPromptM use serde::{Deserialize, Serialize}; use std::sync::Arc; use syncable_ag_ui_core::ToolCallId; -use syncable_ag_ui_core::state::StateManager; use tokio::sync::Mutex; /// Step status for generative UI progress display. diff --git a/src/wizard/cloud_provider_data.rs b/src/wizard/cloud_provider_data.rs index 597e82f6..2b40d511 100644 --- a/src/wizard/cloud_provider_data.rs +++ b/src/wizard/cloud_provider_data.rs @@ -697,7 +697,7 @@ pub async fn get_recommended_server_type( }) .filter(|st| { // If preferred location is set, only include types available there - preferred_location.map_or(true, |loc| st.available_in.contains(&loc.to_string())) + preferred_location.is_none_or(|loc| st.available_in.contains(&loc.to_string())) }) .min_by(|a, b| a.price_monthly.partial_cmp(&b.price_monthly).unwrap()) } @@ -719,8 +719,8 @@ pub async fn find_best_region( // Sort by availability count, preferring specified zone let mut sorted_regions = regions; sorted_regions.sort_by(|a, b| { - let a_zone_match = preferred_zone.map_or(false, |z| a.network_zone == z); - let b_zone_match = preferred_zone.map_or(false, |z| b.network_zone == z); + let a_zone_match = preferred_zone.is_some_and(|z| a.network_zone == z); + let b_zone_match = preferred_zone.is_some_and(|z| b.network_zone == z); match (a_zone_match, b_zone_match) { (true, false) => std::cmp::Ordering::Less, diff --git a/src/wizard/config_form.rs b/src/wizard/config_form.rs index a707d9b5..1caec945 100644 --- a/src/wizard/config_form.rs +++ b/src/wizard/config_form.rs @@ -441,10 +441,10 @@ fn collect_env_vars_manually() -> Vec { is_secret, }); - let add_another = match Confirm::new("Add another?").with_default(false).prompt() { - Ok(v) => v, - Err(_) => false, - }; + let add_another = Confirm::new("Add another?") + .with_default(false) + .prompt() + .unwrap_or_default(); if !add_another { break; @@ -567,13 +567,10 @@ fn collect_env_vars_from_file( ); } - let confirm = match Confirm::new("Use these variables?") + let confirm = Confirm::new("Use these variables?") .with_default(true) .prompt() - { - Ok(v) => v, - Err(_) => false, - }; + .unwrap_or_default(); if confirm { secrets } else { Vec::new() } } diff --git a/src/wizard/recommendations.rs b/src/wizard/recommendations.rs index 892bcc61..84f69119 100644 --- a/src/wizard/recommendations.rs +++ b/src/wizard/recommendations.rs @@ -170,10 +170,7 @@ fn select_provider(input: &RecommendationInput) -> (CloudProvider, String) { .map(|p| p.display_name()) .collect(); let also_available = if connected.len() > 1 { - format!( - ". Also connected: {}", - connected.iter().copied().collect::>().join(", ") - ) + format!(". Also connected: {}", connected.to_vec().join(", ")) } else { String::new() }; diff --git a/src/wizard/service_endpoints.rs b/src/wizard/service_endpoints.rs index 9cd1eda8..8838d404 100644 --- a/src/wizard/service_endpoints.rs +++ b/src/wizard/service_endpoints.rs @@ -192,7 +192,7 @@ pub fn extract_service_hint(env_var_name: &str) -> Option { // Try suffixes longest-first so _SERVICE_URL is tried before _URL let mut suffixes: Vec<&&str> = URL_SUFFIXES.iter().collect(); - suffixes.sort_by(|a, b| b.len().cmp(&a.len())); + suffixes.sort_by_key(|b| std::cmp::Reverse(b.len())); for suffix in suffixes { if upper.ends_with(suffix) { @@ -214,7 +214,7 @@ fn normalize(s: &str) -> String { /// Split a name into tokens on `_` and `-`. fn tokenize(s: &str) -> Vec { s.to_lowercase() - .split(|c: char| c == '_' || c == '-') + .split(['_', '-']) .filter(|t| !t.is_empty()) .map(String::from) .collect() @@ -293,7 +293,7 @@ pub fn match_env_vars_to_services( let mut best: Option<(MatchConfidence, &AvailableServiceEndpoint)> = None; for ep in endpoints { if let Some(conf) = match_hint_to_service(&hint, &ep.service_name) { - if best.as_ref().map_or(true, |(bc, _)| conf > *bc) { + if best.as_ref().is_none_or(|(bc, _)| conf > *bc) { best = Some((conf, ep)); } }